docs(research): A8 RR7 reverted — full WB-port handoff for next session

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>
This commit is contained in:
Erik 2026-05-27 14:10:33 +02:00
parent 4fa3390592
commit 3e9ff7accb

View file

@ -0,0 +1,485 @@
# 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`