fix(render): Phase A8 RR7.2 — _buildingRegistries key mismatch

RR7.1 fixed cell-timing but the indoor branch STILL fired 0 times in
the v2 visual gate (125,476 inside=True frames, all routed outdoor).
Real root cause: a key-form mismatch between storage and lookup.

Storage at line ~5886 used `_buildingRegistries[lb.LandblockId]`. But
lb.LandblockId is the LandBlock dat-file id (e.g. 0xA9B4FFFF — the
0xFFFF low word identifies the file as terrain). Lookups at the gate
(line ~7090) and the drain late-stamp (line ~5708) used
`cell.CellId & 0xFFFF0000u` (e.g. 0xA9B40000). 0xA9B4FFFF ≠ 0xA9B40000
so TryGetValue always missed; camBuildings stayed empty; the gate
fell to the outdoor branch unconditionally.

Fix: normalize all four sites to the masked form
(`& 0xFFFF0000u`) — storage at the build call, both Remove callbacks
in the streaming-controller setup, and the lookups (already correct).

User-visible symptom that surfaced the v2 launch:
  - sky + ground missing through windows
  - buildings + objects still visible
This pattern (stencil-gated outdoor passes failing while ungated
indoor pass works) was actually the OUTDOOR branch running with the
indoor visibility set — `visibleCellIds` filtered out terrain cells
and the sky pre-scene was gated off too because cameraInsideBuilding
was True (correctly) but camBuildings was empty (incorrectly).

Wait — re-reading the indoor branch's gate: it requires
camBuildings.Count > 0 too, so with the key mismatch it took the
outdoor branch. The sky+terrain visibility pattern user reported is
the outdoor branch where sky-pre-scene was correctly gated off by
!cameraInsideBuilding (cameraInsideBuilding is what computes the
ROUTING; it doesn't have to match the actual branch taken when the
extra `camBuildings.Count > 0` filter trips). So initial-sky was
skipped (cameraInsideBuilding=true) but indoor branch didn't fire
either — outdoor branch with no initial sky = the dark window
visual. RR7.2 closes both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-27 12:00:28 +02:00
parent a1a3e0ee3e
commit efe35201fc

View file

@ -1863,7 +1863,7 @@ public sealed class GameWindow : IDisposable
_terrain?.RemoveLandblock(id); _terrain?.RemoveLandblock(id);
_physicsEngine.RemoveLandblock(id); _physicsEngine.RemoveLandblock(id);
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu); _cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
_buildingRegistries.Remove(id); // Phase A8 _buildingRegistries.Remove(id & 0xFFFF0000u); // Phase A8 RR7.2: masked key matches storage
}); });
// A.5 T22.5: apply max-completions from resolved quality. // A.5 T22.5: apply max-completions from resolved quality.
_streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame; _streamingController.MaxCompletionsPerFrame = _resolvedQuality.MaxCompletionsPerFrame;
@ -5872,7 +5872,7 @@ public sealed class GameWindow : IDisposable
_physicsEngine.AddLandblock(lb.LandblockId, terrainSurface, cellSurfaces, _physicsEngine.AddLandblock(lb.LandblockId, terrainSurface, cellSurfaces,
portalPlanes, origin.X, origin.Y); portalPlanes, origin.X, origin.Y);
// Phase A8 (2026-05-26, fixed 2026-05-27 RR7.1): build per-landblock // Phase A8 (2026-05-26, fixed 2026-05-27 RR7.2): build per-landblock
// BuildingRegistry from LandBlockInfo.Buildings, stamping // BuildingRegistry from LandBlockInfo.Buildings, stamping
// LoadedCell.BuildingId for each cell in a building's cell set. // LoadedCell.BuildingId for each cell in a building's cell set.
// Uses _cellVisibility.AllLoadedCells (every cell loaded so far, // Uses _cellVisibility.AllLoadedCells (every cell loaded so far,
@ -5881,9 +5881,19 @@ public sealed class GameWindow : IDisposable
// pass get stamped at drain time (see _pendingCells loop above). // pass get stamped at drain time (see _pendingCells loop above).
// Cells without a building stay at BuildingId == null (outdoor // Cells without a building stay at BuildingId == null (outdoor
// surface cells; dungeon cells not in LandBlockInfo.Buildings). // surface cells; dungeon cells not in LandBlockInfo.Buildings).
//
// KEY NORMALIZATION (RR7.2): lb.LandblockId is the LandBlock file
// id (e.g. 0xA9B4FFFF — the 0xFFFF low word is the dat-file
// discriminator), but cell ids like 0xA9B40150 mask to
// 0xA9B40000. All lookups (drain late-stamp at line ~5708, gate
// check at line ~7090) use `& 0xFFFF0000u`, so storage MUST use
// the same masked form or every lookup misses — which silently
// routed every indoor frame through the outdoor branch in the
// RR7.1 launch.
if (lbInfo is not null) if (lbInfo is not null)
{ {
_buildingRegistries[lb.LandblockId] = uint lbRegistryKey = lb.LandblockId & 0xFFFF0000u;
_buildingRegistries[lbRegistryKey] =
AcDream.App.Rendering.Wb.BuildingLoader.Build( AcDream.App.Rendering.Wb.BuildingLoader.Build(
lbInfo, lb.LandblockId, _cellVisibility.AllLoadedCells); lbInfo, lb.LandblockId, _cellVisibility.AllLoadedCells);
} }
@ -9083,7 +9093,7 @@ public sealed class GameWindow : IDisposable
_terrain?.RemoveLandblock(id); _terrain?.RemoveLandblock(id);
_physicsEngine.RemoveLandblock(id); _physicsEngine.RemoveLandblock(id);
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu); _cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
_buildingRegistries.Remove(id); // Phase A8 _buildingRegistries.Remove(id & 0xFFFF0000u); // Phase A8 RR7.2: masked key
}); });
_streamingController.MaxCompletionsPerFrame = newResolved.MaxCompletionsPerFrame; _streamingController.MaxCompletionsPerFrame = newResolved.MaxCompletionsPerFrame;