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 a terrain full-rebuild bug when DrawEntireTerrain is enabled by removing all DrawEntireTerrain/stretchTerrain knowledge from WorldHeightMap and centralising draw-size decisions in W3DView. It also removes legacy TERRAIN_LOD_STRETCH and TERRAIN_LOD_HALF_* LOD levels along with their dead tree-rendering code paths.

  • Architecture shift: W3DView::getDesiredTerrainDrawSize() now supplies the draw dimensions every frame, with DrawEntireTerrain setting the full map extent and low-pitch / scripted-camera paths handled uniformly. HeightMapRenderObjClass::setTerrainDrawSize separates "desired" from "oversize" widths via std::max, so scripted-camera oversize and DrawEntireTerrain compose correctly.
  • Correctness fixes: m_updating = true is now set before the full-terrain early-return branch so that re-entrant updateCenter calls are guarded on that path; m_needFullUpdate is honoured there too.
  • Legacy cleanup: TERRAIN_LOD_STRETCH_NO_CLOUDS, TERRAIN_LOD_HALF_CLOUDS, TERRAIN_LOD_STRETCH_CLOUDS, and the commented-out half-height tree rendering block are all removed.

Confidence Score: 5/5

Safe to merge; the architectural refactor is coherent and the targeted bug is correctly fixed.

The draw-size logic is well-separated, the oversize/desired interaction composes correctly for both DrawEntireTerrain and scripted-camera paths, and the re-entrancy guard in updateCenter is now correctly placed before the full-terrain early-return branch. The one remaining gap — not resetting m_desiredDrawWidth/Height in reset() — only matters in a narrow ordering edge case and does not affect the primary bug being fixed here.

HeightMap.cpp — the reset() method leaves m_desiredDrawWidth/Height at their last-known values across map loads.

Important Files Changed

Filename Overview
Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp Core terrain draw-size logic refactored: introduces m_desiredDrawWidth/Height, fixes m_updating placement, and correctly handles oversizeTerrain with DrawEntireTerrain; m_desiredDrawWidth/Height are not reset in reset(), leaving stale values across map loads.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp New getDesiredTerrainDrawSize() cleanly centralises draw-size computation; DrawEntireTerrain, scripted-camera, and low-pitch paths all handled; updateTerrain simplified.
Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp Removed DrawEntireTerrain and stretchTerrain overrides from constructor and createDrawArea; createDrawArea simplified correctly.
Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp Replaced magic number 5 with TERRAIN_LOD_MAX constant; removed now-invalid LOD==5 null-object override.
Core/GameEngine/Include/GameClient/TerrainVisual.h Removed TERRAIN_LOD_STRETCH_NO_CLOUDS, TERRAIN_LOD_HALF_CLOUDS, TERRAIN_LOD_STRETCH_CLOUDS enum values and their name-table entries.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTreeBuffer.cpp Removed large block of commented-out half-height/stretch tree rendering code.
Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Updated LOD step-down chain: removed TERRAIN_LOD_HALF_CLOUDS intermediate step since that LOD is now gone.
GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp Same LOD step-down chain fix as the Generals counterpart.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    UT["W3DView::updateTerrain()"] --> GDS["getDesiredTerrainDrawSize()"]
    GDS -->|DrawEntireTerrain| FM["dimensions = heightMap->getXExtent/YExtent()"]
    GDS -->|scripted camera OR high pitch| NW["dimensions = NORMAL_DRAW_WIDTH/HEIGHT"]
    GDS -->|user camera, low pitch| LW["dimensions = LOW_ANGLE_DRAW_WIDTH/HEIGHT"]
    FM --> SDS["setTerrainDrawSize(w, h)"]
    NW --> SDS
    LW --> SDS
    SDS --> UPD["update m_desiredDrawWidth/Height if w>0 && h>0"]
    UPD --> MAX["effective = max(m_oversizeDrawWidth, m_desiredDrawWidth)"]
    MAX --> CLM["clamp to map->getXExtent/YExtent()"]
    CLM --> MAP["WorldHeightMap::setDrawWidth/Height()"]

    OT["oversizeTerrain(N)"] -->|N>0| OSW["m_oversizeDrawWidth = NORMAL + N*TILE_LEN\nsetTerrainDrawSize(0,0)"]
    OT -->|N==0| RST["m_oversizeDrawWidth = 0\nsetTerrainDrawSize(m_desiredDrawWidth, m_desiredDrawHeight)"]
    OSW --> MAX
    RST --> MAX

    UC["updateCenter()"] -->|m_x >= XExtent AND m_y >= YExtent| FU["full terrain: updateBlock if m_needFullUpdate\nm_updating=false, return"]
    UC -->|otherwise| SCROLL["scroll/partial update logic"]
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"}}}%%
flowchart TD
    UT["W3DView::updateTerrain()"] --> GDS["getDesiredTerrainDrawSize()"]
    GDS -->|DrawEntireTerrain| FM["dimensions = heightMap->getXExtent/YExtent()"]
    GDS -->|scripted camera OR high pitch| NW["dimensions = NORMAL_DRAW_WIDTH/HEIGHT"]
    GDS -->|user camera, low pitch| LW["dimensions = LOW_ANGLE_DRAW_WIDTH/HEIGHT"]
    FM --> SDS["setTerrainDrawSize(w, h)"]
    NW --> SDS
    LW --> SDS
    SDS --> UPD["update m_desiredDrawWidth/Height if w>0 && h>0"]
    UPD --> MAX["effective = max(m_oversizeDrawWidth, m_desiredDrawWidth)"]
    MAX --> CLM["clamp to map->getXExtent/YExtent()"]
    CLM --> MAP["WorldHeightMap::setDrawWidth/Height()"]

    OT["oversizeTerrain(N)"] -->|N>0| OSW["m_oversizeDrawWidth = NORMAL + N*TILE_LEN\nsetTerrainDrawSize(0,0)"]
    OT -->|N==0| RST["m_oversizeDrawWidth = 0\nsetTerrainDrawSize(m_desiredDrawWidth, m_desiredDrawHeight)"]
    OSW --> MAX
    RST --> MAX

    UC["updateCenter()"] -->|m_x >= XExtent AND m_y >= YExtent| FU["full terrain: updateBlock if m_needFullUpdate\nm_updating=false, return"]
    UC -->|otherwise| SCROLL["scroll/partial update logic"]
