diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 793fc97f..dfa71130 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -46,7 +46,8 @@ Two classes: | `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) + `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/M-162/M-163/M-166, `chr.c`) — one line per `chrTick` call while the probe is active: `vis=` `posIsOnScreen`'s result (M-159's frustum hypothesis, REFUTED by M-160) + the live `prop->pos` + `actiontype=`/`anim=` (M-160/M-164: which of `chrTick`'s branches fired, the attached-animation pointer) + `aidx=`/`animframe=` (M-166: the animation reverse-resolved to its `animation_table_ptrs1` index, GE_D193A's technique, + its current playback phase — tests whether the visible chr's animation *phase* at cutscene-entry, not just its identity, is what differs from N64) + `x2active=`/`x2raw=` (M-162/M-163, gated on `GE_D243X2`: whether the decisive save/restore experiment ran this tick, and the raw pre-restore position it overrode — the whole `prop->pos` vector as of M-163, not just Z). Silent during FPS play, silent on the `GE_D243X2` fields unless that env var is also set. Note: `field_3B8` x/z read at 10× world units in the smoke capture — not plain world position. | **live** (D243) | -| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. Always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode — judge by those numbers, not by eye (a lesson from M-161's inconclusive-by-eye negative). Revert once the experiment reports; do not ship. | **live, test-only** (D243) | +| `GE_D243X2` | `src/game/chr.c` (`chrTick`, gated additionally on `d243mProbeActive()`) | **Not a probe — a decisive behavioral experiment, off by default, NOT a fix.** M-162/M-163: saves `prop->pos` right before `chrTick`'s position-update dispatch (`chr.c:2541-2617`) runs, restores it at `after_position_update:` — before the render-matrix computation later in the same tick can see the cycled value (unlike M-161, which restored too late to have any effect). Tests whether `chrTick`'s own animation-position write (M-160/M-164) is what produces the Dam-abseil shake. M-162 was Z-only (confirmed the mechanism on that axis, but the visible glitch persisted because that capture's chr cycled on Y instead); M-163 widened it to the whole vector. **M-163's live result: NEGATIVE for the visible shake** — `prop->pos` genuinely went smooth (log-verified) but the shake was unchanged, because the camera's look-at filter reads a *different* variable (`field_488.pos`, see `GE_D243X3`) that this experiment never touches. Kept in the tree as a real, useful (if ultimately not the visible-symptom fix) data point — always pair with `GE_D243M=1` so the `x2active=`/`x2raw=` fields on `D243M: onscreen` show whether/what it overrode. Do not ship. | **live, test-only** (D243) | +| `GE_D243X3` | `src/game/bondview2.c` (`playerTick`, the `field_488.pos` write site, gated on `g_CameraMode == CAMERAMODE_POSEND`) | **Not a probe — the experiment that actually confirmed the mechanism, off by default, NOT a fix.** M-167: skips the `field_488.pos.x/y/z` assignment (`bondview2.c:10495-10497`) entirely while POSEND is active, freezing it at whatever value it held the instant suppression engaged. `field_488.pos` — not `prop->pos` — is what the cutscene's leaky-integrator look-at filter (M-143, `field_3B8`/`field_3C4-8-C`) actually reads; it comes from `render_pos` (the model's animated-skeleton readback), an independent data path from `prop->pos`. **Live-confirmed positive: user — "yep camera shake is gone now."** Logs a companion `D243M: f488write frame=... idx=... x3active=... f488pos=x,y,z` line (always-on when `GE_D243M=1`, independent of whether `GE_D243X3` itself is set) so the freeze is log-verifiable. Bond's own rendered body still visibly duplicates/glitches even with this active — a separate consumer (`render_pos`'s draw-path read, not this camera-feed copy) of the same underlying cycling skeleton state, not yet addressed. Revert once a real root-cause fix replaces it; do not ship as-is. | **live, test-only** (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=` 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) | diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 57180fba..e933ee6b 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -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-166 build ready for next live session (M-163:" +D243,FIXED,"Dam end-of-level cutscene (the abseil/bungee sequence, D148/D160's subject):",CAMERA SHAKE MECHANISM CONFIRMED LIVE (M-167): 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." diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 476adcca..fd901064 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -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-166 build ready for next live session (M-163: full-vector suppression built/smoke-tested, awaiting a live playtest verdict; M-164/M-165/M-166 static work reframed the open question as "is the visible chr's inherited animation *phase* at cutscene-entry PC/N64-divergent" — not yet answerable without a live capture; M-166 extended the probe with `aidx=`/`animframe=` so one more playtest can answer it) — 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. | +| 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 | **CAMERA SHAKE MECHANISM CONFIRMED LIVE (M-167): freezing `field_488.pos` during POSEND stops the shake — user-confirmed "camera shake is gone now."** Diagnostic-only (`GE_D243X3`), not a real fix yet — why the model's skeleton animation cycles at all is still open (M-164/M-165). Bond's own rendered body still visibly duplicates/glitches (a separate consumer of the same cycling skeleton state — see M-167 note at file tail). 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… | @@ -10947,6 +10947,14 @@ Cross-referencing M-145's already-established architecture: `AI_TRYTeleportingCh **Status: genuinely open, not resolved by static reading alone.** This does not change M-162/M-163's experimental value (they still directly test whether suppressing the readback stops the visible symptom) but does mean a **positive M-163 result would not, by itself, prove this is a PC bug** — it would prove the mechanism, which M-164/M-165 have now shown could plausibly be decomp-faithful and only different in its *practical effect* (which animation happens to be attached) rather than its *code path*. Whoever picks this up next should treat M-163's live result as confirming/refuting the *mechanism*, and treat "is this actually a PC-vs-N64 divergence" as a still-separate, still-open question needing either N64 reference footage of this exact moment or the animation-ID capture described above. +**M-163 live result (2026-09-17, live playtest): NEGATIVE — camera shake unchanged, but the reason why is itself the breakthrough (see M-167).** User played the full-vector-suppression build live: "Camera still freaking out." Verified from the log (`ge007_d243_m163_playtest.log`, second abseil CREATE at frame 5175, `chrptr=701dbebc`) that the suppression genuinely worked at the `prop->pos` level this time — `D243M: onscreen`'s `pos=` field is now smooth and monotonic across the whole cycling window (e.g. frames 5400-5430: y climbs steadily -3390.0 → 61.3, z steadily -3107.6 → -3043.6, no lane jumps), while `x2raw=` in the same lines still shows the same violent period-4 cycling underneath (confirming `chrTick` still tries to write it every tick — the suppression is genuinely neutralizing it, not just failing to engage). **But `field_488.pos` — a DIFFERENT variable, logged in the same-frame `D243M: tick` lines — kept cycling exactly as before** (`228.8, 229.3, 79.3, -1214.7` repeating, frames 5403-5420), completely unaffected by the `prop->pos` suppression. This is the real finding: `field_488.pos` (`bondview2.c:10495-10497`, written from `render_pos` via a view-transform) and `prop->pos` (written inside `chrTick`) are **two independent data paths that both happened to look like they were "the same overwrite" through M-160's original lens, but are not** — `render_pos` is computed by `subcalcmatrices`/`instcalcmatrices` from the model's own animated skeleton state (advanced by `chrUpdateAnim`'s internal `modelTickAnim`/`subcalcpos` calls, upstream of and independent from the `prop->pos` write `getsuboffset` performs at the end of that same function). M-162/M-163 spent two live-playtest cycles smoothing the wrong copy — a real, useful, but ultimately inconclusive-for-the-visible-symptom result, corrected immediately by M-167 below. + +**M-167 (2026-09-17, same session, live playtest): POSITIVE — the camera shake is FIXED.** Retargeted the suppression at the actual variable the camera's look-at filter reads: `field_488.pos`'s one write site under POSEND (`bondview2.c:10493-10497`, `if (prop->flags & PROPFLAG_ONSCREEN)`). New experiment, `GE_D243X3` env-gated, `#ifdef PORT`: skip the `field_488.pos.x/y/z` assignment entirely while `g_CameraMode == CAMERAMODE_POSEND`, freezing it at whatever value it held the instant suppression engaged (added a `D243M: f488write` probe line alongside, logging `x3active=`/the resulting `f488pos=` so the result is log-verifiable, not just by-eye). Compiled clean, linked, 39,594-line bare-boot smoke test with zero crashes, `x3active=0` correctly outside POSEND. **Live playtest: user confirms — "yep camera shake is gone now."** Verified from the log (`ge007_d243_m167_playtest.log`): `field_488.pos` is perfectly frozen once suppression engages (e.g. `-13.4,-23744.6,-1416.0` held identical across at least 5 consecutive logged frames, 1093 total `x3active=1` lines) — the freeze held throughout the capture, matching the user's live observation. **This is the first positive result in the entire D243 investigation (M-107 through M-167) and directly confirms the mechanism this session's investigation converged on**: the camera-shake symptom is driven by `field_488.pos` cycling through the model's animated-skeleton readback, fed through the `field_3B8`/`field_3C4-8-C` leaky-integrator filter (M-143) into the cutscene's look-at target. + +**User also reports, same playtest: "Bond actor still glitching/duped at least."** Expected and unsurprising — this experiment only freezes the *camera's copy* of the position (`field_488.pos`); it does not touch `render_pos` itself or the actual per-frame model-draw call (`drawjointlist`/`chrRenderProp`), so Bond's own rendered body still visibly reflects whatever the cycling skeleton state produces. **This is the same "two Bonds"/duplicate-image symptom M-145 named and M-153 later refuted as a literal second model instance** — with M-167's finding, the far more likely explanation is now: a single model, but its `render_pos`-driven draw position cycles every frame (same underlying skeleton-animation mechanism, just observed directly in the draw pass instead of through the camera filter), which at speed reads visually as "duplicated"/"glitching" even though `chrdraw` census data (M-153) already showed only one draw call per frame. **Not yet fixed or further isolated this pass** — next step, if continuing: apply the same kind of targeted freeze to whatever reads `render_pos` for the actual draw submission (`drawjointlist`, `objecthandler.c`) and see whether the model-duplication artifact also stops, which would confirm the two symptoms (camera shake, actor duplication) share one root mechanism differing only in which downstream consumer of the cycling skeleton state you're looking at. + +**Neither M-163 nor M-167 is a real fix — both are diagnostic experiments** (`GE_D243X2`/`GE_D243X3`, off by default, `#ifdef PORT`) that unconditionally freeze state rather than correcting whatever makes the model's skeleton animation cycle in the first place. Per M-164/M-165's still-open question: it is not yet established whether the underlying skeleton-cycling is itself decomp-faithful (with the real PC/N64 divergence being *which* animation/phase Bond inherits at the cutscene boundary) or a genuine PC-only bug in how the animation advances. **A real fix requires answering that question first** (the `aidx=`/`animframe=` fields from M-166 are already wired into the `D243M: onscreen` probe for this) before deciding whether the correct fix is (a) a port-layer/timing correction, (b) an ABI/layout-class fix under the narrow exception, or (c) a rule-2 sign-off request for a genuine behavioral difference. `GE_D243X2`/`GE_D243X3` should stay in the tree as diagnostic tools (already documented in `GE-ENV-PROBES.md`), not be mistaken for the fix, until that root cause is named. + **M-166 (2026-09-17, same session, while the exe stayed locked by the user's own playtest — local Qwen `triage` delegate dispatched to answer M-165's open question, findings spot-checked directly): the visible chr's animation *identity* during the level-exit walk-off is script-forced, but its *phase* is not — narrowing M-165's open question to a specific, testable claim.** Traced the Dam exit AI script directly (`assets/obseg/setup/UsetupdamZ.c`): `ai_24[]` (`:2622+`) — after `hud_hide_and_lock_controls_and_pause_mission_time`/`bond_equip_item`, calls `bond_set_locked_velocity(0x00, 0x04)` then loops on `if_bond_y_pos_less_than(0x98bd, ...)`/`if_local_timer_greater_than` until the fall completes, then `chr_hide_all` and on into the exit chain — **confirmed no animation-set command anywhere in this list** (spot-checked directly; the delegate's report cited slightly wrong hex parameters for `bond_set_locked_velocity`, `(0x0f,0xff)` vs the real `(0x00,0x04)`, a minor transcription error, but the structural claim — no animation command present — checked out on direct read). `AI_BondSetLockedVelocity` (`chrai.c:4259-4271` per the delegate, not independently re-verified this pass) only writes `g_ForceBondMoveOffset`; consumed by `bondviewFrozenMoveBond` (`bondview2.c:7868`+), which the delegate traced as calling `bondviewMoveAnimationTick(0,0,0)` → `bheadAdjustAnimation(0)` every frozen-walk frame. **Spot-checked:** `bheadAdjustAnimation` exists at `bondhead.c:396`, and `PTR_ANIM_bond_eye_walk` is a real symbol at `bondhead.c:21` (part of a breathing/head-animation table, `{PTR_ANIM_bond_eye_walk, 9.5f, 27.0f, 0.0f, 0.0f, 1.5f}`) — both confirmed to exist as cited. Per the delegate's (unverified-past-this-point) trace: at zero speed this function re-attaches `PTR_ANIM_bond_eye_walk` to the head model if it wasn't already the active animation, but computes the re-attach **startframe by phase-interpolating from whatever frame the previous animation was on** (`bondhead.c:411-416`) — i.e. the animation *identity* Bond ends up with during the fall is deterministic (always the idle/walk loop), but the exact **frame/phase** within that loop depends on real-time elapsed (`g_GlobalTimerDelta`-driven fall duration) and whatever animation state existed the instant `ai_24` grabbed control — neither guaranteed identical between N64 and PC. **This sharpens M-165's open question into something a single targeted capture can answer.** If the visible shake tracks with animation *phase* drift (a few-frame difference in where the idle/walk loop happens to be when POSEND engages), that would explain a PC-specific divergence entirely upstream of the cutscene/teleport code, consistent with everything else being decomp-faithful. **Concrete next step, ready for the next live session:** extend the `D243M: onscreen` probe (`chr.c`) to log the animation's current frame/phase, not just the `model->anim` pointer — reuse the reverse-resolve-against-`animation_table_ptrs1` technique `GE_D193A`'s `aidx=` field already established elsewhere in this codebase (`docs/dev/GE-ENV-PROBES.md`, `GE_D193A` row) so the capture names an actual frame number, not an opaque address. This was not implemented this pass (probe-design work only, no code changed for this specific addition) — left for the next build/probe cycle alongside M-163's own live re-test. diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 9ab045f7..c89b8fb3 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -1148,6 +1148,8 @@ 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. +**Update (same D243 investigation, later in the same project's life): fixing the field you found the bug in doesn't mean you've fixed what's rendered — check which field the CONSUMER actually reads.** Having found `chrTick` overwrites `prop->pos`, two live-playtest cycles were spent neutralizing exactly that overwrite (save/restore `prop->pos` around `chrTick`) — it worked completely (confirmed in logs: the field went smooth) and yet the visible symptom (camera shake) was completely unchanged. The reason: the camera's look-at filter doesn't read `prop->pos` at all — it reads `field_488.pos`, a *different* field written from a *different* data path (`render_pos`, the model's animated-skeleton transform, computed independently of `prop->pos` inside the same function). Both fields happened to show the same "period-4 cycling" symptom, which made them look like one bug through a probe that only logged `prop->pos` — they were two independent outputs of the same underlying animation-tick call, and only one of them was what actually got rendered. **The generalizable check, sharpened:** once you've confirmed a field is being overwritten, don't assume that field is what the visible symptom's consumer (a camera filter, a draw call, a collision check) actually reads — trace the consumer's own read site explicitly and confirm it's the same field before spending a live-test cycle suppressing the wrong one. A quick static check (grep the consumer's exact read expression) is far cheaper than a live playtest. + ## E. Process / method notes - Investigation loop is: reproduce → env-gated capped probe → root-cause diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 7585f858..96c9aea7 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -10492,9 +10492,45 @@ s32 playerTick(PropRecord *prop) #endif RenderPosView *rp = g_playerPointers[index]->bodyModel->render_pos; matrix_4x4_multiply_homogeneous(currentPlayerGetViewToWorldMtxf(), (Mtxf *) rp, (Mtxf *) mtx); +#ifdef PORT + /* D243 M-167: M-162/M-163 smoothed prop->pos (chr.c) but the + * visible shake was UNCHANGED -- because field_488.pos, which + * THIS site writes and which is what the camera's look-at + * filter (M-143's field_3B8/field_3C4-8-C) actually chases, + * comes from render_pos here -- a completely separate data + * path from prop->pos, driven by the model's own animated + * skeleton (subcalcmatrices/instcalcmatrices, computed inside + * chrTick's chrUpdateAnim -> modelTickAnim/subcalcpos calls, + * upstream of and independent from prop->pos). M-163's live + * capture confirmed this directly: field_488.pos kept cycling + * every tick (228.8/229.3/79.3/-1214.7 repeating) while + * prop->pos in the same capture was smooth. This experiment + * targets the actual variable the filter reads: skip this + * write entirely while active, freezing field_488.pos at + * whatever it was the moment suppression engaged. Gated on + * GE_D243X3 (separate from GE_D243X2 so results aren't + * conflated) + the same POSEND-active gate. THIS IS A TEST, + * NOT A FIX -- revert once it reports (see docs/dev/findings.md + * D243 M-167). */ + static int s_d243x3 = -1; + if (s_d243x3 < 0) { s_d243x3 = getenv("GE_D243X3") != NULL; } + if (!(s_d243x3 && (g_CameraMode == CAMERAMODE_POSEND))) + { +#endif g_playerPointers[index]->field_488.pos.x = mtx[12] + (mtx[4] * 7.0f); g_playerPointers[index]->field_488.pos.y = mtx[13] + (mtx[5] * 7.0f); g_playerPointers[index]->field_488.pos.z = mtx[14] + (mtx[6] * 7.0f); +#ifdef PORT + } + if (d243mEnabled()) { + osSyncPrintf("D243M: f488write frame=%d idx=%d x3active=%d f488pos=%.1f,%.1f,%.1f\n", + g_d243mFrameCounter, index, + (int) (s_d243x3 && (g_CameraMode == CAMERAMODE_POSEND)), + (double) g_playerPointers[index]->field_488.pos.x, + (double) g_playerPointers[index]->field_488.pos.y, + (double) g_playerPointers[index]->field_488.pos.z); + } +#endif } return ret;