From 7aca79f8eb3a78c0af064ee92ffb5e8d35a61ba6 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 19:18:59 +0200 Subject: [PATCH] =?UTF-8?q?docs(render):=20Phase=20R0=20=E2=80=94=20lock?= =?UTF-8?q?=20the=20render-redesign=20design=20spec=20(brainstorm=20outcom?= =?UTF-8?q?e)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the plan §3 open questions with the user this session: - object/entity/particle draw = LITERAL PER-CELL LOOP (retail DrawCells), not a global MDI batch with per-instance clip. Fidelity > perf > blast-radius. - sequencing = HOLISTIC: build the per-cell DrawInside directly; no intermediate global-pass gate-fix. First visual gate = sealed cottage interior, no bleed. - terrain in the seal = FAITHFUL: drawn only through the exit-portal clip, never as a floor under the interior. Inventory's 'relax Skip' suggestion REJECTED as a non-retail workaround; grey-floor = a sealing bug (verify cell mesh in R1). - WB mesh pipeline KEPT (per-cell draws from the global buffers, batched within a cell); two-camera invariant preserved (eye projects, player cell roots visibility). Phases (holistic): R1 unified per-cell DrawInside (the core) -> R2 outside-looking-in (DrawPortal) -> R3 dungeons -> R4 polish+cleanup. Each ends GREEN + a user visual gate. Retail anchors cited throughout (RenderNormalMode 0x453aa0, DrawCells 0x5a4840, etc). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...026-06-02-render-pipeline-redesign-plan.md | 7 + ...6-06-02-render-pipeline-redesign-design.md | 329 ++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md diff --git a/docs/superpowers/plans/2026-06-02-render-pipeline-redesign-plan.md b/docs/superpowers/plans/2026-06-02-render-pipeline-redesign-plan.md index 04f9ce3..eaf4f47 100644 --- a/docs/superpowers/plans/2026-06-02-render-pipeline-redesign-plan.md +++ b/docs/superpowers/plans/2026-06-02-render-pipeline-redesign-plan.md @@ -134,6 +134,13 @@ rewrite.** No stencil two-pipe, no `isInside` gate, no AABB grace-frame, no WB ` --- ## 3. Open questions for the R0 brainstorm + +> **RESOLVED 2026-06-02** in [`docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md`](../specs/2026-06-02-render-pipeline-redesign-design.md) §1. +> Outcome: object/entity/particle draw = **literal per-cell loop** (retail `DrawCells`), not global MDI; +> sequencing = **holistic** (build the per-cell `DrawInside` directly, no intermediate global-pass gate-fix); +> terrain in the seal = **faithful** (only through the exit-portal clip; the "relax Skip" suggestion is +> rejected as a workaround); WB mesh pipeline kept (per-cell draws from the global buffers); two-camera +> invariant preserved (eye projects, player cell roots visibility). The design spec is the locked authority. - **Outdoor scenery while a door is open:** when indoors looking out, the visible outdoor scenery (the cottage across the street) must draw — but clipped to the doorway. Does that come for free from "LScape through the exit portal" (terrain + scenery both), or does scenery need its own diff --git a/docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md b/docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md new file mode 100644 index 0000000..74520c6 --- /dev/null +++ b/docs/superpowers/specs/2026-06-02-render-pipeline-redesign-design.md @@ -0,0 +1,329 @@ +# Render Pipeline Redesign — Locked Design Spec (R0 outcome, 2026-06-02) + +> **This is the design locked in the R0 brainstorm.** It supersedes the open questions in +> [`docs/superpowers/plans/2026-06-02-render-pipeline-redesign-plan.md`](2026-06-02-render-pipeline-redesign-plan.md) §3 +> with concrete decisions, and is the input `writing-plans` turns into the implementation plan. +> +> **Read first (context, in order):** +> 1. [`docs/research/2026-06-02-render-pipeline-redesign-handoff.md`](../../research/2026-06-02-render-pipeline-redesign-handoff.md) — proven root cause (§2), the three-gate failure (§3), the retail target (§5). +> 2. [`docs/research/2026-06-02-retail-render-pipeline-full-reference.md`](../../research/2026-06-02-retail-render-pipeline-full-reference.md) — the retail PView pipeline + the `DrawCells` seal mechanics (the algorithm being ported). +> 3. [`docs/research/2026-06-02-acdream-render-pipeline-inventory-and-failures.md`](../../research/2026-06-02-acdream-render-pipeline-inventory-and-failures.md) — the concrete bugs (the `WbDrawDispatcher.cs:1756` bypass; the parallel BFS; the terrain Skip model). +> 4. [`docs/research/2026-06-02-render-reference-crosscheck.md`](../../research/2026-06-02-render-reference-crosscheck.md) — why WB's two-pipe stencil is the wrong model (do NOT reintroduce). + +--- + +## 0. Mandate (user, 2026-06-02 — non-negotiable) + +FULLY WORKING outdoor + indoor + dungeon rendering. No flaps, no missing textures, no transparent +walls, no terrain leaking into cellars, no entity/particle bleed-through, outside-looking-in works. +**No shortcuts, no bandaids, no quick fixes** — take the architecturally-correct path even if slower; +redesign the whole pipeline if needed; **port from retail** (the oracle is `docs/research/named-retail/`); +do more research mid-session rather than guess. + +--- + +## 1. Decisions locked in the R0 brainstorm + +These are the resolutions to the plan §3 open questions, decided with the user this session. They are +binding for the implementation plan. + +1. **Object / entity / particle draw = LITERAL PER-CELL LOOP.** A faithful port of retail + `PView::DrawCells`' per-cell loops (iterate `cell_draw_list` closest-first; draw each visible cell's + shell + objects + particles, clipped to that cell's portal-derived region). **NOT** the alternative + of keeping one global `glMultiDrawElementsIndirect` pass with per-instance clip slots. The user's + stated priority order: **retail-structural fidelity > perf > minimal blast-radius.** + +2. **Sequencing = HOLISTIC.** Build the per-cell `DrawInside` **directly**. Do **not** ship an + intermediate "fix the gate on the existing global pass" step. The first phase is the full per-cell + `DrawInside`; the first visual gate is the **sealed cottage interior with no bleed**. (Verification + with the probes + the user's eyes still happens at natural checkpoints *within* the per-cell build — + that is evidence discipline on the real architecture, not the rejected throwaway intermediate.) + +3. **Terrain in the seal = FAITHFUL.** Terrain (and outdoor scenery, and sky/weather) is drawn **only + through the exit-portal clip region**, never as a floor under the interior. The inventory doc's + "REDESIGN item #4" suggestion — *relax `TerrainClipMode.Skip` for `seen_outside=true` cottages so the + cellar floor stops going grey* — is **REJECTED as a non-retail workaround.** The "grey floor / grey + world in cellar" symptom is a **sealing bug** (the closed cell mesh is not covering those pixels: a + missing or back-facing floor polygon, or the GL clear color showing through a gap). The faithful fix + is to make the cell mesh seal; verified by the `[shell]` probe + a dat dump of the cellar EnvCell mesh. + +4. **WB mesh pipeline KEPT; orchestration restructured.** The global VAO/VBO/IBO, GfxObj decode, and + texture cache stay. What changes is that draws are issued **per visible cell** from those global + buffers (batched *within* a cell where the geometry allows), not one global batch across all entities. + "Keep the WB mesh pipeline, restructure the orchestration" still holds. + +5. **Two-camera invariant PRESERVED.** The 3rd-person eye drives the **projection** (`envCellViewProj`); + the **player's physics cell** (`CellGraph.CurrCell`) roots **visibility** and the portal-side test. + This is the U.4c flap fix and it is kept verbatim. + +--- + +## 2. The one model (the inversion) + +There is **one** top-level decision per frame — a faithful port of retail +`SmartBox::RenderNormalMode @ 0x453aa0` (pc:92635): + +``` +RenderWorld(viewer): + if clipRoot == null: # viewer in an outdoor LandCell (retail is_player_outside: id&0xFFFF < 0x100) + DrawOutside() # today's outdoor path: terrain + scenery entities + sky/weather. + # building INTERIORS reached via DrawPortal (R2 — outside-looking-in). + else: # viewer in an EnvCell (clipRoot != null) + DrawInside(clipRoot) # ONE PView flood — nothing else. The outdoor world is NOT drawn; + # it enters ONLY through clipped exit portals, inside DrawInside. +``` + +`DrawInside` is a faithful port of `PView::DrawInside @ 0x5a5860` → `ConstructView @ 0x5a57b0` → +`DrawCells @ 0x5a4840`: + +``` +DrawInside(cell): + frame = PortalVisibilityBuilder.Build(cell, playerPos, eyeViewProj) # KEEP — the PVS is correct + if frame.OutsideView non-empty: # an exit portal is in view + DrawLandscapeClippedTo(frame.OutsideView) # terrain + outdoor scenery entities + sky, thru the doorway + ConditionalDepthOnlyClear(frame.OutsideView)# Z only — never color → no blue hole + for cellId in frame.OrderedVisibleCells: # the per-cell loop (retail DrawCells loops 1-3) + clip = frame.CellViews[cellId] + DrawCellShell(cellId, clip) # EnvCellRenderer.Render(pass, {cellId}) — closed mesh + DrawCellObjects(cellId, clip) # ONLY this cell's entities, clipped to its region + DrawCellParticles(cellId, clip) # ONLY this cell's particles — solves #104 for free +``` + +**Visibility *is* the cull.** No global entity pass; no second visibility computation. The bleed cannot +happen by construction — the outdoor world is never iterated when inside (except the clipped doorway). +This is the inversion of the current bug: acdream draws the outdoor world and *then* gates a few cell +shells on top through three inconsistent gates (handoff §3); retail never visits the outdoor world when +inside. + +--- + +## 3. Keep / Build / Remove + +### KEEP (proven correct — handoff §4, inventory §5) +- `PortalVisibilityBuilder` — the PView BFS. Produces `OutsideView`, `CellViews` (per-cell NDC clip), + `OrderedVisibleCells` (closest-first), `CrossBuildingViews`. The single visibility authority. +- `ClipFrame` / `ClipFrameAssembler` / `ClipPlaneSet` / `PortalView` — the clip-plane machinery and the + per-cell NDC→GPU slot packing. (`ClipFrameAssembly` already exposes `CellIdToSlot`, `OutdoorSlot`, + `OutdoorVisible`, `TerrainMode`, `HasOutsideView`, `OutsideViewNdcAabb` — everything the seal needs.) +- `EnvCellRenderer` mesh/MDI/texture path, including `Render(pass, filter)` (a single-cell filter set + drives one cell — the hook the per-cell loop uses). +- `TerrainModernRenderer` (the renderer; only its caller's clip mode changes). +- The WB mesh pipeline (global VAO/VBO, GfxObj decode, `TextureCache`). +- The membership fix `59f3a13` (`[cell-transit]` confirms correct cell tracking — do NOT reopen). +- The Stage-4 sky NDC-clip + the conditional doorway Z-clear (`ce2edad`/`b595cfb`). +- All diagnostic probes (`ACDREAM_PROBE_CELL` / `_VIS` / `_SHELL` / `_FLAP`). + +### BUILD (the redesign) +- A new **`DrawInside` orchestrator** (the §2 per-cell loop) that replaces the current + "global terrain → global shells → global entity pass" structure in `GameWindow.OnRender` + (~7250–7610). +- The **binary top-level decision** (`clipRoot == null` → `DrawOutside`; else → `DrawInside` only). +- A **per-cell entity dispatch** issued from the global buffers, batched within a cell. +- A **per-cell particle draw** (each cell's particles in its clip scope). +- The **landscape-through-the-door** composition (terrain + outdoor scenery + sky clipped to `OutsideView`) + + the conditional Z-only clear. +- (R2) the **outside-looking-in `DrawPortal`** path (separate outdoor pview). + +### REMOVE (dead / divergent — scheduled in the final polish phase so it can't destabilize earlier work) +- The dormant WB-two-pipe scaffolding: `Building`, `BuildingLoader`, `ExitPortalPolygons` stencil-marking, + the occlusion-query state (`QueryId`/`WasVisible`), and the `IsShellScopedSet` / `BuildingShellAnchorCellId` + anchor machinery (`IsShellScopedSet` already returns `false` — U.1 deleted the live two-pipe). +- `CellVisibility` as a **rendering gate** — its `VisibleCellIds` set retires as the entity gate. Its + `CameraCell` / root-selection role (the indoor/outdoor root decision) **stays**. +- The `CullMode.Landblock → None` double-sided stopgap (`EnvCellRenderer.cs:~1216`) — replace with the + correct per-polygon winding once the seal is verified. + +--- + +## 4. The seal mechanics (`PView::DrawCells @ 0x5a4840` port) + +Two parts: the landscape-through-the-door block, then the per-cell loop. (Full retail verbatim: +research doc A §4.) + +### 4.1 Landscape through the door (only when `OutsideView` is non-empty) +- Draw the "landscape" clipped to the doorway's screen silhouette (`OutsideView`). Retail does this with + one `LScape::draw` (`PortalList = this`). In acdream the landscape is split across three renderers, so + this one step becomes: + - **terrain** (`TerrainModernRenderer`, gated by the clip-plane UBO in Planes mode, or `glScissor` to + the `OutsideView` AABB in Scissor mode), + - **outdoor scenery entities** (`ParentCellId == null`, clipped to `OutsideView` / `OutdoorSlot`), + - **sky / weather** (`SkyRenderer`, `gl_ClipDistance` against the same UBO — the Stage-4 path, kept). +- Then a **conditional depth-only clear** scissored to the `OutsideView` AABB + (`Clear(4, …)` — flag 4 = depth buffer only, retail pc:432731). Color is preserved (terrain painted + through the door stays); depth resets so interior geometry composites without z-fighting at the doorway + edge. **There is NEVER a color clear in the indoor path** — that is structurally why there is no blue hole. + +### 4.2 The per-cell loop over `OrderedVisibleCells` (closest-first; each cell clipped to `CellViews[cellId]`) +- **Shell** (retail DrawCells Loop 2 — `DrawEnvCell`): `EnvCellRenderer.Render(pass, {cellId})` draws the + cell's **closed** mesh (floor + walls + ceiling) from the cell's dat geometry. The ceiling and floor seal + *because they are authored in the dat mesh* — there is **no "cap the ceiling" step**. Doorways are genuine + holes in the mesh, so the landscape from §4.1 shows through them. (Retail Loop 1 stencils the exit-portal + openings; acdream's mesh-hole + the §4.1 Z-clear achieve the same composition — **R1 verifies** whether an + explicit portal stencil is needed or the mesh-hole + Z-clear suffice.) +- **Objects** (Loop 3 — `DrawObjCellForDummies`): only this cell's entities (`object_list` equivalent), + clipped to the cell's region. Live-dynamic entities (player/NPCs/items, `serverGuid != 0`) render + unclipped (depth only) per retail. +- **Particles:** only this cell's particles, in the cell's clip scope (closes #104 — no per-instance slot + needed because the draw is already per-cell). +- **Self-contained GL state:** each per-cell draw SETS every GL state it depends on (view-proj, blend, + depth-mask, cull, front-face, A2C) — never inherited (memory `render-self-contained-gl-state`; this bit + `EnvCellRenderer` three times in U.4). + +### 4.3 Why the seal holds (the four guarantees — retail doc A §4.5) +1. **No blue hole:** outdoors is drawn first (clipped to `OutsideView`); the only clear is Z-only + + conditional; color survives in the doorway. +2. **Sealed ceiling/walls/floor:** each visible cell's mesh is a closed box; portal holes are the only + openings. +3. **No outdoor bleed-in:** the landscape paints only through exit-portal clip regions; if `OutsideView` + is empty (dungeon, or facing away from the door), no terrain/sky is drawn at all. +4. **No object/particle bleed:** objects/particles are drawn per-cell, only for cells in + `OrderedVisibleCells`, clipped to the cell's region. + +--- + +## 5. The per-cell loop & batching (how the mesh pipeline is preserved) + +The user chose the literal per-cell loop over global MDI; this section pins down how that coexists with the +KEPT mesh pipeline so the plan does not regress perf catastrophically or break the working mesh path. + +- **Geometry storage is unchanged.** All cell shells and entity meshes live in the global VAO/VBO/IBO; each + batch references its slice via `BaseVertex` / `FirstIndex`. The per-cell loop issues draws against those + slices — it does not re-upload or re-pack geometry per cell. +- **Batched within a cell.** Each cell's draw still groups by mesh/material and issues an MDI call for that + cell's object set (per-cell MDI), rather than a draw call per object. So we lose *cross-cell* batching + (the retail-faithful cost the user accepted) but keep *within-cell* batching. A cottage interior is a + handful of visible cells, so the per-cell call count is small. +- **Clip is the cell's region, applied per-cell.** Because the loop is per-cell, the cell's clip region is + bound once before that cell's draws (clip-plane UBO / `gl_ClipDistance`), not per-instance. This is what + lets particles (no instanceID) be clipped — the #104 win. +- **`EnvCellRenderer`** is driven with a single-cell filter (`Render(pass, {cellId})`) per loop iteration, + or a small per-cell render entry is added if the snapshot model needs it (decided in the plan; the + existing filter path is the starting point). +- **Entity dispatch** becomes per-cell: instead of `WbDrawDispatcher.Draw(... visibleCellIds ...)` walking + all entities once, the orchestrator asks for each visible cell's entities (entities whose `ParentCellId` + == cellId) and issues that cell's batched draw. The outdoor-scenery entities (`ParentCellId == null`) are + drawn in the §4.1 landscape-through-door step, clipped to `OutsideView`, NOT in any interior cell's loop. + +**Perf note (risk, not a blocker):** per-cell dispatch raises draw-call count vs the single global MDI. The +N.6 baseline showed CPU dominates GPU by 30–50× and the GPU sits at ~3.6% of frame budget, so there is +headroom; a cottage is a few cells. If a complex interior or dungeon regresses, within-cell batching + +the closest-first order (early-Z) are the mitigations. Measure at the R1 gate; do not pre-optimize. + +--- + +## 6. Component-level design + +| Concern | Component | Change | +|---|---|---| +| Top-level decision | `GameWindow.OnRender` | New binary branch: `clipRoot == null` → `DrawOutside` (existing); else → `DrawInside` only. Remove the "global outdoor draw then shells/entities on top" structure. | +| Visibility | `PortalVisibilityBuilder` | **Keep.** Sole authority. Root at the player cell, project from the eye (§1.5 invariant). | +| Clip packing | `ClipFrameAssembler` / `ClipFrame` | **Keep.** Per-cell clip slots + `OutsideView` already produced; the per-cell loop consumes `CellViews` directly and/or the assembled slots. | +| Indoor orchestration | **new** `DrawInside` orchestrator (App layer) | The §2 per-cell loop. Owns: landscape-through-door, conditional Z-clear, the per-cell shell/object/particle draws. Lives in `AcDream.App.Rendering` (not `GameWindow.cs` per Code Structure Rule 1). | +| Cell shells | `EnvCellRenderer` | Driven per-cell (`Render(pass, {cellId})`). Keep the mesh/MDI/texture path. Self-contained GL state. | +| Entities | `WbDrawDispatcher` | Restructured: per-cell entity draw (entities by `ParentCellId == cellId`); outdoor scenery drawn in the landscape-through-door step. Delete the `:1756` `ParentCellId==null → return true` bypass and the dead `IsShellScopedSet` branch. | +| Particles | `ParticleRenderer` | Per-cell draw in the cell's clip scope (give particles a cell via the owning entity's `ParentCellId`). Closes #104. | +| Terrain | `TerrainModernRenderer` | Drawn only in the landscape-through-door step (Planes/Scissor clip). When `OutsideView` empty → not drawn (faithful Skip). | +| Sky/weather | `SkyRenderer` | Drawn in the landscape-through-door step, `gl_ClipDistance` clip (Stage-4, kept). | +| Root selection | `CellVisibility` | Keep `CameraCell` / root selection. Retire `VisibleCellIds` as a render gate. | +| Outside-looking-in | **new** `DrawPortal` path (R2) | Separate outdoor pview; `ConstructView(CBldPortal)` recursion; `DrawCells` the interior through the door's clip. | + +--- + +## 7. Phase plan (holistic) + visual gates + retail anchors + +> Each phase ends GREEN (build + `dotnet test`) **and** at a **user visual gate**. A seal is verified on +> screen (probes + the user's eyes), never off the test suite — the lesson that produced this redesign +> (handoff §9). Phases are NOT batched past a gate. + +### R1 — Unified per-cell `DrawInside` (the core) — THE make-or-break phase +**Retail anchors:** `RenderNormalMode @ 0x453aa0` (binary decision), `PView::DrawInside @ 0x5a5860`, +`ConstructView @ 0x5a57b0`, `DrawCells @ 0x5a4840` (the seal + the three per-cell loops), fact 8 +(visibility is the cull). +- Build the binary top-level decision (indoor → `DrawInside` only; the global outdoor pass is not issued). +- Build the per-cell loop: landscape-through-door (terrain + outdoor scenery + sky, clipped to `OutsideView`) + → conditional Z-only clear → per-cell closed shells → per-cell objects → per-cell particles. +- Delete the `WbDrawDispatcher.cs:1756` bypass (outdoor scenery now enters only via the landscape-through-door + step). Retire the `CellVisibility.VisibleCellIds` render gate. +- **Verify the cell mesh seals** (the grey-floor investigation): `[shell]` probe + a dat dump of the cellar + EnvCell mesh; confirm floor + walls + ceiling are present and front-facing; decide stencil-vs-mesh-hole for + the doorway (§4.2). +- Internal checkpoints (probes + eyes, on the real per-cell architecture): (a) sealed shells + landscape-thru-door; + (b) per-cell objects (no entity bleed); (c) per-cell particles (no smoke bleed). +- **Gate (visual):** Holtburg cottage + cellar — sealed interior (opaque walls, solid floor, ceiling), + sky/rain through the door only, **no blue hole, no terrain under the floor, no grey floor**, no + entity/scenery/particle bleed. + +### R2 — Outside-looking-in (`DrawPortal`) +**Retail anchors:** `PView::DrawPortal @ 0x5a5ab0`, `ConstructView(CBldPortal) @ 0x5a59a0`, fact 9. +- On the outdoor path, for each visible building door, run a separate `outdoor_pview`: + `ConstructView(CBldPortal)` (side-test the door plane, clip to the opening) → recurse into the interior → + `DrawCells` the interior through the door's clip. Same machinery as `DrawInside`. +- **Gate (visual):** standing in the street facing the cottage door/window, the **sealed interior** renders + through the opening — not transparent walls. + +### R3 — Dungeons +**Retail anchors:** fact 10 (emergent — all-EnvCell, `seen_outside == 0`, no exit portals), the +`update_count` watermark (fact 12 / #95), `grab_visible_cells @ 0x52e220` (landscape iff `seen_outside`). +- Validate the all-EnvCell path on a real dungeon: `OutsideView` stays empty → no terrain/sky by + construction; sealed cells; the watermark BFS converges (confirm #95 closed — no FPS collapse). +- **Gate (visual):** a real dungeon is sealed, no terrain/sky, no FPS collapse from the BFS. + +### R4 — Polish + cleanup + conformance +- Resolve the `CullMode.Landblock → None` double-sided stopgap (the real per-polygon winding). +- Remove the dormant WB-two-pipe scaffolding (`Building`, `BuildingLoader`, stencil/occlusion, anchor machinery). +- Headless conformance tests (G1–G4 below). Confirm no missing textures across dungeons. +- Update the roadmap; flip the M1.5 milestone; memory notes. +- **Gate (visual):** cottage + dungeon + outside-looking-in, all sealed and seamless. + +--- + +## 8. Risks + +- **R1 is a large phase** (the holistic choice). Mitigation: build behind internal probe/eyes checkpoints on + the real per-cell architecture; keep the outdoor path working throughout (the 99% case is unaffected — the + binary branch leaves `DrawOutside` intact). +- **Per-cell draw-call count** vs the single global MDI (§5). Mitigation: within-cell batching + closest-first + early-Z; measure at the R1 gate; the N.6 baseline shows large CPU/GPU headroom. +- **The grey-floor cause is unconfirmed** — it may be a missing dat floor polygon, a winding/cull issue, or a + clear-color gap. Mitigation: evidence-first in R1 (probe + dat dump) before any fix; do **not** relax the + faithful terrain Skip (decision §1.3). +- **Do not reintroduce the abandoned approaches** (handoff §9): no stencil two-pipe, no `isInside` gate, no + AABB grace-frame for the visibility root. + +## 9. No-shortcuts rules (enforced on every task — plan §5) +1. A fast-but-wrong path is rejected for the retail-faithful path; the tradeoff is noted in the commit. +2. No suppression flags, grace periods, or `if (problem) return early` guards at a symptom site. +3. Every AC-specific behavior cites a retail decomp anchor (address + pseudo-C line). +4. Mid-session research over guessing — if the retail behavior is unclear, read the decomp / attach cdb. +5. Each phase ends GREEN (build + tests) AND at a user visual gate. The seal is verified on screen. + +## 10. Conformance / acceptance gates (headless asserts — retail doc A §7 CL-G) +- **G1 (cottage, `seen_outside`):** interior sealed; sky/rain through the door; no blue hole; no transparent + walls; no bleed. `OutsideView` non-empty ⇒ the landscape-through-door step runs. +- **G2 (dungeon, `seen_outside == 0`):** sealed; no terrain/sky; `OutsideView` empty ⇒ landscape step NOT run; + BFS converges (watermark) without blowup (#95). +- **G3 (outside-looking-in):** facing an open cottage door from the street, the interior renders through the + doorway (R2). +- **G4 (invariants):** PVS root id == physics `CurrCell.Id` every frame; a cell receiving two clip slices is + processed once per slice (watermark); eye drives projection, player cell roots visibility. + +## 11. Decomp anchor index (verified in the research docs this session) +``` +SmartBox::RenderNormalMode 0x00453aa0 pc:92635 binary decision (DrawInside vs LScape::draw) +SmartBox::is_player_outside 0x00451e80 pc:90996 low-word objcell_id < 0x100 +CellManager::ChangePosition 0x004559b0 pc:94601 keep/release landscape on seen_outside +CEnvCell::grab_visible_cells 0x0052e220 pc:311878 self+stab; landscape iff seen_outside (@311893) +PView::DrawInside 0x005a5860 pc:433793 +PView::ConstructView(CEnvCell) 0x005a57b0 pc:433750 the BFS worklist +PView::ConstructView(CBldPortal) 0x005a59a0 pc:433827 exterior→interior recursion (outside-looking-in) +PView::InitCell 0x005a4b70 pc:432896 per-portal sidedness; update_count = view_count +PView::ClipPortals 0x005a5520 pc:433572 exit portal → outside_view (@433662); interior → OtherPortalClip +PView::AddViewToPortals 0x005a52d0 pc:433446 enqueue neighbours / SetOtherSeen +PView::DrawCells 0x005a4840 pc:432709 LScape-thru-door + Z-clear + 3 per-cell loops +PView::DrawPortal 0x005a5ab0 pc:433895 outside-looking-in entry +CEnvCell::find_visible_child_cell 0x0052dc50 pc:311397 point → child cell via portals/stab_list (camera child) +LScape::draw 0x00506330 terrain+sky; clipped via Render::PortalList +portal_view_type.update_count acclient.h:32346 watermark — BFS convergence (#95/#102) +CCellStruct drawing_bsp acclient.h:32275 closed cell mesh (floor+walls+ceiling); no cap step +CCellPortal.other_cell_id acclient.h:32300 0xFFFFFFFF (low 0xFFFF) ⇒ EXIT PORTAL +```