docs(spec): Phase A8 — render-frame restructure to WB-faithful order (design)

Brainstorm-approved design for the A8 R3.5 → restructure pivot. Replaces
the R3.5 v1+v2 frankenstein (terrain twice + depth-clear workaround) with
WB's RenderInsideOut order verbatim: skip initial sky+terrain when inside,
delete the depth-clear, add a stencil-gated sky step inside the indoor
branch so windows show real sky (closes R4 Issue B).

Unifies the two-flag asymmetry (cameraInsideCell lenient + cameraReallyInside
strict) into a single strict cameraInside flag via PointInCell. Grace
mechanism in CellVisibility stays alive for non-render consumers.

Six tasks ahead, in order:
  RR0 — pre-restructure falsification spike (Issues A + C on main?)
  RR1 — revert R3.5 v1+v2 (38d5374 + 2bfeafd)
  RR2 — restructure render frame to WB-faithful order
  RR3 — verify SkyRenderer doesn't toggle stencil state
  RR4 — visual verification matrix (cottage/cellar/inn/dungeon + transitions)
  RR5 — ship docs (close #78; file new follow-ups if pre-existing on main)

Next: superpowers:writing-plans to produce the per-task plan.

Note: the design references two predecessor docs that are currently
untracked in this worktree (entity-taxonomy + phase-a8-replan). Their
contents are read-stable on disk; committing them is a separate concern
(they belong to the prior session's work). The handoff doc this design
continues from is at f90fa2f.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-26 21:49:18 +02:00
parent f90fa2f863
commit 732f766d1b

View file

@ -0,0 +1,445 @@
# Phase A8 — Render-frame restructure to WB-faithful order (design)
**Date:** 2026-05-26
**Phase:** A8 — render-frame restructure (continuation after R3.5 v1+v2 pause)
**Status:** Design approved 2026-05-26. Ready for `superpowers:writing-plans`.
**Branch:** `claude/strange-albattani-3fc83c` (worktree)
**HEAD at start of restructure session:** `2bfeafd` (R3.5 v2)
**Predecessor docs (REQUIRED reading before execution):**
- [docs/research/2026-05-26-a8-r3.5-restructure-handoff.md](../../research/2026-05-26-a8-r3.5-restructure-handoff.md) — full story of why we paused; the architectural mismatch
- [docs/research/2026-05-26-a8-entity-taxonomy.md](../../research/2026-05-26-a8-entity-taxonomy.md) — approved entity-taxonomy fix-shape (already shipped as R1+R2)
- [docs/superpowers/plans/2026-05-26-phase-a8-replan.md](../plans/2026-05-26-phase-a8-replan.md) — the R1+R2+R3 plan; R1+R2+R3 already shipped, restructure replaces R3.5
- [references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs:73-239](../../../references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs) — the proven WB `RenderInsideOut` reference
---
## TL;DR
Replace the R3.5 v2 "frankenstein" render frame (initial terrain + depth-clear-if-inside + stencil pipeline, three workarounds layered on top of each other) with WB's `RenderInsideOut` order verbatim: skip initial sky+terrain when `cameraInside`, delete the depth-clear block, and add a stencil-gated sky step inside the indoor branch so windows show real sky (closing R4 Issue B). Unify the two gate flags (`cameraInsideCell` lenient + `cameraReallyInside` strict) into a single strict `cameraInside` flag computed via `PointInCell`. Pre-restructure falsification spike (RR0) determines whether R4 Issues A + C are pre-existing on `main` (out of A8 scope, file as separate issues) or A8-caused (expand A8 scope and re-brainstorm).
Six tasks: RR0 falsification spike → RR1 revert R3.5 v1+v2 → RR2 restructure render frame → RR3 verify sky-renderer state contract → RR4 visual verification matrix → RR5 ship docs. ~2.5 hours assuming RR0 doesn't trigger scope expansion.
---
## Brainstorm outcomes (the five settled questions)
**Q1 — initial terrain when inside:** WB-faithful (skip when `cameraInside`).
**Q2 — sky through windows:** add stencil-gated sky step between MarkAndPunch and the terrain re-draw.
**Q3 — Issue C (entry transparent floor):** defer to post-restructure investigation; file as separate issue if pre-existing.
**Q4 — gate-flag asymmetry:** unify on `cameraReallyInside` (renamed `cameraInside`); grace mechanism in `CellVisibility` stays alive for non-render consumers.
**Q5 — R3.5 v1+v2 mechanics:** revert as two new commits before restructure (clean diff against R3 baseline).
**Plus** pre-restructure RR0 spike: falsify R4 Issues A and C against `60f07bc` (R3 baseline) and `main` to settle whether they're A8-caused before committing the restructure.
---
## Architecture
### One new gate flag (replaces the two-flag split)
Computed once at the start of the per-frame render pass, next to the existing `visibility` computation:
```csharp
var visibility = _cellVisibility.ComputeVisibility(camPos);
bool cameraInside = visibility?.CameraCell is not null
&& CellVisibility.PointInCell(camPos, visibility.CameraCell);
```
This becomes the SINGLE source of truth for "are we rendering as if the camera is inside a cell?" Drives:
- Sky pre-scene gate (line ~7090)
- Initial terrain draw gate (line ~7115)
- Stencil branch entry (was at ~7174)
- Weather post-scene gate (line ~7260)
- Sky-PES debug call (line 7032)
Previous flags `cameraInsideCell` (lenient, grace-aware) and `cameraReallyInside` (strict, no grace) both DISAPPEAR — replaced by the single `cameraInside`.
`playerInsideCell` (line 7023, used for lighting) STAYS UNCHANGED — it has different semantics (third-person chase camera enters interiors before player body does; lighting must follow player) and uses `IsInsideAnyCell(_playerController.Position)` which is grace-independent.
The grace mechanism in `CellVisibility.FindCameraCell` (3-frame stickiness via `_cellSwitchGraceFrames`) **stays alive** — only the render-frame consumers stop benefiting from it. Non-render consumers (e.g. `IsInsideAnyCell` for player-side checks) may still depend on grace; auditing them is out of scope for this phase.
### Render frame when `cameraInside == true` (WB-faithful)
Matches `VisibilityManager.RenderInsideOut` Steps 14 verbatim:
```
1. Skip initial sky (gate inverted from `!cameraInsideCell` to `!cameraInside`)
2. Skip initial terrain (NEW gate: `if (!cameraInside) _terrain?.Draw(...)`)
3. NO depth-clear (block deleted)
4. MarkAndPunch — stencil bit 1 + depth=1.0 at camera-cell exit portals
5. IndoorPass — cell mesh + cell statics + building shells
(stencil OFF after MarkAndPunch cleanup; DepthFunc.Less; depth writes ON)
6. EnableOutdoorPass — StencilFunc.Equal(1, 0x01) read-only; DepthFunc.Less
7. Stencil-gated sky — _skyRenderer.RenderSky with DepthMask OFF
(sky color writes through punched depth=1.0 only at portal silhouettes;
DepthMask off so depth stays at the punch value, letting the next step's
terrain re-draw win the depth test wherever closer terrain exists)
8. Stencil-gated terrain re-draw — _terrain?.Draw with stencil still Equal(1)
(terrain at portal silhouettes overwrites sky color with terrain color
where terrain Z is closer than the punched 1.0)
9. Stencil-gated OutdoorScenery — WbDrawDispatcher with EntitySet.OutdoorScenery
(stabs/procedural at portal silhouettes; depth-tested against terrain)
10. DisableStencil — restores normal stencil/color/depth state
11. LiveDynamic — WbDrawDispatcher with EntitySet.LiveDynamic
(server-spawned entities, depth-tested against everything else)
12. Skip weather pass (line 7260, gate inverted to `!cameraInside`)
```
### Render frame when `cameraInside == false` (unchanged from pre-A8)
```
1. Sky
2. Terrain
3. Single Draw(set: All)
4. Weather
```
The `else` branch in the current R3.5 v2 implementation is preserved 1:1.
---
## Components
### Existing infrastructure consumed as-is (no changes)
- **`IndoorCellStencilPipeline`** (`src/AcDream.App/Rendering/IndoorCellStencilPipeline.cs`)
- `UploadPortalMesh(IEnumerable<LoadedCell>)` — triangle-fan portal mesh
- `MarkAndPunch(Matrix4x4 viewProjection)` — WB Steps 1+2 GL state machine
- `EnableOutdoorPass()` — stencil read Equal(1, 0x01); color on; depth normal
- `DisableStencil()` — restores normal stencil/color/depth state
- **`WbDrawDispatcher`** (`src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`)
- `EntitySet.IndoorPass` — cell mesh + cell statics + building shells (Q2-R2 taxonomy)
- `EntitySet.OutdoorScenery` — outdoor stabs + procedural
- `EntitySet.LiveDynamic` — server-spawned (player, NPCs, dropped items)
- `EntitySet.All` — pre-A8 behavior for the outdoor `else` branch
- **`CellVisibility`** — `PointInCell`, `FindCameraCell`, `ComputeVisibility`, grace mechanism. No changes.
- **`WorldEntity.IsBuildingShell`** (R1) — set at `LandblockLoader` from `LandBlockInfo.Buildings`. No changes.
### Net code surface in this restructure
Only `GameWindow.cs` changes structurally. The change is bounded to the render-frame block at lines ~70087242 (~234 lines, mostly comments + a single if/else). After the restructure:
- Two existing local declarations renamed (`cameraInsideCell` and `cameraReallyInside``cameraInside`).
- Two existing comment blocks pared back (the long R3.5 saga explanation deleted).
- One block deleted (the depth-clear `if (cameraReallyInside) _gl!.Clear(...)`).
- Two gate inversions (the sky pre-scene gate at ~7090, the initial terrain gate added at ~7115).
- One new step added (stencil-gated sky between MarkAndPunch and the terrain re-draw).
Estimated net diff: ~80 LOC removed, ~30 LOC added.
### Pre-flight check on `SkyRenderer.RenderSky`
The stencil-gated sky step relies on the inherited stencil state from `EnableOutdoorPass` surviving the `_skyRenderer.RenderSky` call. If `SkyRenderer` toggles `EnableCap.StencilTest` internally for any reason, our gate is lost.
RR3 reads `src/AcDream.App/Rendering/SkyRenderer.cs` and confirms no internal `Enable(StencilTest)`, `Disable(StencilTest)`, `StencilFunc`, `StencilOp`, or `StencilMask` calls. If clean, RR3 commits a verification note + a one-line comment in `GameWindow.cs` at the sky-step call site referencing the verified line range. If dirty, RR3 introduces a save/restore wrapper at the call site (cheap fixed-function state save).
---
## Data flow
### Indoor frame, step-by-step
Pre-frame: visibility computed; `cameraInside == true`; player-lb computed; particle-systems ticked.
```
[ glClear(Color|Depth) — happens earlier in the render frame ]
[ Step 1: skip sky pre-scene ]
[ Step 2: skip initial terrain ]
[ Step 3: no depth-clear ]
[ depth buffer state: cleared to 1.0 from the frame's glClear; no terrain yet ]
[ stencil buffer state: cleared by IndoorCellStencilPipeline.MarkAndPunch ]
Step 4: MarkAndPunch(viewProjection)
- GL state on entry: arbitrary (whatever the prior frame left)
- GL state on exit (per IndoorCellStencilPipeline cleanup):
StencilTest disabled, StencilMask 0xFF, ColorMask all, DepthMask on,
DepthFunc.Less
- Effects on buffers:
depth buffer = 1.0 at portal-silhouette pixels (rest unchanged from glClear)
stencil buffer = 1 at portal-silhouette pixels (rest = 0)
Step 5: IndoorPass — WbDrawDispatcher.Draw(set: IndoorPass)
- GL state: stencil OFF, DepthFunc.Less, DepthMask on
- Effects:
depth buffer writes wall/floor Z values at indoor pixels
these depths now occlude the punched 1.0 for any pixel where the
indoor mesh is closer (which is most of them, since the punch is
only at portal silhouettes which are themselves where indoor mesh
is typically NOT)
Step 6: EnableOutdoorPass()
- GL state on exit: StencilTest enabled, StencilFunc.Equal(1, 0x01),
StencilMask 0x00 (read-only), ColorMask color, DepthMask on,
DepthFunc.Less
Step 7: stencil-gated sky — _skyRenderer.RenderSky(...)
- Pre-step GL state change: DepthMask off
- Pre-step assumption: SkyRenderer does NOT touch stencil (verified by RR3)
- Effects:
color writes sky at pixels where stencil = 1 (portal silhouettes)
depth UNCHANGED (DepthMask off) — still at punched 1.0
- Post-step GL state change: DepthMask on (restore)
Step 8: stencil-gated terrain re-draw — _terrain?.Draw(...)
- GL state: same as step 7 minus the DepthMask off; depth writes on
- Effects:
color writes terrain at portal silhouettes WHERE terrain Z < 1.0 (i.e.
all of them in practice — terrain is closer than the far plane)
depth writes terrain Z at those pixels (overwriting the 1.0 punch and
the sky's writes)
RESULT: at portal silhouettes, sky shows ONLY where terrain doesn't
occlude (sky beyond the terrain horizon); terrain shows where it
does (near-field landscape through the window)
Step 9: stencil-gated OutdoorScenery — WbDrawDispatcher.Draw(set: OutdoorScenery)
- GL state: inherited from step 8
- Effects: scenery stabs + procedural depth-test against terrain at
portal silhouettes; what's not occluded by terrain or walls writes
correctly
Step 10: DisableStencil()
- GL state on exit: StencilTest disabled, normal masks, DepthFunc.Less
Step 11: LiveDynamic — WbDrawDispatcher.Draw(set: LiveDynamic)
- GL state: stencil off, depth normal
- Effects: server-spawned entities (player, NPCs, dropped items)
depth-test against everything else and write where visible
Step 12: skip weather pass
```
### Outdoor frame, step-by-step
```
Step 1: sky pre-scene — _skyRenderer.RenderSky(...) [existing call]
Step 2: terrain — _terrain?.Draw(...) [existing call]
Step 3: dispatcher single call — WbDrawDispatcher.Draw(set: All) [existing call]
Step 4: weather post-scene — _skyRenderer.RenderWeather(...) [existing call]
```
Outdoor path is structurally unchanged. The outdoor `else` branch in R3.5 v2 is preserved verbatim.
---
## Error handling
This is fixed-function GL state machine surgery. There's no error path in the traditional sense — wrong state → wrong pixels.
**Defensive measures in the design:**
1. **Pre-flight check (RR3) before relying on stencil-gated sky** — verify `SkyRenderer.RenderSky` doesn't touch stencil. If it does, wrap.
2. **GL state restoration after each step**`IndoorCellStencilPipeline.MarkAndPunch` already restores state on exit per Tasks 16 review. `EnableOutdoorPass` / `DisableStencil` symmetric. No cleanup gap.
3. **No-op when `_indoorStencilPipeline is null`** — keep the existing R3 guard at line 7174 (`&& _indoorStencilPipeline is not null`). Restructured branch falls through to outdoor path if the pipeline failed to initialize.
4. **Null-flow analysis preserved** — the existing R3 pattern of restating `visibility?.CameraCell is not null` inside the if-condition stays; lets the body use `visibility.CameraCell` without null-forgiving.
**No new try/catch.** Per CLAUDE.md "don't add error handling for scenarios that can't happen." GL state machine errors are debugger / RenderDoc territory, not runtime exception handling.
---
## Testing strategy
### Unit tests
**No new unit tests required for the restructure itself.** The restructure only changes GL state and call ordering in `GameWindow.cs`; these are visual-verification-only by nature.
Existing test surfaces that lock the non-GL bits:
- R1 LandblockLoader IsBuildingShell tagging — 2 tests
- R2 WbDrawDispatcher EntitySet partition — 7 tests
- Tasks 16 infrastructure — 26 dispatcher + 5 IndoorCellStencilPipeline + 2 PortalPolygons + 1 ProbeVisibility = 34 tests
All shipped, all passing in the documented flaky window. The restructure consumes them but doesn't change them.
**Possible new test (RR3 contingent):** if `SkyRenderer.RenderSky` requires a no-state-touch overload, one test asserting it doesn't enable/disable stencil. Likely not needed.
### Visual verification matrix (RR4)
| Scenario | Acceptance | Action if fails |
|---|---|---|
| Cottage interior (ground floor) | Walls solid; sky visible through windows | Investigate stencil-gated sky step or IndoorPass partition |
| Cottage cellar | Walls solid; no grass overlay through stairs from inside | Check if pre-existing on R3 baseline (use RR0 evidence) |
| Holtburg Inn (multi-room) | Walls solid; no see-through to adjacent rooms | Check IndoorPass partition; cross-cell-portal limitation (#102) |
| Dungeon | Corridor walls solid; indoor lighting; no terrain leak | Same as cottage interior; dungeons have no building shells |
| Exit transition (indoor → outdoor) | Clean: sky + terrain + entities depth-test correctly; no through-ground objects; no missing walls | If A reproduces and was confirmed pre-existing in RR0 → documented limitation, not blocker |
| Entry transition (outdoor → indoor) | Clean OR if Issue C reproduces and was confirmed pre-existing in RR0 → documented limitation, not blocker | Same as Issue C path |
### Risk register
| Risk | Likelihood | Mitigation |
|---|---|---|
| SkyRenderer toggles stencil internally, breaking stencil-gated sky | Low | RR3 pre-flight check; wrap if needed |
| Stencil-gated sky costs noticeable GPU time | Very low | One quad/skybox per frame; negligible against existing draw load |
| Removing depth-clear exposes unexpected Z artifacts | Low | RR4 catches; if widespread → revisit Q1 design; if isolated → file as separate issue |
| Issue A turns out to be A8-caused per RR0 | Medium | RR0 explicitly handles this branch; we re-brainstorm if it fires |
| Sky-through-window writes with DepthMask off but sky's own depth-test rejects fragments | Low | If sky uses DepthFunc.Less + far plane, may need DepthFunc.Always or DepthFunc.Lequal for the sky step |
| Cell-id flicker at doorway threshold after dropping grace gate | Very low | PointInCell epsilon + cached-cell fast-path provide hysteresis; only relevant if camera oscillates at AABB edge |
---
## Tasks
### Task RR0 — Falsification spike
**Goal:** determine whether R4 Issues A and C are pre-existing on `main` (out of A8 scope) or A8-caused (in scope).
**Steps:**
1. Launch `2bfeafd` (current HEAD). Stand inside Holtburg cottage. Walk outside. Re-enter. Reproduce or rule out:
- Issue A: exit transition "objects through ground + walls missing"
- Issue C: entry transition "floor transparent showing cellar + wrong texture"
2. `git checkout 60f07bc -- src/AcDream.App/Rendering/GameWindow.cs` (R3 baseline render frame only). Rebuild. Relaunch. Reproduce A + C.
3. `git stash` or checkout `main` in a side worktree. Rebuild. Relaunch. Reproduce A + C.
**Outcomes:**
- **All three reproduce** → pre-existing on `main`. File A + C as new ISSUES.md entries. Restructure goes ahead unchanged.
- **Only R3 + HEAD reproduce** → A8-caused (specifically by R3 work). Re-brainstorm A and/or C in the same session; expand RR2 scope.
- **Only HEAD reproduces** → R3.5 v1/v2 patches caused them. Revert clears them. Restructure goes ahead; RR4 confirms.
**Restore working tree to `2bfeafd` HEAD** before proceeding.
### Task RR1 — Revert R3.5 v1 + v2
```bash
git revert 2bfeafd 38d5374 --no-edit
```
Two new commits land. HEAD logically equivalent to `60f07bc` (R3 baseline) plus two revert commits. Build green expected.
### Task RR2 — Restructure render frame to WB-faithful order
**File:** `src/AcDream.App/Rendering/GameWindow.cs`
**Changes:**
1. Compute the unified gate next to the visibility call (~line 7011):
```csharp
bool cameraInside = visibility?.CameraCell is not null
&& CellVisibility.PointInCell(camPos, visibility.CameraCell);
```
Delete the previous `bool cameraInsideCell = visibility?.CameraCell is not null;` line.
2. Sky-PES debug call (line 7032): change `cameraInsideCell``cameraInside`.
3. Sky pre-scene gate (line 7090): change `if (!cameraInsideCell)``if (!cameraInside)`.
4. Add initial terrain gate (line 7115): wrap `_terrain?.Draw(...)` in `if (!cameraInside) { ... }`.
5. Delete the entire R3.5 comment block + `cameraReallyInside` declaration (lines ~71257148). Replace with a brief comment referencing the design doc.
6. Delete the depth-clear-if-inside block (lines ~71507155).
7. Restructure the indoor branch (lines ~71747233):
- Gate on `cameraInside && _indoorStencilPipeline is not null && visibility?.CameraCell is not null`
- Steps 411 per the data-flow section above
- Step 7 (new stencil-gated sky) inserted between EnableOutdoorPass and the terrain re-draw
8. Outdoor branch (`else` at line 7234): unchanged.
9. Weather post-scene gate (line 7260): change `cameraInsideCell``cameraInside`.
**Build:** `dotnet build -c Debug --nologo`
**Test:** `dotnet test --nologo` — failures within the documented 14-23 flaky window only.
**Commit:** `feat(render): Phase A8 RR2 — restructure render frame to WB-faithful order`
### Task RR3 — Verify SkyRenderer state contract
**File:** `src/AcDream.App/Rendering/SkyRenderer.cs` (read-only)
**Steps:**
1. `grep -n "Stencil\|StencilTest\|StencilFunc\|StencilOp\|StencilMask" src/AcDream.App/Rendering/SkyRenderer.cs`.
2. Confirm no matches (or all matches are inside conditional code paths that won't fire at our call site).
3. If clean: commit a verification note as a code comment at the stencil-gated sky call site in `GameWindow.cs` referencing the verified line range, no behavior change. Tiny diff.
4. If dirty: factor a save/restore wrapper at the call site (`_gl.GetInteger(StencilFunc...)` save, `_skyRenderer.RenderSky(...)`, restore). Or add a `RenderSkyKeepStencilState` overload. Adapt design accordingly.
**Commit:** `chore(render): Phase A8 RR3 — verify SkyRenderer doesn't touch stencil state`
### Task RR4 — Visual verification matrix
Per the matrix in the testing strategy section above. For each scenario, record PASS/FAIL with notes. Append to a follow-up handoff doc or a section of this design doc.
**Acceptance gate for shipping RR5:**
- All four building-type scenarios (cottage interior, cellar, inn, dungeon) PASS
- Sky-through-windows visible (Issue B closed)
- Exit transition: A reproduces only if RR0 confirmed pre-existing
- Entry transition: C reproduces only if RR0 confirmed pre-existing
If any scenario fails an acceptance criterion AND RR0 ruled out pre-existing → STOP, open a `/investigate` skill session for the failure.
### Task RR5 — Ship docs
**Files:** `docs/ISSUES.md`, `CLAUDE.md`
**Changes:**
1. `docs/ISSUES.md`:
- Move #78 to "Recently closed" with the restructure commit SHA.
- If RR0 found Issue A pre-existing → file as new issue (next sequential ID, likely #104).
- If RR0 found Issue C pre-existing → file as new issue (next sequential ID, likely #105 or #106).
- #102 (cross-cell-portal far-side visibility) and #103 (cellar terrain Z-fight from outside) remain open from RR2's predecessor work.
2. `CLAUDE.md`:
- Update the A8 paragraph from "PAUSED for restructure" → "SHIPPED 2026-05-2X" with the restructure commit list.
- Update "currently working toward" if M1.5 sub-target needs refinement.
**Commit:** `ship(render): Phase A8 — render-frame restructure SHIPPED`
---
## Alternatives considered and rejected
### Alt 1 — Hybrid: keep initial terrain unconditional + remove depth-clear
The Q1 alternative. Per the handoff's analysis, this BREAKS the #78 primary fix: cottage floor at world Z=+0.02 (EnvCell render lift) would lose to terrain at world Z=-0.01 (zFightTerrainAdjust nudge), re-exposing outdoor visibility through the floor. Rejected.
### Alt 2 — Status quo: keep R3.5 v2's frankenstein
CLAUDE.md's "no workarounds" rule explicitly forbids this kind of layered patch-on-patch design. The R3.5 v1 + v2 patches were band-aids; per the handoff, "Two patch attempts (R3.5 v1 and R3.5 v2) papered over parts of the symptom but kept producing new edge cases — the exact 'patching symptoms' anti-pattern CLAUDE.md and the predecessor revert handoff explicitly call out." Rejected.
### Alt 3 — Full WB port including BuildingPortalGPU + 3-bit stencil + occlusion queries
Matches WB completely, including Step 5 (cross-cell-portal visibility) and the 3-stencil-bit cross-building pipeline. Multi-week work. Already explicitly deferred in the R1+R2+R3 entity-taxonomy approval (filed as #102). Not in this restructure's scope.
### Alt 4 — Investigate retail's polygon-clip scissor approach before designing
Retail uses screen-space polygon clipping (not stencil) for visibility (per `acclient_2013_pseudo_c.txt:432709` `PView::DrawCells`). Different mechanism, equivalent observable behavior. The original Phase A8 investigation already settled on the WB stencil approach for acdream's modern GL pipeline. Re-opening this would be a major detour. Rejected.
### Alt 5 — Unify gate on `cameraInsideCell` (lenient) instead of `cameraReallyInside` (strict)
Q4 alternative. Would re-introduce the grace-frame issues that R3.5 v1+v2 were trying to patch (depth-clear runs while camera is outside, stencil pipeline marks a cell the camera isn't in). Rejected.
### Alt 6 — Unify AND delete the grace mechanism entirely
Q4 third option. Stronger: also rips out `_cellSwitchGraceFrames` from `CellVisibility`. Saves ~10 LOC but requires auditing all non-render consumers (player-cell logic, audio, lighting). Likely overkill for this restructure; deferred. The render frame doesn't depend on grace under the chosen design.
---
## Open follow-ups (post-RR5)
Highest ISSUES.md ID as of design time: **#101** (DONE 2026-05-25). RR5 files new issues from #102 onward. Tentative numbering:
- **#102** (to be filed by RR5) — Cross-cell-portal far-side visibility (WB Step 5 deferral). Source: the entity-taxonomy approval doc's "first ship approximation" of marking only camera's-own-cell portals.
- **#103** (to be filed by RR5) — Outdoor-to-indoor cellar terrain Z-fight (out-to-in artifact). Source: the predecessor handoff's "deep-cell terrain occlusion" note.
- **R4 Issue A** (filed by RR5 if RR0 confirms pre-existing on `main`) — exit "objects through ground + walls of other buildings missing."
- **R4 Issue C** (filed by RR5 if RR0 confirms pre-existing on `main`) — entry "transparent floor showing cellar + wrong texture."
- **Grace-mechanism cleanup** — Q4 third option; future cleanup phase if no consumer regresses.
- **Retail polygon-clip port** — long-term alternative to WB stencil if visual gaps surface; out of milestone scope.
RR5 reads `docs/ISSUES.md` at filing time to confirm the next available ID and adjusts the numbering above accordingly.
---
## Self-review notes
- **Placeholder scan:** no TBDs, no TODOs, no "similar to" or "TODO: figure out" strings. Every section has actual content.
- **Internal consistency:** task RR0 outcome branches are explicit; RR2 changes match the architecture section step-by-step; RR3 contingency is named; RR4 acceptance gate references RR0 evidence.
- **Scope check:** six tasks, well-bounded, ~2.5 hours estimated. The restructure is one render-frame block in one file. RR0 + RR3 + RR4 are non-code tasks. No scope sprawl.
- **Ambiguity check:** the `cameraInside` flag semantics (strict, no grace) are stated once and used consistently. The stencil-gated sky's `DepthMask off + on` discipline is stated explicitly. The outdoor `else` branch's verbatim preservation is named explicitly.