From 199182d1d99142ef4647006a329dfdbf3d2c1eac Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:32:12 -0400 Subject: [PATCH] =?UTF-8?q?fix(D77):=20in-level=20music=20silent=20?= =?UTF-8?q?=E2=80=94=20missing=20return=20in=20sub=5FGAME=5F7F0C0BF0=20(D6?= =?UTF-8?q?-class=20UB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/dev/findings.md | 93 ++++++++++++++++++++++++++++++++++++++----- docs/porting-notes.md | 26 ++++++++++-- port/src/audio.c | 30 ++++++++++++++ src/game/mp_music.c | 14 +++++++ 4 files changed, 148 insertions(+), 15 deletions(-) diff --git a/docs/dev/findings.md b/docs/dev/findings.md index c1de7532..9002a650 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -380,7 +380,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D59–D68 | intro render: blood-RLE clobber, DMA validate, OSMesgQueue, HEADS/BODIES sentinels, romCopy width, image_entry, Globalimagetable BE→LE | resolved | | D69 · D78–D84 | stage load (`load_bg_file`): bg/stan offline sidecar (`d69_emit.py`) + StandTile/bg_room_data ABI | resolved | | D70–D74 | intro-logo pixels: C-array bswap, UV path, sinf/cosf `DVAL()`, texture-import truncation | resolved | -| D75 · D77 | front-end 3D model transforms · audio | OPEN (parked). **M-32 triage (see §F "D75 ADDENDUM"): (a) D73-scope-gap RULED OUT (gu tree fully endian-clean). Splits in two: Bug 1 = logo/photo transform = the parked D114/D116 fast3d viewport mirror (not game code). Bug 2 = absent animated models = category (b). M-32b runtime probe (`GE_D75=1`): `render_pos`/`dynAllocate`-arena hypothesis RULED OUT (render_pos valid + fresh each frame), model instances valid (nMtx 21/1), zero fast3d DL warnings — failure is downstream in `drawjointlist`/`dotube` vtx/node-DL resolution or an off-screen `basemtx`. Needs a drawjointlist-level probe. M-33 (upright captures, D168 fixed): "Bug 1 = D114/D116 mirror" retracted; **gun-barrel Bond RENDERS fine** (walk + fire, upright) — the "absent" reports were the flipped capture; **Nintendo logo genuinely broken** (renders as 2 white blobs, shifted left — a real `logoinst` transform/geometry bug, not a flip); cast roll not re-captured. See §F "D75 Bug 2 — M-33 UPDATE".** | +| D75 | front-end 3D model transforms (D77 audio split out below — resolved, unrelated cause) | OPEN (parked). **M-32 triage (see §F "D75 ADDENDUM"): (a) D73-scope-gap RULED OUT (gu tree fully endian-clean). Splits in two: Bug 1 = logo/photo transform = the parked D114/D116 fast3d viewport mirror (not game code). Bug 2 = absent animated models = category (b). M-32b runtime probe (`GE_D75=1`): `render_pos`/`dynAllocate`-arena hypothesis RULED OUT (render_pos valid + fresh each frame), model instances valid (nMtx 21/1), zero fast3d DL warnings — failure is downstream in `drawjointlist`/`dotube` vtx/node-DL resolution or an off-screen `basemtx`. Needs a drawjointlist-level probe. M-33 (upright captures, D168 fixed): "Bug 1 = D114/D116 mirror" retracted; **gun-barrel Bond RENDERS fine** (walk + fire, upright) — the "absent" reports were the flipped capture; **Nintendo logo genuinely broken** (renders as 2 white blobs, shifted left — a real `logoinst` transform/geometry bug, not a flip); cast roll not re-captured. See §F "D75 Bug 2 — M-33 UPDATE".** | | D76 · D164 | disclaimer/legal screen only draws line 1 — **root-caused (M-31)**: `constructor_menu00_legalscreen` text loop bounds `legal_text_end` on `&legalscreen_MRD`, a linker-adjacency assumption that mingw breaks (`legalscreen_MRD` links 0x60 *before* `legalpage_text_array`) → `do{}while(ptr= 1000000ull) { + const s16 *s = buf; + u32 n = len / 2u; + s32 peak = 0; + s64 sumsq = 0; + for (u32 i = 0; i < n; i++) { + s32 v = s[i]; + if (v < 0) v = -v; + if (v > peak) peak = v; + sumsq += (s64)s[i] * (s64)s[i]; + } + double rms = n ? __builtin_sqrt((double)sumsq / (double)n) : 0.0; + geTracePrintf("audiotrace.log", "[AUDIOLVL] peak=%d rms=%.1f n=%u\n", + (int)peak, rms, (unsigned)n); + lastReportUs = now; + } + } if (dev && buf && len) { if (audioGetSamplesBuffered() < queueLimit) { SDL_QueueAudio(dev, buf, len); diff --git a/src/game/mp_music.c b/src/game/mp_music.c index 9c482f2e..b43ad6a9 100644 --- a/src/game/mp_music.c +++ b/src/game/mp_music.c @@ -37,9 +37,23 @@ s32 mission_state = MISSION_STATE_0; +/** + * D6-class bug (docs/porting-notes.md): missing "return" on a non-void + * function whose body is a single tail call. The N64/IDO build happened to + * leave get_mTrack2Vol()'s result in $v0, so the ASM ground truth "works" by + * accident; GCC at -O2 (the PC release build) does not guarantee that, and + * every caller of this function (set_missionstate()'s MISSION_STATE_1/4 + * transitions -- i.e. every in-level music-track-start trigger) fed + * musicTrack1ApplySeqpVol()/musicTrack3ApplySeqpVol() a garbage/zero volume, + * silencing level music while leaving note-on processing intact (D77). + */ u16 sub_GAME_7F0C0BF0(void) { +#ifdef AVOID_UB + return get_mTrack2Vol(); +#else get_mTrack2Vol(); +#endif } u16 sub_GAME_7F0C0C10(void)