acdream/docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md
Erik 75b1df9cc3 docs: abandon two-pipe render approach; scope Phase U (unified retail-faithful pipeline)
Decision (2026-05-30, with user): the WB-inherited two-pipe (inside/outside) render
split is the root cause of the indoor seam bugs (flap, missing/transparent walls,
terrain bleed) and cannot be seamless. Abandon A8/A8.F (#103); build ONE unified
pipeline driven by retail's PView portal visibility — seamless by construction. The
2026-05-30 camera-collision + physics viewer-cap work is kept (retail-faithful, but a
detour from the seam fix). New Phase U scoped; #103 superseded; CLAUDE.md / roadmap /
milestones updated; full decision + scope + next-session pickup prompt in
docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 11:35:41 +02:00

12 KiB
Raw Blame History

Unified retail-faithful render pipeline — decision, scope, and handoff (2026-05-30)

TL;DR (the decision)

We are abandoning the two-pipe (inside / outside) rendering approach and committing to a single, unified, retail-faithful render pipeline built around retail's portal-visibility view (PView). Modern code, retail behavior. This is a new roadmap phase — Phase U (Unified Render Pipeline) — and it is milestone-scale work, not a patch.

Why: acdream inherited a two-pipe render structure from WorldBuilder (WB) — a normal outdoor draw plus a separate flat RenderInsideOut stencil pass toggled on cameraInsideBuilding (GameWindow.cs ~7345). That split is the root cause of every indoor/outdoor seam bug (the "flap", missing/transparent walls, terrain bleeding into interiors). Retail has no such split. Retail renders through one portal-visibility traversal: starting from whatever cell the camera is in, it walks recursively through portals (doors, windows, cell openings), builds a screen-space clip region per opening, and draws every visible cell — indoor and outdoor — in one pass. There is no "am I inside a building?" branch, so transitions are seamless by construction.

The A8.F effort tried to graft retail's recursive clip on top of WB's two-pipe stencil (a CPU-built NDC mask bridging the two pipes). That hybrid is inherently fragile and failed its visual gate (issue #103). You cannot make two pipes hand off seamlessly at a doorway; retail avoids the entire bug class by never splitting.

This was confirmed in collaboration with the user, who correctly identified that the whole direction was wrong: "in retail there is totally seamless transitions between out and in… it's like we are on the wrong path here." Correct.


What this session actually shipped (the salvage)

This session was originally a "camera collision" effort (a reframing of the #103 failure onto the camera eye — itself a detour). The camera work is real, retail-faithful, and kept, but it is not the fix for seamless transitions:

  • Swept-sphere camera collision (retail SmartBox::update_viewer, 0x00453ce0): CameraDiagnostics.CollideCamera (default on), ICameraCollisionProbe + PhysicsCameraCollisionProbe (reuses PhysicsEngine.ResolveWithTransition, retail viewer_sphere 0.3 m, init_object(player, 0x5c) = IsViewer|PathClipped|FreeRotate|PerfectClip), wired into RetailChaseCamera (collide into a separate published eye — never the damped sought eye — to avoid the wall-press oscillation), GameWindow wiring + a Camera-menu toggle.
  • Physics fix (retail-faithful): viewer/sight sweeps bypass acdream's 30-step safety cap in Transition.FindTransitionalPosition (retail find_transitional_position has no cap; calc_num_steps has a state & 4 viewer branch). Gate: && !ObjectInfo.IsViewer.
  • Commits 69c7f8daae5300 (plus the design spec/plan docs).

Camera-collision residual nits (minor, deferred — NOT blockers):

  • Eye can clip ~0.3 m into a wall and reveal outside (near-clip plane 1 m vs collision radius 0.3 m — tuning).
  • Residual non-retail-smooth feel at walls (the gross oscillation is fixed; tuning remains).

These are polish; revisit after the pipeline lands (or never — they're cosmetic).


Git state at handoff

  • Branch claude/strange-albattani-3fc83c was 239 commits ahead of main (both forked at Phase O, 2256006, 2026-05-21). The branch carries ~9 days of good work since Phase O: A6 physics, issues #98/#100/#101, A7 lighting, the A8/A8.F rendering arc, and the camera work — all of it.
  • Per user decision (2026-05-30): the whole branch was merged into main so none of that work is lost. The dormant, gated-off A8 two-pipe rendering rides along and is deleted as Task 1 of Phase U (see below). The user pushes main to remotes.
  • The #98/#101 physics WIP from a prior session is preserved in git stash on this branch (a subagent had git stash apply-ed it mid-session; it was re-stashed). Do not lose it.

Phase U — Unified Render Pipeline (scope / design sketch)

This is a scope sketch to start a proper brainstorm + spec from — not a finalized design. The next session brainstorms it.

Goal

One render path. The camera's current cell is the root of a per-frame portal-visibility traversal that yields (visible cells, per-cell screen-space clip region); the renderer draws all visible geometry (indoor cells, outdoor cells, entities, terrain) in a single pass gated by that visibility. No cameraInsideBuilding branch. No RenderInsideOut stencil pass. No outdoor-vs-indoor toggle. Seamless in/out by construction.

Retail oracle (the thing to port)

  • PView::ConstructView (decomp 433750), PView::ClipPortals (433572), PView::GetClip (~432344) — the recursive per-portal screen-space clip-region BFS.
  • CEnvCell::find_visible_child_cell (acclient_2013_pseudo_c.txt:311397, 0x0052dc50) — per-cell portal-visible-child resolution; call site :280028.
  • RenderDeviceD3D::DrawBlock (~430027) — the render loop the visibility chain feeds.
  • See the project memory note indoor-portal-visibility-wb-vs-retail for why WB cannot express per-portal clipping and the oracle is retail PView.

What to KEEP (do not re-port)

  • The WB-derived mesh/dat pipeline is fine and stays: ObjectMeshManager, WbMeshAdapter, WbDrawDispatcher, terrain (TerrainModernRenderer, LandblockMesh), DatCollection, texture decode. Phase U is about visibility + draw orchestration, not mesh extraction.
  • The camera collision and physics work from this session.
  • Cell data: CellVisibility (FindCameraCell, PointInCell, portal data), PhysicsDataCache cell structures.

What is likely SALVAGEABLE from the failed A8.F (verify, don't assume)

The A8.F CPU clip-builder pieces are unit-test-correct (the integration is what failed). They may feed a unified draw directly:

  • PortalProjection (GL near-plane clip), ScreenPolygonClip (2D convex intersection), ViewPolygon/CellView (clip-region model), PortalVisibilityBuilder (recursive portal-clip BFS producing per-cell OutsideView). The failure was the two-pipe stencil graft around them (the CPU NDC mask gating a separate outdoor pipe), not the clip math. A unified pipeline can likely reuse the builder to produce per-cell clip frames and gate one pass.

What to DELETE (Task 1 — clear the deck)

The dead two-pipe rendering, so the unified path is built clean, not bolted on:

  • RenderInsideOutAcdream and the cameraInsideBuilding branch in GameWindow.cs (~7345+), the IndoorCellStencilPipeline / MarkAndPunchNdc stencil graft, the Job-A/B decouple, the ACDREAM_A8_INDOOR_BRANCH kill-switch, the EnvCellRenderer WB RenderInsideOut port (f9a644a lineage).
  • Keep EnvCellRenderManager's mesh path if the unified draw needs it; delete only the inside-out visibility/stencil machinery.
  • Audit before deleting: some A8 commits also fixed real bugs (e.g. BuildingId stamping, pool aliasing 9559726) — keep those.

Approach sketch (for the brainstorm to refine)

  1. Visibility pass (CPU, GL-free, testable): from the camera cell, recursive portal BFS → ordered set of visible cells, each with a screen-space clip polygon (intersection of the portal openings along its chain). This is retail PView::ConstructView. Likely reuses the salvaged A8.F PortalVisibilityBuilder family.
  2. Draw pass (single, unified): for each visible cell (front-to-back), draw its geometry + entities clipped to its clip region (scissor or stencil-per-cell, retail uses a clip rect/region). Outdoor cells are just cells in this set — no special path. Terrain is drawn per visible outdoor cell, gated the same way.
  3. No branch: the camera being indoors vs outdoors changes only which cell is the root, not the algorithm.

Key risks / lessons (do not repeat)

  • Do not graft retail recursion onto WB's flat two-pipe stencil — that's what #103 was. Build the unified pass; don't bridge two pipes.
  • Unit tests on synthetic visibility data did not catch #103 — only the visual gate did. Visual verification at the cottage/cellar/inn/dungeon is the real acceptance. Build a runtime visibility probe early (ACDREAM_PROBE_VIS) and validate against live frames, not just synthetic fixtures.
  • A CPU-built mask gating ALL outdoor geometry is fragile. The unified pass should gate per-cell at draw time (scissor/stencil per visible cell), close to how retail clips, rather than one global mask.
  • The camera is not the fix. That reframing cost this session; the fix is the visibility architecture.

Success criteria (visual)

  • Walk Holtburg cottage → cellar → out the door: no flap, walls solid, no terrain bleed, seamless threshold crossing from any camera angle/zoom.
  • Holtburg Inn: no outdoor stabs/terrain visible through the floor/walls (closes #78).
  • Dungeon via Town Network portal: visibleCells stays sane (~415), no other-dungeon geometry (closes/relates #95).
  • No regression to outdoor rendering (the default game today).

Next-session pickup prompt

We are building Phase U — a single unified retail-faithful render pipeline (retail
PView portal-visibility), abandoning the WB-inherited two-pipe (inside/outside) split
that caused the indoor seam bugs (the flap, missing/transparent walls, terrain bleed).
The decision + full scope is in
docs/research/2026-05-30-unified-render-pipeline-decision-and-handoff.md — READ IT FIRST,
then the project memory note "indoor-portal-visibility-wb-vs-retail" and the #103
failure handoff (docs/research/2026-05-29-a8f-visual-gate-failure-handoff.md).

State both altitudes:
  Currently working toward: M1.5 — Indoor world feels right.
  Current phase: U (Unified Render Pipeline). This supersedes the abandoned A8/A8.F
  two-pipe approach (#103).

Start with superpowers:brainstorming to design the unified pipeline (do NOT jump to
code). The scope sketch in the handoff doc is the input. Key decisions to settle in the
brainstorm: (a) reuse the salvaged A8.F clip-builder (PortalProjection/ScreenPolygonClip/
ViewPolygon/PortalVisibilityBuilder — unit-test-correct) vs fresh port; (b) per-cell
clip mechanism (scissor rect vs stencil-per-cell) matching retail's GetClip; (c) how
terrain + outdoor entities become "just cells" in the visible set.

Task 1 of implementation is to DELETE the dead two-pipe code (RenderInsideOutAcdream,
the cameraInsideBuilding branch, IndoorCellStencilPipeline/MarkAndPunchNdc, the
ACDREAM_A8_INDOOR_BRANCH kill-switch) to clear the deck — but audit first; some A8
commits fixed real bugs (BuildingId stamping, pool aliasing) that must be kept.

Retail anchors: PView::ConstructView ~433750, ClipPortals ~433572, GetClip ~432344,
CEnvCell::find_visible_child_cell :311397, RenderDeviceD3D::DrawBlock ~430027.

Keep: the WB mesh/dat pipeline (ObjectMeshManager/WbDrawDispatcher/terrain), the camera
collision + physics from the 2026-05-30 session. Visual verification at Holtburg
cottage/cellar/inn + a portal dungeon is the acceptance gate — unit tests did not catch
#103.

Preserve the git stash on the branch (#98/#101 physics WIP).

Reference index

  • Decision context / why WB can't do it: project memory indoor-portal-visibility-wb-vs-retail.
  • #103 failure detail: docs/research/2026-05-29-a8f-visual-gate-failure-handoff.md.
  • Camera-collision work this session: docs/superpowers/specs/2026-05-29-a8f-camera-collision-design.md, docs/superpowers/plans/2026-05-29-a8f-camera-collision.md.
  • A8.F portal-frame port (the failed two-pipe graft): docs/superpowers/specs/2026-05-29-phase-a8f-portal-frame-visibility-design.md.
  • Retail decomp: docs/research/named-retail/acclient_2013_pseudo_c.txt.
  • WB visibility reference: references/WorldBuilder/Chorizite.OpenGLSDLBackend/Lib/VisibilityManager.cs.