diff --git a/runtime/include/fntrace.h b/runtime/include/fntrace.h index c2f39b444..361ca2bad 100644 --- a/runtime/include/fntrace.h +++ b/runtime/include/fntrace.h @@ -87,6 +87,16 @@ void fntrace_record(CPUState* cpu, uint32_t target); * First dispatch into [lo, hi) calls cdrom_notify_game_started(). */ void fntrace_set_game_range(uint32_t lo, uint32_t hi); int fntrace_is_game_started(void); +/* One-shot game-start handoff. Idempotent — safe to call from any path + * (compiled dispatch, dirty-RAM interpreter, generated entry function). + * Performs dirty-image baseline clear, low-boot scratch clear, CD speed + * switch, and boot-state capture. */ +void fntrace_mark_game_started(CPUState* cpu); +/* Entry-pc-exact latch for the interpreter path: calls + * fntrace_mark_game_started() only when addr matches the armed game entry + * (fntrace_set_game_range lo). No-op before the range is armed and after + * the latch fires. */ +void fntrace_maybe_mark_game_started(CPUState* cpu, uint32_t addr); /* Arm a target filter. arm_count == 0 means "record all" (default). * When arm_count > 0, only dispatches whose target matches one of the diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 198e61418..dd4372b59 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -34,6 +34,7 @@ #include "ws_backdrop_detect.h" /* shared backdrop-window detector (auto_backdrop) */ #include "lockstep.h" #include "starvation_ring.h" +#include "fntrace.h" /* fntrace_is_game_started / fntrace_mark_game_started */ #include #include @@ -2397,6 +2398,16 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) { psx_fatal_halt("dispatch recursion guard tripped — runaway self-call " "(see dispatch_depth + dirty_block cycle for the recursing PC)"); } + /* Game-start detection for the dirty-RAM interpreter path. + * When dispatch_count == 0 (all code runs interpreted), the native + * dispatch path's fntrace_record() never fires, so widescreen/mouselook/ + * CD-speed-switch are never engaged. This one-shot check closes the gap + * with the SAME semantics as the native path: latch only on the exact + * game entry PC. A broader match (any phys >= 0x10000) fires during + * BIOS boot — the shell/kernel run relocated RAM code above 0x10000 — + * and the handoff's baseline/scratch clears then corrupt the boot + * (observed: MoH SLUS-00974 garbage-jump/VBLANK-wedge, 2026-08-06). */ + fntrace_maybe_mark_game_started(cpu, addr); if (addr == 0x8001A954u) site_note(&g_site_dd954); /* loop head re-dispatch */ else if (addr == 0x80046264u) site_note(&g_site_dd264); /* loop tail re-dispatch */ int prev = g_dirty_interp_active; diff --git a/runtime/src/fntrace.c b/runtime/src/fntrace.c index 45d44bfd5..8e4c6457b 100644 --- a/runtime/src/fntrace.c +++ b/runtime/src/fntrace.c @@ -4,6 +4,7 @@ #include "text_xlate.h" /* on-the-fly string translation hook (framework) */ #include "parity_trace.h" /* general control-flow parity ring (native producer) */ #include "mod_runtime.h" +#include #include #include @@ -44,6 +45,34 @@ void fntrace_set_game_range(uint32_t lo, uint32_t hi) { int fntrace_is_game_started(void) { return s_game_started; } +/* Centralised game-start transition. Idempotent — safe to call from both + * the dispatcher (fntrace_record) and the generated entry-point function. + * Performs the complete handoff side effects: dirty-image baseline clear, + * low-boot scratch clear, CD speed switch, and boot-state capture. */ +void fntrace_mark_game_started(CPUState* cpu) { + if (s_game_started) return; + s_game_started = 1; + extern void dirty_ram_clear_image_baseline(void); + extern void memory_clear_low_boot_scratch(void); + dirty_ram_clear_image_baseline(); + memory_clear_low_boot_scratch(); + cdrom_notify_game_started(); + boot_state_trigger_capture(cpu); +} + +/* Range-guarded latch for the dirty-RAM interpreter path. Mirrors the + * native path's semantics: latch ONLY on the exact game entry PC set via + * fntrace_set_game_range(). Never latch on a broad address heuristic — + * the BIOS shell/kernel execute relocated RAM code well above the game + * load address during boot, and a premature handoff (baseline/scratch + * clears, CD speed switch) corrupts the boot sequence. */ +void fntrace_maybe_mark_game_started(CPUState* cpu, uint32_t addr) { + if (s_game_started) return; + if (s_game_entry_phys == 0) return; /* no game range armed */ + if ((addr & 0x1FFFFFFFu) == s_game_entry_phys) + fntrace_mark_game_started(cpu); +} + static inline int armed_match(uint32_t target) { /* PSX_FNTRACE_ALL=1 records every dispatch from power-on — the ring * then holds the earliest boot execution (before any TCP client can @@ -184,19 +213,7 @@ void fntrace_record(CPUState* cpu, uint32_t target) { if (_tphys == s_game_entry_phys || (dirty_ram_text_image_registered() && psx_game_address_in_text(target) && psx_game_text_native_ok(target))) { - s_game_started = 1; - /* Establish the clean compiled-image baseline now: the boot EXE is fully - * loaded into the game-text region (== compiled image) and no gameplay - * overlay has run yet. The EXE load marked the whole text dirty (false - * positive); clearing it makes dirty_ram_is_dirty() true ONLY for pages a - * later overlay overwrites, so the dispatch runs clean text compiled and - * interprets only true overlays (Tomba 2 boot-text loader overlay). */ - extern void dirty_ram_clear_image_baseline(void); - extern void memory_clear_low_boot_scratch(void); - dirty_ram_clear_image_baseline(); - memory_clear_low_boot_scratch(); - cdrom_notify_game_started(); - boot_state_trigger_capture(cpu); + fntrace_mark_game_started(cpu); } } /* Honor the one-shot capture freeze (insn_freeze): once latched, the ring