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;