docs(render): REOPEN the render half — full retail-faithful redesign dossier (handoff + huge plan + 3 research docs)

The Phase W indoor seal did NOT land. The 2026-06-02 visual gate proved the interior render is fundamentally broken (#78: transparent walls, outdoor terrain + scenery entities bleeding in, grey floors, no outside-looking-in). Stage 4 (sky-through-door clip) was real but a top layer on a base that never sealed.

DECISIVE EVIDENCE (committed in the handoff): the PVS computes correctly AND the cell shells render correctly (opaque, textured, complete — the [shell] probe shows zero NOSNAP / zero missing-texture). The failure is the SEAL + three inconsistent gates — concretely the WbDrawDispatcher.cs:1756 ParentCellId==null -> return true bypass draws outdoor scenery indoors, and the indoor path draws the outdoor world then gates it instead of running ONLY DrawInside. Retail, when inside, runs ONE PView flood: visibility IS the cull; the landscape enters only through clipped exit portals + a conditional depth-only clear.

Dossier (per the user's mandate: NO shortcuts/bandaids, port from retail, redesign the whole pipeline if needed, brainstorm first):
- Master handoff (root cause + retail target + reusable-vs-redesign + apparatus + do-not-repeat + copy-paste pickup prompt).
- Huge staged redesign plan R0(brainstorm)->R1(one visibility authority, kill the bleed)->R2(indoor=DrawInside-only)->R3(the seal, DrawCells port)->R4(per-cell object/particle clip)->R5(outside-looking-in)->R6(dungeons)->R7(polish/conformance). Each ends at a user visual gate.
- 3 research docs: full retail render pipeline reference (705 lines, decomp-verified), acdream pipeline inventory + failure map, reference cross-check (WB two-pipe is the wrong model).

#78 promoted to the redesign. The 5 remaining Core test failures are pre-existing physics/collision bugs, none render-related.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-02 18:28:01 +02:00
parent b595cfbb9f
commit 21bf97ed35
6 changed files with 2394 additions and 2 deletions

View file

@ -0,0 +1,169 @@
# 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 57.
- 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
- **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.