Skip to content

bugfix(heightmap): Fix full terrain rebuild on every frame when DrawEntireTerrain is enabled#2846

Open
xezon wants to merge 2 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-drawentireterrain
Open

bugfix(heightmap): Fix full terrain rebuild on every frame when DrawEntireTerrain is enabled#2846
xezon wants to merge 2 commits into
TheSuperHackers:mainfrom
xezon:xezon/fix-drawentireterrain

Conversation

@xezon

@xezon xezon commented Jun 30, 2026

Copy link
Copy Markdown

Merge with Rebase

This change fixes the terrain update for DrawEntireTerrain.

It has 2 commits:

The first commit removes the legacy and unused code for stretched terrain and half height map.

The second commit fixes the terrain update for DrawEntireTerrain. W3DView tells the HeightMapRenderObjClass which size to use. The terrain oversize logic for the scripted camera was adapted to indirectly account for DrawEntireTerrain as well. All DrawEntireTerrain specific knowledge was removed from height map.

TODO

  • Add pull id to commit titles
  • Replicate in Generals

@xezon xezon added Critical Severity: Minor < Major < Critical < Blocker Performance Is a performance concern Gen Relates to Generals ZH Relates to Zero Hour ThisProject The issue was introduced by this project, or this task is specific to this project Mod Relates to Mods or modding labels Jun 30, 2026
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the bug where enabling DrawEntireTerrain caused a full terrain rebuild every frame. It removes the legacy stretched/half-height-map terrain LOD modes, and refactors draw-size ownership so W3DView drives setTerrainDrawSize each frame rather than having WorldHeightMap bake the full extent into its draw dimensions at load time.

  • Removes TERRAIN_LOD_STRETCH_NO_CLOUDS, TERRAIN_LOD_HALF_CLOUDS, and TERRAIN_LOD_STRETCH_CLOUDS enum values and all associated code paths, marking m_stretchTerrain/m_useHalfHeightMap as legacy-unused in GlobalData.
  • Adds m_desiredDrawWidth/Height to HeightMapRenderObjClass and a new getDesiredTerrainDrawSize() method to W3DView, so DrawEntireTerrain, user-controlled pitch, and scripted-camera sizes are all resolved in one place and applied consistently every frame before updateCenter.
  • Fixes the DrawEntireTerrain path in updateCenter so that when the draw area covers the full map, m_updating is set before the early-return and a pending m_needFullUpdate is still honoured.

Confidence Score: 5/5

The changes are safe to merge; the core bugfix is correct and the legacy code removal is consistent across all affected files.

The refactor correctly moves draw-size ownership to W3DView, which now calls setTerrainDrawSize every frame before updateCenter, eliminating the root cause of the unnecessary full-terrain rebuilds. The enum pruning is consistent across all call sites. The one remaining gap — m_desiredDrawWidth/Height not being reset in reset() — is a narrow edge case that only matters if oversizeTerrain fires between a reset and the first render frame, and its effect is bounded by the map-extent clamp in setTerrainDrawSize.

HeightMap.cpp — the reset() function does not restore m_desiredDrawWidth/Height to defaults, which is a minor follow-up worth addressing.

Important Files Changed

