From a1a3e0ee3e3260301f11d18fb5f1a232d8533c11 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 27 May 2026 11:45:45 +0200 Subject: [PATCH] =?UTF-8?q?fix(render):=20Phase=20A8=20RR7.1=20=E2=80=94?= =?UTF-8?q?=20stamp=20BuildingId=20on=20cells=20loaded=20across=20multiple?= =?UTF-8?q?=20frames?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RR7 visual gate (2026-05-27) revealed the indoor branch NEVER fired even when the strict gate's PointInCell + non-null CameraCell hit: 17,748 inside=True frames, 0 branch=indoor decisions. Root cause: RR4 wired BuildingLoader.Build with the per-frame drainedCells dict — cells that streamed in on earlier frames (the common case, since cells arrive asynchronously over many frames after the landblock-info completion) were not in drainedCells, so the BFS short-circuited and the registry's EnvCellIds set was systematically incomplete. Cells loaded ahead of lbInfo arrival never got their BuildingId stamped. Fix has two parts: 1. CellVisibility.AllLoadedCells — new public IReadOnlyDictionary exposing the existing private _cellLookup. BuildingLoader.Build at landblock-info-arrival now walks the full cell set, not just this frame's drain. 2. _pendingCells drain loop — late-stamps BuildingId on each arriving cell if its landblock's BuildingRegistry already exists. Covers cells that arrive AFTER the registry-build pass. Together these handle all four timing cases: - Cells loaded before lbInfo arrives → stamped in BuildingLoader.Build - Cells loaded with lbInfo (same frame) → stamped in BuildingLoader.Build - Cells loaded after lbInfo arrives → stamped in drain loop - lbInfo never arrives (LB has no info) → registry never built, cells stay at BuildingId == null (intended — flow through outdoor render path) Probe data from the failed gate launch confirmed cell 0xA9B40150 (cottage idx=6 cellar from the #98 saga) was reached as the camera cell with visN=16 visible neighbours, but BuildingId stayed null. This fix gets the indoor branch fired in that scenario. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/CellVisibility.cs | 12 ++++++++ src/AcDream.App/Rendering/GameWindow.cs | 33 ++++++++++++++------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/AcDream.App/Rendering/CellVisibility.cs b/src/AcDream.App/Rendering/CellVisibility.cs index d3cf6f1..87fdc67 100644 --- a/src/AcDream.App/Rendering/CellVisibility.cs +++ b/src/AcDream.App/Rendering/CellVisibility.cs @@ -176,6 +176,18 @@ public sealed class CellVisibility /// Full-ID lookup for O(1) neighbour resolution during BFS. private readonly Dictionary _cellLookup = new(); + /// + /// Phase A8 RR7.1 (2026-05-27): read-only view of every loaded cell, keyed + /// by full 32-bit cell id. Used by at + /// landblock-info-arrival time so its BFS can reach cells that streamed + /// in on earlier frames (not just the per-frame drain). Without this view + /// the registry's EnvCellIds set was systematically short, leaving + /// unset on the cells the camera + /// actually enters — which silently routed indoor frames through the + /// outdoor branch. + /// + public IReadOnlyDictionary AllLoadedCells => _cellLookup; + /// The cell the camera was in during the last call. private LoadedCell? _lastCameraCell; diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index d6ecebb..6c26170 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -5696,12 +5696,21 @@ public sealed class GameWindow : IDisposable _terrain.AddLandblockWithMesh(lb.LandblockId, meshData, origin); // Step 4: drain pending LoadedCells from the worker thread. - // Also collect into a local dict for the BuildingLoader stamping pass below. - var drainedCells = new System.Collections.Generic.Dictionary(); + // Phase A8 RR7.1 (2026-05-27): also late-stamp BuildingId on each + // arriving cell if the landblock's BuildingRegistry already exists + // (cells loaded after the registry-build pass at line ~5876). Cells + // arriving BEFORE the registry are stamped by BuildingLoader.Build + // itself via the AllLoadedCells dict. while (_pendingCells.TryTake(out var cell)) { _cellVisibility.AddCell(cell); - drainedCells[cell.CellId] = cell; + uint cellLbId = cell.CellId & 0xFFFF0000u; + if (_buildingRegistries.TryGetValue(cellLbId, out var existingReg)) + { + var bs = existingReg.GetBuildingsContainingCell(cell.CellId); + if (bs.Count > 0) + cell.BuildingId = bs[0].BuildingId; + } } // Compute the per-landblock AABB for frustum culling. XY from the @@ -5863,18 +5872,20 @@ 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, fixed 2026-05-27 RR7.1): build per-landblock + // BuildingRegistry from LandBlockInfo.Buildings, stamping + // LoadedCell.BuildingId for each cell in a building's cell set. + // Uses _cellVisibility.AllLoadedCells (every cell loaded so far, + // not just the per-frame drain) so the BFS can reach cells that + // streamed in on earlier frames. Cells arriving AFTER this build + // pass get stamped at drain time (see _pendingCells loop above). + // Cells without a building stay at BuildingId == null (outdoor + // surface cells; dungeon cells not in LandBlockInfo.Buildings). if (lbInfo is not null) { _buildingRegistries[lb.LandblockId] = AcDream.App.Rendering.Wb.BuildingLoader.Build( - lbInfo, lb.LandblockId, drainedCells); + lbInfo, lb.LandblockId, _cellVisibility.AllLoadedCells); } }