Commit Graph
237 Commits
Author SHA1 Message Date
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 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 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 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 ccff1374b5 Merge branch 'main' into feat/f10-options-overlay 2026-09-03 10:03:23 -04:00
James Dansereau 203ceaf717 port: fix linux-build link error in optionsoverlay (_SHIFTL)
gbi.h's gDP* DL macros use _SHIFTL/_SHIFTR without defining them; game TUs
pull them in via <ultra64.h>. This is the first port/src TU to build a
display list, and on GCC/ld the missing macro became an undefined symbol
(MinGW's include chain happens to provide it). Define the two pure macros
locally, verbatim from PR/mbi.h -- including mbi.h itself drags in N64 OS
headers that shadow libc.
2026-09-03 09:59:50 -04:00
James Dansereau 76fb7b7a25 port: D184 F10 overlay v2 — mouse control + resolution/fullscreen rows
- Fix invisible text: drawText passed the measured text w/h into textRender's
  width/height args, which are the on-screen CLIP rect (not the text size), so
  every glyph failed the clip test. Pass the full 2D viewport like bondview2.c's
  debug-text path.
- Mouse UI: hover-to-highlight; left-click in the value column toggles/cycles;
  drag sliders; red X close box (clicking a row's label only focuses it, so
  selecting can't flip a toggle). inputSuspendForOverlay() forces the OS cursor
  free + visible while the overlay owns the mouse.
- New rows: Fullscreen (live) and Resolution (windowed presets filtered to the
  desktop, live resize + recenter). SDL window ops are posted from the
  scheduler thread and drained host-side in videoPumpEvents.
- Layout recomputed for the wide BankGothic font: right-aligned values, wider
  row pitch, all 13 rows fit the ~240-unit 2D space; nothing clips.

Port-layer only. Links clean; runtime-verified on a real display (front end +
-level_09): layout, hover/click/drag, live VSync/filter/fullscreen/resolution.
2026-09-03 09:54:21 -04:00
James Dansereau 2d7add1ef9 Merge branch 'main' into feat/f10-options-overlay 2026-09-03 01:15:26 -04:00
James Dansereau 7863ec096f 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 ff957c21fc linux: quiet the false-positive kernel heartbeat watchdog
portHeartbeatCheck fired on any >3s frame gap and re-fired every 2s,
spamming ge007.crash.log with thread dumps during gdb pauses, level
loads, and SIGTERM teardown on a slow/software-GL setup. Widen to 8s and
stop after 3 reports. A genuine hang is permanent so it still gets caught.
2026-09-03 00:00:07 -04:00
James Dansereau 20cd247084 fast3d: reliable pre-swap frame capture + fb_id 0 = window
- gfx_opengl_start_draw_to_framebuffer: fb_id 0 now binds GL framebuffer 0
  (the window) instead of framebuffers[0].fbo, a generated attachment-less
  FBO. Harmless on Windows (verified: -level_09 renders, mean RGB ~80);
  strict GL (Mesa) treats the incomplete FBO as an error and drops draws.
- GE_PCDUMP / F12 screenshots move to a pre-swap hook (gfx_pre_swap_hook,
  called from gfx_sdl_swap_buffers_begin before SDL_GL_SwapWindow). Reading
  the back buffer AFTER the swap is undefined on buffer-exchange drivers
  (Mesa/WSLg) -> black capture. Also: read GL_BACK explicitly, glFinish
  first, GL_PACK_ALIGNMENT=1 (639*3 is not 4-aligned -> row shear), and log
  any glReadPixels error.

Windows -level_09 verified: renders, no crash, capture non-black.
2026-09-02 23:54:35 -04:00
James Dansereau 7b9e078991 linux-runtime: declare getenv()/realloc() for the strict-host build
The N64 <stdlib.h> stub declares neither. MinGW's other CRT headers leak
them; a strict host GCC (Linux/macOS, -std=gnu11) does not, so getenv()'s
64-bit char* return is assumed int and truncated -> configGetFrameDump
(GE_PCDUMP path) dereferenced a low-32 pointer and segfaulted. Add K&R
decls: pc_protos.h (D38 mechanism -> game TUs via <ultra64.h>) and the
port/shim/stdlib.h non-Windows branch (-> port TUs that #include
<stdlib.h>). K&R form stays compatible with the real prototypes; malloc/
calloc/free are already covered elsewhere so not repeated.
2026-09-02 23:03:46 -04:00
James Dansereau fe3d543647 linux-runtime: BUNKER1 boots + renders on Linux
First runtime bring-up on Linux (Ubuntu, gcc-14). -level_09 now renders
past frame 1200 crash-free with a clean shutdown, matching Windows.

- port/src/romdata.c: no-PIE loads the image at 0x400000, so the D34
  'clear low 32 bits of a symbol address' gives 0 -> false 'image base not
  4GiB-aligned' abort. The ANIM_DATA_* lvalues are only ever address-taken
  (never dereferenced), so fall back to a fixed 4GiB-aligned sentinel
  (0x100000000) for g_pc_animdata_base.
- port/src/main.c: the 'image must load at 0x140000000' assertion is
  PE-specific (.refptr base relocations); ELF no-PIE resolves dram_syms.s
  absolute symbols literally regardless of load address. Guard it to
  PLATFORM_WINDOWS.
- port/src/libultra.c: mmap game-thread stacks with MAP_32BIT so they sit
  in the low 2GiB. The decomp truncates stack-buffer pointers to u32 (e.g.
   in texLoad); glibc pthread stacks live above 4GiB and
  those truncations corrupt. One fix covers the whole idiom class. Windows
  pthread stacks are already low.
- src/game/image.c: belt-and-braces #ifdef PORT -- align compbuffer at
  uintptr_t width (identical result on N64).
- CMakeLists.txt: drop the -Ttext-segment experiment (needs -mcmodel=large;
  breaks R_X86_64_32 relocs) -- comment only.

Windows: build 240/241, -level_09 crash-free past frame 1440.
2026-09-02 22:50:56 -04:00
James Dansereau 875b3c7653 linux-build: POSIX crashDumpThreads (watchdog thread dump)
crashDumpThreads was defined only in the PLATFORM_WINDOWS block; the
kernel-heartbeat watchdog in port/src/libultra.c references it
unconditionally -> undefined reference at link on Linux. Add a
PLATFORM_LINUX implementation: log the live thread roster + the watchdog
thread's own backtrace (a full cross-thread stackwalk needs signal-based
peer unwinding -- deferred). Linux-only block; Windows unaffected.
2026-09-02 22:35:37 -04:00
James Dansereau fe7a34c56e linux-build: port/shim/sched.h routes C++ TUs to the host <sched.h>
The decomp's src/sched.h (N64 OSSc* scheduler) is on the include path and
shadows the system header. libstdc++'s <thread>/<mutex> gthr headers in the
fast3d C++ TUs #include <sched.h> and need the real one (cpu_set_t,
sched_yield) -- with -std=gnu++20 libstdc++ defines _GNU_SOURCE so
<pthread.h> also references cpu_set_t. New shim: C++ -> generated
hostsched.h (absolute path, hoststring pattern); C -> #include_next so
src/sched.h still wins as before. Windows build verified green (240/241).
2026-09-02 22:31:33 -04:00
James Dansereau 7c3bb42687 linux-build: -std=gnu extensions + fix remaining snprintf decls
- CMakeLists: CMAKE_C_EXTENSIONS/CXX_EXTENSIONS ON. Strict -std=c11/c++20
  define __STRICT_ANSI__, which disables glibc _DEFAULT_SOURCE and hides
  clock_gettime(CLOCK_REALTIME) etc. that port/src/libultra.c needs.
  -std=gnu11/gnu++20 is GCC's default and what MinGW already used.
