starvation_watchdog_check() sampled the clock before loading the heartbeat,
but the heartbeat is also stamped by the debug-server IO thread on every
send() chunk. A stamp landing between the two reads made last > now, the
uint64 subtraction wrapped past any threshold, and a healthy runtime exited
with code 2 (BoF3 2026-09-02: 402 us of real staleness tripped the 4 s
watchdog within ~2 min whenever the area poller was attached).
- Load the heartbeat first, decide through the new pure helper
starvation_watchdog_stale(), which refuses a negative gap by construction
(same shape as the frame_pacing_sleep_ms fix).
- Make s_last_heartbeat_us _Atomic with relaxed store/load.
- Tag the exit origin: "starvation_watchdog" on the trip, console_close /
console_ctrl_break / console_ctrl_c in the Win32 console handler, and
signal_sigint / signal_sigterm in the soft-exit handler. These all used
to reach psx_atexit_handler as atexit/unknown, indistinguishable from an
SDL window close.
- Add starvation_watchdog_test (ctest) pinning the decision function.
Verified on BoF3 build-dbg: 10 min soak with the poller polling at 1 s and
the watchdog enabled at its 4 s default, no dump, process alive; forced
trip with PSX_STARVATION_TIMEOUT_US=1000 fires and the run report reads
exit_origin "starvation_watchdog".
Co-Authored-By: Claude Fable 5.1 <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>
TX_DATA writes now route through the SIO0 shifter under
SIO_MODEL_CYCLE_PACED. Pad and card share the single-byte shifter and
TX buffer; bus owner is tagged on the first byte of each SELECT cycle.
sio_advance() is driven from psx_advance_cycles() so the shifter and
ACK pipeline progress on the guest CPU clock without touching the
legacy IRQ countdown.
The SELECT-deassert and TX_EN 1->0 paths now restore TX_RDY|TX_EMPTY
when they kill an in-flight shifter. Without this, sio_stat retained
the TX-clear bits set by SHIFT_START and the next handler busy-waited
forever on TX_RDY=0 inside the exception, deadlocking the BIOS.
Watchdog dump captured the deadlock at chain handler 0x445C with
stat=0x0000 across 4 seconds of PC samples.
Diagnostics added (passive/gated):
- starvation_ring records SIO MMIO events + periodic PC samples,
watchdog dumps to JSONL on TCP-poll starvation
- starvation_walk.py queries the dump (last/last_change/why_idle)
Validation: shell-only sanity stable; one CROSS press no longer
hangs; mc_read_done climbs from 0 -> 30+ with mc_aborts=0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>