Session wrap: cellar-lip wedge fixed + visual-verified (cc4590f/9fdf6a5/41db027). Next task per the plan = Render Residual A: keep the chase camera eye inside the player's cell by porting retail SmartBox::update_viewer verbatim (fixes interior walls going grey/transparent from inside). - New canonical handoff with copy-paste fresh-session kickoff prompt, the retail update_viewer decode, the V1 current-state map, the gap to pin (faithful start-cell + AdjustPosition fallbacks + the no-wall-hit cause), and the evidence-first plan ([flap-sweep] capture → deterministic SweepEye test → port). - Key finding recorded: find_valid_position (pc:273890) just calls find_transitional_position — the sweep function is faithful, NOT the divergence. - CLAUDE.md banner updated to point at the new state + handoff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
Handoff — Render Residual A: camera collision (verbatim port of SmartBox::update_viewer) — 2026-06-05
▶ FRESH-SESSION KICKOFF PROMPT (copy-paste)
Continue acdream M1.5 render work: Render Residual A — CAMERA COLLISION (keep the 3rd-person camera
eye inside the player's cell so interior walls stop going grey/transparent from inside). This is a
VERBATIM port of retail SmartBox::update_viewer — no hybrids, no bandaids (master-plan mandate).
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 (Select-String / rg --encoding utf-16le,
NOT GNU grep). Use superpowers:systematic-debugging; the user pre-approved the verbatim-port APPROACH
and the A→C→B order, so when you reach the design step use superpowers:brainstorming only to present
the concrete port design for sign-off before editing.
READ FIRST (in order):
1. docs/research/2026-06-05-camera-collision-residual-a-handoff.md (THIS file — canonical).
2. docs/research/2026-06-03-membership-and-bluehole-shipped-handoff.md (§3 residuals A/B/C; the
blue-hole DON'T-redo: never re-add a CurrCell write inside ResolveWithTransition/ResolveCellId).
3. docs/superpowers/specs/2026-06-03-verbatim-spatial-pipeline-port-master-plan.md (§C Camera: C1/C3).
4. memory/reference_render_pipeline_state.md + project_camera_visibility_coupling.md.
STATE: M1.5 "indoor world feels right." The cellar-lip step-up wedge is FIXED + visual-verified
(committed cc4590f/9fdf6a5/41db027 — check_other_cells now reads the LIVE sphere position). Per the
plan the next task is Render Residual A: camera collision. User-confirmed problem mapping: Residual A
= interior walls/seams go grey/transparent WHILE INSIDE (the chase eye drifts OUT of the player's
cell → near walls back-face/clip away); Residual C = outside-looking-in glass-box (separate, bigger
DrawPortal phase, do AFTER A); Residual B = particles (smallest, last).
GOAL: port retail SmartBox::update_viewer (0x453ce0, pc:92761) faithfully so [flap-cam] eyeInRoot=y
while inside and interior walls stay opaque. Retail behavior: pivot at player head → (indoor) pick the
PIVOT's cell via CPhysicsObj::AdjustPosition → SWEEP the 0.3 viewer_sphere pivot→sought-eye via a
CTransition, stop at first wall → viewer=curr_pos, viewer_cell=curr_cell → fallback AdjustPosition at
sought-eye → fallback snap-to-player.
KEY FINDINGS (do NOT re-derive):
- find_valid_position (pc:273890) is literally `return find_transitional_position(this)` (pc:273898).
So acdream's SweepEye→ResolveWithTransition→FindTransitionalPosition IS the faithful sweep. The
sweep FUNCTION is NOT the divergence — do not re-port it.
- The sweep + viewer_cell are ALREADY wired (V1): RetailChaseCamera.Update (damped eye → pivot →
CollisionProbe.SweepEye) + PhysicsCameraCollisionProbe.SweepEye (viewer sphere r=0.3, moverFlags
IsViewer|PathClipped|FreeRotate|PerfectClip, gated on CameraDiagnostics.CollideCamera).
- THE BUG (per handoff §3 + the [flap-sweep] probe comment): the sweep RUNS but finds NO wall
(pulledIn≈0, resolved=Y, bsp=ok) → the eye flies to full chase distance (eyeInRoot=n ~90%) in cells
like 0xA9B40174/0175. Root cause of the no-wall-hit is NOT yet pinned.
- GAPS per master-plan C1: (a) faithful START-CELL — retail uses AdjustPosition to find the PIVOT's
cell; acdream passes the player cellId straight in. (b) the two AdjustPosition FALLBACKS are missing.
(c) C3 find_visible_child_cell (pc:311397) is not ported (viewer cell uses the sweep curr_cell —
fine for now). Whether (a)/(b) actually cause the no-wall-hit is UNVERIFIED — pin it with evidence.
THE JOB (evidence-first; the saga lesson = do NOT guess):
1. Live capture: launch with ACDREAM_PROBE_FLAP=1 (+ CameraDiagnostics.CollideCamera on), stand inside
the Holtburg cottage, rotate the chase camera into a back wall. Capture [flap-sweep] (cell/resolved/
bsp/desiredBack/eyeBack/pulledIn/collNormValid) + [flap-cam] (root/eyeInRoot). Use the probe-comment
fork in PhysicsCameraCollisionProbe.cs to read WHY: pulledIn≈0 + bsp=ok ⇒ the sweep reaches no wall
geometry in the candidate set (clip/candidate-cell issue or wrong start cell); resolved=n/bsp=nobsp
⇒ collision can't run there (cell/BSP not loaded).
2. Diagnose the no-wall-hit from the capture (likely: the sweep's candidate-cell set doesn't include
the wall's cell, OR the start cell is wrong because AdjustPosition isn't seating the pivot). Confirm
against retail update_viewer before changing anything.
3. Port verbatim: the faithful start-cell (AdjustPosition for the pivot's cell, indoor branch) + the
two AdjustPosition fallbacks, plus whatever the capture proves is the no-wall-hit cause. Consider a
DETERMINISTIC SweepEye test (cell fixture + seed pivot/eye, assert the sweep stops at the wall) —
the CellarLipWedgeTests pattern made the stairs fix iterable in <200ms; do the same here.
4. VALIDATE: eyeInRoot=y inside; build + Core(1317p/4f/1s)/App green. VISUAL GATE: stand inside the
cottage + rotate — interior walls stay solid (no grey/transparent, no NPCs/particles through walls);
inside-looking-out still correct (don't regress the fixed flap); generic outdoor chase unaffected.
DO NOT: guess / speculative-edit (the saga's failure mode); re-add a CurrCell write inside
ResolveWithTransition/ResolveCellId (the blue-hole clobber — CurrCell is player-only via
UpdatePlayerCurrCell); conflate A (camera-eye containment, this task) with C (DrawPortal outside-
looking-in, next task); re-port find_valid_position/the sweep (it's faithful).
TEST BASELINE: Core 1317 pass / 4 fail (documented: Apparatus_Grounded_50cmOffCenter, 2×
DoorBugTrajectoryReplay LiveCompare_*, BSPStepUpTests.D4) / 1 skip. App green. Branch HEAD 41db027.
1. Session summary (2026-06-05)
Shipped + visual-verified: the P2 cellar-lip step-up wedge. Root cause = Transition.CheckOtherCells
collided the other cells against a STALE footCenter snapshotted before the primary collide; after a
step-up climbed the foot onto the cottage floor, the stale (pre-climb, penetrating) position spuriously
near-missed that floor → a doomed second step-up → revert → 0% advance. Fix: re-read
footCenter = sp.GlobalSphere[0].Origin in RunCheckOtherCellsAndAdvance (retail check_other_cells
reads the live sphere_path.global_sphere, pc:272735). 0/29 → 20/29 captured wedge frames climb; zero
regression. User visual-gate: "Yes all works!" (cellar smooth, door blocks, step-up climbs).
Commits cc4590f (fix + tests) / 9fdf6a5 (strip probes) / 41db027 (visual-gate note). Full writeup
- the disproven prior framings:
2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md(top banner) + memoryproject_p2_door_stepup_findings.
Then: picked + scoped the next task (this handoff). Per the plan the next step after the collision
fix is Render Residual A — camera collision. Aligned with the user on the problem statement (the two
symptoms → residuals A/C) and the approach (verbatim port of SmartBox::update_viewer, A→C→B order).
Did the read-only investigation below; NO camera code changed (next session implements after the
evidence-first diagnosis).
2. The problem (user-confirmed)
| Symptom (user words) | Residual | Cause | Fix |
|---|---|---|---|
| Inside a building, walls/seams flicker grey/transparent; can see through walls | A (this task) | 3rd-person chase eye drifts OUTSIDE the player's cell → near walls seen from their back-faces → culled | camera collision: sweep the eye, stop at the wall, keep it in the cell |
| Outside looking in through a doorway, building is a see-through glass box; ground over the floor | C (next) | outdoor→interior portal render (retail DrawPortal) not built |
build that render phase |
| Particles bleed through floor | B (last) | scene particles not cell-clipped (#104) | cell-link the emitters |
Order A → C → B: A is smaller + builds the shared "which cell is the viewpoint in" machinery that C also needs (shrinks C). Mechanism for "transparent wall" = back-face culling (a wall is a one-sided sheet facing into the room; from outside the room you see its culled back) + the renderer drawing from the viewer's cell then flooding portals (so the viewer's cell must be right).
3. Retail target — SmartBox::update_viewer (0x453ce0, pc:92761)
Decoded this session (read the decomp directly for the verbatim port):
- If
player->cell == 0→reenter_visibility; still 0 →set_viewer(player_pos, 1),viewer_cell=null, return. - Compute the desired eye (
viewer_sought_position) from the pivot (head +pivot_offset). - Start cell: if player indoor (
objcell_id >= 0x100),CPhysicsObj::AdjustPosition(&var_90, &viewer_sphere, &cell_1, 0, 1)to find the PIVOT's cell; success →cell = cell_1, elsecell = player->cell. Outdoor →cell = player->cell. - Sweep:
makeTransition→init_object(player, 0x5c)→init_sphere(1, &viewer_sphere, 1.0)(ONE sphere) →init_path(cell_1, pivot, sought_eye)→find_valid_position.- success →
set_viewer(curr_pos, 0),viewer_cell = sphere_path.curr_cell, return. - fallback 1:
AdjustPosition(sought_eye, &viewer_sphere, &var_170, 0, 1)→set_viewer(var_120, 0),viewer_cell = var_170, return. - fallback 2:
set_viewer(player_pos, 1),viewer_cell = null.
- success →
0x5c=IsViewer | PathClipped | FreeRotate | PerfectClip(PathClipped = hard-stop at first contact).find_valid_position(pc:273890) =return find_transitional_position(this)(pc:273898) — the sweep is the ordinary transition; acdream'sResolveWithTransitionis faithful to it. The sweep function is NOT the divergence.
4. acdream current state (V1, partial)
RetailChaseCamera.Update(src/AcDream.App/Rendering/RetailChaseCamera.cs:102): damps_dampedEye;pivotWorld = playerPos + (0,0,1.5); ifCameraDiagnostics.CollideCamera && CollisionProbe != null→swept = CollisionProbe.SweepEye(pivotWorld, _dampedEye, cellId, selfEntityId);publishedEye = swept.Eye,ViewerCellId = swept.ViewerCellId. (Collides into a LOCAL, leaves_dampedEyeclean to avoid wall-press oscillation — keep that.)PhysicsCameraCollisionProbe.SweepEye(src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:24): shifts pivot/eye down by the radius (InitPath sphere-center convention),ResolveWithTransition(viewer sphere r=0.3, height 0, isOnGround=false, body=null, moverFlagsIsViewer|PathClipped|FreeRotate|PerfectClip,movingEntityId=selfEntityId), returns swept eye +r.CellId. Passes the playercellIdstraight in — does NOT do retail's AdjustPosition pivot-cell; has NO AdjustPosition fallbacks.- The
[flap-sweep]probe (in SweepEye, gatedRenderingDiagnostics.ProbeFlapEnabled=ACDREAM_PROBE_FLAP)- the builder's
[flap-cam]/[flap]/[shell]/[vis]probes are the diagnosis apparatus — already in the tree.
- the builder's
5. The gap to pin (next session, evidence-first)
The symptom is "sweep runs, finds no wall" (pulledIn≈0, eyeInRoot=n ~90%). Candidates, in order of
suspicion:
- Start cell — acdream passes the player cell; retail seats the start cell at the PIVOT via
AdjustPosition. If the pivot/eye path's walls live in a cell that isn't the start cell and isn't reached by the sweep'scheck_other_cellscandidate set, the sweep misses them. (Most likely + matches master-plan C1's "faithful start-cell" gap.) - Candidate-cell tracking across the multi-step sweep — the eye is ~2.6 m behind the player and the sweep subdivides; if the carried cell doesn't advance into the wall's cell, the wall poly is never in the per-cell BSP queried. (Related to the Stage-1 membership work; the player path now tracks the carried cell correctly — verify the camera sweep does too.)
- AdjustPosition missing — fallbacks aside, retail's start-cell AdjustPosition may be what seats
the sweep so it engages geometry; acdream has no AdjustPosition port at all (check
CPhysicsObj::AdjustPosition).
Pin with the live [flap-sweep] capture FIRST, then port. A deterministic SweepEye test (cottage
cell fixture, seed pivot inside + eye behind the back wall, assert the swept eye stops at the wall and
ViewerCellId stays the room) would make this iterable like the cellar-lip fix.
6. Apparatus + anchors
- Probes:
ACDREAM_PROBE_FLAP=1→[flap-sweep](PhysicsCameraCollisionProbe) +[flap-cam]/[flap]/[shell]/[vis](CellVisibility / the render builder).CameraDiagnostics.CollideCameratoggles the spring-arm. - Decomp anchors:
SmartBox::update_viewer0x453ce0 pc:92761 ·find_valid_positionpc:273890 →find_transitional_positionpc:273613 ·CPhysicsObj::AdjustPosition(grep the decomp) ·CEnvCell::find_visible_child_cell0x52dc50 pc:311397 (C3, viewer child cell — not yet ported, optional for A). - DON'T-redo: the blue-hole fix (
UpdatePlayerCurrCellplayer-only render-root write) — never re-add aCurrCellwrite inResolveWithTransition/ResolveCellId. Don't conflate A with C.
7. Brainstorming state (for the fresh session)
Approach + order are USER-APPROVED (verbatim port of update_viewer; A→C→B). The brainstorming design
step was NOT completed — resume by doing the evidence-first diagnosis (§5), then present the concrete
port design (start-cell + fallbacks + the no-wall-hit fix) for sign-off before editing (HARD-GATE:
no code until the design is approved).