From aef852e11bc33a2582474ee3fecbab9b8169fe93 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 29 May 2026 23:42:16 -0700 Subject: [PATCH] SIO alias dispatch + instant disc speed + game miss collector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- recompiler/src/config_loader.cpp | 19 ++++++++ recompiler/src/config_loader.h | 16 +++++++ recompiler/src/full_function_emitter.cpp | 29 +++++++++-- recompiler/src/full_function_emitter.h | 6 ++- recompiler/src/main_bios.cpp | 9 ++-- runtime/include/cdrom.h | 5 ++ runtime/src/cdrom.c | 27 ++++++++--- runtime/src/main.cpp | 12 +++++ tools/collect_game_misses.py | 61 ++++++++++++++++++++++++ 9 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 tools/collect_game_misses.py diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index 736ef7c5..c05b8947 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -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(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 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(v, "ram_addr"), + "bios_aliases.ram_addr"); + ba.target_key = parse_hex(toml::find(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), }; } diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index aee5d197..72e03bba 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -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 bios_vectors; // optional vector dispatch tables + std::vector bios_aliases; // optional fixed-target trampolines // [runtime] block (optional) RuntimeConfig runtime; diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index 0515b154..ba247bc4 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -908,7 +908,8 @@ void FullFunctionEmitter::emit_dispatch( const std::string& bios_sha256, const std::vector& rom, uint32_t base_addr, - const std::vector& bios_vectors) + const std::vector& bios_vectors, + const std::vector& 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 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& bios_vectors) + const std::vector& bios_vectors, + const std::vector& 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)); diff --git a/recompiler/src/full_function_emitter.h b/recompiler/src/full_function_emitter.h index b92ae761..613e1030 100644 --- a/recompiler/src/full_function_emitter.h +++ b/recompiler/src/full_function_emitter.h @@ -63,7 +63,8 @@ public: const DiscoveryResult& dr, const std::string& bios_sha256, const std::string& out_dir, - const std::vector& bios_vectors = {}); + const std::vector& bios_vectors = {}, + const std::vector& 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& rom, uint32_t base_addr, - const std::vector& bios_vectors); + const std::vector& bios_vectors, + const std::vector& bios_aliases); static uint32_t normalize_address(uint32_t addr); static uint32_t read_u32_le(const std::vector& rom, uint32_t offset); diff --git a/recompiler/src/main_bios.cpp b/recompiler/src/main_bios.cpp index 96161e33..476f4e6d 100644 --- a/recompiler/src/main_bios.cpp +++ b/recompiler/src/main_bios.cpp @@ -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& bios_vectors = {}) { + const std::vector& bios_vectors = {}, + const std::vector& 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(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); } } diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index bf79dc80..e9d27312 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -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); diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index e674d20e..e2233e4b 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -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; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 58dbf5ca..528445ec 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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 diff --git a/tools/collect_game_misses.py b/tools/collect_game_misses.py new file mode 100644 index 00000000..f49ed3d3 --- /dev/null +++ b/tools/collect_game_misses.py @@ -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 +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})")