- port/src/{pccg,pcmodels,romdata}.c: local 'extern int snprintf(...,
  unsigned long long maxsize, ...)' decls conflict with glibc's size_t one
  (same bug already fixed in pc_protos.h in M-39); spell it size_t.
- libultra.c: drop the _GNU_SOURCE define (pulls cpu_set_t refs into
  <pthread.h> without <sched.h>); the D60 POSIX stack-bounds diagnostic
  now just prints n/a rather than needing pthread_getattr_np.

Windows: full rebuild 240/241, -level_09 crash-free past frame 1740.
2026-09-02 22:12:58 -04:00
James Dansereau 59751d608c linux-build: POSIX branches for time()/bcopy/bzero and Win32-only probes
- pc_protos.h: the vestigial 'long long time()' prototype conflicts with
  glibc's time_t time(time_t*); no game code calls time(), guard it to
  _WIN32 (MinGW shadows <time.h> with the N64 stub; POSIX gets the real one).
- n64stubs.c: don't redefine bcopy/bzero on POSIX -- glibc exports them
  (via <strings.h>, size_t sizes); the local int-signature definitions
  conflict. _WIN32-only now.
- libultra.c: #define _GNU_SOURCE on POSIX for clock_gettime(CLOCK_*) and
  pthread_getattr_np; guard the D60 VirtualQuery committed-memory probe and
  GetCurrentThreadStackLimits behind PLATFORM_WINDOWS with a
  pthread_getattr_np-based stack-bounds fallback for the diagnostic path.

Windows build verified green.
2026-09-02 22:07:36 -04:00
James Dansereau 1077a2db23 linux-build: define wchar_t in the C-on-Linux <stddef.h> shim
SDL2's headers (SDL_stdinc.h, SDL_hidapi.h) and glibc's <bits/wchar2.h>
reference wchar_t. In C it is a library typedef owned by the compiler's
shadowed <stddef.h>, which our shim replaces with builtin-derived types
on Linux/macOS. Add wchar_t from __WCHAR_TYPE__, guarded by _WCHAR_T so a
real <stddef.h> reached another way stays a no-op. C-on-Linux/macOS
branch only; MinGW and C++ paths untouched.
2026-09-02 21:48:44 -04:00
James Dansereau ccef9497ac port: Linux libc-header compat for <stdarg.h> and snprintf
Next GCC-on-Linux breaks after the stddef fix (linux-build CI):

- port/shim/stdarg.h routed to the decomp include/stdarg.h, which defines
  va_list from __builtin_va_list but NOT the __gnuc_va_list alias that
  glibc's <stdio.h> references — 'unknown type name __gnuc_va_list' across
  every TU that pulls <stdio.h>. Add a Linux/macOS branch that provides
  va_list / __gnuc_va_list / va_* straight from compiler builtins and does
  not drag <ultra64.h> into system-header parsing. Windows branch unchanged.

