docs(research): #464 camera investigation report; reclassified as an interior-root draw defect (S4); archway route
Read-only decomp + real-DAT replay: eleven sweep/root differences, each unreachable at the pose or more constrained than retail; the replay seats the pivot in 0xF4180114, stops the boom on its east pier at y=16.448, and the walk from that root floods 114/113/112 with seven exit views. The owner's probe launch line now also sets ACDREAM_PROBE_FACILITY_STAIRS. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
d5fbcc6d5b
commit
c3b989305a
3 changed files with 123 additions and 1 deletions
|
|
@ -0,0 +1,68 @@
|
|||
# #464 - the cathedral stairwell camera: read-only investigation (2026-09-03 night)
|
||||
|
||||
Workflow `investigate-464` (`wf_86230381-584`, one read-only agent, 132 tool uses, about 40 min) against the tree at `590fbb66c`, the named retail decomp, and an offline replay with the real engine and real DATs (F418's 24 interior cells, graph and flat traversal). Lead summary: the chase camera sweep and the render-root rule match retail's `SmartBox::update_viewer` / `CTransition::find_valid_position` / `SmartBox::RenderNormalMode` at the owner's pose; the frame is produced downstream of a correct root (0xF4180114) with seven exit views, which is S4 depth/alpha territory. The lead's four probe-on self-gate rounds the same night agree (`docs/ISSUES.md` #464).
|
||||
|
||||
## Mechanism (acdream, line by line)
|
||||
|
||||
ZOOM-OUT PATH (acdream, tree at 590fbb66c; camera/physics files unchanged vs the owner's ea76a3803 build — only RetailPViewRenderer.cs/WorldSceneRenderer.cs are dirty in the S4-c1 tree).
|
||||
(a) Input: CameraFrameController.Tick (src/AcDream.App/Rendering/CameraFrameController.cs:75-92) feeds RetailChaseCamera.AdjustDistance/AdjustPitch (RetailChaseCamera.cs:339-366, 375-397; the CameraSet::Farther/Raise component limits |x|,|y|<10, -1.8<z<450 at :506-544) continuously per frame (adjustment = CameraAdjustmentSpeed*dt) instead of retail's discrete 0.2/8-degree units — same reachable envelope.
|
||||
(b) Desired eye: RetailChaseCamera.Update (:176-302): pivot = RenderPosition + (0,0,1.5) (:196; retail pivot_part_index is always -1 — SetPivotObject(..,0xffffffff) at pc:92087/97551/97934 — so the head pivot is identical); ComputeDesiredPose (:670-690) = pivot - boomForward*D*cos(pitch) + up*D*sin(pitch); the sought lerps FROM the previously swept viewer (:233-251 = CameraManager::UpdateCamera 0x00456660 → viewer_sought_position), snapped at 0.0004 m.
|
||||
(c) Sweep: :258-289 → ICameraCollisionProbe.SweepEye → PhysicsCameraCollisionProbe.SweepEye (src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:24-105) = SmartBox::update_viewer 0x00453ce0 (pc:92761-92892): cellId==0 → snap to player (retail pc:92775); indoor → PhysicsEngine.AdjustPosition(playerCell, pivot) (PhysicsEngine.cs:1813-1855 → CellTransit.FindVisibleChildCell :1143-1190 = CEnvCell::find_visible_child_cell 0x0052dc50 stab-list mode) seats the START cell at the head pivot, fallback player cell (retail pc:92832-92845); then ResolveWithTransition(begin=pivot-(0,0,0.3), end=sought-(0,0,0.3), startCell, r=0.3, h=0, no step-up/down, body null, flags IsViewer|PathClipped|FreeRotate|PerfectClip=0x5c, selfEntityId) (PhysicsEngine.cs:1882+; InitPath TransitionTypes.cs:1164 places sphere 0 at (0,0,r), so the sphere CENTRE travels pivot→sought exactly like retail's (0,0,0)-centred viewer_sphere r=0.3, pc:93314).
|
||||
(d) Which geometry: Transition.FindTransitionalPosition (TransitionTypes.cs:1410-1631 = CTransition::find_transitional_position 0x0050bdf0): viewer steps of exactly one radius anchored at the start; per step TransitionalInsert(3) (:1967 = transitional_insert 0x0050b6f0) → InsertIntoCell(CheckCellId) (:2348 = insert_into_cell 0x00509e70) → FindPrimaryCellCollisions (:2426): environment = ONLY the carried cell's flat physics BSP (FindEnvCollisions :3217, GetCellStruct(primaryCellId) = CEnvCell::find_env_collisions 0x0052c130 → BSPTREE::find_collisions 0x0053a440 PathClipped branch → collide_with_pt 0x0053a090 PerfectClip = FlatBspQuery.CollideWithPoint :1541-1585), building channel (land cells only, :4210, ONE building per origin land cell = CSortCell::find_collisions 0x005340a0), then that cell's shadow objects. On OK → RunCheckOtherCellsAndAdvance (:3444 = check_other_cells 0x0050ae50): CellTransit.FindCellSet (:1274 → BuildCellSetAndPickContaining :1300-1560 = CObjCell::find_cell_list 0x0052b4e0): the carried cell at index 0, then for each array cell FindTransitCellsSphere (:90-250 = CEnvCell::find_transit_cells 0x0052c820): a LOADED neighbour is added when the sphere intersects the neighbour's CELL BSP (:186-205), an unloaded one by plane hint, an EXIT portal whose plane the sphere straddles (|d|<r+F_EPSILON) adds every land cell the sphere overlaps; CheckOtherCells (:2834) collides every other candidate (indoor: its flat physics BSP; outdoor: ValidateWalkable terrain + FindBuildingCollisions + objects); then the carried cell is retargeted to the ordered containing pick (interior-wins; the outdoor XY column only if an exit was straddled; no container → keep current cell / lateral stab recovery :1500-1560). So: neighbour cells through portals ARE consulted (via the cell-BSP overlap), the landblock's land cells only after an exit straddle, and the building shell only from its origin land cell.
|
||||
(e) Beyond the cell through an opening: the neighbour enters the candidate set while the sphere overlaps it, is collided as an other cell, and becomes the carried cell once the centre is inside; through an exit the outdoor column is picked once the centre leaves every interior cell (replay: the open y=24 face lets the eye out to 0xF418000A with no collision because the tower shell 0x01001FB7 is registered only at its origin cell 0xF4180009 — identical to retail's CSortCell rule).
|
||||
(f) Start inside a wall: step 1 returns Collided/Adjusted → ValidateTransition (:5800 = validate_transition 0x0050aa70) reverts CheckPos to CurPos (the pivot), PathClipped+normal breaks the loop (:1620) → Ok=true, eye = pivot, ViewerCellId = start cell — same as retail.
|
||||
(g) Result and fallbacks: ResolveResult(sp.CheckPos, sp.CurCellId, Ok) (PhysicsEngine.cs:2280-2296); Ok can be false only for CurCellId==0 or an Invalid state (:1970 is the only Invalid source), so fallbacks 1/2 (:96-104, retail pc:92878-92887) are dead code in practice — as in retail, where find_valid_position fails only for a null begin cell.
|
||||
(h) Root: RetailChaseCamera.ViewerCellId = swept cell (:266-271); a 0 result is replaced by the PLAYER cell (:284-288). RuntimeWorldFrameRootSource.Resolve (WorldRenderFrameBuilder.cs:262-340): viewerRoot = CellVisibility.TryGetCell(viewerCellId) (registry = all VisibilityCells of a committed landblock, LandblockRenderPublisher.cs:295); WorldRenderFrame.ClipRoot => Roots.ViewerRoot ?? Buildings.OutdoorNode (:78, AD-21); WorldSceneRenderer (:179-225) → RetailPViewRenderer.DrawInside; walk root = ctx.ViewerCellId (RetailPViewRenderer.cs:188-205: interior id must be in the walk registry or it THROWS) → RetailFrameWalk.WalkFrame (Walk/RetailFrameWalk.cs:111: (id & 0xFFFF) < 0x100 → outdoor default view, else interior flood from the camera cell; portal admission = WalkPView seed side test :95-125 = the retail side==portal_side test). Retail: SmartBox::RenderNormalMode 0x00453aa0 (pc:92635-92660) roots on viewer_cell (a CObjCell*, land or env; the 'edi_2' mush is the viewer_cell==null test → landscape-only frame from viewer.objcell_id).
|
||||
(i) Retail pattern verified line by line: update_viewer → AdjustPosition(pivot in the player's cell, viewer_sphere, stab) → makeTransition → init_object(player,0x5c) → init_sphere(1,&viewer_sphere,1) → init_path(cell, pivot, sought) → find_valid_position → set_viewer(sphere_path.curr_pos,0); viewer_cell = sphere_path.curr_cell; fallbacks AdjustPosition(sought) then set_viewer(player,1)+viewer_cell=null.
|
||||
OFFLINE REPLAY (real engine + real DATs, F418's 24 interior cells, both CollisionTraversalMode.Graph and .Flat; scratch programs under the session scratchpad camsweep/ and walkrep/): at the owner's pose (feet 39.503,19.589,181.119 heading +y; pivot z=182.62) AdjustPosition seats the start in 0xF4180114 (the three stair cells are stacked 1.9 m layers: 0x112 z169.8-180, 0x113 z180-181.9, 0x114 z181.9-190.7, all with full-face exits at y=24 and 0x114 with a 4 m archway at y=16.15, x 33.98-38.02); a straight-back boom at every pitch 16.7-55 degrees stops on 0x114's east pier (physics polys 13/23/24, eye y=16.448, n=(0,1,0), pulled in 3.9-6.1 m) and at 70 degrees under the vault (n=(-0.8,0,-0.6)) — inside 0x114, ViewerCellId=0x114, Ok=true; seeding from the player cell self-heals to 0x114 at step 1; the production RetailFrameWalk from root 0x114 at every one of those eyes floods exactly [0x114,0x113,0x112] with 7 exit views, then the landscape and the buildings.
|
||||
|
||||
## Differences from retail (each with reachability)
|
||||
|
||||
- 1. Sweep flags: retail init_object(player,0x5c) also ORs IsPlayer 0x100 (OBJECTINFO::init 0x0050cf30 pc:274435 when weenie->IsPlayer), ethereal from the player's physics state and step_down = !MISSILE; acdream passes only 0x5c (PhysicsCameraCollisionProbe.cs:59-67, deliberate) and sets ObjectInfo.StepDown=true unconditionally (PhysicsEngine.cs:1897). Reachable effect: none for env/cell walls (only the PvP exemption block in object collisions).
|
||||
|
||||
- 2. Containing-pick failure: retail check_other_cells (0x0050ae50 pc:272762-272795) leaves check_cell NULL when no cell contains the centre, every later transitional_insert returns OK without collision (0x0050b6f0 pc:273140), and update_viewer publishes viewer_cell=NULL; acdream keeps the CURRENT cell and keeps colliding against it (CellTransit.cs:1500-1560 'retail keep-curr' plus a non-retail lateral stab recovery) — acdream is MORE constrained, never less.
|
||||
|
||||
- 3. Null viewer cell root: retail RenderNormalMode treats viewer_cell==NULL as outside and draws the landscape-only frame from viewer.objcell_id (pc:92645-92660); acdream replaces a 0 result with the PLAYER cell (RetailChaseCamera.cs:284-288, the FW3 fix). Only reachable through fallback 2 / cellId==0.
|
||||
|
||||
- 4. Fallback-1 seed: retail AdjustPosition(&viewer_sought_position) re-seats at the sought eye's OWN objcell_id (pc:92878); acdream seeds from the player cell (AD-20, PhysicsCameraCollisionProbe.cs:96-101) and its outdoor snap requires SeenOutside on the seed. Dead in practice (Ok is false only for CurCellId==0/Invalid).
|
||||
|
||||
- 5. Unloaded-neighbour admission: retail adds a portal neighbour whose CEnvCell is not loaded only when a sphere straddles the portal PLANE (pc:310060-310110); acdream's hint uses a PortalSide-directed half-space with a 0.02 m pad (CellTransit.cs:222-240). Irrelevant here (all 24 F418 cells resident).
|
||||
|
||||
- 6. Outdoor other-cell branch: acdream skips a land cell's building and object channels when the terrain sample's cell id differs from the candidate id (TransitionTypes.cs:2878-2880 'continue'); retail CLandCell::find_collisions always runs env → CSortCell building → objects. Fires only at landblock edges.
|
||||
|
||||
- 7. Building shell coverage is IDENTICAL (one building per ORIGIN land cell: CacheBuilding keyed by ComputeOutdoorCellId(building origin) = CLandBlock::init_buildings 0x0052fd80 → add_to_cell/CSortCell::add_building), so both clients let the viewer sphere leave the tower through the open y=24 face into land cell 0x000A without touching the shell (replay: eye (39.5,30,186), cell 0xF418000A, no hit).
|
||||
|
||||
- 8. PerfectClip stop: retail collide_with_pt (0x0053a090) adjusts the check pos onto the contact and returns ADJUSTED, insert_into_cell re-tests up to 3 times; acdream CollideWithPoint (FlatBspQuery.cs:1541-1585) + InsertIntoCell (:2348) reproduce it — the replay lands the eye at exactly r=0.3 from the pier. No difference.
|
||||
|
||||
- 9. Camera adjustment cadence: acdream scales the offset continuously per frame (CameraFrameController.cs:78-91) vs retail's per-keypress 0.2/8-degree units; same limits (|x|,|y|<10, -1.8<z<450), same envelope.
|
||||
|
||||
- 10. Root registry: retail roots on the CObjCell* itself; acdream roots on the id — an interior id absent from the walk registry throws (RetailPViewRenderer.cs:196-205), an outdoor/unregistered id becomes the synthetic OutdoorNode (AD-21). Equivalent for the resident F418 cells.
|
||||
|
||||
- 11. Frame ownership: the sweep runs on the presented (30 Hz object clock) player position while the player cell is the physics cell (CameraFrameController.cs:130-146) — a seam-frame skew retail cannot have; it is corrected by the pivot re-seat every frame, but it is the one place the running-down symptom can originate (see open questions).
|
||||
|
||||
## Root cause
|
||||
|
||||
Not reproduced as a camera-versus-cell difference. Every enumerated difference is either unreachable at this pose or makes acdream MORE constrained than retail, and the offline replay with the real engine and real DAT geometry (both graph and flat traversal) shows the retail-equivalent outcome for the reported input: the head pivot (z=182.62) already sits in the 9 m vaulted upper cell 0xF4180114 (the stair is a stack of three 1.9 m-thin cells), a straight-back boom stops on 0x114's east pier at y=16.448 for every pitch up to 55 degrees and under the vault at 70 degrees (exactly the 'just under the stairwell ceiling' retail eye the owner describes), ViewerCellId=0x114, and the production walk from root 0x114 at each of those eyes floods 0x114, 0x113 and 0x112 with 7 exit views — the stairwell walls are admitted. The only ways the eye leaves the stairwell in the model are two REAL openings (the 4 m archway at y=16.15, x 33.98-38.02, needing a yawed boom; the open y=24 face behind a player facing up the stairs), and retail's sweep passes through both as well (difference 7). The evidence that singles this out: the physics polygon dump of 0x114 (28 polys incl. the pier at x 38.02-40.69 and the vault slopes), the [zoom 16.7/24.7/40/55/70] sweep rows (eye y=16.448 / cell 0x114 / hit) under CollisionTraversalMode.Flat, and the walk DrawCells rows [0xF4180114,0xF4180113,0xF4180112] at the same eyes. Therefore the frame is produced downstream of a correct root and flood — the interior draw with seven exit views (landscape flush, Z clear and exit seals gated on outside_view.view_count>0, PView::DrawCells 0x005a4840; the S4 depth/alpha domain) or by a production-only state the replay cannot see (unpublished collision cells → no stop; a boom through an opening) — and #464's 'camera-versus-cell' classification is not supported by the sweep or root code.
|
||||
|
||||
## Recommended fix
|
||||
|
||||
Do not port anything into the camera for #464 yet: the sweep and the root rule already match update_viewer/find_valid_position/RenderNormalMode at this pose. First classify with one frame at the owner's pose and zoom: ACDREAM_PROBE_FLAP=1 ([flap-sweep]: start/ok/in/out/viewerCell/pulledIn from PhysicsCameraCollisionProbe.cs:77-90) plus ACDREAM_PROBE_FACILITY_STAIRS=1 ([cathedral-stair]: viewer/player/root/eye/rootFlood/visited/lookIn from RetailPViewRenderer.cs:383-410) and the acdream screenshot next to the owner's retail one. Expected (per the replay): out.y=16.448 or the vault, viewerCell=root=0xF4180114, rootFlood including 0x113/0x112 — then #464 is an interior-root draw defect with seven exit views and moves to S4 chunk 2 (retail alpha-depth transcript at THIS pose from the oh-capture kit, the landscape-flush/Z-clear/seal order under view_count>0, no camera change); the matrix row's invariant should be re-worded from 'the eye stays inside' to 'root 0x114 with 0x113/0x112 flooded draws the stairwell solid'. If instead the probe shows out beyond the pier/arch plane with viewerCell 0x113/0x114, the bounded retail-faithful port is difference 2 in CellTransit/RunCheckOtherCellsAndAdvance (a null check_cell after a failed containing pick, viewer_cell=null published, and the RenderNormalMode null-viewer landscape frame instead of the player-cell substitution at RetailChaseCamera.cs:284-288), which is S2 membership-owner scope. Either way, promote the two scratch replays (camera sweep at the stair pose stops on 0x114's pier under Flat traversal; walk from root 0x114 floods 0x114/0x113/0x112) into InstalledDat pins so the eventual fix cannot regress the camera side.
|
||||
|
||||
## Slice
|
||||
|
||||
S4 chunk 2 (the plan's own matrix row already places #464 under S4 / owner gate G3) once the probe confirms root 0x114 with the eye inside — the camera-versus-cell hypothesis should be retired from the row; only if the probe shows the eye outside the stairwell with an interior viewer cell does it become an S2 (membership owner: CellTransit containing-pick / viewer_cell null semantics) item.
|
||||
|
||||
## Open questions
|
||||
|
||||
- The exact boom the owner used: heading/yaw offset, number of Raise presses and zoom presses. A retail cdb capture at the pose with the oh-capture walk template (the P line = viewer eye + objcell_id, the EC/DC lines = flooded cells) settles where retail's eye stops (pier vs vault) and which cells retail floods; if retail's eye is at (39.5,16.45,z) inside 0x114 the camera side is closed.
|
||||
|
||||
- One acdream frame at the pose with ACDREAM_PROBE_FLAP=1 and ACDREAM_PROBE_FACILITY_STAIRS=1 (both documented in docs/launch-options.md:319/:333): the [flap-sweep] out/viewerCell and the [cathedral-stair] root/rootFlood lines classify the defect in one line (eye inside + root 0x114 + flood 113/112 = draw side; anything else = membership/camera).
|
||||
|
||||
- Did the boom pass through a real opening (the archway at y=16.15 x 33.98-38.02 with a yawed camera, or the open y=24 face with the player facing up the stairs)? Both clients' sweeps pass those (the tower shell 0x01001FB7 collides only from land cell 0xF4180009); the owner's retail screenshot should be re-read for the eye position in that case.
|
||||
|
||||
- The running-down symptom (not characterized): during the descent the viewer cell hops 0x114 → 0x113 → 0x112 as the default eye (feet+2.25 m) crosses the horizontal portal planes z=181.895 (feet ≈ 179.65) and z=180 (feet ≈ 177.75) while the pivot is in yet another layer; at those frames the eye is within 0.3 m of a horizontal portal whose seed clip degenerates. Capture [cathedral-stair] root changes across the lead's route-464-run (which shot only screenshots, no probes) and check whether the player's own cell drops out of rootFlood for a frame; the presented-position vs physics-cell skew (difference 11) is the other candidate.
|
||||
|
||||
- Whether the owner's Release build had F418's collision cells published at the moment of the zoom (a [flap-sweep] 'bsp=noroot'/'resolved=n' or ok=False would show an unpublished-cell no-stop, which the replay cannot reproduce).
|
||||
|
||||
- S4 side: the retail alpha-depth/walk transcript at exactly this eye (seven exit views from an interior root) does not exist in the oh-capture kit; it is the capture S4 chunk 2 needs if the probe confirms root 0x114.
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue