Commit Graph
481 Commits
Author SHA1 Message Date
James Dansereau 0f7fbcecdf docs(audio): D207 M-78b — user Bunker repro; player-gun double-buffer lead
User repro: player gunshots specifically go permanently muted mid-firefight
and stay muted after the alarm ends; all other SFX fine. Not generic pool
starvation — a stuck-state bug in the gunfire.c:3178 two-slot double buffer
(audioHandle / field_A48). Leading mechanism: asynchronous sndDeactivate
posts to the 64-slot SFX event queue; under firefight+alarm load the queue
saturates, alEvtqPostEvent drops the deactivate, the state never disposes,
the slot never clears — once both slots stick, no player gunshot can play.
All three pool sizes are byte-match stock GE => port timing bug, not a value
to bump. Next: read the user's audiotrace.log to confirm evtq saturation +
uncleared slots.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 07:56:02 -04:00
James Dansereau 2f503f007d diag(audio): D207 — re-arm [D207-DROP] counter; close D205
D205: CLOSED (M-78). User re-tested explosion->scream on Bunker after the
PR #28 merge — the explosion SFX is correct by ear now. All four D205
symptoms were D206 (the +1 bank-slot alias); the "scream" was a coincident
guard death-yelp (faithful N64 behaviour). M-74's pan audit already ruled
out a presentation-layer bug.

D207: re-add ONLY the [D207-DROP] trace line (not the reverted last-resort
LOOPED-preemption pass) at both sndDisposeSound no-voice sites in snd.c,
gated on GE_AUDIOTRACE, logging state/prio/flags/count=N/max — so a user
capture on the real failing level is decisive between (a) heavy-level 8/8
starvation, (b) a leak on an alarm-stop path, (c) perceptual. Static
re-check noted in findings D207: ALARM3 slot 162 sampleVolume=80 (below
gunfire), keyMax=8 => not RETRIGGER => continuous-loop by design on N64 too.

findings-index.csv regenerated.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 07:44:29 -04:00
James Dansereau e3bebfc454 Merge pull request #28 from jkdansereau/feat/phase3-audio
Merge Phase 3 audio: software mixer + SFX fidelity (D198–D206), D202 closed.

https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 07:32:23 -04:00
James Dansereau 1dcc518e75 docs(dev): GE-ENV-PROBES.md — add GE_FORCEALARM (D207) row
Fixes the validate CI job (gen_env_probes.py --check drift).
2026-09-07 22:35:57 -04:00
James Dansereau c855625643 docs(dev): D202 closed, D206 by-ear PASS, D207 monitoring (M-77c)
User by-ear A/B vs N64 confirms D206: armour pickup, unarmed melee whiff
and silenced PP7 all correct. D202's silenced-PPK 'slap' was D206 (SFX
bank index +1) — CLOSED; M-65/M-66b ownerless-loop mitigations stay.
D205 narrows to symptom (2) explosion->scream only. D207 (alarm klaxon)
acceptable in play per user; voice-starvation hypothesis didn't hold up
(M-77b), no fix shipped, GE_FORCEALARM probe kept for follow-up.

Regenerated findings-index.csv.
2026-09-07 22:30:54 -04:00
James Dansereau 0845d70953 port(audio): D207 diag probe — GE_FORCEALARM
Temporary env-gated probe in handle_alarm_gas_timer_calldamage to force
and trace the alarm klaxon while investigating D207 (user report: alarm
SFX overrides other audio). #ifdef PORT + getenv, inert unless set.
Remove once D207 is resolved (see findings.md D207 / HANDOFF).
2026-09-07 22:23:01 -04:00
James Dansereau 9bf0c49151 fix(audio): D206 — every SFX resolved one bank slot too high
GE's sndPlaySfx reads the on-disk sound bank through the cast alias
struct ALInstrumentAlt_s { s32 unk0, unk4, unk8; ALSound *soundArray[1]; }.
On N64 (4-byte pointers) soundArray sits at struct offset 12 and
deliberately aliases the on-disk ALInstrument's bendRange/soundCount
words, so soundArray[N] == on-disk table entry [N-1] — GE's SFX_IDs are
1-based into the table by design (ID 0 = NOTHING_SFX, short-circuited).
On PC the 8-byte pointer + 8-byte alignment push soundArray to offset 16,
and the converted bank (romdata.c afFixupInst) is packed to match, so
every SFX_ID resolved one bank slot too high.

Root cause of D202's silenced-PPK 'slap' (id 46 -> PUNCH1 instead of
GUN_SILPPK_A) and D205 (3) armour + (4) melee->Klobb. M-61's rejected
'index 45, +0.999' was correct all along.

