diff --git a/CLAUDE.md b/CLAUDE.md index 590cf0f0..8ddc5834 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -763,7 +763,25 @@ H1 (PVS grounding) or H2 (`PortalSide` side-test) — both evidence-disproven. **Currently working toward: M1.5 — Indoor world feels right** (resumed from 2026-05-20 baseline after Phase O ship). -**2026-06-03 — P1 membership DONE + P2 active (read this first).** The verbatim spatial-pipeline +**2026-06-05 — P2 cellar-lip wedge FIXED; next = Render Residual A (camera collision) (READ THIS FIRST).** +The "stuck on the last cellar step" wedge is FIXED + visual-verified (user: "Yes all works!" — cellar +ascent smooth, inn door still BLOCKS, generic step-up climbs). Root cause: `Transition.CheckOtherCells` +collided the other cells against a STALE pre-step-up `footCenter`; retail's `check_other_cells` reads the +LIVE `sphere_path.global_sphere` (pc:272735). Fix = re-read `footCenter = sp.GlobalSphere[0].Origin` in +`RunCheckOtherCellsAndAdvance` (commits `cc4590f`/`9fdf6a5`/`41db027`; 0/29→20/29 captured wedge frames +climb; zero regression; Core 1317p/4f/1s). This DISPROVED the prior "find_walkable never called" framing +(it was a probe-reading artifact — `find_walkable` IS called; the `[fc-dispatch]` cell logged the carried +cell, not the iterated one). The remaining 9/29 are a separate `(0,-1,0)` sliding-normal +Y-kill that did +NOT manifest in live play (buggy-trajectory artifact; documented as `DocumentsResidualWedge_*`, deferred — +slide territory). **NEXT per the plan = Render Residual A: camera collision** — verbatim port of retail +`SmartBox::update_viewer` (pc:92761) to keep the 3rd-person chase eye INSIDE the player's cell (fixes +interior walls going grey/transparent while inside). User-confirmed approach (verbatim port, no hybrids) ++ order (A→C→B; C = outside-looking-in `DrawPortal`, B = particles). **CANONICAL PICKUP:** +[`docs/research/2026-06-05-camera-collision-residual-a-handoff.md`](docs/research/2026-06-05-camera-collision-residual-a-handoff.md) +(+ cellar-lip writeup in the top banner of +[`docs/research/2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md`](docs/research/2026-06-04-p2-cellar-lip-flatfloor-cp-handoff.md)). + +**2026-06-03 — P1 membership DONE + P2 active (history; cellar-lip now FIXED per the 2026-06-05 banner above).** The verbatim spatial-pipeline port (master plan [`docs/superpowers/specs/2026-06-03-verbatim-spatial-pipeline-port-master-plan.md`](docs/superpowers/specs/2026-06-03-verbatim-spatial-pipeline-port-master-plan.md)) is the active effort. **P1 (membership) = DONE** — proven to ALREADY match retail; the believed diff --git a/docs/research/2026-06-05-camera-collision-residual-a-handoff.md b/docs/research/2026-06-05-camera-collision-residual-a-handoff.md new file mode 100644 index 00000000..7f29932c --- /dev/null +++ b/docs/research/2026-06-05-camera-collision-residual-a-handoff.md @@ -0,0 +1,180 @@ +# 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).