From e43892bb5f16fe8ea69e28f2b414e25115b60ff1 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Wed, 29 Apr 2026 16:36:07 -0700 Subject: [PATCH] pi: mirror ROM into kseg1 region of rdram MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds recomp::mirror_rom_to_kseg1(rdram) that copies the loaded ROM into rdram + 0x30000000 with XOR-3-byte-swapped storage so direct MIPS reads of cart vaddrs return the expected bytes. Why: The recompiler's MEM_W formula maps kseg1 vaddrs into the second 512 MiB of the rdram allocation: MEM_W(addr) = *(int32_t*)(rdram + (addr - 0xFFFFFFFF80000000)) For addr=0xB0000000, that's rdram + 0x30000000. Previously, ROM bytes were only available via osPiStartDma -> do_rom_read; direct reads of cart vaddrs (e.g. `lw $t0, 0xE38($t9)` with $t9=0xB000_0000) hit never-written rdram and returned garbage. Visible symptom: Pokemon Stadium's Game_DoCopyProtection at 0x80028FA0 reads *(u32*)0xB0000E38 and compares the low 16 bits against 0x828A. Without the mirror that read returned 0, the magic check tripped, and the function returned -0x10 = 0xFFFFFFF0 — a sentinel state that the main state-machine switch doesn't handle. Title screen flashed for 1-2 frames then reverted to intro on every run. After the fix: copyprot returns input state unchanged, title screen stays up cleanly. Verified via Stadium harness: gCurrentGameState transitions 0x01 -> 0x02 and stays there through 25s of idle with zero copyprot trips logged. Generality: Any N64 game with a ROM-checksum / magic-word check (Stadium, many original-IP games, anti-piracy code in others) needs this. CIC-NUS-6103 / 6105 / 6106 boot also reads ROM via virtual addresses. The mirror is install-once at game-start time, after load_stored_rom populates the rom vector. Co-Authored-By: Claude Opus 4.7 (1M context) --- librecomp/include/librecomp/game.hpp | 5 ++++ librecomp/src/pi.cpp | 38 ++++++++++++++++++++++++++++ librecomp/src/recomp.cpp | 10 ++++++++ 3 files changed, 53 insertions(+) diff --git a/librecomp/include/librecomp/game.hpp b/librecomp/include/librecomp/game.hpp index 6df55ee..2cfcda6 100644 --- a/librecomp/include/librecomp/game.hpp +++ b/librecomp/include/librecomp/game.hpp @@ -78,6 +78,11 @@ namespace recomp { bool is_rom_loaded(); void set_rom_contents(std::vector&& new_rom); std::span get_rom(); + // Mirror ROM into rdram's kseg1 region so direct MIPS reads of + // cart vaddrs (e.g. lw $t1, 0xB0000E38) return correct bytes. + // Call once after rdram is allocated. Without this, ROM-checksum + // / copy-protection routines see garbage. See pi.cpp for detail. + void mirror_rom_to_kseg1(uint8_t* rdram); void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes); void do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr); const Version& get_project_version(); diff --git a/librecomp/src/pi.cpp b/librecomp/src/pi.cpp index 43509ba..6feaf71 100644 --- a/librecomp/src/pi.cpp +++ b/librecomp/src/pi.cpp @@ -21,6 +21,44 @@ void recomp::set_rom_contents(std::vector&& new_rom) { rom = std::move(new_rom); } +// Mirror ROM into the kseg1 region of rdram so that direct MIPS +// reads of cart vaddrs (e.g. `lw $t1, 0xB0000E38`) return the +// correct ROM bytes. Some games read ROM via cart vaddrs without +// going through osPiStartDma — most notably copy-protection / +// CIC-checksum routines that read a known ROM word and compare +// against an expected value (e.g. Pokemon Stadium's +// Game_DoCopyProtection at *(u32*)0xB0000E38). +// +// Without this mirror, MEM_W of a cart vaddr reads rdram bytes +// that were never written, returning garbage. The check trips +// and the game falls into a copy-protection error path +// (e.g. state = -0x10 in Stadium, which then bypasses the +// state-machine switch and the game appears to "stutter back to +// intro"). +// +// MEM_W formula: rdram + (vaddr - 0xFFFFFFFF80000000). For +// kseg1 cart base 0xB0000000, that's rdram + 0x30000000. +// We use the recompiler's BE-byte-swapped storage convention +// (XOR-3 byte index) so that a host-native int32_t read of the +// mirrored bytes returns the BE word that Stadium expects. +// +// Cost: one-time copy at startup, ~32 MiB worst case (size of +// the ROM image). Negligible vs. the rest of the recompile. +void recomp::mirror_rom_to_kseg1(uint8_t* rdram) { + if (rom.empty()) { + fprintf(stderr, + "[mirror] mirror_rom_to_kseg1 called with empty rom — " + "kseg1 cart reads will return garbage\n"); + fflush(stderr); + return; + } + constexpr size_t kKseg1RomOffset = 0x30000000; + const size_t n = rom.size(); + for (size_t i = 0; i < n; i++) { + rdram[(kKseg1RomOffset + i) ^ 3] = rom[i]; + } +} + std::span recomp::get_rom() { return rom; } diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index ea17675..485c756 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -646,6 +646,16 @@ bool wait_for_game_started(uint8_t* rdram, recomp_context* context) { ultramodern::error_handling::message_box("Error opening stored ROM! Please restart this program."); } + // Mirror ROM into the kseg1 region of rdram so direct + // MIPS reads of cart vaddrs (e.g. CIC-checksum / + // copy-protection code reading *(u32*)0xB000XXXX) + // return the expected bytes. Without this, MEM_W of + // a cart vaddr lands on never-written rdram bytes + // and games with ROM-magic checks (e.g. Pokemon + // Stadium's Game_DoCopyProtection at *(u32*)0xB0000E38) + // trip and fall into error paths. + recomp::mirror_rom_to_kseg1(rdram); + auto find_it = game_roms.find(current_game.value()); const recomp::GameEntry& game_entry = find_it->second;