docs(dev): D205 M-73 — user combat capture root-causes all three wrong-sound moments to guards stuck in sustained fire-sound re-triggering (idx 109 barrage, no bullets); LOS/seen_bond_time on converted geometry is the leading suspect

This commit is contained in:
James Dansereau committed 2026-09-07 15:17:03 -04:00
1 parent 087c60d05f
commit fb037b3c0c
1 file changed
+56 -25
+56 -25
View File
@@ -405,7 +405,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D203 | **Steam Deck (SteamOS, x86_64 Linux) v0.1.0 release bundle: Facility (`-level_34`) crashes "after loading in as James Bond" (user bug report).** Idle repro attempts on WSL Ubuntu with the actual `dist/goldeneye-pc-port-0.1.0-linux-x86_64.tar.gz` are **negative**: 120 s idle → frame 3300+ clean exit (the intro auto-advances, so "sitting as Bond" is covered), and a ~4-min stick-forward-only run → frame 6900+ also survived. So the trigger needs real gameplay input (fire/action) or is Deck-environment-specific. **Leading suspect: the D191 family** — both known v0.1.0 field crashes are SIGSEGVs in `modelGetNodeRwData` (`model.c:478`) over a truncated/NULL `root->Parent` node link, and both were triggered by *gameplay state* (first guard kill on Bunker ii; post-cutscene on Statue), never by idling; Facility opens into the Ourumov + soldier-squad scripted beat (D193a instance), which fits the trigger profile. Alternatives: Deck GL driver path (RADV/zink vs llvmpipe) or 40/50/60 Hz timing (D193 family). **Next:** (1) WSL run under gdb with a walk+fire `GE_INPUTSCRIPT` — generate the script into a file first (earlier attempts were blocked by a `bash -lc` quoting quirk that emptied the loop var, so no fire buttons ever went out); on SIGSEGV compare the fault PC against `modelGetNodeRwData`; (2) if not reproducible on WSL, get `ge007.crash.log` from the Deck (the Linux build writes it — proven by a stale one in the smoke tree) + ask what Bond was doing at crash time; optionally a gdb run from the SteamOS terminal. | OPEN — negative idle repros only; not yet reproduced with input. |
| D201 | **`src/libultra/audio/bnkf.c` bank/instrument/sound/wavetable relocation offsets were `s32`, truncating the real 64-bit base pointer.** `alSeqFileNew`/`alBnkfNew` compute `offset = (s32) base` from a heap pointer and thread it through `_bnkfPatchBank/Inst/Sound/WaveTable` as `s32 offset, table` parameters, used to rebase every embedded pointer field in a loaded bank/inst/sound/wavetable/book/loop record. On N64 (32-bit pointers) the truncation is a no-op; on PC, sign-extension of the truncated `s32` during the rebase arithmetic reconstructs the wrong 64-bit address whenever the base allocation's low 32 bits have the top bit set (~50% of allocations, allocator-dependent) — corrupting every relocated pointer in the bank file, a plausible contributor to bad model/audio pointers surfacing downstream (D191-family symptoms are a node-tree walk, not this, but the class is the same). **Fix (ABI-layer, no logic change):** widen `offset`/`table`/`woffset` and all four `_bnkfPatch*` parameters from `s32` to `uintptr_t` (`<stdint.h>`) — already this codebase's idiom for pointer-safe integers in `src/game/*.c`. No behavior change on the 32-bit N64 build. | FIXED (`src/libultra/audio/bnkf.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation |
| D202 | **Silenced PPK plays a "slap" instead of a gunshot, PLUS user reports broader real-time audio corruption (M-52 → M-59 → REOPENED M-60 → M-61/M-62 closed the data chain, recommended close → REOPENED M-63 on a fuller symptom report).** M-56–M-62: exhaustively verified the index/bank/pointer/decode/ROM-offset/source-data chain — soundIndex 46 is correctly requested and the data is ground truth (byte-match checksum corroboration, M-62). **M-63: user's actual complaint is broader — wrong sound always plays, correct one SOMETIMES plays too (both heard together), and sounds "pile up and spirally glitch out" over a session until only a stuck looping sound (e.g. Bunker's door) remains.** Got a live build+run repro this session (`-level_09`, 60s, `GE_MIXERTRACE`+`GE_AUDIOTRACE`+`GE_AUDIODUMP`, 58 scripted PPK fires). **Ruled out voice-pool exhaustion/leak** — only 16 distinct voice slots used, properly recycled (`sndDeactivate` count tracks `sndPlaySfx` count 1:1). **Found an unexplained anomaly**: `soundIndex=109` (AK47 bolt-action SFX) fires 94 times in 60s despite no AK47 in the repro, some retriggering the same voice slot at a suspiciously uniform ~32.6ms cadence (4-in-a-row) — far faster than plausible weapon fire; looks like something re-issuing `sndPlaySfx` every audio tick instead of once per game event. Not yet confirmed as the cause of the user's symptom, but the top concrete lead. Traces preserved: `scratchpad/d202-m63/`. (Side note, not a real bug: chased a `load_bg_file` crash this session that turned out to be an incomplete test-data setup on the investigator's part — see M-63 write-up.) **M-65 PARTIALLY resolved:** the "eventual silence" cascade is root-caused and mitigated (ownerless `SOUND_FLAG_LOOPED` SFX — sound 203, `METAL_SLIDE_CLOSE_SFX`, `decayTime=-1`, played by `doorPlayCloseSound0/1` with `NULL` owner — leaks 7 of 8 voices; a 4-line guard in the preemption scan reclaims only provably-ownerless loops, measured 171→629 acquisitions), and a second independent cause is fixed (`sndCreatePostEvent` was stubbed out per D138, removing ALL distance attenuation). The audible stuck door loop is still not reproduced — every candidate mechanism ruled out by measurement (see M-65); needs a user capture at the failing door with `GE_AUDIOTRACE=1 GE_AUDIODUMP=1`. | ROOT CAUSE ESTABLISHED (M-66); DISPOSITION C IMPLEMENTED + MEASURED (M-66b) — the audible stuck loop is sound 203 (`METAL_SLIDE_CLOSE_SFX`): ROM infinite ADPCM loop (`count=-1`) + `decayTime=-1`, played fire-and-forget by `doorPlayCloseSound0/1` with NULL owner, never stored in any slot, exempt from preemption (flag 0x12) and from decay-stop scheduling, priority 0x41 makes it unstealable — **faithful N64 behaviour / original quirk, not a port bug**. 327 ms period = -550-cent pitch (keyBase 54 + detune 50 − shift 6000 → ratio 0.7278; 5248 samples @ 22050×0.7278 = 327.007 ms). M-65's guard fires only under pool pressure (twice in the user capture) and cannot silence the idle loop. **Disposition C implemented + measured (M-66b):** PC-only `AL_SNDP_PORT_EXPIRE_EVT` fades provably-ownerless infinite-loop SFX out ~2.5 s after start (2 s delay + 0.5 s fade); per-second spectral check: pre-fix drone flat at max t≈39→361 s, post-fix transient bursts with full silence between; run ends allocated=0/8. **M-67: static analysis exhausted — full-bank data scan clean (all 261), DSP audit complete vs PD, A_LOOP branch dead code in GE, reference clip B00I00S2D.wav REFUTED as an in-game capture (best waveform CC 0.19 ≈ noise; M-61's +0.999 was envelope-only correlation); new `GE_VOICEDUMP` per-voice probe + timestamped WIRE/AUDIOTRACE for the decisive runtime A/B; self-test found the offline resample direction had been inverted all along (y[k]=x[ratio·k]) and that GE "music" is an SFX-sequence system with its own ~8-sample set sharing the 8-voice pool; parked anomaly: sound 232 requested at level start produces no audible audio. **M-68 (user captures ×3): all three complaint classes verified per-voice at runtime — doors CC 0.91–0.99, PPK 46 CC 0.90–0.97, armor 81 full 833 ms vs ROM-predicted 837 ms @ ratio 0.5612; the earlier "instant death" metric was a line-count artifact (µs-timestamped `[EVT]` probe shows every STOP lands on schedule). No port bug: the heard "wrong" sounds are the original design (overlapping SFX, short decays, quick-stop truncation).** **M-69 (full-corpus exact match, 320 scripted-run requests): 286/320 CC ≥ 0.85 — every allocated voice plays the exact ROM sample at the exact ROM pitch; the initial 44 "BAD" were tooling artifacts (voicedump stamps are block-END times; coarse-slide missed sharp onsets; refs not clipped at voice death). Remaining failures = dropped allocations (no `[VOICE+]` — suspected 8-voice pool exhaustion, unverified) + shared-wavetable wire misattribution. No corruption.** **M-70 (decisive clean re-capture, 209 requests, matcher v3): every voiced request Line truncated
| D205 | **PC plays wrong/extra SFX for gameplay events the user can A/B against N64: general gunfire has a slap/glass layer audible on PC but silent on N64; an explosion plays a soldier-scream sample; armor pickup plays a wrong sample; unarmed melee (slap) plays Klobb's shooting sound idx 106 instead of the slap effect (M-71/M-72, user by-ear + N64 A/B).** The playback chain is proven faithful end-to-end — M-68/M-70 exact match: every voiced request plays the exact ROM sample at the exact ROM pitch — and this session's offline harness (`scratchpad/banktest/`: real D37 converter + `diff_bank.py`) shows all 261 bank entries map identically N64→PC (base/len/type/keyMap/env, 0 mismatches). So the bug is upstream of the bank: **PC requests different or extra sound indices than N64 in the same scenario.** Ranked suspects: (a) data-driven sndID/explosion-type/item-sound fields in converted level/setup assets (pccg sidecars) differ from ROM — explosions play `sp44->sndID` from `g_ExplosionTypes[]` (`src/game/explosion.c:285`), explosion *type* comes from level data; explosion SFX are idx 169–183, a scream is outside that range; armor idx 81 verified exact in M-68, so a wrong armor sample means a different index is requested; (b) extra hit events — converted collision geometry makes bullet raycasts (D154/D135, both ABI-verified) or explosion proximity checks hit surfaces/NPCs the N64 misses → layered impact/ricochet/scream SFX; the slap+glass-over-gunshot signature is exactly the non-penetrating-OBJ-hit path (`HIT_BULLET_GLASS_SFX` idx 70 via `sub_GAME_7F064720`, `src/game/gunfire.c`). | OPEN — bank + playback exonerated (M-71); next: one controlled explosion + armor pickup on PC with `GE_AUDIOTRACE=1`, read the requested soundIndex list, compare against ROM ranges; N64-side reference via mupen64 or sidecar-vs-ROM byte diff |
| D205 | **PC plays wrong/extra SFX for gameplay events the user can A/B against N64: general gunfire has a slap/glass layer audible on PC but silent on N64; an explosion plays a soldier-scream sample; armor pickup plays a wrong sample; unarmed melee (slap) plays Klobb's shooting sound idx 106 instead of the slap effect (M-71/M-72, user by-ear + N64 A/B). M-73: user combat capture root-causes all three "wrong-sound" moments to ONE phenomenon — guards stuck in sustained ACT_ATTACK re-triggering weapon fire sound idx 109 (AK47/Spectre) 256×/30 s at fixed per-guard periods with NO corresponding bullets/impacts; the "Klobb" was never 106 (never requested); explosion 182 + armor 81 each requested exactly once and correct, but masked by the 109 barrage. Sustained-attack gate = `seen_bond_time` refreshed via `stanTestLineUnobstructed` LOS on converted geometry → leading suspect is a LOS/collision query returning clear when N64 blocks.** The playback chain is proven faithful end-to-end — M-68/M-70 exact match: every voiced request plays the exact ROM sample at the exact ROM pitch — and this session's offline harness (`scratchpad/banktest/`: real D37 converter + `diff_bank.py`) shows all 261 bank entries map identically N64→PC (base/len/type/keyMap/env, 0 mismatches). So the bug is upstream of the bank: **PC requests different or extra sound indices than N64 in the same scenario.** Ranked suspects: (a) data-driven sndID/explosion-type/item-sound fields in converted level/setup assets (pccg sidecars) differ from ROM — explosions play `sp44->sndID` from `g_ExplosionTypes[]` (`src/game/explosion.c:285`), explosion *type* comes from level data; explosion SFX are idx 169–183, a scream is outside that range; armor idx 81 verified exact in M-68, so a wrong armor sample means a different index is requested; (b) extra hit events — converted collision geometry makes bullet raycasts (D154/D135, both ABI-verified) or explosion proximity checks hit surfaces/NPCs the N64 misses → layered impact/ricochet/scream SFX; the slap+glass-over-gunshot signature is exactly the non-penetrating-OBJ-hit path (`HIT_BULLET_GLASS_SFX` idx 70 via `sub_GAME_7F064720`, `src/game/gunfire.c`). | OPEN — bank + playback exonerated (M-71); M-73 root-caused the user's three "wrong-sound" moments to guards stuck in sustained fire-sound re-triggering (idx 109 barrage, no bullets) — next: A/B whether N64 guards sustain fire in the same combat + verify `stanTestLineUnobstructed` LOS results on converted geometry vs ROM for guard→Bond segments |
| D152+ | 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 |
| RC3 · D167 | **Non-power-of-two texture wrap period (`docs/dev/TEXTURE-GLITCH-ANALYSIS.md` §6 RC3 — "textures repeat oddly", residual Depot-ceiling noise after D161).** The N64 RDP masks the texel coordinate of a wrapping render tile at `1<<mask`, and GE sets `mask = texDimensionToMask(dim) = ceil(log2(dim))` (`src/game/tex.c:361`), so a non-PoT tile (Depot's 65×65 / 96×48 / 56×56 room surfaces) repeats at the **next power of two**, not at its image size the way GL `GL_REPEAT` does → the pattern is squashed/stretched and the seam lands in the wrong place. fast3d never stored `masks`/`maskt` at all (`gfx_dp_set_tile` dropped them) and wrapped purely at the uploaded image dimension. **Fix (`port/fast3d/gfx_pc.cpp`, behind the existing `Video.WrapFix` knob, default OFF):** store `masks`/`maskt` on the tile; in the hoisted per-texunit pre-wrap block in `gfx_sp_tri1` (D74 block — already lifted out of the vertex loop, indexed by texunit `t` not vertex `i`), when the tile is WRAP (no CLAMP/MIRROR bit) and `1<<mask != tex_width`, fold the UV at the N64 period `1<<mask` and clamp the `[dim, 1<<mask)` overflow band (which is a TMEM smear on console, no real texels) to the last texel so it reads as an edge streak instead of a bogus early image restart. `GE_WRAPFIX=0/1` env override added (env wins over the ini, matching `Debug.FrameDump`). **Per-level captures (`-level_09`/`-30`/`-34`/`-20`, WrapFix OFF vs ON, `GE_PCDUMP` 6-frame windows):** no crashes, 6/6 frames each; on settled/comparable frames Silo is ~pixel-identical (phash 0–11), Facility 180–260 pixel-identical, Depot shows small localized texel changes on ceiling/wall cells (dmean 5–9, no structural break); the large per-run deltas are all the D117 intro-camera-pan nondeterminism, not the fix. Default kept **OFF** — no regression, but a headless structural diff can't confirm the Depot ceiling actually looks *better*; needs a human eyeball with `Video.WrapFix=1`. Default-off is byte-identical to golden (all new behaviour is inside `if (g_wrap_fix)`). D74's dead in-vertex-loop wrap block was already reworked/hoisted at M-30; this only adds the mask-period trigger. Confidence: **medium** (mechanism correct; overflow-band handling is an approximation, not exact TMEM-smear emulation; visual win unconfirmed). porting-notes.md §D. | KNOB ADDED, default OFF (`port/fast3d/gfx_pc.cpp`). Needs user visual check on Depot. |
| D168 | **`GE_PCDUMP` / F12 PPM captures were vertically flipped — the entire source of the bogus D114/D116 "HUD/text X-mirror" (M-33, developer-confirmed).** `gfx_opengl_dump_bound_fbo` (`port/fast3d/gfx_opengl.cpp`) wrote `glReadPixels` output straight to a P6 PPM. GL framebuffer origin is bottom-left; PPM P6 is top-row-first — so every capture (and the `tools_pc/golden/` set, and every screenshot pasted into the finding log since M-6) was upside-down. On real hardware / an actual screen the game renders correctly (developer confirmed). The successive D114→D116 "shared fast3d mirror" investigations — each of which found *every probed stage clean yet the output "mirrored"* — were reading an inverted capture and pattern-matching upside-down asymmetric content (text, guards, the Nintendo logo) as "mirrored". Fix: emit PPM rows bottom-to-top. `tools_pc/golden/*.png` flipped in place to match (see `tools_pc/golden/README.md`); regenerate from a real run when convenient. **Not runtime-verified this session** (no ROM / toolchain in the migrated tree — see the migration note) — needs a fresh capture to confirm text reads normally. Confidence: **high** (mechanism is unambiguous; developer has hardware confirmation the screen is correct). porting-notes.md §D2. | FIXED (`port/fast3d/gfx_opengl.cpp`); D114 / D116 reclassified as capture-orientation artifacts (below). |
@@ -7517,6 +7517,44 @@ mechanism (b) — extra/different EVENTS during real gameplay (a guard firing
as you punch → 106 layered on the slap; an NPC scream at the blast; a second
surface hit adding glass/slap to a shot).**
**M-73 (user combat capture — root cause of all three "wrong-sound" moments,
this session):** user ran a full combat session with `GE_AUDIOTRACE=1`
(`build-pc/audiotrace.log`, 288 unique requests). Dominant finding: **idx 109
(`GUN_B4_BOLTACTION_SFX` — the AK47/Spectre fire sound, `Sound = 0x6D` in
gunWeaponStats.inc.c) is requested 256× over 29.7 s at ~100 ms aggregate
cadence**, from ~11 distinct NPC state slots (`newState` pointers), each
re-triggering on its OWN fixed period (median gaps 200/434/499/698/934/
1299 ms — multiples of ~200 ms). Decisive: only ~32 NON-109 requests exist in
the whole window (~0.6/s) — if the guards were actually firing 10×/s there
would be ~10 impact SFX/s (flesh 69, ricochets). **The guards are re-triggering
weapon fire sounds WITHOUT firing bullets.** Code path:
`chrlvFireWeaponRelated` (`src/game/chraction.c:6530`) — for auto weapons a
non-bullet tick sets `sp278 = 1`, and `phi_a2 = sp27C || sp278` is passed to
`sub_GAME_7F02BFE4(self, hand, phi_a2)` (:6902), which plays the weapon sound
whenever `SoundTriggerRate > 0 && field_178 < g_GlobalTimer` — so **sustained
ACT_ATTACK = sustained weapon roar regardless of bullets**. Sustained attack
requires `self->seen_bond_time >= g_GlobalTimer - CHRLV_SEEN_RECENT_CHECK`
(100, chraction.h:13; check at chraction.c:6593); `seen_bond_time` is
refreshed by `setSeenBondTimeToNow`, gated on
`stanTestLineUnobstructed(... CDTYPE_OBJS|DOORS|CHRS|PATHBLOCKER|AIOPAQUE ...)`
LOS queries (chraction.c:3917, :3972, and `chrCanSeeBond` :3945). **If the
converted collision geometry makes those guard→Bond LOS raycasts return
clear where the N64 blocks them, guards never lose their target → 30 s of
sustained fire sound.** Per-symptom accounting: (1) "Klobb sound on melee" —
idx 106 is NEVER requested in the capture; the user heard the 109 barrage
(gun-bolt SFX) during his melee combat. (2) "explosion → scream" — explosion
182 was requested exactly once (correct, in 169–183); GET_HIT_MALE screams
(137/138/139) fired ±1 s around it (guards hit by the blast — possibly
legitimate, needs N64 A/B), all under the 109 barrage. (3) "armor wrong" —
idx 81 requested exactly once at t≈1317.4 s (correct!), bracketed by 109 ticks
at 1317.35/1317.45 — the barrage masks it. Continuity note: this is the same
phenomenon as D202 M-63's "unexplained anomaly" (idx 109 ×94 in 60 s during a
scripted PPK repro with no AK47 present) — guards at level_09 doing the same
thing. **Remaining question: WHY do PC guards sustain ACT_ATTACK for ~30 s** —
(i) `stanTestLineUnobstructed` on converted geometry returns clear where ROM
blocks (D154/D135 family), or (ii) AI attack enter/exit differs. Open A/B:
do N64 guards in the same combat also sustain fire for 30 s?
**Therefore: PC requests different or extra sound indices than N64 in the
same scenario.** Two mechanisms, ranked:
(a) **Converted level/setup data** — sndID / explosion-type / item-sound
@@ -7539,30 +7577,23 @@ same scenario.** Two mechanisms, ranked:
GBU/TAJ/RICO_4 (19–26, 31–33) = wall only.
**Next (cheapest first):**
0. **User capture at the exact failing moment** (M-72 made this the
cheapest split): run the PC with `GE_AUDIOTRACE=1` and reproduce one
melee punch / one shot-with-layering / one explosion; the requested
index list in the event window decides per symptom — e.g. melee: 47–49/
105 *plus* 106 → extra NPC fire event (mechanism b); 106 *only* → new
mechanism, escalate. Launcher must have `C:\msys64\mingw64\bin` on PATH.
1. One controlled explosion + one armor pickup on PC with
`GE_AUDIOTRACE=1` (`-level_09` boots straight into Bunker). Read the
requested soundIndex list in each event window: any index outside
169–183 at the blast (→ scream index = extra event, mechanism b) or a
169–183 index whose ROM sample is *not* an explosion (→ type/sndID
wrong, mechanism a). Armor: requested index 81 or something else?
2. N64-side request list for the same scenario: mupen64 at repo root
(untracked emulator checkout, now gitignored) with a `sndPlaySfx`
breakpoint/log — or infer from ROM data by decoding one level's pccg
sidecar vs the ROM segment and diffing the sound-related fields.
3. If mechanism (a): find the converter bug in the level/setup asset
pipeline (`scripts/` + `port/src/romdata.c` fixups) — byte-compare one
level's sidecar against the ROM original.
4. If mechanism (b): dump converted collision near Bond's line of fire for
one shot, decode in Python against the ROM original; identify the
converter bug class.
5. After root-cause + fix: user re-test of explosion + armor pickup +
gunshot layering vs N64.
0. ~~User capture~~ — DONE (M-73): all three moments explained by the guard
109-barrage; no per-symptom data bug found (182 and 81 both requested
correctly).
1. **N64 A/B of the sustained-fire itself**: in the same combat, do N64
guards also fire for ~30 s straight? If not → PC guard AI/LOS differs;
if yes → the difference is elsewhere (e.g. sound-only re-trigger vs
actual bullets) and this may be a faithful-AI / different-perception case.
2. **Probe the PC side**: temporary `GE_AUDIOTRACE`-style probe on
`setSeenBondTimeToNow` (chraction.c:3874) — log guard id + position when
`seen_bond_time` refreshes during a sustained-fire episode; if it fires
every frame while Bond is behind cover → the LOS query is wrong.
3. **Verify `stanTestLineUnobstructed` on converted geometry** for one
guard→Bond segment from step 2: replay the exact ray against the ROM
original collision (decode in Python) vs the pccg sidecar; identify the
converter/parser bug class (D154/D135 family).
4. After root-cause + fix: user re-test of melee / explosion / armor pickup
vs N64 — expect all three "wrong sounds" to disappear with the barrage.
**Probes added this session (live under `GE_AUDIOTRACE`, catalogued in
GE-ENV-PROBES.md):** `[VOL] t=… state=… rawVol=… playing=…` (`src/snd.c: