# Render Residual A — Camera collision: verbatim `SmartBox::update_viewer` completion **Date:** 2026-06-05 · **Phase:** M1.5 render residual A · **Branch:** `claude/thirsty-goldberg-51bb9b` ## 1. The finding (why this is a faithfulness completion, not a visible-bug fix) A live `ACDREAM_PROBE_FLAP` capture this session (Holtburg cottage + cellar) proved the V1 camera spring-arm **already works**: | Metric | Result | Meaning | |---|---|---| | `[flap-cam] eyeInRoot` | 186,349 `Y` / 470 `n` | eye inside the player's cell **99.75%** | | `viewerCell == 0` (eye in the void) | **0** of ~318k frames | the sweep never lands the eye in invalid space | | indoor collide rate, cell `0174` | **97.6%** | spring-arm engages cell-BSP walls hard | The dominant inside-cottage **bluish void** (seeing other buildings / particles / NPCs through the walls) is the render-**sealing** residual **C** (`PView::DrawPortal`), NOT the camera — the eye is already in a valid cell, yet the renderer draws the GL clear colour past unsealed geometry. User-confirmed. This task therefore **completes Residual A as a faithful verbatim port** and lands `FindVisibleChildCell`, which **C also needs**. Its one shot at a *visible* win is the **cellar-corner** (user point 3): there the player's feet are in the cellar but the pivot/head is up at cottage-floor level, so the pivot-seated start cell genuinely differs from the feet cell — the only configuration where the faithful start-cell changes the sweep's outcome. ## 2. Retail target (the oracle — port verbatim) - `SmartBox::update_viewer` `0x453ce0` pc:92761 — start-cell → sweep → fallbacks. - `CPhysicsObj::AdjustPosition` `0x511d80` pc:280009 — indoor → `find_visible_child_cell`; outdoor → `LandDefs::adjust_to_outside`. - `CEnvCell::find_visible_child_cell` `0x52dc50` pc:311397 — `this`/portals/stab_list `point_in_cell`. - `CEnvCell::GetVisible` `0x52dc10` pc:311378 — cell-graph resolve. - `find_valid_position` pc:273890 = `return find_transitional_position(this)` pc:273613 — **the sweep is already faithful; do NOT re-port it.** - `init_object(player, 0x5c)` = `IsViewer | PathClipped | FreeRotate | PerfectClip`; `init_sphere(1, viewer_sphere, 1.0)` (ONE sphere, r=0.3 pc:93314). Decoded `update_viewer` (indoor branch): ``` pivot = head frame · pivot_offset if player indoor (objcell_id >= 0x100): if AdjustPosition(pivot, viewer_sphere) -> cell_1: start = cell_1 # seat start at the PIVOT else: start = player->cell # fallback to feet cell else: start = player->cell sweep viewer_sphere pivot -> sought_eye, startCell=start, flags=0x5c # PathClipped = hard stop if find_valid_position: set_viewer(curr_pos); viewer_cell = curr_cell; return if AdjustPosition(sought_eye, viewer_sphere) -> var_170: # FALLBACK 1 set_viewer(sought_eye); viewer_cell = var_170; return set_viewer(player->m_position); viewer_cell = null # FALLBACK 2: snap to player ``` ## 3. Design — faithful layering (Core primitives ← App orchestration) Retail's `update_viewer` is a **`SmartBox` (camera) method** that calls *down* into physics (`CPhysicsObj::AdjustPosition`, `CTransition`). acdream mirrors that split exactly: ### Core (`AcDream.Core.Physics`) — the physics primitives - **`CellTransit.FindVisibleChildCell(IDataCache, uint startCellId, Vector3 worldPoint, bool useStabList)`** — sibling of the existing `FindCellList` (retail `find_cell_list`); both are cell-membership resolvers. Port of `find_visible_child_cell`: ``` start = cg.GetVisible(startCellId); if start == null: return 0 if PointInsideCellBsp(start, toLocal(start, worldPoint)): return start.Id # point_in_cell ids = useStabList ? start.VisibleCellIds : start.Portals.Select(OtherCellId) foreach id in ids: c = cg.GetVisible(id) if c != null && PointInsideCellBsp(c, toLocal(c, worldPoint)): return c.Id return 0 ``` Each candidate transforms `worldPoint` through its OWN `InverseWorldTransform` before the BSP test (matches `CellTransit.cs:520`). - **`PhysicsEngine.AdjustPosition(uint seedCellId, Vector3 worldPoint) -> (uint cellId, bool found)`** — port of `CPhysicsObj::AdjustPosition`, indoor branch: `FindVisibleChildCell(seed, point, useStabList:true)`. Outdoor branch (`seedLow < 0x100`) reuses the existing terrain-grid resolution. Retail's `seen_outside -> adjust_to_outside` sub-fallback is **deferred** (not on the cottage/cellar path; adding it unverified would be guessing — see §6). - **`ResolveResult.Ok` (new `bool`, default `true`)** — surfaces the `ok` already computed at `PhysicsEngine.cs:718` (`FindTransitionalPosition`), the faithful map of `find_valid_position != 0`. Default-true → existing callers unaffected. ### App (`AcDream.App.Rendering`) — the camera orchestration - **`PhysicsCameraCollisionProbe.SweepEye`** gains the verbatim `update_viewer` body: 1. indoor (`cellId >= 0x100`) → `start = AdjustPosition(cellId, pivot)` else `cellId`; 2. sweep `pivot → desiredEye` from `start` (existing `ResolveWithTransition`, viewer flags); 3. `r.Ok` → return `(swept eye, r.CellId)`; 4. `!r.Ok` → `AdjustPosition(cellId, desiredEye)` → return `(desiredEye, thatCell)` (fallback 1); 5. else → return `(playerPos, 0)` (fallback 2, snap to player). `SweepEye` needs the player world position for fallback 2 → add a `Vector3 playerPos` parameter to `ICameraCollisionProbe.SweepEye` (passed by `RetailChaseCamera.Update`). ## 4. Tests - **Core.Tests (`CellarLipWedgeTests` pattern, RED→GREEN):** load cottage fixtures `0171/0174/0175`. Seed the captured corner frame — player `(153.55, 9.32, 93.11)` in `0174`, pivot `(153.55, 9.32, 94.61)`. Assert `AdjustPosition(0174, pivot)` / `FindVisibleChildCell(0174, pivot, true)` resolves the pivot to its actual (floor-level) cell, not the cellar. <200 ms, iterable. - **App.Tests:** focused `SweepEye` orchestration test — start-cell seated, fallback-2 snaps to `playerPos` when the sweep fails. Fixtures loaded by the `SolutionRoot()` path-walk. ## 5. Validation / visual gate - Core baseline **1317 pass / 4 fail (documented) / 1 skip** maintained (+ the new tests); App green. - **Visual gate:** stand in the cottage cellar, press into a corner, rotate — the **cellar-corner void should improve** (point 3). Inside-looking-out must be **unregressed**. The cottage-room bluish void is **NOT** in scope (Residual C). ## 6. No-shortcuts rules (per master plan §4) 1. Every ported behaviour cites its decomp anchor (address + `pc:line`) in a comment. 2. No suppression flags / grace periods / `if (problem) return` guards. The two fallbacks are retail's own; fallback 2 (snap-to-player) is the faithful "never leave the eye invalid", not a band-aid. 3. The `seen_outside → adjust_to_outside` sub-fallback inside `AdjustPosition` is deferred, not stubbed — documented as out-of-path; revisit if a capture shows the camera needs it. 4. Do NOT re-add a `CurrCell` write inside `ResolveWithTransition`/`ResolveCellId` (the blue-hole clobber — `CurrCell` is player-only via `UpdatePlayerCurrCell`). 5. Do NOT conflate A (eye containment) with C (`DrawPortal` outside-looking-in).