Disc speed: post-BIOS switch via entry_pc, XA guard for FMVs

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) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-05-29 23:59:35 -07:00
parent aef852e11b
commit 8b59da8202
5 changed files with 67 additions and 10 deletions
+5 -3
View File
@@ -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);
+3
View File
@@ -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
+26 -4
View File
@@ -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) {
+21
View File
@@ -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];
+12 -3
View File
@@ -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");