- psx_fatal_halt: deliberate fatal sites (unhandled MMIO access,
trap_crash) now write the structured crash report AND a full
freeze-style ring dump (wedge kind "fatal"), then keep pumping the
TCP debug server forever in debug-tools builds so every ring stays
queryable against the exact crash state (ring-buffer-first: crashes
are interrogated, not snapshotted). Release builds exit(1) as before.
Re-entry guarded; heartbeat JSON gains a "fatal" field.
- gp1_dump: dedicated always-on GP1 (0x1F801814 display control) ring,
512K entries (~15 min of gameplay) with frame-window server-side
filtering. The general MMIO ring rolls over in under a minute of SPU/
DMA traffic, which kept evicting boot-window display history before a
probe could ask "who toggled the display during the logo" (bug 1).
- mmio_dump/mmio_clear documented in TCP_COMMANDS.md.
The halt-and-serve path is what made the pig-crash root cause a
one-repro diagnosis.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- debug_server.c: dump_ram = alias of read_ram (one request, one response,
up to full 2 MB); screenshot = screenshot_file (24-bit BMP, optional
"path", default psx_screenshot.bmp); vram_peek cap 64->128; read_ram hex
encode via nibble table (was 2M snprintf calls stalling the main thread);
send_all_blocking bounded (1 MB chunks, SO_SNDTIMEO 2s, heartbeats the
starvation watchdog per chunk) and drops the client on send failure
instead of letting the watchdog kill the runtime; handle_quit tags exit
origin.
- beetle_debug_server.c: read_ram cap 64 KB->2 MB via heap+send_raw;
dump_ram/screenshot aliases; screenshot w/h->width/height parity;
set_input now holds until clear_input (frames param optional) matching
psx-runtime semantics — the old default expired after 1 frame and the
off-by-one in the getter cleared the override before it was ever read,
so scripted beetle navigation silently did nothing; wtrace_dump response
heap-built + send_raw (per-entry send_fmt dropped chunks on multi-MB
responses, producing broken JSON).
- crash_trace: new psx_crash_trace_set_exit_origin(); report gains
exit_origin (tcp_quit / sdl_window_close / unknown).
- debug_client.py: O(n) string-aware brace parser (old per-byte loop was
O(n^2) — minutes on a 2 MB response, which starved the server's send and
got the runtime killed); generic key=value passthrough for any server
command without a bespoke CLI mapping.
- TCP_COMMANDS.md updated for all of the above.
Verified: 2 MB read_ram round-trip 0.75 s; scripted beetle navigation
reaches the memcard file-select; wtrace_dump returns valid JSON at 21 MB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The "v4" suffix was a holdover from iterative project restarts.
There is now one psxrecomp; all identifiers, include guards, cmake
variables, function names, log strings, schema tags, and tool paths
drop the v4 suffix.
Changes:
- PSXRECOMP_V4_ROOT → PSXRECOMP_ROOT (cmake)
- PSXRECOMP_V4_RUNTIME_SOURCES/_INCLUDE_DIRS/_BIOS_GENERATED → no V4
- psxrecomp_v4_add_runtime_target → psxrecomp_add_runtime_target
- PSXRECOMP_V4_*_H include guards → PSXRECOMP_*_H
- "psxrecomp-v4" log/window strings → "psxrecomp"
- Schema version strings in recompiler JSON output
- Tool script hardcoded absolute paths updated
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
New runtime/src/crash_trace.c writes psx_last_run_report.json with:
reason / timestamp / frame / last_func_addr / last_store_pc
full CPU register state (all 32 GPRs + hi/lo/sr/cause/epc)
dispatch_ring tail (last 256 dispatched func entries)
unknown_dispatch tail (last 50 entries with addr/phys/ra/a0/a1)
dirty_block_log tail (last 100 entries)
Windows SEH info (when triggered from SEH filter)
Triggered from:
- SIGSEGV / SIGABRT signal handlers
- Windows SetUnhandledExceptionFilter (catches access violations)
- atexit
- psx_unknown_dispatch fail-fast path (set PSX_FAIL_FAST_UNKNOWN_DISPATCH=1)
- trap_crash (existing fatal trap path)
File overwrites per dump (last-write-wins). Per CLAUDE.md §3 this is not
a log — it's a one-shot final state snapshot for crashes the running
TCP server cannot intercept.
Pattern mirrors F:/Projects/SuperMarioWorldRecomp/src/post_mortem.c.
Used the new infrastructure to capture a controlled crash with
0xBFC0989C re-seeded. Crash dump revealed the chain:
- JAL at 0xBFC09788 is jalr $s3, NOT jalr $a0
- $s3 was set in func_1FC09598 prologue: $s3 = $a2 (callee saves arg)
- Caller at 0xBFC093C4 in func_1FC092EC: $a2 = $s3 - 128 (delay slot)
- func_1FC092EC at 0xBFC09314: $s3 = $a3 (callee saves arg)
So real bug is upstream-of-upstream caller passing $a3 = 131 (0x83)
to func_1FC092EC, propagating down to a misaligned function pointer.
The 0xA000E1E8 we suspected as a function-pointer table was a
red herring — $a0 in the loop body is loaded from $fp+s1*4 (the
ARG to pass to the called fn), not the call target.
0xBFC0989C reverted out of seed list (was diagnostic-only).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>