From a3ecac53692f8693134ff569d5fbfde280a01833 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 31 May 2026 09:16:35 +0200 Subject: [PATCH] docs(render): Phase U.4 shipped (indoor rendering verified) + flap handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ...05-30-phase-u4-shipped-and-flap-handoff.md | 162 ++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 docs/research/2026-05-30-phase-u4-shipped-and-flap-handoff.md diff --git a/docs/research/2026-05-30-phase-u4-shipped-and-flap-handoff.md b/docs/research/2026-05-30-phase-u4-shipped-and-flap-handoff.md new file mode 100644 index 0000000..c7ae6e5 --- /dev/null +++ b/docs/research/2026-05-30-phase-u4-shipped-and-flap-handoff.md @@ -0,0 +1,162 @@ +# 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.