#include #include #include #include #include #include #include namespace n64 { #ifdef KAIZEN_JIT_ENABLED Mem::Mem(JIT &jit) : flash(saveData), jit(jit) { #else Mem::Mem() : flash(saveData) { #endif rom.cart.resize(CART_SIZE); std::ranges::fill(rom.cart, 0); isviewer_sink = std::ofstream("isviewer.log"); } void Mem::Reset() { std::ranges::fill(isviewer, 0); flash.Reset(); if (saveData.is_mapped()) { std::error_code error; saveData.sync(error); if (error) { Util::Error::GetInstance().Throw({Util::Error::Severity::NON_FATAL}, {Util::Error::Type::COULD_NOT_SYNC_SAVE_DATA}, {}, {}, "[Mem]: Could not sync save data!"); return; } saveData.unmap(); } mmio.Reset(); } void Mem::LoadSRAM(SaveType save_type, fs::path path) { if (save_type == SAVE_SRAM_256k) { std::error_code error; std::string savePath = Options::GetInstance().GetValue("general", "savePath"); if (!savePath.empty()) { path = savePath / path.filename(); } sramPath = path.replace_extension(".sram").string(); if (saveData.is_mapped()) { saveData.sync(error); if (error) { Util::Error::GetInstance().Throw({Util::Error::Severity::NON_FATAL}, {Util::Error::Type::COULD_NOT_SYNC_SAVE_DATA}, {}, {}, R"([Mem]: Could not sync save data stored @ "{}")", sramPath); return; } saveData.unmap(); } auto sramVec = ircolib::ReadFileBinary(sramPath); if (sramVec.empty()) { ircolib::WriteFileBinary(std::array{}, sramPath); sramVec = ircolib::ReadFileBinary(sramPath); } if (sramVec.size() != SRAM_SIZE) { Util::Error::GetInstance().Throw( {Util::Error::Severity::NON_FATAL}, {Util::Error::Type::SAVE_DATA_IS_CORRUPT_OR_INVALID_SIZE}, {}, {}, "[Mem]: Save data is corrupt or has unexpected size! (it's {} KiB)", sramVec.size() / 1024); return; } saveData = mio::make_mmap_sink(sramPath, error); if (error) { Util::Error::GetInstance().Throw({Util::Error::Severity::NON_FATAL}, {Util::Error::Type::MMAP_MAKE_SINK_ERROR}, {}, {}, R"([Mem]: Could not create file sink for save data @ "{}")", sramPath); } } } FORCE_INLINE void SetROMCIC(u32 checksum, ROM &rom) { switch (checksum) { case 0xEC8B1325: rom.cicType = CIC_NUS_7102; break; // 7102 case 0x1DEB51A9: rom.cicType = CIC_NUS_6101; break; // 6101 case 0xC08E5BD6: rom.cicType = CIC_NUS_6102_7101; break; case 0x03B8376A: rom.cicType = CIC_NUS_6103_7103; break; case 0xCF7F41DC: rom.cicType = CIC_NUS_6105_7105; break; case 0xD1059C6A: rom.cicType = CIC_NUS_6106_7106; break; default: warn("Could not determine CIC TYPE! Checksum: 0x{:08X} is unknown!", checksum); rom.cicType = UNKNOWN_CIC_TYPE; break; } } void Mem::LoadROM(const bool isArchive, const std::string &filename) { u32 endianness; { size_t sizeAdjusted; std::vector buf{}; if (isArchive) { buf = Util::OpenArchive(filename, sizeAdjusted); } else { buf = Util::OpenROM(filename, sizeAdjusted); } endianness = std::byteswap(ircolib::ReadAccess(buf, 0)); Util::SwapN64Rom(buf, endianness); std::ranges::copy(buf, rom.cart.begin()); rom.mask = sizeAdjusted - 1; memcpy(&rom.header, buf.data(), sizeof(ROMHeader)); } memcpy(rom.gameNameCart, rom.header.imageName, sizeof(rom.header.imageName)); rom.header.clockRate = std::byteswap(rom.header.clockRate); rom.header.programCounter = std::byteswap(rom.header.programCounter); rom.header.release = std::byteswap(rom.header.release); rom.header.crc1 = std::byteswap(rom.header.crc1); rom.header.crc2 = std::byteswap(rom.header.crc2); rom.header.unknown = std::byteswap(rom.header.unknown); rom.header.unknown2 = std::byteswap(rom.header.unknown2); rom.header.manufacturerId = std::byteswap(rom.header.manufacturerId); rom.header.cartridgeId = std::byteswap(rom.header.cartridgeId); rom.code[0] = rom.header.manufacturerId & 0xFF; rom.code[1] = (rom.header.cartridgeId >> 8) & 0xFF; rom.code[2] = rom.header.cartridgeId & 0xFF; rom.code[3] = '\0'; for (int i = sizeof(rom.header.imageName) - 1; rom.gameNameCart[i] == ' '; i--) { rom.gameNameCart[i] = '\0'; } const u32 checksum = SDL_crc32(0, &rom.cart[0x40], 0x9C0); SetROMCIC(checksum, rom); endianness = std::byteswap(ircolib::ReadAccess(rom.cart, 0)); Util::SwapN64Rom(rom.cart, endianness); rom.pal = IsROMPAL(); } template <> u8 Mem::Read(const u32 paddr) { n64::Registers ®s = n64::Core::GetRegs(); const SI &si = mmio.si; if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) return mmio.rdp.ReadRDRAM(paddr); if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { const auto &src = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; return src[BYTE_ADDRESS(paddr & 0xfff)]; } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) return mmio.pi.BusRead(paddr); if (ircolib::IsInsideRange(paddr, AI_REGION_START, AI_REGION_END)) { const u32 w = mmio.ai.Read(paddr & ~3); const int offs = 3 - (paddr & 3); return w >> offs * 8 & 0xff; } if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) { Util::Error::GetInstance().Throw( {Util::Error::Severity::NON_FATAL}, {Util::Error::Type::MEM_INVALID_ACCESS}, regs.pc, Util::Error::MemoryAccess{false, Util::Error::MemoryAccess::BYTE, paddr, 0}, "8-bit read access from MMIO"); return 0; } if (ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END)) return si.pif.bootrom[BYTE_ADDRESS(paddr) - PIF_ROM_REGION_START]; if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) return si.pif.ram[paddr - PIF_RAM_REGION_START]; if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return 0; Util::Error::GetInstance().Throw({Util::Error::Severity::NON_FATAL}, {Util::Error::Type::MEM_UNHANDLED_ACCESS}, regs.pc, Util::Error::MemoryAccess{false, Util::Error::MemoryAccess::BYTE, paddr, 0}, "8-bit read access in unhandled region"); return 0; } template <> u16 Mem::Read(const u32 paddr) { n64::Registers ®s = n64::Core::GetRegs(); const SI &si = mmio.si; if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) return mmio.rdp.ReadRDRAM(paddr); if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { const auto &src = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; return ircolib::ReadAccess(src, HALF_ADDRESS(paddr & 0xfff)); } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) return mmio.pi.BusRead(paddr); if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) return mmio.Read(paddr); if (ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END)) return ircolib::ReadAccess(si.pif.bootrom, HALF_ADDRESS(paddr) - PIF_ROM_REGION_START); if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) return std::byteswap(ircolib::ReadAccess(si.pif.ram, paddr - PIF_RAM_REGION_START)); if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return 0; Util::Error::GetInstance().Throw({Util::Error::Severity::NON_FATAL}, {Util::Error::Type::MEM_UNHANDLED_ACCESS}, regs.pc, Util::Error::MemoryAccess{false, Util::Error::MemoryAccess::SHORT, paddr, 0}, "16-bit read access in unhandled region"); return 0; } template <> u32 Mem::Read(const u32 paddr) { n64::Registers ®s = n64::Core::GetRegs(); const SI &si = mmio.si; if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) return mmio.rdp.ReadRDRAM(paddr); if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { const auto &src = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; return ircolib::ReadAccess(src, paddr & 0xfff); } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) return mmio.pi.BusRead(paddr); if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) return mmio.Read(paddr); if (ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END)) return ircolib::ReadAccess(si.pif.bootrom, paddr - PIF_ROM_REGION_START); if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) return std::byteswap(ircolib::ReadAccess(si.pif.ram, paddr - PIF_RAM_REGION_START)); if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return 0; Util::Error::GetInstance().Throw({Util::Error::Severity::NON_FATAL}, {Util::Error::Type::MEM_UNHANDLED_ACCESS}, regs.pc, Util::Error::MemoryAccess{false, Util::Error::MemoryAccess::WORD, paddr, 0}, "32-bit read access in unhandled region"); return 0; } template <> u64 Mem::Read(const u32 paddr) { n64::Registers ®s = n64::Core::GetRegs(); const SI &si = mmio.si; if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) return mmio.rdp.ReadRDRAM(paddr); if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { const auto &src = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; return ircolib::ReadAccess(src, paddr & 0xfff); } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) return mmio.pi.BusRead(paddr); if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) return mmio.Read(paddr); if (ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END)) return ircolib::ReadAccess(si.pif.bootrom, paddr - PIF_ROM_REGION_START); if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) return std::byteswap(ircolib::ReadAccess(si.pif.ram, paddr - PIF_RAM_REGION_START)); if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return 0; Util::Error::GetInstance().Throw({Util::Error::Severity::NON_FATAL}, {Util::Error::Type::MEM_UNHANDLED_ACCESS}, regs.pc, Util::Error::MemoryAccess{false, Util::Error::MemoryAccess::DWORD, paddr, 0}, "64-bit read access in unhandled region"); return 0; } template <> void Mem::Write(u32 paddr, u32 val) { n64::Registers ®s = n64::Core::GetRegs(); SI &si = mmio.si; #ifdef KAIZEN_JIT_ENABLED jit.InvalidateBlock(paddr); #endif if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) { mmio.rdp.WriteRDRAM(paddr, val); return; } if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { val = val << (8 * (3 - (paddr & 3))); auto &dest = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; paddr = (paddr & 0xFFF) & ~3; ircolib::WriteAccess(dest, paddr, val); return; } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) { trace("BusWrite @ {:08X} = {:02X}", paddr, val); mmio.pi.BusWrite(paddr, val); return; } if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) panic("MMIO Write!"); if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) { val = val << (8 * (3 - (paddr & 3))); paddr = (paddr - PIF_RAM_REGION_START) & ~3; ircolib::WriteAccess(si.pif.ram, paddr, std::byteswap(val)); si.pif.ProcessCommands(); return; } if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return; panic("Unimplemented 8-bit write at address {:08X} with value {:02X} (PC = {:016X})", paddr, val, (u64)regs.pc); } template <> void Mem::Write(u32 paddr, u32 val) { n64::Registers ®s = n64::Core::GetRegs(); SI &si = mmio.si; #ifdef KAIZEN_JIT_ENABLED jit.InvalidateBlock(paddr); #endif if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) { mmio.rdp.WriteRDRAM(paddr, val); return; } if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { val = val << (16 * !(paddr & 2)); auto &dest = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; paddr = (paddr & 0xFFF) & ~3; ircolib::WriteAccess(dest, paddr, val); return; } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) { trace("BusWrite @ {:08X} = {:04X}", paddr, val); mmio.pi.BusWrite(paddr, val); return; } if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) panic("MMIO Write!"); if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) { val = val << (16 * !(paddr & 2)); paddr &= ~3; ircolib::WriteAccess(si.pif.ram, paddr - PIF_RAM_REGION_START, std::byteswap(val)); si.pif.ProcessCommands(); return; } if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return; panic("Unimplemented 16-bit write at address {:08X} with value {:04X} (PC = {:016X})", paddr, val, (u64)regs.pc); } template <> void Mem::Write(const u32 paddr, const u32 val) { n64::Registers ®s = n64::Core::GetRegs(); SI &si = mmio.si; #ifdef KAIZEN_JIT_ENABLED jit.InvalidateBlock(paddr); #endif if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) { mmio.rdp.WriteRDRAM(paddr, val); return; } if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { auto &dest = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; ircolib::WriteAccess(dest, paddr & 0xfff, val); return; } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) { trace("BusWrite @ {:08X} = {:08X}", paddr, val); mmio.pi.BusWrite(paddr, val); return; } if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) { mmio.Write(paddr, val); return; } if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) { ircolib::WriteAccess(si.pif.ram, paddr - PIF_RAM_REGION_START, std::byteswap(val)); si.pif.ProcessCommands(); return; } if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return; panic("Unimplemented 32-bit write at address {:08X} with value {:08X} (PC = {:016X})", paddr, val, (u64)regs.pc); } void Mem::Write(const u32 paddr, u64 val) { n64::Registers ®s = n64::Core::GetRegs(); SI &si = mmio.si; #ifdef KAIZEN_JIT_ENABLED jit.InvalidateBlock(paddr); #endif if (ircolib::IsInsideRange(paddr, RDRAM_REGION_START, RDRAM_REGION_END)) { mmio.rdp.WriteRDRAM(paddr, val); return; } if (ircolib::IsInsideRange(paddr, DMEM_REGION_START, RSP_MEM_REGION_END)) { auto &dest = paddr & 0x1000 ? mmio.rsp.imem : mmio.rsp.dmem; val >>= 32; ircolib::WriteAccess(dest, paddr & 0xfff, val); return; } if (ircolib::IsInsideRange(paddr, CART_REGION_START_2_1, CART_REGION_END_1_2)) { trace("BusWrite @ {:08X} = {:016X}", paddr, val); mmio.pi.BusWrite(paddr, val); return; } if (ircolib::IsInsideRange(paddr, MMIO_REGION_START_1, MMIO_REGION_END_1) || ircolib::IsInsideRange(paddr, MMIO_REGION_START_2, MMIO_REGION_END_2)) panic("MMIO Write!"); if (ircolib::IsInsideRange(paddr, PIF_RAM_REGION_START, PIF_RAM_REGION_END)) { ircolib::WriteAccess(si.pif.ram, paddr - PIF_RAM_REGION_START, std::byteswap(val)); si.pif.ProcessCommands(); return; } if (ircolib::IsInsideRange(paddr, UNUSED_START_1, UNUSED_END_1) || // unused ircolib::IsInsideRange(paddr, UNUSED_START_2, UNUSED_END_2) || ircolib::IsInsideRange(paddr, UNUSED_START_3, UNUSED_END_3) || ircolib::IsInsideRange(paddr, PIF_ROM_REGION_START, PIF_ROM_REGION_END) || ircolib::IsInsideRange(paddr, UNUSED_START_4, UNUSED_END_4)) return; panic("Unimplemented 64-bit write at address {:08X} with value {:016X} (PC = {:016X})", paddr, val, (u64)regs.pc); } template <> u32 Mem::BackupRead(const u32 addr) { switch (saveType) { case SAVE_NONE: return 0; case SAVE_EEPROM_4k: case SAVE_EEPROM_16k: warn("Accessing cartridge backup type SAVE_EEPROM, returning 0 for word read"); return 0; case SAVE_FLASH_1m: return flash.Read(addr); case SAVE_SRAM_256k: return 0xFFFFFFFF; default: panic("Backup read word with unknown save type"); } } template <> u8 Mem::BackupRead(const u32 addr) { switch (saveType) { case SAVE_NONE: return 0; case SAVE_EEPROM_4k: case SAVE_EEPROM_16k: warn("Accessing cartridge backup type SAVE_EEPROM, returning 0 for word read"); return 0; case SAVE_FLASH_1m: return flash.Read(addr); case SAVE_SRAM_256k: if (saveData.is_mapped()) { assert(addr < saveData.size()); return saveData[addr]; } else { panic("Invalid backup Read if save data is not initialized"); } default: panic("Backup read word with unknown save type"); } } template <> void Mem::BackupWrite(const u32 addr, const u32 val) { switch (saveType) { case SAVE_NONE: warn("Accessing cartridge with save type SAVE_NONE in write word"); break; case SAVE_EEPROM_4k: case SAVE_EEPROM_16k: panic("Accessing cartridge with save type SAVE_EEPROM in write word"); case SAVE_FLASH_1m: flash.Write(addr, val); break; case SAVE_SRAM_256k: break; default: panic("Backup read word with unknown save type"); } } template <> void Mem::BackupWrite(const u32 addr, const u8 val) { switch (saveType) { case SAVE_NONE: warn("Accessing cartridge with save type SAVE_NONE in write word"); break; case SAVE_EEPROM_4k: case SAVE_EEPROM_16k: panic("Accessing cartridge with save type SAVE_EEPROM in write word"); case SAVE_FLASH_1m: flash.Write(addr, val); break; case SAVE_SRAM_256k: if (saveData.is_mapped()) { assert(addr < saveData.size()); saveData[addr] = val; } else { panic("Invalid backup Write if save data is not initialized"); } break; default: panic("Backup read word with unknown save type"); } } } // namespace n64