diff --git a/docs/research/2026-07-06-180-camera-sweep-strobe-handoff.md b/docs/research/2026-07-06-180-camera-sweep-strobe-handoff.md new file mode 100644 index 00000000..acd81325 --- /dev/null +++ b/docs/research/2026-07-06-180-camera-sweep-strobe-handoff.md @@ -0,0 +1,156 @@ +# Pickup handoff — #180 camera-sweep eye strobe (the #176 "stripes") — port retail's stateful sought-position + +**Paste the companion prompt (bottom of this file / the session log) into a fresh +session.** Read this file, then `claude-memory/project_physics_collision_digest.md` +(top banner) + `claude-memory/project_render_pipeline_digest.md` (top banner), then +**ISSUES #180** (and #176's gate-2 note). + +## TL;DR — what #180 is + +The user's #176 residual — "geometrical patterns, triangles, that get weird +stripes… especially when I push the camera to the wall or the openings; if I zoom +out and the camera does not touch the walls I get no pattern" — is **NOT a render +defect. The camera-collision sweep is BISTABLE while the compressed chase boom +moves along a wall**, and the per-frame view hard-cuts tear-interleave on screen: + +> `[flap-sweep]` (launch-176-cameye.log, frames ~38607–38861): consecutive sweeps +> whose INPUT moved **~1.4 mm** (the player's glide) flipped the OUTPUT **0.27 m +> along the boom** — `pulledIn` 0.27 ↔ 0.53, eye (78.500,−38.633,−3.845) ↔ +> (78.669,−38.815,−3.938) — re-flipping every ~5–10 frames. All 368,357 sweeps +> returned `ok=True` (this is NOT the fallback path — the sweep itself has two +> first-contact solutions at an r±ε graze on the corridor's double-faced slabs, +> the #137 threshold family, camera edition). At ~1700 fps with no vsync, each +> monitor refresh interleaves scanline slices of BOTH views → the fine +> stripe/hatch patterns (worst near seams where the 0.27 m parallax is largest), +> flickering with movement. Parked camera = static (all prior "camera stable" +> evidence was parked-camera; the strobe needs motion + a compressed boom). + +This very plausibly also produced the ORIGINAL #176 seam flashes (one of the two +alternating eyes sees the purple under-room parallax, the other doesn't). + +## Eliminated for the stripes (DO-NOT-RETRY — all isolation-killed) + +| Ruled OUT | How | +|---|---| +| Texture / grout-line interaction | `ACDREAM_LIGHT_DEBUG=3` (raw vLit field, texture ignored) still shows the pattern | +| The per-cell shell clip trim (gl_ClipDistance) | `ACDREAM_CLIP_DEBUG=1` (shells drawn WHOLE, slots forced 0) — user: "Not gone, still there" | +| Coplanar dat geometry | 2 cm-epsilon sweep: corridor floor (z=−6) clean; the only stack is the z=−12 under-hall quad×4 — SAME-mesh, bit-identical depth ⇒ stable winner, cannot moiré | +| Shell double-instance / floor-coincident entity / portal seals | `[seam-*]` probe: n=1 per cell at the lifted z; portal entities at z=−12.05; zero depth fans (sealed dungeon) | +| Light-set churn under movement | `[seam-blk]`: the floor's applied set held the SAME 8 identities through a 148k-frame session incl. ~12k distinct positions | +| The light pool tracking the camera | FIXED `d8984e87` (resident collection, player anchor) — verified: flood churned 8→41 across a full turn, set constant. Do NOT re-open; re-gate VISUALLY only after #180 | +| SweepEye itself misporting update_viewer | Read side-by-side: flags 0x5c, AdjustPosition seating, fallbacks all match 0x00453ce0. The bistability is in the transition's contact solution, not the port shape | + +## The retail anchor (the fix direction — read BEFORE coding) + +Retail's camera target is **STATEFUL**; ours re-demands the full boom every frame: + +- `SmartBox` per-frame (pseudo-C line ~91842, addr 0x00452d75): + `CameraManager::UpdateCamera(this->camera_manager, &ret, &this->viewer)` — the + argument is the **CURRENT swept viewer**, and the RETURN is assigned to + `this->viewer_sought_position` (0x00452d84). So the next frame's sweep target + derives from the collided position — the target CONVERGES onto the wall + contact and re-extends gradually; the knife-edge full-length ray is not + re-rolled per frame. That is why retail's camera glides along walls instead of + strobing. +- `CameraManager::UpdateCamera` = **0x00456660** (pseudo-C line ~95505). Only the + head was read this session (offset-movement integration, pivot query); the + sought-position derivation in the tail is UNREAD — **read it fully, write the + pseudocode doc (workflow step 3), then port**. +- `SmartBox::update_viewer` = **0x00453ce0** (line ~92761) — already faithfully + ported by `PhysicsCameraCollisionProbe.SweepEye`; leave it alone. +- **No damping band-aid on the swept result without the UpdateCamera reading** + (the no-workaround rule; the user has already reverted redesigns that replaced + retail mechanisms). + +Port site: `src/AcDream.App/Rendering/RetailChaseCamera.cs` (the desired-eye / +boom derivation) — it must consume the previous swept viewer per retail. + +## Also open from this arc + +- **Site-A weenie light-registration LEAK**: `[seam-ent]` showed one portal cell's + I100 light ×2 → ×4 within a session — re-CreateObject re-registers the light + under a fresh `entity.Id` (GameWindow ~3679: `Id = _liveEntityIdCounter++`) + without tearing down the previous registration. The wash intensifies over + time. Fix with #180's gate or fold into A7 (noted in ISSUES #176). +- **#176 re-gate** happens only after #180 lands: camera pressed into + walls/openings while moving → no patterns; corridor seam turn → steady purple + wedge (the faceted SHAPE is cdb-proven retail — only instability is a defect). +- After BOTH pass: strip the throwaway probes in one cleanup commit — + `[seam-*]` (ACDREAM_PROBE_SEAMDRAW), `ACDREAM_LIGHT_DEBUG` modes, + `ACDREAM_CLIP_DEBUG`. Keep `[flap-sweep]` (it rides ACDREAM_PROBE_FLAP). + +## Apparatus (reuse, don't rebuild) + +- `ACDREAM_PROBE_FLAP=1` → `[flap-sweep]` per-sweep line: `desiredBack/eyeBack/ + pulledIn/in=/out=` — THE #180 instrument (plus [flap]/[flap-cam]). +- `ACDREAM_PROBE_PVINPUT=1` → per-frame `[pv-input] eye=` at 6 dp. +- **The autonomous visual loop** (no user needed, works where RenderDoc can't — + RenderDoc doesn't support GL_ARB_bindless_texture and the modern gate throws): + 1. Launch via `dotnet run` (background) with probes; wait ~12 s. + 2. Input: `keybd_event` **with real scan codes + KEYEVENTF_EXTENDEDKEY** + (GLFW decodes scancodes; vk-only silently does nothing). Arrows: Left + 0x25/0x4B, Right 0x27/0x4D, Down 0x28/0x50. `SetForegroundWindow` first, + verify with `GetForegroundWindow`. Hold-key = down, Sleep, up. + 3. Capture: `GetWindowRect` + `Graphics.CopyFromScreen` → PNG in the + scratchpad → **the Read tool renders it** (take 3 captures ~150 ms apart — + frame-pair diffs expose per-frame strobes). + 4. Repro recipe: back the character into a wall (hold Down ~2.5 s) — the boom + compresses; the strobe fires DURING/just after motion near the wall. + 5. Close gracefully via `CloseMainWindow()` (ACE session clears in ~5 s). +- Launch logs on disk (worktree root): `launch-176-cameye.log` (THE strobe + capture), `launch-176-user-gate.log` (148k-frame user session incl. the light + leak), `launch-176-seamdraw.log`, `launch-176-fix-verify.log`. +- `ACDREAM_LIGHT_DEBUG=1/2/3` + `ACDREAM_CLIP_DEBUG=1` — shader/clip isolation + toggles (throwaway; strip at cleanup). + +## State + +- `main` = worktree branch `claude/vigorous-joliot-f0c3ad` = **`6aabe0b5`** + (lighting fix `d8984e87` + isolation apparatus `6aabe0b5`). Suites green + (Core 2599+2skip / App 726+2skip / UI 425 / Net 385). +- Register: AP-85 rewritten (resident+player-anchored pool; residual = single + 128 pool vs retail 7-dyn/40-static). The a7 pseudocode doc carries a + correction banner (`visible_cell_table` = RESIDENT registry — the + identifier-name-≠-semantics lesson, memory + `feedback_bn_decomp_field_names` class 4). +- A client may be running — the user manages its lifecycle; ASK before + rebuilding if the App DLL is locked (MSB3027 = lock, not code). +- The test char spawns in the Facility Hub corridor (cell `0x8A020164`). + +## Companion prompt (paste into the fresh session) + +``` +Fix #180 — the camera-collision sweep strobes the eye 0.27m per few frames when +the compressed chase boom moves along walls; at ~1700fps the tear-interleaved +views ARE the #176 "stripes/triangles" the gates keep failing on. The mechanism +is probe-pinned; every render suspect is isolation-killed. Do NOT re-investigate +the render, the light pool (fixed d8984e87, data-verified), or SweepEye's port +(faithful, exonerated). + +READ FIRST, in order: +1. docs/research/2026-07-06-180-camera-sweep-strobe-handoff.md (evidence, + DO-NOT-RETRY table, retail anchors, the autonomous visual-loop recipe) +2. claude-memory/project_physics_collision_digest.md (top banner) +3. docs/ISSUES.md #180 (+ #176's gate-2 note) + +THE FIX = port retail's STATEFUL camera target: SmartBox (0x00452d75) feeds the +CURRENT swept viewer into CameraManager::UpdateCamera (0x00456660) and assigns +the RETURN to viewer_sought_position (0x00452d84) — the target converges onto +the wall contact instead of re-rolling the full knife-edge boom ray per frame +like RetailChaseCamera does. Workflow: read UpdateCamera's tail (the sought +derivation — UNREAD so far, pseudo-C line ~95505), write the pseudocode doc, +port into RetailChaseCamera. NO damping band-aids on the swept result. + +Verify autonomously with the handoff's visual loop (keybd_event scan-codes + +GDI window capture + [flap-sweep]): back into a wall, confirm consecutive-frame +eye deltas stay continuous and no stripe patterns in captures. Then the user +gate covers #180 AND the #176 re-gate (steady purple wedge at the seam — the +wedge SHAPE is retail, cdb-proven). After both pass: fix the site-A light- +registration leak (one portal light stacked ×4/session — re-CreateObject +re-registers under fresh entity ids) and strip the throwaway probes +([seam-*], ACDREAM_LIGHT_DEBUG, ACDREAM_CLIP_DEBUG) in a cleanup commit. + +State: main @ 6aabe0b5, suites green (Core 2599+2skip). Test char spawns in the +Facility Hub corridor 0x8A020164. A client may be running — the user manages +its lifecycle; ASK before rebuilding if the App DLL is locked. +```