Fix: src/snd.c:1001, #ifndef PORT / #else -> soundArray[soundIndex - 1],
N64 line kept verbatim. ABI/layout only (D3x class), no game-logic change.
Also serves the retrigger chain (same 1-based space).

Verified: build clean; BUNKER1 GE_AUDIOTRACE 60s crash-free, all ~90
distinct requested indices resolve to rom_sfx_decode.py slot N-1 (0/90 at
N); -level_09 golden framediff 3/3. User playtest: 'most audio is good now.'

findings.md D206 + D205 (M-73/M-74) + D207; porting-notes.md §E.
2026-09-07 22:22:55 -04:00
James Dansereau 044d24a15f docs(dev): D205 M-73 correction — Klobb is a specific in-game SMG (Skorpion, idx 106); the user's sound identifications stand; capture shows no 106 while user was punching (47-49/105 all voiced), so melee->Klobb identification is OPEN (H3 vs H4), not resolved by the 109 barrage 2026-09-07 15:23:50 -04:00
James Dansereau fb037b3c0c 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 2026-09-07 15:17:03 -04:00
James Dansereau 087c60d05f docs(dev): D205 M-72 — 4th symptom (melee→Klobb idx 106); controlled GE_AUDIOTRACE runs exonerate all request paths; mechanism (b) extra-events leading suspect 2026-09-07 14:47:25 -04:00
James Dansereau c0047f76f7 docs(dev): D202 M-71 by-ear PASS + close-ready; new D205 wrong/extra SFX indices (bank exonerated); [VOL]/[DISTVOL] probes 2026-09-07 14:18:39 -04:00
James Dansereau c511b9060b docs(dev): D202 M-67/M-68/M-69 findings writeups + probe catalogue + audio-plan status
- findings.md: D202 §M-67 (static analysis exhausted, GE_VOICEDUMP probe
  motivation), §M-68 (decisive runtime A/B — all three user complaint
  classes play the exact ROM samples at the exact ROM pitches) and new
  §M-69 (320-request full-corpus exact match: 286/320 CC >= 0.85; the
  initial 44 'BAD' were tooling artifacts — voicedump stamps are block-END
  times, coarse-slide missed sharp onsets, refs unclipped at voice death;
  remaining failures = dropped allocations / shared-wavetable wire
  attribution, not corruption) + index row.
- GE-ENV-PROBES.md: GE_VOICEDUMP entry (incl. block-END-stamp gotcha);
  [WIRE]/[VOICEMAP]/[EVT]/[STOP-EVT] documented under GE_AUDIOTRACE.
- AUDIO-PLAN.md: status banner — Phases 0-2 done (D199/D200/D201/D204),
  Phase 3 verification is the active track.
2026-09-06 08:21:20 -04:00
James Dansereau 4ceb917ad8 port(audio): D202 M-67-M-69 runtime probes — GE_VOICEDUMP, [VOICEMAP], timestamped [WIRE]/[EVT]/[STOP-EVT]
Env-gated diagnostics for the D202 per-voice sample-correctness work
(M-67/M-68/M-69), all inert unless the env var is set:

- GE_VOICEDUMP (mixer.c aEnvMixerImpl): per-voice resampled mono stream
  to voicedump.raw, records [stateAddr][nSamples][us][s16 x n]. NB the
  us stamp is mix time — records render ahead, samples END at the stamp.
- [VOICEMAP] (synthesizer.c, under GE_AUDIOTRACE): full PVoice ->
  decoder/resampler/env-state address map at synth init, tying voicedump
  slot addresses to WIRE filter addresses.
- [WIRE] (load.c) now carries a µs timestamp; sndPlaySfx trace gains
  t=µs + wavetable pointer.
- [EVT]/[STOP-EVT] (snd.c): every ALSndpEvent the player processes, plus
  the release ramp a STOP/DEACTIVATE computes (deltaUs).
- audiodebug.ps1: -VoiceDump switch.

With scratchpad/exact_match.py these prove every allocated voice plays
the exact ROM sample at the exact ROM pitch (286/320 CC >= 0.85 on the
M-69 full-corpus run; remaining failures are dropped allocations + wire
attribution, not corruption). Remove once D202 closes.
2026-09-06 08:21:14 -04:00
James Dansereau 9b31f864d3 tools(pc): audiodebug.ps1 audio-probe driver; gitignore run artifacts
audiodebug.ps1 drives the D202/D204 audio probes (health monitor,
GE_AUDIOTRACE/GE_AUDIODUMP, soundIndex histogram, -AB before/after runs —
how D204 was measured); companion to debug.ps1 (crashes). README table row
added. .gitignore: scratchpad/ + root-level audiodump/audiotrace/mixertrace
artifacts.
2026-09-05 19:07:43 -04:00
James Dansereau 6736d6ee16 docs(dev): add live research/plan docs (D172, D176a, viewmodel, QOL, widescreen, unlocked-fps)
All referenced from HANDOFF.md / GRAPHICS-BACKLOG.md but untracked:
- D172-PARTICLE-COLOR-RESEARCH.md: magenta/cyan blood root-cause candidate
  (unhandled GBI opcode 0xB9 in ROM particle records + D146 guard)
