docs(findings): migrate fat §F rows D201-D216 to tail sections (ctx-opt P1, batch 5/8)

10 rows: topic/status cells moved verbatim to new '## Dxx' sections at file
tail; table rows now carry a one-line summary + pointer. CSV regenerated, label
set unchanged (171). Nothing deleted.
This commit is contained in:
James Dansereau committed 2026-09-15 21:23:20 -04:00
1 parent 4ea303e25e
commit 5446cc295b
2 files changed
+71 -11

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ D206,FIXED,PC played every SFX one bank slot too high —,"FIXED (M-76, src/snd.
D207,PROPOSED,Alarm klaxon starves combat SFX and (per user) doesn't recover after it stops —,MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) —
D208,FIXED,Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) —,FIXED + user playtest-verified (M-78d) —
D209,FIXED,Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands —,FIXED + user-playtest-verified (M-81).
D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else)."
D210,FIXED,chrToPatrol() leaves act_patrol.lastvisible60 uninitialised on PC —,"FIXED (chraction.c:3872, #ifdef PORT → self->act_patrol.lastvisible60 = -1;, N64 line under #else).…"
D205,RESOLVED,PC plays wrong/extra SFX for gameplay events the user can A/B against N64:,CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear vs N64.
D152+,FIXED,D152 addendum (M-31):,"FIXED (src/snd.c, port/src/libultra.c, #ifdef PORT) —"
D168,FIXED,GE_PCDUMP / F12 PPM captures were vertically flipped —,FIXED (port/fast3d/gfx_opengl.cpp); D114 / D116 reclassified as capture-orientation artifacts (below).
1 # GENERATED by tools_pc/gen_findings_index.py -- do not hand-edit.
49 D207
50 D208
51 D209
52 D210
53 D205
54 D152+
55 D168
+70 -10
View File
@@ -415,14 +415,14 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D244 | **Self-hosted CI runner (`corpyvt6agent01`) link failure: `undefined reference to 'objectiveGetStatus_WEAK'` (2026-09-12, first-ever build attempt on the reinstalled box, run `34707798707`).** `src/game/objective_status.c:162` uses `#pragma weak objectiveGetStatus_WEAK = get_status_of_objective` (a decomp ground-truth alias, referenced 2-arg from `chrai.c:2538` — comment calls it "horrible hack to get ai matching"). The box's `g++`/`ld` reported as `x86_64-w64-mingw32/16.1.0`; a minimal local repro of the identical `#pragma weak` cross-TU function alias **links cleanly** on this session's own machine (`mingw-w64-x86_64-gcc 16.2.0-3`, `binutils 2.47-3`). Not a game-logic bug (the alias is unmodified decomp code, ground truth) and not a CMakeLists/link-flag issue (no `--gc-sections`/LTO in play) — the leading, unverified explanation is the self-hosted box's MSYS2 packages predate a PE/COFF weak-external fix or behavior change versus current MSYS2, consistent with rollout step 2's earlier finding that the box's mingw64 package set was stale/incomplete at initial setup. | **OPEN, not yet fixed.** Next: on the box, `pacman -Syu` (or at minimum `pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-binutils`) to bring the toolchain current, then re-dispatch `selfhosted.yml`. If updating doesn't clear it, the fallback is a narrow `#ifdef PORT` replacement of the alias with an explicit thin wrapper function in `objective_status.c` (ABI-preserving, no logic change) — but try the toolchain update first since the local repro suggests this is a version/environment gap, not a fundamental PE/COFF limitation. |
| D228 | **AK47 (and other) weapon models still show WHITE missing-texture patches after D217's texpool-alignment fix — a separate failure mode from the green wrong-palette defect (M-97, user playtest of PR #47, `-level_34` FACILITY).** Split out of D217 defect ①, exactly as the M-90 write-up anticipated ("do not close D217 defect ① on this merge — reopen/retitle as the AK47 + NPC-world-model white-texture residual"). **Playtest result that motivates the split:** with PR #47's `texInitPool` 8-byte arena alignment in, the *green* symptom is GONE — PP7/PPK grip and hammer render correct, Bond's watch face correct, no green cast on hands or the 3rd-person world model. But **NPC-held AK47s still render with white patches**, i.e. tiles sampling a missing/undecoded texture rather than a wrong-but-present palette. White ≠ wrong colour: a wrong palette binds a real TLUT with wrong contents (D217's mechanism, now fixed), whereas white is the signature of a texture that never got decoded/uploaded at all, or a tile descriptor pointing at an empty/cleared texpool slot. The M-87 matrix described these as distinct from the start ("AK47 viewmodel has white (missing-texture) patches on the magazine + barrel" vs. the green/blue colour corruption) — this playtest confirms they have distinct root causes, since fixing one left the other untouched. | **FIXED (`port/fast3d/gfx_pc.cpp`, `palette_to_rgba32`) — a real byte-order bug, not a texpool/decode-never-ran issue as originally guessed.** `GE_TEXDUMP` on Facility (`-level_34`) found ~14% of LOD-textured uploads (fmt=CI, `palette_fmt=G_TT_IA16`) rendering solid near-white; a `GE_D228` correlation probe ruled out the texpool/RC2-clip hypothesis (the decoded size exactly matched the tile's base image every time — the data reaches `import_texture` fine, sized correctly). The actual bug: `palette_to_rgba32`'s `G_TT_IA16` branch read `intensity = palentry & 0xff` (low byte) and `alpha = palentry >> 8` (high byte) — backwards relative to the already-correct direct-IA16 decode two functions above it in the same file (`import_texture_ia16`: `intensity = addr[2*i]` (first/high byte after `gfx_dp_load_tlut`'s `PD_BE16` swap), `alpha = addr[2*i+1]`). A dim, fully-opaque palette entry (e.g. intensity=0x58, alpha=0xff) decoded backwards as bright and mostly-transparent (intensity=0xff, alpha=0x58) — exactly the white-patch symptom, on any CI-format material whose TLUT happens to be IA16 rather than RGBA16 (weapon metal/grip materials commonly are, since IA is a natural fit for grayscale metal with a separate opacity channel). Fix: swap the two lines to match the working convention. **User-verified live:** AK47 and other metallic guns render correctly (no more white patches). Verified crash-free with a `-level_34` headless run before the live check; a full 21-level sweep is still owed (not run this session — the user was using their own game instance concurrently, `verify.sh`/`taskkill` would have killed it). D217 · D161 lineage — same general "CI/LUT format-assumption" bug class, distinct specific bug. |
| D204 | **PC audio ran ~2 % below real time permanently: GE's AI feedback loop (`src/audi.c:531`) has a ~3 ms setpoint that PC scheduling jitter clears, so the SDL queue starves and the device pads playback with silence (FIXED, measured).** `amMain` wakes at 30 Hz (`sched.c:334` forwards every 2nd retrace to the audio client) and asks for `g_MinFrameSize`=720 samples per block = 21600/s against a 22050 Hz device — structurally 2 % short, relying on 784-sample top-up blocks that `audi.c` only requests once the reported AI length falls under ~69 frames (3 ms). Fine on N64 (double-buffered AI, exact VI interrupt); on PC the queue empties before the loop reacts, and the padded silence is unrecoverable time. **Fix (F5, port-only):** `audioGetAiLengthBytes()` subtracts a 1024-frame (~46 ms) cushion before reporting, so the loop tops up while slack remains; `audi.c`'s control law untouched (rule #2 clean). Measured A/B in one binary via `GE_D204_OLD`: **rt 0.980 → 1.000, q min 0 → 368, drop 0.** Also: F1 correct AI_LEN_REG single-buffer semantics (robustness, measures as a no-op), F3 `Audio.QueueLimit` 8192→2880 + non-silent drops (NB: an existing `ge007.ini` pins it), F4 oversize invariant guard, and `GE_D204=1` cheap audio-health monitor (`rt`/`q`/`drop`/`max`, safe to leave on for a full playtest). **Falsified in-session, do not re-open:** the u32 wrap at `audi.c:531` is real (high-water 2064 frames > the 789 threshold) but harmless — `frameSamples` is `s16` (`audi.c:145`), so it lands negative and audi.c's own lower clamp catches it; max block stays 3136 B vs the 3156 B allocation, no heap overrun. **Corrects two M-63 claims:** `soundIndex=109` is guards' return fire (count was double-logged; the "32.6 ms cadence" is just the 2880-byte block quantum every `dumppos` is rounded to) — closed negative; and M-63's "13 % of real time" was `GE_MIXERTRACE`'s own 25 MB unbuffered log starving the audio thread (same repro without it: 58.0 s / 60 s). Does NOT explain D202. | FIXED (`port/src/audio.c`, `port/src/libultra.c`, `port/include/audio.h`) — measured; a by-ear pass on a real playthrough still owed |
| 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; **RESOLVED (D253):** reproduced on WSL Ubuntu by walking Bond into the crouch-forcing linked tile near the Facility opening; gdb capture showed SIGSEGV in `stanCheckLinkedSpecialTile` (`stan.c:2441`) storing to a stale `%r9` (0x3) while the caller's `record` local was valid — an x86-64 SysV int/float argument-class mismatch, not the D191 family. Fixed in D253; verified on WSL (same route no longer crashes). | CLOSED — root cause + fix in D253. |
| 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 |
| 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).** — full `## D203` entry at file tail | CLOSED — root cause + fix in D253. |
| D201 | **`src/libultra/audio/bnkf.c` bank/instrument/sound/wavetable relocation offsets were `s32`, truncating the real 64-bit base pointer.** — full `## D201` entry at file tail | 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
| D206 | **PC played every SFX one bank slot too high — a pointer-width layout shift in `struct ALInstrumentAlt_s`, not a game-logic bug (M-76 FIXED).** Root cause of D205 (3) armour + (4) melee→Klobb and of D202's silenced-PPK "slap". `sndPlaySfx` resolves a sound via `soundBank->instArray[0]->soundArray[soundIndex]` (`snd.c:1001`), casting the on-disk standard `ALInstrument` (12-byte u8 header, `s16 bendRange`@12, `s16 soundCount`@14, `ALSound* soundArray[]`@16) through GE's `struct ALInstrumentAlt_s { s32 unk0, unk4, unk8; ALSound *soundArray[1]; }` (`src/snd.h:165`). On N64 (4-byte pointers) that struct's `soundArray` sits at **offset 12** — it deliberately aliases the on-disk `bendRange`/`soundCount` words, so `soundArray[N]` == on-disk table entry **[N-1]**, i.e. GE's `SFX_ID` values are **1-based** into the sound table (`SFX_ID 0 = NOTHING_SFX`, short-circuited at `snd.c:992`, never dereferenced). On PC the 8-byte pointer + 8-byte alignment pushes `soundArray` to **offset 16**, and the converted bank (`port/src/romdata.c` `afFixupInst`, which faithfully reproduces the standard layout) is packed to match — so `soundArray[N]` landed on entry **[N]**, one slot high, on every SFX. Verified by static struct/converter arithmetic **and** at runtime: a BUNKER1 `GE_AUDIOTRACE` capture post-fix shows all ~90 distinct requested indices resolve to `rom_sfx_decode.py` slot **N-1** (0/90 at N). e.g. `ARMOUR_COLLECT_SFX`=81 → slot 80 (= rip `S50`, the user's "correct" armour); `skorpion_stats.Sound`=0x6A=106 → slot 105 (= rip `S69`, "the actual Klobb sound"); `wppksil_stats.Sound`=0x2E=46 → slot 45 (= rip `S2D`, M-61's rejected "+0.999" clip). The earlier "262 enum vs 261 bank ⇒ needs a -1" arithmetic was right about the *symptom*; the *mechanism* is the alias, and `BIG_CLANK_SFX`=261 → slot 260 is in-bounds (never an OOB). Upstream `n64decomp/007` `snd.c` byte-matches with no `-1` and no `NON_MATCHING` guard — correct, because the N64 struct offset supplies the -1. **NOT D205 (2)** explosion→scream (169-183 → slots 168-182, all still explosions) — stays with D205. | **FIXED (M-76, `src/snd.c:1001`, `#ifdef PORT` → `soundArray[soundIndex - 1]`, N64 line verbatim under `#else`).** ABI/layout-only, D3x class (pointer-width struct-layout reconciliation); no game-logic change. Build clean; BUNKER1 60s crash-free; `-level_09` golden framediff 3/3. **User by-ear A/B vs N64 = PASS (M-77c)** — armour / melee / silenced PP7 all correct now → D202 closed, D205 down to symptom (2). Committed `378386d1`. Scratchpad `rom_sfx_decode.py` / `exact_match.py` / `diff_bank.py` still walk slots 0-based — correct for "what is at bank slot i", but a check against a `SFX_ID` must compare to slot `id-1`. |
| D207 | **Alarm klaxon starves combat SFX and (per user) doesn't recover after it stops — surfaced by D206 (M-77, ROOT-CAUSED, fix designed not applied).** Before D206, `ALARM3_SFX` (id 163) resolved to the wrong bank slot 163 — a **one-shot** sample: played once, freed its voice. After D206 it correctly resolves to slot 162 = the **real infinite-loop klaxon** (`decayTime=-1` -> `SOUND_FLAG_LOOPED`; ADPCM `loop.count=-1`). Played by `handle_alarm_gas_timer_calldamage()` (`src/game/propobj.c:14457`) with owner `&ptr_alarm_sfx` while `alarmIsActive()`. It now **permanently holds 1 of only 8 SFX voices** (`MUSIC_SFX_SEQ_MAYBE_MAX_SOUNDS=8`, `src/music.c`), at priority `0x41` (looped sounds get `decayTimeFlag + 0x40`, `sndSetupSound` `snd.c:820`) — and is **exempt from the port's preemption scan** (`snd.c` ~369: `!(unk3e & 0x12)` refuses to steal any LOOPED/RETRIGGER voice; the M-65 carve-out only covers *ownerless* loops, and the alarm is owned). **M-77b:** the voice-starvation hypothesis did NOT hold up — a BUNKER1 `GE_FORCEALARM` + scripted-fire soak showed **0 true drops** (`sndDisposeSound` no-voice sites instrumented), the designed last-resort preemption pass **never fired**, and `g_sndAllocatedVoicesCount` recovered to 0 after every alarm-off (no leak). The earlier "~20% drop" was a log-latency artifact. The alarm klaxon (`ALARM3` slot 162) is a *continuous, gapless* infinite loop — a real GE alarm pulses — so the leading remaining explanation is **perceptual** (the drone masking gunfire, and/or slot-162 mixed too loud vs N64), or genuine 8/8 starvation only on a heavy level not yet tested. Needs a user repro. | MONITORING (M-77c: user reports the alarm is acceptable in play after D206; no fix shipped) — **the voice-starvation hypothesis did NOT hold up (M-77b).** The designed last-resort LOOPED-preemption pass was built, and instrumented drop counters (`[D207-DROP]` at both `sndDisposeSound` no-voice sites) + a `[D207-YIELD]` trace were added. Result on BUNKER1 (`GE_FORCEALARM` + scripted sustained fire, ~85 s): **zero true drops**, the last-resort pass **never fired** (pass-1 always found a non-looped victim), and `g_sndAllocatedVoicesCount` sat at 1 during quiet alarm and recovered to **0** after every alarm-off — no leak, no permanent starvation. The earlier "~20% dropped" figure was a measurement artifact (log-line latency, not real drops; every requested sound eventually got a voice via the `unk38` retries). So the fix was **reverted** (kept: nothing in `snd.c`; D206 `-1` only). BUNKER1 combat is too light to peg the 8-voice pool. **Next session needs a real repro from the user:** which level + weapon, and a `GE_AUDIOTRACE=1` capture at the failing alarm — is it (a) genuine 8/8 starvation on a heavy level (Facility/Silo/Statue firefight), (b) a leak that only triggers on a specific alarm-stop path, or (c) perceptual — the continuous klaxon drone masking gunfire / the alarm mixed too loud (check `ALARM3` slot 162 volume vs N64). Temp `GE_FORCEALARM` probe kept in `src/game/propobj.c` for that work. **M-78: re-added just the `[D207-DROP]` counter (NOT the reverted last-resort pass) at both `sndDisposeSound` no-voice sites in `snd.c` (gated on `GE_AUDIOTRACE`), logging `state`/`prio`/`flags`/`count=N/max`, so the user's real-level capture is decisive between (a)/(b)/(c). Static re-check of `ALARM3` slot 162 this session: `sampleVolume=80` (vs gunfire 90-110, punches 100-110 — the klaxon is quieter per-sample, not louder, weakening the "mixed too loud" half of (c)); `decayTime=-1` + `keyMax=8` (`&0xf0=0` → NOT `SOUND_FLAG_RETRIGGER`) → it is a continuous looped sound by design on N64 too, the pulse is baked into the 225→7512-sample ADPCM loop body. So (c) narrows to "the continuous drone masks gunfire" or a loop-point playback defect; (a)/(b) still need the heavy-level capture. Launcher `build-pc/d207_run.ps1` added (forced-alarm auto-repro on level_09).** |
| D208 | **Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) — `struct` field `bondview.h:200 s32 field_A48` is used as a second `ALSoundState *` sound-handle slot, but is only 32 bits wide on PC (M-78c, ROOT CAUSE FOUND, D3x pointer-width class).** This is the "doesn't recover after the alarm stops" half of the user's D207 report — a distinct bug from D207's voice-pool question. The player's per-hand fire sound is double-buffered across two handle slots: `hands[h].audioHandle` (`ALSoundState *`, correctly pointer-width) and `hands[h].field_A48` (`s32`). `gunfire.c:3186-3200` (`bondfirefunc`) each trigger interval calls `sndPlaySfx(bank, idx, (ALSoundState *)&handptr->field_A48)`, and `sndPlaySfx` writes the new state through it as `pendingState->link.next = nextState` — an **8-byte pointer store** into a 4-byte field, overrunning into the adjacent `field_A4C` (which is the auto-fire sound-cadence timer, written back every frame at `gunfire.c:3168`). `sndUnlinkClearSound` likewise does an 8-byte `state->state->link.next = NULL` there. The two writers stomp each other; after enough double-buffered fire cycles `field_A48` holds a value that is neither `0` nor a valid pointer, so the `else if ((ALSoundState *)handptr->field_A48 == 0)` re-arm gate in the double buffer is **never true again → no further player gunshot is ever issued.** `audioHandle` alone can't cover because the code only replays into it `if (audioHandle == NULL)` and the async `sndDeactivate(audioHandle)` leaves it briefly non-NULL, deferring to the jammed `field_A48` branch. **Evidence (user Bunker capture, `audiotrace.log`, silenced PP7 = idx 46):** idx-46 `sndPlaySfx` requests stop dead at t=776609203515 (log line 18930 of 25086) while guard fire (109), pickups (232), impacts, doors all continue to the end; at line 19159 the player-gun slot `0x70076630` is caught holding `was 0x000001db00000000` — a torn 64-bit value = `field_A48=0x00000000` + `field_A4C=0x1db` (475, a `g_GlobalTimer`-relative cadence stamp) read as one word. The alarm is not required — it just raises `sndPlaySfx` churn so the tear is hit sooner. On N64 both fields are 4 bytes, `audioHandle` is a 4-byte pointer, and `&field_A48` punned as `ALLink*` is a 4-byte `link.next` write — all consistent, no overrun. **`bondview` / `hands[]` is runtime-only state (`g_CurrentPlayer`), never ROM-serialized**, so widening `field_A48` to pointer width is layout-safe (no by-offset or ROM-cast access — grep-verified; only `field_A50`/`field_A4C` sit after it and are plain `s32` timers). | FIXED + user playtest-verified (M-78d) — Bunker firefight: player gunshots persist through the whole engagement and survive the klaxon stopping. Committed 7601b9db (PR #29, merged). |
| D209 | **Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands — `act_ubytes.padding[45]`, a raw-byte alias into the action union for `act_gopos.unk59` (the SPEED tier), shifts from offset 45 to 81 at 64-bit (M-81, ROOT CAUSE of D193, D3x pointer-width class).** GE character *travel* is animation-root-motion driven, so the bound locomotion animation sets the speed. `get_sound_at_range()` (`chraction.c:3573`, the locomotion-anim selector — misleading name) picks `ANIM_DATA_sprinting`/`running`/`walking` (and the one-handed/female variants) from its `arg1` tier. Its **only** caller, `play_hit_soundeffect_and_proper_volume()` (`chraction.c:3665`), passes the tier as `self->act_ubytes.padding[45]` — a raw byte read out of the `ChrRecord` action union rather than the named field. On N64 that alias is exact: `struct act_gopos` = `{coord3d targetpos@0, StandTile *target@12, waypoint *target_path@16, waypoint *waypoints[6]@20, u8 curindex@44, u8 unk59@45}`, so `padding[45]` **is** `unk59`. On x86-64 the three pointer members widen 4→8 B (`target@16, target_path@24, waypoints@32`), pushing `curindex`→80 and `unk59`→**81**, while the literal `45` now lands inside `waypoints[1]` (spanning 40..47) — specifically **byte 5 of a heap pointer**. The port's arena is low-4GB (`0x00000000_70xxxxxx`), so byte 5 is **always `0x00`** → `arg1 = 0` → `ANIM_DATA_walking` / `ANIM_DATA_walking_unarmed`, unconditionally, for every AI chr on every level since the 64-bit transition. The named-field reads (`chrlvApplySpeed(self, ..., (s32) self->act_gopos.unk59, ...)` at `:9104`, and the `unk59 == 2` / `== 1` `modelSetAnimSpeed` tiers at `:9106-9130`) resolve correctly, which is why the tier *looked* right everywhere it was inspected — only the alias was broken. **Runtime evidence (Cradle `-level_41`, `GE_D193A` extended with `tier`/`aidx`):** all six chrs report `act=15 ACT_GOPOS tier=1 SPEED_RUN` on 420/440 samples, yet `aidx` (live `model->anim` reverse-resolved against `animation_table_ptrs1`) is **40 = `PTR_ANIM_walking`** for Trevelyan (chr 0) and **107 = `PTR_ANIM_walking_unarmed`** for the guards on every single sample — `aidx 42 (running)` / `41 (sprinting)` never appear. Travel speed sat in a flat 140–155 u/s band for all of them, independent of animation, which is the walk-cycle stride. `act_ubytes` has exactly one use in the whole tree, so this is a single site. Both callers of `play_hit_soundeffect_and_proper_volume` reach it with `actiontype == ACT_GOPOS` (`:3723`, immediately after `actiontype = ACT_GOPOS; act_gopos.unk59 = speed;`; and `:9087`, inside the explicit `else` of `actiontype == ACT_PATROL`, mirroring the `chrlvApplySpeed` split directly below it), so reading the named field is exactly the byte N64 reads — no behavioural change. | **FIXED + user-playtest-verified (M-81).** `chraction.c:3665`, `#ifdef PORT` → `get_sound_at_range(self, self->act_gopos.unk59, ...)`; the N64 line kept verbatim under `#else`. ABI/pointer-width only, no game-logic change (rule-2 narrow exception, D3x class). User ran Cradle on the fix build: **Trevelyan and the guards run.** Also corrected the `// guess: room` comment on `unk59` (`src/bondtypes.h:2191`) — it is the SPEED tier. |
| D210 | **`chrToPatrol()` leaves `act_patrol.lastvisible60` uninitialised on PC — a raw-byte union alias (`act_init.padding[0x13]`) that shifts when `act_patrol` widens (M-82, A1 class, sibling of D209).** `chrToPatrol` (`chraction.c:3872`, sets `ACT_PATROL`) does `self->act_init.padding[0x13] = -1;` — union-relative byte `0x4c`. On N64 `struct act_patrol` = `{patrol_path *path@0, s32 nextstep@4, bool forward@8, waydata@0xc (0x40 B), s32 lastvisible60@0x4c, f32 speed@0x50}`, so byte `0x4c` **is** `lastvisible60`, initialised to `-1`. On x86-64 the leading `path` pointer widens 4→8 B, so everything after shifts +4: `lastvisible60` is now at `0x50`, and the literal `0x13`/byte-`0x4c` write lands in the last 4 bytes of `waydata` (`segdisttotal`). Net PC effect: `lastvisible60` keeps whatever garbage the previous union arm left, and the "chr hasn't seen the player recently → break off / resume patrol" gate at `chraction.c:~9347` (`(act_patrol.lastvisible60 + CHRLV_DEFAULT_TIMER) < g_GlobalTimer`) reads that garbage on the first tick(s) after entering PATROL — until `chr.c:2569` / `chraction.c:3482` / `:9359` overwrite it (only when the chr actually sees the player). Same root class as D209 (raw index into a union arm whose pointer members widened); found by the post-D209 A1 sweep. Only new instance in the sweep — `act_init.padding[0..3]` in `chrlvTickAnim`/`chrlvTickDead` alias `act_anim`/`act_dead`, both pointer-free, so those are layout-stable. | **FIXED (`chraction.c:3872`, `#ifdef PORT` → `self->act_patrol.lastvisible60 = -1;`, N64 line under `#else`). ABI/pointer-width only (D3x/A1 class), no game-logic change.** Build clean; bunker1/facility/archives/dam verify PASS, no crash. **Owed:** a playtest eyeball of patrol-guard behaviour on entry to patrol (do they over/under-react to a player they haven't seen). Low individual impact but exactly the class the D170/D193 "AI behaviour looks off" reports keep landing in. |
| D208 | **Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) — `struct` field `bondview.h:200 s32 field_A48` is used as a second…** — full `## D208` entry at file tail | FIXED + user playtest-verified (M-78d) — Bunker firefight: player gunshots persist through the whole engagement and survive the klaxon stopping. Committed 7601b9db (PR #29, merged). |
| D209 | **Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands — `act_ubytes.padding[45]`, a raw-byte alias into…** — full `## D209` entry at file tail | **FIXED + user-playtest-verified (M-81).** `chraction.c:3665`, `#ifdef PORT` → `get_sound_at_range(self, self->act_gopos.unk59, ...)`; the N64 line kept verbatim under `#else`.… |
| D210 | **`chrToPatrol()` leaves `act_patrol.lastvisible60` uninitialised on PC — a raw-byte union alias (`act_init.padding[0x13]`) that shifts when `act_patrol` widens…** — full `## D210` entry at file tail | **FIXED (`chraction.c:3872`, `#ifdef PORT` → `self->act_patrol.lastvisible60 = -1;`, N64 line under `#else`).… |
| 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-74 WITHDRAWS M-73's root cause.** M-73 read the user's combat capture as guards stuck re-triggering fire sound idx 109 ("256x/29.7 s, no bullets") and pinned it on `stanTestLineUnobstructed` LOS over converted collision geometry. Re-counting the same file: idx 109 is requested **128x** not 256x (the double-log artifact D204 already corrected for M-63 on this very index), = 4.31/s across ~11 guards = ~0.4/s each, **~6x UNDER** the AK47's own `SoundTriggerRate` (`RATE_AK47`=4 ticks => 15/s ceiling) — so the `field_178` gate never binds; and impacts are plentiful in the same window (~30 ricochet/wall-hit 19-41, 69 x2 flesh, 12 body-falls 123-132, 14 yelps 134-147). The eye-catching monotone sweeps 134->147 / 123->132 are the ground-truth round-robin `male_guard_yelp_counter` (`chraction.c:2454-2470`), faithful, not a broken selector. **The 109 traffic is normal in-spec guard combat**; do NOT open the pccg-stan/LOS geometry investigation on this evidence. Everything upstream of presentation is proven: bank 261/261 identical (M-71), sample+pitch exact (M-68/M-70), and every index in the capture correct (182 explosion x1, 81 armor x1, 46/47/48/49/105). **Remaining untested layer = spatial presentation (per-voice volume, pan, concurrency)** — it explains all four complaints at once (a wrongly-placed sound is by ear a sound at your own position) and is the youngest code in the stack (`sndCreatePostEvent` was fully stubbed by D138 until M-65). **M-78: CLOSED.** All four symptoms were D206 (the +1 bank-slot alias). (3) armour + (4) melee->Klobb + (1) slap/glass over gunfire: user by-ear A/B vs N64 = PASS (M-77c). (2) explosion->scream was thought NOT explained by D206 (169-183 -> 168-182, all still explosions) but the user re-tested on Bunker post-merge and the explosion SFX is now correct by ear — the earlier "scream" was a coincident guard death-yelp (GET_HIT_MALE 134-147 seen +/-1s of the blast in M-73's capture), faithful N64 behaviour, not a wrong sample. No presentation-layer bug: pan audit (M-74) already showed GE never spatially pans SFX (252/261 bank sounds `samplePan==64`, no `AL_SNDP_PAN_EVT` poster). | CLOSED (M-78) — root cause was D206; all four symptoms resolved + user-confirmed by ear vs N64. No separate presentation-layer fix needed. |
| D152+ | **D152 addendum (M-31):** — full `## D152+` entry at file tail | 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. |
@@ -441,11 +441,11 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
| D181 | **`Game.ScreenShakeIntensity` — first route-(b) `src/` gameplay-cosmetic hook (M-XX QoL run).** — full `## D181` entry at file tail | **Menu row pulled (v0.2.1, user testing).** The hook is correct on its own narrow terms —… |
| D184 | **F10 port-layer options overlay — the approach-(C) surface from `OPTIONS-MENU-PLAN.md` (M-37).** — full `## D184` entry at file tail | **LANDED, port-only, no `#ifdef PORT` in `src/`.** Build links 241/241.… |
| D211 | **PC FOV slider — `Video.FovScale` (M-83 QoL wave, port-only, `WIDESCREEN-FOV-PLAN.md` Phase 4).** fast3d applies a multiplicative half-angle scale to every *perspective* projection matrix as it is loaded (`gfx_apply_fov_scale()` in `gfx_pc.cpp gfx_sp_matrix`, on the `G_MTX_PROJECTION | G_MTX_LOAD` path only — GE always reloads its view projection via `guPerspectiveF` at `fr.c:725`, so a relative `G_MTX_MUL` onto an already-scaled matrix cannot compound). Columns 0 and 1 are scaled by the same factor `tan(half_orig)/tan(half_adj)` → H and V FOV widen uniformly, aspect ratio exactly preserved (no stretch), and because the game rewrites fovy every frame for aim-zoom the scale composes with zoom for free. Ortho matrices (`P[3][3]==1`) are left untouched so HUD/menu 2D is unaffected. `Video.FovScale` = percent of the original vertical FOV, 50–150, **default 100 ⇒ `g_fov_scale == 1.0f` ⇒ guarded early-return, zero FP ops on the matrix ⇒ golden dumps byte-identical**. F10 overlay row "FOV scale %". Math checked offline (`s=1.25 → fovy 60°→75°`, aspect held at 1.3333). | **PARTIAL — M-84 refix: game-side FOV scale (`src/fr.c`, `#ifdef PORT`), front-end gated, user-verified.** Original M-83 note follows. LANDED, port-only; build clean; `bunker1` framediff worst_cell 12.219 vs 12.221 baseline (= render noise, no-op confirmed). **M-84 refix — the fast3d hook only scaled *pure* perspective loads; GE draws the world through `g_CurrentPlayer->field_10E0`, a projection × view matrix pre-combined on the CPU (`bondview2.c:8224`) that failed the `gfx_apply_fov_scale` guard, so only the pause/watch model + sky FOV changed (user report).** Moved the scale game-side to the `guPerspectiveF` chokepoint (`fr.c:718`, `#ifdef PORT`, `frFovY *= portFovScale`, clamp 160°), which lands *before* the CPU combine; fast3d `gfx_apply_fov_scale`/`gfx_set_fov_scale` removed; `port/src/video.c` sets `f32 portFovScale`. Also gated to `lvlGetCurrentStageToLoad() != LEVELID_TITLE` — the front end (main menu / file+mission select / briefing) renders through the same path (`lv.c lvlRender`) and its 3D is authored at a fixed FOV. **User-verified: front end unaffected + in-level view widens (Dam tunnel at high FovScale).** M-85 headless nit check (`-level_09`, FovScale 100 vs 120): **HUD-static confirmed** — the ammo counter ("7 | 93") sits at the identical bottom-right screen position in both, and the door-arrow stays square (no 4:3 stretch); viewmodel scales down + stays bottom-right anchored. **aim-zoom-compose — M-86 static analysis: correct by construction.** `bondview2.c:3123` lerps `zoominfovy` then `viSetFovY()` stores it in `g_ViBackData->fovy`; `fr.c:737` applies `frFovY = fovy * portFovScale` **once**, at the per-frame chokepoint, *after* the zoom lerp. Hipfire and zoomed states scale by the same factor so the zoom magnification ratio is preserved exactly; no double-application; the smooth zoom interpolation is unaffected (scale is post-lerp). Residual human eyeball (low risk, cosmetic) at FovScale ~120–150 + full aim-zoom: the viewmodel (`c_perspfovy`, `bondview.c:644`) and frustum-cull (`currentPlayerSetCameraScale`) use the *unscaled* fovy, so a subtle viewmodel-scale or edge-cull-early mismatch is possible; the scope reticle is 2D screen-space, unaffected. Caveat (same root): edge geometry culls a hair early at large scale (cosmetic). **User-verified good at big FovScale.** New adjacent report → **D220**: 1P muzzle flash position shifts upward at higher FovScale (Jungle, M16) — likely the same unscaled-viewmodel-fovy root as the caveat above. Wide FOV also exposes the per-level draw-distance boundary → **D218** (logged, parked). **M-87: the frustum-cull residual is a real bug, not cosmetic** → split to **D222**. |
| D212 | **Anisotropic-filtering config wiring — `Video.Anisotropy` (M-83 QoL wave, port-only).** The fast3d GL backend already implemented `set_anisotropy_level` / `get_max_anisotropy_level` and applied `GL_TEXTURE_MAX_ANISOTROPY` on mipmapped tiles with a hardcoded `current_anisotropy_level = 4`; there was no way to configure it. Added the `gfx_set_anisotropy_level()` extern-C wrapper in `gfx_pc.cpp` (clamps to `[1, GL max]`, `reset_texture_state()` so live changes take), declared in `gfx_api.h`, and `Video.Anisotropy` (1–16) in `port/src/video.c` wired through `videoApplyImageOptions()` on init + live-config. **Default 4 = the value fast3d already applied ⇒ no visual delta at defaults.** F10 overlay row "Anisotropic". | **LANDED, port-only.** Build clean. **Owed:** eyeball at 16× on a grazing surface (Depot roof / Dam walkway) vs 1×. |
| D213 | **On-screen FPS readout — `Video.DisplayFPS` (M-83 QoL wave, port-only, PD parity).** `port/src/optionsoverlay.c` samples frame time once per emitted frame (`fpsTick()` via `sysGetMicroseconds()`, ~0.5 s window) and, when enabled, emits a small top-right green "N FPS" DL even while the F10 panel is closed. Config-only knob (kept off the 13-row panel, which is at its documented layout limit — matches PD, whose `DisplayFPS` is also file-only). **Default 0 ⇒ `optionsOverlayEmit()` still returns NULL when the panel is closed ⇒ golden dumps byte-identical.** | **LANDED, port-only.** Build clean; `bunker1` no-op confirmed. **Owed:** eyeball that the counter reads a sane ~30 and doesn't overlap the HUD. |
| D214 | **Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).** `port/src/input.c` gains 12 gameplay actions (Forward/Back/StrafeLeft/StrafeRight/TurnLeft/TurnRight/Fire/Aim/Action/Cancel/LeanLeft/Start), each a comma-separated list of up to 4 SDL scancode names (`Input.Bind.Forward = W,Up` …). `inputRebuildBinds()` (called from `inputInit()`, after `configLoad()`) parses them via `SDL_GetScancodeFromName`; unknown names log a warning and are skipped. The old hardcoded `keyDown(ks, SDL_SCANCODE_*)` block in `inputComputePad` is replaced by `actHeld(ks, IA_*)`. Mouse buttons (fire = LMB, aim = RMB) stay hardwired; the mouse-wheel weapon-cycle A-pulse is untouched. **Defaults reproduce the previous FPS layout exactly — verified: all 20 default key names round-trip to the identical `SDL_Scancode` the old code used (`SDL_GetScancodeFromName` test, 0 mismatches).** Config-file only (no in-overlay text entry — matches PD, whose rebinding is also file-driven); a fresh `ge007.ini` gets the full `[Input.Bind]` block with the defaults visible. | **PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.** LANDED, port-only, no `#ifdef PORT`; build clean. One doc bug fixed (no code change): `config.c` groups keys by the substring *before the last `.`*, so the section header is **`[Input.Bind]`**, not `[Bind]` — earlier finding/commit/QOL text was wrong. Working syntax: put `Fire = Left Alt` under `[Input.Bind]`, or write a fully-qualified `Input.Bind.Fire = Left Alt` line anywhere (`config.c:307`). Separately: the pause→**watch menu** does not wire Esc→back (still Enter to open/close/back) — pre-existing, `options.c`, minor. |
| D215 | **1P weapon viewmodel never drew in-level — `gunRenderFirstPersonGunModels` builds its `ModelRenderData` from a reinterpret across separately-declared adjacent globals that only works on N64 (M-83, D3x/ABI class; retires VIEWMODEL-RESEARCH.md).** The N64 line `renderdata = *(ModelRenderData *)&D_80035CC0;` (`gunfire.c`) casts the lone `u32 D_80035CC0 = 0;` to a whole `ModelRenderData` and reads on into the next declared global, `u32 D_80035CC4[] = {1, 3, 0, 0, …}` (`gun.c`) — yielding `{ basemtx=0, zbufferenabled=1, flags=3, rest=0 }` **only because the N64 linker lays the two globals out contiguously in declaration order and pointers are 4 B**. On x86-64 it breaks twice: `ModelRenderData`'s three pointer members (`basemtx`/`gdl`/`mtxlist`) widen 4→8 B so `flags` is no longer at struct offset 8, and C guarantees nothing about the two globals' relative placement. Measured (`GE_DVM=1`, BUNKER1): `renderdata.flags == 0`, so **every** weapon-model DL node was gated off at `modelRenderNodeGundl` (`(renderdata->flags & 1)`), and `subdraw()` for the weapon emitted just its `gSPSegment` — 1 gfx command instead of ~150. All three VIEWMODEL-RESEARCH.md ranked suspects were ruled out by the same probe first: `field_87F==1` (gate open), `RootNode` valid, `numRecords 22 < 192` (pool fine), node walk visits 24+ valid `DL`/`SWITCH`/`BSP` nodes. **Fix:** `#ifdef PORT` → set the three fields explicitly (`zbufferenabled=TRUE; flags=3;`), N64 line verbatim under `#else`. ABI/layout only, no logic change. | **FIXED (`src/game/gunfire.c`, `#ifdef PORT`).** Build clean. `GE_DVM=1` post-fix: `flags=0x3`, weapon `subdraw()` now emits **153** gfx commands (was 1). Regression sweep crash-free. `GE_DVM` gate+render probes left in, env-gated (cf. `GE_D193*`). **Owed:** one BUNKER1 eyeball that the gun model is actually visible + correctly posed (headless can't see pixels; the DL is now provably generated). The committed `bunker1` golden will need re-baselining — the gun now renders in-frame. |
| D216 | **`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** New `s32 portSkipIntro` (`port/src/video.c`, `configRegisterInt("Game.SkipIntro", 0, 1)`), read once in `src/game/lv.c` at `LEVELID_TITLE` stage load — in the `else` of the existing `GE_STARTMENU` `#ifdef PORT` block. When set it does what every intro stage's own `!is_first_time_on_main_menu` branch does (`front.c`): `is_first_time_on_main_menu = FALSE; prev_keypresses = TRUE; maybe_is_in_menu = TRUE; menu_update = MENU_FILE_SELECT;` — i.e. it reuses the game's own post-intro route rather than inventing one, so the legal screen + Nintendo/Rare/GoldenEye logo attract loop is skipped. `GE_STARTMENU` still wins when both are set. Default 0 ⇒ untouched. | **LANDED + VERIFIED this session.** Build clean. Headless: default boot frame 90 = the TWYCROSS classification screen (10% non-black), `SkipIntro=1` frame 90 = the **SELECT FILE menu** (folders/photos/cursor all render, 89% non-black), no crash across repeated boots. Screenshots captured + eyeballed. **Owed:** nothing critical — a human boot to confirm the menu is fully interactive (cursor moves, a save slot opens). **REGRESSED (v0.2.1 user testing, 2026-09-15): user reports enabling `Skip intro` breaks audio.** Not root-caused this session — leading suspect is that the intro/attract stages perform some audio-subsystem init (title/attract music kickoff, or a first-touch of the mixer) that `portSkipIntro`'s jump to `MENU_FILE_SELECT` bypasses. **Mitigation applied:** the `Game.SkipIntro` row was pulled from the F10 options overlay (`port/src/optionsoverlay.c`) so players can no longer enable it from the menu; the config var, default (0/off), and `lv.c` hook are untouched (so an existing `ge007.ini` with it already set would still trigger the bug — worth a follow-up hard-disable if that turns out to matter). Do not re-add the menu row without root-causing the audio interaction first. |
| D212 | **Anisotropic-filtering config wiring — `Video.Anisotropy` (M-83 QoL wave, port-only).** — full `## D212` entry at file tail | **LANDED, port-only.** Build clean. **Owed:** eyeball at 16× on a grazing surface (Depot roof / Dam walkway) vs 1×. |
| D213 | **On-screen FPS readout — `Video.DisplayFPS` (M-83 QoL wave, port-only, PD parity).** — full `## D213` entry at file tail | **LANDED, port-only.** Build clean; `bunker1` no-op confirmed. **Owed:** eyeball that the counter reads a sane ~30 and doesn't overlap the HUD. |
| D214 | **Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).** — full `## D214` entry at file tail | **PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.** LANDED, port-only, no `#ifdef PORT`; build clean.… |
| D215 | **1P weapon viewmodel never drew in-level — `gunRenderFirstPersonGunModels` builds its `ModelRenderData` from a reinterpret across separately-declared adjacent…** — full `## D215` entry at file tail | **FIXED (`src/game/gunfire.c`, `#ifdef PORT`).** Build clean.… |
| D216 | **`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** — full `## D216` entry at file tail | **LANDED + VERIFIED this session.** Build clean.… |
| D217 | **1P weapon viewmodel + third-person Bond model textures render garbled — wrong/stale palette on model-DL CI textures (M-84, user QA; surfaced by D215 now that the viewmodel draws).** BUNKER1/Facility: the PP7 viewmodel has yellow+blue scrambled texels and a solid **green** patch by the hammer; Bond's in-game **watch face** shows the same green mismatch; user reports the **third-person Bond world model** does this "sometimes" too. Signature matches the **D161 family** — a CI-format tile drawn without the expected TLUT state, so fast3d's `import_texture` indexes a **stale `rdp.palette`**. D161 fixed the explicit `G_TT_NONE` case; this looks like model DLs not emitting `gDPLoadTLUT` through the port path, or the palette segment not resolving for weapon/character geometry. **Not a D215 regression** — D215 only un-gated the DL. | **FIXED (M-90/M-92/M-97; PR #47 texpool alignment + PR #49 CI-cache palette key) — the green-texture defect is fixed and merged, confirmed by two independent human playtests. The white missing-texture residual (AK47 / NPC-held world-model weapons) is split out as D228.** Status cell had gone stale at OPEN; corrected M-105. History: root cause characterised M-86; headless repro: `-level_34` FACILITY ~frame 300–400, PP7 **grip = flat green block** + green ring at the barrel joint + green-cast hand; barrel/slide upper correct black. Also green on `-level_30` DEPOT. **Clean on `-level_09` BUNKER1** (A/B control — same PP7, identical DL). **Two separate defects found:** ① **Wrong palette bound (the visible green — upstream of fast3d).** The grip is a CI4 solid-fill of palette index 0. Green `(24,65,0)` == RGBA5551 `0x1A00` == `rdp.palette[0]` at grip-import time. The grip's `count=1` model `G_LOADTLUT` is fed **different source bytes per level** for the same tile: BUNKER `00 01`→`PD_BE16`→`0x0001` (black ✓), FACILITY `1a 00`→`0x1A00` (green ✗). fast3d byteswaps + stores faithfully (proven correct by BUNKER) — the wrong bytes *arrive* from the model-DL / segment-address path. Strong tell: every *larger* model TLUT has `raw[0]==raw[1]==0x0000` with real colour from index 2 ⇒ GE's model-TLUT payload likely carries a **4-byte (2-entry) leading zero pad**, OR the port's model-TLUT source pointer is **4 bytes / 2 entries too low** (then the `count=1` grip load reads the tail of the preceding CI blob — BUNKER lucky, FACILITY not; fits "widespread across levels + weapons"). Ruled out this session: not tail-overflow (index 0 is in-range; matches M-85 `GE_D217Z`), not the fast3d byteswap. **Next:** dump the grip TLUT `G_SETTIMG` target vs the ROM/`pcmodels` sidecar PP7-grip material bytes; confirm the 4-byte offset vs N64; fix in model-DL segment resolution or `tools_pc/d43_emit.py` blob placement (sidecar regen). ② **CI texture-cache collision (proven, real, separate; fix landed).** `gfx_texture_cache_lookup` (`gfx_pc.cpp:522`) keyed CI textures on `{addr, palette_addrs[0/1], palidx, size_bytes}` with **no palette-content check**; GE reissues `gDPLoadTLUT` from repeated scratch-arena addresses with different content, so a later material could take a stale HIT and sample another material's decoded palette. **Fix on branch `fix/d217-model-texture-palette` (commit `f6fc0399`):** FNV-1a hash of the 512-byte `rdp.palette` computed only in `gfx_dp_load_tlut()`, added to the CI cache key (`port/fast3d/gfx_pc.{cpp,h}`, fast3d-internal, no `#ifdef`). Cheap, cannot worsen a decode; **owes a full 21-level `verify.sh sweep`** before merge (touches every palettized world texture's import path — cache-churn risk only). Does **not** fix defect ①. Screenshots: session scratchpad `d217/`, `fac_gun_crop.png`. Cross-ref `docs/dev/GRAPHICS-BACKLOG.md`. **User playtest matrix (M-87, raw notes — not yet triaged):** DAM/PPK: watch still green; AK47 viewmodel has white (missing-texture) patches on the magazine + barrel. FACILITY: PPK garbled **green AND blue**; 3rd-person Bond world model shows greenness on level load-in; AK47 has "weird blue / glitched" textures. JUNGLE: M16 viewmodel is orange in spots. Confirms multi-weapon (PP7/AK47/M16), multi-color (green/blue/white/orange) — consistent with the upstream model-TLUT-source theory (different trailing garbage per weapon/material) rather than one single fixed offset. **Scope clarified (M-87):** not just Bond's 1P viewmodel / 3P world model — **NPC-held weapon models (other characters' guns in third person) show the same glitched-color/white-missing texture symptom.** So the defect is in the shared weapon-model-DL/TLUT path used by *any* character holding that weapon, not something specific to Bond's model or the 1P viewmodel rig — widens the likely fix surface (weapon model loading in general) beyond "Bond's gun." **M-87 clarification:** not uniform across levels — severity and glitch *type* both vary per level (some levels show it lightly, some not at all in a given session, and the specific wrong-color/missing pLine truncated
| D218 | **Wide FOV exposes the per-level draw-distance / fog boundary — "blue artifacting" down long peripheral sightlines (M-84, user QA on the D211 FOV slider).** BUNKER→**Dam main tunnel** at `FovScale` ~150: geometry / backdrop past the level's fog-end distance shows at the widened frustum edges (blue = Dam's fog tint). Not a render bug — `fogLoadCurrentEnvironment` (`bgfog.c:305`) does `viSetZRange(Visibility.BlendMultiplier, Visibility.FarFog)`, so **`Visibility.FarFog` is both the far clip plane and the fog-saturation distance**; levels tune it for the stock ~60° FOV. | **Logged (M-84), fix proposed + parked.** Plan: `Video.DrawDistance` (percent, 100–400, default 100 ⇒ no-op) `#ifdef PORT` multiplier on `FarFog` at `bgfog.c:305` (+ the direct `arg0->Visibility.FarFog` reads just below, so fog stays consistent) plus `Video.DrawDistanceAutoFov` (0/1, default 1 — couples draw distance to `FovScale` unless `DrawDistance` is set explicitly). Clamp effective multiplier ≲2.5× (small znear ⇒ far-field z-fighting risk). **M-87: user confirms still occurring** at high FOV, and — reversing the earlier "not implemented by user request" — **now wants `Video.DrawDistance` implemented as a real QoL feature**, plus scope it to cover **LOD/detail-distance in general**, not just the fog/far-clip boundary this row root-caused. Open question for that broader ask: does GE have a *separate* geometry/object LOD-swap or draw-culling distance mechanism (distinct from `Visibility.FarFog`) worth exposing too, or is fog-distance the only meaningful "how far can you see" knob in this engine? Check for a per-object visibility-radius / simple-model-swap system before assuming `FarFog` is the whole story. **FIXED as designed, awaiting visual verification (M-121).** Implemented exactly the parked plan: `f32 portDrawDistanceMultiplier(void)` (`port/src/video.c`) — an explicit `Video.DrawDistance != 100` wins outright, otherwise `Video.DrawDistanceAutoFov` (default on) couples the multiplier 1:1 to `Video.FovScale`, clamped to `[1.0, 2.5]` (never shrinks draw distance; the 2.5x ceiling is the finding's own far-field z-fighting caution). Applied in `fogLoadCurrentEnvironment` (`bgfog.c`) to a new local (`scaledFarFog`) used at *both* the `viSetZRange` far-clip call and the `g_CurFogDetails.scaled_far_fog_dist` fog-blend read just below it, so the clip plane and the fog math stay consistent per the original plan's own note — the level's `EnvironmentRecord` itself is never mutated (it's shared/reloaded, not a per-frame copy). Both new config keys added to the F10 overlay (`Video.DrawDistance` slider, `Video.DrawDistanceAutoFov` toggle) alongside the existing `FovScale` row. Identity at both defaults (`DrawDistance=100`, `FovScale=100` ⇒ `portFovScale==1.0f`) by construction. **Broader LOD-swap question from M-87 ANSWERED — yes, and now exposed too (same session, M-121 continued).** `src/game/model.c` has a real, distinct geometry-LOD system: `MODELNODE_OPCODE_LOD` nodes whose children are shown/hidden per-frame by `modelUpdateDistanceRelations()` testing a view-space `distance` (scaled by `getPlayer_c_lodscalez()`, itself now FOV-consistent per this session's D222 fix) against each node's `MinDistance`/`MaxDistance` — a genuine detail-swap mechanism, separate from the fog-visibility-fade this row already fixed. It even has an existing ground-truth multiplier hook, `g_ModelDistanceScale`, already used transiently by the game itself (`chr.c`'s 0.3125f close-up override, `front.c`/`title.c`'s menu-model `modelSetDistanceDisabled`). Rather than write to that shared global (would fight those existing overrides), added a second, composed multiplier at the same call site: `f32 portLodDistanceMultiplier(void)` (`port/src/video.c`), driven by new `Video.LodDistance` (percent, 25-400, default 100) and `Video.LodDistanceAutoFov` (**default OFF**, unlike `DrawDistanceAutoFov` — this is meant as a standalone performance lever the user can pull *down*, not something that silently adds cost as FovScale widens) — `distance *= portLodDistanceMultiplier()`, inverse-scaled so `LodDistance=200` keeps full detail twice as far (more cost) and `LodDistance=50` drops to the lower LOD twice as soon (less cost). Both new keys in the F10 overlay alongside `DrawDistance`. Identity at `LodDistance=100`. Rebuilt clean, crash-free at `LodDistance=40` (aggressive) on Dam and at defaults on bunker1. **M-121 live playtest, iterated to a working state.** Two real issues found and fixed while testing live on Dam with the user: (1) **regression** — the D222 fix (above) correctly widened `c_lodscalez` for the screen-edge cull planes, but that same value is *also* read directly by `propobj.c`'s `chrobjFogVisRangeRelated()`/`sub_GAME_7F054C58()` (the character/prop fog-visibility-fade cutoff) — uncompensated, this made guards fade/disappear *sooner* at wide FOV than before D222, the opposite of the intent. Fixed by dividing that same Line truncated
| D219 | **PPK explosions render purple on Dam (M-87, user QA, "not 3D but noting").** Effect-color defect, not a texture-decode one — likely the D172 particle-color family (`gfx_lod_tile_offset` / `G_CC_INTERFERENCE` channel-order class) recurring on a different particle type/weapon than the D172 fix covered, or a fresh instance of the same class. | **RESOLVED (M-115, 2026-09-12)** — byte-order fix in `src/game/image.c` scoped by pixel format (16-bit family bswap16-at-store, 32-bit identity); user playtest-confirmed. Superseded history below, which began: **OPEN — observed, not investigated.** Next: `GE_PCDUMP`/`GE_TEXDUMP` a Dam PPK explosion; check whether it's the same `gfx_lod_tile_offset` tile-sampling defect D172 fixed for blood, on an explosion DL D172's fix didn't touch. **Status: re-checked at runtime M-90 — mechanism covered by the D172 fix on current `main`; close pending a human by-eye pass (folds into PR #47 playtest).** **M-89 triage (local-Qwen, static analysis only, not yet runtime-confirmed):** the explosion DLs (`assets/oddtextures.c` `globalDL_0x078/0x120/0x1c8/0x270…`) use the same `G_CYC_2CYCLE`+`G_CC_INTERFERENCE,G_CC_MODULATEIA2`+`G_TL_TILE`, tile0=IA smoke/tile1=RGBA fire mechanism as the already-fixed D172 blood/spark records — no separate/unfixed code path found in `explosion.c` or the asset DLs. If still purple after D172's fix (which the user's Dam report postdates — needs re-confirming on current `main`), the next suspect is the GLSL combiner implementation of `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2` for cycle 2 specifically (`gfx_generate_cc`, `port/fast3d/gfx_pc.cpp:312-494`), not the tile-offset fix. **Next, concrete:** run Dam (`-level_33`) with `GE_D172=1` while triggering a PPK/grenade explosion and check for "BUG" lines in the log (silence = D172's fix already covers this DL and the row can likely close on a re-test; needs a scripted-fire repro, not yet built). **M-90 runtime re-check DONE (scripted-fire repro built with `GE_INPUTSCRIPT`): the D172 fix demonstrably covers Dam's particle DLs — row can close on a human by-eye pass.** Ran `-level_33` headless with `GE_D172=1`, 64 scripted Z-pulses (`GE_INPUTSCRIPT="200:Z;215:Z;..."`, ~15 controller-reads apart) and a `GE_PCDUMP` window over the same span. Result: **40/40 particle multitex tris log `SAMPLED1=tile1 (tmem=392)`** — the correct RGBA fire tile, i.e. exactly the post-D172-fix behaviour (pre-fix these read `SAMPLED1 = tile0 (tmem=0 fmt=IA)`, the smoke texture); **zero** "BUG"/unknown-GBI/D146-abort lines; **0** purple/magenta pixels (`r>120, b>120, g<90, r-g>50, b-g>50`) across all 28 captured frames. Caveat: the user's M-87 "purple" report may predate the D172 fix on their binary (playtests run on whatever build was current); this re-check proves the sampling path is correct on current `main`, which is as far as a headless pass can take it — a final by-eye look at a live Dam PPK firefight is the honest closer (cheap for the user, folds into the D217 PR #47 playtest). **M-108 — REOPENED, the by-eye closer FAILED.** User played a live Dam (`-level_33`) PPK firefight on the current `main` build (post-D233/D234 fixes, same session as D243) and confirmed explosions are **still blue/purple**, directly contradicting M-90's clean scripted-repro result (40/40 correct-tile samples, 0 purple pixels, 28 frames). This means the M-90 `GE_INPUTSCRIPT` scripted-fire repro was very likely **not exercising the same explosion instance/DL the live firefight hits** — plausible gaps: a different explosion variant (grenade vs. bullet-impact spark vs. enemy-death vs. PPK-specific muzzle/hit effect) using a DL the scripted test's Z-pulses didn't trigger, or a state/lighting/prim-color condition (M-89's alternate suspect: the `G_CC_INTERFERENCE`+`G_CC_MODULATEIA2` cycle-2 GLSL combiner implementation, `gfx_generate_cc`, `port/fast3d/gfx_pc.cpp:312-494`) that only manifests under conditions the synthetic repro didn't reproduce. **Do not trust the M-90 all-clear for this finding going forward** — it proved one particle path clean, not that Dam's live-fire purple is fixed. Next: a live `GE_PCDUMP`/`GE_D172=1` capture during an actual purple explosion in real play (not a scripted approximation) is now the only way forward — need to pin down exactly what triggers it (PPK gunshots hitting a surface/enemy? a thrown/tossed explosive? which). **User has now confirmed (2026-09-12) this is NOT Dam-exclusive — game-wide, not just suspected.** Treat as confirmed scope, not pending a second-level check. **User wants this actively fixed, not just triaged** — next session should prioritize a live `GE_PCDUMP`/`GE_D172=1` capture during a real explosion (any level) over further static analysis, since the M-89/M-90 static passes are exhausted on this finding without producing a fix. **New HIGH-interest lead (M-110): `explosion.c` never clears `G_LIGHTING` before drawing any of the 15 particle DLs** (`globalDL_0x078` through `0Line truncated
@@ -10524,3 +10524,63 @@ D152 addendum (M-31): static audit of every compiled-audio `osSetIntMask(OS_IM_N
**Status (from §F table):** **LANDED, port-only, no `#ifdef PORT` in `src/`.** Build links 241/241. **Runtime verification NOT run this session** — the headless env kills the GUI process after ~8 s and `gfx_opengl_dump_bound_fbo` writes 0-byte PPMs here, so framediff / 60 s soak / overlay-frame eyeball are all owed on an interactive machine. Overlay-closed no-op is code-evident (emit returns NULL before touching the DL buffer).
## 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).
**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; **RESOLVED (D253):** reproduced on WSL Ubuntu by walking Bond into the crouch-forcing linked tile near the Facility opening; gdb capture showed SIGSEGV in `stanCheckLinkedSpecialTile` (`stan.c:2441`) storing to a stale `%r9` (0x3) while the caller's `record` local was valid — an x86-64 SysV int/float argument-class mismatch, not the D191 family. Fixed in D253; verified on WSL (same route no longer crashes).
**Status (from §F table):** CLOSED — root cause + fix in D253.
## D201 — `src/libultra/audio/bnkf.c` bank/instrument/sound/wavetable relocation offsets were `s32`, truncating the real 64-bit base pointer.
**`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.
**Status (from §F table):** FIXED (`src/libultra/audio/bnkf.c`) — part of the Phase-3 mixer landing, not yet build/runtime-verified in isolation
## D208 — Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) — `struct` field `bondview.h:200 s32 field_A48` is used as a second…
**Player weapon-fire sound goes permanently silent mid-firefight (all other SFX unaffected) — `struct` field `bondview.h:200 s32 field_A48` is used as a second `ALSoundState *` sound-handle slot, but is only 32 bits wide on PC (M-78c, ROOT CAUSE FOUND, D3x pointer-width class).** This is the "doesn't recover after the alarm stops" half of the user's D207 report — a distinct bug from D207's voice-pool question. The player's per-hand fire sound is double-buffered across two handle slots: `hands[h].audioHandle` (`ALSoundState *`, correctly pointer-width) and `hands[h].field_A48` (`s32`). `gunfire.c:3186-3200` (`bondfirefunc`) each trigger interval calls `sndPlaySfx(bank, idx, (ALSoundState *)&handptr->field_A48)`, and `sndPlaySfx` writes the new state through it as `pendingState->link.next = nextState` — an **8-byte pointer store** into a 4-byte field, overrunning into the adjacent `field_A4C` (which is the auto-fire sound-cadence timer, written back every frame at `gunfire.c:3168`). `sndUnlinkClearSound` likewise does an 8-byte `state->state->link.next = NULL` there. The two writers stomp each other; after enough double-buffered fire cycles `field_A48` holds a value that is neither `0` nor a valid pointer, so the `else if ((ALSoundState *)handptr->field_A48 == 0)` re-arm gate in the double buffer is **never true again → no further player gunshot is ever issued.** `audioHandle` alone can't cover because the code only replays into it `if (audioHandle == NULL)` and the async `sndDeactivate(audioHandle)` leaves it briefly non-NULL, deferring to the jammed `field_A48` branch. **Evidence (user Bunker capture, `audiotrace.log`, silenced PP7 = idx 46):** idx-46 `sndPlaySfx` requests stop dead at t=776609203515 (log line 18930 of 25086) while guard fire (109), pickups (232), impacts, doors all continue to the end; at line 19159 the player-gun slot `0x70076630` is caught holding `was 0x000001db00000000` — a torn 64-bit value = `field_A48=0x00000000` + `field_A4C=0x1db` (475, a `g_GlobalTimer`-relative cadence stamp) read as one word. The alarm is not required — it just raises `sndPlaySfx` churn so the tear is hit sooner. On N64 both fields are 4 bytes, `audioHandle` is a 4-byte pointer, and `&field_A48` punned as `ALLink*` is a 4-byte `link.next` write — all consistent, no overrun. **`bondview` / `hands[]` is runtime-only state (`g_CurrentPlayer`), never ROM-serialized**, so widening `field_A48` to pointer width is layout-safe (no by-offset or ROM-cast access — grep-verified; only `field_A50`/`field_A4C` sit after it and are plain `s32` timers).
**Status (from §F table):** FIXED + user playtest-verified (M-78d) — Bunker firefight: player gunshots persist through the whole engagement and survive the klaxon stopping. Committed 7601b9db (PR #29, merged).
## D209 — Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands — `act_ubytes.padding[45]`, a raw-byte alias into…
**Every AI character in the game is locked to a *walk* animation regardless of the speed the ailist commands — `act_ubytes.padding[45]`, a raw-byte alias into the action union for `act_gopos.unk59` (the SPEED tier), shifts from offset 45 to 81 at 64-bit (M-81, ROOT CAUSE of D193, D3x pointer-width class).** GE character *travel* is animation-root-motion driven, so the bound locomotion animation sets the speed. `get_sound_at_range()` (`chraction.c:3573`, the locomotion-anim selector — misleading name) picks `ANIM_DATA_sprinting`/`running`/`walking` (and the one-handed/female variants) from its `arg1` tier. Its **only** caller, `play_hit_soundeffect_and_proper_volume()` (`chraction.c:3665`), passes the tier as `self->act_ubytes.padding[45]` — a raw byte read out of the `ChrRecord` action union rather than the named field. On N64 that alias is exact: `struct act_gopos` = `{coord3d targetpos@0, StandTile *target@12, waypoint *target_path@16, waypoint *waypoints[6]@20, u8 curindex@44, u8 unk59@45}`, so `padding[45]` **is** `unk59`. On x86-64 the three pointer members widen 4→8 B (`target@16, target_path@24, waypoints@32`), pushing `curindex`→80 and `unk59`→**81**, while the literal `45` now lands inside `waypoints[1]` (spanning 40..47) — specifically **byte 5 of a heap pointer**. The port's arena is low-4GB (`0x00000000_70xxxxxx`), so byte 5 is **always `0x00`** → `arg1 = 0` → `ANIM_DATA_walking` / `ANIM_DATA_walking_unarmed`, unconditionally, for every AI chr on every level since the 64-bit transition. The named-field reads (`chrlvApplySpeed(self, ..., (s32) self->act_gopos.unk59, ...)` at `:9104`, and the `unk59 == 2` / `== 1` `modelSetAnimSpeed` tiers at `:9106-9130`) resolve correctly, which is why the tier *looked* right everywhere it was inspected — only the alias was broken. **Runtime evidence (Cradle `-level_41`, `GE_D193A` extended with `tier`/`aidx`):** all six chrs report `act=15 ACT_GOPOS tier=1 SPEED_RUN` on 420/440 samples, yet `aidx` (live `model->anim` reverse-resolved against `animation_table_ptrs1`) is **40 = `PTR_ANIM_walking`** for Trevelyan (chr 0) and **107 = `PTR_ANIM_walking_unarmed`** for the guards on every single sample — `aidx 42 (running)` / `41 (sprinting)` never appear. Travel speed sat in a flat 140–155 u/s band for all of them, independent of animation, which is the walk-cycle stride. `act_ubytes` has exactly one use in the whole tree, so this is a single site. Both callers of `play_hit_soundeffect_and_proper_volume` reach it with `actiontype == ACT_GOPOS` (`:3723`, immediately after `actiontype = ACT_GOPOS; act_gopos.unk59 = speed;`; and `:9087`, inside the explicit `else` of `actiontype == ACT_PATROL`, mirroring the `chrlvApplySpeed` split directly below it), so reading the named field is exactly the byte N64 reads — no behavioural change.
**Status (from §F table):** **FIXED + user-playtest-verified (M-81).** `chraction.c:3665`, `#ifdef PORT` → `get_sound_at_range(self, self->act_gopos.unk59, ...)`; the N64 line kept verbatim under `#else`. ABI/pointer-width only, no game-logic change (rule-2 narrow exception, D3x class). User ran Cradle on the fix build: **Trevelyan and the guards run.** Also corrected the `// guess: room` comment on `unk59` (`src/bondtypes.h:2191`) — it is the SPEED tier.
## D210 — `chrToPatrol()` leaves `act_patrol.lastvisible60` uninitialised on PC — a raw-byte union alias (`act_init.padding[0x13]`) that shifts when `act_patrol` widens…
**`chrToPatrol()` leaves `act_patrol.lastvisible60` uninitialised on PC — a raw-byte union alias (`act_init.padding[0x13]`) that shifts when `act_patrol` widens (M-82, A1 class, sibling of D209).** `chrToPatrol` (`chraction.c:3872`, sets `ACT_PATROL`) does `self->act_init.padding[0x13] = -1;` — union-relative byte `0x4c`. On N64 `struct act_patrol` = `{patrol_path *path@0, s32 nextstep@4, bool forward@8, waydata@0xc (0x40 B), s32 lastvisible60@0x4c, f32 speed@0x50}`, so byte `0x4c` **is** `lastvisible60`, initialised to `-1`. On x86-64 the leading `path` pointer widens 4→8 B, so everything after shifts +4: `lastvisible60` is now at `0x50`, and the literal `0x13`/byte-`0x4c` write lands in the last 4 bytes of `waydata` (`segdisttotal`). Net PC effect: `lastvisible60` keeps whatever garbage the previous union arm left, and the "chr hasn't seen the player recently → break off / resume patrol" gate at `chraction.c:~9347` (`(act_patrol.lastvisible60 + CHRLV_DEFAULT_TIMER) < g_GlobalTimer`) reads that garbage on the first tick(s) after entering PATROL — until `chr.c:2569` / `chraction.c:3482` / `:9359` overwrite it (only when the chr actually sees the player). Same root class as D209 (raw index into a union arm whose pointer members widened); found by the post-D209 A1 sweep. Only new instance in the sweep — `act_init.padding[0..3]` in `chrlvTickAnim`/`chrlvTickDead` alias `act_anim`/`act_dead`, both pointer-free, so those are layout-stable.
**Status (from §F table):** **FIXED (`chraction.c:3872`, `#ifdef PORT` → `self->act_patrol.lastvisible60 = -1;`, N64 line under `#else`). ABI/pointer-width only (D3x/A1 class), no game-logic change.** Build clean; bunker1/facility/archives/dam verify PASS, no crash. **Owed:** a playtest eyeball of patrol-guard behaviour on entry to patrol (do they over/under-react to a player they haven't seen). Low individual impact but exactly the class the D170/D193 "AI behaviour looks off" reports keep landing in.
## D212 — Anisotropic-filtering config wiring — `Video.Anisotropy` (M-83 QoL wave, port-only).
**Anisotropic-filtering config wiring — `Video.Anisotropy` (M-83 QoL wave, port-only).** The fast3d GL backend already implemented `set_anisotropy_level` / `get_max_anisotropy_level` and applied `GL_TEXTURE_MAX_ANISOTROPY` on mipmapped tiles with a hardcoded `current_anisotropy_level = 4`; there was no way to configure it. Added the `gfx_set_anisotropy_level()` extern-C wrapper in `gfx_pc.cpp` (clamps to `[1, GL max]`, `reset_texture_state()` so live changes take), declared in `gfx_api.h`, and `Video.Anisotropy` (1–16) in `port/src/video.c` wired through `videoApplyImageOptions()` on init + live-config. **Default 4 = the value fast3d already applied ⇒ no visual delta at defaults.** F10 overlay row "Anisotropic".
**Status (from §F table):** **LANDED, port-only.** Build clean. **Owed:** eyeball at 16× on a grazing surface (Depot roof / Dam walkway) vs 1×.
## D213 — On-screen FPS readout — `Video.DisplayFPS` (M-83 QoL wave, port-only, PD parity).
**On-screen FPS readout — `Video.DisplayFPS` (M-83 QoL wave, port-only, PD parity).** `port/src/optionsoverlay.c` samples frame time once per emitted frame (`fpsTick()` via `sysGetMicroseconds()`, ~0.5 s window) and, when enabled, emits a small top-right green "N FPS" DL even while the F10 panel is closed. Config-only knob (kept off the 13-row panel, which is at its documented layout limit — matches PD, whose `DisplayFPS` is also file-only). **Default 0 ⇒ `optionsOverlayEmit()` still returns NULL when the panel is closed ⇒ golden dumps byte-identical.**
**Status (from §F table):** **LANDED, port-only.** Build clean; `bunker1` no-op confirmed. **Owed:** eyeball that the counter reads a sane ~30 and doesn't overlap the HUD.
## D214 — Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).
**Keyboard rebinding — `[Input.Bind]` config section (M-83, port-only, PD parity, Phase 4 debt).** `port/src/input.c` gains 12 gameplay actions (Forward/Back/StrafeLeft/StrafeRight/TurnLeft/TurnRight/Fire/Aim/Action/Cancel/LeanLeft/Start), each a comma-separated list of up to 4 SDL scancode names (`Input.Bind.Forward = W,Up` …). `inputRebuildBinds()` (called from `inputInit()`, after `configLoad()`) parses them via `SDL_GetScancodeFromName`; unknown names log a warning and are skipped. The old hardcoded `keyDown(ks, SDL_SCANCODE_*)` block in `inputComputePad` is replaced by `actHeld(ks, IA_*)`. Mouse buttons (fire = LMB, aim = RMB) stay hardwired; the mouse-wheel weapon-cycle A-pulse is untouched. **Defaults reproduce the previous FPS layout exactly — verified: all 20 default key names round-trip to the identical `SDL_Scancode` the old code used (`SDL_GetScancodeFromName` test, 0 mismatches).** Config-file only (no in-overlay text entry — matches PD, whose rebinding is also file-driven); a fresh `ge007.ini` gets the full `[Input.Bind]` block with the defaults visible.
**Status (from §F table):** **PARTIAL — M-84: code PASS (alt/ctrl); one doc-only fix.** LANDED, port-only, no `#ifdef PORT`; build clean. One doc bug fixed (no code change): `config.c` groups keys by the substring *before the last `.`*, so the section header is **`[Input.Bind]`**, not `[Bind]` — earlier finding/commit/QOL text was wrong. Working syntax: put `Fire = Left Alt` under `[Input.Bind]`, or write a fully-qualified `Input.Bind.Fire = Left Alt` line anywhere (`config.c:307`). Separately: the pause→**watch menu** does not wire Esc→back (still Enter to open/close/back) — pre-existing, `options.c`, minor.
## D215 — 1P weapon viewmodel never drew in-level — `gunRenderFirstPersonGunModels` builds its `ModelRenderData` from a reinterpret across separately-declared adjacent…
**1P weapon viewmodel never drew in-level — `gunRenderFirstPersonGunModels` builds its `ModelRenderData` from a reinterpret across separately-declared adjacent globals that only works on N64 (M-83, D3x/ABI class; retires VIEWMODEL-RESEARCH.md).** The N64 line `renderdata = *(ModelRenderData *)&D_80035CC0;` (`gunfire.c`) casts the lone `u32 D_80035CC0 = 0;` to a whole `ModelRenderData` and reads on into the next declared global, `u32 D_80035CC4[] = {1, 3, 0, 0, …}` (`gun.c`) — yielding `{ basemtx=0, zbufferenabled=1, flags=3, rest=0 }` **only because the N64 linker lays the two globals out contiguously in declaration order and pointers are 4 B**. On x86-64 it breaks twice: `ModelRenderData`'s three pointer members (`basemtx`/`gdl`/`mtxlist`) widen 4→8 B so `flags` is no longer at struct offset 8, and C guarantees nothing about the two globals' relative placement. Measured (`GE_DVM=1`, BUNKER1): `renderdata.flags == 0`, so **every** weapon-model DL node was gated off at `modelRenderNodeGundl` (`(renderdata->flags & 1)`), and `subdraw()` for the weapon emitted just its `gSPSegment` — 1 gfx command instead of ~150. All three VIEWMODEL-RESEARCH.md ranked suspects were ruled out by the same probe first: `field_87F==1` (gate open), `RootNode` valid, `numRecords 22 < 192` (pool fine), node walk visits 24+ valid `DL`/`SWITCH`/`BSP` nodes. **Fix:** `#ifdef PORT` → set the three fields explicitly (`zbufferenabled=TRUE; flags=3;`), N64 line verbatim under `#else`. ABI/layout only, no logic change.
**Status (from §F table):** **FIXED (`src/game/gunfire.c`, `#ifdef PORT`).** Build clean. `GE_DVM=1` post-fix: `flags=0x3`, weapon `subdraw()` now emits **153** gfx commands (was 1). Regression sweep crash-free. `GE_DVM` gate+render probes left in, env-gated (cf. `GE_D193*`). **Owed:** one BUNKER1 eyeball that the gun model is actually visible + correctly posed (headless can't see pixels; the DL is now provably generated). The committed `bunker1` golden will need re-baselining — the gun now renders in-frame.
## D216 — `Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).
**`Game.SkipIntro` — boot straight to the file-select menu (M-83, port + one `#ifdef PORT` game-code line, PD parity, Phase 4 quick win).** New `s32 portSkipIntro` (`port/src/video.c`, `configRegisterInt("Game.SkipIntro", 0, 1)`), read once in `src/game/lv.c` at `LEVELID_TITLE` stage load — in the `else` of the existing `GE_STARTMENU` `#ifdef PORT` block. When set it does what every intro stage's own `!is_first_time_on_main_menu` branch does (`front.c`): `is_first_time_on_main_menu = FALSE; prev_keypresses = TRUE; maybe_is_in_menu = TRUE; menu_update = MENU_FILE_SELECT;` — i.e. it reuses the game's own post-intro route rather than inventing one, so the legal screen + Nintendo/Rare/GoldenEye logo attract loop is skipped. `GE_STARTMENU` still wins when both are set. Default 0 ⇒ untouched.
**Status (from §F table):** **LANDED + VERIFIED this session.** Build clean. Headless: default boot frame 90 = the TWYCROSS classification screen (10% non-black), `SkipIntro=1` frame 90 = the **SELECT FILE menu** (folders/photos/cursor all render, 89% non-black), no crash across repeated boots. Screenshots captured + eyeballed. **Owed:** nothing critical — a human boot to confirm the menu is fully interactive (cursor moves, a save slot opens). **REGRESSED (v0.2.1 user testing, 2026-09-15): user reports enabling `Skip intro` breaks audio.** Not root-caused this session — leading suspect is that the intro/attract stages perform some audio-subsystem init (title/attract music kickoff, or a first-touch of the mixer) that `portSkipIntro`'s jump to `MENU_FILE_SELECT` bypasses. **Mitigation applied:** the `Game.SkipIntro` row was pulled from the F10 options overlay (`port/src/optionsoverlay.c`) so players can no longer enable it from the menu; the config var, default (0/off), and `lv.c` hook are untouched (so an existing `ge007.ini` with it already set would still trigger the bug — worth a follow-up hard-disable if that turns out to matter). Do not re-add the menu row without root-causing the audio interaction first.