# 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=indoor` count = **0** (out of 47,266 outdoor decisions). 17,748 frames had `inside=True really=True` (camera in an indoor cell) — but the gate's `BuildingId is not null` check failed every time. - **Why "looks good" was misleading:** RR7's call site used `drainedCells` (the per-frame `_pendingCells` drain). Cells streamed in over many frames, but `BuildingLoader.Build` ran 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, so `BuildingId` stayed null. Then `cameraInsideBuilding=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=indoor` STILL 0 of 163,670 (with 125,476 `inside=True`). - **Why it got worse:** RR7.1 made `BuildingLoader.Build` use `_cellVisibility.AllLoadedCells` (every loaded cell, not just the drain) which stamped MORE cells with `BuildingId`. That made `cameraInsideBuilding=true` for more frames. But the registry-key lookup at the gate STILL missed (storage at `0xA9B4FFFF`, lookup at `0xA9B40000` — see RR7.2 below). So `cameraInsideBuilding=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, `MarkAndPunch` runs, `Draw(IndoorPass, cellIds: camCellIds)` runs. Building shells (cottage walls / inn walls — the `IsBuildingShell` entities) render. But cell-mesh entities (registered with `MeshRef(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 — `camCellIds` contains 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: ```csharp 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: ```csharp _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: 1. **Retail's algorithm doesn't fit modern GL.** Retail's `PView::DrawCells` at `acclient_2013_pseudo_c.txt:432709` uses 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. 2. **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 + `glMultiDrawElementsIndirect` mandatory. 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 at `src/AcDream.App/Rendering/Wb/`). Adopting WB's `EnvCellRenderManager` + `VisibilityManager` is the natural continuation. 3. **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. 4. **Same exact stack.** WB is MIT-licensed Silk.NET + .NET 10 + DatReaderWriter — verbatim our stack. No translation cost. 5. **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 `_currentEnvCellIds` from `_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 `_otherBuildings` from `_visibleBuildingPortals` filtering OUT buildings that contain `currentEnvCellId`. - For each other-building (per `_otherBuildings`): - Read back previous frame's occlusion query (`GetQueryObject(building.QueryId, ResultAvailable)`, `GetQueryObject(... Result)`). Update `building.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)`. ### Cleanup (lines 234-238) - `Disable(StencilTest)`, `StencilMask(0xFF)`, `ColorMask(true×3, false)`. ## Why our RR7 didn't match this 1. **No `envCellManager.Render(...)` call.** We routed cells through `Dispatcher.Draw(IndoorPass)`, which is per-GfxObj-batched, not per-cell. 2. **No separate transparency pass for cells.** Step 3's `DepthMask(false) + Render(Transparent)` was missing. 3. **No `sceneryShader.Bind()` between passes.** WB's algorithm assumes a specific shader is bound at each step; we never did. 4. **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. 5. **ColorMask alpha-bit pattern not preserved.** WB uses `ColorMask(true, true, true, false)` deliberately — alpha-bit OFF. Our outdoor branch's `Draw(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: 1. Read `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.cs` in full. 2. Identify its dependencies — likely `GlobalMeshBuffer`, `ObjectMeshManager` (already extracted), `TextureAtlasManager`, `IRenderManager`, `RenderPass`, `SceneData`. Extract any missing dependencies. 3. Copy `EnvCellRenderManager.cs` to `src/AcDream.App/Rendering/Wb/EnvCellRenderManager.cs`. 4. Adapt namespaces (`Chorizite.OpenGLSDLBackend.Lib` → `AcDream.App.Rendering.Wb`). 5. Resolve any references to types we don't have. Stub or extract as needed. 6. 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 by `EnvCellRenderManager.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: 1. **`PortalRenderManager.RenderBuildingStencilMask` mesh upload.** Does WB upload exit portal polygons differently than we do? Our `UploadBuildingPortalMesh` (Phase A8 RR6) might map cleanly to WB's expectation, or might need adjustment. 2. **`EnvCellRenderManager.Register` API.** What does it accept? Compare to our `_pendingCellMeshes[envCellId] = cellSubMeshes`. Identify the seam. 3. **Transparency pass.** WB's Step 3 has an `if (state.EnableTransparencyPass)` second `Render(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). 4. **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. 5. **`OutdoorScenery` EntitySet.** WB's Step 4 calls `sceneryManager.Render(pass1RenderPass)` and `staticObjectManager.Render(pass1RenderPass)` separately. We've collapsed both into `Draw(EntitySet.OutdoorScenery)`. Need to verify our `OutdoorScenery` partition 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) 1. **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. 2. **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. 3. **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. 4. **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. 5. **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: > > 1. `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239` (the RenderInsideOut algorithm we're porting verbatim) > 2. `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/EnvCellRenderManager.cs` (the manager to extract — 862 LOC) > 3. `references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/PortalRenderManager.cs` (the other dependency — extract the stencil-mask method + any infrastructure) > 4. `docs/architecture/worldbuilder-inventory.md` (what we've already extracted from WB and where it lives) > 5. `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`