pi: mirror ROM into kseg1 region of rdram

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) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley 2026-04-29 16:36:07 -07:00
parent 0bb76b0fc7
commit e43892bb5f
3 changed files with 53 additions and 0 deletions

View file

@ -78,6 +78,11 @@ namespace recomp {
bool is_rom_loaded();
void set_rom_contents(std::vector<uint8_t>&& new_rom);
std::span<const uint8_t> 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();

View file

@ -21,6 +21,44 @@ void recomp::set_rom_contents(std::vector<uint8_t>&& 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<const uint8_t> recomp::get_rom() {
return rom;
}

View file

@ -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;