- pc_protos.h declared snprintf's size argument as 'unsigned long long' —
  correct on MinGW (LLP64) but not glibc, where size_t is 'unsigned long':
  'conflicting types for snprintf'. Spell it size_t (now visible via the
  stddef fix); identical to ull on MinGW.

Windows: both changes are no-ops on MinGW (stdarg Windows branch byte-identical;
size_t == unsigned long long).
2026-09-02 21:31:21 -04:00
James Dansereau a8c5652dac port(stddef shim): Windows keeps the stub; Linux/macOS use GCC builtins
The first cut routed C TUs to hoststddef.h unconditionally. That regresses
MinGW: its <stddef.h> defines `errno (*_errno())`, which collides with
PR/os.h's `u8 errno;` field. And hoststddef.h's bin/../include/stddef.h path
does not exist on Linux anyway.

Split three ways: C++ -> hoststddef.h (unchanged); C on Windows -> the empty
N64 stub (unchanged, pre-existing behaviour); C on Linux/macOS -> size_t /
ptrdiff_t / NULL / offsetof from __SIZE_TYPE__ / __PTRDIFF_TYPE__ /
__builtin_offsetof (always predefined, no header, no collision).

Verified: Windows ntsc-final builds 240/240, -level_09 boots crash-free.
2026-09-02 21:24:43 -04:00
James Dansereau 01fd39a089 port: route C TUs to the host <stddef.h>; link no-PIE on Linux
Two GCC-on-Linux build breaks that MinGW masked (surfaced by the new
linux-build CI job):

- port/shim/stddef.h routed C TUs to the empty N64 include/stddef.h stub.
  MinGW's CRT headers leak size_t/ptrdiff_t transitively; glibc/GCC define
  them only in the compiler's own <stddef.h>, so include/PR/ultratypes.h's
  non-N64 path ("typedef ptrdiff_t ssize_t") failed with "unknown type name".
  Route C TUs to hoststddef.h too (C++ already was). Additive — the active
  N64 stub typedefs nothing.

- CMakeLists: dram_syms.s absolute symbols at 0x70000000 need a non-PIE image
  on Linux exactly as they need --disable-dynamicbase on Windows. Add an
  elseif(UNIX AND NOT APPLE) branch: -fno-pie / -no-pie.

Windows unaffected (host <stddef.h> is strictly more correct than the stub;
the MINGW linker branch is unchanged).
2026-09-02 21:19:46 -04:00
James Dansereau b01c7fa89c 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 2f5d7f9063 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 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 433907b566 port: absolute menu cursor + dual-pump event fix + auto window size (M-35)
Follow-on to ad748654 (D180). Playtested this session: ESC-to-free now
works; Dam -> Surface seamless on Agent.

- Dual SDL event pump race: port/src/video.c (host thread) and
  port/fast3d/gfx_sdl2.cpp (render thread) both drain the same SDL queue,
  so either loop can dequeue a given event. ESC-to-free / click-to-lock /
  mouse wheel / focus events were being eaten by the render-thread pump
  and lost. Now handled in BOTH pumps (inputReleaseCapture /
  inputNotifyClick / inputPostWheel / inputSetMouseGrab).

- Menu pointer rewritten for capture mode. front.c's cursor integrator
  caps at ~5.75 virtual px/poll, so the D165/D169 P-controller fed from
  an absolute mouse position was always laggy/floaty and could not reach
  the bottom of the grid. With MouseCaptureMode=1 the front end has a
  real free OS cursor: write cursor_h_pos/v_pos directly from the
  absolute mouse position and emit a zero stick. Legacy relative path
  (MouseCaptureMode=0) unchanged, and its estimate clamp now tracks the
  live front-end rect (was pinned to 320x240 -> right/bottom unreachable).

- OS cursor hidden while the window is focused (applyCursorVisibility);
  shown again on focus loss.

- Window.Width/Height <= 0 now auto-fits a 4:3 window to ~85% of the
  desktop (gfx_sdl2 gfx_sdl_init) instead of 640x480 native. video.c
  passes 0 through; [Window] persistence still pins whatever you leave.

Verified: build-pc.sh ntsc-final green 240/240; -level_09 framediff
3/3 within threshold (nonclear d <= 0.01pp, phash = documented D117).

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 09:54:17 -04:00
James Dansereau ad74865404 port: click-to-lock mouse capture + absolute menu cursor + aim retune (D180)
WI-1: Input.MouseCaptureMode (0 = legacy always-grab, default; 1 =
Quake/Nightdive-style click-to-lock). In mode 1 the OS cursor is free
until a click lands in the game window; ESC, focus loss, or entering
any front-end menu frees it again, and re-entering a stage while armed
re-locks so unpausing needs no click. Mouse buttons are withheld from
the game while the cursor is free in-stage (no phantom fire). Controller
input paths are untouched. reconcileGrab() runs once per controller-0
poll off the game's current_menu global (UI context only).