- D176a-SKY-NOTES.md: RDPHALF sky-tri wire format decoded; Path B draft state
- VIEWMODEL-RESEARCH.md: 1P weapon model static audit, three ranked suspects
- QOL-INVENTORY.md / WIDESCREEN-FOV-PLAN.md / UNLOCKED-FPS-PLAN.md: parked
  Phase-4 proposals
2026-09-05 19:07:43 -04:00
James Dansereau efe477033f docs: D202 M-63/M-65/M-66/M-66b + D204 writeups; audio status in README/AGENTS
- findings.md: D202 full session history (M-63 live repro, M-65 voice-leak
  guard + sndCreatePostEvent unstub, M-66 root cause — sound 203 infinite
  ADPCM loop played fire-and-forget = faithful N64 quirk; M-66b option-C
  expiration implemented + measured) and D204 (AI-queue starvation, fixed
  + measured). Index row updated; findings-index.csv regenerated.
- GE-ENV-PROBES.md: D202 probe set documented ([EXPIRE] marked permanent).
- GRAPHICS-BACKLOG.md: D172 / D176 / 1P-viewmodel rows point at the new
  research docs.
- README + AGENTS.md: audio status corrected (mixer landed post-v0.1.0;
  Phase 3 in progress, not 'no audio').
2026-09-05 19:07:39 -04:00
James Dansereau 54c5095c23 port(audio): D202 env-gated diagnostics probes (removable on close)
Env-gated trace probes added while root-causing D202 (stuck door loop +
voice leak). All off by default; catalogue: docs/dev/GE-ENV-PROBES.md.
Remove this commit's additions when D202 closes after the user by-ear pass:

- GE_AUDIOTRACE: [VOICE+]/[VOICE-] voice acquire/release pairing,
  [EXPIRE] (permanent — part of the M-66b fix), sndPlaySfx resolution
  lines (src/snd.c)
- [DOORSND] + [SLOTWRITE] door sound state/slot ownership (propobj.c)
- [WAVELOOP] per-sound ALADPCMloop presence (libultra/audio/load.c)
- GE_PULLTRACE mixer pull diagnostics (port/src/mixer.c)
- GE_BANKDUMP / GE_KEYMAPDUMP ROM audio-table dumps (port/src/romdata.c)
- GE_MIXERTRACE [DMAHIT]/[DMAMISS] AI DMA callback trace (src/audi.c,
  __x86_64__-only)
2026-09-05 19:07:38 -04:00
James Dansereau 388d932df7 port(audio): D202 sndp fixes — ownerless-loop guard (M-65), sndCreatePostEvent unstub, option-C loop expiration (M-66b)
Three #ifdef PORT changes in src/snd.c (N64 build zero-diff), all
documented in findings.md D202:

1. M-65 voice-leak guard: the preemption scan reclaims provably-ownerless
   SOUND_FLAG_LOOPED SFX voices only, and only under pool pressure.
   Sound 203 (METAL_SLIDE_CLOSE_SFX) is played fire-and-forget by
   doorPlayCloseSound0/1 with a NULL owner; stock GE never preempts
   flag-0x12 voices, so leaked loops accumulate until new SFX are dropped.

2. sndCreatePostEvent un-stub (D138 stub removed): the stub had deleted ALL
   distance attenuation from SFX volume/pan. Verified rt=0.996, drop 0.

3. M-66b option-C expiration (user-chosen disposition for D202): new PC-only
   AL_SNDP_PORT_EXPIRE_EVT (reuses the unused 1<<13 slot in snd.h). After a
   voice starts, if it is provably ownerless (LOOPED + FINAL_IN_SEQUENCE +
   not RETRIGGER + state->state == NULL) and its wave carries an ADPCM loop
   with count == -1, post the expire at D202_EXPIRE_DELAY_US (2 s); the
   handler re-validates the predicate, fades volume to 0 over
   D202_EXPIRE_FADE_US (500 ms), and posts END_EVT. Not a reused STOP_EVT:
   its ramp uses the envelope releaseTime (~2.7 ms for sound 203) and would
   hard-cut/click. Stale-event safe because sndDisposeSound removes ALL
   pending events for a state; owned loops and retrigger sounds untouched.
   Placed in sndp (not port/src/mixer.c) because ownership is only visible
   there. Measured: pre-fix capture had the 256.7 Hz loop line flat at max
   t=39->361 s; post-fix run shows transient bursts with full silence
   between and ends allocated=0/8.
