Diagnosis session: the indoor bluish void + grey/texture flicker is visibility metastability at cell boundaries, not a missing flood (R1's per-cell DrawInside is built; the cellar seals). Confirmed by named-retail decomp AND a live cdb capture of retail (viewer_cell rock-stable: clean monotonic transitions, zero oscillation across 4916 samples). Retail stays stable via boom stability + a 0.2mm viewer-cell dead-zone + clip-space portal clipping; acdream diverges on all three. Handoff documents the root cause, the cdb evidence, and the prioritized 3-part retail-faithful fix (boom stability -> dead-zone -> w-space clip) with decomp anchors + a planning/implementation kickoff prompt. Adds the reusable retail viewer-cell cdb capture script and the superseding CLAUDE.md banner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
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
DrawInsideis 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. Branchclaude/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 (commitc4fd711, 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:
- Flicker (grey↔texture) at a "stationary" position = the viewer (camera) cell flips per-frame
0170↔0171because the 3rd-person camera boom drifts (desiredBack 3.11→3.07while 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. - 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.
- Flicker (grey↔texture) at a "stationary" position = the viewer (camera) cell flips per-frame
- CONFIRMED by live cdb on retail (this is the payoff): retail's
viewer_cellis 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]flips0170↔0171at 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 (
5f596f2NDC frustum side-plane clip — keep;9f95252eye-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)
- Read the "core inside render (R1)" handoff. Discovered R1's inversion + per-cell
DrawInsidealready exist and are live (c4fd711/4b75c68/cf85ea4, 2026-06-02; not reverted). So this was a debug task, not a port task. - 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).
- 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=0on exit/interior portals when the eye is close ([flap] p->0xFFFF D=-0.28 proj=4 clip=0; laterp1->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] viewerCellflips0170↔0171asDcrosses0.00, while the player stands still and the boom drifts (desiredBack 3.11→3.07).
- Refuted the prior handoff's hypothesis ("flood doesn't reach the cellar") — rooted at the room, the
flood does reach the cellar (
- Decomp spike (3 parallel agents + 1 verified-myself crux): mapped how retail stays stable. §2.
- Live cdb on retail (matching v11.4186 binary, PDB-paired): captured retail's
viewer_cellacross 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(defaulty = -2.5 m) is fixed (changes only on zoom keys).UpdateCamera(≈0x00456660, lerp body0x00456d0d) lerps the camera position towardpivot + viewer_offsetwith 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_viewerpc≈92870) writesthis->viewer(rendered eye) but NOTviewer_sought_position. So the collision result never feeds back into the desired boom. PlayerPhysicsUpdatedCallback(0x00452d60, pc:91836) computesviewer_sought_position = UpdateCamera(current_viewer).- acdream diverges:
RetailChaseCameradesired 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_cellafter the collision sweep, which starts from the stable player cell each frame (cell_1= player cell orAdjustPosition-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) promotescurr_cell = check_cell(pc:272608) only when the cell/pos actually changed, else restorescurr_cell. - The dead-zone (VERIFIED at pc:325513/325522):
BSPNODE::point_inside_cell_bsp(0x0053c1f0) uses0.000199999995f(≈0.2 mm) symmetrically — a point within ±0.2 mm of a splitting plane belongs to neither cell, so at a boundary grazecheck_cellis null andcurr_cellstays at the start cell. - acdream diverges:
RetailChaseCamera.ViewerCellId = swept.ViewerCellIdis 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 callsACRender::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 commit5f596f2added 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→ vestibule0170brief 7–15-sample pass-through → room0171, and back). ZERO0170↔0171oscillation anywhere. - Standing still, retail rests in the substantial cells (room
0171×134–197, outside long runs), never lingering in the thin vestibule0170. - Contrast acdream:
[flap-sweep] viewerCellflips0170↔0171per-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 collidedswept.Eyeis consumed). Verify whetherswept.Eyefeeds the next-frame desired (the drift hypothesis) and whether a snap exists. - Anchors:
CameraManager::UpdateCamera0x00456660(snap ~0x00456d0dregion),PlayerPhysicsUpdatedCallback0x00452d60(pc:91836),set_viewerarg3=0 firewall (update_viewerpc≈92870). - Verify: with the boom stable, the
[flap-sweep] eyeBack/desiredBackis 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 changesviewer_cellon a definite crossing. - Anchors:
point_inside_cell_bsp0x0053c1f0(0.000199999995f, pc:325513/325522),init_pathpc:274370,validate_transitionpc:272608,check_other_cellspc:272717.
Part 3 — w-space portal clip robustness → kills the stable grey void
- Goal: extend
5f596f2to 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 testCameraOnInteriorSide). After this, reassess9f95252(the eye-in-portal flood may be redundant → revert). - Anchors:
GetClip0x005a4320(pc:432344),polyClipFinish0x006b6d00(pc:702749, the w=0 clip),InitCell0x005a4b70(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_viewerport) — 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_outsidedecides;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
DrawInsideat 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 retailSmartBox::viewer_cell~6/sec, auto-.detachafter 6000 hits. Binary verified MATCH viatools/pdb-extract/check_exe_pdb.py. Re-run pattern + RLE analysis are in this session's transcript (PowerShellSelect-Stringonretail-viewer-cell.log). Lesson: per-frame bp +dt/.printfis heavy but survived here (retail intact); keep samples sparse.qdis 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 (after9f95252). 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";
+Acdreamspawns 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).