D243 M-168: render_pos freeze -- 2 crashes fixed, stable but incomplete

Extended M-167's freeze technique to render_pos (Bond's own draw
position), aiming at the model-duplication symptom M-167 didn't fix.
Two live crashes along the way, both from the same root cause: model
pointers get reused by the allocator across different logical model
instances, so any attempt to skip subcalcmatrices() based on inspecting
render_pos's value or caching pointer identity can be fooled into
treating a brand-new, uninitialized instance as already-validated --
leaving render_pos at its fresh tombstone value for a later consumer
(sub_GAME_7F06C768, computing zDepth) to crash on.

Fixed by never skipping subcalcmatrices() at all -- it always runs now,
guaranteeing render_pos is never left invalid regardless of model
lifecycle. The freeze is purely a contents copy on top of a
now-guaranteed-valid buffer. Live-playtest stable (no crash, full
session).

But the result is informative-negative: freezing unconditionally also
suppresses the LEGITIMATE per-shot camera/model repositioning
(AI_TRYTeleportingChrToPad moves Bond between several distinct shots
during the cutscene), not just the illegitimate intra-shot cycling --
so the camera now looks the wrong direction for later shots and Bond
still appears stuck. Both GE_D243X3/GE_D243X4 remain diagnostic
experiments, not real fixes. Documented two next-step candidates
(delta-threshold re-baseline, or hooking the teleport call directly)
for distinguishing legitimate repositioning from the cycling bug.

porting-notes.md gets the generalized lesson: freezing a value and
freezing a pointer target are different operations -- the latter needs
the call that keeps the pointer valid to always run, with the freeze
applied to contents afterward, not to whether the call happens.
This commit is contained in:
James Dansereau committed 2026-09-17 13:43:08 -04:00
1 parent 53219c18ae
commit d2caaa09bc
3 files changed
+90 -1

No files matched your search