2026-09-05 19:07:32 -04:00
James Dansereau 1087820bc3 port(audio): D204 AI-queue starvation fix + GE_D204 health monitor
GE's amMain 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 -> audio runs permanently ~2 % below real time.

- F5 (the fix): 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).
- F1: osAiGetLength reports AI_LEN_REG single-buffer semantics (bytes left
  in the buffer currently playing), not full SDL queue depth.
- F3: Audio.QueueLimit 8192->2880 + non-silent drops on overflow.
- F4: oversize-block invariant guard.
- GE_D204=1 cheap audio-health monitor (rt/q/drop/max), safe to leave on;
  GE_D204_OLD restores pre-fix behaviour for A/B measurement.

Measured A/B in one binary: rt 0.980 -> 1.000, queue min 0 -> 368,
drop 0. Findings: docs/dev/findings.md D204.
2026-09-05 19:07:32 -04:00
James Dansereau c0ae2cf295 chore(ci): regenerate findings-index.csv for D195/D197-201
tools_pc/gen_findings_index.py output after the audio-findings commit.
2026-09-05 07:22:49 -04:00
James Dansereau a9cc629de6 port(audio): Phase-3 software mixer + K0_TO_PHYS/bnkf pointer fixes (D198-201)
Software RSP-audio-ucode mixer (macro-swap aXxx -> Impl functions, PD-port
pattern) makes libaudio output audible on PC. Alongside it, two pointer-width
bugs surfaced by exercising the audio path for the first time:

- D198: alAdpcmPull/_decodeChunk used K0_TO_PHYS (N64 kseg0 mask) on 64-bit
  heap pointers; switched to osVirtualToPhysical like every sibling call site.
- D201: bnkf.c bank/inst/sound/wavetable relocation offsets were s32,
  truncating the real 64-bit base pointer for ~50% of allocations; widened
  to uintptr_t.
- D200 (already committed-pending from a prior session): reverb's ALDelay
  u32 negative indices zero-extended into +4GB offsets on 64-bit; (s32)/(s64)
  casts match PD ground truth.
- D199: documents the mixer's aSetBuffer persistent-DMEM-context design,
  reconstructed from call-site pairs (no RSP disassembly to check against)
  since there's no N64 audio ucode source to reference.
- D195/D197 (playtest reports referenced by an earlier HANDOFF note but never
  written into findings.md): backfilled as OPEN with next steps.

