fix(D77): in-level music silent — missing return in sub_GAME_7F0C0BF0 (D6-class UB)

Root cause: src/game/mp_music.c's sub_GAME_7F0C0BF0() is a one-line wrapper
around get_mTrack2Vol() with no return statement. On N64/IDO the value
happened to already sit in $v0; at the PC release build's -O2 the return
register is genuinely undefined (docs/porting-notes.md D6 class, same as
D187). Every in-level music trigger goes through set_missionstate()'s
MISSION_STATE_1/4 cases (the only path that starts a level's background
track), which feeds this garbage/zero value straight into
musicTrack1ApplySeqpVol()/musicTrack3ApplySeqpVol() right before
musicTrack1Play()/musicTrack3Play() — silencing the whole track for its
entire duration while note-on processing continues normally. Front-end/menu
music bypasses set_missionstate() entirely (front.c calls musicTrack1Play()
directly), which is why it was never affected.

Fix: return the tail call under #ifdef AVOID_UB, #else keeps the N64 body
verbatim. One line, no game-logic change.

Diagnostic: added a temporary, cheap (~1/s, gated on GE_AUDIOTRACE)
[AUDIOLVL] peak/RMS probe of the exact buffer audioSetNextBuffer() hands to
SDL (port/src/audio.c), interleaved into the same audiotrace.log as the
existing csplayer.c [MUSICNOTE] probe via the shared geTracePrintf lock.
Before the fix: 173 [MUSICNOTE] events over a 28s BUNKER1 capture, but every
single [AUDIOLVL] reading (29 samples spanning the full run) read
peak=0 rms=0.0. After the fix: same note count, every [AUDIOLVL] reading
sustained non-zero (peak ~2700-10300, rms ~800-2900). tools_pc/verify.sh
bunker1: PASS, no visual regression.

Docs: docs/dev/findings.md D77 updated from stale "nothing reaches PC
speakers" framing to the actual root cause + verification; new D6 instance
logged in docs/porting-notes.md.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
This commit is contained in:
James Dansereau committed 2026-09-10 23:34:40 -04:00
1 parent 7c437c0ed6
commit 199182d1d9
4 files changed
+148 -15

No files matched your search

+22 -4
View File
@@ -702,10 +702,28 @@ silently disappears.
Invisible under `-Og`; vanished the day release switched to `-O2`
(`34885535`). Fix: `return` the tail call under `#ifdef AVOID_UB`
(`#else` keeps the N64 body verbatim).
- The one currently flagged in-source is `grep -rn "missing a \"return\"" src/`
(just `gunfire.c` today). Others may exist without the banner — suspect this
class whenever an `-O2` build drops a small on-screen element that an `-Og`
build shows. One-line `#ifdef AVOID_UB` fix each.
- Instance: **D77** — `sub_GAME_7F0C0BF0()` (`src/game/mp_music.c`), a
one-line wrapper around `get_mTrack2Vol()` with no `return`. Every
in-level music trigger (`set_missionstate()`'s MISSION_STATE_1/4 cases —
i.e. the only path that starts a level's background track; the front-end
menu/intro music bypasses this and calls `musicTrack1Play()` directly,
which is why menu music worked while level music didn't) feeds this
garbage/zero return straight into `musicTrack1ApplySeqpVol()` /
`musicTrack3ApplySeqpVol()`. The result isn't a missing on-screen
primitive but a **silent audio channel that still runs**: the compact-seq
player keeps processing note-on events (visible in a `GE_AUDIOTRACE`
`[MUSICNOTE]` capture with normal-looking key/velocity data) but every
note's synthesized volume is scaled by the zeroed track volume, so nothing
reaches the mixed output. A per-note trace alone looks completely healthy
— the tell is a *track/channel-level* gain feed with a missing `return`
upstream. This class isn't limited to display-list cursors; audit any
`-O2`-only "quietly broken but not crashing" symptom for the same shape.
Fix: `return` under `#ifdef AVOID_UB` (`#else` keeps the N64 body).
- The one flagged in-source with an explicit comment is
`grep -rn "missing a \"return\"" src/` (`gunfire.c`). Others (like D77's)
exist without the banner — suspect this class whenever an `-O2` build
silently drops or mutes a small on-screen/audio element that an `-Og`
build shows/plays correctly. One-line `#ifdef AVOID_UB` fix each.
- Not caught headless by a symbol/link check or a default-config framediff
(the missing primitive is often small). Needs an `-O2` build + an eyeball
or a targeted crop diff.