Filename Overview
Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp Core logic change: adds m_desiredDrawWidth/Height, reworks setTerrainDrawSize and oversizeTerrain, moves m_updating = true before the full-map early-return. reset() still does not restore m_desiredDrawWidth/Height to defaults.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp Adds getDesiredTerrainDrawSize() to unify DrawEntireTerrain, scripted-camera, and pitch-based sizing; updateTerrain always calls setTerrainDrawSize before updateCenter. Logic is clean and correctly handles the null-map guard.
Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp Removes DrawEntireTerrain/stretchTerrain overrides from the constructor and createDrawArea, simplifying draw-area creation to a pure clamp of m_drawWidthX/Y to the map extent.
Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp Replaces magic number 7 with TERRAIN_LOD_MAX and removes the dead == 5 (TERRAIN_LOD_STRETCH_CLOUDS) branch; correct after the enum pruning.
Core/GameEngine/Include/GameClient/TerrainVisual.h Removes TERRAIN_LOD_STRETCH_NO_CLOUDS, TERRAIN_LOD_HALF_CLOUDS, TERRAIN_LOD_STRETCH_CLOUDS from enum and name table; consistent with all other removal sites.
GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Updates calculateTerrainLOD fallback chain to skip the removed TERRAIN_LOD_HALF_CLOUDS step; TERRAIN_LOD_NO_WATER now falls directly to TERRAIN_LOD_DISABLE.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Removes a large commented-out block of legacy panel-tree rendering code that depended on m_useHalfHeightMap/m_stretchTerrain; no logic change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant W3DView
    participant HeightMapRenderObj
    participant WorldHeightMap

    Note over W3DView: Each render frame
    W3DView->>W3DView: getDesiredTerrainDrawSize()
    alt DrawEntireTerrain on
        W3DView->>WorldHeightMap: getXExtent() / getYExtent()
        WorldHeightMap-->>W3DView: full map dimensions
    else user-controlled + low pitch
        W3DView-->>W3DView: LOW_ANGLE_DRAW_WIDTH/HEIGHT
    else scripted camera or normal pitch
        W3DView-->>W3DView: NORMAL_DRAW_WIDTH/HEIGHT
    end
    W3DView->>HeightMapRenderObj: setTerrainDrawSize(w, h)
    Note over HeightMapRenderObj: stores m_desiredDrawWidth/Height<br/>applies max with m_oversizeDrawWidth<br/>clamps to map extent
    W3DView->>HeightMapRenderObj: updateCenter(camera, pivot, lights)
    alt "draw area == full map extent"
        HeightMapRenderObj->>HeightMapRenderObj: updateBlock() if m_needFullUpdate
    else partial view
        HeightMapRenderObj->>WorldHeightMap: createDrawArea(newOrgX, newOrgY)
        HeightMapRenderObj->>HeightMapRenderObj: updateBlock() for deltas
    end

    Note over HeightMapRenderObj,WorldHeightMap: oversizeTerrain (scripted camera)
    HeightMapRenderObj->>HeightMapRenderObj: oversizeTerrain(N)
    HeightMapRenderObj->>HeightMapRenderObj: setTerrainDrawSize(0,0) triggers max with oversize
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant W3DView
    participant HeightMapRenderObj
    participant WorldHeightMap

    Note over W3DView: Each render frame
    W3DView->>W3DView: getDesiredTerrainDrawSize()
    alt DrawEntireTerrain on
        W3DView->>WorldHeightMap: getXExtent() / getYExtent()
        WorldHeightMap-->>W3DView: full map dimensions
    else user-controlled + low pitch
        W3DView-->>W3DView: LOW_ANGLE_DRAW_WIDTH/HEIGHT
    else scripted camera or normal pitch
        W3DView-->>W3DView: NORMAL_DRAW_WIDTH/HEIGHT
    end
    W3DView->>HeightMapRenderObj: setTerrainDrawSize(w, h)
    Note over HeightMapRenderObj: stores m_desiredDrawWidth/Height<br/>applies max with m_oversizeDrawWidth<br/>clamps to map extent
    W3DView->>HeightMapRenderObj: updateCenter(camera, pivot, lights)
    alt "draw area == full map extent"
        HeightMapRenderObj->>HeightMapRenderObj: updateBlock() if m_needFullUpdate
    else partial view
        HeightMapRenderObj->>WorldHeightMap: createDrawArea(newOrgX, newOrgY)
        HeightMapRenderObj->>HeightMapRenderObj: updateBlock() for deltas
    end

    Note over HeightMapRenderObj,WorldHeightMap: oversizeTerrain (scripted camera)
    HeightMapRenderObj->>HeightMapRenderObj: oversizeTerrain(N)
    HeightMapRenderObj->>HeightMapRenderObj: setTerrainDrawSize(0,0) triggers max with oversize
Loading

Reviews (2): Last reviewed commit: "bugfix(heightmap): Prevent full terrain ..." | Re-trigger Greptile

