Skip to content

Commit c80024b

Browse files
)2onin 4 ever!!)2onin 4 ever!!
authored andcommitted
#Bugfix(terrain): stop black terrain cells on maps with more than 32 terrain textures.
The per-material splat bake gave every terrain material its own weight channel across the whole map, and the terrain pixel shader reads at most 32 (8 weight textures x 4 channels, with every ps_3_0 sampler already in use). Materials past the 32nd got no channel, their cells summed zero weight and drew pure black. USA mission 1 uses 37. Slot selection was also keyed on a texture's position in the map's list rather than on the materials in use. Weight channels are now reused per draw tile: - The bake scans every 32-cell draw-tile window plus a 4-cell apron, records which materials meet, and colours that conflict graph: materials that ever share a window never share a channel. USA mission 1: 37 materials in 13 channels. - Each cell stores the materials within the apron. Each draw tile ORs its cells and uploads its own channel-to-material region table and enable mask before drawing; the result is cached until the draw window slides or the map rebakes. - On additive atlas-page passes, a tile with nothing on that page is skipped. - The bake serial the tile cache keys on is global, so a new map allocated at the old map's address cannot reuse its tables. - The pixel shader is unchanged; its material loop now runs only over the channels in use. Added a [TERRAIN] readout (materials, channels, dropped cells, pages, most materials per tile), off by default, and extends the bake log. Design, limits and the constants the apron is tied to the dedicated terrain splat map doc file, Appendix E.
1 parent 0af69ee commit c80024b

4 files changed

Lines changed: 308 additions & 93 deletions

File tree

‎Core/GameEngineDevice/Include/W3DDevice/GameClient/WorldHeightMap.h‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,22 @@ class WorldHeightMap : public RefCountClass,
379379
return (activeIdx >= 0 && activeIdx < m_numActiveMaterials)
380380
? m_activeMaterialIndices[activeIdx] : -1;
381381
}
382+
// Ronin @diagnostic 14/09/2026 DX9: §19e.3. For the [TERRAIN] readout.
383+
Int getSplatWeightableClasses() const { return m_splatWeightableClasses; }
384+
Int getSplatDroppedCells() const { return m_splatDroppedCells; }
385+
Int getSplatMaxPerTile() const { return m_splatMaxPerTile; }
386+
Int getSplatChannelsNeeded() const { return m_splatChannelsNeeded; }
387+
// Ronin @bugfix 14/09/2026 DX9: §19e.3. Channel reuse — materials within the bake apron of a DRAW-LOCAL cell (same coordinates as
388+
// getTerrainTexturePageForCell), as compact bits; OR these over a draw tile and hand the result to buildSplatRegionTable.
389+
UnsignedInt64 getSplatCellMaskForCell(Int xIndex, Int yIndex) const {
390+
const Int x = xIndex + m_drawOriginX;
391+
const Int y = yIndex + m_drawOriginY;
392+
return (x >= 0 && y >= 0 && x < m_width && y < m_height && (Int)m_splatCellMask.size() == m_width * m_height)
393+
? m_splatCellMask[y * m_width + x] : 0;
394+
}
395+
Int getSplatBakeSerial() const { return m_splatBakeSerial; }
396+
Int buildSplatRegionTable(UnsignedInt64 presentMask, Int terrainPage,
397+
float* outRegionA, float* outRegionB, float* outSlotEnableMask) const;
382398

383399
// @feature Ronin 29/04/2026 Splat S20-A2d2: public read-only accessors for the
384400
// weight-atlas pages allocated by ensurePerMaterialWeightAtlasTextures().
@@ -396,6 +412,18 @@ class WorldHeightMap : public RefCountClass,
396412
// Bake state. Cleared by freeMapResources(); allocated by buildPerMaterialWeightTextures().
397413
Int m_numActiveMaterials = 0;
398414
Int m_activeMaterialIndices[SPLAT_MAX_ACTIVE_MATERIALS] = {};
415+
// Ronin @diagnostic 14/09/2026 DX9: §19e.3. Bake readout: classes the bake can weight, cells left with no channel.
416+
Int m_splatWeightableClasses = 0;
417+
Int m_splatDroppedCells = 0;
418+
Int m_splatMaxPerTile = 0;
419+
Int m_splatChannelsNeeded = 0;
420+
// Ronin @bugfix 14/09/2026 DX9: §19e.3. Channel reuse: compact material -> class and -> weight channel, per-cell material masks.
421+
Int m_splatNumCompact = 0;
422+
Int m_splatCompactClass[64] = {};
423+
Int m_splatCompactChannel[64] = {};
424+
std::vector<UnsignedInt64> m_splatCellMask;
425+
Int m_splatBakeSerial = 0;
426+
399427
UnsignedByte* m_perMaterialWeightBytes = nullptr; // [activeIdx][texelY*pitch + texelX], one byte per texel
400428
Int m_perMaterialWeightWidth = 0;
401429
Int m_perMaterialWeightHeight = 0;

