Files
dolphin/Source/Core/Core/PowerPC/MMU.cpp
T
JosJuice ff4a7c8a95 Reword the invalid read/write panic alert
Some users seem to be under the impression that the panic alert is
saying that enabling MMU will fix the issue, but that's not what it
actually says. Let's try to make this a bit clearer.
2026-03-24 22:25:01 +01:00

2093 lines
70 KiB
C++

// Copyright 2003 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Some of the code in this file was originally based on PearPC, though it has been modified since.
// We have been given permission by the author to re-license the code under GPLv2+.
/*
* PearPC
* ppc_mmu.cc
*
* Copyright (C) 2003, 2004 Sebastian Biallas ([email protected])
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "Core/PowerPC/MMU.h"
#include <algorithm>
#include <bit>
#include <cstddef>
#include <cstring>
#include <memory>
#include <string>
#include <utility>
#ifdef _M_X86_64
#include <emmintrin.h>
#endif
#include "Common/Align.h"
#include "Common/Assert.h"
#include "Common/BitUtils.h"
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
#include "Core/Core.h"
#include "Core/HW/CPU.h"
#include "Core/HW/GPFifo.h"
#include "Core/HW/MMIO.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/ProcessorInterface.h"
#include "Core/PowerPC/GDBStub.h"
#include "Core/PowerPC/JitInterface.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "VideoCommon/EFBInterface.h"
namespace PowerPC
{
MMU::MMU(Core::System& system, Memory::MemoryManager& memory, PowerPC::PowerPCManager& power_pc)
: m_system(system), m_memory(memory), m_power_pc(power_pc), m_ppc_state(power_pc.GetPPCState())
{
}
MMU::~MMU() = default;
void MMU::Reset()
{
ClearPageTable();
}
void MMU::DoState(PointerWrap& p, bool sr_changed)
{
// Instead of storing m_page_table in savestates, we *could* refetch it from memory
// here in DoState, but this could lead to us getting a more up-to-date set of page mappings
// than we had when the savestate was created, which could be a problem for TAS determinism.
if (p.IsReadMode())
{
if (!m_system.GetJitInterface().WantsPageTableMappings())
{
// Clear page table mappings if we have any.
p.Do(m_page_table);
ClearPageTable();
}
else if (sr_changed)
{
// Non-incremental update of page table mappings.
p.Do(m_page_table);
SRUpdated();
}
else
{
// Incremental update of page table mappings.
p.Do(m_temp_page_table);
PageTableUpdated(m_temp_page_table);
}
}
else
{
p.Do(m_page_table);
}
}
// Overloaded byteswap functions, for use within the templated functions below.
[[maybe_unused]] static u8 bswap(u8 val)
{
return val;
}
[[maybe_unused]] static s8 bswap(s8 val)
{
return val;
}
[[maybe_unused]] static u16 bswap(u16 val)
{
return Common::swap16(val);
}
[[maybe_unused]] static s16 bswap(s16 val)
{
return Common::swap16(val);
}
[[maybe_unused]] static u32 bswap(u32 val)
{
return Common::swap32(val);
}
[[maybe_unused]] static u64 bswap(u64 val)
{
return Common::swap64(val);
}
static constexpr bool IsOpcodeFlag(XCheckTLBFlag flag)
{
return flag == XCheckTLBFlag::Opcode || flag == XCheckTLBFlag::OpcodeNoException;
}
static constexpr bool IsNoExceptionFlag(XCheckTLBFlag flag)
{
return flag == XCheckTLBFlag::NoException || flag == XCheckTLBFlag::OpcodeNoException;
}
// Nasty but necessary. Super Mario Galaxy pointer relies on this stuff.
static u32 EFB_Read(const u32 addr)
{
u32 var = 0;
// Convert address to coordinates. It's possible that this should be done
// differently depending on color depth, especially regarding PeekColor.
const u32 x = (addr & 0xfff) >> 2;
const u32 y = (addr >> 12) & 0x3ff;
if (addr & 0x00800000)
{
ERROR_LOG_FMT(MEMMAP, "Unimplemented Z+Color EFB read @ {:#010x}", addr);
}
else if (addr & 0x00400000)
{
var = g_efb_interface->PeekDepth(x, y);
DEBUG_LOG_FMT(MEMMAP, "EFB Z Read @ {}, {}\t= {:#010x}", x, y, var);
}
else
{
var = g_efb_interface->PeekColor(x, y);
DEBUG_LOG_FMT(MEMMAP, "EFB Color Read @ {}, {}\t= {:#010x}", x, y, var);
}
return var;
}
static void EFB_Write(u32 data, u32 addr)
{
const u32 x = (addr & 0xfff) >> 2;
const u32 y = (addr >> 12) & 0x3ff;
if (addr & 0x00800000)
{
// It's possible to do a z-tested write to EFB by writing a 64bit value to this address range.
// Not much is known, but let's at least get some logging.
ERROR_LOG_FMT(MEMMAP, "Unimplemented Z+Color EFB write. {:08x} @ {:#010x}", data, addr);
}
else if (addr & 0x00400000)
{
g_efb_interface->PokeDepth(x, y, data);
DEBUG_LOG_FMT(MEMMAP, "EFB Z Write {:08x} @ {}, {}", data, x, y);
}
else
{
g_efb_interface->PokeColor(x, y, data);
DEBUG_LOG_FMT(MEMMAP, "EFB Color Write {:08x} @ {}, {}", data, x, y);
}
}
template <XCheckTLBFlag flag, std::unsigned_integral T, bool never_translate>
T MMU::ReadFromHardware(u32 em_address)
{
// ReadFromHardware is currently used with XCheckTLBFlag::OpcodeNoException by host instruction
// functions. Actual instruction decoding (which can raise exceptions and uses icache) is handled
// by TryReadInstruction.
static_assert(flag == XCheckTLBFlag::NoException || flag == XCheckTLBFlag::Read ||
flag == XCheckTLBFlag::OpcodeNoException);
const u32 em_address_start_page = em_address & ~HW_PAGE_MASK;
const u32 em_address_end_page = (em_address + sizeof(T) - 1) & ~HW_PAGE_MASK;
if (em_address_start_page != em_address_end_page)
{
// This could be unaligned down to the byte level... hopefully this is rare, so doing it this
// way isn't too terrible.
// TODO: floats on non-word-aligned boundaries should technically cause alignment exceptions.
// Note that "word" means 32-bit, so paired singles or doubles might still be 32-bit aligned!
T var = 0;
for (u32 i = 0; i < sizeof(T); ++i)
{
var = (var << 8) | ReadFromHardware<flag, u8, never_translate>(em_address + i);
}
return var;
}
bool wi = false;
if (!never_translate &&
(IsOpcodeFlag(flag) ? m_ppc_state.msr.IR.Value() : m_ppc_state.msr.DR.Value()))
{
auto translated_addr = TranslateAddress<flag>(em_address);
if (!translated_addr.Success())
{
if (flag == XCheckTLBFlag::Read)
GenerateDSIException(em_address, false);
return 0;
}
em_address = translated_addr.address;
wi = translated_addr.wi;
}
if (flag == XCheckTLBFlag::Read && (em_address & 0xF8000000) == 0x08000000)
{
if (em_address < 0x0c000000)
{
return EFB_Read(em_address);
}
else
{
return static_cast<T>(
m_memory.GetMMIOMapping()->Read<std::make_unsigned_t<T>>(m_system, em_address));
}
}
// Locked L1 technically doesn't have a fixed address, but games all use 0xE0000000.
if (m_memory.GetL1Cache() && (em_address >> 28) == 0xE &&
(em_address < (0xE0000000 + m_memory.GetL1CacheSize())))
{
T value;
std::memcpy(&value, &m_memory.GetL1Cache()[em_address & 0x0FFFFFFF], sizeof(T));
return bswap(value);
}
if (m_memory.GetRAM() && (em_address & 0xF8000000) == 0x00000000)
{
// Handle RAM; the masking intentionally discards bits (essentially creating
// mirrors of memory).
T value;
em_address &= m_memory.GetRamMask();
if (!m_ppc_state.m_enable_dcache || wi)
{
std::memcpy(&value, &m_memory.GetRAM()[em_address], sizeof(T));
}
else
{
m_ppc_state.dCache.Read(m_memory, em_address, &value, sizeof(T),
HID0(m_ppc_state).DLOCK || flag != XCheckTLBFlag::Read);
}
return bswap(value);
}
if (m_memory.GetEXRAM() && (em_address >> 28) == 0x1 &&
(em_address & 0x0FFFFFFF) < m_memory.GetExRamSizeReal())
{
T value;
em_address &= 0x0FFFFFFF;
if (!m_ppc_state.m_enable_dcache || wi)
{
std::memcpy(&value, &m_memory.GetEXRAM()[em_address], sizeof(T));
}
else
{
m_ppc_state.dCache.Read(m_memory, em_address + 0x10000000, &value, sizeof(T),
HID0(m_ppc_state).DLOCK || flag != XCheckTLBFlag::Read);
}
return bswap(value);
}
// In Fake-VMEM mode, we need to map the memory somewhere into
// physical memory for BAT translation to work; we currently use
// [0x7E000000, 0x80000000).
if (m_memory.GetFakeVMEM() && ((em_address & 0xFE000000) == 0x7E000000))
{
T value;
std::memcpy(&value, &m_memory.GetFakeVMEM()[em_address & m_memory.GetFakeVMemMask()],
sizeof(T));
return bswap(value);
}
PanicAlertFmt("Unable to resolve read address {:x} PC {:x}", em_address, m_ppc_state.pc);
if (m_system.IsPauseOnPanicMode())
{
m_system.GetCPU().Break();
m_ppc_state.Exceptions |= EXCEPTION_DSI | EXCEPTION_FAKE_MEMCHECK_HIT;
}
return 0;
}
template <XCheckTLBFlag flag, bool never_translate>
void MMU::WriteToHardware(u32 em_address, const u32 data, const u32 size)
{
static_assert(flag == XCheckTLBFlag::NoException || flag == XCheckTLBFlag::Write);
DEBUG_ASSERT(size <= 4);
const u32 em_address_start_page = em_address & ~HW_PAGE_MASK;
const u32 em_address_end_page = (em_address + size - 1) & ~HW_PAGE_MASK;
if (em_address_start_page != em_address_end_page)
{
// The write crosses a page boundary. Break it up into two writes.
// TODO: floats on non-word-aligned boundaries should technically cause alignment exceptions.
// Note that "word" means 32-bit, so paired singles or doubles might still be 32-bit aligned!
const u32 first_half_size = em_address_end_page - em_address;
const u32 second_half_size = size - first_half_size;
WriteToHardware<flag, never_translate>(em_address, std::rotr(data, second_half_size * 8),
first_half_size);
WriteToHardware<flag, never_translate>(em_address_end_page, data, second_half_size);
return;
}
bool wi = false;
if (!never_translate && m_ppc_state.msr.DR)
{
auto translated_addr = TranslateAddress<flag>(em_address);
if (!translated_addr.Success())
{
if (flag == XCheckTLBFlag::Write)
GenerateDSIException(em_address, true);
return;
}
em_address = translated_addr.address;
wi = translated_addr.wi;
}
// Check for a gather pipe write (which are not implemented through the MMIO system).
//
// Note that we must mask the address to correctly emulate certain games; Pac-Man World 3
// in particular is affected by this. (See https://bugs.dolphin-emu.org/issues/8386)
//
// The PowerPC 750CL manual says (in section 9.4.2 Write Gather Pipe Operation on page 327):
// "A noncacheable store to an address with bits 0-26 matching WPAR[GB_ADDR] but with bits 27-31
// not all zero will result in incorrect data in the buffer." So, it's possible that in some cases
// writes which do not exactly match the masking behave differently, but Pac-Man World 3's writes
// happen to behave correctly.
if (flag == XCheckTLBFlag::Write &&
(em_address & 0xFFFFF000) == GPFifo::GATHER_PIPE_PHYSICAL_ADDRESS)
{
switch (size)
{
case 1:
m_system.GetGPFifo().Write8(static_cast<u8>(data));
return;
case 2:
m_system.GetGPFifo().Write16(static_cast<u16>(data));
return;
case 4:
m_system.GetGPFifo().Write32(data);
return;
default:
// Some kind of misaligned write. TODO: Does this match how the actual hardware handles it?
auto& gpfifo = m_system.GetGPFifo();
for (size_t i = size * 8; i > 0;)
{
i -= 8;
gpfifo.Write8(static_cast<u8>(data >> i));
}
return;
}
}
if (flag == XCheckTLBFlag::Write && (em_address & 0xF8000000) == 0x08000000)
{
if (em_address < 0x0c000000)
{
EFB_Write(data, em_address);
return;
}
switch (size)
{
case 1:
m_memory.GetMMIOMapping()->Write<u8>(m_system, em_address, static_cast<u8>(data));
return;
case 2:
m_memory.GetMMIOMapping()->Write<u16>(m_system, em_address, static_cast<u16>(data));
return;
case 4:
m_memory.GetMMIOMapping()->Write<u32>(m_system, em_address, data);
return;
default:
// Some kind of misaligned write. TODO: Does this match how the actual hardware handles it?
for (size_t i = size * 8; i > 0; em_address++)
{
i -= 8;
m_memory.GetMMIOMapping()->Write<u8>(m_system, em_address, static_cast<u8>(data >> i));
}
return;
}
}
const u32 swapped_data = Common::swap32(std::rotr(data, size * 8));
// Locked L1 technically doesn't have a fixed address, but games all use 0xE0000000.
if (m_memory.GetL1Cache() && (em_address >> 28 == 0xE) &&
(em_address < (0xE0000000 + m_memory.GetL1CacheSize())))
{
std::memcpy(&m_memory.GetL1Cache()[em_address & 0x0FFFFFFF], &swapped_data, size);
return;
}
if (wi && (size < 4 || (em_address & 0x3)))
{
// When a write to memory is performed in hardware, 64 bits of data are sent to the memory
// controller along with a mask. This mask is encoded using just two bits of data - one for
// the upper 32 bits and one for the lower 32 bits - which leads to some odd data duplication
// behavior for write-through/cache-inhibited writes with a start address or end address that
// isn't 32-bit aligned. See https://bugs.dolphin-emu.org/issues/12565 for details.
// TODO: This interrupt is supposed to have associated cause and address registers
// TODO: This should trigger the hwtest's interrupt handling, but it does not seem to
// (https://github.com/dolphin-emu/hwtests/pull/42)
m_system.GetProcessorInterface().SetInterrupt(ProcessorInterface::INT_CAUSE_PI);
const u32 rotated_data = std::rotr(data, ((em_address & 0x3) + size) * 8);
const u32 start_addr = Common::AlignDown(em_address, 8);
const u32 end_addr = Common::AlignUp(em_address + size, 8);
for (u32 addr = start_addr; addr != end_addr; addr += 8)
{
WriteToHardware<flag, true>(addr, rotated_data, 4);
WriteToHardware<flag, true>(addr + 4, rotated_data, 4);
}
return;
}
if (m_memory.GetRAM() && (em_address & 0xF8000000) == 0x00000000)
{
// Handle RAM; the masking intentionally discards bits (essentially creating
// mirrors of memory).
em_address &= m_memory.GetRamMask();
if (m_ppc_state.m_enable_dcache && !wi)
m_ppc_state.dCache.Write(m_memory, em_address, &swapped_data, size, HID0(m_ppc_state).DLOCK);
if (!m_ppc_state.m_enable_dcache || wi || flag != XCheckTLBFlag::Write)
std::memcpy(&m_memory.GetRAM()[em_address], &swapped_data, size);
return;
}
if (m_memory.GetEXRAM() && (em_address >> 28) == 0x1 &&
(em_address & 0x0FFFFFFF) < m_memory.GetExRamSizeReal())
{
em_address &= 0x0FFFFFFF;
if (m_ppc_state.m_enable_dcache && !wi)
{
m_ppc_state.dCache.Write(m_memory, em_address + 0x10000000, &swapped_data, size,
HID0(m_ppc_state).DLOCK);
}
if (!m_ppc_state.m_enable_dcache || wi || flag != XCheckTLBFlag::Write)
std::memcpy(&m_memory.GetEXRAM()[em_address], &swapped_data, size);
return;
}
// In Fake-VMEM mode, we need to map the memory somewhere into
// physical memory for BAT translation to work; we currently use
// [0x7E000000, 0x80000000).
if (m_memory.GetFakeVMEM() && ((em_address & 0xFE000000) == 0x7E000000))
{
std::memcpy(&m_memory.GetFakeVMEM()[em_address & m_memory.GetFakeVMemMask()], &swapped_data,
size);
return;
}
PanicAlertFmt("Unable to resolve write address {:x} PC {:x}", em_address, m_ppc_state.pc);
if (m_system.IsPauseOnPanicMode())
{
m_system.GetCPU().Break();
m_ppc_state.Exceptions |= EXCEPTION_DSI | EXCEPTION_FAKE_MEMCHECK_HIT;
}
}
// =====================
// =================================
/* These functions are primarily called by the Interpreter functions and are routed to the correct
location through ReadFromHardware and WriteToHardware */
// ----------------
u32 MMU::Read_Opcode(u32 address)
{
TryReadInstResult result = TryReadInstruction(address);
if (!result.valid)
{
GenerateISIException(address);
return 0;
}
return result.hex;
}
TryReadInstResult MMU::TryReadInstruction(u32 address)
{
bool from_bat = true;
if (m_ppc_state.msr.IR)
{
auto tlb_addr = TranslateAddress<XCheckTLBFlag::Opcode>(address);
if (!tlb_addr.Success())
{
return TryReadInstResult{false, false, 0, 0};
}
else
{
address = tlb_addr.address;
from_bat = tlb_addr.result == TranslateAddressResultEnum::BAT_TRANSLATED;
}
}
u32 hex;
// TODO: Refactor this. This icache implementation is totally wrong if used with the fake vmem.
if (m_memory.GetFakeVMEM() && ((address & 0xFE000000) == 0x7E000000))
{
hex = Common::swap32(&m_memory.GetFakeVMEM()[address & m_memory.GetFakeVMemMask()]);
}
else
{
hex = m_ppc_state.iCache.ReadInstruction(m_memory, m_ppc_state, address);
}
return TryReadInstResult{true, from_bat, hex, address};
}
u32 MMU::HostRead_Instruction(const Core::CPUThreadGuard& guard, const u32 address)
{
return guard.GetSystem().GetMMU().ReadFromHardware<XCheckTLBFlag::OpcodeNoException, u32>(
address);
}
std::optional<ReadResult<u32>> MMU::HostTryReadInstruction(const Core::CPUThreadGuard& guard,
const u32 address,
RequestedAddressSpace space)
{
if (!HostIsInstructionRAMAddress(guard, address, space))
return std::nullopt;
auto& mmu = guard.GetSystem().GetMMU();
switch (space)
{
case RequestedAddressSpace::Effective:
{
const u32 value = mmu.ReadFromHardware<XCheckTLBFlag::OpcodeNoException, u32>(address);
return ReadResult<u32>(!!mmu.m_ppc_state.msr.IR, value);
}
case RequestedAddressSpace::Physical:
{
const u32 value = mmu.ReadFromHardware<XCheckTLBFlag::OpcodeNoException, u32, true>(address);
return ReadResult<u32>(false, value);
}
case RequestedAddressSpace::Virtual:
{
if (!mmu.m_ppc_state.msr.IR)
return std::nullopt;
const u32 value = mmu.ReadFromHardware<XCheckTLBFlag::OpcodeNoException, u32>(address);
return ReadResult<u32>(true, value);
}
}
ASSERT(false);
return std::nullopt;
}
void MMU::Memcheck(u32 address, u64 var, bool write, size_t size)
{
if (!m_power_pc.GetMemChecks().HasAny())
return;
TMemCheck* mc = m_power_pc.GetMemChecks().GetMemCheck(address, size);
if (mc == nullptr)
return;
if (m_system.GetCPU().IsStepping())
{
// Disable when stepping so that resume works.
return;
}
mc->num_hits++;
const bool pause = mc->Action(m_system, var, address, write, size, m_ppc_state.pc);
if (!pause)
return;
m_system.GetCPU().Break();
if (GDBStub::IsActive())
GDBStub::TakeControl();
// Fake a DSI so that all the code that tests for it in order to skip
// the rest of the instruction will apply. (This means that
// watchpoints will stop the emulator before the offending load/store,
// not after like GDB does, but that's better anyway. Just need to
// make sure resuming after that works.)
// It doesn't matter if ReadFromHardware triggers its own DSI because
// we'll take it after resuming.
m_ppc_state.Exceptions |= EXCEPTION_DSI | EXCEPTION_FAKE_MEMCHECK_HIT;
}
template <std::unsigned_integral T>
T MMU::Read(const u32 address)
{
T var = ReadFromHardware<XCheckTLBFlag::Read, T>(address);
Memcheck(address, var, false, sizeof(T));
return var;
}
template u8 MMU::Read<u8>(const u32 address);
template u16 MMU::Read<u16>(const u32 address);
template u32 MMU::Read<u32>(const u32 address);
template u64 MMU::Read<u64>(const u32 address);
template <std::unsigned_integral T>
std::optional<ReadResult<T>> MMU::HostTryRead(const Core::CPUThreadGuard& guard, const u32 address,
RequestedAddressSpace space)
{
if (!HostIsRAMAddress(guard, address, space))
return std::nullopt;
auto& mmu = guard.GetSystem().GetMMU();
switch (space)
{
case RequestedAddressSpace::Effective:
{
T value = mmu.ReadFromHardware<XCheckTLBFlag::NoException, T>(address);
return ReadResult<T>(!!mmu.m_ppc_state.msr.DR, std::move(value));
}
case RequestedAddressSpace::Physical:
{
T value = mmu.ReadFromHardware<XCheckTLBFlag::NoException, T, true>(address);
return ReadResult<T>(false, std::move(value));
}
case RequestedAddressSpace::Virtual:
{
if (!mmu.m_ppc_state.msr.DR)
return std::nullopt;
T value = mmu.ReadFromHardware<XCheckTLBFlag::NoException, T>(address);
return ReadResult<T>(true, std::move(value));
}
}
ASSERT(false);
return std::nullopt;
}
template std::optional<ReadResult<u8>> MMU::HostTryRead<u8>(const Core::CPUThreadGuard& guard,
const u32 address,
RequestedAddressSpace space);
template std::optional<ReadResult<u16>> MMU::HostTryRead<u16>(const Core::CPUThreadGuard& guard,
const u32 address,
RequestedAddressSpace space);
template std::optional<ReadResult<u32>> MMU::HostTryRead<u32>(const Core::CPUThreadGuard& guard,
const u32 address,
RequestedAddressSpace space);
template std::optional<ReadResult<u64>> MMU::HostTryRead<u64>(const Core::CPUThreadGuard& guard,
const u32 address,
RequestedAddressSpace space);
template <std::unsigned_integral T>
void MMU::Write(const Common::MakeAtLeastU32<T> var, const u32 address)
{
Memcheck(address, var, true, sizeof(T));
WriteToHardware<XCheckTLBFlag::Write>(address, var, sizeof(T));
}
template void MMU::Write<u8>(const u32 var, const u32 address);
template void MMU::Write<u16>(const u32 var, const u32 address);
template void MMU::Write<u32>(const u32 var, const u32 address);
template <>
void MMU::Write<u64>(const u64 var, const u32 address)
{
Memcheck(address, var, true, 8);
WriteToHardware<XCheckTLBFlag::Write>(address, static_cast<u32>(var >> 32), 4);
WriteToHardware<XCheckTLBFlag::Write>(address + sizeof(u32), static_cast<u32>(var), 4);
}
void MMU::Write_U16_Swap(const u32 var, const u32 address)
{
Write<u16>((var & 0xFFFF0000) | Common::swap16(static_cast<u16>(var)), address);
}
void MMU::Write_U32_Swap(const u32 var, const u32 address)
{
Write<u32>(Common::swap32(var), address);
}
void MMU::Write_U64_Swap(const u64 var, const u32 address)
{
Write<u64>(Common::swap64(var), address);
}
template <std::unsigned_integral T>
T MMU::HostRead(const Core::CPUThreadGuard& guard, const u32 address)
{
auto& mmu = guard.GetSystem().GetMMU();
return mmu.ReadFromHardware<XCheckTLBFlag::NoException, T>(address);
}
template u8 MMU::HostRead<u8>(const Core::CPUThreadGuard& guard, const u32 address);
template u16 MMU::HostRead<u16>(const Core::CPUThreadGuard& guard, const u32 address);
template u32 MMU::HostRead<u32>(const Core::CPUThreadGuard& guard, const u32 address);
template u64 MMU::HostRead<u64>(const Core::CPUThreadGuard& guard, const u32 address);
template <std::unsigned_integral T>
void MMU::HostWrite(const Core::CPUThreadGuard& guard, const Common::MakeAtLeastU32<T> var,
const u32 address)
{
auto& mmu = guard.GetSystem().GetMMU();
mmu.WriteToHardware<XCheckTLBFlag::NoException>(address, var, sizeof(T));
}
template void MMU::HostWrite<u8>(const Core::CPUThreadGuard& guard, const u32 var,
const u32 address);
template void MMU::HostWrite<u16>(const Core::CPUThreadGuard& guard, const u32 var,
const u32 address);
template void MMU::HostWrite<u32>(const Core::CPUThreadGuard& guard, const u32 var,
const u32 address);
template <>
void MMU::HostWrite<u64>(const Core::CPUThreadGuard& guard, const u64 var, const u32 address)
{
auto& mmu = guard.GetSystem().GetMMU();
mmu.WriteToHardware<XCheckTLBFlag::NoException>(address, static_cast<u32>(var >> 32), 4);
mmu.WriteToHardware<XCheckTLBFlag::NoException>(address + sizeof(u32), static_cast<u32>(var), 4);
}
template <std::unsigned_integral T>
std::optional<WriteResult> MMU::HostTryWrite(const Core::CPUThreadGuard& guard,
const Common::MakeAtLeastU32<T> var, const u32 address,
RequestedAddressSpace space)
{
constexpr auto size = sizeof(T);
if (!HostIsRAMAddress(guard, address, space))
return std::nullopt;
auto& mmu = guard.GetSystem().GetMMU();
switch (space)
{
case RequestedAddressSpace::Effective:
mmu.WriteToHardware<XCheckTLBFlag::NoException>(address, var, size);
return WriteResult(!!mmu.m_ppc_state.msr.DR);
case RequestedAddressSpace::Physical:
mmu.WriteToHardware<XCheckTLBFlag::NoException, true>(address, var, size);
return WriteResult(false);
case RequestedAddressSpace::Virtual:
if (!mmu.m_ppc_state.msr.DR)
return std::nullopt;
mmu.WriteToHardware<XCheckTLBFlag::NoException>(address, var, size);
return WriteResult(true);
}
ASSERT(false);
return std::nullopt;
}
template std::optional<WriteResult> MMU::HostTryWrite<u8>(const Core::CPUThreadGuard& guard,
const u32 var, const u32 address,
RequestedAddressSpace space);
template std::optional<WriteResult> MMU::HostTryWrite<u16>(const Core::CPUThreadGuard& guard,
const u32 var, const u32 address,
RequestedAddressSpace space);
template std::optional<WriteResult> MMU::HostTryWrite<u32>(const Core::CPUThreadGuard& guard,
const u32 var, const u32 address,
RequestedAddressSpace space);
template <>
std::optional<WriteResult> MMU::HostTryWrite<u64>(const Core::CPUThreadGuard& guard, const u64 var,
const u32 address, RequestedAddressSpace space)
{
const auto result = HostTryWrite<u32>(guard, static_cast<u32>(var >> 32), address, space);
if (!result)
return result;
return HostTryWrite<u32>(guard, static_cast<u32>(var), address + 4, space);
}
std::string MMU::HostGetString(const Core::CPUThreadGuard& guard, u32 address, size_t size)
{
std::string s;
do
{
if (!HostIsRAMAddress(guard, address))
break;
u8 res = HostRead<u8>(guard, address);
if (!res)
break;
s += static_cast<char>(res);
++address;
} while (size == 0 || s.length() < size);
return s;
}
std::u16string MMU::HostGetU16String(const Core::CPUThreadGuard& guard, u32 address, size_t size)
{
std::u16string s;
do
{
if (!HostIsRAMAddress(guard, address) || !HostIsRAMAddress(guard, address + 1))
break;
const u16 res = HostRead<u16>(guard, address);
if (!res)
break;
s += static_cast<char16_t>(res);
address += 2;
} while (size == 0 || s.length() < size);
return s;
}
std::optional<ReadResult<std::string>> MMU::HostTryReadString(const Core::CPUThreadGuard& guard,
u32 address, size_t size,
RequestedAddressSpace space)
{
auto c = HostTryRead<u8>(guard, address, space);
if (!c)
return std::nullopt;
if (c->value == 0)
return ReadResult<std::string>(c->translated, "");
std::string s;
s += static_cast<char>(c->value);
while (size == 0 || s.length() < size)
{
++address;
const auto res = HostTryRead<u8>(guard, address, space);
if (!res || res->value == 0)
break;
s += static_cast<char>(res->value);
}
return ReadResult<std::string>(c->translated, std::move(s));
}
bool MMU::IsOptimizableRAMAddress(const u32 address, const u32 access_size) const
{
if (m_power_pc.GetMemChecks().HasAny())
return false;
if (!m_ppc_state.msr.DR)
return false;
if (m_ppc_state.m_enable_dcache)
return false;
// We store whether an access can be optimized to an unchecked access
// in dbat_table.
const u32 last_byte_address = address + (access_size >> 3) - 1;
const u32 bat_result_1 = m_dbat_table[address >> BAT_INDEX_SHIFT];
const u32 bat_result_2 = m_dbat_table[last_byte_address >> BAT_INDEX_SHIFT];
return (bat_result_1 & bat_result_2 & BAT_PHYSICAL_BIT) != 0;
}
bool MMU::IsPhysicalRAMAddress(const u32 address) const
{
const u32 segment = address >> 28;
if (m_memory.GetRAM() && segment == 0x0 && (address & 0x0FFFFFFF) < m_memory.GetRamSizeReal())
{
return true;
}
if (m_memory.GetEXRAM() && segment == 0x1 && (address & 0x0FFFFFFF) < m_memory.GetExRamSizeReal())
{
return true;
}
if (m_memory.GetFakeVMEM() && (address & 0xFE000000) == 0x7E000000)
{
return true;
}
if (m_memory.GetL1Cache() && segment == 0xE && address < 0xE0000000 + m_memory.GetL1CacheSize())
{
return true;
}
return false;
}
template <XCheckTLBFlag flag>
bool MMU::IsEffectiveRAMAddress(const u32 address)
{
const auto translate_address = TranslateAddress<flag>(address);
return translate_address.Success() && IsPhysicalRAMAddress(translate_address.address);
}
bool MMU::HostIsRAMAddress(const Core::CPUThreadGuard& guard, u32 address,
RequestedAddressSpace space)
{
auto& mmu = guard.GetSystem().GetMMU();
switch (space)
{
case RequestedAddressSpace::Effective:
return mmu.m_ppc_state.msr.DR ? mmu.IsEffectiveRAMAddress<XCheckTLBFlag::NoException>(address) :
mmu.IsPhysicalRAMAddress(address);
case RequestedAddressSpace::Physical:
return mmu.IsPhysicalRAMAddress(address);
case RequestedAddressSpace::Virtual:
if (!mmu.m_ppc_state.msr.DR)
return false;
return mmu.IsEffectiveRAMAddress<XCheckTLBFlag::NoException>(address);
}
ASSERT(false);
return false;
}
bool MMU::HostIsInstructionRAMAddress(const Core::CPUThreadGuard& guard, u32 address,
RequestedAddressSpace space)
{
// Instructions are always 32bit aligned.
if (address & 3)
return false;
auto& mmu = guard.GetSystem().GetMMU();
switch (space)
{
case RequestedAddressSpace::Effective:
return mmu.m_ppc_state.msr.IR ?
mmu.IsEffectiveRAMAddress<XCheckTLBFlag::OpcodeNoException>(address) :
mmu.IsPhysicalRAMAddress(address);
case RequestedAddressSpace::Physical:
return mmu.IsPhysicalRAMAddress(address);
case RequestedAddressSpace::Virtual:
if (!mmu.m_ppc_state.msr.IR)
return false;
return mmu.IsEffectiveRAMAddress<XCheckTLBFlag::OpcodeNoException>(address);
}
ASSERT(false);
return false;
}
void MMU::DMA_LCToMemory(const u32 mem_address, const u32 cache_address, const u32 num_blocks)
{
// TODO: It's not completely clear this is the right spot for this code;
// what would happen if, for example, the DVD drive tried to write to the EFB?
// TODO: This is terribly slow.
// TODO: Refactor.
// Avatar: The Last Airbender (GC) uses this for videos.
if ((mem_address & 0x0F000000) == 0x08000000)
{
for (u32 i = 0; i < 32 * num_blocks; i += 4)
{
const u32 data = Common::swap32(m_memory.GetL1Cache() + ((cache_address + i) & 0x3FFFF));
EFB_Write(data, mem_address + i);
}
return;
}
// No known game uses this; here for completeness.
// TODO: Refactor.
if ((mem_address & 0x0F000000) == 0x0C000000)
{
for (u32 i = 0; i < 32 * num_blocks; i += 4)
{
const u32 data = Common::swap32(m_memory.GetL1Cache() + ((cache_address + i) & 0x3FFFF));
m_memory.GetMMIOMapping()->Write(m_system, mem_address + i, data);
}
return;
}
const u8* src = m_memory.GetL1Cache() + (cache_address & 0x3FFFF);
m_memory.CopyToEmu(mem_address, src, 32 * num_blocks);
}
void MMU::DMA_MemoryToLC(const u32 cache_address, const u32 mem_address, const u32 num_blocks)
{
// No known game uses this; here for completeness.
// TODO: Refactor.
if ((mem_address & 0x0F000000) == 0x08000000)
{
for (u32 i = 0; i < 32 * num_blocks; i += 4)
{
const u32 data = Common::swap32(EFB_Read(mem_address + i));
std::memcpy(m_memory.GetL1Cache() + ((cache_address + i) & 0x3FFFF), &data, sizeof(u32));
}
return;
}
// No known game uses this.
// TODO: Refactor.
if ((mem_address & 0x0F000000) == 0x0C000000)
{
for (u32 i = 0; i < 32 * num_blocks; i += 4)
{
const u32 data =
Common::swap32(m_memory.GetMMIOMapping()->Read<u32>(m_system, mem_address + i));
std::memcpy(m_memory.GetL1Cache() + ((cache_address + i) & 0x3FFFF), &data, sizeof(u32));
}
return;
}
u8* dst = m_memory.GetL1Cache() + (cache_address & 0x3FFFF);
m_memory.CopyFromEmu(dst, mem_address, 32 * num_blocks);
}
static bool TranslateBatAddress(const BatTable& bat_table, u32* address, bool* wi)
{
u32 bat_result = bat_table[*address >> BAT_INDEX_SHIFT];
if ((bat_result & BAT_MAPPED_BIT) == 0)
return false;
*address = (bat_result & BAT_RESULT_MASK) | (*address & (BAT_PAGE_SIZE - 1));
*wi = (bat_result & BAT_WI_BIT) != 0;
return true;
}
void MMU::ClearDCacheLine(u32 address)
{
DEBUG_ASSERT((address & 0x1F) == 0);
if (m_ppc_state.msr.DR)
{
auto translated_address = TranslateAddress<XCheckTLBFlag::Write>(address);
if (translated_address.result == TranslateAddressResultEnum::DIRECT_STORE_SEGMENT)
{
// dcbz to direct store segments is ignored. This is a little
// unintuitive, but this is consistent with both console and the PEM.
// Advance Game Port crashes if we don't emulate this correctly.
return;
}
if (translated_address.result == TranslateAddressResultEnum::PAGE_FAULT)
{
// If translation fails, generate a DSI.
GenerateDSIException(address, true);
return;
}
address = translated_address.address;
}
// TODO: This isn't precisely correct for non-RAM regions, but the difference
// is unlikely to matter.
for (u32 i = 0; i < 32; i += 4)
WriteToHardware<XCheckTLBFlag::Write, true>(address + i, 0, 4);
}
void MMU::StoreDCacheLine(u32 address)
{
address &= ~0x1F;
if (m_ppc_state.msr.DR)
{
auto translated_address = TranslateAddress<XCheckTLBFlag::Write>(address);
if (translated_address.result == TranslateAddressResultEnum::DIRECT_STORE_SEGMENT)
{
return;
}
if (translated_address.result == TranslateAddressResultEnum::PAGE_FAULT)
{
// If translation fails, generate a DSI.
GenerateDSIException(address, true);
return;
}
address = translated_address.address;
}
if (m_ppc_state.m_enable_dcache)
m_ppc_state.dCache.Store(m_memory, address);
}
void MMU::InvalidateDCacheLine(u32 address)
{
address &= ~0x1F;
if (m_ppc_state.msr.DR)
{
auto translated_address = TranslateAddress<XCheckTLBFlag::Write>(address);
if (translated_address.result == TranslateAddressResultEnum::DIRECT_STORE_SEGMENT)
{
return;
}
if (translated_address.result == TranslateAddressResultEnum::PAGE_FAULT)
{
return;
}
address = translated_address.address;
}
if (m_ppc_state.m_enable_dcache)
m_ppc_state.dCache.Invalidate(m_memory, address);
}
void MMU::FlushDCacheLine(u32 address)
{
address &= ~0x1F;
if (m_ppc_state.msr.DR)
{
auto translated_address = TranslateAddress<XCheckTLBFlag::Write>(address);
if (translated_address.result == TranslateAddressResultEnum::DIRECT_STORE_SEGMENT)
{
return;
}
if (translated_address.result == TranslateAddressResultEnum::PAGE_FAULT)
{
// If translation fails, generate a DSI.
GenerateDSIException(address, true);
return;
}
address = translated_address.address;
}
if (m_ppc_state.m_enable_dcache)
m_ppc_state.dCache.Flush(m_memory, address);
}
void MMU::TouchDCacheLine(u32 address, bool store)
{
address &= ~0x1F;
if (m_ppc_state.msr.DR)
{
auto translated_address = TranslateAddress<XCheckTLBFlag::Write>(address);
if (translated_address.result == TranslateAddressResultEnum::DIRECT_STORE_SEGMENT)
{
return;
}
if (translated_address.result == TranslateAddressResultEnum::PAGE_FAULT)
{
// If translation fails, generate a DSI.
GenerateDSIException(address, true);
return;
}
address = translated_address.address;
}
if (m_ppc_state.m_enable_dcache)
m_ppc_state.dCache.Touch(m_memory, address, store);
}
u32 MMU::IsOptimizableMMIOAccess(u32 address, u32 access_size) const
{
if (m_power_pc.GetMemChecks().HasAny())
return 0;
if (!m_ppc_state.msr.DR)
return 0;
if (m_ppc_state.m_enable_dcache)
return 0;
// Translate address
// If we also optimize for TLB mappings, we'd have to clear the
// JitCache on each TLB invalidation.
bool wi = false;
if (!TranslateBatAddress(m_dbat_table, &address, &wi))
return 0;
// Check whether the address is an aligned address of an MMIO register.
const bool aligned = (address & ((access_size >> 3) - 1)) == 0;
if (!aligned || !MMIO::IsMMIOAddress(address, m_system.IsWii()))
return 0;
return address;
}
bool MMU::IsOptimizableGatherPipeWrite(u32 address) const
{
if (m_power_pc.GetMemChecks().HasAny())
return false;
if (!m_ppc_state.msr.DR)
return false;
// Translate address, only check BAT mapping.
// If we also optimize for TLB mappings, we'd have to clear the
// JitCache on each TLB invalidation.
bool wi = false;
if (!TranslateBatAddress(m_dbat_table, &address, &wi))
return false;
// Check whether the translated address equals the address in WPAR.
return address == GPFifo::GATHER_PIPE_PHYSICAL_ADDRESS;
}
TranslateResult MMU::JitCache_TranslateAddress(u32 address)
{
if (!m_ppc_state.msr.IR)
return TranslateResult{address};
// TODO: We shouldn't use FLAG_OPCODE if the caller is the debugger.
const auto tlb_addr = TranslateAddress<XCheckTLBFlag::Opcode>(address);
if (!tlb_addr.Success())
return TranslateResult{};
const bool from_bat = tlb_addr.result == TranslateAddressResultEnum::BAT_TRANSLATED;
return TranslateResult{from_bat, tlb_addr.address};
}
void MMU::GenerateDSIException(u32 effective_address, bool write)
{
// DSI exceptions are only supported in MMU mode.
if (!m_system.IsMMUMode())
{
if (write)
{
PanicAlertFmtT(
"Invalid write to {0:#010x}, PC = {1:#010x}.\n\nThe game probably would have crashed on "
"real hardware. Enable MMU in advanced settings to accurately emulate game crashes.",
effective_address, m_ppc_state.pc);
}
else
{
PanicAlertFmtT(
"Invalid read from {0:#010x}, PC = {1:#010x}.\n\nThe game probably would have crashed on "
"real hardware. Enable MMU in advanced settings to accurately emulate game crashes.",
effective_address, m_ppc_state.pc);
}
if (m_system.IsPauseOnPanicMode())
{
m_system.GetCPU().Break();
m_ppc_state.Exceptions |= EXCEPTION_DSI | EXCEPTION_FAKE_MEMCHECK_HIT;
}
return;
}
constexpr u32 dsisr_page = 1U << 30;
constexpr u32 dsisr_store = 1U << 25;
if (write)
m_ppc_state.spr[SPR_DSISR] = dsisr_page | dsisr_store;
else
m_ppc_state.spr[SPR_DSISR] = dsisr_page;
m_ppc_state.spr[SPR_DAR] = effective_address;
m_ppc_state.Exceptions |= EXCEPTION_DSI;
}
void MMU::GenerateISIException(u32 effective_address)
{
// Address of instruction could not be translated
m_ppc_state.npc = effective_address;
m_ppc_state.Exceptions |= EXCEPTION_ISI;
WARN_LOG_FMT(POWERPC, "ISI exception at {:#010x}", m_ppc_state.pc);
}
void MMU::SDRUpdated()
{
const auto sdr = UReg_SDR1{m_ppc_state.spr[SPR_SDR]};
const u32 htabmask = sdr.htabmask;
if (!Common::IsValidLowMask(htabmask))
WARN_LOG_FMT(POWERPC, "Invalid HTABMASK: 0b{:032b}", htabmask);
// While 6xx_pem.pdf §7.6.1.1 mentions that the number of trailing zeros in HTABORG
// must be equal to the number of trailing ones in the mask (i.e. HTABORG must be
// properly aligned), this is actually not a hard requirement. Real hardware will just OR
// the base address anyway. Ignoring SDR changes would lead to incorrect emulation.
const u32 htaborg = sdr.htaborg;
if ((htaborg & htabmask) != 0)
WARN_LOG_FMT(POWERPC, "Invalid HTABORG: htaborg=0x{:08x} htabmask=0x{:08x}", htaborg, htabmask);
m_ppc_state.pagetable_base = htaborg << 16;
m_ppc_state.pagetable_mask = (htabmask << 16) | 0xffc0;
PageTableUpdated();
}
void MMU::SRUpdated()
{
// Our incremental handling of page table updates can't handle SR changing, so throw away all
// existing mappings and then reparse the whole page table.
m_memory.RemoveAllPageTableMappings();
ReloadPageTable();
}
enum class TLBLookupResult
{
Found,
NotFound,
UpdateC
};
static TLBLookupResult LookupTLBPageAddress(PowerPC::PowerPCState& ppc_state,
const XCheckTLBFlag flag, const u32 vpa, const u32 vsid,
u32* paddr, bool* wi)
{
const u32 tag = vpa >> HW_PAGE_INDEX_SHIFT;
const size_t tlb_index = IsOpcodeFlag(flag) ? PowerPC::INST_TLB_INDEX : PowerPC::DATA_TLB_INDEX;
TLBEntry& tlbe = ppc_state.tlb[tlb_index][tag & HW_PAGE_INDEX_MASK];
if (tlbe.tag[0] == tag && tlbe.vsid[0] == vsid)
{
UPTE_Hi pte2(tlbe.pte[0]);
// Check if C bit requires updating
if (flag == XCheckTLBFlag::Write)
{
if (pte2.C == 0)
{
pte2.C = 1;
tlbe.pte[0] = pte2.Hex;
return TLBLookupResult::UpdateC;
}
}
if (!IsNoExceptionFlag(flag))
tlbe.recent = 0;
*paddr = tlbe.paddr[0] | (vpa & 0xfff);
*wi = (pte2.WIMG & 0b1100) != 0;
return TLBLookupResult::Found;
}
if (tlbe.tag[1] == tag && tlbe.vsid[1] == vsid)
{
UPTE_Hi pte2(tlbe.pte[1]);
// Check if C bit requires updating
if (flag == XCheckTLBFlag::Write)
{
if (pte2.C == 0)
{
pte2.C = 1;
tlbe.pte[1] = pte2.Hex;
return TLBLookupResult::UpdateC;
}
}
if (!IsNoExceptionFlag(flag))
tlbe.recent = 1;
*paddr = tlbe.paddr[1] | (vpa & 0xfff);
*wi = (pte2.WIMG & 0b1100) != 0;
return TLBLookupResult::Found;
}
return TLBLookupResult::NotFound;
}
static void UpdateTLBEntry(PowerPC::PowerPCState& ppc_state, const XCheckTLBFlag flag, UPTE_Hi pte2,
const u32 address, const u32 vsid)
{
if (IsNoExceptionFlag(flag))
return;
const u32 tag = address >> HW_PAGE_INDEX_SHIFT;
const size_t tlb_index = IsOpcodeFlag(flag) ? PowerPC::INST_TLB_INDEX : PowerPC::DATA_TLB_INDEX;
TLBEntry& tlbe = ppc_state.tlb[tlb_index][tag & HW_PAGE_INDEX_MASK];
const u32 index = tlbe.recent == 0 && tlbe.tag[0] != TLBEntry::INVALID_TAG;
tlbe.recent = index;
tlbe.paddr[index] = pte2.RPN << HW_PAGE_INDEX_SHIFT;
tlbe.pte[index] = pte2.Hex;
tlbe.tag[index] = tag;
tlbe.vsid[index] = vsid;
}
void MMU::InvalidateTLBEntry(u32 address)
{
const u32 entry_index = (address >> HW_PAGE_INDEX_SHIFT) & HW_PAGE_INDEX_MASK;
m_ppc_state.tlb[PowerPC::DATA_TLB_INDEX][entry_index].Invalidate();
m_ppc_state.tlb[PowerPC::INST_TLB_INDEX][entry_index].Invalidate();
if (m_ppc_state.msr.DR)
PageTableUpdated();
else
m_ppc_state.pagetable_update_pending = true;
}
void MMU::ClearPageTable()
{
// If we've skipped processing any update to the page table, we need to remove all host mappings,
// because we don't know which of them are still valid.
m_memory.RemoveAllPageTableMappings();
// Because we removed host mappings, incremental updates won't work correctly.
// Start over from scratch.
m_page_mappings.clear();
m_page_table.clear();
}
void MMU::ReloadPageTable()
{
m_page_mappings.clear();
m_temp_page_table.clear();
std::swap(m_page_table, m_temp_page_table);
if (m_system.GetJitInterface().WantsPageTableMappings())
PageTableUpdated(m_temp_page_table);
else
m_memory.RemoveAllPageTableMappings();
}
void MMU::PageTableUpdated()
{
m_ppc_state.pagetable_update_pending = false;
if (!m_system.GetJitInterface().WantsPageTableMappings())
{
// If the JIT has no use for page table mappings, setting them up would be a waste of time.
ClearPageTable();
return;
}
const u32 page_table_base = m_ppc_state.pagetable_base;
const u32 page_table_end =
Common::AlignUp(page_table_base | m_ppc_state.pagetable_mask, PAGE_TABLE_MIN_SIZE);
const u32 page_table_size = page_table_end - page_table_base;
u8* page_table_view = m_system.GetMemory().GetPointerForRange(page_table_base, page_table_size);
if (!page_table_view)
{
WARN_LOG_FMT(POWERPC, "Failed to read page table at {:#010x}-{:#010x}", page_table_base,
page_table_end);
ClearPageTable();
return;
}
PageTableUpdated(std::span(page_table_view, page_table_size));
}
void MMU::PageTableUpdated(std::span<const u8> page_table)
{
// PowerPC's priority order for PTEs that have the same logical adress is as follows:
//
// * Primary PTEs (H=0) take priority over secondary PTEs (H=1).
// * If two PTEs have equal H values, they must be in the same PTEG due to how the hash
// incorporates the logical address and H. The PTE located first in the PTEG takes priority.
if (page_table.size() % PAGE_TABLE_MIN_SIZE != 0)
{
// Should only happen if a maliciously crafted savestate was loaded
PanicAlertFmt("Impossible page table size {}", page_table.size());
ClearPageTable();
return;
}
m_removed_mappings.clear();
m_added_readonly_mappings.clear();
m_added_readwrite_mappings.clear();
if (m_page_table.size() != page_table.size())
{
m_memory.RemoveAllPageTableMappings();
m_page_mappings.clear();
m_page_table.resize(0);
m_page_table.resize(page_table.size(), 0);
}
u8* old_page_table = m_page_table.data();
const u8* new_page_table = page_table.data();
constexpr auto compare_64_bytes = [](const u8* a, const u8* b) -> bool {
#ifdef _M_X86_64
// MSVC (x64) doesn't want to optimize the memcmp call. This has a large performance impact
// in GameCube games that use page tables, so let's use our own vectorized version instead.
const __m128i a1 = _mm_load_si128(reinterpret_cast<const __m128i*>(a));
const __m128i b1 = _mm_load_si128(reinterpret_cast<const __m128i*>(b));
const __m128i cmp1 = _mm_cmpeq_epi8(a1, b1);
const __m128i a2 = _mm_load_si128(reinterpret_cast<const __m128i*>(a + 0x10));
const __m128i b2 = _mm_load_si128(reinterpret_cast<const __m128i*>(b + 0x10));
const __m128i cmp2 = _mm_cmpeq_epi8(a2, b2);
const __m128i cmp12 = _mm_and_si128(cmp1, cmp2);
const __m128i a3 = _mm_load_si128(reinterpret_cast<const __m128i*>(a + 0x20));
const __m128i b3 = _mm_load_si128(reinterpret_cast<const __m128i*>(b + 0x20));
const __m128i cmp3 = _mm_cmpeq_epi8(a3, b3);
const __m128i a4 = _mm_load_si128(reinterpret_cast<const __m128i*>(a + 0x30));
const __m128i b4 = _mm_load_si128(reinterpret_cast<const __m128i*>(b + 0x30));
const __m128i cmp4 = _mm_cmpeq_epi8(a4, b4);
const __m128i cmp34 = _mm_and_si128(cmp3, cmp4);
const __m128i cmp1234 = _mm_and_si128(cmp12, cmp34);
return _mm_movemask_epi8(cmp1234) == 0xFFFF;
#else
return std::memcmp(std::assume_aligned<64>(a), std::assume_aligned<64>(b), 64) == 0;
#endif
};
const auto get_page_index = [this](UPTE_Lo pte1, u32 hash) -> std::optional<EffectiveAddress> {
u32 page_index_from_hash = hash ^ pte1.VSID;
if (pte1.H)
page_index_from_hash = ~page_index_from_hash;
// Due to hash masking, the upper bits of page_index_from_hash might not match the actual
// page index. But these bits fully overlap with the API (abbreviated page index), so we can
// overwrite these bits with the API from pte1 and thereby get the correct page index.
//
// In other words: logical_address.API must be written to after logical_address.page_index!
EffectiveAddress logical_address;
logical_address.offset = 0;
logical_address.page_index = page_index_from_hash;
logical_address.API = pte1.API;
// If the hash mask is large enough that one or more bits specified in pte1.API can also be
// obtained from page_index_from_hash, check that those bits match.
const u32 api_mask = ((m_ppc_state.pagetable_mask & ~m_ppc_state.pagetable_base) >> 16) & 0x3f;
if ((pte1.API & api_mask) != ((page_index_from_hash >> 10) & api_mask))
return std::nullopt;
return logical_address;
};
const auto fixup_shadowed_mappings = [this, old_page_table, new_page_table](
UPTE_Lo pte1, u32 page_table_offset, bool* run_pass_2) {
DEBUG_ASSERT(pte1.V == 1);
bool switched_to_secondary = false;
while (true)
{
const u32 big_endian_pte1 = Common::swap32(pte1.Hex);
const u32 pteg_start = Common::AlignDown(page_table_offset, 64);
const u32 pteg_end = pteg_start + 64;
for (u32 i = page_table_offset; i < pteg_end; i += 8)
{
if (std::memcmp(new_page_table + i, &big_endian_pte1, sizeof(big_endian_pte1)) == 0)
{
// We've found a PTE that has V set and has the same logical address as the passed-in PTE.
// The found PTE was previously skipped over because the passed-in PTE had priority, but
// the passed-in PTE is being changed, so now we need to re-check the found PTE. This will
// happen naturally later in the loop that's calling this function, but only if the 8-byte
// memcmp reports that the PTE has changed. Therefore, if the PTE currently compares
// equal, change an unused bit in the PTE.
if (std::memcmp(old_page_table + i, new_page_table + i, 8) == 0)
{
UPTE_Hi pte2(Common::swap32(old_page_table + i + 4));
pte2.reserved_1 = pte2.reserved_1 ^ 1;
const u32 big_endian_pte2 = Common::swap32(pte2.Hex);
std::memcpy(old_page_table + i + 4, &big_endian_pte2, sizeof(big_endian_pte2));
if (switched_to_secondary)
*run_pass_2 = true;
}
// This PTE has priority over any later PTEs we might find, so no need to keep scanning.
return;
}
}
if (pte1.H == 1)
{
// We've scanned the secondary PTEG. Nothing left to do.
return;
}
else
{
// We've scanned the primary PTEG. Now let's scan the secondary PTEG.
pte1.H = 1;
page_table_offset =
((~pteg_start & m_ppc_state.pagetable_mask) | m_ppc_state.pagetable_base) -
m_ppc_state.pagetable_base;
switched_to_secondary = true;
}
}
};
const auto try_add_mapping = [this, &get_page_index](UPTE_Lo pte1, UPTE_Hi pte2,
u32 page_table_offset) {
std::optional<EffectiveAddress> logical_address = get_page_index(pte1, page_table_offset / 64);
if (!logical_address)
return;
for (u32 i = 0; i < std::size(m_ppc_state.sr); ++i)
{
const auto sr = UReg_SR{m_ppc_state.sr[i]};
if (sr.VSID != pte1.VSID || sr.T != 0)
continue;
logical_address->SR = i;
bool host_mapping = true;
const bool wi = (pte2.WIMG & 0b1100) != 0;
if (wi)
{
// There are quirks related to uncached memory that can't be correctly emulated by fast
// accesses, so we don't map uncached memory. (However, no software at all is known to
// trigger these quirks through page address translation, only through block address
// translation.)
host_mapping = false;
}
else if (m_dbat_table[logical_address->Hex >> PowerPC::BAT_INDEX_SHIFT] &
PowerPC::BAT_MAPPED_BIT)
{
// Block address translation takes priority over page address translation.
host_mapping = false;
}
else if (m_power_pc.GetMemChecks().OverlapsMemcheck(logical_address->Hex,
PowerPC::HW_PAGE_SIZE))
{
// Fast accesses don't support memchecks, so force slow accesses by removing fastmem
// mappings for all overlapping virtual pages.
host_mapping = false;
}
const u32 priority = (page_table_offset % 64 / 8) | (pte1.H << 3);
const PageMapping page_mapping(pte2.RPN, host_mapping, priority);
const auto it = m_page_mappings.find(logical_address->Hex);
if (it == m_page_mappings.end()) [[likely]]
{
// There's no existing mapping for this logical address. Add a new mapping.
m_page_mappings.emplace(logical_address->Hex, page_mapping);
}
else
{
if (it->second.priority < priority)
{
// An existing mapping has priority.
continue;
}
// The new mapping has priority over an existing mapping. Replace the existing mapping.
if (it->second.host_mapping)
m_removed_mappings.emplace(it->first);
it->second.Hex = page_mapping.Hex;
}
// If the R bit isn't set yet, the actual host mapping will be created once
// TranslatePageAddress sets the R bit.
if (host_mapping && pte2.R)
{
const u32 physical_address = pte2.RPN << 12;
(pte2.C ? m_added_readwrite_mappings : m_added_readonly_mappings)
.emplace(logical_address->Hex, physical_address);
}
}
};
bool run_pass_2 = false;
// Pass 1: Remove old mappings and add new primary (H=0) mappings.
for (u32 i = 0; i < page_table.size(); i += PAGE_TABLE_MIN_SIZE)
{
if ((i & m_ppc_state.pagetable_mask) != i || (i & m_ppc_state.pagetable_base) != 0)
continue;
for (u32 j = 0; j < PAGE_TABLE_MIN_SIZE; j += 64)
{
if (compare_64_bytes(old_page_table + i + j, new_page_table + i + j)) [[likely]]
continue;
for (u32 k = 0; k < 64; k += 8)
{
if (std::memcmp(old_page_table + i + j + k, new_page_table + i + j + k, 8) == 0) [[likely]]
continue;
// Remove old mappings.
UPTE_Lo old_pte1(Common::swap32(old_page_table + i + j + k));
if (old_pte1.V)
{
const u32 priority = (k / 8) | (old_pte1.H << 3);
std::optional<EffectiveAddress> logical_address = get_page_index(old_pte1, (i + j) / 64);
if (!logical_address)
continue;
for (u32 l = 0; l < std::size(m_ppc_state.sr); ++l)
{
const auto sr = UReg_SR{m_ppc_state.sr[l]};
if (sr.VSID != old_pte1.VSID || sr.T != 0)
continue;
logical_address->SR = l;
const auto it = m_page_mappings.find(logical_address->Hex);
if (it != m_page_mappings.end() && priority == it->second.priority)
{
if (it->second.host_mapping)
m_removed_mappings.emplace(logical_address->Hex);
m_page_mappings.erase(it);
// It's unlikely but possible that this was shadowing another PTE that's using the
// same logical address but has a lower priority. If this happens, we must make sure
// that we don't skip over that other PTE because of the 8-byte memcmp.
fixup_shadowed_mappings(old_pte1, i + j + k, &run_pass_2);
}
}
}
// Add new primary (H=0) mappings.
UPTE_Lo new_pte1(Common::swap32(new_page_table + i + j + k));
UPTE_Hi new_pte2(Common::swap32(new_page_table + i + j + k + 4));
if (new_pte1.V)
{
if (new_pte1.H)
{
run_pass_2 = true;
continue;
}
try_add_mapping(new_pte1, new_pte2, i + j + k);
}
// Update our copy of the page table.
std::memcpy(old_page_table + i + j + k, new_page_table + i + j + k, 8);
}
}
}
// Pass 2: Add new secondary (H=1) mappings. This is a separate pass because before we can process
// whether a mapping should be added, we first need to check all PTEs that have equal or higher
// priority to see if their mappings should be removed. For adding primary mappings, this ordering
// comes naturally from doing a linear scan of the page table from start to finish. But for adding
// secondary mappings, the primary PTEG that has priority over a given secondary PTEG is in the
// other half of the page table, so we need more than one pass through the page table. But most of
// the time, there are no secondary mappings, letting us skip the second pass.
if (run_pass_2) [[unlikely]]
{
for (u32 i = 0; i < page_table.size(); i += PAGE_TABLE_MIN_SIZE)
{
if ((i & m_ppc_state.pagetable_mask) != i || (i & m_ppc_state.pagetable_base) != 0)
continue;
for (u32 j = 0; j < PAGE_TABLE_MIN_SIZE; j += 64)
{
if (compare_64_bytes(old_page_table + i + j, new_page_table + i + j)) [[likely]]
continue;
for (u32 k = 0; k < 64; k += 8)
{
if (std::memcmp(old_page_table + i + j + k, new_page_table + i + j + k, 8) == 0)
[[likely]]
{
continue;
}
UPTE_Lo new_pte1(Common::swap32(new_page_table + i + j + k));
UPTE_Hi new_pte2(Common::swap32(new_page_table + i + j + k + 4));
// We don't need to check new_pte1.V and new_pte1.H. If the memcmp above returned nonzero,
// pass 1 must have skipped running memcpy, which only happens if V and H are both set.
DEBUG_ASSERT(new_pte1.V == 1);
DEBUG_ASSERT(new_pte1.H == 1);
try_add_mapping(new_pte1, new_pte2, i + j + k);
std::memcpy(old_page_table + i + j + k, new_page_table + i + j + k, 8);
}
}
}
}
if (!m_removed_mappings.empty())
m_memory.RemovePageTableMappings(m_removed_mappings);
for (const auto& [logical_address, physical_address] : m_added_readonly_mappings)
m_memory.AddPageTableMapping(logical_address, physical_address, false);
for (const auto& [logical_address, physical_address] : m_added_readwrite_mappings)
m_memory.AddPageTableMapping(logical_address, physical_address, true);
}
void MMU::PageTableUpdatedFromJit(MMU* mmu)
{
mmu->PageTableUpdated();
}
// Page Address Translation
template <const XCheckTLBFlag flag>
MMU::TranslateAddressResult MMU::TranslatePageAddress(const EffectiveAddress address, bool* wi)
{
const auto sr = UReg_SR{m_ppc_state.sr[address.SR]};
const u32 VSID = sr.VSID; // 24 bit
// TLB cache
// This catches 99%+ of lookups in practice, so the actual page table entry code below doesn't
// benefit much from optimization.
u32 translated_address = 0;
const TLBLookupResult res =
LookupTLBPageAddress(m_ppc_state, flag, address.Hex, VSID, &translated_address, wi);
if (res == TLBLookupResult::Found)
{
return TranslateAddressResult{TranslateAddressResultEnum::PAGE_TABLE_TRANSLATED,
translated_address};
}
if (sr.T != 0)
return TranslateAddressResult{TranslateAddressResultEnum::DIRECT_STORE_SEGMENT, 0};
// TODO: Handle KS/KP segment register flags.
// No-execute segment register flag.
if ((flag == XCheckTLBFlag::Opcode || flag == XCheckTLBFlag::OpcodeNoException) && sr.N != 0)
{
return TranslateAddressResult{TranslateAddressResultEnum::PAGE_FAULT, 0};
}
const u32 offset = address.offset; // 12 bit
const u32 page_index = address.page_index; // 16 bit
const u32 api = address.API; // 6 bit (part of page_index)
// hash function no 1 "xor" .360
u32 hash = (VSID ^ page_index);
UPTE_Lo pte1;
pte1.VSID = VSID;
pte1.API = api;
pte1.V = 1;
for (int hash_func = 0; hash_func < 2; hash_func++)
{
// hash function no 2 "not" .360
if (hash_func == 1)
{
hash = ~hash;
pte1.H = 1;
}
u32 pteg_addr = ((hash << 6) & m_ppc_state.pagetable_mask) | m_ppc_state.pagetable_base;
for (int i = 0; i < 8; i++, pteg_addr += 8)
{
constexpr XCheckTLBFlag pte_read_flag =
IsNoExceptionFlag(flag) ? XCheckTLBFlag::NoException : XCheckTLBFlag::Read;
const u32 pteg = ReadFromHardware<pte_read_flag, u32, true>(pteg_addr);
if (pte1.Hex == pteg)
{
UPTE_Hi pte2(ReadFromHardware<pte_read_flag, u32, true>(pteg_addr + 4));
const UPTE_Hi old_pte2 = pte2;
// set the access bits
switch (flag)
{
case XCheckTLBFlag::NoException:
case XCheckTLBFlag::OpcodeNoException:
break;
case XCheckTLBFlag::Read:
pte2.R = 1;
break;
case XCheckTLBFlag::Write:
pte2.R = 1;
pte2.C = 1;
break;
case XCheckTLBFlag::Opcode:
pte2.R = 1;
break;
}
if (!IsNoExceptionFlag(flag) && pte2.Hex != old_pte2.Hex)
{
m_memory.Write_U32(pte2.Hex, pteg_addr + 4);
const u32 page_logical_address = address.Hex & ~HW_PAGE_MASK;
const auto it = m_page_mappings.find(page_logical_address);
if (it != m_page_mappings.end())
{
const u32 priority = (pteg_addr % 64 / 8) | (pte1.H << 3);
if (it->second.Hex == PageMapping(pte2.RPN, true, priority).Hex)
{
const u32 swapped_pte1 = Common::swap32(reinterpret_cast<u8*>(&pte1));
std::memcpy(m_page_table.data() + pteg_addr - m_ppc_state.pagetable_base,
&swapped_pte1, sizeof(swapped_pte1));
const u32 swapped_pte2 = Common::swap32(reinterpret_cast<u8*>(&pte2));
std::memcpy(m_page_table.data() + pteg_addr + 4 - m_ppc_state.pagetable_base,
&swapped_pte2, sizeof(swapped_pte2));
const u32 page_translated_address = pte2.RPN << 12;
m_memory.AddPageTableMapping(page_logical_address, page_translated_address, pte2.C);
}
}
}
// We already updated the TLB entry if this was caused by a C bit.
if (res != TLBLookupResult::UpdateC)
UpdateTLBEntry(m_ppc_state, flag, pte2, address.Hex, VSID);
*wi = (pte2.WIMG & 0b1100) != 0;
return TranslateAddressResult{TranslateAddressResultEnum::PAGE_TABLE_TRANSLATED,
(pte2.RPN << 12) | offset};
}
}
}
return TranslateAddressResult{TranslateAddressResultEnum::PAGE_FAULT, 0};
}
void MMU::UpdateBATs(BatTable& bat_table, u32 base_spr)
{
// TODO: Separate BATs for MSR.PR==0 and MSR.PR==1
// TODO: Handle PP settings.
// TODO: Check how hardware reacts to overlapping BATs (including
// BATs which should cause a DSI).
// TODO: Check how hardware reacts to invalid BATs (bad mask etc).
for (int i = 0; i < 4; ++i)
{
const u32 spr = base_spr + i * 2;
const UReg_BAT_Up batu{m_ppc_state.spr[spr]};
const UReg_BAT_Lo batl{m_ppc_state.spr[spr + 1]};
if (batu.VS == 0 && batu.VP == 0)
continue;
if ((batu.BEPI & batu.BL) != 0)
{
// With a valid BAT, the simplest way to match is
// (input & ~BL_mask) == BEPI. For now, assume it's
// implemented this way for invalid BATs as well.
WARN_LOG_FMT(POWERPC, "Bad BAT setup: BEPI overlaps BL");
}
if ((batl.BRPN & batu.BL) != 0)
{
// With a valid BAT, the simplest way to translate is
// (input & BL_mask) | BRPN_address. For now, assume it's
// implemented this way for invalid BATs as well.
WARN_LOG_FMT(POWERPC, "Bad BAT setup: BPRN overlaps BL");
}
if (!Common::IsValidLowMask((u32)batu.BL))
{
// With a valid BAT, the simplest way of masking is
// (input & ~BL_mask) for matching and (input & BL_mask) for
// translation. For now, assume it's implemented this way for
// invalid BATs as well.
WARN_LOG_FMT(POWERPC, "Bad BAT setup: invalid mask in BL");
}
for (u32 j = 0; j <= batu.BL; ++j)
{
// Enumerate all bit-patterns which fit within the given mask.
if ((j & batu.BL) == j)
{
// This bit is a little weird: if BRPN & j != 0, we end up with
// a strange mapping. Need to check on hardware.
u32 physical_address = (batl.BRPN | j) << BAT_INDEX_SHIFT;
u32 virtual_address = (batu.BEPI | j) << BAT_INDEX_SHIFT;
// BAT_MAPPED_BIT is whether the translation is valid
// BAT_PHYSICAL_BIT is whether we can use the fastmem arena
// BAT_WI_BIT is whether either W or I (of WIMG) is set
u32 valid_bit = BAT_MAPPED_BIT;
const bool wi = (batl.WIMG & 0b1100) != 0;
if (wi)
valid_bit |= BAT_WI_BIT;
// Enable fastmem mappings for cached memory. There are quirks related to uncached memory
// that can't be correctly emulated by fast accesses, so we don't map uncached memory.
// (No normal games are known to rely on the quirks, though.)
if (!wi)
{
if (m_memory.GetFakeVMEM() && (physical_address & 0xFE000000) == 0x7E000000)
{
valid_bit |= BAT_PHYSICAL_BIT;
}
else if (physical_address < m_memory.GetRamSizeReal())
{
valid_bit |= BAT_PHYSICAL_BIT;
}
else if (m_memory.GetEXRAM() && physical_address >> 28 == 0x1 &&
(physical_address & 0x0FFFFFFF) < m_memory.GetExRamSizeReal())
{
valid_bit |= BAT_PHYSICAL_BIT;
}
else if (physical_address >> 28 == 0xE &&
physical_address < 0xE0000000 + m_memory.GetL1CacheSize())
{
valid_bit |= BAT_PHYSICAL_BIT;
}
}
// Fast accesses don't support memchecks, so force slow accesses by removing fastmem
// mappings for all overlapping virtual pages.
if (m_power_pc.GetMemChecks().OverlapsMemcheck(virtual_address, BAT_PAGE_SIZE))
valid_bit &= ~BAT_PHYSICAL_BIT;
// (BEPI | j) == (BEPI & ~BL) | (j & BL).
bat_table[virtual_address >> BAT_INDEX_SHIFT] = physical_address | valid_bit;
}
}
}
}
void MMU::UpdateFakeMMUBat(BatTable& bat_table, u32 start_addr)
{
for (u32 i = 0; i < (0x10000000 >> BAT_INDEX_SHIFT); ++i)
{
// Map from 0x4XXXXXXX or 0x7XXXXXXX to the range
// [0x7E000000,0x80000000).
u32 e_address = i + (start_addr >> BAT_INDEX_SHIFT);
u32 p_address = 0x7E000000 | (i << BAT_INDEX_SHIFT & m_memory.GetFakeVMemMask());
u32 flags = BAT_MAPPED_BIT | BAT_PHYSICAL_BIT;
if (m_power_pc.GetMemChecks().OverlapsMemcheck(e_address << BAT_INDEX_SHIFT, BAT_PAGE_SIZE))
flags &= ~BAT_PHYSICAL_BIT;
bat_table[e_address] = p_address | flags;
}
}
void MMU::DBATUpdated()
{
m_dbat_table = {};
UpdateBATs(m_dbat_table, SPR_DBAT0U);
bool extended_bats = m_system.IsWii() && HID4(m_ppc_state).SBE;
if (extended_bats)
UpdateBATs(m_dbat_table, SPR_DBAT4U);
if (m_memory.GetFakeVMEM())
{
// In Fake-MMU mode, insert some extra entries into the BAT tables.
UpdateFakeMMUBat(m_dbat_table, 0x40000000);
UpdateFakeMMUBat(m_dbat_table, 0x70000000);
}
#ifndef _ARCH_32
m_memory.UpdateDBATMappings(m_dbat_table);
// Calling UpdateDBATMappings removes all fastmem page table mappings, so we have to recreate
// them. We need to go through them anyway because there may have been a change in which DBATs
// or memchecks are shadowing which page table mappings.
if (!m_page_table.empty())
ReloadPageTable();
#endif
// IsOptimizable*Address and dcbz depends on the BAT mapping, so we need a flush here.
m_system.GetJitInterface().ClearSafe();
}
void MMU::IBATUpdated()
{
m_ibat_table = {};
UpdateBATs(m_ibat_table, SPR_IBAT0U);
bool extended_bats = m_system.IsWii() && HID4(m_ppc_state).SBE;
if (extended_bats)
UpdateBATs(m_ibat_table, SPR_IBAT4U);
if (m_memory.GetFakeVMEM())
{
// In Fake-MMU mode, insert some extra entries into the BAT tables.
UpdateFakeMMUBat(m_ibat_table, 0x40000000);
UpdateFakeMMUBat(m_ibat_table, 0x70000000);
}
m_system.GetJitInterface().ClearSafe();
}
// Translate effective address using BAT or PAT. Returns 0 if the address cannot be translated.
// Through the hardware looks up BAT and TLB in parallel, BAT is used first if available.
// So we first check if there is a matching BAT entry, else we look for the TLB in
// TranslatePageAddress().
template <const XCheckTLBFlag flag>
MMU::TranslateAddressResult MMU::TranslateAddress(u32 address)
{
bool wi = false;
if (TranslateBatAddress(IsOpcodeFlag(flag) ? m_ibat_table : m_dbat_table, &address, &wi))
return TranslateAddressResult{TranslateAddressResultEnum::BAT_TRANSLATED, address, wi};
return TranslatePageAddress<flag>(EffectiveAddress{address}, &wi);
}
std::optional<u32> MMU::GetTranslatedAddress(u32 address)
{
auto result = TranslateAddress<XCheckTLBFlag::NoException>(address);
if (!result.Success())
{
return std::nullopt;
}
return std::optional<u32>(result.address);
}
void ClearDCacheLineFromJit(MMU& mmu, u32 address)
{
mmu.ClearDCacheLine(address);
}
template <std::unsigned_integral T>
Common::MakeAtLeastU32<T> ReadFromJit(MMU& mmu, u32 address)
{
return mmu.Read<T>(address);
}
template u32 ReadFromJit<u8>(MMU& mmu, u32 address);
template u32 ReadFromJit<u16>(MMU& mmu, u32 address);
template u32 ReadFromJit<u32>(MMU& mmu, u32 address);
template u64 ReadFromJit<u64>(MMU& mmu, u32 address);
template <std::unsigned_integral T>
void WriteFromJit(MMU& mmu, Common::MakeAtLeastU32<T> var, u32 address)
{
mmu.Write<T>(var, address);
}
template void WriteFromJit<u8>(MMU& mmu, u32 var, u32 address);
template void WriteFromJit<u16>(MMU& mmu, u32 var, u32 address);
template void WriteFromJit<u32>(MMU& mmu, u32 var, u32 address);
template void WriteFromJit<u64>(MMU& mmu, u64 var, u32 address);
void WriteU16SwapFromJit(MMU& mmu, u32 var, u32 address)
{
mmu.Write_U16_Swap(var, address);
}
void WriteU32SwapFromJit(MMU& mmu, u32 var, u32 address)
{
mmu.Write_U32_Swap(var, address);
}
void WriteU64SwapFromJit(MMU& mmu, u64 var, u32 address)
{
mmu.Write_U64_Swap(var, address);
}
} // namespace PowerPC