# 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`](2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md) (top banner) + memory `project_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): 1. If `player->cell == 0` → `reenter_visibility`; still 0 → `set_viewer(player_pos, 1)`, `viewer_cell=null`, return. 2. Compute the desired eye (`viewer_sought_position`) from the pivot (head + `pivot_offset`). 3. **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`, else `cell = player->cell`. Outdoor → `cell = player->cell`. 4. **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`. - `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's `ResolveWithTransition` is 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](../../src/AcDream.App/Rendering/RetailChaseCamera.cs)): damps `_dampedEye`; `pivotWorld = playerPos + (0,0,1.5)`; if `CameraDiagnostics.CollideCamera && CollisionProbe != null` → `swept = CollisionProbe.SweepEye(pivotWorld, _dampedEye, cellId, selfEntityId)`; `publishedEye = swept.Eye`, `ViewerCellId = swept.ViewerCellId`. (Collides into a LOCAL, leaves `_dampedEye` clean to avoid wall-press oscillation — keep that.) - `PhysicsCameraCollisionProbe.SweepEye` ([src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:24](../../src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs)): shifts pivot/eye down by the radius (InitPath sphere-center convention), `ResolveWithTransition` (viewer sphere r=0.3, height 0, isOnGround=false, body=null, moverFlags `IsViewer|PathClipped|FreeRotate|PerfectClip`, `movingEntityId=selfEntityId`), returns swept eye + `r.CellId`. **Passes the player `cellId` straight in — does NOT do retail's AdjustPosition pivot-cell; has NO AdjustPosition fallbacks.** - The `[flap-sweep]` probe (in SweepEye, gated `RenderingDiagnostics.ProbeFlapEnabled` = `ACDREAM_PROBE_FLAP`) + the builder's `[flap-cam]`/`[flap]`/`[shell]`/`[vis]` probes are the diagnosis apparatus — already in the tree. ## 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: 1. **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's `check_other_cells` candidate set, the sweep misses them. (Most likely + matches master-plan C1's "faithful start-cell" gap.) 2. **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.) 3. **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.CollideCamera` toggles the spring-arm. - **Decomp anchors:** `SmartBox::update_viewer` 0x453ce0 pc:92761 · `find_valid_position` pc:273890 → `find_transitional_position` pc:273613 · `CPhysicsObj::AdjustPosition` (grep the decomp) · `CEnvCell::find_visible_child_cell` 0x52dc50 pc:311397 (C3, viewer child cell — not yet ported, optional for A). - **DON'T-redo:** the blue-hole fix (`UpdatePlayerCurrCell` player-only render-root write) — never re-add a `CurrCell` write in `ResolveWithTransition`/`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).