# Handoff — Indoor flicker/void ROOT CAUSE confirmed (decomp + live cdb); 3-part retail-faithful fix planned — 2026-06-05 (PM) > **Canonical pickup for the next (fix) session. Read this FIRST.** This session did the diagnosis > the previous "core inside render (R1)" handoff asked for, and it landed somewhere different than that > handoff predicted. The indoor **bluish void + grey/texture flicker** is NOT a missing per-cell flood > port — R1's per-cell `DrawInside` is built and the cellar/ceiling seal correctly. The residual is > **camera/viewer-cell instability at cell boundaries**, confirmed by both the named-retail decomp AND a > live cdb capture of retail. The fix is a **3-part retail-faithful port** (camera boom stability + > viewer-cell dead-zone + w-space portal clip), de-risked and ready to plan + implement. > Branch `claude/thirsty-goldberg-51bb9b`. PowerShell on Windows; launch logs are UTF-16. --- ## 0. TL;DR - **The premise we started with was stale.** "Cellar floor drops / R1 incomplete" — actually R1's per-cell `DrawInside` + binary inversion already exist (commit `c4fd711`, 2026-06-02) and the cellar **is sealed** (user visual-verified T1 this session). The flood reaches the cellar; the shell draws. - **The real bug is two faces of ONE root cause — visibility metastability at cell boundaries:** 1. **Flicker (grey↔texture) at a "stationary" position** = the **viewer (camera) cell flips per-frame** `0170↔0171` because the 3rd-person camera **boom drifts** (`desiredBack 3.11→3.07` while the player stands still), walking the eye across a portal plane, and acdream re-resolves the viewer cell fresh each frame with **no hysteresis**. The render roots at the viewer cell, so it redraws two different solves → flicker. 2. **Stable bluish void** = when the eye is firmly in/at a thin/transition cell, the portal flood **degenerates** because acdream projects-to-NDC-then-2D-clips instead of clipping in clip-space, so a close/grazing portal drops (`proj=0`) → no terrain / neighbour not flooded → grey. - **CONFIRMED by live cdb on retail** (this is the payoff): retail's `viewer_cell` is **rock-stable** at the same Holtburg-cottage boundary — clean single monotonic transitions, **zero oscillation** across 4,916 samples; retail rests the camera in the *substantial* cell, never lingering in the thin doorway cell. acdream's `[flap-sweep]` flips `0170↔0171` at the same spot. Retail is stable, acdream is not — exactly as the decomp predicted. - **The fix (3 parts, prioritized) is retail-faithful and anchored to specific decomp functions.** §4. - **Two partial fixes shipped this session** (`5f596f2` NDC frustum side-plane clip — keep; `9f95252` eye-in-portal flood — band-aid, likely superseded → reassess/revert). §3. - **Test baseline:** App **183 pass / 0 fail** (179 + 4 new). Core **1326 pass / 4 fail (documented) / 1 skip**. Branch HEAD after this handoff commit. Build green. --- ## 1. The session arc (so you don't repeat it) 1. Read the "core inside render (R1)" handoff. **Discovered R1's inversion + per-cell `DrawInside` already exist and are live** (`c4fd711`/`4b75c68`/`cf85ea4`, 2026-06-02; not reverted). So this was a *debug* task, not a *port* task. 2. **Visual gate (user):** the cellar (T1) is **sealed**. The real symptoms are at **transitions**: a bluish **void flap** exiting the building (screenshot), a **grey flash** cellar→room, **outdoor content through the ground** looking out, and a **stationary flicker** (textures alternate grey↔texture while standing still). 3. **Evidence-first (probes `ACDREAM_PROBE_FLAP/_VIS/_SHELL/_CELL`):** - Refuted the prior handoff's hypothesis ("flood doesn't reach the cellar") — rooted at the room, the flood **does** reach the cellar (`[vis] root=0171 ids=[...,0174]`). - Found the void is `terrain=Skip` / `proj=0` on exit/interior portals when the eye is close (`[flap] p->0xFFFF D=-0.28 proj=4 clip=0`; later `p1->0x0171 D=0.16 proj=0`). - Found the **flicker** is the **viewer cell flipping** at a knife-edge boundary: in a held pose the `[flap-sweep] viewerCell` flips `0170↔0171` as `D` crosses `0.00`, while the **player stands still** and the boom **drifts** (`desiredBack 3.11→3.07`). 4. **Decomp spike (3 parallel agents + 1 verified-myself crux):** mapped how retail stays stable. §2. 5. **Live cdb on retail** (matching v11.4186 binary, PDB-paired): captured retail's `viewer_cell` across the same crossings → **clean, no oscillation**. §2.4. Confirms the fix direction. --- ## 2. ROOT CAUSE — retail stays stable via THREE mechanisms; acdream diverges on each ### 2.1 Camera boom is a stable spring (Q3) — `CameraManager::UpdateCamera` - Retail's boom vector `CameraManager::viewer_offset` (default `y = -2.5 m`) is **fixed** (changes only on zoom keys). `UpdateCamera` (≈`0x00456660`, lerp body `0x00456d0d`) lerps the camera *position* toward `pivot + viewer_offset` with stiffness, and **snaps to the current position when within 0.0004 m** → holds a constant fixed point at rest. - **The collided eye is firewalled from the desired position:** `set_viewer(…, arg3=0)` (the normal success path, `update_viewer` pc≈92870) writes `this->viewer` (rendered eye) but **NOT** `viewer_sought_position`. So the collision result never feeds back into the desired boom. - `PlayerPhysicsUpdatedCallback` (`0x00452d60`, pc:91836) computes `viewer_sought_position = UpdateCamera(current_viewer)`. - **acdream diverges:** `RetailChaseCamera` desired boom **drifts at rest** (`desiredBack 3.11→3.07`). Hypotheses (verify in code): the collided eye is fed back into the desired (no firewall), and/or no convergence snap. **This drift is the flicker trigger** (walks the eye across the boundary). ### 2.2 Viewer cell is sticky via a 0.2 mm dead-zone (Q1) — VERIFIED MYSELF - `SmartBox::update_viewer` (`0x00453ce0`, pc:92761): `viewer_cell = sphere_path.curr_cell` after the collision sweep, which **starts from the stable player cell** each frame (`cell_1` = player cell or `AdjustPosition`-seated). - `SPHEREPATH::init_path` (`0x0050ce20`, pc:274370): `curr_cell = arg2` (the start cell). - The sweep updates the cell only on a **definite** crossing: `check_other_cells` (`0x0050ae50`, pc:272717) → `find_cell_list` → `check_cell`; `validate_transition` (`0x0050aa70`) promotes `curr_cell = check_cell` (pc:272608) only when the cell/pos actually changed, else **restores `curr_cell`**. - **The dead-zone (VERIFIED at pc:325513/325522):** `BSPNODE::point_inside_cell_bsp` (`0x0053c1f0`) uses `0.000199999995f` (≈0.2 mm) symmetrically — a point within ±0.2 mm of a splitting plane belongs to **neither** cell, so at a boundary graze `check_cell` is null and `curr_cell` stays at the start cell. - **acdream diverges:** `RetailChaseCamera.ViewerCellId = swept.ViewerCellId` is re-resolved fresh **every frame** with **no dead-zone** ("graph-tracked, deterministic, NO grace frames" — the comment) → flips at the boundary. ### 2.3 Portal clip is homogeneous (w-space), before the divide (Q2) — `GetClip`/`polyClipFinish` - `PView::GetClip` (`0x005a4320`, pc:432344) projects the portal then calls `ACRender::polyClipFinish` (`0x006b6d00`, pc:702749), which **clips against the near plane (w=0) in clip-space, generating synthetic edge vertices, BEFORE the perspective divide** — so a close portal never blows up to garbage NDC. - `PView::InitCell` (`0x005a4b70`, pc:432896) **side-test culls** in-plane/back-facing portals (same 0.2 mm band, pc≈432936) before any projection. - The flood is substantially **root-invariant** for adjacent cells (both seeded full-screen; side-test is symmetric). - **acdream diverges:** projects-to-NDC then 2D-clips → degenerates at grazing/close angles (`proj=0` → portal dropped → grey void / neighbour not flooded). This session's commit `5f596f2` added the eye-plane + side-plane clip (partial); the missing piece is the **w=0 near-plane clip with synthetic verts** + the side-test dead-band. ### 2.4 LIVE CDB CONFIRMATION (retail v11.4186, PDB-paired) — the payoff Captured `SmartBox::viewer_cell` (`viewer.objcell_id`) at the Holtburg cottage while passing inside↔outside + the stairs + standing still. Run-length-encoded camera-cell sequence (4,916 samples): ``` 0xa9b40032 ×3360 → 0031 ×173 → 0170 ×8 → 0171 ×134 → 0170 ×14 → 0031 ×129 → 0170 ×7 → 0171 ×139 → 0170 ×15 → 0031 ×110 → 0170 ×7 → 0171 ×167 → … (clean repeats) ``` - **Every crossing is a clean, single, monotonic pass** (outside `0031/0032` → vestibule `0170` brief 7–15-sample pass-through → room `0171`, and back). **ZERO `0170↔0171` oscillation** anywhere. - Standing still, retail rests in the **substantial** cells (room `0171` ×134–197, outside long runs), **never lingering in the thin vestibule `0170`**. - **Contrast acdream:** `[flap-sweep] viewerCell` flips `0170↔0171` per-frame at the same boundary. - **Conclusion:** retail's viewer cell is stable (boom holds + 0.2 mm dead-zone + sweep-from-player-cell); acdream's is not. No surprises — the fix is de-risked. --- ## 3. What shipped this session (committed; partial) | SHA | What | Keep? | |---|---|---| | `5f596f2` | `PortalProjection.ProjectToNdc` clips eye + 4 frustum **side planes** in clip-space before the divide (replaces the 2026-06-03 `MinW`-only workaround). Bounds NDC to the screen. | **KEEP.** Real correctness, retail-consistent (partial of §2.3). | | `9f95252` | `PortalVisibilityBuilder` floods the neighbour when the eye **stands in** an interior portal (`EyeInsidePortalOpening`). Fixed the cellar **ceiling** (visual-verified). | **REASSESS / likely REVERT.** A coverage band-aid for the thin-cell-root case; the §4 boom + dead-zone keep the camera out of thin cells, and the w=0 clip handles close portals — this may become unnecessary or over-include. Easy `git revert 9f95252`. | Neither is the flicker fix. Both green (App 183), Core baseline held (1326/4/1). --- ## 4. THE FIX — 3-part retail-faithful port (prioritized). Plan, then implement TDD. ### Part 1 (HIGHEST leverage) — Camera boom stability → kills the flicker trigger - **Goal:** acdream's desired boom settles and **holds** at rest (no drift). Match `UpdateCamera`: desired position derived each frame from a **fixed** boom offset + the player pivot; **firewall the collided eye** out of the desired chain; add the **convergence snap** (return current when within ~0.0004 m). - **acdream targets:** `src/AcDream.App/Rendering/RetailChaseCamera.cs` (the `_dampedEye` / `desiredBack` / the lerp + where the collided `swept.Eye` is consumed). Verify whether `swept.Eye` feeds the next-frame desired (the drift hypothesis) and whether a snap exists. - **Anchors:** `CameraManager::UpdateCamera` `0x00456660` (snap ~`0x00456d0d` region), `PlayerPhysicsUpdatedCallback` `0x00452d60` (pc:91836), `set_viewer` arg3=0 firewall (`update_viewer` pc≈92870). - **Verify:** with the boom stable, the `[flap-sweep] eyeBack/desiredBack` is flat at rest and the eye stops grazing the boundary. ### Part 2 — Viewer-cell dead-zone hysteresis → belt-and-suspenders for the flicker - **Goal:** acdream's camera-sweep viewer-cell resolution doesn't flip on a sub-mm/boundary graze. Port the retail dead-zone: a point within ±0.2 mm of a portal plane belongs to neither cell → keep the prior/start (player) cell. - **acdream targets:** `src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs` (`SweepEye`) and the Core cell-resolution it calls (`CellTransit.FindVisibleChildCell` / the point-in-cell test). Ensure the sweep starts from the player cell and only changes `viewer_cell` on a definite crossing. - **Anchors:** `point_inside_cell_bsp` `0x0053c1f0` (0.000199999995f, pc:325513/325522), `init_path` pc:274370, `validate_transition` pc:272608, `check_other_cells` pc:272717. ### Part 3 — w-space portal clip robustness → kills the *stable* grey void - **Goal:** extend `5f596f2` to a true **near-plane (w=0) clip with synthetic edge vertices before the divide** + the **InitCell side-test dead-band** that culls in-plane/back-facing portals before projection. - **acdream targets:** `src/AcDream.App/Rendering/PortalProjection.cs` + `PortalVisibilityBuilder.cs` (the side test `CameraOnInteriorSide`). After this, **reassess `9f95252`** (the eye-in-portal flood may be redundant → revert). - **Anchors:** `GetClip` `0x005a4320` (pc:432344), `polyClipFinish` `0x006b6d00` (pc:702749, the w=0 clip), `InitCell` `0x005a4b70` (pc:432896 side-test). **Order:** Part 1 → visual gate → Part 2 → Part 3 → reassess `9f95252`. Each is independently verifiable. --- ## 5. KEEP / DON'T **KEEP:** - R1 per-cell `DrawInside` + the binary inversion (`c4fd711`) — built and correct; the cellar seals. - Residual A (camera collision, `update_viewer` port) — the viewer cell is *accurate*; we're stabilizing it, not removing it. - Commit `5f596f2` (NDC side-plane clip). - The two-camera-ish reality: eye drives projection; the fix makes the *viewer cell* stable (boom + dead-zone), matching retail (`is_player_outside` decides; `DrawInside(viewer_cell)` roots). **DON'T:** - Don't re-attempt "the flood doesn't reach the cellar" — refuted (`[vis]` shows it does). - Don't add a render-side debounce/grace-period for the flicker — it's a **membership/visibility stability** bug; fix the *input* (boom + dead-zone), not the render (memory: render-downstream-of-membership). - Don't switch the render root to the *player* cell — retail roots `DrawInside` at the *viewer* cell; the fix is to make the viewer cell *stable*, not to change which cell roots. - Don't put a `;` inside a cdb `$$` comment (it splits into a command — bit me this session; use `*` comments). --- ## 6. APPARATUS (committed / ready) - **Probes** (all live): `ACDREAM_PROBE_FLAP` (`[flap]`/`[flap-cam]`/`[flap-sweep]`), `ACDREAM_PROBE_VIS` (`[vis]`), `ACDREAM_PROBE_SHELL` (`[shell]`), `ACDREAM_PROBE_CELL` (`[cell-transit]`). - **cdb script** `tools/cdb/retail-viewer-cell.cdb` — samples retail `SmartBox::viewer_cell` ~6/sec, auto-`.detach` after 6000 hits. Binary verified MATCH via `tools/pdb-extract/check_exe_pdb.py`. Re-run pattern + RLE analysis are in this session's transcript (PowerShell `Select-String` on `retail-viewer-cell.log`). **Lesson:** per-frame bp + `dt`/`.printf` is heavy but survived here (retail intact); keep samples sparse. `qd` is ignored in bp actions — use `.detach`. - **TTD** is available (`tools/ttd-record.ps1` / `tools/ttd-query.ps1`) if a lower-overhead capture is needed. --- ## 7. STATE - Branch `claude/thirsty-goldberg-51bb9b`. HEAD: this handoff's docs commit (after `9f95252`). No push (ask first). - Build green. App **183 / 0**. Core **1326 / 4 (documented: 2× DoorBugTrajectoryReplay LiveCompare, BSPStepUpTests.D4, DoorCollisionApparatus) / 1 skip**. - Running the client: see CLAUDE.md "Running the client"; `+Acdream` spawns in the Holtburg cottage. --- ## 8. KICKOFF PROMPT (copy-paste for the next session) ``` Continue acdream M1.5 indoor render: fix the boundary FLICKER + stable bluish VOID with the 3-part retail-faithful port that the 2026-06-05 spike confirmed. ROOT CAUSE (decomp + live cdb, both done): the indoor flicker/void is VISIBILITY METASTABILITY at cell boundaries, not a missing flood — R1's per-cell DrawInside is built and the cellar seals. Retail stays stable via three mechanisms; acdream diverges on each. Branch claude/thirsty-goldberg-51bb9b (do NOT branch/worktree; do NOT push without asking; NEVER git stash/gc). PowerShell on Windows; launch logs are UTF-16. READ FIRST (in order): 1. docs/research/2026-06-05-viewer-cell-flicker-rootcause-and-fix-plan-handoff.md (THIS handoff — root cause §2, cdb confirmation §2.4, the 3-part fix §4, KEEP/DON'T §5). 2. memory: reference_render_pipeline_state.md, feedback_render_downstream_of_membership.md, feedback_render_one_gate.md, project_camera_visibility_coupling.md. 3. The decomp anchors cited in §2/§4 (named-retail) for each part you implement. THE FIX (plan first, then implement TDD; each part independently visual-verified): Part 1 (highest leverage) — Camera boom stability (RetailChaseCamera): desired boom settles + HOLDS at rest; firewall the collided eye out of the desired chain; add the convergence snap. Anchors: CameraManager::UpdateCamera 0x00456660, PlayerPhysicsUpdatedCallback 0x00452d60, set_viewer arg3=0. Part 2 — Viewer-cell dead-zone (PhysicsCameraCollisionProbe.SweepEye / Core cell resolution): ±0.2 mm dead-zone so a graze keeps the player/start cell. Anchors: point_inside_cell_bsp 0x0053c1f0 (0.000199999995f), validate_transition pc:272608, init_path pc:274370. Part 3 — w-space portal clip (PortalProjection/PortalVisibilityBuilder): near-plane (w=0) clip with synthetic verts before the divide + InitCell side-test dead-band; then reassess/revert the eye-in-portal flood band-aid (commit 9f95252). Anchors: GetClip 0x005a4320, polyClipFinish 0x006b6d00, InitCell 0x005a4b70. START by using superpowers:writing-plans (or brainstorming if a part's shape is unclear) to turn §4 into a step plan with per-part acceptance + visual gates, THEN implement Part 1 first via TDD. DON'T (§5): no render-side debounce for the flicker (fix the boom/cell input); don't switch the render root to the player cell (retail roots DrawInside at the viewer cell — stabilize it instead); don't reopen "flood doesn't reach the cellar" (refuted). TEST BASELINE: App 183 pass / 0 fail. Core 1326 pass / 4 fail (documented) / 1 skip. Build green. This session committed 5f596f2 (NDC side-plane clip — KEEP) + 9f95252 (eye-in-portal flood — reassess). ```