WI-2: when capture mode is on and the cursor is free in a menu, the
D165/D169 pointer P-controller takes its target from the real cursor's
absolute window position mapped onto the live virtual front-end rect
(getPlayer_c_screen*), for true 1:1 tracking. The relative-delta path
(legacy / grabbed) is unchanged.

B3: Input.MouseAimSpeed default 25 -> 16 (aim mode still overshot at 25).

Build 241/241; -level_09 framediff 3/3 within threshold (unregressed);
GE_STARTMENU menu boot crash-free. Feel-checks owed (headless can't
drive the mouse).

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 01:26:33 -04:00
James Dansereau 91b7c5aa4e port: config system — add Float/UInt option types with clamps
Modelled on the PD port's configRegister{Float,UInt}. Needed for the
in-game video/options menu work (WI-3): sliders that store fractional
or unsigned values (sensitivity, screen-shake, fov). configSave() now
flattens all four option kinds into one section-grouped pass so mixed
int/float keys in the same [Section] stay together, and the
newly-registered-key migration check covers them.

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
2026-09-02 01:26:24 -04:00
James Dansereau 231a8a91bf D169: level-select mouse pointer reaches the whole grid
The front-end pointer P-controller clamped its target/estimate to a
320x240 field (MENU_CURSOR_HI_H/_V 300/220); GE's front end runs 440x330,
so the pointer saturated before the outer mission-select grid columns and
bottom row -- only an inner ~3x3 was selectable.

port/ only, no game-logic change: the menuMode branch now derives its
clamp bounds from the live virtual screen (getPlayer_c_screenwidth/height
/left/top, src/game/bondview.c) -- [screenleft+20 .. screenleft+w-20] x
[screentop+20 .. screentop+h-20], matching front.c
frontUpdateControlStickPosition -- and seeds the estimate from the real
cursor_h_pos/cursor_v_pos instead of a 320x240 centre guess. Falls back
to the old constants when the front-end screen isn't set.

