forked from Ports/goldeneye-pc-port
D167: D152 addendum -- audio osSetIntMask audit + narrow lock-leak fixes
Static audit of every osSetIntMask(OS_IM_NONE) in the compiled audio code (event.c, csplayer.c, synaddplayer.c, snd.c): all balanced on every exit path, including alEvtqPostEvent's freeList-empty early return. No unbalanced same-thread leak exists -- the D152 s_imLock wedge is not a missing restore. Two real defects fixed (#ifdef PORT, N64 verbatim under #else): 1. snd.c sndSetSfxSlotVolume walked the live ALSoundState list and posted to the shared ALEventQueue without holding OS_IM_NONE, unlike its twin sndDeactivateAllSfxByFlag. On PC: unguarded walk racing amMain + one lock acquire/release per matching sound. During the mission-failed fade-out (sndSetScalerApplyVolumeAllSfxSlot -> sndApplyVolumeAllSfxSlot -> here, per frame) that is a per-frame lock-acquire storm. Now holds the mask once across the walk; sndApplyVolumeAllSfxSlot batches its slot loop under one recursive hold. 2. port/src/libultra.c: portThreadWrapper now calls imThreadExitRelease() when a host thread returns from its entry -- if it still owns s_imHeld, release the orphan (+ LOG_ERROR). Kills the 'transient thread acquired OS_IM_NONE and exited' leak (the §F dump's own alternate hypothesis) and the re-wedge when a new thread reuses the dead pthread id and imAcquire mis-detects recursion. Steal-lock kept as backstop. Build 240/240 green; -level_09 frame 300 renders 91.7% non-clear crash-free; -level_20 boots crash-free. Fade-out repro is playtest-gated (not headless- reproducible) -- user to confirm no black screen + no D152: log lines.
This commit is contained in:
1 parent
75641fb0b7
commit
dc5739bc93
4 files changed
+94
No files matched your search
@@ -807,6 +807,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D159 | front-end wallet-Bond photo "interlaced"/combed (RC1 / D149) — `texSwapAltRowBytes` odd-row 8-byte pre-swap (N64 RDP odd-line TMEM XOR compensation) not reversed by fast3d | FIXED (`#ifdef PORT` no-op the swap in `image.c`) |
|
||||
| D161 | Depot (`-level_30`) ceiling = bright-blue speckle + radial rays (B2). A CI8 tile drawn with `gsDPSetTextureLUT(G_TT_NONE)` was decoded against the stale `rdp.palette` → garbage. Fix: `#ifdef`-free narrow route CI→I when `palette_fmt == G_TT_NONE` in `gfx_pc.cpp import_texture()`. | FIXED (`port/fast3d/gfx_pc.cpp`) |
|
||||
| D165 · D166 | input-layer polish (M-31, port-only, `port/src/input.c`): D165 front-end cursor is now a true ~1:1 pointer (P-controller estimating the game cursor via front.c's own integrator) instead of velocity²; D166 hipfire mouse pitch emits speed-proportional C-button pulses instead of a fixed digital threshold. | FIXED |
|
||||
| D167 | D152 addendum (M-31): static audit of every compiled-audio `osSetIntMask(OS_IM_NONE)` — **all balanced**, no unbalanced early-return. Real fixes: `sndSetSfxSlotVolume` now holds the mask across its list walk (matches its twin `sndDeactivateAllSfxByFlag`) + `sndApplyVolumeAllSfxSlot` batches the slot loop under one recursive hold (kills the fade-out lock-acquire storm); `portThreadWrapper` calls `imThreadExitRelease()` on thread exit (kills the "transient thread acquired `OS_IM_NONE` and died" leak + the pthread-id-reuse re-wedge). Steal-lock kept as backstop. | FIXED (`src/snd.c`, `port/src/libultra.c`, `#ifdef PORT`) — fade-out repro playtest-gated, not headless-verified |
|
||||
| D85 | room primary/secondary DL → `texLoadFromGdl` garbage | OPEN (safety-netted; widen/pool fixes landed, geometry now renders) |
|
||||
| D86 · D87 | modelInitRwData truncated ptr · attract-demo BE `ramromfilestructure` | resolved |
|
||||
| D88.1–D88.3 · D88.5–D88.6 | `Usetup*Z` header + sub-table width/endian conversion (`d88_emit.py`) | resolved |
|
||||
@@ -843,6 +844,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D141 | **Watch-menu item-model crash (M-27), exposed by D140.** Pressing Start → watch page renders → `set_enviro_fog_for_items_in_solo_watch_menu` (`gunfire.c:1620`) → `modelGetNodeRwData(&model, root)` with `root` garbage → AV reading `root->Opcode` (`model.c:478`). `gunfire.c:1720-1743` walks `bodymodel->Switches[]` (a `ModelNode*` array) with **raw byte offsets** `((u8*)Switches) + j + 0x48` / `+ 0x5c` and `j += 4` — assumes 4-byte N64 pointers. `0x48/4=18`, `0x5c/4=23`, `k=j>>2=0..4` → intent is `Switches[18+k]` and `Switches[23+k]`. PC's 8-byte stride makes the raw math read misaligned garbage → bogus non-NULL `ModelNode*`. PORT-LEARNINGS §B (raw hardcoded stride into a pointer array, cf. D128). Fix: `#ifdef PORT` uses `bodymodel->Switches[18 + (j>>2)]` / `[23 + (j>>2)]`. | FIXED (`src/game/gunfire.c`, `#ifdef PORT`). Watch page now renders (weapon page verified — mirrored text D114/D116, dark weapon model D75, both parked cosmetic). `-level_09`/`-level_20` unregressed. |
|
||||
| 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). |
|
||||
| D152+ | **D152 addendum — static audit + narrow fix (M-31).** Audited every `osSetIntMask(OS_IM_NONE)` in the *compiled* audio code (`event.c` `alEvtqNextEvent`/`alEvtqPostEvent`/`alEvtqFlush`/`alEvtqFlushType`, `csplayer.c` `__CSPRepostEvent`, `synaddplayer.c` `alSynAddPlayer`, `snd.c` `sndRemoveEvents`/`sndSetupSound`/`sndDeactivateAllSfxByFlag`): **all balanced on every path** — `alEvtqPostEvent`'s lone early `return` (`event.c:86`, freeList empty) restores the mask first. The §F D152 guess "libaudio has unbalanced early-return mask paths" is **not** borne out. Two real defects found + fixed `#ifdef PORT` (N64 verbatim `#else`): **(1)** `sndSetSfxSlotVolume` (`snd.c`) walks the live `ALSoundState` list and posts `AL_SNDP_RELEASE_EVT` to the shared `ALEventQueue` **without holding `OS_IM_NONE`** — unlike its structural twin `sndDeactivateAllSfxByFlag`, which wraps its identical walk. On PC that's an unguarded walk racing `amMain` *and* one lock acquire/release per matching sound; the mission-failed fade (`sndSetScalerApplyVolumeAllSfxSlot`→`sndApplyVolumeAllSfxSlot`→ this, every frame, every active sound) turns it into a per-frame lock-acquire storm on `mainThread` = exactly the §F dump's "hammers the event queue hard enough that the lock is left owned." Fix: hold `OS_IM_NONE` once across the whole walk (nested `alEvtqPostEvent` hits the recursive depth++ fast path, no new contention window); also wrap `sndApplyVolumeAllSfxSlot`'s slot loop so the whole volume update is one recursive hold (1 real acquire/frame instead of `SFX_SLOT_COUNT × active-sounds`). **(2)** The actual leak matches the §F dump's *other* hypothesis — "a transient thread acquired `OS_IM_NONE` and exited" (`gdb.txt` shows `New Thread`/`exited` churn). `port/src/libultra.c` `portThreadWrapper` now calls new `imThreadExitRelease()` after the thread entry returns: if that thread still owns `s_imHeld`, release the orphan immediately (+ `LOG_ERROR` naming it). This removes the wedge *and* the ~2 s steal hitch, and stops the **re-wedge** that occurs when a new host thread reuses the dead thread's pthread id and `imAcquire` mis-detects recursion (`s_imOwner == self`). The M-28 steal-lock stays as the last-resort backstop for a genuinely unbalanced *same-thread* path (none found, but cheap insurance). Build green (240/240); `-level_09` frame 300 renders 91.7% non-clear, crash-free; `-level_20` boots crash-free (frame-count flaky under load per D117/D134, no `D152:` log). Fade-out repro is **playtest-gated** — not reproducible headless, so not end-to-end verified; user should replay the mission-fail and confirm no black screen + no `D152:` steal/orphan log lines. | FIXED (`src/snd.c`, `port/src/libultra.c`, `#ifdef PORT`). PORT-LEARNINGS §D4. |
|
||||
| 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). | **M-30 re-audit + fixes (still playtest-gated for final verify).** Cross-checked the ported G_TRI1 + all 12 G_TRI4 nibble recoveries twice against the N64 `#else` and the raw BE-word bit derivation — **G_TRI1 and G_TRI4 are correct**. **Two bugs fixed:** (1) `vtxoff` — the port read `gdl->dma.par & 0xf`, but the PC `Gdma_le` shim (`port/shim/PR/gbi.h`) maps `.par` to bits 0-23 of word0 (the packed *length*, always 16-aligned) not the N64 byte-1 params field at bits 16-23, so vtxoff was silently forced to 0 (breaks any G_VTX batch with `v0 != 0`, i.e. multi-batch rooms → wrong `vtxbase[idx]` → OOB). Now `((u32)gdl->words.w0 >> 16) & 0xf`. (2) **Sibling ported** — `bgTestBulletHitBackground` tail (`bg.c:~3841`, the post-hit G_SETTILE back-scan for `tileformat`/`tilesize`) was itself an unported raw `((u8*)gdl)[0]`/`[1]` + 64-bit `words.w0 << 11 >> 30` parser over the same widened DL; wrapped `#ifdef PORT` (opcode from bits 24-31, byte1 from bits 16-23, `(u32)`-forced 32-bit width for the tilesize shift). **Diagnostic:** `GE_D154=1` → `osSyncPrintf` of the parsed cmd/vtx-index stream (call header + per-TRI1/TRI4 `idx`) for the first 64 invocations, for the user/integrator to eyeball. Build green (`ntsc-final`, 242/242). Runtime unverifiable in a worktree (no `data/`); still needs a firefight-into-wall on an idle machine + `-level_09` framediff. Confidence: HIGH on the mechanical correctness (bit-for-bit vs N64), MEDIUM that nothing else in the walk still bites. 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. |
|
||||
|
||||
@@ -402,6 +402,36 @@ a few others use it as their **only** mutual-exclusion primitive. On PC the
|
||||
leak (worst case ~2 s audio hiccup). Proper narrow fix (dedicated
|
||||
`ALEventQueue` lock) still owed — do it once the steal-log names the
|
||||
leaking call site. §F D152.
|
||||
- **D152 addendum (M-31 static audit).** Every `osSetIntMask(OS_IM_NONE)` in
|
||||
the *compiled* audio code was audited: `event.c` (`alEvtqNextEvent`,
|
||||
`alEvtqPostEvent` — its lone early `return` at `event.c:86` **does** restore
|
||||
first —, `alEvtqFlush`, `alEvtqFlushType`), `csplayer.c` `__CSPRepostEvent`,
|
||||
`synaddplayer.c` `alSynAddPlayer`, `snd.c` `sndRemoveEvents` / `sndSetupSound`
|
||||
/ `sndDeactivateAllSfxByFlag`. **All balanced on every path** — the §F guess
|
||||
"libaudio has unbalanced early-return mask paths" is *not* borne out. Two
|
||||
real problems remain and were fixed `#ifdef PORT`:
|
||||
(1) `sndSetSfxSlotVolume` (`snd.c`) walks the live `ALSoundState` list and
|
||||
posts to the shared `ALEventQueue` **without** holding `OS_IM_NONE` — unlike
|
||||
its structural twin `sndDeactivateAllSfxByFlag`, which does. On PC that is an
|
||||
unguarded walk racing `amMain` *and* one lock acquire/release per matching
|
||||
sound; during the mission-failed fade (`sndSetScalerApplyVolumeAllSfxSlot`
|
||||
→ `sndApplyVolumeAllSfxSlot` → this, per frame) that is a per-frame
|
||||
lock-acquire storm — exactly what the §F dump means by "hammers the queue
|
||||
hard enough that the lock is left owned." Fix: hold the mask once across the
|
||||
whole walk (nested `alEvtqPostEvent` then hits the recursive fast path);
|
||||
also wrap `sndApplyVolumeAllSfxSlot`'s slot loop so the whole update is one
|
||||
recursive hold.
|
||||
(2) The actual leak is the "transient thread acquired `OS_IM_NONE` and
|
||||
exited" case (the dump shows `New Thread`/`exited` churn). `portThreadWrapper`
|
||||
now calls `imThreadExitRelease()` after the thread's entry returns: if that
|
||||
thread still owns `s_imHeld`, release the orphaned section immediately (+
|
||||
`LOG_ERROR`). Removes the wedge *and* the 2 s steal hitch, and stops the
|
||||
re-wedge that happens when a new host thread reuses the dead thread's
|
||||
pthread id and `imAcquire` mis-detects recursion. Steal-lock stays as the
|
||||
last-resort backstop for a genuinely unbalanced same-thread path.
|
||||
**Rule:** when two sibling functions walk the same list and post to the same
|
||||
queue, they must take the same lock — grep for one holding `OS_IM_NONE` and
|
||||
the other not.
|
||||
|
||||
## D5. Loop bounds that assume linker adjacency of two file-scope globals
|
||||
|
||||
|
||||
@@ -281,6 +281,8 @@ void portKernelInit(void)
|
||||
/* Threads */
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
void imThreadExitRelease(void); /* D152: defined in the interrupt-mask section below */
|
||||
|
||||
/* pthread entry wrapper. */
|
||||
static void *portThreadWrapper(void *arg)
|
||||
{
|
||||
@@ -302,6 +304,7 @@ static void *portThreadWrapper(void *arg)
|
||||
}
|
||||
|
||||
pt->entry(pt->arg);
|
||||
imThreadExitRelease(); /* D152: don't leak an OS_IM_NONE section on exit */
|
||||
pt->exited = 1;
|
||||
if (pt->os) pt->os->state = OS_STATE_STOPPED;
|
||||
return NULL;
|
||||
@@ -1351,6 +1354,32 @@ static void imRelease(void)
|
||||
pthread_mutex_unlock(&s_imMx);
|
||||
}
|
||||
|
||||
/* D152: a host thread that returns from its entry function while still
|
||||
* "inside" an OS_IM_NONE section (an unbalanced acquire on a code path only
|
||||
* that transient thread takes, or an osStopThread that let it bail early)
|
||||
* would leave s_imHeld owned forever -> mainThread + amMain both wedge in
|
||||
* alEvtq* until the 2 s steal fires, and can re-wedge every cycle once a new
|
||||
* host thread reuses the dead thread's pthread id (imAcquire then
|
||||
* mis-detects recursion). The steal-lock recovers from it but with a visible
|
||||
* hitch; releasing the orphaned section the instant its owner dies removes
|
||||
* the hitch entirely. Called from portThreadWrapper on thread exit. */
|
||||
void imThreadExitRelease(void)
|
||||
{
|
||||
unsigned long self = imSelf();
|
||||
pthread_mutex_lock(&s_imMx);
|
||||
if (s_imHeld && s_imOwner == self) {
|
||||
sysLogPrintf(LOG_ERROR,
|
||||
"D152: thread %lu exited still holding osSetIntMask section "
|
||||
"(depth=%d) -- releasing the orphan. An OS_IM_NONE on this "
|
||||
"thread's path was never restored.", self, s_imDepth);
|
||||
s_imHeld = 0;
|
||||
s_imOwner = 0;
|
||||
s_imDepth = 0;
|
||||
pthread_cond_signal(&s_imCv);
|
||||
}
|
||||
pthread_mutex_unlock(&s_imMx);
|
||||
}
|
||||
|
||||
OSIntMask osSetIntMask(OSIntMask mask)
|
||||
{
|
||||
if (mask == OS_IM_NONE) { /* enter critical section */
|
||||
|
||||
@@ -1075,11 +1075,21 @@ u16 sndGetSfxSlotFirstNaturalVolume(void)
|
||||
void sndApplyVolumeAllSfxSlot(u16 volume)
|
||||
{
|
||||
u8 i;
|
||||
#ifdef PORT
|
||||
/* D152: collapse the per-slot sndSetSfxSlotVolume() lock acquisitions
|
||||
* into one recursive hold for the whole update (the fade-out calls this
|
||||
* every frame). One real lock acquire/release per frame instead of
|
||||
* SFX_SLOT_COUNT * (active sounds). No behaviour change. */
|
||||
OSIntMask mask = osSetIntMask(OS_IM_NONE);
|
||||
#endif
|
||||
|
||||
for (i = 0; i < SFX_SLOT_COUNT; i++)
|
||||
{
|
||||
sndSetSfxSlotVolume(i, volume);
|
||||
}
|
||||
#ifdef PORT
|
||||
osSetIntMask(mask);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1110,6 +1120,26 @@ void sndSetSfxSlotVolume(u8 sfxIndex, u16 volume)
|
||||
|
||||
ALSndpEvent evt;
|
||||
ALSoundState *item;
|
||||
#ifdef PORT
|
||||
/* D152: this walks the live ALSoundState list and posts to the shared
|
||||
* ALEventQueue, exactly like its sibling sndDeactivateAllSfxByFlag() --
|
||||
* which DOES take OS_IM_NONE around its walk. The omission here is benign
|
||||
* on N64 (alEvtqPostEvent masks internally, and the audio job is a
|
||||
* cooperative RSP task) but not on PC: the audio manager (amMain) is a
|
||||
* real preemptible thread, so the unguarded walk both races a concurrent
|
||||
* list mutation AND takes/drops the interrupt-mask lock (D147/D152)
|
||||
* once per matching sound. During the mission-failed audio fade-out
|
||||
* (sndSetScalerApplyVolumeAllSfxSlot -> sndApplyVolumeAllSfxSlot -> here,
|
||||
* every frame, over every active sound) that is dozens of lock
|
||||
* acquire/release per frame on the game thread, each a fresh window to
|
||||
* collide with amMain and eat the 2 s steal-lock timeout -> the frame
|
||||
* never renders -> permanent black screen. Hold the mask once across the
|
||||
* whole walk: the nested alEvtqPostEvent() calls then hit the recursive
|
||||
* fast path (depth++) and open no new contention window, and the walk is
|
||||
* atomic w.r.t. amMain as the N64 "interrupts off" intent requires.
|
||||
* Concurrency-correctness only; no behaviour change. */
|
||||
OSIntMask mask = osSetIntMask(OS_IM_NONE);
|
||||
#endif
|
||||
|
||||
item = (ALSoundState *)D_800243E4.node.next;
|
||||
|
||||
@@ -1131,4 +1161,7 @@ void sndSetSfxSlotVolume(u8 sfxIndex, u16 volume)
|
||||
|
||||
item = (ALSoundState *)item->link.next;
|
||||
}
|
||||
#ifdef PORT
|
||||
osSetIntMask(mask);
|
||||
#endif
|
||||
}
|
||||
Reference in new issue
Block a user