forked from Ports/goldeneye-pc-port
D156: guard NaN/blown-up anim frame -> unbounded frame-step loop (Facility outro hang)
After D155 the user re-ran the playthrough and it hung again at the end of Facility -- same stack but frames=12962 frozen CONSTANT for minutes = a true infinite loop, not D155's slow spiral. The `while (1)` at model.c:3131 in modelSetAnimFrame2WithChrStuff steps root-motion accumulation one anim frame at a time from framea to frameb. frameb comes from modelTickAnim's `frame += playspeed * speed` per tick. A cutscene anim transition with a near-zero blend/timespeed/unkb0 denominator (model.c:3436/:3477, guarded >0 but not against tiny values) makes model->speed/playspeed huge or NaN -> frameb huge/NaN -> floorFloatToInt garbage -> endframe-curframe ~= 2^31 -> effectively infinite. #ifdef PORT guards (N64 unchanged): - modelSetAnimFrame2WithChrStuff: if frameb non-finite or |frameb| >= 1e6, snap to framea (loop does ~0 iterations; modelConstrainOrWrapAnimFrame at :3211 still clamps model->frameb to a real frame). - modelTickAnim: after the tick loop, if frame/frame2 went non-finite, fall back to the pre-loop model->animframe1/animframe2 so the NaN can't poison model state every frame. `!(x > -1e6f && x < 1e6f)` catches NaN too. Anim frames are 0..few-hundred so 1e6 is a safe ceiling. Likely deeper cause (needs the repro): these speed/blend fields are Model members -- a D100/D140-style struct-pun misalignment would feed garbage exactly when a cutscene sets an unusual transition. Dump model->speed/playspeed/timespeed/unkb0 in modelTickAnim during the cutscene if it still hangs. Verified: build green; -level_09 frame 200 unregressed (guards never fire in normal play). PORT-LEARNINGS §E.
This commit is contained in:
1 parent
f33894f2a2
commit
f887165ff4
3 files changed
+57
-2
No files matched your search
@@ -836,6 +836,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D150 | **Watch objective/briefing page crash (M-28).** Opening the watch → BRIEF (any subpage) or OBJECTIVES page → AV in `strcat` (`str.c:25`, `Rdi/src = 0`). The pages assemble their display text with `strcpy`/`strcat(buf, langGet(id))` (`options.c:3940-4068`, `front.c:3538-3562`, `propobj.c`). `langGet()` returns **NULL** on PC for a string bank the menu flow never loaded (D129/D143 guards — briefing/objective banks in the in-level watch flow), so `strcat(buf, NULL)` derefs NULL. On N64 those ids always resolve so the decomp never guards. Twist: `str.c`'s `strcpy/strncpy/strcat` are `__nonnull__` **builtins** to GCC, so a plain `if (src == NULL)` on the parameter is optimised away as provably-dead (verified: `-Og` elided it, no `test` in the disassembly). Fix: `#ifdef PORT` NULL guards that launder the pointer through an empty `__asm__` (`GE_IS_NULL()` / `ge_launder_ptr()`) so the check survives — NULL src → treat as empty string (blank text, not a crash), NULL dst → return. Same philosophy as the D143 textRender/textMeasure NULL guards, one layer down. Inert on N64. | FIXED (`src/str.c`, `#ifdef PORT`). Build green (`ntsc-final`); `-level_09` 600+ frames @ 91.67% unregressed. Watch-page repro is interactive — user to re-verify. PORT-LEARNINGS §C. |
|
||||
| D152 | **Mission-failed / objective-failed → permanent black screen (M-28, user bug report — LIVE process inspected, not yet fixed).** User killed Trevelyan in Facility (Ctrl = fire, no crouch bind) → failed the objective → screen faded to black and never came back; process stayed alive under `debug.ps1`. `gdb.txt` (kept running): rendering stops dead at frame 12600 (`frames=12657` frozen across 206 heartbeat dumps / ~7 real min), VI pacemaker still posts. **Deadlock on `s_imLock`** — the D147 recursive-mutex that backs `osSetIntMask`. Both consumer threads are blocked in `pthread_mutex_lock(&s_imLock)` and neither owns it: `mainThread` = `sndSetScalerApplyVolumeAllSfxSlot` → `sndApplyVolumeAllSfxSlot` → `sndSetSfxSlotVolume` → `alEvtqPostEvent` (`event.c:82`) → `osSetIntMask(OS_IM_NONE)` (`libultra.c:1232`); `amMain`/audioThread = `amHandleFrameMessage` → `alAudioFrame` → `sndPlayerVoiceHandler` → `alEvtqNextEvent` (`event.c:47`) → `osSetIntMask(OS_IM_NONE)`. `sndSetScalerApplyVolumeAllSfxSlot` is the **mission-failed audio fade-out** (ramps `g_sndSfxVolumeScale` → 0 each frame, `AL_SNDP_RELEASE_EVT` posted per active `ALSoundState` per frame) — so the fade-to-black *is* the intended fail transition, and it hammers the event queue hard enough that the `osSetIntMask` lock is left owned by a party not in the 3-thread dump (a leaked unbalanced `OS_IM_NONE` — libaudio has early-return mask paths — or a transient thread that acquired it and exited; `gdb.txt` shows `New Thread`/`exited` churn every few k frames). D147's "the decomp never blocks while holding OS_IM_NONE → cannot deadlock" assumption is **falsified** by this path. Same family as D147 / PORT-LEARNINGS §D4. | MITIGATED (`port/src/libultra.c`, M-28, `#ifdef PORT`/port-only). `osSetIntMask` is now a **self-healing** logical lock: `OS_IM_NONE`/`OS_IM_ALL` acquire/release a process-wide recursive critical section tracked by `s_imHeld`/`s_imOwner`/`s_imDepth` under a short bookkeeping mutex `s_imMx` + condvar. Normal (balanced, microsecond) audio critical sections behave exactly as the D147 recursive mutex did. A waiter that blocks **> 2 s** (`OS_IM_STUCK_NS`) concludes the holder leaked it, logs `LOG_ERROR` with the stale `s_imOwner` + the stealing caller's return address, and **steals** the section — so a leaked/unbalanced `OS_IM_NONE` (or an acquire-and-exit transient thread) can no longer wedge the game forever; worst case is a ~2 s audio hiccup + a log line naming the leak. Builds clean. Still OPEN: the exact leaking call site (find it from the `D152: ... stealing from owner=` log next time it fires) and the proper narrow fix (dedicated `ALEventQueue` lock, or restore-on-thread-exit). Regression check deferred — `-level_09`/`-level_20` both flaky under the current machine load (D117/D134), and clean `master` itself has a separate nondeterministic `-level_09` frame ~900-1400 `0xc0000005` (see below). |
|
||||
| D154 | **`bg.c` room-geometry ray/hit-test GBI parser port (M-28, WRITTEN / UNVERIFIED / UNCOMMITTED).** `bgTestRayIntersectionInRoom` (`bg.c:3331`, called by `bgTestBulletHitBackground` on every shot that resolves against background geometry) is the D135 sibling flagged since M-23: raw `((u8*)gdl)[k]` / `((u32*)gdl)[i]` byte/word indexing into what is now the 16-byte PC `Gfx` room DL (D85 `bgWidenRoomGdl` + `texLoadFromGdl`) → desync → OOB `vtxbase[idx]` / walk-off AV the first time you shoot a wall or floor. Ported `#ifdef PORT` (N64 verbatim under `#else`): `vtxoff`/`vtxbase`/`op` via the `.dma` view (matches the already-ported `bgBuildRoomVtxBounds`); G_TRI1 indices from `(w1>>16/8/0)&0xff /10`; the 12 G_TRI4 nibble extractions from `(u32)gdl->words.w0/.w1` (each mapped from the N64 byte/halfword layout — table in the code comment, cross-checked twice); both `texturenum` recoveries → `-1` (the KSEG0 `*(u16*)(w1-8)` deref is invalid for the port's converted GDLs, same as D135; texnum only picks the impact decal/sound, parked D77). Builds clean. **Verification blocked** — a no-input `GE_PCDUMP` capture never fires a bullet so it never calls this function; needs a real firefight into a wall on an idle machine (this session's box was thrashed → D153/driver crash on every run). | UNVERIFIED — build green, `#ifdef PORT` only. Next: idle-machine wall-shoot test + `-level_09` framediff, then commit. PORT-LEARNINGS §B (D135 corollary). |
|
||||
| D156 | **Facility outro-cutscene "hang" — the actual fix (M-29, user bug report, 2nd occurrence after D155).** After D155 shipped the user re-ran the playthrough and it hung again at the end of Facility — same stack (`modelSetAnimFrame2WithChrStuff` ← `modelTickAnim` ← `chrUpdateAnim` ← `chrTick` ← `playerTick` ← `propsTick` ← `lvlRender` ← `bossMainloop`) but this time **`frames=12962` frozen CONSTANT across ~200 heartbeat dumps / minutes** — a true infinite loop, not the D155 slow-catch-up spiral. Root: the `while (1)` at `model.c:3131` in `modelSetAnimFrame2WithChrStuff` steps the root-motion accumulation one anim frame at a time from `framea` (`model->animframe1`) to `frameb` (`frame`, accumulated in `modelTickAnim` as `frame += playspeed * speed` per tick). If `frameb` arrives **NaN or blown up** — a cutscene anim whose blend/transition math produces a huge/non-finite `model->speed`/`playspeed`/`timespeed` (`model.c:3436` `/ model->unkb0`, `:3477` `/ model->timespeed`, both guarded `> 0` but not against tiny values) — then `floorFloatToInt(frameb)` yields garbage, `endframe - curframe` ≈ 2^31, and the loop (each iteration: `sub_GAME_7F06D3F4` joint transform + `cosf`/`sinf` + `modelConstrainOrWrapAnimFrame`) runs effectively forever. **Likely deeper cause (unconfirmed, needs the repro):** these speed/blend fields are `Model` struct members; if this model is punned/inline (D100/D140 family) and `sizeof(Model)` growth misaligned them, they read garbage exactly when a cutscene sets an unusual anim transition. | GUARDED (`src/game/model.c`, `#ifdef PORT`): (1) in `modelSetAnimFrame2WithChrStuff`, if `frameb` is non-finite or `|frameb| >= 1e6` snap it to `framea` (loop then does ~0 iterations; the `modelConstrainOrWrapAnimFrame` at :3211 still clamps `model->frameb` to a valid frame); (2) in `modelTickAnim` after the tick loop, if `frame`/`frame2` went non-finite/absurd fall back to the pre-loop `model->animframe1`/`animframe2` so the NaN can't poison model state every frame. `!(x > -1e6f && x < 1e6f)` catches NaN too. Anim frames are 0..few-hundred so 1e6 is a safe ceiling. Verified: build green; `-level_09` frame 200 unregressed (guards never fire in normal play). **User to re-verify the Facility outro.** If it still hangs, the NaN source is upstream — dump `model->speed/playspeed/timespeed/unkb0` in `modelTickAnim` during the cutscene. PORT-LEARNINGS §E. |
|
||||
| D155 | **Facility outro-cutscene end → "hang" = unclamped `deltaFrames` spiral (M-29, user bug report).** User watched the Facility outro cutscene; at the end the game froze (process alive, kernel-heartbeat dumps). Symbolicated `mainThread` stack from the port's own hang dump: `modelConstrainOrWrapAnimFrame` ← `modelTickAnim` ← `chrUpdateAnim` ← `chrTick` ← `playerTick` ← `propsTick` ← `lvlRender`, sampled there across *every* heartbeat dump. Root cause: `waitForNextFrame()` (`src/game/frametiming.c`) sets `nextFrameTime = (osGetCount() − prev + 387937) / 775875` and passes it **unclamped** to `updateFrameCounters` as `deltaFrames`. `osGetCount()` is wall-clock on the port (D117), not VI-locked, so a real-time stall — the asset load at the cutscene→debrief boundary, made far worse by the user running a local LLM on the same box — makes `nextFrameTime` balloon to hundreds/thousands. That becomes `speedgraphframes` → `g_ClockTimer`, which drives `modelTickAnim(model, g_ClockTimer, 1)`'s `while (numticks-- > 0)` loop once per character per render, plus dozens of `for (i = 0; i < g_ClockTimer; i++)` sim loops (`bondhead.c`, `bondview2.c`, `explosion.c`, `options.c`…). One render then does thousands of anim ticks × N characters → multi-second frame → heartbeat trips, and the slow frame feeds an even bigger delta next time → unrecoverable spiral. The N64 was physically VI-bound and never produced `deltaFrames > ~2`. Timing-compensation class, cf. D117/D134. | FIXED (`src/game/frametiming.c`, `#ifdef PORT` — clamp `nextFrameTime` to `FRAMETIMING_PORT_MAX_CATCHUP` = 6). After a hitch the sim just resumes at real-time-ish pace instead of spiralling, exactly as the console did when it dropped frames under load. N64 path verbatim. Confidence: high (stack + arithmetic both point here). PORT-LEARNINGS §E. |
|
||||
| D153 | **`-level_09` frame ~900–1400 `0xc0000005` = D117/D134 load flakiness, NOT a bug (M-28).** Reproduced 6/6 while the machine was under heavy concurrent build+run load; on a **quiet** machine `-level_09` ran past frame 2400 with 0 heartbeats, no crash. Same false-positive class the docs warn about every session (M-13..M-22, M-25 Silo ~frame 300 freeze). The crash PC was in a system DLL (a `memcpy` off a buffer) and the frame varied run-to-run — both consistent with the port's host-scheduling jitter under load (`osGetCount` is wall-clock, variable-timestep sim), not a deterministic defect. `romdataFixupMusicSeqTable: seqCount 63 exceeds blob capacity 1` is an EXPECTED benign warning on the header-only first `romdataFixupMusicSeqTable` call (music.c:736; the full-size second call at :742 is silent) — not related. **Lesson: don't regression-test while builds/other runs are in flight.** | NOT A BUG — closed. Run anchors on a quiet machine. |
|
||||
| D151 | **Watch data all blank — objectives / mission background / M / Q / Moneypenny (M-28, user bug report).** Not a crash (D150 fixed that); every line renders empty. The watch text lives in `propDefs` record types **35** (`WatchMenuObjectiveText`) and **23** (`ObjectiveStart`). In the setup stream each record's 3rd word is a **plain 32-bit language slot id** (see `assets/obseg/setup/Usetup*Z.c` `propDefs[]`: `_mkword(0,_mkshort(0,35)), menu, 11281, 0`). The decomp structs `struct watchMenuObjectiveText` / `struct objective_entry` decode that word as `u16 reserved; u16 text;` and read `text` at offset `0xA` — which only works on the N64 because the id sits in the **low 16 bits of a big-endian word**. On LE, `tools_pc/d88_propdefs.py` `_bswap32`'s the whole word (correct for a 32-bit field), moving the id to the low bytes, so `text` (u16 @0xA) reads `0x0000` → `langGet(0)` → NULL → `get_ptr_text_for_watch_breifing_page` / `get_text_for_objective` return NULL/blank. This is the "briefing/objective text renders blank" side-effect left open under D143 / D150. The per-level lang bank itself IS loaded in-level (`langLoadToAddr(langGetLangBankIndexFromStagenum(stageId))`, `prop.c:1274`). | FIXED (`src/bondtypes.h`, `#ifdef PORT` — `text` is a full `u32` covering the whole word in both structs; N64 `u16 reserved; u16 text;` kept under `#else`). No converter change, no sidecar regen. `nextentry`/`difficulty`/`unkD` unchanged. PORT-LEARNINGS §C. Build compiles clean; interactive re-verify pending (open the watch in any level). |
|
||||
|
||||
@@ -423,3 +423,19 @@ a few others use it as their **only** mutual-exclusion primitive. On PC the
|
||||
"number of elapsed ticks" value run unbounded is the same latent trap — grep
|
||||
for `g_ClockTimer` / `speedgraphframes` / `lvupdate*` consumers that loop.
|
||||
§F **D155**.
|
||||
- **D156 — a NaN / blown-up `f32` frame or speed drives an unbounded
|
||||
frame-stepping loop → hang.** `modelSetAnimFrame2WithChrStuff`'s `while (1)`
|
||||
(`model.c:3131`) walks one anim frame at a time from `framea` to `frameb`;
|
||||
`frameb` comes from `modelTickAnim`'s `frame += playspeed * speed` per tick.
|
||||
A cutscene anim transition with a near-zero blend/`timespeed`/`unkb0`
|
||||
denominator (`model.c:3436`/`:3477`, guarded `> 0` but not against tiny
|
||||
values) makes `model->speed`/`playspeed` huge or NaN → `frameb` huge/NaN →
|
||||
`floorFloatToInt` garbage → ~2^31 iterations → frozen (`frames=N` constant
|
||||
in the hang dump, distinct from D155's slowly-incrementing spiral). Fix:
|
||||
`#ifdef PORT` finiteness+magnitude guards (`!(x > -1e6f && x < 1e6f)` also
|
||||
catches NaN) on `frameb` before the loop and on `frame`/`frame2` before they
|
||||
reach `model` state. **When an N64 float pipeline feeds a loop bound or an
|
||||
array index on PC, guard it** — the console's fixed timestep + bounded
|
||||
anim/physics data never produced the degenerate value, so the decomp never
|
||||
checks. Suspect a misaligned `Model`/struct field (D100/D140 pun family) as
|
||||
the NaN source before blaming the data. §F **D156**.
|
||||
+40
-2
@@ -3117,6 +3117,27 @@ void modelSetAnimFrame2WithChrStuff(Model *model, f32 framea, f32 frameb, f32 fr
|
||||
forward = 1;
|
||||
}
|
||||
|
||||
#ifdef PORT
|
||||
/* D156: this loop steps the root-motion accumulation one anim
|
||||
* frame at a time from framea to frameb. If frameb arrives NaN or
|
||||
* blown up -- a cutscene anim whose speed/timespeed math in
|
||||
* modelTickAnim produced a huge/non-finite model->speed, which
|
||||
* feeds `frame` and lands here -- then floorFloatToInt(frameb)
|
||||
* yields garbage and the loop walks ~2^31 steps -> permanent hang
|
||||
* (seen at the Facility outro cutscene: mainThread pinned here for
|
||||
* minutes, frames=N frozen). The animation has at most anim->unk04
|
||||
* frames, so a valid step is tiny; if frameb is non-finite or
|
||||
* absurd, snap it to framea (loop then does ~0 iterations) and let
|
||||
* the single modelConstrainOrWrapAnimFrame at :3211 clamp frameb
|
||||
* to a real frame. N64 was fixed-timestep with bounded speeds and
|
||||
* never reached this. `!(x > -1e6 && x < 1e6)` is also true for
|
||||
* NaN. */
|
||||
if (!(frameb > -1.0e6f && frameb < 1.0e6f))
|
||||
{
|
||||
frameb = framea;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (forward)
|
||||
{
|
||||
curframe = floorFloatToInt(framea) + 1;
|
||||
@@ -3595,9 +3616,26 @@ void modelTickAnim(struct Model *model, s32 numticks, s32 update_chrstuff)
|
||||
numticks--;
|
||||
}
|
||||
|
||||
if (update_chrstuff)
|
||||
#ifdef PORT
|
||||
/* D156: if the per-tick `frame += playspeed * speed` accumulation
|
||||
* went non-finite or absurd (bad cutscene anim speed/timespeed math),
|
||||
* don't let it reach modelSetAnimFrame2* -- it would drive the
|
||||
* frame-interpolation loop for ~2^31 steps (Facility outro hang) and
|
||||
* poison model->animframe1 for every later frame. Fall back to the
|
||||
* pre-loop frame; the anim just holds instead of hanging. */
|
||||
if (!(frame > -1.0e6f && frame < 1.0e6f))
|
||||
{
|
||||
if (model->anim2 != NULL)
|
||||
frame = model->animframe1;
|
||||
}
|
||||
if (!(frame2 > -1.0e6f && frame2 < 1.0e6f))
|
||||
{
|
||||
frame2 = model->animframe2;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (update_chrstuff)
|
||||
{
|
||||
if (model->anim2 != NULL)
|
||||
{
|
||||
modelSetAnimFrame2WithChrStuff(model, model->animframe1, frame, model->animframe2, frame2);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user