Builds + links clean (engine symbols resolve). GE_STARTMENU=7 boots
crash-free; -level_09 framediff 3/3 within threshold. Interactive
feel-check still owed (headless can't drive the mouse).

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 20:22:23 -04:00
James Dansereau fa54f8fb90 docs: add operating manual, fix cross-references after doc restructure
- Commit CLAUDE.md + AGENTS.md so a fresh clone has the operating manual and
  the ABI/no-game-logic rules (trimmed of stale phase status; the personal
  pd_port path removed; points at the split docs).
- Sweep every stale doc reference across the tree after the restructure:
  PCPortResearch.md -> internals.md / dev/findings.md,
  PORT-LEARNINGS.md -> porting-notes.md,
  AGENT-WORKFLOW.md -> dev-process.md,
  docs/{GRAPHICS-BACKLOG,HANDOFF-ARCHIVE,GE-ENV-PROBES,LEVEL-*,TEXTURE-GLITCH-ANALYSIS,AUDIT-M6-*,AUDIO-PLAN}.md -> docs/dev/.
- Drop the superseded docs/PLAN-linear-level-sweep.md.
- docs/HANDOFF.md + docs/BACKLOG.md stay local-only (rolling working files);
  CLAUDE.md now handles their absence in a fresh clone.
2026-09-01 20:01:09 -04:00
James Dansereau 203549f57e D168: fix vertically-flipped GE_PCDUMP captures; close D114/D116
gfx_opengl_dump_bound_fbo wrote glReadPixels output (GL bottom-left
origin) straight into a top-row-first P6 PPM, so every GE_PCDUMP / F12
capture was upside-down. This is the entire source of the D114/D116
"HUD/text X-mirror" - four sessions chasing a fast3d mirror that never
existed, misreading inverted screenshots of asymmetric content. The
developer confirms the game renders correctly on real hardware.

- writer now emits rows top-to-bottom
- tools_pc/golden/*.png flipped in place to match + golden/README.md
- D114/D116 reclassified as capture-orientation artifacts (not deleted);
  D75 "logo mispositioned" / RC1 "photo 180 deg" flagged for re-judging
- annotations in findings.md, GRAPHICS-BACKLOG.md,
  TEXTURE-GLITCH-ANALYSIS.md, porting-notes.md D2, GE-ENV-PROBES.md

Not runtime-verified (migrated tree has no ROM/toolchain) - needs a
fresh capture to confirm text reads normally.

Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
2026-09-01 19:44:39 -04:00
James Dansereau 52539d10de Prepare for public release
- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING.
- Restructure docs: internals.md, porting-notes.md, dev-process.md +
  dev/agentic-development.md (the research write-up), dev/ (raw record).
- Replace decomp status-badge CI with a PC-port validate workflow.
- Remove session/agent scaffolding and stale planning docs from the tree.
- Add .gitattributes (normalize line endings to LF); add screenshots.

The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken
(KholdFuzion) and contributors, preserved unchanged. The PC-port work from
2026-08-17 on is mine.
2026-09-01 19:28:59 -04:00
James Dansereau 40f177a982 RC3/D167: non-power-of-two texture wrap period (Video.WrapFix, default off)
fast3d never stored the N64 tile mask and wrapped textures purely at the
uploaded image size; the RDP wraps a WRAP render tile at 1<<mask, which GE
sets to ceil(log2(dim)) - so non-power-of-two surfaces (Depot room
textures) repeat at the next power of two on console and one image-width
too soon in the port ('textures repeat oddly').

- gfx_dp_set_tile: store masks/maskt on the tile.
- gfx_sp_tri1 hoisted pre-wrap block (the reworked D74 block, indexed by
  texunit): when WRAP and 1<<mask != tex_width, fold UV at 1<<mask and
  clamp the [dim,1<<mask) overflow band (no real texels) to the edge.
- GE_WRAPFIX env override (wins over the ini) for testing.

Behind the existing Video.WrapFix knob, default OFF -> byte-identical to
golden. Per-level captures (-level_09/-30/-34/-20, off vs on): no crashes,
no structural regression; Depot shows small localized ceiling/wall texel
changes. Kept default off pending a human visual check on Depot.

docs: PCPortResearch.md RC3/D167, PORT-LEARNINGS.md D, TEXTURE-GLITCH-ANALYSIS.md 6d.
2026-08-31 12:49:39 -04:00
James Dansereau dc5739bc93 D167: D152 addendum -- audio osSetIntMask audit + narrow lock-leak fixes
Static audit of every osSetIntMask(OS_IM_NONE) in the compiled audio code
(event.c, csplayer.c, synaddplayer.c, snd.c): all balanced on every exit
path, including alEvtqPostEvent's freeList-empty early return. No unbalanced
same-thread leak exists -- the D152 s_imLock wedge is not a missing restore.

Two real defects fixed (#ifdef PORT, N64 verbatim under #else):

1. snd.c sndSetSfxSlotVolume walked the live ALSoundState list and posted to
   the shared ALEventQueue without holding OS_IM_NONE, unlike its twin
   sndDeactivateAllSfxByFlag. On PC: unguarded walk racing amMain + one lock
   acquire/release per matching sound. During the mission-failed fade-out
   (sndSetScalerApplyVolumeAllSfxSlot -> sndApplyVolumeAllSfxSlot -> here,
   per frame) that is a per-frame lock-acquire storm. Now holds the mask once
   across the walk; sndApplyVolumeAllSfxSlot batches its slot loop under one
   recursive hold.

2. port/src/libultra.c: portThreadWrapper now calls imThreadExitRelease() when
   a host thread returns from its entry -- if it still owns s_imHeld, release
   the orphan (+ LOG_ERROR). Kills the 'transient thread acquired OS_IM_NONE
   and exited' leak (the §F dump's own alternate hypothesis) and the re-wedge
   when a new thread reuses the dead pthread id and imAcquire mis-detects
   recursion. Steal-lock kept as backstop.

Build 240/240 green; -level_09 frame 300 renders 91.7% non-clear crash-free;
-level_20 boots crash-free. Fade-out repro is playtest-gated (not headless-
reproducible) -- user to confirm no black screen + no D152: log lines.
2026-08-31 12:49:06 -04:00
James Dansereau 99c5926924 D165/D166: front-end 1:1 pointer P-controller + speed-proportional hipfire pitch pulses
port/src/input.c only (port-layer, no game-code change):

D165 - the M-30 menu 'pointer mode' fed mouse velocity into the stick, but
front.c frontUpdateControlStickPosition integrates the stick as a velocity
into a screen-pixel cursor -> velocity^2 feel. Replace with a P-controller:
maintain menuEst{H,V} (our estimate of the game cursor, stepped each poll with
front.c's exact recurrence via menuCursorStep), accumulate menuTgt{H,V} from
raw mouse motion, emit stick = clamp(6.0*(target-est), +/-70). Gain 6.0 =>
per-poll error decay 0.45, no overshoot, ~8-poll settle, parks under the
game deadzone with no drift. Input.MenuPointerMode (default 1) keeps the old
velocity mode as a fallback; Input.MenuPointerSpeed scales the mapping;
GE_INPUTLOG prints an est/tgt/eff/stick trace. No mouse motion => emitted
stick unchanged => prior behaviour exactly.

D166 - hipfire mouse pitch was a fixed digital threshold while yaw is analog.
Emit the C-button as a duty-cycled pulse whose frequency scales with mouse-Y
speed (fast = solid hold as before, slow = sparse taps). Input.HipfirePitchSpeed
tunes it. Aim-mode (RMB, already analog) untouched.

Build green; -level_09 (~360 frames) + GE_STARTMENU=6 smoke crash-free.
Feel-check owed (headless can't drive the mouse).
2026-08-31 12:18:15 -04:00
James Dansereau d23802ff9b D161: Depot ceiling blue-speckle — CI tile with G_TT_NONE decoded as palette
The Depot roof (CI8 tile drawn with gsDPSetTextureLUT(G_TT_NONE)) was
decoded against a stale rdp.palette, producing blue/magenta noise that
aliased into radial rays on the receding ceiling plane. A CI-siz tile
with the TLUT disabled must be treated as raw intensity (I). Route
CI4/CI8 -> I4/I8 in import_texture() when palette_fmt == G_TT_NONE.

Adds GE_TEXDUMP (env-gated PPM + param dump) as an inert diagnostic.
-level_09/-30/-34 sweep PASS, unregressed.
2026-08-31 12:16:19 -04:00
James Dansereau 3e5720c01b D74: rework sub-tile UV pre-wrap as opt-in Video.WrapFix (was dead + OOB)
The D74 block in gfx_sp_tri was inert (guard `cms & G_TX_WRAP` == `& 0`,
always false) AND OOB (`tex_width2[i]` indexed by the vertex i=0..2, but
the array is per-texunit [2]). SMALL-FIXES B1: the naive in-place
correction boot-crashed -level_09.

Rework per the B1 plan: precompute per-texunit `wrap_s/t[t]`,
`wrap_tw/th[t]`, `wrap_uls/ult[t]` once in the texunit setup loop (needs
the original cms/cmt before the CLAMP-clear), keyed on 'clamp bit not
set' + window smaller than the uploaded image; apply per vertex indexed
by `t`. Gated behind `Video.WrapFix` (default 0 -- new/never-run path).
Verified: WrapFix=0 unchanged; WrapFix=1 -level_09 renders clean through
frame 375, no boot crash, no visible corruption. Visual fix on wrapped
triangle textures (room panels) needs a level with them + an N64
reference -- Depot per B1.
2026-08-31 11:40:42 -04:00
James Dansereau ddef038238 fast3d RC2: clip mip-contaminated texture uploads to base height (+ GE_DTEX probe)
Reported: menu / room textures look 'interlaced' (alternating garbage
rows). Root cause (docs/TEXTURE-GLITCH-ANALYSIS.md RC2): GE's
texGetDepthAndSize() packs base level + every LOD mip into one
gDPLoadBlock, so fast3d's upload height (size_bytes / row) runs ~1.3x
taller than the base image and the mip bytes render as rows below it.
GE_DTEX=1 confirmed it: 64x64 I4 uploaded 64x87, 65x65 -> 65x95, etc.

Fix in import_texture(): when LOD is active (rdp.tex_lod) and the load
is a plain full-width block (not a windowed gfx_dp_load_tile - the D74
sub-tile / Rare-logo case, which is lod=0), clip size_bytes to the
SETTILESIZE base-tile height. GL then builds correct mips from a correct
base. New knob Video.FixMipTextures (default 1; =0 restores old upload,
verified byte-identical to golden -level_09 3/3). BUNKER1 with the fix
on: side-wall textures visibly cleaner, geometry/HUD unchanged; Silo
unregressed. -level_09/-level_20 framediff WILL flag with the fix on
(textures changed for the better) - refresh golden after the user
confirms the menu + Depot look right.

New GE_DTEX=1 env probe logs load params for the first 64 textures.
2026-08-31 08:25:34 -04:00
James Dansereau 8dc952774b input: fix inverted Y in front-end pointer mode (mouse-down moved cursor up) 2026-08-31 08:10:34 -04:00
James Dansereau cbc83c24ab input: front-end pointer mode - mouse drives the menu cursor on both axes
Reported: on SELECT FILE the mouse only moved side-to-side; up/down
needed W/S. The front-end cursor is stick-driven (frontUpdateControl
StickPosition reads joyGetStickX/Y); the port only fed mouse-X to the
stick in menus (mouse-Y went to C-buttons, which the menus ignore).

New: when current_menu != MENU_RUN_STAGE (and != MENU_INVALID, so bare
-level_XX in-game aim is untouched), mouse velocity drives stick X+Y as
a pointer, no C-buttons/aim-band, LMB=A (select) RMB=B (back). Reads the
game global current_menu directly for UI context only - no logic change.
New knob Input.MenuPointerSpeed (%, default 100). -level_09 3/3
framediff; FILE_SELECT boots clean 900 frames.
2026-08-31 08:07:23 -04:00
James Dansereau 7d31654f2e D2/D146: strip unconditional D63 debug trail from gfx_pc.cpp
The D63 branch-trail dump fired ~10 sysLogPrintf lines + a full trail
walk on EVERY unknown GBI opcode, unconditionally. On the front-end
menu (D149 corrupt model DL) this ran per-frame and collapsed the menu
to ~5fps with input starved. Removed the trail statics, the per-G_DL
recording, and the default-case dump; the rate-limited D146 'ending DL'
log stays. -level_09 3/3 framediff, -level_20 91.6% unregressed.
2026-08-31 07:56:35 -04:00
James Dansereau 944b28bd0d M-29: wire up opt-in N64 3-point texture filtering (BACKLOG B1 groundwork)
The sm64ex 3-point filter shader path already existed in gfx_opengl.cpp but
was never reachable from config, and its min-filter row was all GL_NEAREST
(no mip sampling) so it would shimmer badly.

- gfx_opengl.cpp: 3-point min-filter row -> GL_LINEAR_MIPMAP_{NEAREST,LINEAR}
  so minification uses the mips it already force-generates at upload;
  anisotropy default 0 -> 4 (0 was invalid for GL_TEXTURE_MAX_ANISOTROPY).
- video.c: Video.TextureFilter now accepts 2 = 3-point + trilinear.
  DEFAULT STAYS 1 (bilinear) -- 3-point is more console-authentic but
  softens textures at normal viewing distance, and (tested) did NOT fix
  the Depot roof garbage. That is a separate texture-data/UV/effect bug,
  not filtering -- BRIEF-B2 updated, next step is a GE_DTEX probe.

Verified: default (bilinear) -level_09 frame 320 unregressed vs golden
(nonclear delta 0.00pp, phash 11); build green; 1200-frame -level_30 run
crash-free with 3-point active.
2026-08-30 21:09:53 -04:00
James Dansereau d0d18b2472 M-29: file-backed EEPROM saves (BACKLOG B5 / Phase 4)
port/src/libultra.c: osEeprom{Probe,Read,Write,LongRead,LongWrite} were
no-op stubs, so campaign progress (unlocked missions, times, difficulty)
was lost on every quit. Implement a real 16Kbit (2048-byte) EEPROM backed
by $S/ge007.eep: lazy-load on first access, write-through on every write,
bounds-checked against the 256-block device. Pattern mirrors the PD port
(pd_port/port/src/libultra.c). Probe now reports EEPROM_TYPE_16K so the
decomp save logic (src/game/file2.c, already compiled) runs unchanged.

Verified: build green (ntsc-final); -level_09 boots, the game writes
data/ge007.eep (2048 B, checksum @block 0 + save_data slots @block 4),
600+ frames crash-free; -level_09 unregressed. File persists across runs.
2026-08-30 20:52:47 -04:00
James Dansereau 609bb732b9 M-29: aim sensitivity fix (BACKLOG B3 / SMALL-FIXES B4)
port/src/input.c: RMB manual-aim mouse sensitivity overshot badly at the
old MouseAimSpeed=50 default (25 px/poll saturated the aim band, so a
flick ran GE's (stick-60)/10 curve at full rate for many polls). Lower
the default to 25 and expose the band itself as Input.AimBand (int 5..40,
default 20 -> unchanged behaviour at the new speed). Port-only, config-
backed, fully reversible via ge007.ini.

port/fast3d/gfx_pc.cpp: annotate the dead D74 texture-wrap block
(SMALL-FIXES B1). The in-place correction (G_TX_CLAMP test + per-texunit
index) activates never-run code and crashes -level_09 at boot; it needs
the hoist-out-of-vertex-loop rework + per-level visual verification, not
a one-liner. Left inert with a NOTE; no behaviour change.

Verified: build green (ntsc-final); -level_09 + -level_20 crash-free for
a full ~75s capture window on a settled machine (D153: earlier back-to-
back runs crashed under build/run thrash, not a regression).
2026-08-30 20:49:50 -04:00
James Dansereau 57e24a374b M-28: watch-menu text (D150/D151) + audio-lock deadlock mitigation (D152)
D150 (src/str.c) — NULL-tolerant strcpy/strncpy/strcat under #ifdef PORT.
The watch BRIEF/OBJECTIVES pages build text with strcat(buf, langGet(id));
langGet() returns NULL on PC for a string bank the in-level flow never
loaded, and str.c's primitives are __nonnull__ builtins so a plain param
NULL check is optimised away — the guard launders the ptr through an empty
asm (GE_IS_NULL). NULL src -> empty string instead of an AV crash.

D151 (src/bondtypes.h) — watch data (objectives / mission background / M /
Q / Moneypenny) all rendered blank. propDef types 35 (WatchMenuObjectiveText)
and 23 (ObjectiveStart) store a plain s32 lang slot id in their 3rd word;
the decomp structs decode it as [u16 reserved][u16 text] reading text at
offset 0xA, which only works because the id is in the low 16 bits of a
big-endian word. d88_propdefs.py bswap32's the whole word -> text @0xA
reads 0 -> langGet(0) -> NULL. Fix: text is a full u32 at the word offset
under #ifdef PORT (N64 layout kept under #else). No converter change, no
sidecar regen. This is the D143/D150 "briefing text renders blank" root cause.

D152 (port/src/libultra.c) — mission-failed / objective-failed permanent
black screen. The D147 recursive-mutex behind osSetIntMask can be leaked
(libaudio has unbalanced / early-return mask calls; a transient thread can
acquire OS_IM_NONE and exit) -> mainThread + amMain both block forever in
alEvtq* -> gfx pipeline stalls. Live gdb.txt from the user's session
confirmed the deadlock (mainThread in the mission-fail audio fade-out
sndSetScalerApplyVolumeAllSfxSlot -> alEvtqPostEvent). Mitigation: model
the mask as a self-healing logical lock (owner/depth under s_imMx + condvar)
— normal microsecond critical sections behave as before, but a waiter
blocked > 2s concludes the holder leaked it, logs the stale owner + the
stealing caller's return address, and steals the section. Worst case is a
~2s audio hiccup + a log line naming the leak, instead of a hung game.
Proper narrow fix (dedicated ALEventQueue lock) still owed.

D153 investigated and dismissed — the "-level_09 crashes at frame ~900"
seen while regression-testing was D117/D134 host-scheduling flakiness under
heavy concurrent build+run load; on a quiet machine -level_09 runs past
frame 2400 clean with all three changes applied.

Build: green (ntsc-final). Golden framediff not run clean (same machine-load
flakiness); one quiet -level_09 run = 2400+ frames, 0 heartbeats.
2026-08-30 20:25:02 -04:00
James Dansereau 48bac33068 M-27: D147 - osSetIntMask is a real recursive lock (audio-queue race -> hang)
The hang seen near the end of Dam (and latent everywhere a sound effect
plays): a door finishes opening, objTick -> doorPlayCloseSound0 ->
sndPlaySfx -> alEvtqPostEvent walks g_sndPlayer's ALEventQueue insert list
on the main thread while the audio-manager thread (amMain -> alAudioFrame ->
sndPlayerVoiceHandler -> sndRemoveEvents) mutates the same list. Both guard
with osSetIntMask(OS_IM_NONE) -- which the port stubbed as a no-op -- so on
PC there is no mutual exclusion: the list corrupts and alEvtqPostEvent's
for-loop spins forever (thread-dump confirmed both threads inside the evtq
under a bogus/circular link).

osSetIntMask now maps to one process-wide recursive mutex: OS_IM_NONE
acquires, the OS_IM_ALL token it returns releases on the paired restore,
other masks (OS_IM_VI in sched.c) pass through. The decomp never blocks
while holding OS_IM_NONE (N64 contract), so it cannot deadlock. Covers
every cross-thread critical section in libaudio + sched, not just this one.

Verified: -level_09 1200 frames, 0 heartbeats, 91.66% coverage (golden).
Thread dump captured via CTRL_BREAK to the batch gdb + fresh attach.
2026-08-30 15:40:59 -04:00
James Dansereau a82e90d457 M-27: D146 hardening - guard gfx_sp_vertex/movemem/set_vertex_colors ptrs
A desynced/corrupt front-end display list (D75 family - the compiled
wallet/dossier/logo sub-DLs at seg5+offset) feeds wild pointers to the RSP
command handlers before the walk reaches an unknown opcode. Added a shared
fast3d_ptr_ok() mapped-range check; gfx_sp_vertex, gfx_sp_movemem and
gfx_sp_set_vertex_colors now bail on a wild pointer instead of faulting.
Pairs with the D146 unknown-opcode -> end-DL change.

Verified: -level_09 unregressed.
2026-08-30 15:30:16 -04:00
James Dansereau cea9ce0df9 M-27: D146 unknown-GBI-opcode -> end DL instead of abort(); tools_pc/debug.ps1
D146 (port/fast3d/gfx_pc.cpp): an unrecognised opcode in the DL walk called
sysFatalError() -> abort() -> whole process dies with no ge007.crash.log
(the crash handler does not cover abort). Seen on the real post-Dam
MISSION COMPLETE screen: "[FATAL] Unknown GBI opcode 0x40c03 at
0x70161a8c" - a front-end 3D model (dossier, D75/D144 family) whose display
list was never built, sitting at a valid DRAM address full of junk. D144
guarded the bad matrix pointer; the walk then hit more garbage. Now: log
loudly (rate-limited x20) and `return` to end the (sub-)DL so the frame
finishes. A real level-render desync is still visible in the log.

tools_pc/debug.ps1: launch the port under gdb from the repo root so a
playtest crash always leaves a backtrace (abort/fatal or silent exit).
-Menu <id> sets GE_STARTMENU; passes through -level_XX etc. Prints the
FATAL line + backtrace + crash log when the game exits.

Verified: GE_STARTMENU=13 mission-complete -> Next chain renders, no abort,
no crash (D75 model cosmetics aside).
2026-08-30 15:27:23 -04:00
James Dansereau 36acf324fa M-27: D145 - ESC no longer hard-quits (it is the menu back key)
port/src/video.c, port/fast3d/gfx_sdl2.cpp, port/src/input.c: bare ESC was
wired to exit(0) in both event handlers. On the front-end / debrief / mission-
complete screens ESC is the natural "back / previous page" key, so a player
paging through the post-mission report with ESC quit the whole game -- a
clean exit(0), no crash log, looked like a crash. ESC now feeds the N64 B
button (back / cancel) through input.c alongside X/R/F; quitting is
window-close (X) or Alt+F4 only.

Verified: -level_09 boots crash-free, 8 frames rendered.
2026-08-30 15:05:12 -04:00
James Dansereau 70793eba4f M-27: D143 null-string text guard + D144 bad-matrix-ptr fast3d guard
D143 (src/game/textrelated.c): langGet() returns NULL for a string slot the
current flow has not loaded (non-DEBUG builds; DEBUG substitutes a
placeholder). The N64 retail menu flow guarantees every front-end string is
resident before it is drawn; the PC flow does not yet, so a missing
briefing/objective/mode-select string reached textRender/textMeasure/
textWrap as NULL and faulted in the `*text` scan. Added #ifdef PORT NULL
guards at all four entry points -> degrade to "draw nothing" (blank text)
instead of crashing. User verified: front-end now navigable folder -> mode
-> difficulty -> level -> in-game.

D144 (port/fast3d/gfx_pc.cpp gfx_sp_matrix): a front-end 3D model
(MISSION COMPLETE dossier / mode-select wallets - D75 family) emits a
gSPMatrix whose pointer field was never resolved; w1 arrives as
0xFFFFFFFFFFFFFFFF, seg_addr() passes it through, and gfx_sp_matrix faults
reading addr[]. Guard: an unmapped/non-canonical addr -> substitute identity
(model draws with wrong transform, already a parked D75 cosmetic) so the
screen and its "Next" -> menu transition survive.

Verified: -level_09 unregressed (framediff 2/2 ok w/ HUD mask, no crash).
2026-08-30 14:47:37 -04:00