findings: D243 M-145 - two-Bonds observation confirms third-person model lifecycle bug

Static trace confirms user's hypothesis architecturally: the Dam exit
script runs on a model-less BG driver chr, while the visible Bond is the
player's own third-person model teleported via chr_try_teleporting_to_pad
(CHR_BOND_CINEMA). Ties the abseil camera shake (M-143/M-144) and the
double-Bond image to one model-lifecycle mechanism; cross-refs D173/D292.
This commit is contained in:
James Dansereau committed 2026-09-16 09:38:02 -04:00
1 parent 08dbd581e9
commit 6f28156956
1 file changed
+3 -1
+3 -1
View File
@@ -10435,7 +10435,7 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A
**Third-person Bond model spawns too high** (user QA report): the player-representing figure at level start floats well above the ground "a lot of the time" while the actual player spawn is correct; same on the Cuba end credits — JB ~6 ft in the air above Natalya. D75 animated-model family but a *position* defect, not absence (M-33: gun-barrel Bond renders fine). Suspects: intro/credits puppet chr spawn position vs model base transform; anim root-motion accumulation (`modelSetAnimFrame2WithChrStuff`, cf. D156); packed-float `coord3d` decode in intro setup. "Player spawning OK" localises it to the render-side model, not `g_CurrentPlayer`. See `GRAPHICS-BACKLOG.md` D173.
**Status (from §F table):** OPEN — observed, not investigated. **M-87 user playtest: still present** — "Bond floating in the sky / cutscene glitch thing still exists." Related, newly reported: **in some cutscenes Bond spins in circles** (named instance: end of Cradle, when Trevelyan falls) — likely the same actor-position/anim-root class rather than a new bug; logged under D148/D160 (`GRAPHICS-BACKLOG.md`) since it's cutscene-triggered. **M-106 investigation (static only — no build/runtime access in this worktree, see note below):** checked the two most D209-shaped candidates for a puppet Y-position aliasing bug and RULED OUT both. (1) `chrSpawnAtPad`/`chrSpawnAtCoord`/`chrAdjustPosForSpawn` (`src/game/chraction.c:10653-10794`) — the puppet-spawn path a cutscene `guard_play_animation`-style AI cmd would use. `PadRecord.stan` (`src/bondtypes.h:1744-1751`) is a ROM-serialized *pointer* field (`StandTile *stan` at offset 0x28) — exactly the tail-pointer shape D122/D126/D132 warn about — and `tools_pc/d88_emit.py`'s `emit_pad()` zeroes it on conversion with a comment claiming it's "dead-on-load, always set before use." **Verified this claim is TRUE, not a converter bug**: every `PadRecord` literal in the N64 source asset tables (checked `assets/obseg/setup/UsetupdamZ.c:37-51`) initialises `stan` to the literal `0` — the ROM binary never carries a live pointer there on N64 either, so zeroing it on PC is byte-identical behaviour, not a regression. `chrAdjustPosForSpawn` receiving a NULL seed `StandTile*` is therefore the original game's normal case, not a port defect. (2) The anim-root-motion Y-accumulation chain (`modelTickAnim`→`modelSetAnimFrame2WithChrStuff`, `model.c:3060+`, `pos24 = pos34 + rotate(pos)` at `model.c:3322`) was already exhaustively audited for exactly this class of bug during D193/D209 (M-80/M-81) and found self-consistent, with the `padding[]`-union sweep that found D209/D210 explicitly noting `act_anim`/`act_dead` (the union arms active during a played-animation/dead puppet) are "layout-stable" (`porting-notes.md` §A, D210 entry) — i.e. that sweep already covers the cutscene-puppet actiontype, and it came back clean. So the two most probable D209-style aliasing sites for this specific symptom are both cold. **Did not find the actual root cause.** Did not get to: the *separate* Cuba end-credits case (`front.c` `init_menu18_displaycast`/`constructor_menu18_displaycast`, `front.c:7821-8320`+ — a **different**, single-actor "cast roster" display model shared by the intro roster and the credits roster, using `getsuboffset`/`subcalcpos`/`cast_rootpos_smoothed` root-motion smoothing, not the in-level chr path at all) — spot-checked `getsuboffset` (`model.c:684-700`) and it is `#MATCH`-verified clean, but did not trace the smoothing math or confirm this is even the code path the "JB above Natalya" report hits (that report implies two actors on screen at once, which this single-`cast_model` code doesn't obviously support — may be a distinct, not-yet-located two-actor cutscene-puppet path). **Confidence: LOW-MEDIUM that this is the D209-style struct-aliasing family at all** — the two most likely aliasing sites check out clean, which argues *against* the shared-root-cause theory for D173 specifically (unlike D160, where the analogous check also came back clean but for a different reason — no pointer fields to alias in the first place). Next investigator: get a live Cradle (`-level_41`) capture of Trevelyan's chr during the death sequence with a position-logging probe (none shipped yet — would need a new `#ifdef PORT` env-gated trace, not added this session per the no-build-verification constraint), and separately locate the actual two-actor Cuba credits code path (grep for wherever `intro_char_table`/cast display is invoked with two simultaneous models, if it exists — not found this session). **M-141: no new lead this session** (see D243's M-141 entry) — checked whether D243's stale-memory front-end/menu bug could share a mechanism with this puppet-position bug; no code-level link found, kept as an unconfirmed hypothesis to watch for during the next live cutscene capture, not a new suspect to chase on its own. **M-141: Steam Deck playtest confirmation** — user reports Bond floating at level start on both Dam and Facility ("chemical plant"), "but fine when it moves to first person" (matches this row's own framing exactly: player spawn/first-person is correct, only the third-person render-side puppet floats). No new mechanism; corroborates the bug is general across levels, not Dam-specific. **User confirms D292** (Bond sunk into the floor on death) **is the same underlying bug**, a third instance alongside level-start floating and the Cradle cutscene spin — see D292 for the consolidated cross-reference.
**Status (from §F table):** OPEN — observed, not investigated. **M-87 user playtest: still present** — "Bond floating in the sky / cutscene glitch thing still exists." Related, newly reported: **in some cutscenes Bond spins in circles** (named instance: end of Cradle, when Trevelyan falls) — likely the same actor-position/anim-root class rather than a new bug; logged under D148/D160 (`GRAPHICS-BACKLOG.md`) since it's cutscene-triggered. **M-106 investigation (static only — no build/runtime access in this worktree, see note below):** checked the two most D209-shaped candidates for a puppet Y-position aliasing bug and RULED OUT both. (1) `chrSpawnAtPad`/`chrSpawnAtCoord`/`chrAdjustPosForSpawn` (`src/game/chraction.c:10653-10794`) — the puppet-spawn path a cutscene `guard_play_animation`-style AI cmd would use. `PadRecord.stan` (`src/bondtypes.h:1744-1751`) is a ROM-serialized *pointer* field (`StandTile *stan` at offset 0x28) — exactly the tail-pointer shape D122/D126/D132 warn about — and `tools_pc/d88_emit.py`'s `emit_pad()` zeroes it on conversion with a comment claiming it's "dead-on-load, always set before use." **Verified this claim is TRUE, not a converter bug**: every `PadRecord` literal in the N64 source asset tables (checked `assets/obseg/setup/UsetupdamZ.c:37-51`) initialises `stan` to the literal `0` — the ROM binary never carries a live pointer there on N64 either, so zeroing it on PC is byte-identical behaviour, not a regression. `chrAdjustPosForSpawn` receiving a NULL seed `StandTile*` is therefore the original game's normal case, not a port defect. (2) The anim-root-motion Y-accumulation chain (`modelTickAnim`→`modelSetAnimFrame2WithChrStuff`, `model.c:3060+`, `pos24 = pos34 + rotate(pos)` at `model.c:3322`) was already exhaustively audited for exactly this class of bug during D193/D209 (M-80/M-81) and found self-consistent, with the `padding[]`-union sweep that found D209/D210 explicitly noting `act_anim`/`act_dead` (the union arms active during a played-animation/dead puppet) are "layout-stable" (`porting-notes.md` §A, D210 entry) — i.e. that sweep already covers the cutscene-puppet actiontype, and it came back clean. So the two most probable D209-style aliasing sites for this specific symptom are both cold. **Did not find the actual root cause.** Did not get to: the *separate* Cuba end-credits case (`front.c` `init_menu18_displaycast`/`constructor_menu18_displaycast`, `front.c:7821-8320`+ — a **different**, single-actor "cast roster" display model shared by the intro roster and the credits roster, using `getsuboffset`/`subcalcpos`/`cast_rootpos_smoothed` root-motion smoothing, not the in-level chr path at all) — spot-checked `getsuboffset` (`model.c:684-700`) and it is `#MATCH`-verified clean, but did not trace the smoothing math or confirm this is even the code path the "JB above Natalya" report hits (that report implies two actors on screen at once, which this single-`cast_model` code doesn't obviously support — may be a distinct, not-yet-located two-actor cutscene-puppet path). **Confidence: LOW-MEDIUM that this is the D209-style struct-aliasing family at all** — the two most likely aliasing sites check out clean, which argues *against* the shared-root-cause theory for D173 specifically (unlike D160, where the analogous check also came back clean but for a different reason — no pointer fields to alias in the first place). Next investigator: get a live Cradle (`-level_41`) capture of Trevelyan's chr during the death sequence with a position-logging probe (none shipped yet — would need a new `#ifdef PORT` env-gated trace, not added this session per the no-build-verification constraint), and separately locate the actual two-actor Cuba credits code path (grep for wherever `intro_char_table`/cast display is invoked with two simultaneous models, if it exists — not found this session). **M-141: no new lead this session** (see D243's M-141 entry) — checked whether D243's stale-memory front-end/menu bug could share a mechanism with this puppet-position bug; no code-level link found, kept as an unconfirmed hypothesis to watch for during the next live cutscene capture, not a new suspect to chase on its own. **M-141: Steam Deck playtest confirmation** — user reports Bond floating at level start on both Dam and Facility ("chemical plant"), "but fine when it moves to first person" (matches this row's own framing exactly: player spawn/first-person is correct, only the third-person render-side puppet floats). No new mechanism; corroborates the bug is general across levels, not Dam-specific. **User confirms D292** (Bond sunk into the floor on death) **is the same underlying bug**, a third instance alongside level-start floating and the Cradle cutscene spin — see D292 for the consolidated cross-reference. **M-145 (see D243):** the Dam end-cutscene double-Bond + camera shake is now traced to the same family — the exit script moves Bond's own third-person model vLine truncated
## D179 — Packaged build crashes after the logos — the D43/D69 model + bg sidecars are ROM-derived and absent from any build that has no ROM (M-34, alpha-release QA).
@@ -10705,6 +10705,8 @@ crash class C2 (Facility `-level_34`, Runway `-level_35`): `import_texture_i8` A
**M-144: CONFIRMED — real N64 does NOT show this shake (user, direct comparison).** This settles the open question M-143 ended on: the abseil cutscene's look-at snap is a genuine PC-only divergence, not an original-game quirk. Since the filter math itself (`bondviewUpdatePlayerCollisionPositionFields`) is decompiled/byte-exact and its update loop is already `g_ClockTimer`-scaled, the bug is not "the filter is unreset" in isolation — that property is presumably just as true in the original binary, since nothing in `src/game` differs from what N64 would execute at this call site. **Reframed root-cause question:** something upstream of the filter must differ between N64 and PC — either (a) the actual position jump the filter is asked to absorb is larger/different on PC than on N64 (i.e. `field_488.pos` itself warps to a wrong or differently-timed value at cutscene entry — a genuine bug in the warp, with the filter just faithfully rendering visible whatever bad delta it's fed), or (b) something about call *ordering/frequency* around the `CAMERAMODE_INTRO` transition differs — e.g. `bondviewUpdatePlayerCollisionPositionFields()` (three call sites: two in normal/frozen movement ticks, one in the `DEATH_CAM_SP` branch) running an extra or missing time relative to when `g_CameraMode`/`dword_CODE_bss_80079A18` actually flips, which would desync the filter's internal state from what N64's single deterministic tick loop produces even though the per-call math matches. **Next, concrete (single next step, supersedes the N64-comparison item above since that question is now closed):** add one more `#ifdef PORT` probe logging `g_CurrentPlayer->field_488.pos` (the raw, pre-filter position) and `g_CurrentPlayer->field_3B8` (the filter's raw accumulator, pre-`FACTOR_2` scale) every tick for a few seconds either side of the moment `dword_CODE_bss_80079A18` becomes `CAMERAMODE_INTRO` — this directly shows (i) how large the real position jump is at the exact warp tick (confirms or refutes candidate (a)) and (ii) whether `field_3B8` is already desynced from `field_488.pos` *before* the warp even happens, which would point at candidate (b) instead. Do not touch `src/game/*.c` behavior yet (rule #2) — this is still diagnosis; if the eventual root cause is call-ordering in the port's scheduler (`port/src/gesched.c`) rather than game logic, that's a legitimate port-layer fix with no `#ifdef PORT` needed in `src/game` at all.
**M-145: independent review pass on the user's new "two Bonds" observation — the user's third-person-world-model hypothesis is confirmed architecturally; the camera bug and the model lifecycle are one mechanism.** User reports that during the Dam abseil, **two instances of Bond are visible at once — one standing on the dam, one floating in the air playing a jump/fall animation; neither animates correctly** — and suspects all the cutscene bugs (D148 umbrella) relate to a bug with Bond's third-person (world) model. Static trace this pass (no build): **(1) The exit script runs on an invisible, model-less "false GUARD" BG chr.** `alloc_false_GUARDdata_to_exec_global_action()` (`src/game/deb_loadallmodels.c:38`, called unconditionally from level setup at `prop.c:2054`; file IS compiled — `SRC_GAME` glob) binds every ailist with ID ≥ 0x1000 to a **zeroed** `ChrRecord` (chrnum 0xFE, model/prop NULL) ticked by `chrlvActionTick` (`chraction.c:9516`). Dam's exit chain — ai_24 (ID 0x1004: Bond auto-walks off the edge via `bond_set_locked_velocity`, falls until `if_bond_y_pos_less_than(0x98bd)` ≈ s16 −24771, fade, `chr_hide_all`, `camera_switch`) → ai_17 (ID 0x0412: the abseil) — runs on such a driver. Consequence: ai_17's `guard_play_animation(0xb100…)` rappel commands act on the **empty** driver record — `check_if_able_to_then_perform_animation` (`chraction.c:10263`) has no model to animate (D122's C1 NULL-deref crash class hit exactly this call on Dam). **(2) The visible Bond is the player's own third-person model, moved by `chr_try_teleporting_to_pad(0xFD, 0x4b01)`** — 0xFD = `CHR_BOND_CINEMA` (`bondconstants.h:4604`, comment: *"only works when bond has a third person model (intro/exit cutscene)"*), resolved by `chrResolveId` (`chraction.c:9792`) to `g_CurrentPlayer->prop->chr`. The teleport handler (`chrai.c:4064`) sets `prop->pos`, `setsuboffset(model, &pos)`, and `field_488.collision_position`/`current_tile_ptr` — but **not** `field_488.pos` and **not** the filter `field_3B8`. **(3) This reconciles M-143's sweep direction.** For `pos2.y` to rise from −22832 (the void, where ai_24's fall ends) up to +691 (dam top), the filter's input `field_488.pos` must be re-seeded to the post-teleport position — but under `CAMERAMODE_POSEND`, `bondviewUpdatePlayerCollisionPositionFields` (`bondview2.c:~4720`) **skips** the `pos ← collision_position` copy *unless `bodyModel == 0`*. The observed sweep therefore implies a window where Bond's third-person model was **removed** (bodyModel==0) after the teleport — tying the camera shake directly to the model lifecycle, exactly the user's intuition. **(4) Nothing in the script hides Bond:** `chr_hide_all` (`chrai.c:4164`) iterates guard slots only. The double image is most plausibly two live Bond models (stale pre-cutscene model still drawn at its fall position playing the fall anim + teleported cinema model standing on the dam with no rappel anim to play) or a stale draw of a removed model (D75 family) — not yet distinguishable statically. **Confidence: MEDIUM-HIGH** that D243's shake and the double image share the third-person-model-lifecycle root (arithmetic consistency with M-143's log + code trace; the bodyModel==0 window is inferred, not directly observed). **Next (supersedes M-144's single step):** one `#ifdef PORT` probe pass logging per tick around cutscene entry: `field_488.pos`, `field_3B8`, `prop->pos`, `bodyModel` (NULL/non-NULL), `g_CameraMode`, `dword_CODE_bss_80079A18`; plus call-trace on `bondviewRemovePlayerBody` and any `bodyModel` reassignment; plus a per-frame count of Bond-model draws during the cutscene to identify the second instance. That single capture discriminates (a) the re-seed window timing, (b) the double-draw source, (c) whether `guard_play_animation` is a PC no-op. No game-logic changes yet (rule #2). Cross-ref D173/D292: all three are now the same "third-person world model transform/lifecycle" family.
**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
## D244 — Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the…