Loading

Reviews (6): 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's no big deal to keep them for the time being, I think.


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 !

@Caball009 Caball009 linked an issue Jul 1, 2026 that may be closed by this pull request
4 tasks
@Caball009

Copy link
Copy Markdown

There's an issue with building placement that I noticed with this branch & DrawEntireTerrain enabled. The placement is not smooth but severely lagging sometimes, possibly related to the world terrain:

gen_zh_building_placement.mp4

@xezon

xezon commented Jul 9, 2026

Copy link
Copy Markdown
Author

Very odd. Looks to be related to ray cast. Mouse Position says world: none. Mouse cursor apparently does not hit terrain.

shot_20260709_224729_1

@stephanmeesters

stephanmeesters commented Jul 10, 2026

Copy link
Copy Markdown

I ran the new chat "5.6-sol" model over this and it had this to say:

[P2] Clamp terrain draw dimensions before the initial buffer build

Location: Core/GameEngineDevice/Source/W3DDevice/GameClient/WorldHeightMap.cpp:529

After parsing, the constructor can leave the initial draw area at 129x129 even when the stored heightmap is smaller.

Before W3DView can correct the dimensions, W3DTerrainVisual::load() passes them to initHeightData(). This immediately calls updateBlock() at Core/GameEngineDevice/Source/ W3DDevice/GameClient/HeightMap.cpp:1338.

For small maps, the initial buffer build requests coordinates outside the heightmap. Because getDisplayHeight() performs an unchecked array access, this can result in out-of-
bounds reads.

The first view update clamps the dimensions and rebuilds the terrain before rendering, so affected maps may load and display without visible problems. No shipped map has a dimension
below 129, but WorldBuilder permits sufficiently small custom maps.

Preserve the unconditional min(drawSize, extent) clamps after parsing while removing only the DrawEntireTerrain override.

and indeed it does appear to read the map height array out of bounds for very small maps after the changes made in this PR, i tested with a 30x30 map, but also maps like "[NoMoney] 1v1 Survival" would be affected, still pretty niche though,

Could be verified using this at WorldHeightMap.h:260

UnsignedByte getDisplayHeight(Int x, Int y)
  {
  	const Int mapX = x + m_drawOriginX;
  	const Int mapY = y + m_drawOriginY;
  	const Int ndx = mapX + m_width * mapY;

  	DEBUG_ASSERTCRASH(
  		//mapX >= 0 && mapX < m_width &&
  		//mapY >= 0 && mapY < m_height &&
  		ndx >= 0 && ndx < m_dataSize,
  		("Height sample (%d, %d) is outside %dx%d heightmap; index %d, size %d",
  			mapX, mapY, m_width, m_height, ndx, m_dataSize));

  	return m_data[ndx];
  }