Verified: ./build-pc.sh ntsc-final links clean; -level_09 runs 600+ frames
crash-free with music + SFX audible.
2026-09-05 07:22:21 -04:00
James Dansereau 6d6609a550 Merge pull request #27 from jkdansereau/chore/agentic-speedups-s1-4-6
chore(agentic): speed-ups plan Steps 1, 4 (N2/R8/R9), 6
2026-09-04 22:28:03 -04:00
James Dansereau a5e6cd443a fix(ci): regenerate findings-index.csv, document GE_DETERM_TRACE
The D117 findings.md addenda in the prior commit changed D117's status
text without regenerating findings-index.csv, and the new GE_DETERM_TRACE
env var wasn't yet mentioned in GE-ENV-PROBES.md -- both fail their
--check gate in CI's validate job. Caught by a real CI run, not predicted.

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:20:44 -04:00
James Dansereau 70208feb64 docs(claude.md): correct wrong local-Qwen-worker shell claim
The "worker's shell is cmd.exe, no grep/cat" note was wrong -- verified
directly against the delegate-local harness's own _run_bash: grep, sed,
cat, ls, head, pipes, and grep -rn all work (Git-for-Windows/MSYS2 on
PATH). The stale claim, baked into 4 of 5 local agent .md files, caused a
real dispatch to burn its whole turn budget full-read_file-ing files
instead of grepping them, hit the turn limit, and returned nothing.

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:11:07 -04:00
James Dansereau bf195d3eb5 port(determ): experimental GE_DETERM=1 fixed-tick mode, not yet achieved (D117)
Adds an env-gated (GE_DETERM=1, default OFF), test-only deterministic-clock
mode to port/src/libultra.c per the design in docs/dev/findings.md section D117:
osGetCount() returns a virtual clock advanced synchronously in osRecvMesg()
when __scMain (the sole consumer of the scheduler's retrace queue) asks for
the next message, instead of tracking real elapsed time. Goal: two runs of
the same build take the same simulation path, for exact frame-diff
regression testing and reproducible crash repro.

Four passes this session, each is a real, verified result, not a guess:
1. Fixed the predicted pre-first-frame deadlock (a poll-driven fallback
   caused it in the first cut).
2. Replaced the poll-driven fallback with synchronous per-ask generation
   in osRecvMesg -- fixed the deadlock cleanly, but two-run divergence was
   still 33-38%.
3. Capped the pre-first-task tick backlog to a fixed constant -- divergence
   dropped to 17-20%, still not the ~0% target.
4. Added GE_DETERM_TRACE=1 instrumented tracing and diffed two runs' clock
   event logs directly. Proved the tick mechanism is now fully
   deterministic (byte-identical clock values through the first real task).
   Root-caused the actual remaining divergence: every synthesized retrace
   increments the scheduler's frameCount, which gates a half-rate client
   (the audio client) by parity -- since the pre-task spin count is still
   real-time-scheduling-dependent, that parity isn't guaranteed to match
   between runs.

GE_DETERM is NOT achieving its determinism goal yet and is not meant to be
used for anything today -- it ships here purely as a well-documented,
zero-risk-when-off checkpoint (verify.sh bunker1 PASS, worst_cell unchanged,
confirmed after every change this session) so the next attempt starts from
proof instead of re-deriving four sessions of dead ends. Full trace numbers,
exact line citations, and the mechanism: docs/dev/findings.md section D117
(four M-52 addenda).

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:10:58 -04:00
James Dansereau 226f0490cb ci: add ccache to Linux + Windows build jobs (speed-ups plan Step 5)
CMakeLists.txt already opt-in enables CMAKE_{C,CXX}_COMPILER_LAUNCHER=
ccache when found (b49907de); CI just never installed ccache or persisted
its cache dir across runs. Wires actions/cache + ccache install into both
linux-build and windows-build, keyed on CMakeLists.txt's hash. Game
sources are frozen, so this should give a near-total hit rate after the
first run on each job.

YAML-validated; the actual hit-rate payoff needs a real CI run (cold, then
warm) to confirm -- not verifiable locally.

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:10:41 -04:00
James Dansereau 884cfdb9b7 tools(verify): add verify.sh + crash_brief.py (speed-ups plan Step 2, 9)
verify.sh wraps the manual render-verify recipe (build-if-stale, 640x480
ini pin/restore, headless GE_PCDUMP capture, pixcount, framediff, crash
symbolication) into one command with a machine-readable PASS/CRASH/
NO-FRAMES/REGRESSION verdict + --json, for a single level or a sweep
subset. Live-tested on Windows: single-level and subset-sweep runs both
produce correct verdicts and leave the tree clean.

crash_brief.py turns a ge007.crash.log into a symbolicated, dispatch-ready
investigation brief: greps porting-notes.md/findings-index.csv for
sections covering the crashing function/file, and flags known
parked-crash signatures (D188 va_list, D189 stack overrun) so agents stop
re-investigating them. verify.sh calls it automatically on a CRASH
verdict. Live-tested against a synthetic-but-real crash log built from an
actual built symbol; caught and fixed 3 bugs before trusting it (addr2line
unresolvable under native Windows Python's PATH handling, a substring
keyword-match false-positive, a Windows drive-letter path-split bug).

Claude-Session: https://claude.ai/code/session_01P3WrUHAFQZUuhrHhiJgTUr
2026-09-04 22:09:58 -04:00
James Dansereau dbea5a30fe chore(agentic): speed-ups plan Steps 1, 4 (N2/R8/R9), 6
Static/tooling slice of docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md — no build or
runtime environment needed to verify any of it.

- Step 6 / N7: add a VERIFY: line to the investigation-brief template
  (docs/dev-process.md) and the CLAUDE.md dispatch-preflight checklist — the
  brief must now name the exact probe/verify invocation that proves the task
  done, or state that runtime verification is impossible here.
- Step 4 / N2: tools_pc/README.md — indexes the ~60 scripts with a
  living vs artifact marker so closed-finding artifacts can be archived.
- Step 4 / R8: tools_pc/gen_findings_index.py -> docs/dev/findings-index.csv
  (label, bucket, one_liner, status_raw) — grep-before-you-read aid for the
  ~200 KB finding log. Heuristic parser of the §F/§H tables.
- Step 4 / R9: tools_pc/gen_env_probes.py — re-greps live getenv("GE_*")
  sites and reports NEW/GONE drift vs docs/dev/GE-ENV-PROBES.md (keeps the
  curated prose, only checks the site map). Doc reconciled: 7 new probes
  added, GE_D54 kept as a tombstone.
- CI: validate job now runs both generators with --check.

Step 1 / N1 (HANDOFF preflight) applied in the local-only docs/HANDOFF.md.
Step 2 (verify.sh) and the HANDOFF/HANDOFF-ARCHIVE trim are left for a
session with a build environment.
2026-09-04 19:21:35 -04:00
James Dansereau 5c3944582b docs: v0.1.0 alpha framing + hygiene pass (#25)
Update the public-facing docs to the M-49 state (v0.1.0 alpha, first
full-campaign playtest: 19/21 missions completable, D191 crashes Bunker ii
+ Statue, D193 AI pacing is the top flaw).

- README: lead with what it is + status, research angle second; new
  Quick start section; Status/Download rewritten for the alpha + dual
  platform; "By the numbers" refreshed (~3 weeks, ~320 commits, ~190
  findings, ~49 sessions, 69 PORT files).
- .github/release-notes.md: v0.1.0 title + known-issues rewrite.
- .github/social-preview.html: status/stat lines (regen + re-upload the
  PNG separately).
- docs/index.md, docs/dev/LEVEL-STATUS.md: M-49 status.
- docs/dev/findings.md, docs/dev/GRAPHICS-BACKLOG.md: D191-D197 +
  D148/D170/D173 M-49 escalations.

Hygiene:
- Fix the `&amp;` literal in internals.md H1.
- Repoint two ../README.md links that 404 on the Pages site.
- Standardise British -> American spelling across the public docs
  (behaviour->behavior, colour->color, catalogue->catalog,
  centre->center, etc.). Deep internal logs left untouched.
- .gitignore: document the local-notes convention (docs/HANDOFF.md,
  docs/BACKLOG.md, and a new docs/dev/notes/ drop for roadmaps and
  session plans) so it survives a fresh clone.
2026-09-04 00:13:43 -04:00
James Dansereau 86440e3b58 fix(linux): survive the decomp's latent stack-buffer overruns (D189) (#24)
* fix(linux): survive the decomp's latent stack-buffer overruns (D189)

The decompiled game code has a class of fixed-size stack-buffer over/under-
runs that write a few words into adjacent stack scratch:

  * stan.c  sub_GAME_7F0B1DDC  — `StandTile *tileStack[39]` with a bail check
    of `if (cat >= 41)` that only runs once per outer iteration, so `cat`
    reaches 40 + pointCount and the append writes several slots past the end.
    BUNKER1 (-level_09) prop room-list setup legitimately visits ~40 tiles.
  * bondview2.c  bondviewCalcIntroSwirlCamera  — `f32 pointbuf[10]` indexed
    `&pointbuf[i*3]` for i in [-1..2] then written [0..5], i.e. [-3 .. 11].

On the N64 and on the MinGW/Windows build (neither uses a stack protector)
these are harmless in practice — the Windows build is stable over thousands
of frames. Only Ubuntu's gcc default (`-fstack-protector-strong`) turns them
into a fatal `__stack_chk_fail`, at BUNKER1 stage load and again at the intro
swirl camera (~frame 5).

Two-part fix:
  * `-fno-stack-protector` globally (CMakeLists) — matches the N64 and MinGW
    builds so the same code runs the same way. A hardened Linux build would
    need the whole overrun class swept first.
  * `StandTile *tileStack[64]` under `#ifdef PORT` (stan.c) — the one case
    that is cleanly understood and bounded (`pointCount` is 4-bit, so 40+15
    is the true worst case). Pure local scratch, never escapes, no behaviour
    change. N64 keeps `[39]` under `#else`.
  * stan.c:2225 link-resolve also switched to the D177 PORT_PTRADD macro for
    idiom consistency (the blob maps below 4 GiB so the bare cast is
    currently lossless; #else is byte-identical).

Stacked on the D188 xprintf fix. Verified: Windows -level_09 framediff 3/3
within golden threshold (all changes are #ifdef PORT or no-op on MinGW);
Linux -O2 -level_09 now runs to frame 2100+ with no crash.

* docs: D189 update (stack-protector / latent overrun class) + porting-notes D8

* docs: D190 — Linux -level_45 objective.c load crash (open); D189 sweep 5/6
2026-09-03 20:18:25 -04:00
James Dansereau 1261389a5c fix(linux): thread a va_list* through _Printf (x86-64 SysV ABI) (#22)
* fix(linux): thread a va_list* through _Printf (x86-64 SysV ABI)

On the x86-64 System V ABI `va_list` is an array type, so a by-value
`va_list` parameter decays to a pointer. `_Printf()` was passing `&args`
(the address of that local pointer, not the argument list) to `_Putfld()`,
which does `va_arg(*args, ...)` — so `_Putfld` walked garbage and
segfaulted. Latent at `-Og`, fatal at `-O2` (PR #9), which is why the
Linux release build crashed at boot:

  bossInitMainthreadData (boss.c:247, the D121 per-level -m token sprintf)
    -> sprintf("-level_%c%c %s", ...) -> _Putfld -> SIGSEGV @ xprintf.c

Same UB class as D187. Windows x64's `va_list` is a scalar (`char*`), so
`&args` happened to work there — the Windows build is unaffected.

Fix: under `#ifdef PORT`, `_Printf` takes `va_list *` and passes it
straight to `_Putfld` (which already expects `va_list *`); `src/sprintf.c`
(the only PC caller — `rmon.c` is excluded from the build) passes `&args`
of a real `va_list` object. N64 build untouched (`#ifdef PORT`, and it
byte-matches the ROM).

Verified:
- Windows: build links, `-level_09` framediff 3/3 within golden threshold.
- Linux -O2: boot crash gone, now advances past bossInitMainthreadData
  into stage load (uncovers a separate pre-existing stan.c stack overflow
  during BUNKER1 prop setup — tracked separately).

* docs: D188 (xprintf va_list ABI) + D189 (stan.c stack-smash) findings + porting-notes D7
2026-09-03 20:12:56 -04:00
James Dansereau bde5e0bdfa docs(release): drop fixed items from the alpha known-issues list (#21)
The HUD weapon/ammo-type icon (D187, PR #19) and blank briefing objective
text (D178, cd1ed574) are both fixed and on main. Remove them from the
release-notes and bundled-README known-issues blurbs so a v0.1.0 tag ships
an accurate list. Black sky and the front-end 3D models (Nintendo logo,
MISSION COMPLETE / mode-select) are still open and stay listed.
2026-09-03 16:37:10 -04:00
James Dansereau ed6e677d06 build(release): fall back to 7z / Compress-Archive when zip is absent (#20)
A stock MSYS2 MINGW64 shell ships neither `zip` nor `unzip`, so
`tools_pc/bundle-win.sh` failed at the packaging step for a local release
build (the M-45 handoff's documented v0.1.0 cut path). CI is unaffected
(it `pacman -S zip unzip`, so the `zip` branch is still taken).

Fallback order: zip -> 7z on PATH -> "C:\Program Files\7-Zip\7z.exe" ->
powershell Compress-Archive. Verified end-to-end: bundle builds, unpacks
to a clean dir, prepare-assets runs, ge007.x86_64.exe boots -level_09 to
frame 2100+ crash-free.
2026-09-03 16:36:42 -04:00
James Dansereau 368062e319 Merge pull request #19 from jkdansereau/fix/hud-weapon-icon-missing-return
fix(hud): restore missing return → weapon/ammo-type icon draws again
2026-09-03 15:20:31 -04:00
James Dansereau 187d792e23 Merge branch 'main' into fix/hud-weapon-icon-missing-return 2026-09-03 15:04:54 -04:00
James Dansereau ac7223959f docs(hud): D187 finding + porting-notes D6 (missing-return / -O2 class)
M-47 root-cause + interactive-console verification for the missing HUD
ammo-type icon: framediff -level_09 frame 320 worst-cell dmean 21.8 -> 4.0
with the one-line AVOID_UB return; orange clip glyph confirmed present
again. porting-notes D6 covers the non-void-no-return / latent-until-O2
pattern.
2026-09-03 15:04:16 -04:00
James Dansereau a9d8e4d44c fix(hud): restore missing return in set_rgba_redirect_generate_microcode
The HUD weapon/ammo-type icon (bottom-right, and bottom-left when
dual-wielding) stopped drawing ~2026-09-02; the ammo *numbers* still
render. Root cause: set_rgba_redirect_generate_microcode (gunfire.c, the
sole icon emitter) is non-void but has no return statement (the decomp
banner flags it). At -Og the tail call's result stayed in $v0 and it
worked; commit 7549570b restored -O2 for playtest builds, where the
undefined return register makes callers resume the DL from the icon's
start offset and overwrite the icon commands with the following
microcode_constructor / gunDrawHudInteger output.

Fix: add the return under AVOID_UB (already -D'd in the PC build;
CMakeLists.txt:207), N64 byte-match path unchanged under #else.
Codegen-correctness only, same class as the existing AVOID_UB edits.

Owed: build (-O2) + -level_09 capture diff vs docs/img/bunker1-2.png.
A/B: Debug build (-Og) shows the icon, -O2 build without this fix does not.
2026-09-03 13:11:42 -04:00
James Dansereau fcc23dd584 Merge pull request #17 from jkdansereau/release/v0.1.0-alpha-dual-platform
release: Linux x86-64 bundle + dual-platform alpha release notes
2026-09-03 12:13:32 -04:00
James Dansereau 174233f89f ci: mark bundle-linux.sh executable
Committed without the +x bit from a Windows checkout -> CI hit
"Permission denied" (exit 126) invoking tools_pc/bundle-linux.sh.

Claude-Session: https://claude.ai/code/session_01V4pK3pT83YnPsBp3Ey4RP2
2026-09-03 11:50:29 -04:00
James Dansereau 7a3791b533 Merge pull request #16 from jkdansereau/ci/docs-pr-mergeable
ci: make docs-only PRs mergeable
2026-09-03 10:55:22 -04:00
James Dansereau a0611727e9 release: add Linux x86-64 bundle + dual-platform release notes
- tools_pc/bundle-linux.sh: package a Linux build (exe + README + licenses +
  prepare-assets) as a .tar.gz. Mirrors bundle-win.sh's ROM / size guards and
  prepare-assets assembly. No shared libs bundled — the generated README tells
  the user to install SDL2 / zlib / libGL from their distro.
- tools_pc/dist/README.md.in: made OS-neutral via @PLATFORM@ / @EXE@ / @DEPS@ /
  @LICENSE_EXTRA@ tokens; refreshed the known-issues list (dropped the stale
  "ladders don't work" line — D177 fixed M-36; added black sky + missing HUD
  weapon icon).
- .github/workflows/ci.yml: linux-build now packages + uploads the Linux
  bundle; the release job attaches both platforms to the draft pre-release.
- .github/release-notes.md: rewritten for the two-platform NTSC-U alpha with
  the prepare-assets step and current known issues.

Alpha scope: NTSC-U only, one-time prepare-assets step (Part A launcher and
Part B native converter from release-dropin-rom-plan.md remain follow-ups).

Claude-Session: https://claude.ai/code/session_01V4pK3pT83YnPsBp3Ey4RP2
2026-09-03 10:54:06 -04:00
James Dansereau e886b970d7 ci: make docs-only PRs mergeable
Drop `paths-ignore` from the `pull_request:` trigger. With it, a docs-only
PR never triggered the workflow, so the required `validate` / `windows-build`
checks stayed "expected" forever and the PR could not be merged without an
owner bypass (M-44 hit this on #14/#15).

The workflow now always runs on PRs; the `changes` job diffs the PR against
its base sha (mirroring the existing branch-push logic against
`github.event.before`) and, for a docs-only change, reports the build jobs
as skipped — which branch protection accepts as success.

Claude-Session: https://claude.ai/code/session_01V4pK3pT83YnPsBp3Ey4RP2
2026-09-03 10:41:41 -04:00
James Dansereau 362a386650 Merge pull request #14 from jkdansereau/docs/m42-graphics-honesty
docs: D176(a) -level_22 headless repro; retire stale D74 wrap-block row
2026-09-03 10:19:59 -04:00
James Dansereau 7d90aca7d3 Merge remote-tracking branch 'origin/main' into docs/m42-graphics-honesty
# Conflicts:
#	docs/dev/findings.md
2026-09-03 10:18:06 -04:00
James Dansereau ae29fe2b3b Merge pull request #15 from jkdansereau/docs/d176a-sky-stream-spec
docs(D176a): full RDPHALF sky-tri stream spec + implementation paths
2026-09-03 10:17:00 -04:00
James Dansereau 2b7d3d5df4 Merge branch 'main' into docs/d176a-sky-stream-spec 2026-09-03 10:16:50 -04:00
James Dansereau 70d8ebdd4d Merge pull request #10 from jkdansereau/feat/f10-options-overlay
port: D184 — F10 in-game options overlay
2026-09-03 10:09:55 -04:00
James Dansereau 17e6e1ffb3 Merge pull request #11 from jkdansereau/feat/alpha-asset-prep-tool
feat(release): self-contained asset-prep tool for the alpha bundle
2026-09-03 10:09:36 -04:00
James Dansereau ccff1374b5 Merge branch 'main' into feat/f10-options-overlay 2026-09-03 10:03:23 -04:00
James Dansereau fdf34a7dcd Merge branch 'main' into feat/alpha-asset-prep-tool 2026-09-03 10:03:05 -04:00