fix(render): Phase A8 — stamp BuildingId on already-loaded cells too

First visual-gate launch showed 8,737 [vis] lines (player at Holtburg
cottage cell 0xA9B40143, inside=True really=True) but ZERO [buildings] /
[envcells] / [stencil] / [draworder] probe emissions. Root cause: same as
the original RR7.1 saga — BuildingLoader.Build was passed only the
per-frame drainedCells dict, missing cells loaded on PRIOR frames. Those
cells stayed with BuildingId=null, the strict cameraInsideBuilding gate
returned false, the indoor branch never fired.

Fix: in ApplyLoadedTerrainLocked, merge drainedCells with the cells
already registered in _cellVisibility for the same landblock prefix
before passing to BuildingLoader. The richer dict ensures the stamping
loop in BuildingLoader.Build covers EVERY cell in this landblock.

Added IReadOnlyList<LoadedCell> GetCellsForLandblock(uint lbId) on
CellVisibility — minimal API expose; existing _cellsByLandblock dict
was already the right shape (lbId = upper 16 bits).

Build green. Tests unchanged.

Next: relaunch the client. With the fix, [buildings] probe should fire
with camBldgs=[0x1,...] when the player is inside a Holtburg cottage,
[envcells] should report cells>=1 tris>=1 per indoor frame, and the
indoor branch should be exercising the WB-faithful Steps 1-5 pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-27 15:18:07 +02:00
parent 8532c84f57
commit 0fc6003c2a
2 changed files with 45 additions and 8 deletions

View file

@ -208,6 +208,23 @@ public sealed class CellVisibility
_cellLookup[cell.CellId] = cell;
}
/// <summary>
/// Phase A8 (2026-05-28): enumerates the loaded cells that belong to a
/// landblock prefix. Used by <c>GameWindow.ApplyLoadedTerrainLocked</c> when
/// building the per-landblock <c>BuildingRegistry</c> — the per-frame
/// <c>drainedCells</c> dict misses cells loaded on prior frames, so the
/// stamping loop in <see cref="Wb.BuildingLoader.Build"/> needs access to
/// every cell currently in the landblock to ensure <c>BuildingId</c> is set.
/// </summary>
/// <param name="lbId">Upper 16 bits of the landblock key (e.g. <c>0xA9B4</c>
/// for landblock <c>0xA9B40000</c>). NOT the full 32-bit landblock id.</param>
public IReadOnlyList<LoadedCell> GetCellsForLandblock(uint lbId)
{
return _cellsByLandblock.TryGetValue(lbId, out var list)
? list
: System.Array.Empty<LoadedCell>();
}
/// <summary>
/// Removes all cells belonging to <paramref name="lbId"/> (upper 16 bits of
/// the landblock key, e.g. 0xA9B4 for landblock 0xA9B40000). Called when a

View file

@ -5878,18 +5878,38 @@ public sealed class GameWindow : IDisposable
_physicsEngine.AddLandblock(lb.LandblockId, terrainSurface, cellSurfaces,
portalPlanes, origin.X, origin.Y);
// Phase A8 (2026-05-26): build per-landblock BuildingRegistry from
// LandBlockInfo.Buildings, stamping LoadedCell.BuildingId for each cell
// in a building's cell set. Uses the already-drained drainedCells dict
// (LoadedCells registered this frame) so stamping and registry build
// happen in the same render-thread pass — no extra dat reads required.
// Cells without a building stay at BuildingId == null (outdoor surface
// cells; dungeon cells not enumerated in LandBlockInfo.Buildings).
// Phase A8 (2026-05-26 / fix 2026-05-28): build per-landblock
// BuildingRegistry from LandBlockInfo.Buildings, stamping
// LoadedCell.BuildingId for each cell in a building's cell set.
//
// FIX 2026-05-28: previously passed `drainedCells` only — that's the
// dict of cells drained THIS frame. Cells loaded on prior frames
// were missed, leaving their BuildingId null and the
// `cameraInsideBuilding` gate FALSE even when the player was inside
// a tagged cottage. (First visual-gate launch showed 8737 [vis]
// lines with inside=True really=True but ZERO [buildings] probe
// emissions — same root cause as the RR7.1 / RR7.2 saga.) The fix:
// merge `drainedCells` with every cell currently registered with
// _cellVisibility for this landblock prefix. BuildingLoader's BFS
// + stamping pass then sees the complete cell set.
//
// Cells without a building stay at BuildingId == null (outdoor
// surface cells; dungeon cells not enumerated in LandBlockInfo.Buildings).
if (lbInfo is not null)
{
// Merge: previously-loaded cells + freshly-drained cells.
// drainedCells wins on key collision (it has the same instance
// anyway — both dicts reference the same LoadedCell objects).
var lbStampCells = new System.Collections.Generic.Dictionary<uint, LoadedCell>(drainedCells);
uint lbPrefix = (lb.LandblockId >> 16) & 0xFFFFu;
foreach (var c in _cellVisibility.GetCellsForLandblock(lbPrefix))
{
if (!lbStampCells.ContainsKey(c.CellId))
lbStampCells[c.CellId] = c;
}
_buildingRegistries[lb.LandblockId] =
AcDream.App.Rendering.Wb.BuildingLoader.Build(
lbInfo, lb.LandblockId, drainedCells);
lbInfo, lb.LandblockId, lbStampCells);
}
// Phase A8: finalize EnvCellRenderer's per-landblock instance store.