Commit Graph
60 Commits
Author SHA1 Message Date
James Dansereau 7601b9db49 fix(audio): D208 — player fire sound goes permanently silent (field_A48 ptr width)
Root cause of the "doesn't recover after the alarm stops" half of the user's
D207 report (distinct bug from D207's voice-pool question).

struct field bondview.h:200 `s32 field_A48` is used as a SECOND
ALSoundState* sound-handle slot for the player's double-buffered per-hand
fire sound (gunfire.c:3186-3200). sndPlaySfx stores the new state through
`&field_A48` as an 8-byte `pendingState->link.next = nextState`; as s32 that
write tears into the adjacent field_A4C (the auto-fire cadence timer, rewritten
every frame). After enough double-buffered fire cycles field_A48 holds a value
that is neither 0 nor a valid pointer, so the `else if (field_A48 == 0)`
re-arm gate never fires again and no further player gunshot is issued.

Evidence: user Bunker audiotrace.log — silenced-PP7 idx-46 sndPlaySfx requests
stop dead at log line 18930/25086 while all other SFX continue; the player-gun
slot 0x70076630 is then caught holding a torn value 0x000001db00000000
(field_A48=0 + field_A4C=0x1db read as one word).

Fix (D3x ABI/layout, #ifdef PORT): declare field_A48 as ALSoundState* under
PORT, s32 verbatim under #else. hands[] is runtime-only state (g_CurrentPlayer),
never ROM-serialized, and all downstream fields are accessed by member name —
widening 4 bytes is layout-safe. The (struct ALSoundState *) casts at the use
sites become no-ops and stay for a minimal diff.

Build clean (gunfire.c + full link). Runtime verification owed: replay the
Bunker firefight, confirm idx-46 requests continue for its full duration.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 08:06:28 -04:00
James Dansereau 2298756b70 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 842d73a799 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 d56c15d9df 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 c25285d8c5 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 378386d19d 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 2645618a4b 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 0181c3f2bf 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 9596eb58b4 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 4c460e551a 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 1ac92c7d25 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 35f9bdda91 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 02fc231043 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 6eb7ceeee6 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 34b4040948 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 f24d3df503 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 2aac809c4b 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 14b2c74b65 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 7aba3637c9 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 `&` 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 881cbf5b84 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 08feb5c273 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 df321d7685 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 8f9bb7a6f3 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 ea29172548 Merge branch 'main' into docs/d176a-sky-stream-spec 2026-09-03 10:16:50 -04:00
James Dansereau b6c5e4b3f7 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 08b49d1820 Merge branch 'main' into feat/f10-options-overlay 2026-09-03 10:03:23 -04:00
James Dansereau c9b3d6f424 Merge branch 'main' into feat/alpha-asset-prep-tool 2026-09-03 10:03:05 -04:00
James Dansereau 41ee4a3fc9 Merge branch 'main' into docs/d176a-sky-stream-spec 2026-09-03 10:03:02 -04:00
James Dansereau 3314dfa834 Merge branch 'main' into docs/m42-graphics-honesty 2026-09-03 10:00:52 -04:00
James Dansereau 163a215036 Merge branch 'main' into feat/f10-options-overlay 2026-09-03 01:15:26 -04:00
James Dansereau ea0f0a398a docs(D176a): full RDPHALF sky-tri stream spec + two implementation paths
The M-37 scratch note (D176a-sky-rootcause.md) was never committed. Re-derive
the complete format from src/game/sky.c and include/PR/gbi.h:

- skyRenderTri/skyRenderFull emit a verbatim N64 RDP triangle command
  (2-word header + 3 edge-slope pairs + 16-word shade block + 16-word
  texture block) chopped into gImmp1(G_RDPHALF_1/_CONT) pairs, terminated
  by G_RDPHALF_2. GE's RSP ucode DMAs it straight to the RDP.
- sub_GAME_7F097388 already yields complete screen-space verts; the
  projection math is endian-clean and is not the defect.

Two implementation paths documented with a recommendation (Path B: an
#ifdef PORT emit swap in sky.c, first; Path A: an RDP-tri decoder in
gfx_pc.cpp, as the long-term answer). Word layout cross-checked against
gbi.h packing conventions.
2026-09-03 01:13:47 -04:00
James Dansereau 4f1a02a08e port: D186 — refuse a sub-30 Video.FpsCap instead of throttling the sim
sync_framerate_with_timer() runs inline on the scheduler thread
(osSpTaskStartGo -> gfx_run -> swap_buffers_begin), so a low frame cap
blocks VI-retrace delivery and drags every game thread down to the cap
rate rather than just dropping presented frames. There is no N64 analogue
(RSP/RDP/VI are separate silicon; the CPU never waits on them).

Clamp 0 < FpsCap < 30 to 0 (uncapped) with a warning, at both:
  - videoInit, so a bad ge007.ini value (e.g. dinged to 10 via the F10
    options overlay) is normalised and persists sane on next configSave();
  - gfx_sdl_set_target_fps, the fast3d chokepoint, which also covers the
    overlay's live set path.

Caps >= 30 are unchanged. Default (FpsCap = 0) is byte-identical to
golden (-level_09 framediff 3/3). The real fix — moving frame pacing off
the scheduler thread — is documented as owed in findings.md D186.
2026-09-03 00:51:40 -04:00
James Dansereau 76dd2d5cab docs: D176(a) -level_22 headless repro; retire stale D74 wrap-block row
- findings.md / GRAPHICS-BACKLOG.md: -level_22 (Statue, night) is a clean
  no-input headless repro of the D176(a) black sky — bare boot, frame ~360,
  upper half pure black. Best verification target for the RDPHALF sky-tri
  decoder. Note -level_36/-43 intro cams face the terrain (not a sky test
  without GE_INPUTSCRIPT); -level_29 top-of-frame is dim, not black.
- GRAPHICS-BACKLOG "D74 wrap-block": struck through — the dead-code / OOB
  in-vertex-loop wrap block was reworked and hoisted to a per-texunit
  pre-wrap window behind Video.WrapFix at M-30 (RC3/D167). No longer latent.
2026-09-03 00:49:38 -04:00
James Dansereau bda123e75d Merge branch 'main' into feat/linux-ci-compile 2026-09-03 00:13:40 -04:00
James Dansereau 3d537a4eee port(romdata): POSIX fixed-address ROM map (mmap) for the Linux build
The cart-base map at 0x10000000 (so absolute ROM-asset symbols are live host
pointers) was Windows-only; the #else branch was a stub that logged
"not implemented" and fell to the degraded heap-copy path.

- Factor the post-map work (memcpy + pcmodels/pccg sidecar load + D55 RLE
  header fixup) into romdataFinishCartMap(), shared by both paths.
- POSIX #else: anonymous mmap at CART_BASE with MAP_FIXED_NOREPLACE where
  available; on any failure munmap + fall through to the identical heap copy.
- romdataDestroy() munmaps on POSIX (length tracked in mappedLen).

Verified on Windows: builds 242/242, -level_09 boots crash-free (identical
behaviour with/without the refactor). Linux compile + runtime verification
owed — see docs/dev/LINUX-PORT.md. No behaviour change on Windows.
2026-09-02 21:15:01 -04:00
James Dansereau f9adfe51c8 docs(D185): resolved — missing d88 step, not a d69 regression 2026-09-02 21:00:00 -04:00
James Dansereau 31e1b9d8c5 docs(D185): fresh d69_emit.py run produces a crashing pccg sidecar — alpha blocker 2026-09-02 20:55:22 -04:00
James Dansereau c53da6b63e port(romdata): POSIX fixed-address ROM map (mmap) for the Linux build
The cart-base map at 0x10000000 (so absolute ROM-asset symbols are live host
pointers) was Windows-only; the #else branch was a stub that logged
"not implemented" and fell to the degraded heap-copy path.

- Factor the post-map work (memcpy + pcmodels/pccg sidecar load + D55 RLE
  header fixup) into romdataFinishCartMap(), shared by both paths.
- POSIX #else: anonymous mmap at CART_BASE with MAP_FIXED_NOREPLACE where
  available; on any failure munmap + fall through to the identical heap copy.
- romdataDestroy() munmaps on POSIX (length tracked in mappedLen).

Verified on Windows: builds 242/242, -level_09 boots crash-free (identical
behaviour with/without the refactor). Linux compile + runtime verification
owed — see docs/dev/LINUX-PORT.md. No behaviour change on Windows.
2026-09-02 20:54:20 -04:00
James Dansereau ea08d5698f docs: drop-in-ROM release plan, mermaid rendering, Qwen dispatch guide
- docs/dev/release-dropin-rom-plan.md: proposal for a 'download zip, drop
  in your ROM, double-click' release — covers the romassets/pcmodels/pccg
  sidecar gap (D179), a native first-run converter, CI and region handling.
- docs/_includes/head-custom.html: render fenced mermaid blocks on the
  docs site (kramdown code fence -> mermaid.run, theme-aware).
- CLAUDE.md: 'Local Qwen dispatch' section — tools, worker list, the
  dispatch rubric, backend reality, and the cmd.exe spot-check quirk.
2026-09-02 20:17:10 -04:00
James Dansereau 2a39743117 docs: correct download status — no pre-built binaries yet
Nothing runs end to end (Phase 2 WIP), so the 'pre-built Windows bundles
are published under Releases' language in the README and site was
inaccurate. Reword to: no binaries yet, build from source; bundles may
follow once the port is playable. Also rename the local model Qwen 3 ->
Qwen 3.8 in the docs to match reality.
2026-09-02 20:17:10 -04:00
James Dansereau 4acadadaf9 docs(D176): root-cause D176(a) black sky (RDPHALF no-op); D176(b) M-37 update 2026-09-02 19:07:55 -04:00
James Dansereau 71659ec26c port: D184 — F10 port-layer options overlay (approach C)
New port/src/optionsoverlay.c + port/include/optionsoverlay.h: an
immediate-mode options overlay drawn as its own fast3d 2D display list,
appended after the game DL. Implements docs/dev/OPTIONS-MENU-PLAN.md §2/§3.

- config.c/h: configForEachOption() + configSetOptionMeta() label/step/enum
  side table; CONFIG_OPT_* enum. config.c stays free of specific-key knowledge.
- gfx_pc.cpp gfx_run(): one-line hook runs optionsOverlayEmit() after the game
  DL, before gfx_flush. Returns NULL when closed -> zero bytes appended, golden
  dumps byte-identical.
- video.c: F10 toggles (videoPumpEvents, next to F12); ESC closes; wheel
  scrolls; videoRequestLiveConfig()/liveCfgDirty re-applies VSync/FpsCap/
  TextureFilter at frame start.
- input.c: while open, controller 0 returns a neutral pad and nav
  (arrows/enter/wheel/mouse) routes to optionsOverlayHandleInput() — mirrors
  the D180 WI-1 swallow pattern.
- v1 rows: VSync, FpsCap, MSAA(restart), TextureFilter, MouseAimSpeed,
  MouseTurnSpeed, MouseInvertY, MouseCaptureMode, ScreenShakeIntensity.
  Live-apply where possible; configSave() on close.
- GE_OPTIONSOVERLAY=1 auto-opens at boot (env-gated diagnostic, left in).

Port-layer only, no src/ edits, no #ifdef PORT in game code. Build links
241/241. Runtime framediff/soak/eyeball owed on an interactive machine
(this headless env kills the GUI process early and writes 0-byte PPMs).
2026-09-02 19:03:37 -04:00
James Dansereau 3c419e8d91 docs(D177): mark ladders closed — PR #5 merged, climb test confirmed 2026-09-02 18:51:23 -04:00
James Dansereau 4b6eb3b5dd port: D177 — ladders non-functional (progression blocker), pointer-width ABI
MoveBond gates the entire ladder-collision path on
stanGetLocusCount(&curLocus), which was pinned at 0 on PC. Three compounding
pointer-width bugs in the stan navigation layer (same class as D79/D90),
no game logic touched:

1. stanCheckLinkedSpecialTile receives the caller's
   struct StandTileLocusCallbackRecord typed as `s32 *outFlags` and writes
   the LADDER signal with raw `outFlags[1] = 1`. The record's first member
   is `s32 *rooms` — 8 bytes on PC — so `[1]` is its high half and `count`
   (moved +4 -> +8) is never written. stanGetLocusCount() reads ->count,
   stays 0, ladder path never runs. (FORCECROUCH survived by luck: LE low
   half of rooms is at +0.) Fix: #ifdef PORT cast to the real struct, write
   ->rooms / ->count by name.

2. curLocus was declared `struct move_bond_temp_struct` — an 8-byte
   "placeholder while matching" that equals the N64 record size but is too
   small for the widened PC record, so stan's field writes overflowed the
   local. Fix: #ifdef PORT declare it as the real
   struct StandTileLocusCallbackRecord.

3. stanGetMoveBondCollisionTiles would AV on the first climb:
   `(coord3d *)((s32)coords + off)` truncates the 64-bit &bondCollision
   stack pointer. Fix: PORT_PTRADD (uintptr_t arithmetic) added to stan.c.

All #ifdef PORT; N64 path kept verbatim under #else. Candidate root-cause
from an M-36 subagent (parts 1/3); part 2 found on review.

Verified: build links clean; -level_09 framediff 3/3 (frame_200 run
variance only); -level_36 (Surface, mandatory ladders) boots crash-free;
interactive climb confirmed working in-game by the owner.

findings.md D177 (§F + §H), porting-notes.md §A.
2026-09-02 18:26:38 -04:00
James Dansereau 809e7378e1 port: D176(a) Surface black-sky investigation + GE_D176 probe (M-36, partial)
Not root-caused; narrowed substantially. Headless -level_36 + GE_D176 probe
(bgfog.c fogLoadLevelEnvironment, sky.c skyRender):

- Environment data is CORRECT: fog_tables[] matches Id=36, Sky.Clouds=1,
  warm CloudRGB=240,120,30, SkyImageId=0. Not a serialized-struct
  byte-order bug (fog_tables is a compiled C initializer).
- skyRender runs the cloud path; corner classification is correct
  (switch case 12, "sky is top half").
- But skyRenderTri/skyRenderFull emit nothing - not even the env->RGB
  fill shows. Sky polys degenerate after projection, culled, or the
  skywaterimages[0] bind is failing.

Next (findings.md "D176(a)"): probe sub_GAME_7F097388 projected coords;
verify skywaterimages resolves on PC (image_bank.c GIMG offset 0xFB4);
fast3d sky-tri trace; cross-check Dam/Statue exteriors.

GE_D176=1 probe left in tree (#ifdef PORT, env-gated, inert). Build
240/240 links clean; -level_09 framediff 3/3.
2026-09-02 18:04:36 -04:00
James Dansereau 4069677b82 docs: M-36 triage — record Family A/B outcomes (B fixed, A disproven)
Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 11:01:06 -04:00
James Dansereau 4b71435d73 port: D183 — fast3d source-pitch de-stride + texture diagnostics (M-36 Family A)
Family A of the M-36 triage (Surface "grey static" D176b, file-select
interlacing D182-2). The assigned hypothesis — import_texture_* shear when
line_size_bytes != full_image_line_size_bytes — is DISPROVEN for the Surface
repro:

- Only gfx_dp_load_tile produces a strided load; GE loads every texture via
  gDPLoadBlock (tex.c texWriteLoadToTmem*), which sets line == full. Measured:
  0/64 GE_DTEX + 0/166 GE_TEXDUMP imports strided on -level_36; 0 in the
  front end either.
- Independent offline pitch scan (new GE_TEXRAW dump) shows the importers
  pick exactly the pitch the source is stored at. The Surface cliff texture
  is a 32x32 IA8 whose ROM bytes are genuinely a noise field — decoded
  faithfully, tiled ~1 texel/px. D176(b) is likely a tiling-density / UV /
  shift-LOD issue (RC3/D167 family) or a wrong-texture bind, not a decode
  bug. Full evidence: findings.md D183. D176(b)/D182(2) stay OPEN.
- Re-confirmed D159's texSwapAltRowBytes no-op is correct (re-applying it
  makes the dumps worse).

Shipped anyway (golden-safe, reversible):
- import_texture() compacts strided rows into a contiguous buffer before the
  format dispatch, so every importer sees line == full — the behaviour the
  seven SUPPORT_CHECK(full == line) asserts only assert (and assert compiles
  out under NDEBUG). Provably inert when full == line (all current loads).
  GE_TEXPITCH=0 restores the old flat read.
- GE_DTEX gains line=/full=/STRIDED markers; GE_TEXRAW=1 dumps raw
  importer-input bytes for offline decode-vs-source analysis.

Verified: build 240/240; -level_09 framediff 3/3; GE_STARTMENU=10 briefing
crash-free, renders. Investigation: subagent (worktree).

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 11:00:11 -04:00
James Dansereau cd1ed57449 port: D178 — byte-swap the raw briefing segment (blank objectives / briefing text)
Family B of the M-36 triage. The Ubrief*Z segment (assets/obseg/brief/) is
a raw 48-byte ROM image of struct BriefStruct { u16 brief[4]; struct { u16
textid; u16 enabled_difficulty; } objective[10]; }, loaded by front.c
load_briefing_text_for_stage() via _fileNameLoadToAddr() with no converter
and no BE->LE fixup. On the LE host all 24 u16 read byte-swapped:

- textid 0x2C04 (getStringID(LDAM,4)) -> 0x042C -> langGet() resolves an
  unloaded bank -> NULL -> blank objective/briefing text. This is the whole
  of the D143 "briefing text blank" residual (the bank loaded fine; the ids
  into it were swapped).
- enabled_difficulty 0x0001 -> 0x0100 = 256, so `selected_difficulty >=
  enabled_difficulty` is always false -> difficulty-gated objectives vanish
  (on Agent only the one AGENT-difficulty line survived -> lone "a." bullet).

Fix: romdataFixupBriefing() in port/src/romdata.c, called from a #ifdef PORT
block in load_briefing_text_for_stage() right after the load — same shape as
langFixupLoadedBank(). BE-serialized-struct reconciliation (AGENTS.md #2),
semantics-preserving, N64 build untouched. GE_D178=1 dumps raw + fixed words.

Verified: Dam briefing renders all four objectives at 00 Agent with correct
difficulty gating and "007: James Bond" header; -level_09 framediff 3/3;
GE_STARTMENU=7/=13 crash-free; build links clean 240/240.

Investigation + patch: subagent (worktree). Suspect 2 (Gfx-width temp_s0
arithmetic) was ruled out — benign 512-byte gap, langLoadToBank records the
bank base itself. Follow-ups noted in findings.md: annotate D143 as
superseded; latent 64->32 pointer truncation at front.c:6569 / :2202.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 10:56:03 -04:00
James Dansereau 9a69d57afd docs: M-36 triage sweep — bug families, correct the D159 mis-attribution
Headless GE_PCDUMP (-level_36, GE_STARTMENU=10) + static read of the
implicated port code. Two headline findings:

- The "comb interlacing" (D176b, D182-2) is NOT the D159 family.
  texSwapAltRowBytes is a full #ifdef PORT no-op (image.c:2199) and
  cannot cause anything. Real cause: gfx_pc.cpp import_texture_* assume
  line_size_bytes == full_image_line_size_bytes and shear non-PoT /
  sub-rect tiles into diagonal noise. New "Family A". Backlog D176/D182
  corrected.

- D178 briefing objectives are BLANK, not wrong-listed: header renders,
  then just "a." with no objective text. That is the D143/D151
  langGet-NULL blank-text family ("Family B"), not a propDef walk desync.

docs/dev/M-36-TRIAGE.md: full family table (A fast3d texture pitch,
B string bank, C D75 models [defer], D blood/particle, E transient
hang [defer], F misc). Recommends Families A and B as the two
high-ROI targets, disjoint enough for two subagents.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 10:06:56 -04:00
James Dansereau ef10985343 docs: sidecar build step + D176/D182 Surface/file-select bug reports (M-35)
- building.md: document the d43_emit.py / d69_emit.py sidecar generation
  as an explicit required step (4), split Run to step 5. A plain clone
  needs no ROM to build but the sidecars are required to run (D179).

- GRAPHICS-BACKLOG.md: expand D176 (Surface exterior: black sky +
  comb-interlaced tree-line, screenshots) and add D182 (SELECT FILE
  background degrades after navigate-away-and-back: Bond image stops
  drawing + comb interlacing). Both observed this session, not fixed.
  D176(b) and D182(2) interlacing share the D159 texSwapAltRowBytes
  family recurring on texture re-import.

- docs/img/bugs/: d176 our-build vs reference, d182 backout capture.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 09:54:25 -04:00