+17 -1
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 | **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. |
| 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."** M-168 (3 attempts, 2 live crashes fixed by never skipping `subcalcmatrices`, see file tail) extended the same idea to `render_pos`/Bond's own draw position — stable (no crash) on attempt 3, but freezing unconditionally also suppresses the *legitimate* per-shot camera/model repositioning (`AI_TRYTeleportingChrToPad`), so the camera now looks the wrong direction for later shots and Bond still appears stuck — both experiments remain diagnostic-only, not real fixes; a real fix needs to distinguish legitimate teleports from illegitimate intra-shot cycling (see M-168's two next-step candidates 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… |
@@ -10953,6 +10953,22 @@ Cross-referencing M-145's already-established architecture: `AI_TRYTeleportingCh
**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.
**M-168 (2026-09-17, same session): three attempts to extend the freeze technique to Bond's own rendered body (the "duplicated/glitching actor" symptom M-167 didn't fix) — two crashed live, the third holds.** Applying M-167's technique to `render_pos` (the actual draw-path consumer, read by `objecthandler.c`'s `drawjointlist` via `gSPSegment`) seemed structurally analogous, but wasn't: `render_pos` is a pointer into a per-frame dynamically-allocated arena, reassigned every tick by `subcalcmatrices`/`instcalcmatrices` (`model.c:2453`), not a fixed field like `field_488.pos`.
*Attempt 1* (a local-delegate-reviewed suggestion, initially promising): skip the `subcalcmatrices()` call entirely, on the reasoning that leaving `render_pos` untouched reuses last tick's valid buffer. Guarded by checking `render_pos != NULL && != (RenderPosView*)-1` before deciding it was safe to skip. **Crashed on the very first smoke test** — fault addr exactly `-1`, at the identical class of bug M-154 already catalogued: a freshly-`CREATE`d model's `render_pos` starts as whatever garbage an AnimModelSlot's reused memory holds, not necessarily the clean `-1` sentinel or `NULL` — inspecting the *value* can never reliably distinguish "already validated" from "fresh tombstone that happens to look plausible."
*Attempt 2*: replaced the value-inspection with tracking our own history — skip only once *this code* had already run `subcalcmatrices` for this exact `Model*` pointer at least once (`s_d243x4Model` static, compared by identity). Smoke-tested clean (never reached a second model creation in the short bare-boot window) — **but crashed on the live playtest**, at the exact abseil transition. Symbolized the crash properly this time (`addr2line` against the `RelWithDebInfo` build): `chr.c:2822` (`sub_GAME_7F06C768`) → `model.c:459` (`return -mtx->m[3][2]`) → `getsubmatrix`/`modelFindNodeMtx` (`model.c:403-411`, `&model->render_pos[index].pos`) — a completely different consumer of `render_pos` than the draw path, one that runs unconditionally every tick regardless of my skip logic, to compute the model's z-sort depth. Root cause: **model POINTERS get reused by the allocator across genuinely different logical model instances** — the abseil's model happened to land at the same address a previously-removed model had occupied, so `s_d243x4Model`'s identity check wrongly concluded "already validated" for a brand-new instance whose `render_pos` was still the fresh, uninitialized `-1` tombstone. Skipping `subcalcmatrices` left it that way; `sub_GAME_7F06C768` dereferenced it and crashed at fault address exactly `-1` again — same failure class as attempt 1, reached through a different code path, because **there is no way to tell "is this the same logical instance" from a pointer value or from `render_pos`'s own content** — this is the same sentinel-guessing trap M-154 already named, encountered a second time from a different angle.
*Attempt 3* (current): stopped trying to skip `subcalcmatrices()` at all — it now **always** runs, unconditionally, guaranteeing `render_pos` is never left invalid by this experiment regardless of model lifecycle/memory reuse. The freeze is purely a *contents* overwrite applied after: cache the buffer for the first 2 ticks after the `GE_D243X4` gate engages (letting the pose settle past the transition moment), then copy that cached snapshot back over each subsequent tick's freshly (and validly) computed buffer. The active→inactive→active edge resets the warm-up counter so a later, different model under the same gate re-warms instead of inheriting a stale snapshot. Compiled clean, linked, a 2700+-frame bare-boot smoke test with zero crashes.
**Live playtest (attempt 3): NO CRASH — stable this time.** Full session, 7681+ frames, clean window close, abseil model created and ticked normally (`D243M: CREATE frame=4027 ... cam=7 subcam=1`). Structural fix (always-run + contents-only freeze) holds under real gameplay, unlike attempts 1/2.
**But the result is informative-negative, not a win — freezing unconditionally breaks legitimate repositioning, not just the bug.** User's report: no more shake (M-167 still holds, X3 unaffected by this pass), but (a) the camera angle now looks down/wrong for parts of the cutscene, and (b) Bond's own model still appears stuck/duplicated. **This makes sense and is a real, useful finding**: the abseil cutscene is not one continuous shot — `AI_TRYTeleportingChrToPad` (M-145/M-164) re-teleports Bond to a new pad for each of several shots, and each of those teleports is a *legitimate* position change the camera/render SHOULD track. `GE_D243X3`/`GE_D243X4` freeze `field_488.pos`/`render_pos` at whatever they held **the instant the gate first engaged** and never update again for the rest of POSEND — correctly killing the illegitimate *intra-shot* cycling (M-160's mechanism), but also incorrectly suppressing the legitimate *inter-shot* repositioning, so later shots render/frame from a stale, too-early position. This explains both symptoms without needing a new hypothesis: "camera looks down/wrong angle" = frozen at an early shot's framing; "Bond stuck in place" = same freeze applied to the model's own draw position.
**This sharpens what a real fix needs to do, distinct from what these two blunt experiments do:** distinguish the two kinds of position change — legitimate per-shot teleports (should update `field_488.pos`/`render_pos`) from the illegitimate per-tick cycling within a shot (should not) — rather than freezing everything indiscriminately. Concrete next-step candidates, neither attempted yet: (a) re-baseline the freeze on a large-delta threshold (a jump far bigger than the observed intra-shot cycling amplitude is very likely a real teleport, not cycling — M-153/M-160's data already characterizes the cycling amplitude, so a threshold is derivable from existing captures), or (b) hook the freeze/re-baseline directly to `AI_TRYTeleportingChrToPad` firing (`chrai.c:4064`), which is the actual, precise signal for "a legitimate reposition just happened" rather than inferring it from position deltas.
**Generalizable lesson (added to `porting-notes.md` alongside the existing D11 material):** freezing a *value* (M-167's `field_488.pos`, a plain field) and freezing a *pointer target* (M-168's `render_pos`, a per-tick-reallocated buffer) are not the same kind of operation — the latter requires either (a) never skipping the call that keeps the pointer valid and only touching contents afterward, or (b) extremely careful instance-identity tracking that accounts for allocator address reuse, which is easy to get wrong twice (as this session did) before landing on (a).
**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.
+2
View File
@@ -1150,6 +1150,8 @@ 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.
**Update 2 (same investigation, immediately after): freezing a *value* and freezing a *pointer target* are different operations — treating them the same crashed twice.** Extending the same freeze technique to `render_pos` (a `Model*` field, unlike `field_488.pos` a plain value) by skipping the call that reassigns it (`subcalcmatrices`) seemed equivalent to M-167's "skip the write" but wasn't: `render_pos` is a pointer into a per-tick dynamically-reallocated arena, and skipping its reassignment only works if the pointer it's left holding is still valid — which depends on model lifecycle, not just on this tick's logic. Two fixes in a row tried to detect "is it safe to skip" by inspecting either the pointer's *value* (a sentinel check, `!= NULL && != -1`) or a *cached identity* (comparing the pointer against a remembered "already validated" pointer) — both crashed, because **the allocator reuses model-pointer addresses across genuinely different logical model instances**: a freshly-`CREATE`d model can land at the exact address a previously-removed model occupied, so both the sentinel check and the identity check wrongly conclude "this is already valid" for an instance whose `render_pos` is still an uninitialized tombstone. This is the same "don't guess validity from a value that can coincidentally look right" trap as the paragraph above, encountered a second time from a different angle (a *pointer's* target identity, not a *field's* content). **The robust fix:** never skip the call that establishes the pointer's validity — let it always run — and apply the freeze as a *contents* copy afterward, on top of a buffer that's now unconditionally guaranteed valid. If you must "hold something steady" and that something is reached through a pointer that some other code reassigns, freeze the *pointee's bytes*, not the *act of assignment* — the assignment is usually also the thing keeping the pointer sane.
## E. Process / method notes
- Investigation loop is: reproduce → env-gated capped probe → root-cause
+71
View File
@@ -2776,6 +2776,77 @@ after_position_update:
subcalcmatrices(&renderdata, model);
#ifdef PORT
/* D243 M-168 (rewritten after two live crashes -- see findings.md
* D243 M-168 for the full postmortem): same technique as M-167
* (field_488.pos freeze), aimed at the actual draw-path consumer of
* the cycling skeleton state -- render_pos itself. M-167 only froze
* the camera's copy (field_488.pos) and fixed the shake, but the
* user still saw Bond's own rendered body glitch/duplicate --
* expected, since the draw call reads render_pos directly
* (objecthandler.c's drawjointlist, via gSPSegment), a path M-167
* never touched.
*
* The first two attempts tried to SKIP this subcalcmatrices() call
* entirely once a model "looked already validated" -- both crashed,
* because a model POINTER can be reused by the allocator for a
* genuinely new logical model instance (a new CREATE at the same
* freed address), and there is no way to tell "is this the same
* instance" from the pointer value or from render_pos's own content
* (M-154 already learned the render_pos sentinel-guessing lesson
* once; this is the same trap from a different angle). Skipping the
* call left a brand-new instance's render_pos at its fresh,
* uninitialized -1 tombstone, and sub_GAME_7F06C768() (model.c:459,
* called moments later at chr.c:2822 to compute zDepth) dereferenced
* it and crashed at fault address -1.
*
* This version NEVER skips the call -- subcalcmatrices() always
* runs above, unconditionally, so render_pos is always left valid.
* The freeze is purely a CONTENTS overwrite on top of that
* guaranteed-valid buffer: cache the buffer for a couple of ticks
* after the gate first engages (letting the pose settle from
* whatever it was mid-transition), then hold it steady by copying
* the cached contents back over each subsequent tick's freshly
* (and validly) computed buffer. Gated on GE_D243X4 +
* d243mProbeActive(); the active/inactive edge resets the warm-up
* counter so a later, different model under the same gate re-warms
* instead of inheriting a stale snapshot. Capped at 128 matrices
* (generous; GE character models are well under this). THIS IS A
* TEST, NOT A FIX -- revert once it reports. */
{
extern int d243mProbeActive(void);
static int s_d243x4 = -1;
static int s_d243x4WasActive = 0;
static int s_d243x4WarmTicks = 0;
static RenderPosView s_d243x4Snap[128];
static s16 s_d243x4Count = 0;
if (s_d243x4 < 0) { s_d243x4 = getenv("GE_D243X4") != NULL; }
{
int active = s_d243x4 && d243mProbeActive();
if (active && !s_d243x4WasActive) { s_d243x4WarmTicks = 0; }
s_d243x4WasActive = active;
if (active && (model->render_pos != NULL) && (model->obj != NULL))
{
s16 n = model->obj->numMatrices;
if (n > 0 && n <= 128)
{
s32 i;
if (s_d243x4WarmTicks < 2)
{
for (i = 0; i < n; i++) { s_d243x4Snap[i] = model->render_pos[i]; }
s_d243x4Count = n;
s_d243x4WarmTicks++;
}
else if (n == s_d243x4Count)
{
for (i = 0; i < n; i++) { model->render_pos[i] = s_d243x4Snap[i]; }
}
}
}
}
}
#endif
g_ModelJointPositionedFunc = NULL;
modelSetDistanceScale(1.0f);