Four RR7 variants shipped + reverted in one session (RR7, RR7.1, RR7.2,
RR7.3). The root architectural mismatch: RR7 routed cell-mesh rendering
through ObjectMeshManager / WbDrawDispatcher.Draw(IndoorPass) — a per-
GfxObj batched pipeline. WB uses a separate EnvCellRenderManager (862
LOC) for cells; we never extracted it. Indoor branch fires correctly
after RR7.2 + RR7.3 but interior cell geometry doesn't render.
User direction (verbatim, 2026-05-27): port WB verbatim. No band-aids.
Visual test launch only when fix is ready; probe data verified first.
Handoff captures:
- Session log of all four RR7 attempts + why each failed
- Why WB over retail (modern GL fit + existing Phase N.4/N.5/O
commitment to WB as rendering base)
- The full WB RenderInsideOut algorithm spec (Steps 1-5, line refs)
- 5-phase next-session plan (extract EnvCellRenderManager + deps,
wire into landblock load, replicate RenderInsideOut byte-for-byte,
probe trail mandatory before visual gate, single visual gate)
- Process rules carved from this session's mistakes (no visual gate
without probe data first, no partial WB ports, no conceptual
adaptations, trust-but-verify, slow at brainstorm not implement)
RR3-RR6 infrastructure remains shipped + tested in isolation
(Building/Registry/Loader/Dispatcher cellIds overload/Stencil pipeline).
Branch is at pre-A8 visual ("looks good") with infrastructure dormant.
Next session opens cold against the pickup prompt at the bottom of
the handoff doc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 KiB
Phase A8 RR7 reverted — full WB port handoff (2026-05-27)
TL;DR for next session
RR7 (render-frame integration) shipped 4 times in one session; all 4 broke
the visual differently. All four are reverted. Branch is back to the
pre-A8 visual ("looks good"). RR3-RR6 infrastructure (Building,
BuildingRegistry, BuildingLoader, WbDrawDispatcher.Draw(cellIds:)
overload, IndoorCellStencilPipeline 3-bit + occlusion-query) remains
shipped + tested in isolation.
The fundamental mistake: RR3-RR7 ported WB's RenderInsideOut Steps 1-4
conceptually but routed cell-mesh rendering through our
ObjectMeshManager / WbDrawDispatcher.Draw(IndoorPass) pipeline. WB
doesn't do that — WB has a separate EnvCellRenderManager (862 LOC) that
renders cells via a different path. Without extracting that, the indoor
branch fires (gate works post-RR7.2) but cell interiors never render →
flat fog-color floors.
Next session's mission: port WB verbatim, including extracting
EnvCellRenderManager.cs + dependencies into our tree. No conceptual
adaptations. No "modern equivalent" decisions. Follow WB byte-for-byte
where the algorithm runs, just as Phase O extracted WB's mesh path.
User direction (verbatim, 2026-05-27):
"Either we port exact behavior from retail or we port exact behavior from WB. ... Make a detailed plan to port WB verbatim behaviour to fix this. No quickfixes or fixes that might cause issues down the line ... use superpowers but DONT stop me for questions, be perfect, no band-aids. When you have a visual test ready with all rendering fix for this you launch the client for me to verify."
User decision: WB. (See decision rationale in "Why WB and not retail" below.)
Session log — what was tried and why it failed
This session opened picking up RR2 (BuildingInfo data-shape spike, shipped clean) and then drove RR3 → RR4 → RR5 → RR6 → RR7 as planned. The four RR7-variant fix attempts came after the user reported broken visuals at the first visual gate.
Commits shipped this session, before revert
| SHA | Phase | Status now | What it did |
|---|---|---|---|
f44a9bf |
RR2 | KEPT | Findings doc — BuildingInfo data shape + WB walk algorithm |
f125fdb |
RR3 | KEPT | Building + BuildingRegistry + BuildingLoader + 10 unit tests |
f8d0499 |
RR4 | KEPT | LoadedCell.BuildingId + landblock-load wiring + 1 test |
3361933 |
RR5 | KEPT | WbDrawDispatcher.Draw(cellIds:) overload + 2 tests |
6a7894a |
RR6 | KEPT | IndoorCellStencilPipeline 3-bit + 9 occlusion-query/state methods |
3d28d70 |
RR7 | REVERTED by 4fa3390 |
GameWindow render-frame restructure |
a1a3e0e |
RR7.1 | REVERTED by 21dc72b |
AllLoadedCells + late-stamp on drain |
efe3520 |
RR7.2 | REVERTED by 9aaae02 |
_buildingRegistries key normalization |
56673e1 |
RR7.3 | REVERTED by 07c5981 |
Dat-driven BFS in BuildingLoader |
Net infrastructure shipped: 5 commits, ~1100 LOC of production + 13 unit tests. All correct in isolation. None of the integration code remains on the branch.
Visual-gate launches and what they revealed
Launch v1 — RR7 alone (commit 3d28d70)
- User reported: "Yes looks good!"
[vis]log:branch=indoorcount = 0 (out of 47,266 outdoor decisions). 17,748 frames hadinside=True really=True(camera in an indoor cell) — but the gate'sBuildingId is not nullcheck failed every time.- Why "looks good" was misleading: RR7's call site used
drainedCells(the per-frame_pendingCellsdrain). Cells streamed in over many frames, butBuildingLoader.Buildran once per landblock load with whatever was in drainedCells THAT frame. Most building cells were stamped on a frame when they weren't yet drained, soBuildingIdstayed null. ThencameraInsideBuilding=false, the outdoor branch ran with full sky + initial terrain. Visually indistinguishable from pre-A8. - My process failure: declared visual gate passed without reading
the
[vis]data first. "Looks good" without diagnostic correlation is not verification.
Launch v2 — RR7 + RR7.1 (a1a3e0e)
- User reported: "All textures are missing, ground, sky only buildings and objects are visible. Looks much worse."
[vis]log:branch=indoorSTILL 0 of 163,670 (with 125,476inside=True).- Why it got worse: RR7.1 made
BuildingLoader.Builduse_cellVisibility.AllLoadedCells(every loaded cell, not just the drain) which stamped MORE cells withBuildingId. That madecameraInsideBuilding=truefor more frames. But the registry-key lookup at the gate STILL missed (storage at0xA9B4FFFF, lookup at0xA9B40000— see RR7.2 below). SocameraInsideBuilding=true→ sky + initial terrain GATED OFF → indoor branch's inner gate (camBuildings.Count > 0) FAILED → outdoor branch ran WITHOUT sky and terrain → black through windows.
Launch v3 — RR7 + RR7.1 + RR7.2 (efe3520)
- User reported: missing texture indoors (screenshot shows light-grey fog-color areas where cell interior surfaces should be).
[vis]log:branch=indoor= 119,471 vs outdoor 2,910. Indoor branch finally fires.- Why it still broke: RR7.2 fixed the registry key. Indoor branch
fires,
MarkAndPunchruns,Draw(IndoorPass, cellIds: camCellIds)runs. Building shells (cottage walls / inn walls — theIsBuildingShellentities) render. But cell-mesh entities (registered withMeshRef(envCellId, ...)) don't produce a textured floor. The[vis]data confirms the gate works; the visual confirms the cell-mesh path doesn't.
Launch v4 — RR7 + RR7.1 + RR7.2 + RR7.3 (56673e1)
- User reported: still flat grey areas.
- Why it still broke: RR7.3 made BFS dat-driven so building
EnvCellIds is complete regardless of cell load timing. Confirmed
BFS short-circuiting was NOT the cause —
camCellIdscontains the user's current cell, the cell-mesh entity is walked, but the floor doesn't appear.
Root cause (only fully understood at session end)
WB's VisibilityManager.RenderInsideOut
(references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239)
renders the inside-building cells via:
envCellManager!.Render(pass1RenderPass, _currentEnvCellIds);
This calls into a separate manager class —
EnvCellRenderManager.cs, 862 LOC, also in WB — that handles cell
rendering with its own GL pipeline, separate from
ObjectMeshManager.cs. The two managers exist because cell rendering
has different requirements (per-cell texture batching, different
transparency handling, cell-portal-aware geometry) from per-GfxObj
rendering.
Our RR7 collapsed Steps 3 (cell rendering) and Step 4 (stencil-gated outdoor) into:
_wbDrawDispatcher!.Draw(camera, ..., cellIds: camCellIds,
set: EntitySet.IndoorPass);
The dispatcher's IndoorPass walks entities including cell-mesh
entities (created in GameWindow.BuildInteriorEntitiesForStreaming at
line ~5441 with MeshRefs = new[] { cellMeshRef } where
cellMeshRef.GfxObjId = envCellId). But ObjectMeshManager's draw
path is fundamentally per-GfxObj batched + MDI; it has a dat-side
PrepareEnvCellMeshData path (line ~1184 of WB's ObjectMeshManager,
also in our extracted copy) but that path's output isn't wired into
the dispatcher's instance-buffer layout the same way GfxObj meshes
are. Building shells render (they ARE GfxObj entities with proper
mesh refs after hydration at line ~5160). Cell meshes don't render
correctly.
In short: the cell-mesh entity scheme we use is an architectural
mismatch with WB's render algorithm. WB renders cells through
EnvCellRenderManager.Render(cellIdSet) — a per-cell rendering call.
We render cells through Dispatcher.Draw(set: IndoorPass) — a
per-entity batched call. The two are not interchangeable.
Why WB and not retail
User asked decisively: "Either we port exact behavior from retail or we port exact behavior from WB. What do you want?"
I chose WB. Reasons:
-
Retail's algorithm doesn't fit modern GL. Retail's
PView::DrawCellsatacclient_2013_pseudo_c.txt:432709uses software polygon-clip rects (set per portal during recursive cell traversal). Porting verbatim requires either (a) inventing a modern-equivalent — which is what WB already did — or (b) implementing per-fragment shader-discard against portal polygons, which is expensive and non-trivial. -
WB is already our rendering base. Phase N.4 (2026-05-08) adopted WB as our rendering oracle. Phase N.5 made WB's bindless +
glMultiDrawElementsIndirectmandatory. Phase O (2026-05-21) extracted WB's mesh + dat-handling code into our tree (references/WorldBuilder/remains as read-reference, but the actual pipeline files live atsrc/AcDream.App/Rendering/Wb/). Adopting WB'sEnvCellRenderManager+VisibilityManageris the natural continuation. -
Modern code, retail behavior — WB is the existing "modern code, retail-equivalent behavior" port. WB's stencil-based RenderInsideOut is the modern-GL realization of retail's polygon-clip algorithm. The observable behavior matches.
-
Same exact stack. WB is MIT-licensed Silk.NET + .NET 10 + DatReaderWriter — verbatim our stack. No translation cost.
-
Tested by WB's developers. WB's RenderInsideOut works in their tool. Faithful porting means we inherit their validation.
What WB's render frame actually does (the spec for the redo)
The render frame algorithm lives at
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239.
The RenderInsideOut method takes managers as parameters
(portalManager, envCellManager, terrainManager, sceneryManager,
staticObjectManager, sceneryShader). Each step:
Step 1: Stencil bit 1 at our building's portals (lines 78-97)
Enable(StencilTest),ClearStencil(0),Clear(StencilBufferBit).Disable(CullFace),StencilFunc.Always(1, 0xFF),StencilOp(Keep, Keep, Replace),StencilMask(0x01),ColorMask(false×4),DepthMask(false),Enable(DepthTest),DepthFunc.Always.- For each building containing the current cell:
portalManager?.RenderBuildingStencilMask(building, snapshotVP, false).
Step 2: Punch depth at portals (lines 99-105)
DepthMask(true),DepthFunc.Always.- For each building containing the current cell:
RenderBuildingStencilMask(building, snapshotVP, true).
Step 3: Render OUR cells (stencil OFF) (lines 107-127)
ColorMask(true, true, true, false)(note: alpha bit OFF — WB intentional choice).DepthMask(true),Disable(StencilTest),DepthFunc.Less.sceneryShader?.Bind().- Collect
_currentEnvCellIdsfrom_buildingsWithCurrentCell.SelectMany(b => b.EnvCellIds). envCellManager!.Render(pass1RenderPass, _currentEnvCellIds).- If transparency enabled:
DepthMask(false), render transparent pass,DepthMask(true).
Step 4: Stencil-gated outdoor — terrain + scenery + static objects (lines 129-154)
- If
didInsideStencil(we had buildings):Enable(StencilTest),StencilFunc.Equal(1, 0x01),StencilOp(Keep, Keep, Keep),StencilMask(0x00),ColorMask(true, true, true, false),DepthMask(true),Enable(CullFace),DepthFunc.Less. terrainManager.Render(snapshotView, snapshotProj, snapshotVP, snapshotPos, snapshotFov).sceneryShader?.Bind().- If scenery enabled:
sceneryManager?.Render(pass1RenderPass). - If static-objects/buildings shown:
staticObjectManager?.Render(pass1RenderPass).
Step 5: Other-buildings' cells through portals (lines 156-232)
- Collect
_otherBuildingsfrom_visibleBuildingPortalsfiltering OUT buildings that containcurrentEnvCellId. - For each other-building (per
_otherBuildings):- Read back previous frame's occlusion query
(
GetQueryObject(building.QueryId, ResultAvailable),GetQueryObject(... Result)). Updatebuilding.WasVisible. - Start new query:
BeginQuery(SamplesPassed, building.QueryId),building.QueryStarted = true. - a. Mark Bit 2 (Ref=3, Mask=0x02) where Bit 1 set
(
StencilFunc.Equal(3, 0x01),StencilOp Replace,StencilMask 0x02,ColorMask off,DepthMask off,Disable(CullFace)).portalManager?.RenderBuildingStencilMask(building, snapshotVP, false). EndQuery(SamplesPassed).- b. Clear depth where Stencil == 3 (
StencilFunc.Equal(3, 0x03),StencilMask 0x00,DepthMask true,DepthFunc.Always).RenderBuildingStencilMask(building, snapshotVP, true). - c. Render other-building's EnvCells gated by Stencil == 3
(
ColorMask(true, true, true, false),DepthFunc.Less,Enable(CullFace)).sceneryShader.Bind().envCellManager.Render(pass1RenderPass, building.EnvCellIds)(+ transparent). - d. Reset Bit 2 back to 0 for next iteration
(
StencilMask 0x02,StencilFunc.Always(1, 0x02),StencilOp Replace,ColorMask off,DepthMask off).RenderBuildingStencilMask(building, snapshotVP, false).
- Read back previous frame's occlusion query
(
Cleanup (lines 234-238)
Disable(StencilTest),StencilMask(0xFF),ColorMask(true×3, false).
Why our RR7 didn't match this
- No
envCellManager.Render(...)call. We routed cells throughDispatcher.Draw(IndoorPass), which is per-GfxObj-batched, not per-cell. - No separate transparency pass for cells. Step 3's
DepthMask(false) + Render(Transparent)was missing. - No
sceneryShader.Bind()between passes. WB's algorithm assumes a specific shader is bound at each step; we never did. - Step 5 missing entirely. Cross-building visibility (cottage cellar visible from cottage above, inn rooms visible through doors) not implemented. Would have shipped in RR9 but RR7 should have at least scaffolded the order.
- ColorMask alpha-bit pattern not preserved. WB uses
ColorMask(true, true, true, false)deliberately — alpha-bit OFF. Our outdoor branch'sDraw(All)doesn't toggle alpha bit, but WB's path does. Could affect alpha-to-coverage downstream.
The plan for the next session
Phase 1: Extract EnvCellRenderManager into our tree (~862 LOC)
Mirror Phase O's pattern:
- Read
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.csin full. - Identify its dependencies — likely
GlobalMeshBuffer,ObjectMeshManager(already extracted),TextureAtlasManager,IRenderManager,RenderPass,SceneData. Extract any missing dependencies. - Copy
EnvCellRenderManager.cstosrc/AcDream.App/Rendering/Wb/EnvCellRenderManager.cs. - Adapt namespaces (
Chorizite.OpenGLSDLBackend.Lib→AcDream.App.Rendering.Wb). - Resolve any references to types we don't have. Stub or extract as needed.
- Build green. No tests yet at this step.
Phase 2: Wire EnvCellRenderManager into the existing landblock load
EnvCellRenderManager.Register(envCell, cellStruct, worldTransform, ...) is
how cells join its registry. Currently we call CellMesh.Build at
GameWindow.BuildInteriorEntitiesForStreaming (line ~5423). Replace
that with the EnvCellRenderManager registration path — cell meshes
flow through ITS pipeline, not through ObjectMeshManager via fake-
GfxObj-id MeshRefs.
The WorldEntity we create with MeshRefs = [cellMeshRef] (line 5441)
becomes irrelevant for cell rendering — the EnvCellRenderManager owns
the cells, the dispatcher renders only entities that have real GfxObj
mesh refs.
Phase 3: Replicate VisibilityManager.RenderInsideOut byte-for-byte
In GameWindow.cs render frame (after the per-frame glClear +
visibility computation), replace the if (cameraInsideBuilding) { ... } else { ... } block we shipped + reverted with a call to a
new method RenderInsideOutAcdream that follows WB's Steps 1-5 line by
line.
PortalRenderManager.RenderBuildingStencilMask(building, vp, punch) is
the other dependency. Extract from
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs
(702 LOC) — at minimum the stencil-mask method + its mesh upload path.
The plumbing may be reusable for our existing IndoorCellStencilPipeline.
Our IndoorCellStencilPipeline already implements WB's Steps 1+2 +
Step 5 a/b/c/d. The mismatch is what calls them — our code calls
them with _indoorStencilPipeline.MarkAndPunch(...) etc. WB calls them
via portalManager.RenderBuildingStencilMask(building, vp, punch).
The pipelines are equivalent in spirit but the entry point differs.
Map our pipeline methods onto WB's interface signature so the
RenderInsideOut algorithm can call them by name.
Phase 4: Probes BEFORE visual launches
Mandatory before any visual gate. Add (gated on
ACDREAM_PROBE_VIS=1 or a new ACDREAM_PROBE_ENVCELL=1 flag):
[envcells]per frame: count of cells walked byEnvCellRenderManager.Render, count of triangles drawn, the cellId set being rendered.[stencil]per frame: vertex count uploaded for MarkAndPunch (the existing pipeline emits this internally — surface it).[draworder]per frame: assertion that the algorithm ran each step in the right order with the right GL state on entry.
When a visual gate fires:
- ALWAYS read the probe data FIRST. Confirm indoor branch fired, envcells were rendered, stencil mask was non-empty.
- Compare probe data to expected (the design doc has the algorithm spelled out).
- ONLY THEN ask the user for visual confirmation.
Phase 5: Visual gate (single)
Once Phases 1-4 done + probe data confirms correct behavior: launch the client for the user to verify. ONE gate. Not four.
Open questions for the next session to investigate
These DON'T require user input — investigate during execution:
-
PortalRenderManager.RenderBuildingStencilMaskmesh upload. Does WB upload exit portal polygons differently than we do? OurUploadBuildingPortalMesh(Phase A8 RR6) might map cleanly to WB's expectation, or might need adjustment. -
EnvCellRenderManager.RegisterAPI. What does it accept? Compare to our_pendingCellMeshes[envCellId] = cellSubMeshes. Identify the seam. -
Transparency pass. WB's Step 3 has an
if (state.EnableTransparencyPass)secondRender(Transparent)call. We don't have a state object yet; need to either add one or pick the default (likely enabled, since indoor transparency matters for stained glass, ornate furniture). -
Occlusion queries (RR9 scope). RR7's job was Steps 1-4 only; RR9 was supposed to add Step 5. But WB's RenderInsideOut has Step 5 inline — we shouldn't split it. Land Steps 1-5 together in the next attempt. RR9 becomes a no-op or absorbed.
-
OutdoorSceneryEntitySet. WB's Step 4 callssceneryManager.Render(pass1RenderPass)andstaticObjectManager.Render(pass1RenderPass)separately. We've collapsed both intoDraw(EntitySet.OutdoorScenery). Need to verify ourOutdoorScenerypartition matches what WB's two managers cover, OR split them into two dispatch calls.
Process rules for the next session (carved from this session's mistakes)
-
No visual-gate launch without probe data first. If the probe says branch=indoor count = 0, the user's "looks good" doesn't confirm A8 is working. Read the probe BEFORE asking the user.
-
No partial WB ports. Extract the manager. Wire it. Implement the algorithm in full. No "Steps 1-4 now, Step 5 later." The steps are interdependent; partial implementations have wrong cumulative state.
-
No conceptual adaptations of WB. If WB does X, do X. If our stack has a different way of doing it, either extract the WB way into our stack OR use the existing analog 1:1 without "improvement." No new abstractions invented mid-port.
-
Trust-but-verify after every subagent dispatch. Subagents compile + pass tests in their isolation but don't verify visual correctness. The harness pattern from #98 saga applies: build the apparatus first, then trust evidence over plausible-looking code.
-
Acknowledge the cost-of-failure asymmetry. Each "fix" that doesn't work costs the user a launch cycle, screenshot review, bug-report write-up. Three wrong fixes in a row > one fully-thought fix. Slow down at the brainstorming step, not at the implementation step.
Files that remain shipped (RR3-RR6 infrastructure)
These work in isolation and stay on the branch:
| File | LOC | Tested |
|---|---|---|
src/AcDream.App/Rendering/Wb/Building.cs |
57 | 2 tests |
src/AcDream.App/Rendering/Wb/BuildingRegistry.cs |
73 | 4 tests |
src/AcDream.App/Rendering/Wb/BuildingLoader.cs |
144 | 5 tests |
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs (additions: Draw(cellIds:) overload + WalkEntitiesForTestByCellIds) |
+153 | 2 tests |
src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs (additions: 4 stencil-3-bit methods + 4 occlusion-query methods + UploadBuildingPortalMesh) |
+243 | 0 tests (GL required) |
The LoadedCell.BuildingId field also persists (from RR4) — that's a
1-property addition to CellVisibility.cs. RR4's wire-in in
GameWindow.cs (the _buildingRegistries dict + the
BuildingLoader.Build(...) call at line ~5876 + the RemoveLandblock
callbacks) is also reverted by the RR7 revert chain — the dict and
all references to it are gone now. Confirm via:
grep -n _buildingRegistries src/AcDream.App/Rendering/GameWindow.cs
If zero matches, the revert is complete. If matches remain, RR4 needs manual cleanup (likely a stray field declaration the revert didn't catch).
Pickup prompt for next session
Read this entire handoff doc, then read these in order:
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239(the RenderInsideOut algorithm we're porting verbatim)references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.cs(the manager to extract — 862 LOC)references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs(the other dependency — extract the stencil-mask method + any infrastructure)docs/architecture/worldbuilder-inventory.md(what we've already extracted from WB and where it lives)docs/superpowers/plans/2026-05-26-phase-a8-wb-full-port.md(the original A8 plan — IGNORE its RR7 design, follow this handoff doc's plan instead)Then brainstorm + write a fresh detailed plan covering:
- The exact extraction list (every WB file to copy into our tree)
- The exact wire-in points in GameWindow.cs
- The probe trail with format specifications
- The expected visual outcomes per step
- The order of execution (extraction → wiring → probes → visual gate)
Use the superpowers:writing-plans skill. The plan goes to
docs/superpowers/plans/2026-05-28-phase-a8-wb-render-inside-out-port.md.Once the plan is written, execute it without stopping. No questions to the user mid-flight. When the visual test is ready, launch the client for visual confirmation. Read probe data BEFORE accepting any "looks good" report.
User authorization (verbatim 2026-05-27): "use superpowers but DONT stop me for questions, be perfect, no bandaids."
Key references
- Plan we deviated from:
docs/superpowers/plans/2026-05-26-phase-a8-wb-full-port.md - Design doc:
docs/superpowers/specs/2026-05-26-phase-a8-wb-full-port-design.md - WB extraction precedent (Phase O): commit
6a7894a's parent chain - WB code root:
references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/ - This session's RR1 handoff (still relevant for project context):
docs/research/2026-05-26-a8-wb-full-port-rr1-shipped-handoff.md - RR2 findings (BuildingInfo data shape — still accurate, useful for
understanding the building model):
docs/research/2026-05-26-a8-buildings-data-shape.md