Bool m_stretchTerrain;
Bool m_useHalfHeightMap;
Bool m_stretchTerrain; // TheSuperHackers @info Legacy, unused
Bool m_useHalfHeightMap; // TheSuperHackers @info Legacy, unused

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if it should be removed or left. The settings still exist in the game INI files, and this allows to see that it does nothing if someone searched for it.


BaseHeightMapRenderObjClass *newROBJ = nullptr;
if (TheGlobalData->m_terrainLOD==7) {
if (TheGlobalData->m_terrainLOD == TERRAIN_LOD_MAX) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 previously referred to TERRAIN_LOD_MAX

newROBJ = NEW_REF( FlatHeightMapRenderObjClass, () );
}
}
if (TheGlobalData->m_terrainLOD == 5)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 previously referred to enum value TERRAIN_LOD_STRETCH_CLOUDS, which was removed.

It set newROBJ to null which meant it would have leaked here. Very strange code.

@xezon xezon force-pushed the xezon/fix-drawentireterrain branch from 2b94631 to 5ce335a Compare June 30, 2026 17:13
@sailro

sailro commented Jun 30, 2026

Copy link
Copy Markdown

Thanks for taking this on, @xezon - I reviewed the branch locally and I'm happy to see it land in place of #2786. The approach here is cleaner than mine: making W3DView::getDesiredTerrainDrawSize() the single source of truth and stripping all DrawEntireTerrain knowledge out of HeightMap removes the "two sources of truth" that caused the per-frame rebuild in the first place, rather than just reconciling them like I did.

A few things I checked while reviewing:

•  updateCenter rework - the earlier m_updating = true is safe; every early  return  after it resets the flag (or falls through), so no risk of the update flag getting stuck.
_TerrainLOD  renumbering - no compatibility concerns: the only hardcoded integer comparisons are replaced with TERRAIN_LOD_MAX , LOD is parsed from INI by name via TerrainLODNames, and m_terrainLOD isn't serialized into saves/replays.
• The oversize/desired size math checks out - clamping the final max(oversize, desired) to the map extent is equivalent to the old per-source clamp.

I also confirmed it carries over the updateCenter m_needFullUpdate flush from #2786, which was the second half of that fix (the shell-map / scripted-camera regression), so nothing is lost in the supersede.


the branch doesn't compile for the base Generals target (Zero Hour /  GeneralsMD  builds fine).

The  chore  commit ( ffe9ce6 ) removes  TERRAIN_LOD_HALF_CLOUDS  from the shared enum in  Core/GameEngine/Include/GameClient/TerrainVisual.h , and you correctly updated  calculateTerrainLOD()  in GeneralsMD/.../W3DDevice/GameClient/W3DDisplay.cpp  - but the mirrored  Generals/ copy was missed. It still references the now-deleted enumerator:

Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp:1646
    case TERRAIN_LOD_HALF_CLOUDS: curLOD = TERRAIN_LOD_DISABLE; break;
    case TERRAIN_LOD_NO_WATER:    curLOD = TERRAIN_LOD_HALF_CLOUDS; break;

So the base-game compile fails with C2065: 'TERRAIN_LOD_HALF_CLOUDS': undeclared identifier  (×2).

The fix is just to mirror the GeneralsMD change — collapse those two lines to:

    case TERRAIN_LOD_NO_WATER: curLOD = TERRAIN_LOD_DISABLE; break;

That's the only dangling reference I found - grepping the tree, the removed  TERRAIN_LOD_HALF_CLOUDS  /  TERRAIN_LOD_STRETCH_*  /  STRETCH_DRAW_*  symbols are clean everywhere else; this is the one spot where the Generals/GeneralsMD mirror got out of sync. Everything else in the PR looks good.


LGTM from my side. Thanks for the cleanup commit too — good to see the dead stretch-terrain / half-height-map paths finally gone !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Critical Severity: Minor < Major < Critical < Blocker Gen Relates to Generals Mod Relates to Mods or modding Performance Is a performance concern ThisProject The issue was introduced by this project, or this task is specific to this project ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Massive performance degradation with DrawEntireTerrain=Yes

2 participants