SIO alias dispatch + instant disc speed + game miss collector

SIO handler alias (0x0CF0 → func_0000641C):
  Runtime capture showed BIOS installs lui+addiu+jalr to RAM 0x641C at 0x0CF0.
  New [[recompiler.bios_aliases]] TOML mechanism emits a one-liner wrapper and
  registers it in the dispatch table. Eliminates 30K dirty_ram blocks/session.

Instant disc speed (disc_speed = "instant" in game.toml [runtime]):
  cdrom_set_speed(0) collapses all seek/read delays to 1 cycle. Correct INT
  sequence still fires; no artificial wait. "2x" and "4x" divisors available.
  Default remains authentic 1x. Tomba set to instant.

Persistent game miss collector (tools/collect_game_misses.py):
  Appends newly-seen dirty_ram dispatch misses in the game text range to a
  file (TombaRecomp/seeds/dirty_ram_misses.txt) for later seeding.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-05-29 23:42:16 -07:00
parent f7258927ae
commit aef852e11b
9 changed files with 168 additions and 16 deletions
+19
View File
@@ -54,6 +54,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
rt.memcard_dir = fs::absolute(root / rel);
rt.has_memcard_dir = true;
}
if (runtime.contains("disc_speed")) {
rt.disc_speed = toml::find<std::string>(runtime, "disc_speed");
rt.has_disc_speed = true;
}
return rt;
}
@@ -204,6 +208,20 @@ BiosConfig load_bios_config(const fs::path& config_path_in) {
}
}
// [[recompiler.bios_aliases]] — fixed-target RAM trampolines
std::vector<BiosAlias> bios_aliases;
if (recomp.contains("bios_aliases")) {
const auto& arr = recomp.at("bios_aliases").as_array();
for (const auto& v : arr) {
BiosAlias ba;
ba.ram_addr = parse_hex(toml::find<std::string>(v, "ram_addr"),
"bios_aliases.ram_addr");
ba.target_key = parse_hex(toml::find<std::string>(v, "target_key"),
"bios_aliases.target_key");
bios_aliases.push_back(ba);
}
}
return BiosConfig{
/*config_path*/ config_path,
/*project_root*/ root,
@@ -218,6 +236,7 @@ BiosConfig load_bios_config(const fs::path& config_path_in) {
/*strict*/ strict,
/*out_stem*/ out_stem,
/*bios_vectors*/ std::move(bios_vectors),
/*bios_aliases*/ std::move(bios_aliases),
/*runtime*/ parse_runtime_block(cfg, root),
};
}
+16
View File
@@ -36,6 +36,12 @@ struct RuntimeConfig {
bool has_memcard_dir = false;
std::filesystem::path memcard_dir; // absolute path (resolved against project root)
// disc_speed: "1x" (default) | "2x" | "4x" | "instant"
// Controls how quickly CD-ROM timing delays fire. "instant" collapses all
// seek/read delays to 1 cycle — correct INT sequence, no artificial wait.
bool has_disc_speed = false;
std::string disc_speed; // raw string; main.cpp converts to divisor
};
// One entry from [[recompiler.bios_vectors]].
@@ -54,6 +60,15 @@ struct BiosVectorTable {
uint32_t table_ram_addr;
};
// One entry from [[recompiler.bios_aliases]].
// A RAM address the BIOS installs a simple fixed-target trampoline at
// (e.g. the SIO handler at 0x0CF0 which just jalrs to 0x641C). Emitted as
// a one-liner wrapper in the dispatch table — no table lookup, no switch.
struct BiosAlias {
uint32_t ram_addr; // the installed stub address (e.g. 0x0CF0)
uint32_t target_key; // normalized dispatch key of the target (e.g. 0x641C)
};
struct BiosConfig {
std::filesystem::path config_path; // the toml file itself
std::filesystem::path project_root; // resolved via .gitignore/.git/CMakeLists.txt walk
@@ -72,6 +87,7 @@ struct BiosConfig {
bool strict; // currently always true
std::string out_stem; // derived if not explicit
std::vector<BiosVectorTable> bios_vectors; // optional vector dispatch tables
std::vector<BiosAlias> bios_aliases; // optional fixed-target trampolines
// [runtime] block (optional)
RuntimeConfig runtime;
+25 -4
View File
@@ -908,7 +908,8 @@ void FullFunctionEmitter::emit_dispatch(
const std::string& bios_sha256,
const std::vector<uint8_t>& rom,
uint32_t base_addr,
const std::vector<BiosVectorTable>& bios_vectors)
const std::vector<BiosVectorTable>& bios_vectors,
const std::vector<BiosAlias>& bios_aliases)
{
out += "/* AUTO-GENERATED by psxrecomp-bios --emit-full. DO NOT EDIT.\n";
out += " *\n";
@@ -955,12 +956,31 @@ void FullFunctionEmitter::emit_dispatch(
struct VecHandler { uint32_t ram_addr; std::string func_name; };
std::vector<VecHandler> vec_handlers;
if (!bios_vectors.empty()) {
if (!bios_vectors.empty() || !bios_aliases.empty()) {
// psx_dispatch_call is defined later in this file; forward-declare it
// so the vector handlers can call it for runtime-table fallbacks.
out += "void psx_dispatch_call(CPUState* cpu, uint32_t addr, uint32_t return_addr);\n\n";
}
// --- BIOS fixed-target aliases ---
// Simple one-liner wrappers for trampolines that always redirect to a
// single known function (e.g. SIO handler at 0x0CF0 → func_0000641C).
for (const auto& ba : bios_aliases) {
if (emitted_normalized.count(ba.target_key) == 0) {
out += fmt::format("/* bios_alias 0x{:08X} -> 0x{:08X}: "
"target not in dispatch table, skipped */\n",
ba.ram_addr, ba.target_key);
continue;
}
out += fmt::format("/* BIOS fixed alias: 0x{:08X} -> func_{:08X} */\n",
ba.ram_addr, ba.target_key);
out += fmt::format("static void bios_alias_{:08X}(CPUState* cpu) "
"{{ func_{:08X}(cpu); }}\n\n",
ba.ram_addr, ba.target_key);
vec_handlers.push_back({ba.ram_addr,
fmt::format("bios_alias_{:08X}", ba.ram_addr)});
}
{
const uint32_t base_phys = base_addr & 0x1FFFFFFFu;
for (const auto& bvt : bios_vectors) {
@@ -1148,7 +1168,8 @@ EmitStats FullFunctionEmitter::emit(
const DiscoveryResult& dr,
const std::string& bios_sha256,
const std::string& out_dir,
const std::vector<BiosVectorTable>& bios_vectors)
const std::vector<BiosVectorTable>& bios_vectors,
const std::vector<BiosAlias>& bios_aliases)
{
EmitStats stats;
@@ -1332,7 +1353,7 @@ EmitStats FullFunctionEmitter::emit(
{
std::string dispatch_c;
emit_dispatch(dispatch_c, dr, emitted_normalized, unique_continuations,
bios_sha256, rom, base_addr, bios_vectors);
bios_sha256, rom, base_addr, bios_vectors, bios_aliases);
std::string path = out_dir + "/SCPH1001_dispatch.c";
std::ofstream f(path, std::ios::binary);
if (!f) throw std::runtime_error(fmt::format("cannot write {}", path));
+4 -2
View File
@@ -63,7 +63,8 @@ public:
const DiscoveryResult& dr,
const std::string& bios_sha256,
const std::string& out_dir,
const std::vector<BiosVectorTable>& bios_vectors = {});
const std::vector<BiosVectorTable>& bios_vectors = {},
const std::vector<BiosAlias>& bios_aliases = {});
private:
// Emit a single function's C code to the output stream.
@@ -97,7 +98,8 @@ private:
const std::string& bios_sha256,
const std::vector<uint8_t>& rom,
uint32_t base_addr,
const std::vector<BiosVectorTable>& bios_vectors);
const std::vector<BiosVectorTable>& bios_vectors,
const std::vector<BiosAlias>& bios_aliases);
static uint32_t normalize_address(uint32_t addr);
static uint32_t read_u32_le(const std::vector<uint8_t>& rom, uint32_t offset);
+5 -4
View File
@@ -709,7 +709,8 @@ int run_boot_slice(const fs::path& bios_path, const fs::path& out_dir,
int run_emit_full(const fs::path& bios_path, const fs::path& out_dir,
const fs::path& seed_path,
const std::vector<PSXRecompV4::BiosVectorTable>& bios_vectors = {}) {
const std::vector<PSXRecompV4::BiosVectorTable>& bios_vectors = {},
const std::vector<PSXRecompV4::BiosAlias>& bios_aliases = {}) {
// 1. Load + validate BIOS file.
const auto rom = load_file_strict(bios_path, kBiosSize);
const std::string sha = sha256_hex(rom);
@@ -735,7 +736,7 @@ int run_emit_full(const fs::path& bios_path, const fs::path& out_dir,
// 4. Emit full C.
const auto stats = PSXRecompV4::FullFunctionEmitter::emit(
rom, kBiosBase, kBiosBase + static_cast<uint32_t>(kBiosSize) - 1,
dr, sha, out_dir.string(), bios_vectors);
dr, sha, out_dir.string(), bios_vectors, bios_aliases);
std::fprintf(stdout,
"psxrecomp-bios: EMIT OK emitted=%u skipped=%u instructions=%u "
@@ -835,13 +836,13 @@ int main(int argc, char** argv) {
cfg.out_dir.string().c_str(),
cfg.out_stem.c_str());
return run_emit_full(cfg.rom_path, cfg.out_dir, cfg.seeds_path,
cfg.bios_vectors);
cfg.bios_vectors, cfg.bios_aliases);
}
if (a == "--config=" || a.rfind("--config=", 0) == 0) {
const fs::path config_path = a.substr(std::string("--config=").size());
const auto cfg = PSXRecompV4::load_bios_config(config_path);
return run_emit_full(cfg.rom_path, cfg.out_dir, cfg.seeds_path,
cfg.bios_vectors);
cfg.bios_vectors, cfg.bios_aliases);
}
}
+5
View File
@@ -9,6 +9,11 @@ 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. */
void cdrom_set_speed(int divisor);
/* MMIO read/write (0x1F801800-0x1F801803) */
uint32_t cdrom_read(uint32_t addr);
void cdrom_write(uint32_t addr, uint32_t value);
+21 -6
View File
@@ -86,10 +86,25 @@ 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. */
static int g_disc_speed_divisor = 1;
void cdrom_set_speed(int divisor) {
g_disc_speed_divisor = divisor;
}
static int apply_speed(int delay) {
if (g_disc_speed_divisor == 0) return 1;
int d = delay / g_disc_speed_divisor;
return d < 1 ? 1 : d;
}
static int sector_delay_cycles(void) {
/* PS1 CPU is 33.8688 MHz. CD-ROM sectors arrive at 75 Hz in 1x
* mode, or twice that rate when SetMode bit 7 enables double speed. */
return (mode_reg & 0x80) ? 225792 : 451584;
int base = (mode_reg & 0x80) ? 225792 : 451584;
return apply_speed(base);
}
/* Pending command */
@@ -453,7 +468,7 @@ static void exec_command(uint8_t cmd) {
set_irq(CDIRQ_ACK);
pending.cmd = 0x09;
pending.pending = 1;
pending.delay = 10000;
pending.delay = apply_speed(10000);
pending.phase = 1;
break;
@@ -466,7 +481,7 @@ static void exec_command(uint8_t cmd) {
set_irq(CDIRQ_ACK);
pending.cmd = 0x0A;
pending.pending = 1;
pending.delay = 50000;
pending.delay = apply_speed(50000);
pending.phase = 1;
break;
@@ -517,7 +532,7 @@ static void exec_command(uint8_t cmd) {
set_irq(CDIRQ_ACK);
pending.cmd = 0x15;
pending.pending = 1;
pending.delay = 20000;
pending.delay = apply_speed(20000);
pending.phase = 1;
break;
@@ -529,7 +544,7 @@ static void exec_command(uint8_t cmd) {
set_irq(CDIRQ_ACK);
pending.cmd = 0x1A;
pending.pending = 1;
pending.delay = 30000;
pending.delay = apply_speed(30000);
pending.phase = 1;
break;
@@ -549,7 +564,7 @@ static void exec_command(uint8_t cmd) {
set_irq(CDIRQ_ACK);
pending.cmd = 0x1E;
pending.pending = 1;
pending.delay = 100000;
pending.delay = apply_speed(100000);
pending.phase = 1;
break;
+12
View File
@@ -1072,6 +1072,7 @@ int main(int argc, char** argv) {
uint16_t debug_port = (uint16_t)DEFAULT_DEBUG_PORT;
std::string game_name;
std::string game_id;
std::string disc_speed; /* "1x" | "2x" | "4x" | "instant" */
if (game_config_path) {
try {
@@ -1082,6 +1083,7 @@ int main(int argc, char** argv) {
if (gc.runtime.has_memcard_dir) memcard_dir = gc.runtime.memcard_dir;
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;
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) {
@@ -1123,6 +1125,16 @@ int main(int argc, char** argv) {
sio_connect_pad(0); /* Controller on port 1 */
spu_init();
cdrom_init(disc_path_str.empty() ? NULL : disc_path_str.c_str());
{
int divisor = 1; /* default: authentic 1x timing */
if (disc_speed == "instant") divisor = 0;
else if (disc_speed == "4x") divisor = 4;
else if (disc_speed == "2x") divisor = 2;
if (divisor != 1)
std::fprintf(stdout, "psxrecomp: disc_speed=%s (divisor=%d)\n",
disc_speed.c_str(), divisor);
cdrom_set_speed(divisor);
}
memcard_init(memcard_dir_str.c_str());
std::atexit(memcard_flush_all);
#ifndef PSX_NO_DEBUG_TOOLS
+61
View File
@@ -0,0 +1,61 @@
"""Collect dirty_ram dispatch misses in the game text range and append
new unique addresses to a persistent log file. Run this during gameplay
to build up the seed list over time.
Usage: python3 collect_game_misses.py <port> <game_text_lo> <game_text_hi> <output_file>
Example (Tomba): python3 collect_game_misses.py 4470 0x10000 0x98000 seeds/dirty_ram_misses.txt
"""
import socket, json, sys, os
port = int(sys.argv[1], 0) if len(sys.argv) > 1 else 4470
lo = int(sys.argv[2], 0) if len(sys.argv) > 2 else 0x10000
hi = int(sys.argv[3], 0) if len(sys.argv) > 3 else 0x98000
out_file = sys.argv[4] if len(sys.argv) > 4 else 'dirty_ram_misses.txt'
def send(cmd, **kw):
s = socket.create_connection(('127.0.0.1', port), timeout=5)
s.sendall(json.dumps({'id':1,'cmd':cmd,**kw}).encode() + b'\n')
d = b''
while True:
c = s.recv(65536)
if not c: break
d += c
try: json.loads(d.decode()); break
except: pass
s.close()
return json.loads(d.decode())
# Load existing known addresses
known = set()
if os.path.exists(out_file):
with open(out_file) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
try:
known.add(int(line.split()[0], 0))
except:
pass
r = send('dirty_ram_stats')
if not r.get('ok'):
print('error:', r); sys.exit(1)
new_entries = []
for entry in r.get('per_pc', []):
pc = int(entry['pc'], 16)
if lo <= pc < hi:
vaddr = 0x80000000 | pc
if vaddr not in known:
new_entries.append((vaddr, entry['hits'], entry['insns']))
known.add(vaddr)
if new_entries:
with open(out_file, 'a') as f:
for vaddr, hits, insns in sorted(new_entries, key=lambda x: -x[1]):
f.write(f"0x{vaddr:08X} # dirty_ram miss: {hits} hits, {insns} insns\n")
print(f"Appended {len(new_entries)} new addresses to {out_file}")
for vaddr, hits, insns in sorted(new_entries, key=lambda x: -x[1]):
print(f" 0x{vaddr:08X} ({hits} hits, {insns} insns)")
else:
print(f"No new addresses (already have {len(known)} in {out_file})")