From 8b59da8202013c41750e23cb10bdba6e14dbe64f Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 29 May 2026 23:59:35 -0700 Subject: [PATCH] Disc speed: post-BIOS switch via entry_pc, XA guard for FMVs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Boot stays at authentic 1x. Speed switch fires once when the game's entry_pc is first dispatched — safely after the BIOS disc init and Sony/PlayStation logo sequences complete. Uses entry_pc exact-match rather than text range to avoid false-triggering on the BIOS shell (RAM 0x30000-0x5B000 overlaps the game text range). XA guard in apply_speed(): reverts to 1x whenever xa_stream_active, protecting FMV playback from speed-up regardless of disc_speed setting. CDROM_MIN_DELAY floor of 500 cycles prevents instant mode from firing IRQs faster than the interrupt handler can process them. Tomba game.toml: disc_speed = "4x" (instant proved too aggressive for early game disc reads; 4x safe with XA guard protecting FMVs). Co-Authored-By: Claude Sonnet 4.6 (1M context) --- runtime/include/cdrom.h | 8 +++++--- runtime/include/fntrace.h | 3 +++ runtime/src/cdrom.c | 30 ++++++++++++++++++++++++++---- runtime/src/fntrace.c | 21 +++++++++++++++++++++ runtime/src/main.cpp | 15 ++++++++++++--- 5 files changed, 67 insertions(+), 10 deletions(-) diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index e9d27312..59a40d1a 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -9,10 +9,12 @@ extern "C" { void cdrom_init(const char* cue_path); -/* Set disc speed multiplier. divisor=0 → instant (all delays collapse to - * 1 cycle, correct INT sequence fires as fast as host can process). - * divisor=1 → authentic 1x. divisor=2 → 2x. Call before first cdrom_tick. */ +/* Set disc speed multiplier immediately. divisor=0 → instant, 1 → 1x, 2 → 2x. */ void cdrom_set_speed(int divisor); +/* Store configured game speed (applied post-BIOS via cdrom_notify_game_started). */ +void cdrom_set_game_speed(int divisor); +/* Called by fntrace on first game-range dispatch; switches to game speed. */ +void cdrom_notify_game_started(void); /* MMIO read/write (0x1F801800-0x1F801803) */ uint32_t cdrom_read(uint32_t addr); diff --git a/runtime/include/fntrace.h b/runtime/include/fntrace.h index 3d5ee23d..ef119e7e 100644 --- a/runtime/include/fntrace.h +++ b/runtime/include/fntrace.h @@ -48,6 +48,9 @@ extern uint64_t g_fntrace_seq; /* monotonic; index into ring = seq % CAP /* Hot path. Called from psx_dispatch on every entry. Inlinable. */ void fntrace_record(CPUState* cpu, uint32_t target); +/* Register the game's text range for one-shot game-start detection. + * First dispatch into [lo, hi) calls cdrom_notify_game_started(). */ +void fntrace_set_game_range(uint32_t lo, uint32_t hi); /* 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/cdrom.c b/runtime/src/cdrom.c index e2233e4b..abce8619 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -86,18 +86,40 @@ static uint8_t xa_stream_file; static uint8_t xa_stream_channel; static int xa_stream_active; -/* 0 = instant (every delay collapses to 1 cycle), >0 = divisor applied to - * all timing constants. Set via cdrom_set_speed() from main before first use. */ +/* Operating divisor: 1x during BIOS boot, switches to g_game_divisor + * when the game's entry point first fires (via cdrom_notify_game_started). */ static int g_disc_speed_divisor = 1; +/* Configured target speed — applied post-BIOS. */ +static int g_game_divisor = 1; void cdrom_set_speed(int divisor) { g_disc_speed_divisor = divisor; } +/* Store the configured speed for post-BIOS application. Boot stays at 1x. */ +void cdrom_set_game_speed(int divisor) { + g_game_divisor = divisor; +} + +/* Called by fntrace_record on first game-range dispatch. */ +void cdrom_notify_game_started(void) { + g_disc_speed_divisor = g_game_divisor; +} + +/* Minimum cycles between CD-ROM IRQs in fast modes. Must be enough for the + * interrupt handler to save state, check the IRQ flag, process data, and + * return. Too low → interrupt fires before the previous one is processed → + * game hangs. 500 cycles ≈ 15µs at 33MHz, still ~900x faster than authentic. */ +#define CDROM_MIN_DELAY 500 + static int apply_speed(int delay) { - if (g_disc_speed_divisor == 0) return 1; + /* XA streaming (FMV / CDDA background music): preserve authentic timing. + * FMVs interleave XA audio + MDEC video — speeding up sector delivery + * would cause both to play faster than the display refresh rate. */ + if (xa_stream_active) return delay; + if (g_disc_speed_divisor == 0) return CDROM_MIN_DELAY; int d = delay / g_disc_speed_divisor; - return d < 1 ? 1 : d; + return d < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : d; } static int sector_delay_cycles(void) { diff --git a/runtime/src/fntrace.c b/runtime/src/fntrace.c index f357ecac..eb0f9d75 100644 --- a/runtime/src/fntrace.c +++ b/runtime/src/fntrace.c @@ -13,6 +13,21 @@ static uint32_t s_arm_targets[FNTRACE_ARM_MAX]; static uint32_t s_arm_count = 0; static uint32_t s_arm_record_all = 0; /* opt-in via fntrace_arm(0xFFFFFFFF) */ +/* One-shot game-start detection: fire cdrom_notify_game_started() the first + * time the game's entry_pc is dispatched. Using entry_pc (not a range) avoids + * false-triggering on the BIOS shell, which runs from RAM 0x30000-0x5B000 — + * overlapping the game text range but never at entry_pc. */ +static uint32_t s_game_entry_phys = 0; +static int s_game_started = 0; +extern void cdrom_notify_game_started(void); + +void fntrace_set_game_range(uint32_t lo, uint32_t hi) { + /* lo is treated as entry_pc; hi is ignored (kept for API compat). */ + (void)hi; + s_game_entry_phys = lo & 0x1FFFFFFFu; + s_game_started = 0; +} + static inline int armed_match(uint32_t target) { if (s_arm_record_all) return 1; /* Hot path: when nothing is armed, record nothing. Recording every @@ -27,6 +42,12 @@ static inline int armed_match(uint32_t target) { } void fntrace_record(CPUState* cpu, uint32_t target) { + if (!s_game_started && s_game_entry_phys != 0) { + if ((target & 0x1FFFFFFFu) == s_game_entry_phys) { + s_game_started = 1; + cdrom_notify_game_started(); + } + } if (!armed_match(target)) return; uint64_t idx = g_fntrace_seq++ & (FNTRACE_RING_CAP - 1); FntraceEntry* e = &g_fntrace_ring[idx]; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 528445ec..c8f629c6 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -8,6 +8,7 @@ #include "cpu_state.h" #include "psx_interpreter.h" #include "cdrom.h" +#include "fntrace.h" #include "gpu.h" #include "sio.h" #include "spu.h" @@ -1073,6 +1074,7 @@ int main(int argc, char** argv) { std::string game_name; std::string game_id; std::string disc_speed; /* "1x" | "2x" | "4x" | "instant" */ + uint32_t game_entry_pc = 0; if (game_config_path) { try { @@ -1084,6 +1086,7 @@ int main(int argc, char** argv) { if (gc.runtime.has_window_title) window_title = gc.runtime.window_title; if (gc.runtime.has_debug_port) debug_port = gc.runtime.debug_port; if (gc.runtime.has_disc_speed) disc_speed = gc.runtime.disc_speed; + game_entry_pc = gc.entry_pc; std::fprintf(stdout, "psxrecomp: loaded game config %s (%s, %s)\n", game_config_path, game_name.c_str(), game_id.c_str()); } catch (const std::exception& ex) { @@ -1130,10 +1133,12 @@ int main(int argc, char** argv) { if (disc_speed == "instant") divisor = 0; else if (disc_speed == "4x") divisor = 4; else if (disc_speed == "2x") divisor = 2; + /* Store for post-BIOS application; boot always runs at 1x so the + * BIOS disc-init sequence sees correct timing. */ + cdrom_set_game_speed(divisor); if (divisor != 1) - std::fprintf(stdout, "psxrecomp: disc_speed=%s (divisor=%d)\n", - disc_speed.c_str(), divisor); - cdrom_set_speed(divisor); + std::fprintf(stdout, "psxrecomp: disc_speed=%s (applied post-BIOS)\n", + disc_speed.c_str()); } memcard_init(memcard_dir_str.c_str()); std::atexit(memcard_flush_all); @@ -1144,6 +1149,10 @@ int main(int argc, char** argv) { #endif /* Heartbeat always on — see freeze_heartbeat.c rationale. */ freeze_heartbeat_start("psx-runtime"); + /* Register game entry_pc for post-BIOS disc speed switch. Fires once when + * the BIOS hands control to the game EXE — not on the BIOS shell. */ + if (game_entry_pc != 0) + fntrace_set_game_range(game_entry_pc, 0); /* ---- SDL init ---- */ SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0");