D300: hipfire direct camera look (WI-1); D243 M-159/160/161 investigation

D300: new hipDirectCompute() in port/src/input.c writes vv_theta/vv_verta
directly for hipfire mouse-look, mirroring GEPD's linear px->degree model
and the existing D194 aim-mode pattern -- replaces the N64 stick-curve
routing that saturated at ~13 px/poll (issue #89). Ported GEPD's
dead/watch/pause safety gates. Input.MouseDirectLook=1 default, escape
hatch to the legacy path. User-confirmed better feel in a live playtest.

D243: added a permanent onscreen-visibility probe (D243M: onscreen,
chr.c) to test whether chrTick's posIsOnScreen gate explains the
abseil cutscene's frozen render position (M-159). A live capture
refuted that and found the real mechanism instead: chrTick overwrites
prop->pos with a stale value via its own chrUpdateAnim/modelTickAnim
animation-position logic, racing the AI script's scripted write
(M-160). A decisive throwaway experiment neutralizing that overwrite
produced no visible change in a live playtest (M-161) -- the mechanism
is real but isn't the dominant visible cause. Experiment reverted;
D243 parked pending a new lead after ~15 session-passes without a fix.

Docs: findings.md/findings-index.csv/porting-notes.md/GE-ENV-PROBES.md
updated throughout.
This commit is contained in:
James Dansereau committed 2026-09-17 10:56:41 -04:00
1 parent 7b405abc63
commit fec396f53b
8 files changed
+168 -5

No files matched your search

+1
View File
@@ -26,6 +26,7 @@
| `port/src/` | Shims: `libultra.c` (OS API), `gesched.c` (scheduler), `n64stubs.c` (boot/TLB/FPU/rmon), `random.c` (PRNG ported verbatim from `random.s`), `ucode.c` (microcode segment markers), `main.c`, `video.c`, … |
| `port/fast3d/` | Software RSP (adapted from the PD port). The main Phase 2 work. |
| `rsp/graphics/gmain.s` | GE's RSP ucode — ground truth for GBI/CC/RM. |
| `reference/mouse-injector/` | Vendored GEPD-Edition Mouse Injector source (GPLv2, read-only reference — never compile or modify). Ground truth for the mouse-aim model: `games/goldeneye.c` is the whole GE aim/hipfire/menu model (our D194 mirror); see its README + `docs/dev/GEPD-INPUT-PLAN.md`. |
| A local **Perfect Dark PC port** checkout ([fgsfdsfgs/perfect_dark](https://github.com/fgsfdsfgs/perfect_dark)) | **Standing reference** — consult it whenever a work item has a PD analogue (same Rare engine family): port-layer ground truth (`port/fast3d/`, crash/system/video), plus copy candidates `port/src/preprocess/` (N64→PC asset conversion; `filemodel.c` is the D43 near-analogue) and `mixer.c`/`input.c`/`fs.c`. Port-layer files only; same family ≠ identical format — validate per field. Full audit: `docs/internals.md` §2.4. |
## Build
+1 -1
View File
@@ -45,7 +45,7 @@ Two classes:
| `GE_D160` | `src/boss.c:744`, `src/game/bondview2.c:791`, `src/aicommands.def:10055,10181,10236,10242,10247` (all `#ifdef PORT`) | Dam exit-cutscene (D148/D160) diagnostic: trace points in `bossReturnTitleStage`, `bondviewSetCameraMode`, and AI cmds `EndLevel`/`exit_level`/`CameraLookAtBondFromPad`/`CameraSwitch`. **Investigation in progress** — user owes a `GE_D160=1` Dam run. | **live** |
| `GE_D243` | `src/game/front.c` (`menu_init`, cached `s_d243`) | Per-menu-transition trace with a local per-tick frame counter (D243 stale-DL-burst screen identification). Counter is NOT numerically identical to the port's internal frame count — cross-reference by elapsed offset. | **live** (D243) |
| `GE_D243CAM` | `src/game/bondview2.c` (`bondviewFrozenCameraTick`, three `CAMERAMODE_POSEND` branches) | Dumps eye/look-at positions (+theta/verta on the `gBondViewCutscene` branch, +dt/angle/pad on the orbit branch) per call. **Found M-143**: the `gBondViewCutscene` branch fires and its look-at (`field_3C4/8/C`) sweeps through a filter catch-up after the cutscene warp — the shake mechanism. | **live** (D243) |
| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) | M-151/M-154 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)). Silent during FPS play. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) |
| `GE_D243M` | `src/game/bondview2.c` (`solo_char_load`, `bondviewRemovePlayerBody`, `playerTick`; cached `d243mEnabled`) + `src/game/objecthandler.c` (`drawjointlist`, both region variants) + `src/game/chr.c` (`chrTick`, `after_position_update:`) | M-151/M-154/M-159 probe for the Dam-abseil "frozen Bonds + shake": `D243M: CREATE/REMOVE` at both `bodyModel` sites; `D243M: tick` (idx/model/**propptr/chrptr**/cam modes/`prop->pos`/`field_488.pos`/`field_3B8`/`field_3C4-8-C`/**rp** = render_pos pointer, **never dereferenced** — M-154 fix 2: tick-time `render_pos` holds arbitrary stale slot contents until the first draw pass, so the pre-view root translation is logged by `chrdraw` only) while the model exists or POSEND/INTRO is active; `D243M: draw` per on-screen player render submission; `D243M: chrdraw` — M-154 per-frame model-draw census, one line per CHR render submission (Model* + raw root translation) while a scripted camera mode (POSEND/INTRO/SWIRL/FADESWIRL) is active; a second Bond instance = extra lines with *distinct* model pointers at the same frame (same model is drawn twice/frame — two passes — dedupe by (frame, model)); `D243M: onscreen` (M-159, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (the gate that controls whether `render_pos` refreshes that tick) + the live `prop->pos`, to test whether freeze/teleport boundaries in the tick/chrdraw data line up with `vis` flipping 0→1. Silent during FPS play. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) |
| `GE_D193` | `src/game/frametiming.c` (`waitForNextFrame`, `#ifdef PORT`) | Per-wall-second line: `render=` rendered frames, `sim=` sim ticks delivered (Σ of the `deltaFrames`/`g_ClockTimer` fed downstream), `clamped=` frames whose catch-up hit `FRAMETIMING_PORT_MAX_CATCHUP`, `maxraw=` largest pre-clamp delta. Healthy steady state = `render=30 sim=60 clamped=0 maxraw=2` (measured M-80, all levels idle). Use to check whether the wall-clock timer under-advances under real AI load (D193). Cheap; safe for a full playtest. | **live** (D193) |
| `GE_D193A` | `src/game/chr.c` (`chrTick` → `d193aSample`, plus `d193aMoved` in `chrUpdateAnim`; `#ifdef PORT`) | Per-wall-second per-ticked-chr locomotion telemetry (D193/D209): `chr=` chrnum, `act=` ACT_TYPE, `speed=` world-units travelled/s, `gtd=` g_GlobalTimerDelta, `tier=` `act_gopos.unk59` SPEED tier (0 walk / 1 run / 2 sprint; `-1` when not ACT_GOPOS), `gspd=` `act_gopos.speed` (**a turn rate, not a travel rate** — 0 means running straight), `typ=` PROP_TYPE (3=CHR, 6=VIEWER), `tick=`/`move=`/`frz=` chrTick entries vs `chrUpdateAnim` calls vs freeze-gated (move « tick = the chr is being visibility-culled out of its own locomotion; `move=0` at high `speed` = WAYMODE_MAGIC), `hid=` CHRFLAG_HIDDEN, then the root-motion scalars `scale` / `xlscale` / `animrate` / `playspeed` / `mspeed` (model->speed) / `endframe` / `anim` ptr / `aidx=` that pointer reverse-resolved to its `animation_table_ptrs1` index (40 walking, 41 sprinting, 42 running, 107 walking_unarmed — map via `src/game/initanitable.c`). `GE_D193A_CHR=<n>` restricts to one chrnum. **This probe found D209**: Cradle showed `tier=1` (RUN) with `aidx=40/107` (walk anims) on every sample. | **live** (D193/D209) |
| `GE_D193B` | `src/game/model.c` (`sub_GAME_7F06D3F4`, `#ifdef PORT`) | Rate-limited (1/97) raw anim root-motion decode trace (D193): `joint=` / `flip=` / `base=` mtx channel index / `frame=` / `tmp=` decoded s16 translation triple from the anim bitstream / `angle=` / `anim` ptr / `bd=`+`bs=` the D32 u32 bitDescriptors/bitStream addresses / `njoints`. M-80 Facility: `base=0`, `angle=0` **always** (root-motion facing delta never non-zero — top lead), `tmp` ≈ (±small, ~1086 hip-height, ±small). Compare `tmp`/`angle` magnitudes against a clean N64 decomp build of the same anim+frame. | **live** (D193) |
+2 -1
View File
@@ -139,7 +139,7 @@ D239,?,"QoL ask (M-106, user QA):",CLOSED — executed in the v0.2.0 prep pass (
D240,OPEN,"Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).","OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment."
D241,OPEN,Player gunshot SFX goes silent under some conditions —,"OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug."
D242,?,Player-death sequence UX regression vs N64 original:,NOT A PORT-LOGIC BUG as far as static analysis can tell.
D243,OPEN,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","OPEN — M-144: CONFIRMED PC-only (user, direct N64 comparison) —"
D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):","PARKED (M-161, diminishing returns after ~15 session-passes; resume only with a new lead —"
D245,OPEN,IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) —,"OPEN — REOPENED, M-142 headless probe found the tc-shift k measurably flips during ordinary view rotation (new root-cause evidence); an opt-in GE_D245_FIXEDSHIFT candidate is build-verified but needs…"
D246,OPEN,"Tiny pixel strips at the left and right edges of the screen, ""like overscan"" —","OPEN — one candidate ruled out (M-116 follow-up), root cause not found."
D247,RESOLVED,Black bars top and bottom of screen —,"NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made."
@@ -177,3 +177,4 @@ D296,OPEN,"Control Center, 00 Agent:","OPEN — single live report, un-triaged;
D297,FIXED,v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).,FIXED (M-150) — migration shim landed:
D298,?,Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build —,"s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says ""don't fix, just for context""; not yet investigated —"
D299,FIXED,Game.SkipIntro crashed selecting any save slot except one —,"FIXED — fileValidateSaves() (only called from the skipped legal screen) never ran under SkipIntro, leaving saves[] all-zero; added the missing call to the SkipIntro shortcut (src/game/lv.c, #ifdef PO…"
D300,FIXED,Hipfire look feels non-uniform (issue #89):,FIXED (pending build-verify) —
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
139 D240
140 D241
141 D242
142 D243
143 D245
144 D246
145 D247
177 D297
178 D298
179 D299
180 D300
+37 -2
View File
@@ -546,7 +546,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D240 | **Player gunshot SFX timing/cadence reportedly does not match the N64 original's rate (M-106, user QA report).** — full `## D240` entry at file tail | **OPEN — not root-caused; static call-site tracing only, no runtime verification possible in this environment.** Confidence LOW-MEDIUM that this is a *new* bug distinct from the D155/D186/D193/D204/D209 timing-keystone family (all of which gate the identical `deltaFrames→speedgraphframes→g_ClockTimer→g_GlobalTimer` chain this SFX trigger consumes).… |
| D241 | **Player gunshot SFX goes silent under some conditions — reported on Bunker with the alarm sounding, "or some other factor" — but only for PP7 and AK47, Klobb…** — full `## D241` entry at file tail | **OPEN — not newly root-caused; high confidence this is D207 reproduced, not an independent bug.** Recommend folding this repro into D207 (or closing D241 as a duplicate once D207 is reopened) rather than tracking separately.… |
| D242 | **Player-death sequence UX regression vs N64 original: PC requires an LMB click to proceed to the mission-failed/file-select screen; user recalls N64 auto-plays…** — full `## D242` entry at file tail | **NOT A PORT-LOGIC BUG as far as static analysis can tell.** Confidence HIGH that the LMB-click skip path is faithful original behavior (Z_TRIG-mapped, exists unmodified in `bondview2.c`), not a defect to remove.… |
| D243 | **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle…** — full `## D243` entry at file tail | OPEN — M-144: CONFIRMED PC-only (user, direct N64 comparison) — real N64 does not show this shake. Mechanism (M-143): the `gBondViewCutscene` look-at branch reads `field_3C4/8/C`, a leaky-integrator low-pass filter of Bond's real position that isn't reset when the scripted cutscene warps him, producing a ~1s look-at snap. Filter math is decomp-exact and `g_ClockTimer`-scaled, so the bug must be upstream: either the position warp itself is wrong/bigger on PC, or a call-ordering desync around the `CAMERAMODE_INTRO` transition. M-153: live `GE_D243M` capture (full Dam → abseil; `scratch/d243/m153_dam_abseil_capture.log`) REFUTES the double-instance hypothesis (single model, one draw/frame, no remove/recreate window) and shows the real divergence: the model's rendered position (`field_488.pos` = render readback) freezes on dam-surface points then locks into a strict period-4 cycle through four fixed world points, while `prop->pos` executes the smooth scripted void sweep — the lagged filter (`field_3C4`) chasing that cycle is exactly the observed shake. Next: code dive into `chrTick`→`render_pos` for what produces the four fixed points; D173/D292 are the same family. D146 still fully decoupled (zero hits, 4 runs). M-154: user sharpened the symptom (one new frozen Bond per shot; old copies persist); log re-read corrects M-153 — in phase 3 `prop->pos` itself cycles A,A,B,C through three z-lanes while y falls smoothly, and the rendered model tracks it (per-lane pose offsets), so it teleports every frame rather than freezing; H1 (multiple Model instances) vs H2 (framebuffer accumulation / subcam ghosts) now discriminated by a new `D243M: chrdraw` per-frame model-draw census in `drawjointlist` + extended tick fields (propptr/chrptr/rp); probe crashed twice inside its own `render_pos` read (stale slot contents, not a uniform tombstone) — final fix: tick probe never dereferences render_pos (raw= moved to chrdraw-only where the game guarantees validity); awaiting re-capture. |
| D243 | **Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject): the camera violently shakes/"spazzes" during each of the scripted camera-angle…** — full `## D243` entry at file tail | PARKED (M-161, diminishing returns after ~15 session-passes; resume only with a new lead — see below) — M-144: CONFIRMED PC-only (user, direct N64 comparison) — real N64 does not show this shake. Mechanism (M-143): the `gBondViewCutscene` look-at branch reads `field_3C4/8/C`, a leaky-integrator low-pass filter of Bond's real position that isn't reset when the scripted cutscene warps him, producing a ~1s look-at snap. Filter math is decomp-exact and `g_ClockTimer`-scaled, so the bug must be upstream: either the position warp itself is wrong/bigger on PC, or a call-ordering desync around the `CAMERAMODE_INTRO` transition. M-153: live `GE_D243M` capture (full Dam → abseil; `scratch/d243/m153_dam_abseil_capture.log`) REFUTES the double-instance hypothesis (single model, one draw/frame, no remove/recreate window) and shows the real divergence: the model's rendered position (`field_488.pos` = render readback) freezes on dam-surface points then locks into a strict period-4 cycle through four fixed world points, while `prop->pos` executes the smooth scripted void sweep — the lagged filter (`field_3C4`) chasing that cycle is exactly the observed shake. Next: code dive into `chrTick`→`render_pos` for what produces the four fixed points; D173/D292 are the same family. D146 still fully decoupled (zero hits, 4 runs). M-154: user sharpened the symptom (one new frozen Bond per shot; old copies persist); log re-read corrects M-153 — in phase 3 `prop->pos` itself cycles A,A,B,C through three z-lanes while y falls smoothly, and the rendered model tracks it (per-lane pose offsets), so it teleports every frame rather than freezing; H1 (multiple Model instances) vs H2 (framebuffer accumulation / subcam ghosts) now discriminated by a new `D243M: chrdraw` per-frame model-draw census in `drawjointlist` + extended tick fields (propptr/chrptr/rp); probe crashed twice inside its own `render_pos` read (stale slot contents, not a uniform tombstone) — final fix: tick probe never dereferences render_pos (raw= moved to chrdraw-only where the game guarantees validity); awaiting re-capture. M-159 (static code-dive, no build): proposed `chrTick`'s frustum-visibility gate as the mechanism — REFUTED by M-160's live capture (`vis=1` throughout the observed freeze). M-160 (live `D243M: onscreen` capture, user playtest): found `chrTick` overwriting `prop->pos` with a stale/stepped value every tick via its own `chrUpdateAnim`/`modelTickAnim` animation-position logic (`chr.c:2578-2616`, no POSEND/cutscene check), downstream of and competing with the AI script's smooth write to the same field. **M-161 (decisive experiment, live playtest): NEGATIVE.** Neutralizing M-160's overwrite (save/restore `prop->pos` around `chrTick` during POSEND, `GE_D243X` env-gated hack, reverted after) produced **no visible change** — user: "still happened, looked the same as it always does." M-160's mechanism is real but is not the (or not the whole) visible cause. **Parked** per user's call (diminishing returns after ~15 session-passes) — see D148 umbrella; resume only with a specific new lead. |
| D245 | **IsWater water: two competing patterns with a visible seam that moves as the player looks around (mouse/aim) — "looks almost exactly like skybox" (M-116, user…** — full `## D245` entry at file tail | OPEN — REOPENED, M-142 headless probe found the tc-shift `k` measurably flips during ordinary view rotation (new root-cause evidence); an opt-in `GE_D245_FIXEDSHIFT` candidate is build-verified but needs a live A/B, not shipped as default.… |
| D246 | **Tiny pixel strips at the left and right edges of the screen, "like overscan" — look like raw pixels, not 3D graphics (M-116, user QA report).** — full `## D246` entry at file tail | **OPEN — one candidate ruled out (M-116 follow-up), root cause not found.** Step (1) from the original plan is done… |
| D247 | **Black bars top and bottom of screen — possible aspect-ratio defect (M-116, user QA report).** — full `## D247` entry at file tail | **NOT A BUG — confirmed faithful, unmodified N64 decomp behaviour, no code change made.** `GE_PCDUMP` captures (both against the pre-existing save and a fresh-EEPROM run, to rule out a stale `Screen` option from earlier test sessions) show the exact same inset on Frigate… |
@@ -585,6 +585,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D297 | **v0.3.0 silently wipes ALL pre-fix player saves on first launch (D284 side effect; found during D295/M-148).** — full `## D297` entry at file tail | FIXED (M-150) — migration shim landed: `port/src/legacycrc.c` (isolated, deliberately-wrong pre-D284 PRNG, reviewer-verified bit-exact against `0de99b4d^`) + eeprom-shim read-path hook in `libultra.c` (block 4 only; persists pristine+migrated bytes before the read-time-only AllUnlocked patch). Verified: user's #87 artifact → `D297-migrated 5`, all slots OK, idempotent on relaunch; native post-fix save untouched; corrupt slot left to the game's own wipe path; fresh eep untouched; 21/21 sweep PASS. Block-0 header needed no migration (factory seal, verified). |
| D298 | **Full HUD (not just the aim-mode crosshair) missing in a v0.3.0 dev build — user suspects a debug-launch artifact and asked NOT to raise it until seen in a regular release-targeted build; parked** — full `## D298` entry at file tail |s — regression reported 2026-09-16 during v0.3.0 work (build ce8427cf); user says "don't fix, just for context"; not yet investigated** — full `## D298` entry at file tail |
| D299 | **Game.SkipIntro crashed selecting any save slot except one — reproduced on a fresh save and on a clean Release rebuild, ruling out stale save data (found + fixed 2026-09-17).** — full `## D299` entry at file tail | FIXED — `fileValidateSaves()` (only called from the skipped legal screen) never ran under `SkipIntro`, leaving `saves[]` all-zero; added the missing call to the `SkipIntro` shortcut (`src/game/lv.c`, `#ifdef PORT`, zero game-logic change). Build-verified both `RelWithDebInfo`/`Release`; live user re-test confirms all 4 slots now selectable with `SkipIntro=1`. Not yet run through a full 21-level sweep. |
| D300 | **Hipfire look feels non-uniform (issue #89): mouse px routed through the N64 analog stick's quadratic natural-turn curve, so slow motion is "almost unrecognised" and fast motion saturates at full turn speed by ~13 px/poll.** — full `## D300` entry at file tail | FIXED (pending build-verify) — WI-1 per `docs/dev/GEPD-INPUT-PLAN.md`: new `hipDirectCompute()` (`port/src/input.c`) writes `vv_theta`/`vv_verta` directly, mirroring GEPD's hipfire formula and D194's existing aim-mode direct-write pattern — linear px→degree, FOV-scaled, dt-normalized, bypassing the stick curve entirely. Safety-gated on `bonddead`/`outside_watch_menu`/`pause_state` (GEPD's `!dead && !watch && !pause`, ported from decomp fields since hipfire, unlike aim mode, has no RMB-held natural gate). `Input.MouseDirectLook` (default 1) is the escape hatch back to the legacy stick path. Compile-verified clean (`ninja`, no new warnings); link/smoke-test pending (exe was locked by a running instance at compile time). |
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software
@@ -10900,7 +10901,23 @@ if that's recorded anywhere in the D280 write-up above.
**M-153 (2026-09-16): the live `GE_D243M` capture (full Dam playthrough → abseil; 351 KB log, 2,991 probe lines, preserved at `scratch/d243/m153_dam_abseil_capture.log`) discriminates M-145's three questions — the double-instance hypothesis is REFUTED, and the model's rendered position turns out to be a period-4 cycle through four fixed world points that diverges from the scripted `prop` sweep; the camera shake is now fully explained mechanically.** **(1) There is no second Bond model.** Exactly one (idx, model-pointer) pair for the entire capture — idx=0, single `bodyModel`, exactly one `draw` line per frame. No REMOVE/CREATE window around the abseil: the model is created at tick 4818 **directly at the post-teleport position** (12.6, −23744, −1330) with `cam=7` POSEND + subcam=1 (INTRO) already set (the D160 `bondviewSetCameraMode(7)` line immediately precedes CREATE), and it persists to the last tick — no REMOVE is logged even through mission-complete/title return (the stage-unload path evidently doesn't route idx 0's body through `bondviewRemovePlayerBody`, or removal happens after the final `playerTick`; minor). M-145's double-image candidates — "two live Bond models" and "stale draw of a removed model via a bodyModel==0 re-seed window" — are both refuted on PC. **(2) Two divergent position streams.** (a) `prop->pos` executes the smooth scripted bungee sweep through the void: y −23859 → +175 (tick ~5000) → −10724 (tick 5910, mission end). (b) The model's *actual rendered position* — `field_488.pos`, which under POSEND+bodyModel≠NULL is written **only** by the on-screen render readback (`bondview2.c:10452`; the `pos ← collision_position` copy at ~4773 is skipped in POSEND, and the other filter re-seed site at 8912 is `mp_respawn_handler`, not this path) — behaves as: tick 4819 = true void position (== prop); ticks 4820–4880 frozen at (y=729.7, z≈−3111); ticks 4881–5041 frozen at (y=229.1, z≈−3106); ticks 5042–5045 a transition burst containing one frame that matches `prop` exactly (y=−12874.7); tick 5046→end: a strict **period-4 cycle through four fixed world points** P1(228.7, −3111) P2(229.2, −3107) P3(79.2, −2512) P4(−1214.8, −2221). The model transform is decoupled from the script in all three axes (by tick 5910 `prop.z`=−1797 while the cycle's z-range is −3111…−2221); it is not terrain-following (the same four values repeat exactly for ~800 ticks while x/z drift). **(3) The camera shake closes mechanically.** `field_3C4/8/C` (= 0.1 × leaky integral of `field_488.pos`) starts at ≈ −22832 — the *exact* `pos2.y` start M-143 observed live — sweeps to +229 over ~180 ticks as the filter converges onto the frozen surface point, then tracks the period-4 cycle. The "violent shake/spazz" = a fixed-eye camera whose look-at anchor (i) starts ~20k units displaced and (ii) teleports between four points at 15 Hz. D146 remains fully decoupled (zero hits, fourth consecutive run). **(4) "Two Bonds" — not present as two instances in this capture.** If the user still saw a double image on this run, the leading candidate is stale `render_pos` submissions lingering in the draw queue while the model teleports between the four cycle points (D75 family): P4 sits ~1,400 units below the dam surface ("floating in the air"), P1/P2 are on it ("standing on the dam"), and each copy would be a frozen snapshot → "neither animates correctly." Not verifiable from this log (the draw probe counts submissions — one per frame — not on-screen images). **Open question for the user: did two Bonds appear in *this* run?** **(5) Cross-ref:** D173 (level-start floating, Cradle spin) and D292 (death sinking) are the same "third-person world model transform" family — the period-4 fixed-point cycle is now a concrete shared-mechanism candidate for all three. **Next:** code dive into where the model's render transform gets its translation during POSENT (`chrTick` → `render_pos`, chraction.c/objecthandler): what state produces the four fixed points (a 4-keyframe anim attached to Bond's model on PC only? a pad table?), and why the smooth `prop` sweep never reaches the model transform after spawn. If the double image is a stale-queue artifact, that is a port/render-layer fix (fast3d draw list) — no game-logic change needed.
**M-154 (2026-09-16): user symptom sharpened to "one new frozen Bond per camera shot"; the M-153 log reading is corrected (the rendered model is NOT frozen in phase 3 — `prop->pos` itself cycles); a per-frame model-draw census probe (`D243M: chrdraw`) is implemented and smoke-tested to discriminate the two remaining hypotheses.** **(1) User observation (reproducible every time so far):** during the abseil, one *new frozen* Bond appears with each camera shot — shot 1 standing, shot 2 mid-jump, shot 3 rappelling; the old copies persist and never animate; the camera glitches when the second Bond appears. **(2) Corrected log reading (supersedes M-153's "period-4 cycle through four fixed points").** The POSENT window (frames 4819–5910) has three phases: **P1** (4820–4880): `prop.y` rises −19791→+175 while `f488` stays frozen at (−4.4, 729.7, −3111.6) — the rendered model is decoupled from prop during the ascent; **P2** (4881–5041): both frozen (prop (−4.3, 175.9, −3107.6), f488 (−4.4, 229.1, −3106.7)); **P3** (5042→end): **`prop->pos` itself cycles period-4 through three z-lanes** — A(≈−31xx), A, B(≈−25xx), C(≈−22xx) — while y falls smoothly at ~30 units/frame. `f488` tracks the *same* cycle exactly with per-lane y offsets (A: +229, B: +79, C: −1215 = per-pad model pose), so in P3 the rendered model is **not frozen** — it teleports between three fixed world points every frame, following prop. M-153's "four fixed points" was lane A (two near-identical samples) + B + C. Structural tell: y stays perfectly continuous across lane boundaries while x/z rotate → either one prop whose x/z come from a cycling source (AI-script GOTO loop setting position to different pads one command per frame — cf. the teleport fall-through at chrai.c:4064 where a failed `chrAdjustPosForSpawn` drops into the next command; or keyframe sampling without interpolation), or `pp->prop` rotating among three PropRecords with synchronized y. **(3) Hypotheses for the visible double/triple image:** **H1** multiple Model instances — each shot spawns a frozen Bond, all drawn every frame (the M-145 family, but spawned via a path invisible to the playerTick probe); **H2** single model + framebuffer accumulation — `subcam=1` snapshot buffer never cleared, so the per-frame teleporting model leaves a trail of frozen ghosts at all three lanes; "old copies persist and never animate" fits H2 naturally (ghosts are frozen by construction). M-153's stale-draw-queue candidate is subsumed by H2. **(4) Probe (implemented, smoke-tested).** `D243M: tick` extended with `propptr=`/`chrptr=` (a changing pointer proves prop rotation; constant → cycling x/z source), `rp=` (the render_pos pointer — a change between phases = matrix-buffer reassignment/aliasing), `raw=` (render_pos[0] translation BEFORE the view transform — distinguishes model-matrix cycling from view-matrix cycling). New `D243M: chrdraw` census in `drawjointlist` (both region variants, objecthandler.c): one line per CHR render submission while a scripted camera mode is active (POSEND/INTRO/SWIRL/FADESWIRL), logging Model* + raw root translation. All CHR draws funnel through it (`chrRenderProp`; player path bondview2.c:10883, generic chrprop.c:443). H1 = extra lines with *distinct* model pointers at the same frame; note the same model is drawn twice per frame (two passes) — dedupe by (frame, model). Accessors `d243mProbeActive()`/`d243mGetFrameCounter()` live in bondview2.c (the camera gate is folded into `active()` so objecthandler.c needs no bondview.h include); D250 cached-getenv kept. Smoke: 45 s bare `-level_33` boot → 4,053 chrdraw + 1,066 tick lines, formats verified, silent in FPS play (cam=FP/NONE gate). **(5) Next:** user re-runs Dam→abseil with `GE_D243M=1` (`cmd /c` capture — see HANDOFF); analysis: (a) distinct-Model* count per frame during POSENT → H1 vs H2; (b) propptr/chrptr stability → prop rotation vs cycling source; (c) raw vs f488pos → model-matrix vs view-matrix origin of the cycle. **M-154 fix (same day): the first live abseil run CRASHED inside the new tick probe itself** — access violation, fault addr `0xFFFFFFFFFFFFFFFF`, PC in `playerTick` (bondview2.c:10419), on the very first tick after `D243M: CREATE frame=4109 … cam=7 subcam=1`. Cause: `bodyModel->render_pos` is assigned by `instcalcmatrices()` **during the draw pass** (`model.c:2453`: `arg1->render_pos = arg0->mtxlist`), i.e. *after* the probe point — on the first tick post-CREATE it still holds the reused `AnimModelSlot`'s stale value, observed as the 0xFF tombstone `(RenderPosView*)-1`. The game only ever dereferences `render_pos` post-`chrTick`, so it is legitimately unset at the probe; the M-153-era tick fields never touched it, which is why the earlier capture survived. Fix: guard the `raw=` read against NULL **and** the `-1` tombstone; `rp=%p` is still logged unconditionally, so the exact frame where `render_pos` transitions -1 → real pLine truncated
**M-154 (2026-09-16): user symptom sharpened to "one new frozen Bond per camera shot"; the M-153 log reading is corrected (the rendered model is NOT frozen in phase 3 — `prop->pos` itself cycles); a per-frame model-draw census probe (`D243M: chrdraw`) is implemented and smoke-tested to discriminate the two remaining hypotheses.** **(1) User observation (reproducible every time so far):** during the abseil, one *new frozen* Bond appears with each camera shot — shot 1 standing, shot 2 mid-jump, shot 3 rappelling; the old copies persist and never animate; the camera glitches when the second Bond appears. **(2) Corrected log reading (supersedes M-153's "period-4 cycle through four fixed points").** The POSENT window (frames 4819–5910) has three phases: **P1** (4820–4880): `prop.y` rises −19791→+175 while `f488` stays frozen at (−4.4, 729.7, −3111.6) — the rendered model is decoupled from prop during the ascent; **P2** (4881–5041): both frozen (prop (−4.3, 175.9, −3107.6), f488 (−4.4, 229.1, −3106.7)); **P3** (5042→end): **`prop->pos` itself cycles period-4 through three z-lanes** — A(≈−31xx), A, B(≈−25xx), C(≈−22xx) — while y falls smoothly at ~30 units/frame. `f488` tracks the *same* cycle exactly with per-lane y offsets (A: +229, B: +79, C: −1215 = per-pad model pose), so in P3 the rendered model is **not frozen** — it teleports between three fixed world points every frame, following prop. M-153's "four fixed points" was lane A (two near-identical samples) + B + C. Structural tell: y stays perfectly continuous across lane boundaries while x/z rotate → either one prop whose x/z come from a cycling source (AI-script GOTO loop setting position to different pads one command per frame — cf. the teleport fall-through at chrai.c:4064 where a failed `chrAdjustPosForSpawn` drops into the next command; or keyframe sampling without interpolation), or `pp->prop` rotating among three PropRecords with synchronized y. **(3) Hypotheses for the visible double/triple image:** **H1** multiple Model instances — each shot spawns a frozen Bond, all drawn every frame (the M-145 family, but spawned via a path invisible to the playerTick probe); **H2** single model + framebuffer accumulation — `subcam=1` snapshot buffer never cleared, so the per-frame teleporting model leaves a trail of frozen ghosts at all three lanes; "old copies persist and never animate" fits H2 naturally (ghosts are frozen by construction). M-153's stale-draw-queue candidate is subsumed by H2. **(4) Probe (implemented, smoke-tested).** `D243M: tick` extended with `propptr=`/`chrptr=` (a changing pointer proves prop rotation; constant → cycling x/z source), `rp=` (the render_pos pointer — a change between phases = matrix-buffer reassignment/aliasing), `raw=` (render_pos[0] translation BEFORE the view transform — distinguishes model-matrix cycling from view-matrix cycling). New `D243M: chrdraw` census in `drawjointlist` (both region variants, objecthandler.c): one line per CHR render submission while a scripted camera mode is active (POSEND/INTRO/SWIRL/FADESWIRL), logging Model* + raw root translation. All CHR draws funnel through it (`chrRenderProp`; player path bondview2.c:10883, generic chrprop.c:443). H1 = extra lines with *distinct* model pointers at the same frame; note the same model is drawn twice per frame (two passes) — dedupe by (frame, model). Accessors `d243mProbeActive()`/`d243mGetFrameCounter()` live in bondview2.c (the camera gate is folded into `active()` so objecthandler.c needs no bondview.h include); D250 cached-getenv kept. Smoke: 45 s bare `-level_33` boot → 4,053 chrdraw + 1,066 tick lines, formats verified, silent in FPS play (cam=FP/NONE gate). **(5) Next:** user re-runs Dam→abseil with `GE_D243M=1` (`cmd /c` capture — see HANDOFF); analysis: (a) distinct-Model* count per frame during POSENT → H1 vs H2; (b) propptr/chrptr stability → prop rotation vs cycling source; (c) raw vs f488pos → model-matrix vs view-matrix origin of the cycle. **M-154 fix (same day): the first live abseil run CRASHED inside the new tick probe itself** — access violation, fault addr `0xFFFFFFFFFFFFFFFF`, PC in `playerTick` (bondview2.c:10419), on the very first tick after `D243M: CREATE frame=4109 … cam=7 subcam=1`. Cause: `bodyModel->render_pos` is assigned by `instcalcmatrices()` **during the draw pass** (`model.c:2453`: `arg1->render_pos = arg0->mtxlist`), i.e. *after* the probe point — on the first tick post-CREATE it still holds the reused `AnimModelSlot`'s stale value, observed as the 0xFF tombstone `(RenderPosView*)-1`. The game only ever dereferences `render_pos` post-`chrTick`, so it is legitimately unset at the probe; the M-153-era tick fields never touched it, which is why the earlier capture survived. Fix: guard the `raw=` read against NULL **and** the `-1` tombstone; `rp=%p` is still logged unconditionally, so the exact frame where `render_pos` transitions -1 → real pLine truncated
**M-154 fix 2 (second abseil run, same day): the sentinel guard was not enough — the probe crashed again at the identical PC/fault address.** Second capture (244 KB, longer run than run 1) died at `D243M: CREATE … cam=7 subcam=1` with AV fault addr `0xFFFFFFFFFFFFFFFF`, PC `playerTick+0x64c` = bondview2.c:10427 — the same `raw=` read, now *past* the NULL/-1 guard (disassembly confirms the guard compiled correctly: `lea -1(%r10),%rdx; cmp $-3,%rdx; jbe <read block>` passes everything except exactly NULL and -1). Conclusion: between CREATE and the first draw pass, `render_pos` holds **arbitrary stale slot contents**, not a uniform tombstone — run 1 drew -1, run 2 drew some other bad value (the exact fault addr of -1 with the `0x30(%r10)` displacement implies r10 ≈ -48 at fault time; the crash-handler register dump is not fully trustworthy, but the PC/line are). **Sentinel-whack-a-mole is the wrong shape for a probe.** Final fix: the tick probe **never dereferences `render_pos` at all** — `raw=` is dropped from the tick line entirely; `rp=%p` stays (logging a pointer value costs no deref, and the -1 → real transition frame is still captured). No data is lost: the pre-view root translation is already logged by the `D243M: chrdraw` census in `drawjointlist`, where validity is guaranteed because the game's own `gSPSegment(render_pos)` runs moments later — and draw-time is the *more accurate* sample anyway (a tick-time read would return the previous frame's matrix buffer). The chrdraw probe also gained the same -1 rejection as belt-and-braces. Smoke: 30 s bare boot, 4,051 chrdraw lines, no crash. Re-capture pending.
**M-159 (2026-09-17): static code-dive names a concrete, evidence-consistent mechanism for the period-4/lane cycling itself — the model's render transform only refreshes on ticks that pass the game's own on-screen visibility test, and during the void sweep most ticks fail it.** Traced `chrTick` (`src/game/chr.c:2442`) end to end for the path that actually recomputes `bodyModel->render_pos`: at `chr.c:2580/2593/2615`, `headSwitchVisible = posIsOnScreen(prop, &prop->pos, getinstsize(model), 1)` gates a large block (`chr.c:2633-2681`) that only runs `subcalcmatrices()` (→ `instcalcmatrices()`, `model.c:2453`, the sole site that assigns `render_pos`) **when `headSwitchVisible` is true**; when false, `render_pos` — and therefore `field_488.pos`, which `playerTick` (`bondview2.c:10493-10497`) reads back from `render_pos` every tick `prop->flags & PROPFLAG_ONSCREEN` is set — is simply not touched that tick, so the model's rendered position freezes wherever it last was. `posIsOnScreen` (`propobj.c:13584`) itself reduces to a same-room check (`getROOMID_isRendered`) plus `camIsPosInScreen`/`camIsPosInScreenBox` (`bondview.c:1090`/`1131`) — a straight point-vs-frustum-plane test against `g_CamFrustum{Near,Left,Right,Top,Bottom}{Normal,Offset}`, which are recomputed once per frame in `bondviewUpdateFrustumPlanes()` (`bondview.c:949`, called from the camera-matrix-build function at `bondview2.c:8401`) purely from `g_CurrentPlayer->viewtoworldmtxf` and the player's FOV-derived half-width/half-height. **This mechanically fits every M-153/M-154 observation:** `prop->pos` genuinely sweeps a huge range through the void during the abseil (y −23859→+175, M-153); most of that sweep is far outside the fixed cutscene camera's frustum (the void is below/behind the dam, off-screen from this shot), so `posIsOnScreen(prop->pos, ...)` is false on nearly every tick, and `render_pos`/`field_488.pos` only update on the rare ticks where the live position happens to cross back inside frustum — producing exactly the "frozen, then teleports to a new fixed point, then frozen again" pattern M-154 documented (P1/P2 static, P3 a small cycling set of points) without needing a second model instance, a stale draw-queue entry, or a cycling prop/pointer at all. **This is decomp-exact game code — `posIsOnScreen`/`camIsPosInScreen`/`bondviewUpdateFrustumPlanes` are ordinary, unmodified `src/game` functions with no ABI/pointer-width surface (no ROM-serialized struct, no union aliasing) — so per M-144 (real N64 does NOT show this shake), the divergence must be in an INPUT to this test differing between platforms, not the test itself.** Two live candidates, neither confirmed yet: **(a)** `g_CurrentPlayer->viewtoworldmtxf` — and hence the whole frustum — is itself built from the filtered look-at chain (`bondviewFrozenCameraTick`'s `gBondViewCutscene` branch → `field_3C4/8/C`, the same leaky-integrator filter M-143 named) that in turn is fed by `field_488.pos`, which this very gate controls — a **feedback loop**: if call/update ordering between `bondviewUpdateFrustumPlanes()`/the camera-matrix build and `chrTick`'s visibility test differs by even one frame or one tick-count between PC and N64 (a scheduler/`port/src/gesched.c` timing question, not game logic — bucket (b) if confirmed), the frustum a given tick tests against would differ, changing which ticks pass `posIsOnScreen` and thus which fixed points get sampled; **(b)** `getinstsize(model)`/`c_halfheight`/`c_halfwidth`-derived frustum margins, or the FOV/aspect the port's `viewtoworldmtxf` construction uses, could differ from N64's in a way that shrinks the effective on-screen box during this specific low/behind-camera shot. **Not yet distinguished; no `src/game` or `port/` code changed this pass — still diagnosis only per rule #2.** **Next, concrete:** an `#ifdef PORT` probe on `posIsOnScreen`'s return value + `prop->pos` + the current frustum offsets, logged every `chrTick` call during POSEND for the abseil window, would directly show whether the pass/fail pattern lines up with the observed freeze/teleport boundaries (confirms the gate is the mechanism) and whether `viewtoworldmtxf`/frustum values are stale relative to `field_488.pos`'s own last-updated tick (tests candidate (a)). If confirmed, the fix is either a `port/`-layer scheduling/ordering correction (no `src/game` touch) or, if N64's own `posIsOnScreen` genuinely also fails most of these ticks but the *filter* happens to converge invisibly fast enough there not to be noticed (i.e. this really is decomp-faithful and only more visible on PC due to unrelated frame-pacing), a case for treating D243 as closed-not-a-bug and folding it into D148's general umbrella instead — that determination needs the probe capture, not more static reading.
**M-159 probe (`D243M: onscreen`, `chr.c`) landed and build/smoke-verified:** compiles clean, links, and a 1200+-frame bare `-level_33` boot with `GE_D243M=1` produced 41,165 `D243M: onscreen` lines (plus the existing 1,067 `D243M: tick` lines) with zero crashes/exceptions — confirms the probe fires correctly for every ticked chr (not just Bond; sample shows multiple `chr=` pointers per frame, `vis=` flipping 0/1 per chr's position), format `D243M: onscreen frame=%d chr=%p vis=%d pos=%.1f,%.1f,%.1f`.
**M-160 (2026-09-17, same day): the user played a live Dam→abseil capture (`ge007_d243_onscreen_capture.log`, 3.7 MB, 44,728 probe lines) with the new `D243M: onscreen` probe — M-159's frustum-culling hypothesis is REFUTED by the data, and a stronger, more specific mechanism is now directly evidenced: `chrTick` overwrites `prop->pos` with a stale/stepped value every tick regardless of on-screen visibility.** Player-body CREATE for the abseil: `frame=9352 ... cam=7 subcam=1`, `chrptr=00000000701dbebc`. Cross-referencing `D243M: tick` (bondview2.c's `playerTick`, fires BEFORE `chrTick` runs each frame) against the new `D243M: onscreen` (fires INSIDE `chrTick`, at `after_position_update:`, i.e. AFTER `chrTick`'s own position-update logic runs) for the **identical frame numbers**: `tick`'s `prop=` sweeps smoothly (frame 9353 y=-23859.7 → frame 9369 y=-1130.0, matching the AI script's scripted void descent, consistent with M-153/M-154), while `onscreen`'s `pos=` for the same frames is **frozen at exactly (-4.3, 676.5, -3111.6)** for all of frames 9353-9413, then steps to a new frozen value (-4.3, 176.2, -3111.6) at frame 9414 — **and `vis=1` (on-screen/frustum test PASSES) on every single one of these frames, no exceptions.** This directly refutes M-159: the model's render transform is not failing to update because it's off-camera — `posIsOnScreen` is true throughout, `subcalcmatrices`/`instcalcmatrices` run every tick (confirmed by the varying `rp=` render_pos pointer values in the `tick` lines across these frames — different buffer each tick, i.e. actively reallocating/redrawing, not stalled). **The freeze is `prop->pos` itself being overwritten inside `chrTick`, downstream of the AI script's write, before the model transform is computed from it.** `chr.c`'s `after_position_update:` label is reached via one of three branches gated on `chr->actiontype` (`ACT_ANIM`/`ACT_STAND`/else, `chr.c:2578-2616`), each of which calls `chrUpdateAnim`/`modelTickAnim` — ordinary per-chr animation/locomotion logic that has no POSEND/cutscene-mode check anywhere in this function. The player's chr (`chrptr=701dbebc`) is the same chr the CHR_BOND_CINEMA teleports move (M-145) but is otherwise still being ticked as a normal, un-scripted character every frame — its own `chrUpdateAnim`/`modelTickAnim` position write is competing with, and winning against, the AI script's `prop->pos` write for what ends up rendered. `field_488.pos`'s value in the same `tick` lines (frozen at (-4.4, 727.8, -3111.6), ≈7 units off the `onscreen` reading — matches the `mtx[4]*7.0f` view-transform offset in `bondview2.c:10495-10497`) confirms this same frozen post-chrTick value is exactly what later gets read back into the render position, closing the loop from M-153's original observation to this mechanism.
**Not yet determined: which of the three `chrTick` branches is taken (needs `chr->actiontype` logged — not in the current probe), and whether N64 hardware exhibits the same overwrite** (if GE's own scripted-cutscene state is supposed to freeze/skip a chr's normal animation-position tick via some flag this port isn't honoring, that's a port-layer or narrow ABI-class fix; if N64 genuinely runs the same competing writers and the visual result there is different for an unrelated platform reason — e.g. the two writes interleave differently under N64's single-threaded deterministic tick vs. this port's own tick/render cadence — that needs establishing before any fix). **Next, concrete:** extend the `D243M: onscreen` probe (or add a sibling) to log `chr->actiontype` and whether the branch actually called `chrUpdateAnim` vs `modelTickAnim` vs neither, for the same window — this directly names which branch is responsible and whether it's gated by anything checkable (e.g. `CHRFLAG_HIDDEN`, `docentreupdown`, or a `g_CameraMode`/POSEND check elsewhere in the call chain that should also cover this path but doesn't). Cross-ref M-145's false-GUARD/CHR_BOND_CINEMA architecture — this is now the concrete mechanism that architecture predicted but didn't pin down.
**User confirms (live playtest, this session): the visible cutscene glitch is unchanged** — this probe pass is diagnosis-only (no behavior change, `#ifdef PORT`/env-gated), so an unchanged symptom is expected and does not weigh against the finding above.
**M-161 (2026-09-17, same session): decisive experiment — NEGATIVE result. M-160's mechanism, while real, is NOT the (or not the whole) visible cause.** To test M-160 without more diagnosis, added a throwaway `#ifdef PORT` hack in `playerTick` (`bondview2.c:10474`, env-gated `GE_D243X`, off by default): save `prop->pos` immediately before the `chrTick(prop)` call, restore it immediately after, **only** while `g_CameraMode == CAMERAMODE_POSEND` — directly neutralizing the competing write M-160 identified. Compiled clean, linked, 1380-frame bare-boot smoke test with zero crashes, then a full live Dam→abseil playthrough with `GE_D243X=1`. **User's report: "still happened, looked the same as it always does."** No perceptible change to the glitch. Since M-160's own capture already established `g_CameraMode == CAMERAMODE_POSEND` holds throughout the freeze window (same session, `D243M: tick`'s `cam=7` field), there's good reason to trust the experiment's condition was actually satisfied and not silently inert — though this run did not have `GE_D243M=1` set alongside `GE_D243X=1` to directly confirm the branch fired on this specific playthrough (a residual gap, noted for completeness, not chased further given the clear negative result). **Conclusion:** `chrTick`'s competing overwrite of `prop->pos` (M-160) is a real, independently-evidenced bug — worth keeping in `porting-notes.md` §D11 as a documented pattern — but restoring the AI script's position after the overwrite does not fix what the player actually sees, so either (a) the visible glitch's dominant cause is elsewhere in the render/filter chain (M-143's `field_3C4/8/C` leaky-integrator, or something not yet identified), or (b) M-160's mechanism matters but only as one contributor among several, and neutralizing it alone isn't sufficient. Experiment reverted in full (`git diff` clean on `bondview2.c` bar one pre-existing whitespace byte); no lasting code change from this pass.
**Status assessment after M-161:** this bug has now had ~15 session-passes (M-107 through M-161) across multiple sessions without a landed fix, each pass narrowing the mechanism further without closing it. Per the user's own call this session (diminishing returns, "going at this without much luck for a while"), **recommend parking D243 under the D148 umbrella** (general cutscene-bug tracking entry, `docs/dev/GRAPHICS-BACKLOG.md`) rather than continuing further open-ended diagnostic passes, unless a specific new lead or live capture surfaces. The mechanism knowledge gained (M-143's filter, M-145's teleport architecture, M-160's competing-writer bug) remains valid and should inform any *future* pass — this is a park, not a reset — but the next session should default to other backlog items unless the user explicitly wants another D243 cycle.
**Status (from §F table):** **OPEN — M-109 re-analysis of the same session's raw logs corrects the timing/pattern claims above; the D134/pendingGfx race hypothesis is likely wrong.** Confidence MEDIUM on the correction, LOW-MEDIUM on the new lead. Re-reading `data/playtest_d160.log`/`_run2.log` at the `g_framesRendered` level (not just grepping for `D146`) shows the D146 burst fires **~1600-3900 frames (tens of real seconds) *after*** `bossReturnTitleStage`/the `camera_switch(7)` calls, not during them, in **both** runs — "right in the camera-cut window" and "run 1 only" are both wrong as stated above. This also weakens the D134/`pendingGfx` citation: `osSpTaskStartGo` runs `gfx_run()` synchronously and inline (`port/src/libultra.c:1232-1258`), so nothing from the cutscene's own DL submission is still in flight by the time any teardown code runs — the port doesn't have the async-RSP race the hypothesis above assumes. **New lead:** whatever screen renders in that multi-thousand-frame gap after mission-complete is the more likely site — probably `MENU_MISSION_COMPLETE` (`front.c:7267`, `constructor_menu0D_missioncomplete`) or an adjacent briefing/file-select screen in that chain, same general class as D75 (front-end model absence) / D141 (menu bad-pointer bug), not a cutscene-render race. Of the 4 repeating fault addresses, 3 read as literal zero words and only 1 matches the `0xfafbfbfc…` pattern, arguing against a deliberate poison-fill (none found anywhere in `port/`) and for either genuinely-unwritten memory or a decode landing inside unrelated valid data. Next: an env-gated `front.c` menu-transition trace (`MENU_*` id + `g_framesRendered` on every transition after `bossReturnTitleStage`) plus a `GE_PCDUMP` window over frames ~4500-8000 (not the camera-cut window) to name the exact screen/segment. Cross-ref D160 (this supersedes its stale "cutscene skipped" framing with a "cutscene renders corrupted" one, now further corrected to "corruption isn't in the cutscene render at all") and D173 (Cradle's Bond-floating/spinning cutscene bug — same subsystem family, unconfirmed shared root cause). **M-120: the `GE_D243` trace above was BUILT (statically, i.e. written) on branch `investigate/d243-cutscene-race` (`764a1f26`, pushed, not merged) — `src/game/front.c` only, `#ifdef PORT`, logs the target `MENU_*` name + a local per-tick counter on every `menu_init()` state transition. NOT build-verified: two independent sessions (the dispatched subagent, then this session re-verifying in a fresh worktree) both hit the same reproducible failure — `cmake`'s C-compiler ABI check fails with "Cannot create temporary file in C:\Windows\: Permission denied" when gcc is invoked *through* CMake/ninja, even though a bare `gcc -c` invocation from the same shell, same session, succeeds and correctly resolves its temp dir to `%TEMP%`. This is a genuine, reproducible environment issue distinct from the diff's content (static review of the diff found nothing wrong: enum cases all verified to exist in `bondconstants.h`, brace/`#ifdef` balance checked, follows the existing `GE_D160`/`GE_D178` env-gated trace idiom used elsewhere in the same file) — worth a session with a working toolchain investigating the CMake/ninja-vs-bare-gcc environment discrepancy itself as its own item, separate from D243. Next: build `investigate/d243-cutscene-race` on a session where `./build-pc.sh` actually completes, then run the `-level_33` Dam repro with `GE_D243=1` (and the existing `GE_D160`/`GE_PCDUMP` mechanisms) to correlate the trace against the D146 burst's frame range. **M-141: the M-120 toolchain blocker is EXPLAINED and worked around, GE_D243 is now build-verified.** The "Cannot create temporary file in C:\Windows\" failure isn't specific to CMake/ninja vs. bare `gcc -c` — it's this Claude Code session's `Bash` tool shell (Git-for-Windows Bash, not a real MSYS2 MINGW64 window) not exposing a usable `TMP`/`TEMP` to child processes the way an interactive MSYS2 shell does; a bare `gcc -c` from the same shell happened to not hit the temp-file path M-120 tested, which is why it looked clean while the full build wasn't. Fix: explicitly `export TMP=... TEMP=...` (Windows-style path) alongside the already-known `PATH=/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH` fix, both before invoking `./build-pc.sh`/`ninja`/`cmake` — now also recorded in `porting-notes.md` §E as a general session-start gotcha, not just a D243-specific note. Cherry-picked the `GE_D243` trace commit (`764a1f26`) cleanly onto `release/v0.3.0` (branch `investigate/d243-fresh`), fixed one thing the original diff got wrong per this project's own D250 rule (`menu_init()` runs ~once/frame during front-end states — the diff's three `getenv("GE_D243")` calls were uncached; now a single cached `static int s_d243` check), and confirmed it builds clean. **Smoke-tested, not fully exercised:** a bare `-level_33` boot runs 1100+ frames with zero crashes and (correctly) zerLine truncated
@@ -11336,3 +11353,21 @@ This is a **perfect, smooth white-hot → yellow → orange → deep-red → fad
**Generalisable lesson (added to `porting-notes.md`):** a port-only "skip to state X" shortcut must replicate every side effect the skipped screens perform, not just the terminal menu-state flags — `SkipIntro`'s original comment claimed to fully reuse "the game's own post-intro route," but missed the legal screen's one-time `fileValidateSaves()` call.
**Status:** **FIXED** — verified live this session. Not yet run through a full 21-level sweep (docs-only + one `#ifdef PORT` line touching front-end boot, not level code) or committed.
## D300 — Hipfire look non-uniform (GitHub #89): direct camera write replaces the N64 stick-curve routing (WI-1, `docs/dev/GEPD-INPUT-PLAN.md`)
**Symptom (issue #89):** hipfire mouse-look feels non-uniform — slow mouse motion is "almost unrecognised," fast motion saturates quickly, overall bang-bang rather than smooth.
**Root cause (diagnosed in the plan, confirmed by code reading):** hipfire converted mouse px into an *analog stick value* (`sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN)`, `port/src/input.c`), which the game then runs through its own N64-era natural-turn response — a quadratic-ish curve with a soft floor that saturates at full 315°/s by ~13 px/poll (D238). Consequences: small stick values from slow motion square off to near-zero turn rate; fast motion saturates almost immediately, narrowing the usable band; `(int)` truncation adds a quantized dead zone. Aim mode (D194's `aimGepdCompute`) never had this problem because it already writes `vv_theta`/`vv_verta` directly, bypassing the stick curve entirely — hipfire was the one leftover N64-stick-shaped path.
**Fix (`port/src/input.c`, port-only, no `src/game` touch):** new `hipDirectCompute()`, structurally mirroring `aimGepdCompute()` and GEPD's own hipfire branch (`reference/mouse-injector/games/goldeneye.c`: `camx += XPOS/10 * (SENS/40) * (fov/basefov)`). Converts this poll's dt-scaled, already-invert/Y-scale-adjusted px directly to degrees (`px * 0.1 * (MouseTurnSpeed/100) * (MouseSensitivity/100) * (fovy/GEPD_BASE_FOV)`) and adds straight to `g_CurrentPlayer->vv_theta`/`vv_verta` (pitch clamped ±90°, matching the aim-mode convention). At default `MouseTurnSpeed=50`/`MouseSensitivity=100` this reduces to GEPD's implied px/10 baseline scaled by the existing 50% fine-trim — same "MouseSensitivity multiplies everything" convention D238 established.
**Safety gates (per the plan's GEPD-parity requirement):** GEPD gates injection on `camera==4||0 && menupage==11 && !dead && !watch && !pause`. Aim mode never needed explicit dead/watch/pause checks because it only runs while RMB is held, which a player can't do through those states in practice; hipfire look is always live, so it needs the check explicitly or it would fight the game's own camera control during a death cam or cutscene. `menupage==11` is already covered by the existing `menuMode`/`current_menu` gate (hipfire's branch is only reached when `!menuMode`). Added explicit reads of `g_CurrentPlayer->bonddead`, `->outside_watch_menu`, `->pause_state` (decomp-canonical fields, `src/game/bondview.h:465/672/705`) — read-only, no logic change, function returns 0 (falls back to the legacy stick path) if any gate is closed.
**Escape hatch:** `Input.MouseDirectLook` (default 1) — `0` reverts to the pre-existing `MOUSE_TURN_GAIN` stick path unchanged. Keyboard-driven turn (arrows) is unaffected either way (only mouse px is rerouted).
**Not done this pass (deferred per the plan's phasing):** WI-2 (raw-input-by-default), WI-3 (sensitivity-knob consolidation), tank-specific direct write (GEPD's separate `0x80036484` tank-rotation global — `g_TankOrientationAngleRad`, `bondview.h:2529`/`bondview.c:269`, per a background mapping pass this session; not wired up — tanks are rare in single-player, phase 2 per the plan).
**Verified:** compiles clean (`ninja`, `port/src/input.c` — no new warnings); full link succeeds; a 1200+-frame bare `-level_33` smoke boot (`GE_D243M=1`, unrelated to this change but running concurrently) completed with zero exceptions/crash signatures, confirming the build is otherwise healthy with this change in it. **Manual playtest still owed** — mouse feel (hipfire slow/fast linearity, aim mode unaffected, scope zoom, menu, death/cutscene safety-gate behavior) needs a human with real mouse input; a headless boot can't exercise it. `GE_INPUTLOG` already logs `hipdirect d=... cam=...` lines for the linearity check the plan's Verification section calls for — run with `GE_INPUTLOG=1` during that playtest.
**Status:** **FIXED (pending manual playtest)** — code complete, compiles clean, links, and a concurrent smoke run showed no regression; mouse-feel verification needs a human session.
+27
View File
@@ -1121,6 +1121,33 @@ non-flag ones (data loads, one-time validation, subsystem init) explicitly.
Grepping the skipped `init_*`/`update_*` functions for calls with no
matching state flag is the concrete check.
## D11. A chr's own per-tick animation-position logic can silently overwrite an AI script's scripted position in the same frame (D243)
`chrTick` (`src/game/chr.c:2442`) always runs one of three `chr->actiontype`-
gated branches (`ACT_ANIM`/`ACT_STAND`/else, `chr.c:2578-2616`) that call
`chrUpdateAnim`/`modelTickAnim` — ordinary per-chr animation/locomotion
logic with no check for whether the chr is currently being driven by a
scripted cutscene camera/AI-script position write instead. If something
else (an AI script's `GOTO`/teleport, a cinema-pad move, D243's
`CHR_BOND_CINEMA` teleport) writes `prop->pos` earlier in the same frame,
`chrTick`'s own animation-position write can silently overwrite it before
the model transform is computed — two writers racing for the same field
within one tick, no error, no crash, just whichever runs last wins. This
produced D243's "frozen render position that doesn't track the smooth
scripted sweep" symptom: a first pass (M-159) wrongly attributed it to
the on-screen/frustum-visibility gate (`posIsOnScreen`) skipping the
render-transform refresh on off-camera ticks — REFUTED by a live capture
that logged BOTH the pre-`chrTick` position (from the AI script, smoothly
sweeping) and the post-`chrTick` position (frozen) for the same frame
numbers, with the visibility gate returning true throughout. **The
generalizable check:** when a rendered position looks "frozen" or
lagging behind a scripted/AI-driven `prop->pos`, don't assume a
visibility/culling gate is skipping an update — log the same field both
immediately before and immediately after the suspect tick function to
see whether something *inside* that function is overwriting it. A
visibility gate stopping updates and a competing writer stomping them
produce the same visual symptom but need opposite fixes.
## E. Process / method notes
- Investigation loop is: reproduce → env-gated capped probe → root-cause
+79
View File
@@ -329,6 +329,11 @@ static int aimAbsolute = 1; /* D194: 1 = GEPD-style aim while RMB is hel
* crosshair locked at centre, mouse MOVEMENT
* rotates the view 1:1 in screen angle; 0 =
* legacy velocity stick from mouse delta. */
static int mouseDirectLook = 1; /* WI-1 (GEPD-INPUT-PLAN.md #89): 1 = hipfire
* writes vv_theta/vv_verta directly (see
* hipDirectCompute), same linear-in-px model
* as aim mode; 0 = legacy stick-curve path
* (MOUSE_TURN_GAIN), kept as an escape hatch. */
/* D194 GEPD-aim state. Touched ONLY in inputComputePad (game thread), the
* same confinement as every other static here. The per-poll grabbed delta
@@ -343,6 +348,7 @@ static int aimAbsolute = 1; /* D194: 1 = GEPD-style aim while RMB is hel
static double s_gepdCrossX = 0.0, s_gepdCrossY = 0.0;
static int s_gepdHeldPrev = 0; /* aim held last tick -> adopt on entry */
static int aimGepdCompute(double dxPx, double dyLook);
static int hipDirectCompute(double dxPx, double dyLook);
/* D194: bondview2's "look-ahead" pitch centreing (docentreupdown) arms during
* hip-fire walking whenever the pitch strays from the horizon target, and --
* once armed -- keeps pulling vv_verta back to it even in aim mode, EXCEPT
@@ -1044,6 +1050,19 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
} else {
double hipEdx = edx * lookDtScale, hipDyLook = dyLook * lookDtScale;
double hipSens = (mouseTurnSpeed / 100.0) * (mouseSensitivity / 100.0);
/* WI-1: direct camera write, same linear px->degree model as
* aim mode (GEPD's hipfire branch) -- bypasses the N64 stick's
* quadratic natural-turn curve entirely (D238/#89: that curve
* saturated at ~13 px/poll, making slow motion "almost
* unrecognised" and fast motion bang-bang). Falls through to
* the legacy stick path below when it declines (disabled, no
* player, or a safety gate is closed). */
if (mouseDirectLook && hipDirectCompute(hipEdx, hipDyLook)) {
/* Pitch handled inside hipDirectCompute too; nothing left
* to do for yaw/pitch this poll. Digital pitch-pulse
* (naturalPitchMode==0) still applies below only in the
* legacy path, so skip both branches here. */
} else {
sx += (int)(hipEdx * hipSens * MOUSE_TURN_GAIN);
if (naturalPitchMode) {
/* D194/D238: SOLITARE gives hipfire pitch the same
@@ -1071,6 +1090,7 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
hipPitchPhase = 0.0;
}
}
} /* end mouseDirectLook fallback (WI-1) */
}
}
@@ -1481,11 +1501,70 @@ static int aimGepdCompute(double dxPx, double dyLook)
return 1;
}
/* WI-1 (GEPD-INPUT-PLAN.md #89): hipfire direct camera write.
*
* Mirrors GEPD's hipfire branch (games/goldeneye.c): `camx += XPOS/10 *
* (SENS/40) * (fov/basefov)` degrees this poll -- linear in px, FOV-scaled,
* no stick curve in between. We fold GEPD's SENS/40 into our own
* MouseTurnSpeed*MouseSensitivity product (both already 100% at defaults,
* same "in line" convention D238 established for aimGepdCompute's GepdSens):
* at defaults this reduces to exactly GEPD's px/10 baseline.
*
* dxPx/dyLook are this poll's dt-scaled px (same convention as
* aimGepdCompute -- callers pre-multiply by lookDtScale).
*
* GEPD's safety gates (`camera==4||0 && menupage==11 && !dead && !watch &&
* !pause`) matter here in a way they didn't for aim mode: aim mode requires
* RMB held, so it naturally can't fire during a death/cutscene/pause state a
* player would be holding RMB through; hipfire looks are always live, so a
* frozen or scripted-camera state (POSEND/INTRO cutscenes, death cam, pause)
* must be checked explicitly or this would fight the game's own camera
* control during those states. `current_menu` already gates menupage==11
* (checked by the caller's menuMode branch, same as aim mode); the rest are
* read here directly -- read-only, no logic change.
*
* Returns 1 if it handled this poll (caller must not also run the legacy
* stick path); 0 to fall back (disabled, no player, or a safety gate is
* closed -- e.g. mid-death or mid-cutscene, matching GEPD's !dead/!watch).
*/
static int hipDirectCompute(double dxPx, double dyLook)
{
struct player *p = g_CurrentPlayer;
if (!mouseDirectLook || !mouseGrabbed || p == NULL)
return 0;
/* GEPD: !dead && !watch && !pause. bonddead/outside_watch_menu/
* pause_state are the decomp-canonical equivalents (bondview.h). */
if (p->bonddead || !p->outside_watch_menu || p->pause_state != 0)
return 0;
f32 fov = viGetFovY();
f32 scale = (fov > 0.0f) ? fov / GEPD_BASE_FOV : 1.0f;
double sens = (mouseTurnSpeed / 100.0) * (mouseSensitivity / 100.0);
/* dyLook already carries MouseInvertY + MouseYScale (applied by the
* caller before dt-scaling, same as every other consumer of dyLook) --
* do not re-apply either here. */
p->vv_theta += (f32) (dxPx * 0.1 * sens * scale);
p->vv_verta -= (f32) (dyLook * 0.1 * sens * scale);
if (p->vv_verta > 90.0f) p->vv_verta = 90.0f;
if (p->vv_verta < -90.0f) p->vv_verta = -90.0f;
if (configGetInputLog()) {
sysLogPrintf(LOG_NOTE,
"GE_INPUTLOG hipdirect d=(%.1f,%.1f) cam=(%.1f,%.1f)",
dxPx, dyLook, (double)p->vv_theta, (double)p->vv_verta);
}
return 1;
}
PD_CONSTRUCTOR static void inputConfigInit(void)
{
configRegisterInt("Input.MouseEnabled", &mouseEnabled, 0, 1);
configRegisterInt("Input.MouseAimSpeed", &mouseAimSpeed, 1, 500);
configRegisterInt("Input.AimAbsolute", &aimAbsolute, 0, 1); /* D194 */
configRegisterInt("Input.MouseDirectLook", &mouseDirectLook, 0, 1); /* WI-1 */
/* D194: renamed Input.GepdSens -> Input.AimModeSens (community name for
* the RMB aim mode; "GEPD" is internal provenance jargon). The old key
* stays registered against the same variable as a deprecated alias --
+1 -1
View File
@@ -10474,7 +10474,7 @@ s32 playerTick(PropRecord *prop)
g_playerPointers[index]->field_AC = 0;
ret = chrTick(prop);
g_playerPointers[index]->field_AC = 1;
g_playerPointers[index]->field_488.collision_position.x = g_playerPointers[index]->prop->pos.x;
g_playerPointers[index]->field_488.collision_position.y = g_playerPointers[index]->prop->pos.y;
g_playerPointers[index]->field_488.collision_position.z = g_playerPointers[index]->prop->pos.z;
+20
View File
@@ -2617,6 +2617,26 @@ s32 chrTick(PropRecord *prop)
}
after_position_update:
#ifdef PORT
/* D243 M-159/M-160: does render_pos only refresh on ticks that pass the
* on-screen visibility gate? Log the gate's result + the live prop
* position every chrTick call while a scripted cutscene camera is
* active (reuses bondview2.c's GE_D243M accessors -- gate already folds
* in POSEND/INTRO/SWIRL/FADESWIRL, so this is silent during FPS play).
* If the freeze/teleport boundaries line up with headSwitchVisible
* flipping 0->1, this confirms the mechanism named in findings.md D243
* M-159. Diagnosis only; zero behaviour change when GE_D243M is unset. */
{
extern int d243mProbeActive(void);
extern int d243mGetFrameCounter(void);
if (d243mProbeActive())
{
osSyncPrintf("D243M: onscreen frame=%d chr=%p vis=%d pos=%.1f,%.1f,%.1f\n",
d243mGetFrameCounter(), (void *) chr, (int) headSwitchVisible,
(double) prop->pos.f[0], (double) prop->pos.f[1], (double) prop->pos.f[2]);
}
}
#endif
if (((chr->actiontype != ACT_STAND) || (model->anim2 != NULL)) || (prop->type == PROP_TYPE_VIEWER))
{
chr->hidden |= CHRHIDDEN_BACKGROUND_AI;