# 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.