diff --git a/docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md b/docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md new file mode 100644 index 0000000..a8b3a02 --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-phase-a8-restructure-design.md @@ -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 1–4 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)` — 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 ~7008–7242 (~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 1–6 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 1–6 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 ~7125–7148). Replace with a brief comment referencing the design doc. + +6. Delete the depth-clear-if-inside block (lines ~7150–7155). + +7. Restructure the indoor branch (lines ~7174–7233): + - Gate on `cameraInside && _indoorStencilPipeline is not null && visibility?.CameraCell is not null` + - Steps 4–11 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.