acdream/docs/research/2026-05-30-phase-u4-shipped-and-flap-handoff.md
Erik a3ecac5369 docs(render): Phase U.4 shipped (indoor rendering verified) + flap handoff
Phase U (U.1-U.4) shipped: the unified retail-faithful render pipeline replacing the
abandoned two-pipe split (#103). Indoor rendering VISUALLY VERIFIED — solid walls, no
terrain bleed, per-cell clip gating works. Two root-caused EnvCellRenderer
self-contained-GL-state fixes landed (uViewProjection stale-matrix; inherited
blend/depth-mask). Residual threshold "flap" (OutsideView instability from the per-frame
view-dependent portal BFS) is precisely root-caused via ACDREAM_PROBE_VIS and scoped to
U.4c (PVS / stab_list grounding, retail-faithful). Handoff captures the [vis] evidence,
the retail anchors, and the next-session pickup. U.5 (outdoor->building peering) + U.6
(dungeon scale) remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 09:16:35 +02:00

162 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase U.4 — shipped (unified pipeline + indoor rendering) + the threshold "flap" handoff (2026-05-30)
## TL;DR
The **unified retail-faithful render pipeline (Phase U) is built and shipped through U.4**,
and **indoor rendering is visually verified correct**: standing inside a Holtburg cottage /
cellar / inn, the cell-shell walls render solid, terrain no longer bleeds into interiors,
and the per-cell clip gating works. This replaced the abandoned two-pipe (inside/outside)
split (#103). Modern code, retail behavior.
**One residual remains: the threshold "flap"** — crossing a doorway (inside↔outside), terrain
+ building-shells briefly vanish leaving only un-gated geometry (particles + live entities)
over the bluish clear color. This is **precisely root-caused** (not a mystery): our per-frame
view-dependent portal BFS is unstable at multi-hop exit paths, so `OutsideView` flickers empty
→ terrain gets `Skip`-ped. The retail-faithful fix is **PVS (stab_list) grounding** — a focused
sub-step, **U.4c**. It was deliberately NOT attempted at the end of this (very long) session to
avoid thrashing a fragile cell-resolution area (the #98 lesson + the "don't push tired design
calls late-session" rule).
Visual gate status: **PASS for the indoor case; the seamless-threshold criterion is deferred to U.4c.**
---
## What shipped this session (Phase U, all committed on `claude/thirsty-goldberg-51bb9b`)
| Stage | What | Commit(s) |
|---|---|---|
| Spec + plan | Phase U design + implementation plan | `8601137`, `0f7b395` |
| **U.1** | Delete the two-pipe machinery (kept all audited fixes) | `3fc77be` |
| **U.2a** | Portal BFS: closest-first ordering + retail fixpoint termination | `d880775` (+ `306cdb0` review fixups) |
| **U.2b** | Reciprocal `OtherPortalClip` (+ CRITICAL fix: resolve by `other_portal_id`, not first-`OtherCellId`-scan) | `3916b2b``65781f5` |
| **U.2c** | `ClipPlaneSet` (NDC convex region → `gl_ClipDistance` planes, 8-cap + scissor fallback) | `a83b430` |
| **U.2d** | `ACDREAM_PROBE_VIS` visibility probe (in `RenderingDiagnostics`, Core) | `0b12583` |
| **U.3** | GPU gate: `gl_ClipDistance` in mesh+terrain shaders, `ClipFrame`, scoped clip bracket | `bf2e559``864fc5f` |
| **U.4** | Unified gated draw pass (`ClipFrameAssembler`, per-instance slots, `EnvCellRenderer.Render` wired, terrain Skip/Scissor/Planes) + `ResolveEntitySlot` tests | `7993e06``354ca74` |
| **U.4 fix 1** | `EnvCellRenderer.Render` uploads its own `uViewProjection` (was inheriting WbDrawDispatcher's → stale → seam flicker) | `d6d4671` |
| **U.4 fix 2** | `EnvCellRenderer.Render` sets its own BLEND + DepthMask per pass (was inheriting → opaque walls blended against clear color → "transparent walls") | `9be9547` |
Build green, App tests 151/151 throughout. Core failures are the documented pre-existing
static-leak flakiness (zero Core production files touched by U.4). **Branch is UNPUSHED**
push decision is the user's.
### Two reviews caught real CRITICALs (the process earned its keep)
- U.2b: reciprocal-portal resolved by scanning for the first `OtherCellId` match → mis-resolved
when a cell has two portals to one neighbour (real on Holtburg cellar `0x148``0x149`) →
hidden geometry. Fixed by plumbing the dat's `OtherPortalId` back-link.
- U.3: `GL_CLIP_DISTANCE0..7` enabled globally while 6 non-clip-writing shaders ran → undefined
behavior (benign on the dev driver, a portability landmine). Fixed by scoping the enable to
the world-geometry draws.
### The recurring lesson (now 3×): EnvCellRenderer must own its GL state
`EnvCellRenderer.Render` was dormant pre-U.4 (only the deleted two-pipe path called it). When
U.4 wired it into the live loop, it surfaced THREE inherited-GL-state bugs in sequence:
1. (2026-05-28, pre-U.4) cull state → "missing walls".
2. (U.4 fix 1) `uViewProjection` → stale-matrix seam flicker.
3. (U.4 fix 2) BLEND + DepthMask → opaque walls blending against the clear color.
A renderer that runs mid-frame after other consumers MUST establish every GL state it depends
on (matrix, blend, depth-mask, cull, front-face, A2C) — never inherit. See the memory note
`render-self-contained-gl-state`.
---
## The flap — root cause (evidence-based)
### Symptom
Crossing a doorway (the user's screenshot, inside→outside): terrain + building-shells + cell-
shells vanish, leaving only particles + live entities (NPCs/doors/items, slot 0 = no-clip) over
the bluish clear color (`glClearColor(0.05,0.10,0.18)`).
### Evidence — `ACDREAM_PROBE_VIS` `[vis]` lines, SAME cell across frames
```
root=0xA9B40171 cells=4 ids=[...,0xA9B40170] outside(polys=1,planes=4) ← window cell reached → terrain draws
root=0xA9B40171 cells=3 ids=[0xA9B40171,75,74] outside(polys=0,planes=0) ← window cell dropped → terrain SKIPPED
```
Over one cellar traversal: **10 empty-`OutsideView` frames vs 16 non-empty** — for the *same*
cells. The ground-floor cell `0xA9B40170` (which holds the window / `0xFFFF` exit portal)
**flickers in and out of the visible set** as the camera moves.
### Mechanism
1. Our `PortalVisibilityBuilder` runs a **per-frame, view-dependent** portal BFS. The
`CameraOnInteriorSide` portal-side test culls portals based on the camera's exact pose.
2. Near a portal boundary, a tiny camera move flips which portals pass the test → the multi-hop
path (cellar → ground floor → window) **breaks** in some frames.
3. When the exit-portal cell isn't reached, `OutsideView` is empty.
4. `ClipFrameAssembler` maps empty `OutsideView``TerrainMode.Skip` (the bleed fix) AND
`outdoorVisible=false` → building-shells culled.
5. Result: terrain + building-shells flap off whenever the exit path momentarily breaks.
### Why retail is seamless (the fix direction)
Retail grounds visibility in a **precomputed potentially-visible-set**: on cell entry,
`CEnvCell::grab_visible_cells` populates the `visible_cell_table` from the cell's `stab_list`
(a STABLE per-cell PVS), and `seen_outside` is a stable per-cell flag ("this cell is adjacent to
the exterior"). The per-frame `PView` clip refines WHERE things draw, but the SET of reachable
cells (and thus whether the exit portal is reachable) is stable. Our pure per-frame view-dependent
BFS has no such anchor → it flaps. Retail anchors: `CEnvCell::grab_visible_cells` ~311878,
`seen_outside` set in `find_cell_list` ~311044, `stab_list` on `CEnvCell`.
---
## U.4c — proposed scope (stabilize portal visibility)
Goal: make the visible-cell set (and therefore `OutsideView` / the terrain-draw decision)
**stable** so the threshold is seamless, the retail way — NOT a hysteresis/last-frame band-aid
(that's a workaround; forbidden).
Candidate approaches to settle in a brainstorm (do NOT jump to code — fragile area, #98 history):
1. **PVS / stab_list grounding (most retail-faithful).** Load each cell's `stab_list` (the dat
has it) into a stable per-cell visible set on cell entry; the per-frame BFS operates within /
is anchored by it, so the exit-portal cell never drops out. This is what makes retail seamless
by construction. Largest change; needs the stab_list dat read + integration.
2. **Stable `seen_outside` terrain-draw decision.** Decouple "should terrain draw" (stable: does
the camera cell statically reach a `0xFFFF` exit portal within its building's portal graph?)
from "where to clip it" (`OutsideView`). Still needs a clip region when `OutsideView`
momentarily empties (else bleed) — likely the raw exit-portal projection as a fallback.
3. **Investigate the specific instability first.** Why does `0xA9B40170` drop from the cellar's
BFS at certain angles — is it `CameraOnInteriorSide` on the stairwell portal being pose-brittle?
A more robust (epsilon / reciprocal-aware) side test might stabilize the common case before a
full PVS port. Cheapest; verify it's retail-faithful, not a fudge.
Recommendation: **brainstorm U.4c** (superpowers:brainstorming) starting from approach 1 vs 3,
using the `[vis]` probe as the apparatus. Build a stable visible-set; the clip stays per-frame.
### Also deferred (not the flap, separately tracked)
- **U.5** — outdoor-camera → building-interior peering (retail `outdoor_pview` / `DrawBuilding` /
`DrawPortal` / `ConstructView(CBldPortal)`). Standing OUTSIDE looking INTO a house still shows
no interior; that's U.5, not the flap. Open data dependency: render-side building-exterior
portal geometry (we carry `BldPortalInfo` physics-side).
- **U.6** — dungeon-scale validation; close/relate #95 + the residual #102 diamond-clip note.
- Minor leftovers flagged in the U.4 review: `AppendSlot` collapses the 3 `Count==0` states (U.4c
should branch `IsNothingVisible`/`UseScissorFallback` before calling it); orphaned
`LandblockEntriesWithoutAnimatedIndex`; dead `BuildingShellAnchorPass/Reject` counters.
---
## Apparatus (use this, evidence-first)
- **`ACDREAM_PROBE_VIS=1`** — `[vis]` line per cell change: `root` cell, visible-cell count + ids,
`outside(polys=N,planes=M)`, per-cell plane counts, scissor `fallbacks`. The flap shows as
`outside(polys=0)` frames interleaved with `outside(polys=1)` for the same cell. Owner:
`AcDream.Core.Rendering.RenderingDiagnostics.EmitVis`.
- Launch block: CLAUDE.md "Running the client" + `ACDREAM_PROBE_VIS=1`, pipe to a fresh log.
Read it with PowerShell `Select-String` (the Tee log is UTF-16) or `tr -d '\0' | grep` in bash.
## Next-session pickup prompt
```
Phase U.4c — stabilize portal visibility (fix the threshold "flap"). The unified render
pipeline (Phase U, U.1-U.4) is shipped and indoor rendering is visually verified correct.
The remaining issue is the doorway "flap": terrain + building-shells flicker off when
crossing the threshold. Root cause is in
docs/research/2026-05-30-phase-u4-shipped-and-flap-handoff.md — READ IT FIRST. It is the
per-frame view-dependent portal BFS being unstable at multi-hop exit paths (OutsideView
flickers empty → terrain Skip'd). The retail-faithful fix is PVS / stab_list grounding
(retail grab_visible_cells / seen_outside). This is a FRAGILE cell-resolution area (#98
saga) — start with superpowers:brainstorming on PVS-grounding vs a targeted side-test
stabilization; use ACDREAM_PROBE_VIS as the apparatus; NO workarounds (no hysteresis
band-aid). Do NOT touch the indoor rendering (it works). U.5 (outdoor→building peering)
and U.6 (dungeon scale) remain after U.4c.
```
## Git state
- All Phase U work committed on `claude/thirsty-goldberg-51bb9b`, **unpushed** (push is the user's call).
- Two `git stash` entries on the branch (`#98/#101` physics WIP, pre-triage backup) — preserve, do not drop.