‎Core/GameEngineDevice/Source/W3DDevice/GameClient/HeightMap.cpp‎

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,51 +3015,50 @@ void HeightMapRenderObjClass::renderPrimaryBlendControlPass()
30153015

30163016
Bool drewAnyContribution = FALSE;
30173017

3018+
// Ronin @bugfix 14/09/2026 DX9: §19e.3. CHANNEL REUSE — per draw tile, the materials around its cells. Cached until the draw
3019+
// window slides or the bake reruns; same draw-local cell mapping as the paged-terrain cache above.
3020+
static std::vector<UnsignedInt64> s_tileMaterialMask;
3021+
static const WorldHeightMap* s_tileMaskMap = nullptr;
3022+
static Int s_tileMaskKey[6] = { -1, -1, -1, -1, -1, -1 };
3023+
{
3024+
const Int keyNow[6] = { m_originX, m_originY, m_map->getDrawOrgX(), m_map->getDrawOrgY(),
3025+
m_map->getSplatBakeSerial(), m_numVBTilesX * 4096 + m_numVBTilesY };
3026+
Bool stale = (s_tileMaskMap != m_map) || ((Int)s_tileMaterialMask.size() != m_numVBTilesX * m_numVBTilesY);
3027+
for (Int k = 0; k < 6; ++k) {
3028+
if (keyNow[k] != s_tileMaskKey[k]) stale = TRUE;
3029+
}
3030+
if (stale) {
3031+
s_tileMaskMap = m_map;
3032+
for (Int k = 0; k < 6; ++k) s_tileMaskKey[k] = keyNow[k];
3033+
s_tileMaterialMask.assign(m_numVBTilesX * m_numVBTilesY, 0);
3034+
for (Int tj = 0; tj < m_numVBTilesY; ++tj) {
3035+
for (Int ti = 0; ti < m_numVBTilesX; ++ti) {
3036+
Int ccX = VERTEX_BUFFER_TILE_LENGTH, ccY = VERTEX_BUFFER_TILE_LENGTH;
3037+
if (ti == m_numVBTilesX - 1 && m_numBlockColumnsInLastVB > 0) ccX = m_numBlockColumnsInLastVB;
3038+
if (tj == m_numVBTilesY - 1 && m_numBlockRowsInLastVB > 0) ccY = m_numBlockRowsInLastVB;
3039+
UnsignedInt64 mask = 0;
3040+
for (Int ly = 0; ly < ccY; ++ly) {
3041+
const Int mY = getYWithOrigin(tj * VERTEX_BUFFER_TILE_LENGTH + ly);
3042+
for (Int lx = 0; lx < ccX; ++lx)
3043+
mask |= m_map->getSplatCellMaskForCell(getXWithOrigin(ti * VERTEX_BUFFER_TILE_LENGTH + lx), mY);
3044+
}
3045+
s_tileMaterialMask[tj * m_numVBTilesX + ti] = mask;
3046+
}
3047+
}
3048+
}
3049+
}
3050+
const Int pmNumSlots = m_map->getActiveMaterialCount(); // weight channels in use; the shader loops this many slots
3051+
30183052
for (Int terrainPage = 0; terrainPage < terrainPageCount; ++terrainPage) {
30193053
TextureClass* pTerrainAtlas =
30203054
useMultiPageAccumulation ? m_map->getTerrainTexture(terrainPage) : m_stageZeroTexture;
30213055
if (pTerrainAtlas == nullptr) {
30223056
continue;
30233057
}
3024-
3025-
float pmRegionA[WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS * 4];
3026-
float pmRegionB[WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS * 4];
3027-
float pmSlotEnableMask[WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS];
3028-
3029-
Int pmNumSlots = 0;
3030-
if (useMultiPageAccumulation) {
3031-
pmNumSlots = m_map->getSplatAtlasRegionsForActiveSetPage(
3032-
terrainPage, pmRegionA, pmRegionB, pmSlotEnableMask);
3033-
}
3034-
else {
3035-
pmNumSlots = m_map->getSplatAtlasRegionsForActiveSet(pmRegionA, pmRegionB);
3036-
for (Int s = 0; s < WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS; ++s) {
3037-
pmSlotEnableMask[s] = (s < pmNumSlots) ? 1.0f : 0.0f;
3038-
}
3039-
}
3040-
3041-
// @debug Ronin 03/05/2026 Splat S20: viz modes need to see ALL active slots so
3042-
// the heatmap / wSum reflects the full material set, not just one page's subset.
3043-
if (forceSinglePassForViz) {
3044-
for (Int s = 0; s < WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS; ++s) {
3045-
pmSlotEnableMask[s] = (s < pmNumSlots) ? 1.0f : 0.0f;
3046-
}
3047-
}
3048-
30493058
if (pmNumSlots <= 0) {
30503059
continue;
30513060
}
30523061

3053-
Int pageEnabledSlots = 0;
3054-
for (Int s = 0; s < pmNumSlots; ++s) {
3055-
if (pmSlotEnableMask[s] > 0.5f) {
3056-
++pageEnabledSlots;
3057-
}
3058-
}
3059-
if (pageEnabledSlots <= 0) {
3060-
continue;
3061-
}
3062-
30633062
const float pmActiveCount[4] = { (float)pmNumSlots, 0.0f, 0.0f, 0.0f };
30643063

30653064
W3DShaderManager::setTexture(0, pTerrainAtlas);
@@ -3074,16 +3073,8 @@ void HeightMapRenderObjClass::renderPrimaryBlendControlPass()
30743073
IDirect3DDevice9* pPmDev = DX8Wrapper::_Get_D3D_Device8();
30753074
if (pPmDev) {
30763075
pPmDev->SetPixelShaderConstantF(0, pmControlParams, 1);
3077-
pPmDev->SetPixelShaderConstantF(1, pmRegionA, WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS);
3078-
pPmDev->SetPixelShaderConstantF(
3079-
1 + WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS,
3080-
pmRegionB,
3081-
WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS);
3076+
// Ronin @bugfix 14/09/2026 DX9: §19e.3. c1..c64 and c68..c75 go up per draw tile (channel reuse), not per pass.
30823077
pPmDev->SetPixelShaderConstantF(65, pmActiveCount, 1);
3083-
pPmDev->SetPixelShaderConstantF(
3084-
68,
3085-
pmSlotEnableMask,
3086-
WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS / 4);
30873078

30883079
{
30893080
// @bugfix Ronin 11/05/2026 Splat S20 / Normal-map N3: explicit sampler
@@ -3285,11 +3276,36 @@ void HeightMapRenderObjClass::renderPrimaryBlendControlPass()
32853276
}
32863277
}
32873278

3279+
// Ronin @bugfix 14/09/2026 DX9: §19e.3. Each tile uploads its OWN channel table. After the first accumulated draw, a tile
3280+
// with no material on this page adds nothing and is skipped.
3281+
const Bool passIsAdditive = useMultiPageAccumulation && drewAnyContribution;
3282+
IDirect3DDevice9* pTileDev = DX8Wrapper::_Get_D3D_Device8();
32883283
for (Int pmJ = 0; pmJ < m_numVBTilesY; ++pmJ) {
32893284
for (Int pmI = 0; pmI < m_numVBTilesX; ++pmI) {
32903285
// Ronin @perf 30/08/2026 DX9: §29i.3. Depth pass only — FALSE in every other pass.
32913286
if (shadowDepthSkipTile(pmI, pmJ, m_numVBTilesX))
32923287
continue;
3288+
3289+
float pmRegionA[WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS * 4];
3290+
float pmRegionB[WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS * 4];
3291+
float pmSlotEnableMask[WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS];
3292+
const Int tileEnabled = m_map->buildSplatRegionTable(
3293+
s_tileMaterialMask[pmJ * m_numVBTilesX + pmI],
3294+
useMultiPageAccumulation ? terrainPage : -1,
3295+
pmRegionA, pmRegionB, pmSlotEnableMask);
3296+
if (tileEnabled <= 0 && passIsAdditive)
3297+
continue;
3298+
if (forceSinglePassForViz) {
3299+
for (Int s = 0; s < WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS; ++s)
3300+
pmSlotEnableMask[s] = (s < pmNumSlots) ? 1.0f : 0.0f;
3301+
}
3302+
if (pTileDev) {
3303+
pTileDev->SetPixelShaderConstantF(1, pmRegionA, WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS);
3304+
pTileDev->SetPixelShaderConstantF(1 + WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS, pmRegionB,
3305+
WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS);
3306+
pTileDev->SetPixelShaderConstantF(68, pmSlotEnableMask, WorldHeightMap::SPLAT_MAX_ACTIVE_MATERIALS / 4);
3307+
}
3308+
32933309
DX8Wrapper::Set_Vertex_Buffer(getVertexBufferTile(pmI, pmJ));
32943310
if (Is_Hidden() == 0) {
32953311
DX8Wrapper::Draw_Triangles(0, HEIGHTMAP_POLYGON_NUM, 0, HEIGHTMAP_VERTEX_NUM);

0 commit comments

Comments
 (0)