Live ACDREAM_PROBE_FLAP capture (Holtburg cottage/cellar) proved the V1 camera spring-arm already contains the eye (eyeInRoot=Y 99.75%, viewerCell never 0, indoor collide 97.6% in 0174). The dominant inside-cottage bluish void is the render-sealing residual C (DrawPortal), NOT the camera. This spec scopes the FAITHFUL completion of Residual A: port the two missing update_viewer pieces verbatim — the indoor start-cell seated at the pivot via CPhysicsObj::AdjustPosition (pc:280009) → CEnvCell::find_visible_child_cell (pc:311397), plus the two AdjustPosition/snap-to-player fallbacks — and land FindVisibleChildCell (which residual C also needs). Faithful layering (mirrors retail SmartBox→CPhysicsObj): primitives in Core (PhysicsEngine.AdjustPosition + CellTransit.FindVisibleChildCell + ResolveResult.Ok), orchestration in App PhysicsCameraCollisionProbe.SweepEye. Deterministic crux test (start-cell resolution) in Core.Tests with the cottage fixtures; SweepEye glue in App.Tests. Visible payoff is narrow (the cellar-corner, point 3); the cottage-room void stays for residual C. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.2 KiB
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_viewer0x453ce0pc:92761 — start-cell → sweep → fallbacks.CPhysicsObj::AdjustPosition0x511d80pc:280009 — indoor →find_visible_child_cell; outdoor →LandDefs::adjust_to_outside.CEnvCell::find_visible_child_cell0x52dc50pc:311397 —this/portals/stab_listpoint_in_cell.CEnvCell::GetVisible0x52dc10pc:311378 — cell-graph resolve.find_valid_positionpc: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 existingFindCellList(retailfind_cell_list); both are cell-membership resolvers. Port offind_visible_child_cell:
Each candidate transformsstart = 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 0worldPointthrough its OWNInverseWorldTransformbefore the BSP test (matchesCellTransit.cs:520).PhysicsEngine.AdjustPosition(uint seedCellId, Vector3 worldPoint) -> (uint cellId, bool found)— port ofCPhysicsObj::AdjustPosition, indoor branch:FindVisibleChildCell(seed, point, useStabList:true). Outdoor branch (seedLow < 0x100) reuses the existing terrain-grid resolution. Retail'sseen_outside -> adjust_to_outsidesub-fallback is deferred (not on the cottage/cellar path; adding it unverified would be guessing — see §6).ResolveResult.Ok(newbool, defaulttrue) — surfaces theokalready computed atPhysicsEngine.cs:718(FindTransitionalPosition), the faithful map offind_valid_position != 0. Default-true → existing callers unaffected.
App (AcDream.App.Rendering) — the camera orchestration
PhysicsCameraCollisionProbe.SweepEyegains the verbatimupdate_viewerbody:- indoor (
cellId >= 0x100) →start = AdjustPosition(cellId, pivot)elsecellId; - sweep
pivot → desiredEyefromstart(existingResolveWithTransition, viewer flags); r.Ok→ return(swept eye, r.CellId);!r.Ok→AdjustPosition(cellId, desiredEye)→ return(desiredEye, thatCell)(fallback 1);- else → return
(playerPos, 0)(fallback 2, snap to player).SweepEyeneeds the player world position for fallback 2 → add aVector3 playerPosparameter toICameraCollisionProbe.SweepEye(passed byRetailChaseCamera.Update).
- indoor (
4. Tests
- Core.Tests (
CellarLipWedgeTestspattern, RED→GREEN): load cottage fixtures0171/0174/0175. Seed the captured corner frame — player(153.55, 9.32, 93.11)in0174, pivot(153.55, 9.32, 94.61). AssertAdjustPosition(0174, pivot)/FindVisibleChildCell(0174, pivot, true)resolves the pivot to its actual (floor-level) cell, not the cellar. <200 ms, iterable. - App.Tests: focused
SweepEyeorchestration test — start-cell seated, fallback-2 snaps toplayerPoswhen the sweep fails. Fixtures loaded by theSolutionRoot()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)
- Every ported behaviour cites its decomp anchor (address +
pc:line) in a comment. - No suppression flags / grace periods /
if (problem) returnguards. The two fallbacks are retail's own; fallback 2 (snap-to-player) is the faithful "never leave the eye invalid", not a band-aid. - The
seen_outside → adjust_to_outsidesub-fallback insideAdjustPositionis deferred, not stubbed — documented as out-of-path; revisit if a capture shows the camera needs it. - Do NOT re-add a
CurrCellwrite insideResolveWithTransition/ResolveCellId(the blue-hole clobber —CurrCellis player-only viaUpdatePlayerCurrCell). - Do NOT conflate A (eye containment) with C (
DrawPortaloutside-looking-in).