@xezon
xezon force-pushed the xezon/fix-drawentireterrain branch from 5ce335a to ac2971e Compare July 10, 2026 20:13
@xezon

xezon commented Jul 10, 2026

Copy link
Copy Markdown
Author

There's an issue with building placement that I noticed with this branch & DrawEntireTerrain enabled. The placement is not smooth but severely lagging sometimes, possibly related to the world terrain:

The branch was outdated. Is fixed after Rebase.

@Caball009

Copy link
Copy Markdown

There's an issue with building placement that I noticed with this branch & DrawEntireTerrain enabled. The placement is not smooth but severely lagging sometimes, possibly related to the world terrain:

The branch was outdated. Is fixed after Rebase.

Good, I cannot reproduce the issue anymore.

@xezon

xezon commented Jul 16, 2026

Copy link
Copy Markdown
Author

Good, I cannot reproduce the issue anymore.

Great. Need review.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's no big deal to keep them for the time being, I think.

Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp Outdated
Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp Outdated
Comment thread Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp
@Caball009

Copy link
Copy Markdown

@stephanmeesters

also maps like "[NoMoney] 1v1 Survival" would be affected

I cannot reproduce this.

@xezon
xezon force-pushed the xezon/fix-drawentireterrain branch from ac2971e to a9c93b2 Compare July 17, 2026 17:44
Caball009
Caball009 previously approved these changes Jul 17, 2026

@Caball009 Caball009 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good to me.

@xezon
xezon force-pushed the xezon/fix-drawentireterrain branch from a9c93b2 to 9316bf2 Compare July 17, 2026 18:27
@xezon

xezon commented Jul 17, 2026

Copy link
Copy Markdown
Author

Replicated in Generals without conflicts.

First commit verified to compile on its own.

@stephanmeesters

Copy link
Copy Markdown

Exception trigger on 32x32 map (note its bigger because of borders):
image

Exception trigger on 1v1 nomoney survival:
image

smallmap.zip
( 1v1 ) nomoney survival!.zip

@Caball009
Caball009 dismissed their stale review July 17, 2026 19:59

Out-of-bounds bug.

@xezon

xezon commented Jul 17, 2026

Copy link
Copy Markdown
Author

Exception trigger on 32x32 map

Is this bug retail or new in this change?

@stephanmeesters

Copy link
Copy Markdown

Exception trigger on 32x32 map

Is this bug retail or new in this change?

New, it doesn’t happen on main for me

@xezon

xezon commented Jul 17, 2026

Copy link
Copy Markdown
Author

Callstack?

I cannot reproduce crash.

@xezon
xezon force-pushed the xezon/fix-drawentireterrain branch from 9316bf2 to 2e36078 Compare July 17, 2026 20:21
@xezon

xezon commented Jul 17, 2026

Copy link
Copy Markdown
Author

Tentative fix pushed. Please recheck.

@stephanmeesters

Copy link
Copy Markdown

OK appears fixed.

Here was the callstack:

HeightMapRenderObjClass::updateVB(DX8VertexBufferClass *, VertexFormatXYZDUV2 *, int, int, int, int, int, int, WorldHeightMap *, RefMultiListIterator<…> *) HeightMap.cpp:373
HeightMapRenderObjClass::updateBlock(int, int, int, int, WorldHeightMap *, RefMultiListIterator<…> *) HeightMap.cpp:1012
HeightMapRenderObjClass::initHeightData(int, int, WorldHeightMap *, RefMultiListIterator<…> *, bool) HeightMap.cpp:1338
W3DTerrainVisual::load(AsciiString) W3DTerrainVisual.cpp:614
TerrainLogic::loadMap(AsciiString, bool) TerrainLogic.cpp:1311
W3DTerrainLogic::loadMap(AsciiString, bool) W3DTerrainLogic.cpp:164
GameLogic::tryStartNewGame(bool) GameLogic.cpp:1357
GameLogic::startNewGame(bool) GameLogic.cpp:1138
GameLogic::update() GameLogic.cpp:3714
SubsystemInterface::UPDATE() SubsystemInterface.h:131
GameEngine::update() GameEngine.cpp:921
Win32GameEngine::update() Win32GameEngine.cpp:91
GameEngine::execute() GameEngine.cpp:983
GameMain() GameMain.cpp:55
WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int) WinMain.cpp:929

@xezon

xezon commented Jul 17, 2026

Copy link
Copy Markdown
Author

Ok great

@xezon

xezon commented Jul 18, 2026

Copy link
Copy Markdown
Author

Needs new approval.

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.

Severe in-game performance degradation in certain mods Massive performance degradation with DrawEntireTerrain=Yes

4 participants