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) <noreply@anthropic.com>
176 lines
12 KiB
Markdown
176 lines
12 KiB
Markdown
# 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.
|