# Render Pipeline Redesign — Full Staged Plan (2026-06-02) > **Read the master handoff first:** `docs/research/2026-06-02-render-pipeline-redesign-handoff.md` > (§2 evidence, §3 root cause, §5 retail target + porting checklist CL-A..G). > Then the 3 research docs. Then **BRAINSTORM** (Phase R0) before any code. ## The mandate (user, 2026-06-02 — non-negotiable, repeated so it's never lost) **FULLY WORKING outdoor + indoor + dungeon rendering. No flaps, no missing textures, no transparent walls, no terrain leaking into cellars, no entity/particle bleed. NO shortcuts, NO bandaids, NO quick fixes — if the architecturally-correct way is slower, take it. Refactor or redesign the whole pipeline if that's what it takes. Port from retail. Do more research mid-session rather than guess. Start with a brainstorm.** --- ## 1. The target architecture (retail-faithful — the ONE model) From the retail decomp (handoff §5; doc A). The single inversion that fixes everything: > **When the viewer is in an EnvCell, run ONLY `DrawInside` — the one PView portal flood. > Do NOT draw the outdoor world and then gate it. Visibility IS the cull: only the visible > cells and their per-cell objects render; the landscape (terrain/sky/rain) is pulled in > ONLY through clipped exit portals, followed by a conditional depth-only clear.** ``` RenderWorld(viewer): if viewer.cell is outdoor landcell: DrawOutside() # LScape: terrain + scenery + sky + weather (today's outdoor path) # buildings seen from outside render their interiors via DrawPortal (R5, outside-looking-in) else: # viewer in an EnvCell DrawInside(viewer.cell): # ONE flood — nothing else frame = ConstructView(cell) # PView BFS → cell_draw_list + per-cell clip + outside_view if frame.outside_view nonEmpty: DrawLScapeClippedTo(frame.outside_view) # terrain/sky/rain through the doorway ConditionalDepthOnlyClear(frame.outside_view) # Z only — never color → no blue hole for cell in frame.cell_draw_list (closest-first): DrawCellShell(cell, clip=frame.clip[cell]) # closed geometry: floor+walls+ceiling DrawCellObjects(cell, clip=frame.clip[cell]) # ONLY this cell's objects (statics/entities) DrawCellParticles(cell, clip=frame.clip[cell]) # ONLY this cell's particles ``` **Components to KEEP (research says these are correct):** `PortalVisibilityBuilder` (the BFS), `ClipFrame`/`ClipFrameAssembler`/`ClipPlaneSet`/`PortalView` (the clip machinery), `EnvCellRenderer` mesh/geometry path, `TerrainModernRenderer`, the WB mesh pipeline, the membership fix (`59f3a13`), the Stage-4 sky NDC-clip + doorway Z-clear, the diagnostic probes. **The work is RESTRUCTURING THE ORCHESTRATION + the entity/particle draw, not a from-scratch rewrite.** No stencil two-pipe, no `isInside` gate, no AABB grace-frame, no WB `RenderInsideOut` (handoff §6/§9). --- ## 2. The phases (each is visually-verifiable; retail-anchored; no bandaids) > Sizing note: this is a multi-session arc (M1.5 "indoor world feels right"). Each phase ends > at a **user visual gate** (the only acceptance that counts for a render seal — handoff §9 > lesson). Do NOT batch phases past a gate. The phases are ordered so each is independently > testable and the bleed is killed early. ### Phase R0 — Brainstorm + lock the design (FIRST, with the user) - `superpowers:brainstorming` the §1 architecture with the user. Confirm the single-flood model, the per-cell object draw, the keep/redesign split. Resolve the open questions in §3. - Write the detailed per-phase spec (`docs/superpowers/specs/2026-06-…-render-redesign-design.md`). - **No code.** Gate: design approved. ### Phase R1 — One visibility authority + kill the outdoor-scenery bleed **Retail anchor:** CL-B (render-root unification), CL-F (visibility is the cull). Fact 8 (§5.1). - Make `PortalVisibilityBuilder.Build`'s `OrderedVisibleCells` / `CellViews` the **single** visibility answer. Route the **entity** dispatch off it (the same `pvFrame` the shells/terrain use) instead of the parallel `CellVisibility.ComputeVisibilityFromRoot` BFS. - **Delete the `ParentCellId==null → return true` bypass** at `WbDrawDispatcher.cs:1756`. Outdoor scenery (houses/trees/stabs) is gated to `OutdoorVisible` (drawn only when an exit portal makes the outdoors visible, and then clipped to it). This is the unification, not a special-case patch. - Decommission the duplicate `CellVisibility` entity path (or make it return the identical set). - **Gate (visual):** standing in the cellar, **no houses/trees/outdoor stabs are visible.** The `[shell]`/`[vis]`/entity probes confirm one visibility set drives all geometry. ### Phase R2 — The binary render decision: indoor = `DrawInside` only **Retail anchor:** CL-B1 (single decision), `RenderNormalMode @ 0x453aa0`, fact 2 (§5.1). - Restructure `GameWindow.OnRender` so that when `CellGraph.CurrCell` is an EnvCell, the **full outdoor draw is NOT issued** — only the indoor flood runs. The outdoor terrain/scenery/sky are reachable only via the exit-portal path (R3). When outdoors, the existing outdoor path runs. - Remove the "draw outdoor world, then draw cell shells on top, gated" structure. The render root is the physics `CurrCell` (already wired, Stage 3); the *consequence* (only-DrawInside-when-inside) is the new part. - **Gate (visual):** indoors you no longer see the full-screen outdoor "world background"; you see the interior (walls/floor/ceiling) and, through the door, the outside (still rough until R3). ### Phase R3 — The seal mechanics (`DrawCells` faithful port) — THE seal **Retail anchor:** CL-D, `PView::DrawCells @ 0x5a4840` (the three-loop seal sequence), fact 5–7. - In the indoor flood, when `outside_view` is non-empty: draw `LScape` (terrain/sky/rain) **clipped to the doorway** (reuse the Stage-4 sky NDC-clip + the terrain OutsideView clip) → **conditional depth-only clear** (the Stage-4 Z-clear) scissored to the doorway → then the cells. - Verify cell shells are **closed** (floor + walls + ceiling from `drawing_bsp`); the `[shell]` evidence shows geometry is present — confirm the floor face is in the mesh and faces correctly. - Resolve the terrain *model*: terrain is drawn ONLY through the exit-portal clip, never as a floor under the interior. Remove the `TerrainClipMode.Skip`-as-floor-removal confusion. - Self-contained GL state per draw (memory `render-self-contained-gl-state`). - **Gate (visual):** the cottage interior is **fully sealed** — opaque walls, solid floor, ceiling, **sky + rain visible through the door only** (no blue hole, no full-screen), no terrain under the floor, no grey-floor. The cellar is sealed. ### Phase R4 — Per-cell object + particle clipping (no bleed) **Retail anchor:** CL-F, fact 8; `find_visible_child_cell @ 0x52dc50`; #104. - Make the object draw **per visible cell** (iterate `cell_draw_list`, draw each cell's objects clipped to that cell's region) instead of one global entity pass. Entities straddling a portal are portal-clipped. - Give particles a cell (`OwnerCellId` from the owning entity's `ParentCellId`) and clip them to the visible set (#104). - **Gate (visual):** no NPC / door / smoke bleed through walls; an NPC just outside the door is visible through the doorway but not through the wall. ### Phase R5 — Outside-looking-in (U.5) **Retail anchor:** CL-E, `DrawPortal @ 0x5a5ab0`, `ConstructView(CBldPortal) @ 0x5a59a0`, fact 9. - When outdoors and a building's door/window is in view, render the building's **interior through that portal's clip** (the mirror of DrawInside, entered from the outdoor cell). Same machinery. - **Gate (visual):** looking through the cottage door/window from outside shows the **sealed interior**, not transparent walls. ### Phase R6 — Dungeons **Retail anchor:** fact 10 (emergent), the `update_count` watermark (fact 12 — #95), CL-C5. - Validate the all-EnvCell / `seen_outside==0` / no-exit-portal path on a real dungeon: no terrain, no sky, sealed cells, BFS convergence (the watermark bounds the reprocesses — confirm #95 closed). - **Gate (visual):** a real dungeon is sealed, no terrain/sky, no FPS collapse from the BFS. ### Phase R7 — Polish + conformance - Resolve the `CullMode.Landblock→None` double-sided stopgap (the actual winding). - Textures: confirm no missing textures anywhere (the `[shell]` zh=0 evidence says good; verify across dungeons). - Conformance tests (headless): the PView frame product, the seal asserts (handoff §8 apparatus). - Update roadmap; flip the M1.5 milestone; memory notes. - **Final gate (visual):** cottage + dungeon + outside-looking-in, all sealed and seamless. --- ## 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 exit-portal-clipped pass? (Retail draws LScape — which includes scenery — through the clip.) - **Building shells from outdoors:** when outdoors, the cottage's *exterior* shell must draw (it's a building). Is that an EnvCell shell drawn from the outdoor root, or part of the outdoor scenery? Reconcile with R5 (outside-looking-in) so the exterior + the door-interior compose correctly. - **The `EnvCellRenderer` filter vs per-cell-clip:** today shells use one `Render(filter)` with per-cell clip slots. R4 wants per-cell object draw. Confirm the EnvCellRenderer + WbDrawDispatcher can both be driven per-cell from `cell_draw_list` without a global pass. - **Two cameras (eye vs player cell):** the U.4c flap fix roots visibility at the player cell while projecting from the eye. Confirm the redesign preserves that (the eye can be outside the player cell in 3rd person) — `find_visible_child_cell` for the camera child. - **Scope of the entity-draw restructure:** is per-cell object draw a refactor of `WbDrawDispatcher` or a new dispatch path? (Per CLAUDE.md: don't break the working mesh pipeline; restructure the orchestration around it.) ## 4. Risks - **Big restructure of the render loop** — do it behind the visual gates, one phase at a time; keep the outdoor path working throughout (it's the 99% case). - **Per-cell object draw vs MDI batching** — the modern dispatcher batches across entities; a naive per-cell loop could regress perf. Design the per-cell clip to preserve batching (the clip-slot SSBO already supports per-instance clip; the cull is the membership, not a per-cell draw call necessarily). - **Don't reintroduce the abandoned approaches** (handoff §9): no stencil two-pipe, no isInside gate, no AABB grace-frame. - **Get the user's eyes early** — every phase ends at a visual gate; never declare a seal off tests. ## 5. The no-shortcuts rules (enforce on every task) 1. If a task tempts a fast-but-wrong path, take the retail-faithful path; note the tradeoff 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.