diff --git a/Source/Core/Common/Swap.h b/Source/Core/Common/Swap.h index 965d1aabc2b..bf9b7d472b1 100644 --- a/Source/Core/Common/Swap.h +++ b/Source/Core/Common/Swap.h @@ -127,6 +127,22 @@ inline u64 swap64(const u8* data) return swap64(value); } +inline void WriteSwap16(u8* data, u16 value) +{ + value = swap16(value); + std::memcpy(data, &value, sizeof(u16)); +} +inline void WriteSwap32(u8* data, u32 value) +{ + value = swap32(value); + std::memcpy(data, &value, sizeof(u32)); +} +inline void WriteSwap64(u8* data, u64 value) +{ + value = swap64(value); + std::memcpy(data, &value, sizeof(u64)); +} + template void swap(u8*); diff --git a/Source/Core/Core/HW/Memmap.h b/Source/Core/Core/HW/Memmap.h index 8779be8210c..ce45d730346 100644 --- a/Source/Core/Core/HW/Memmap.h +++ b/Source/Core/Core/HW/Memmap.h @@ -85,6 +85,8 @@ public: u8* GetPhysicalPageMappingsBase() const { return m_physical_page_mappings_base; } u8* GetLogicalPageMappingsBase() const { return m_logical_page_mappings_base; } + u32 GetHostPageSize() const { return m_page_size; } + // FIXME: these should not return their address, but AddressSpace wants that u8*& GetRAM() { return m_ram; } u8*& GetEXRAM() { return m_exram; } diff --git a/Source/UnitTests/Core/CMakeLists.txt b/Source/UnitTests/Core/CMakeLists.txt index 8725995729f..30aeae44775 100644 --- a/Source/UnitTests/Core/CMakeLists.txt +++ b/Source/UnitTests/Core/CMakeLists.txt @@ -21,6 +21,7 @@ add_dolphin_test(SkylandersTest IOS/USB/SkylandersTest.cpp) if(_M_X86_64) add_dolphin_test(PowerPCTest PowerPC/DivUtilsTest.cpp + PowerPC/PageTableHostMappingTest.cpp PowerPC/Jit64Common/ConvertDoubleToSingle.cpp PowerPC/Jit64Common/Fres.cpp PowerPC/Jit64Common/Frsqrte.cpp @@ -28,6 +29,7 @@ if(_M_X86_64) elseif(_M_ARM_64) add_dolphin_test(PowerPCTest PowerPC/DivUtilsTest.cpp + PowerPC/PageTableHostMappingTest.cpp PowerPC/JitArm64/ConvertSingleDouble.cpp PowerPC/JitArm64/FPRF.cpp PowerPC/JitArm64/Fres.cpp @@ -37,9 +39,11 @@ elseif(_M_ARM_64) else() add_dolphin_test(PowerPCTest PowerPC/DivUtilsTest.cpp + PowerPC/PageTableHostMappingTest.cpp ) endif() target_sources(PowerPCTest PRIVATE PowerPC/TestValues.h + StubJit.h ) diff --git a/Source/UnitTests/Core/PageFaultTest.cpp b/Source/UnitTests/Core/PageFaultTest.cpp index f7805427612..8c29d31d640 100644 --- a/Source/UnitTests/Core/PageFaultTest.cpp +++ b/Source/UnitTests/Core/PageFaultTest.cpp @@ -7,10 +7,11 @@ #include "Common/ScopeGuard.h" #include "Core/Core.h" #include "Core/MemTools.h" -#include "Core/PowerPC/JitCommon/JitBase.h" #include "Core/PowerPC/JitInterface.h" #include "Core/System.h" +#include "StubJit.h" + // include order is important #include // NOLINT @@ -23,26 +24,11 @@ enum #endif }; -class PageFaultFakeJit : public JitBase +class PageFaultFakeJit : public StubJit { public: - explicit PageFaultFakeJit(Core::System& system) : JitBase(system) {} + explicit PageFaultFakeJit(Core::System& system) : StubJit(system) {} - // CPUCoreBase methods - void Init() override {} - void Shutdown() override {} - void ClearCache() override {} - void Run() override {} - void SingleStep() override {} - const char* GetName() const override { return nullptr; } - // JitBase methods - JitBaseBlockCache* GetBlockCache() override { return nullptr; } - void Jit(u32 em_address) override {} - void EraseSingleBlock(const JitBlock&) override {} - std::vector GetMemoryStats() const override { return {}; } - std::size_t DisassembleNearCode(const JitBlock&, std::ostream&) const override { return 0; } - std::size_t DisassembleFarCode(const JitBlock&, std::ostream&) const override { return 0; } - const CommonAsmRoutinesBase* GetAsmRoutines() override { return nullptr; } bool HandleFault(uintptr_t access_address, SContext* ctx) override { m_pre_unprotect_time = std::chrono::high_resolution_clock::now(); diff --git a/Source/UnitTests/Core/PowerPC/PageTableHostMappingTest.cpp b/Source/UnitTests/Core/PowerPC/PageTableHostMappingTest.cpp new file mode 100644 index 00000000000..fcb0071054c --- /dev/null +++ b/Source/UnitTests/Core/PowerPC/PageTableHostMappingTest.cpp @@ -0,0 +1,883 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include + +#include + +#include "Common/Align.h" +#include "Common/CommonTypes.h" +#include "Common/Swap.h" +#include "Core/Core.h" +#include "Core/MemTools.h" +#include "Core/PowerPC/BreakPoints.h" +#include "Core/PowerPC/Gekko.h" +#include "Core/PowerPC/JitInterface.h" +#include "Core/PowerPC/MMU.h" +#include "Core/PowerPC/PowerPC.h" +#include "Core/System.h" + +#include "../StubJit.h" + +#include + +// All guest addresses used in this unit test are arbitrary, aside from alignment requirements +static constexpr u32 ALIGNED_PAGE_TABLE_BASE = 0x00020000; +static constexpr u32 ALIGNED_PAGE_TABLE_MASK_SMALL = 0x0000ffff; +static constexpr u32 ALIGNED_PAGE_TABLE_MASK_LARGE = 0x0001ffff; + +static constexpr u32 MISALIGNED_PAGE_TABLE_BASE = 0x00050000; +static constexpr u32 MISALIGNED_PAGE_TABLE_BASE_ALIGNED = 0x00040000; +static constexpr u32 MISALIGNED_PAGE_TABLE_MASK = 0x0003ffff; + +static constexpr u32 HOLE_MASK_PAGE_TABLE_BASE = 0x00080000; +static constexpr u32 HOLE_MASK_PAGE_TABLE_MASK = 0x0002ffff; +static constexpr u32 HOLE_MASK_PAGE_TABLE_MASK_WITHOUT_HOLE = 0x0003ffff; + +static constexpr u32 MISALIGNED_HOLE_MASK_PAGE_TABLE_BASE = 0x000e0000; +static constexpr u32 MISALIGNED_HOLE_MASK_PAGE_TABLE_BASE_ALIGNED = 0x000d0000; +static constexpr u32 MISALIGNED_HOLE_MASK_PAGE_TABLE_MASK = 0x0002ffff; +static constexpr u32 MISALIGNED_HOLE_MASK_PAGE_TABLE_MASK_WITHOUT_HOLE = 0x0003ffff; + +static constexpr u32 TEMPORARY_MEMORY = 0x00000000; +static u32 s_current_temporary_memory = TEMPORARY_MEMORY; + +// This is the max that the unit test can handle, not the max that Core can handle +static constexpr u32 MAX_HOST_PAGE_SIZE = 64 * 1024; +static u32 s_minimum_mapping_size = 0; + +static volatile const void* volatile s_detection_address = nullptr; +static volatile size_t s_detection_count = 0; +static u32 s_counter = 0; +static std::set s_temporary_mappings; + +class PageFaultDetector : public StubJit +{ +public: + explicit PageFaultDetector(Core::System& system) : StubJit(system), m_block_cache(*this) {} + + bool HandleFault(uintptr_t access_address, SContext* ctx) override + { + if (access_address != reinterpret_cast(s_detection_address)) + { + std::string logical_address; + auto& memory = Core::System::GetInstance().GetMemory(); + auto logical_base = reinterpret_cast(memory.GetLogicalBase()); + if (access_address >= logical_base && access_address < logical_base + 0x1'0000'0000) + logical_address = fmt::format(" (PPC {:#010x})", access_address - logical_base); + + ADD_FAILURE() << fmt::format("Unexpected segfault at {:#x}{}", access_address, + logical_address); + + return false; + } + + s_detection_address = nullptr; + s_detection_count = s_detection_count + 1; + + // After we return from the signal handler, the memory access will happen again. + // Let it succeed this time so the signal handler won't get called over and over. + auto& memory = Core::System::GetInstance().GetMemory(); + const uintptr_t logical_base = reinterpret_cast(memory.GetLogicalBase()); + const u32 logical_address = static_cast(access_address - logical_base); + const u32 mask = s_minimum_mapping_size - 1; + for (u32 i = logical_address & mask; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + { + const u32 current_logical_address = (logical_address & ~mask) + i; + memory.AddPageTableMapping(current_logical_address, s_current_temporary_memory + i, true); + s_temporary_mappings.emplace(current_logical_address); + } + return true; + } + + bool WantsPageTableMappings() const override { return true; } + + // PowerPC::MMU::DBATUpdated wants to clear blocks in the block cache, + // so this can't just return nullptr + JitBaseBlockCache* GetBlockCache() override { return &m_block_cache; } + +private: + StubBlockCache m_block_cache; +}; + +// This is used as a performance optimization. If several page table updates are performed while +// DR is disabled, MMU.cpp will only have to rescan the page table one time once DR is enabled again +// instead of after each page table update. +class DisableDR final +{ +public: + DisableDR() + { + auto& system = Core::System::GetInstance(); + system.GetPPCState().msr.DR = 0; + system.GetPowerPC().MSRUpdated(); + } + + ~DisableDR() + { + auto& system = Core::System::GetInstance(); + system.GetPPCState().msr.DR = 1; + system.GetPowerPC().MSRUpdated(); + } +}; + +class PageTableHostMappingTest : public ::testing::Test +{ +public: + static void SetUpTestSuite() + { + if (!EMM::IsExceptionHandlerSupported()) + GTEST_SKIP() << "Skipping PageTableHostMappingTest because exception handler is unsupported."; + + auto& system = Core::System::GetInstance(); + auto& memory = system.GetMemory(); + const u32 host_page_size = memory.GetHostPageSize(); + s_minimum_mapping_size = std::max(host_page_size, PowerPC::HW_PAGE_SIZE); + + if (!std::has_single_bit(host_page_size) || host_page_size > MAX_HOST_PAGE_SIZE) + { + GTEST_SKIP() << fmt::format( + "Skipping PageTableHostMappingTest because page size {} is unsupported.", host_page_size); + } + + memory.Init(); + if (!memory.InitFastmemArena()) + { + memory.Shutdown(); + GTEST_SKIP() << "Skipping PageTableHostMappingTest because InitFastmemArena failed."; + } + + Core::DeclareAsCPUThread(); + EMM::InstallExceptionHandler(); + system.GetJitInterface().SetJit(std::make_unique(system)); + + // Make sure BATs and SRs are cleared + auto& power_pc = system.GetPowerPC(); + power_pc.Reset(); + + // Set up an SR + SetSR(1, 123); + + // Specify a page table + SetSDR(ALIGNED_PAGE_TABLE_BASE, ALIGNED_PAGE_TABLE_MASK_SMALL); + + // Enable address translation + system.GetPPCState().msr.DR = 1; + system.GetPPCState().msr.IR = 1; + power_pc.MSRUpdated(); + } + + static void TearDownTestSuite() + { + auto& system = Core::System::GetInstance(); + + system.GetJitInterface().SetJit(nullptr); + EMM::UninstallExceptionHandler(); + Core::UndeclareAsCPUThread(); + system.GetMemory().Shutdown(); + } + + static void SetSR(size_t index, u32 vsid) + { + ASSERT_FALSE(index == 4 || index == 7) + << fmt::format("sr{} has conflicts with fake VMEM mapping", index); + + UReg_SR sr{}; + sr.VSID = vsid; + + auto& system = Core::System::GetInstance(); + system.GetPPCState().sr[index] = sr.Hex; + system.GetMMU().SRUpdated(); + } + + static void SetSDR(u32 page_table_base, u32 page_table_mask) + { + UReg_SDR1 sdr; + sdr.htabmask = page_table_mask >> 16; + sdr.reserved = 0; + sdr.htaborg = page_table_base >> 16; + + auto& system = Core::System::GetInstance(); + system.GetPPCState().spr[SPR_SDR] = sdr.Hex; + system.GetMMU().SDRUpdated(); + } + + static void SetBAT(u32 spr, UReg_BAT_Up batu, UReg_BAT_Lo batl) + { + auto& system = Core::System::GetInstance(); + auto& ppc_state = system.GetPPCState(); + auto& mmu = system.GetMMU(); + + ppc_state.spr[spr + 1] = batl.Hex; + ppc_state.spr[spr] = batu.Hex; + + if ((spr >= SPR_IBAT0U && spr <= SPR_IBAT3L) || (spr >= SPR_IBAT4U && spr <= SPR_IBAT7L)) + mmu.IBATUpdated(); + if ((spr >= SPR_DBAT0U && spr <= SPR_DBAT3L) || (spr >= SPR_DBAT4U && spr <= SPR_DBAT7L)) + mmu.DBATUpdated(); + } + + static void SetBAT(u32 spr, u32 logical_address, u32 physical_address, u32 size) + { + UReg_BAT_Up batu{}; + batu.VP = 1; + batu.VS = 1; + batu.BL = (size - 1) >> PowerPC::BAT_INDEX_SHIFT; + batu.BEPI = logical_address >> PowerPC::BAT_INDEX_SHIFT; + + UReg_BAT_Lo batl{}; + batl.PP = 2; + batl.WIMG = 0; + batl.BRPN = physical_address >> PowerPC::BAT_INDEX_SHIFT; + + SetBAT(spr, batu, batl); + } + + static void ExpectMapped(u32 logical_address, u32 physical_address) + { + SCOPED_TRACE( + fmt::format("ExpectMapped({:#010x}, {:#010x})", logical_address, physical_address)); + + auto& memory = Core::System::GetInstance().GetMemory(); + u8* physical_base = memory.GetPhysicalBase(); + u8* logical_base = memory.GetLogicalBase(); + + auto* physical_ptr = reinterpret_cast(physical_base + physical_address); + auto* logical_ptr = reinterpret_cast(logical_base + logical_address); + + *physical_ptr = ++s_counter; + EXPECT_EQ(*logical_ptr, s_counter) + << "Page was mapped to a different physical page than expected"; + + *logical_ptr = ++s_counter; + EXPECT_EQ(*physical_ptr, s_counter) + << "Page was mapped to a different physical page than expected"; + } + +#ifdef _MSC_VER +#define ASAN_DISABLE __declspec(no_sanitize_address) +#else +#define ASAN_DISABLE +#endif + + static void ASAN_DISABLE ExpectReadOnlyMapped(u32 logical_address, u32 physical_address) + { + SCOPED_TRACE( + fmt::format("ExpectReadOnlyMapped({:#010x}, {:#010x})", logical_address, physical_address)); + + auto& memory = Core::System::GetInstance().GetMemory(); + u8* physical_base = memory.GetPhysicalBase(); + u8* logical_base = memory.GetLogicalBase(); + + auto* physical_ptr = reinterpret_cast(physical_base + physical_address); + auto* logical_ptr = reinterpret_cast(logical_base + logical_address); + + *physical_ptr = ++s_counter; + EXPECT_EQ(*logical_ptr, s_counter) + << "Page was mapped to a different physical page than expected"; + + s_detection_address = logical_ptr; + s_detection_count = 0; + + // This line should fault + *logical_ptr = ++s_counter; + + memory.RemovePageTableMappings(s_temporary_mappings); + s_temporary_mappings.clear(); + EXPECT_EQ(s_detection_count, u32(1)) << "Page was mapped as writeable, against expectations"; + } + + static void ASAN_DISABLE ExpectNotMapped(u32 logical_address) + { + SCOPED_TRACE(fmt::format("ExpectNotMapped({:#010x})", logical_address)); + + auto& memory = Core::System::GetInstance().GetMemory(); + u8* logical_base = memory.GetLogicalBase(); + + auto* logical_ptr = reinterpret_cast(logical_base + logical_address); + s_detection_address = logical_ptr; + s_detection_count = 0; + + // This line should fault + *logical_ptr; + + memory.RemovePageTableMappings(s_temporary_mappings); + s_temporary_mappings.clear(); + EXPECT_EQ(s_detection_count, u32(1)) << "Page was mapped, against expectations"; + } + + static void ExpectMappedOnlyIf4KHostPages(u32 logical_address, u32 physical_address) + { + if (s_minimum_mapping_size > PowerPC::HW_PAGE_SIZE) + ExpectNotMapped(logical_address); + else + ExpectMapped(logical_address, physical_address); + } + + static std::pair GetPTE(u32 logical_address, u32 index) + { + auto& system = Core::System::GetInstance(); + auto& ppc_state = system.GetPPCState(); + + const UReg_SR sr(system.GetPPCState().sr[logical_address >> 28]); + u32 hash = sr.VSID ^ (logical_address >> 12); + if ((index & 0x8) != 0) + hash = ~hash; + + const u32 pteg_addr = ((hash << 6) & ppc_state.pagetable_mask) | ppc_state.pagetable_base; + const u32 pte_addr = (index & 0x7) * 8 + pteg_addr; + + const u8* physical_base = system.GetMemory().GetPhysicalBase(); + const UPTE_Lo pte1(Common::swap32(physical_base + pte_addr)); + const UPTE_Hi pte2(Common::swap32(physical_base + pte_addr + 4)); + + return {pte1, pte2}; + } + + static void SetPTE(UPTE_Lo pte1, UPTE_Hi pte2, u32 logical_address, u32 index) + { + auto& system = Core::System::GetInstance(); + auto& ppc_state = system.GetPPCState(); + + pte1.H = (index & 0x8) != 0; + + u32 hash = pte1.VSID ^ (logical_address >> 12); + if (pte1.H) + hash = ~hash; + + const u32 pteg_addr = ((hash << 6) & ppc_state.pagetable_mask) | ppc_state.pagetable_base; + const u32 pte_addr = (index & 0x7) * 8 + pteg_addr; + + u8* physical_base = system.GetMemory().GetPhysicalBase(); + Common::WriteSwap32(physical_base + pte_addr, pte1.Hex); + Common::WriteSwap32(physical_base + pte_addr + 4, pte2.Hex); + + system.GetMMU().InvalidateTLBEntry(logical_address); + } + + static std::pair CreateMapping(u32 logical_address, u32 physical_address) + { + auto& ppc_state = Core::System::GetInstance().GetPPCState(); + + UPTE_Lo pte1{}; + pte1.API = logical_address >> 22; + pte1.VSID = UReg_SR{ppc_state.sr[logical_address >> 28]}.VSID; + pte1.V = 1; // Mapping is valid + + UPTE_Hi pte2{}; + pte2.C = 1; // Page has been written to (MMU.cpp won't map as writeable without this) + pte2.R = 1; // Page has been read from (MMU.cpp won't map at all without this) + pte2.RPN = physical_address >> 12; + + return {pte1, pte2}; + } + + static void AddMapping(u32 logical_address, u32 physical_address, u32 index) + { + auto [pte1, pte2] = CreateMapping(logical_address, physical_address); + SetPTE(pte1, pte2, logical_address, index); + } + + static void AddHostSizedMapping(u32 logical_address, u32 physical_address, u32 index) + { + DisableDR disable_dr; + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + AddMapping(logical_address + i, physical_address + i, index); + } + + static void RemoveMapping(u32 logical_address, u32 physical_address, u32 index) + { + auto [pte1, pte2] = CreateMapping(logical_address, physical_address); + pte1.V = 0; // Mapping is invalid + SetPTE(pte1, pte2, logical_address, index); + } + + static void RemoveHostSizedMapping(u32 logical_address, u32 physical_address, u32 index) + { + DisableDR disable_dr; + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + RemoveMapping(logical_address + i, physical_address + i, index); + } +}; + +TEST_F(PageTableHostMappingTest, Basic) +{ + s_current_temporary_memory = 0x00100000; + + // Create a basic mapping + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + { + ExpectNotMapped(0x10100000 + i); + AddMapping(0x10100000 + i, 0x00100000 + i, 0); + } + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + ExpectMapped(0x10100000 + i, 0x00100000 + i); + + // Create another mapping pointing to the same physical address + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + { + ExpectNotMapped(0x10100000 + s_minimum_mapping_size + i); + AddMapping(0x10100000 + s_minimum_mapping_size + i, 0x00100000 + i, 0); + } + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + { + ExpectMapped(0x10100000 + i, 0x00100000 + i); + ExpectMapped(0x10100000 + s_minimum_mapping_size + i, 0x00100000 + i); + } + + // Remove the first page + RemoveMapping(0x10100000, 0x00100000, 0); + ExpectNotMapped(0x10100000); + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + ExpectMapped(0x10100000 + s_minimum_mapping_size + i, 0x00100000 + i); + + s_current_temporary_memory = TEMPORARY_MEMORY; +} + +TEST_F(PageTableHostMappingTest, LargeHostPageMismatchedAddresses) +{ + { + DisableDR disable_dr; + AddMapping(0x10110000, 0x00111000, 0); + for (u32 i = PowerPC::HW_PAGE_SIZE; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + AddMapping(0x10110000 + i, 0x00110000 + i, 0); + } + + ExpectMappedOnlyIf4KHostPages(0x10110000, 0x00111000); +} + +TEST_F(PageTableHostMappingTest, LargeHostPageMisalignedAddresses) +{ + { + DisableDR disable_dr; + for (u32 i = 0; i < s_minimum_mapping_size * 2; i += PowerPC::HW_PAGE_SIZE) + AddMapping(0x10120000 + i, 0x00121000 + i, 0); + } + + ExpectMappedOnlyIf4KHostPages(0x10120000, 0x00121000); + ExpectMappedOnlyIf4KHostPages(0x10120000 + s_minimum_mapping_size, + 0x00121000 + s_minimum_mapping_size); +} + +TEST_F(PageTableHostMappingTest, ChangeSR) +{ + { + DisableDR disable_dr; + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + { + auto [pte1, pte2] = CreateMapping(0x20130000 + i, 0x00130000 + i); + pte1.VSID = 0xabc; + SetPTE(pte1, pte2, 0x20130000 + i, 0); + } + } + ExpectNotMapped(0x20130000); + + SetSR(2, 0xabc); + ExpectMapped(0x20130000, 0x00130000); + ExpectNotMapped(0x30130000); + + SetSR(3, 0xabc); + ExpectMapped(0x20130000, 0x00130000); + ExpectMapped(0x30130000, 0x00130000); + ExpectNotMapped(0x00130000); + ExpectNotMapped(0x10130000); +} + +// DBAT takes priority over page table mappings. +TEST_F(PageTableHostMappingTest, DBATPriority) +{ + SetSR(5, 5); + + AddHostSizedMapping(0x50140000, 0x00150000, 0); + ExpectMapped(0x50140000, 0x00150000); + + SetBAT(SPR_DBAT0U, 0x50000000, 0x00000000, 0x01000000); + ExpectMapped(0x50140000, 0x00140000); +} + +// Host-side page table mappings are for data only, so IBAT has no effect on them. +TEST_F(PageTableHostMappingTest, IBATPriority) +{ + SetSR(6, 6); + + AddHostSizedMapping(0x60160000, 0x00170000, 0); + ExpectMapped(0x60160000, 0x00170000); + + SetBAT(SPR_IBAT0U, 0x60000000, 0x00000000, 0x01000000); + ExpectMapped(0x60160000, 0x00170000); +} + +TEST_F(PageTableHostMappingTest, Priority) +{ + // Secondary PTEs for 0x10180000 + + AddHostSizedMapping(0x10180000, 0x00180000, 10); + ExpectMapped(0x10180000, 0x00180000); + + AddHostSizedMapping(0x10180000, 0x00190000, 12); + ExpectMapped(0x10180000, 0x00180000); + + AddHostSizedMapping(0x10180000, 0x001a0000, 8); + ExpectMapped(0x10180000, 0x001a0000); + + RemoveHostSizedMapping(0x10180000, 0x00180000, 10); + ExpectMapped(0x10180000, 0x001a0000); + + RemoveHostSizedMapping(0x10180000, 0x001a0000, 8); + ExpectMapped(0x10180000, 0x00190000); + + // Primary PTEs for 0x10180000 + + AddHostSizedMapping(0x10180000, 0x00180000, 2); + ExpectMapped(0x10180000, 0x00180000); + + AddHostSizedMapping(0x10180000, 0x001a0000, 4); + ExpectMapped(0x10180000, 0x00180000); + + AddHostSizedMapping(0x10180000, 0x001b0000, 0); + ExpectMapped(0x10180000, 0x001b0000); + + RemoveHostSizedMapping(0x10180000, 0x00180000, 2); + ExpectMapped(0x10180000, 0x001b0000); + + RemoveHostSizedMapping(0x10180000, 0x001b0000, 0); + ExpectMapped(0x10180000, 0x001a0000); + + // Return to secondary PTE for 0x10180000 + + RemoveHostSizedMapping(0x10180000, 0x001a0000, 4); + ExpectMapped(0x10180000, 0x00190000); + + // Secondary PTEs for 0x11180000 + + AddHostSizedMapping(0x11180000, 0x01180000, 11); + ExpectMapped(0x11180000, 0x01180000); + + AddHostSizedMapping(0x11180000, 0x01190000, 13); + ExpectMapped(0x11180000, 0x01180000); + + AddHostSizedMapping(0x11180000, 0x011a0000, 9); + ExpectMapped(0x11180000, 0x011a0000); + + RemoveHostSizedMapping(0x11180000, 0x01180000, 11); + ExpectMapped(0x11180000, 0x011a0000); + + RemoveHostSizedMapping(0x11180000, 0x011a0000, 9); + ExpectMapped(0x11180000, 0x01190000); + + // Primary PTEs for 0x11180000 + + AddHostSizedMapping(0x11180000, 0x01180000, 3); + ExpectMapped(0x11180000, 0x01180000); + + AddHostSizedMapping(0x11180000, 0x011a0000, 5); + ExpectMapped(0x11180000, 0x01180000); + + AddHostSizedMapping(0x11180000, 0x011b0000, 1); + ExpectMapped(0x11180000, 0x011b0000); + + RemoveHostSizedMapping(0x11180000, 0x01180000, 3); + ExpectMapped(0x11180000, 0x011b0000); + + RemoveHostSizedMapping(0x11180000, 0x011b0000, 1); + ExpectMapped(0x11180000, 0x011a0000); + + // Return to secondary PTE for 0x11180000 + + RemoveHostSizedMapping(0x11180000, 0x011a0000, 5); + ExpectMapped(0x11180000, 0x01190000); + + // Check that 0x10180000 is still working properly + + ExpectMapped(0x10180000, 0x00190000); + + AddHostSizedMapping(0x10180000, 0x00180000, 0); + ExpectMapped(0x10180000, 0x00180000); + + // Check that 0x11180000 is still working properly + + ExpectMapped(0x11180000, 0x01190000); + + AddHostSizedMapping(0x11180000, 0x01180000, 1); + ExpectMapped(0x11180000, 0x01180000); +} + +TEST_F(PageTableHostMappingTest, ChangeAddress) +{ + // Initial mapping + AddHostSizedMapping(0x101c0000, 0x001c0000, 0); + ExpectMapped(0x101c0000, 0x001c0000); + + // Change physical address + AddHostSizedMapping(0x101c0000, 0x001d0000, 0); + ExpectMapped(0x101c0000, 0x001d0000); + + // Change logical address + AddHostSizedMapping(0x111c0000, 0x001d0000, 0); + ExpectMapped(0x111c0000, 0x001d0000); + ExpectNotMapped(0x101c0000); + + // Change both logical address and physical address + AddHostSizedMapping(0x101c0000, 0x011d0000, 0); + ExpectMapped(0x101c0000, 0x011d0000); + ExpectNotMapped(0x111c0000); +} + +TEST_F(PageTableHostMappingTest, InvalidPhysicalAddress) +{ + AddHostSizedMapping(0x101d0000, 0x0ff00000, 0); + ExpectNotMapped(0x101d0000); +} + +TEST_F(PageTableHostMappingTest, WIMG) +{ + for (u32 i = 0; i < 16; ++i) + { + { + DisableDR disable_dr; + for (u32 j = 0; j < s_minimum_mapping_size; j += PowerPC::HW_PAGE_SIZE) + { + auto [pte1, pte2] = CreateMapping(0x101e0000 + j, 0x001e0000 + j); + pte2.WIMG = i; + SetPTE(pte1, pte2, 0x101e0000 + j, 0); + } + } + + if ((i & 0b1100) != 0) + ExpectNotMapped(0x101e0000); + else + ExpectMapped(0x101e0000, 0x001e0000); + } +} + +TEST_F(PageTableHostMappingTest, RC) +{ + auto& mmu = Core::System::GetInstance().GetMMU(); + + const auto set_up_mapping = [] { + DisableDR disable_dr; + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + { + auto [pte1, pte2] = CreateMapping(0x101f0000 + i, 0x001f0000 + i); + pte2.R = 0; + pte2.C = 0; + SetPTE(pte1, pte2, 0x101f0000 + i, 0); + } + }; + + const auto expect_rc = [](u32 r, u32 c) { + auto [pte1, pte2] = GetPTE(0x101f0000, 0); + EXPECT_TRUE(pte1.V); + EXPECT_EQ(pte2.R, r); + EXPECT_EQ(pte2.C, c); + }; + + // Start with R=0, C=0 + set_up_mapping(); + ExpectNotMapped(0x101f0000); + expect_rc(0, 0); + + // Automatically set R=1, C=0 + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + mmu.Read(0x101f0000 + i); + ExpectReadOnlyMapped(0x101f0000, 0x001f0000); + expect_rc(1, 0); + + // Automatically set R=1, C=1 + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + mmu.Write(0x12345678, 0x101f0000 + i); + ExpectMapped(0x101f0000, 0x001f0000); + expect_rc(1, 1); + + // Start over with R=0, C=0 + set_up_mapping(); + ExpectNotMapped(0x101f0000); + expect_rc(0, 0); + + // Automatically set R=1, C=1 + for (u32 i = 0; i < s_minimum_mapping_size; i += PowerPC::HW_PAGE_SIZE) + mmu.Write(0x12345678, 0x101f0000 + i); + ExpectMapped(0x101f0000, 0x001f0000); + expect_rc(1, 1); +} + +TEST_F(PageTableHostMappingTest, ResizePageTable) +{ + AddHostSizedMapping(0x10200000, 0x00200000, 0); + AddHostSizedMapping(0x10600000, 0x00210000, 1); + ExpectMapped(0x10200000, 0x00200000); + ExpectMapped(0x10600000, 0x00210000); + + SetSDR(ALIGNED_PAGE_TABLE_BASE, ALIGNED_PAGE_TABLE_MASK_LARGE); + ExpectMapped(0x10200000, 0x00200000); + ExpectNotMapped(0x10600000); + + AddHostSizedMapping(0x10600000, 0x00220000, 1); + ExpectMapped(0x10200000, 0x00200000); + ExpectMapped(0x10600000, 0x00220000); + + AddHostSizedMapping(0x10610000, 0x00200000, 1); + ExpectMapped(0x10610000, 0x00200000); + + SetSDR(ALIGNED_PAGE_TABLE_BASE, ALIGNED_PAGE_TABLE_MASK_SMALL); + ExpectMapped(0x10200000, 0x00200000); + ExpectMapped(0x10600000, 0x00210000); + ExpectNotMapped(0x10610000); +} + +// The PEM says that all bits that are one in the page table mask must be zero in the page table +// address. What it doesn't tell you is that if this isn't obeyed, the Gekko will do a logical OR of +// the page table base and the page table offset, producing behavior that might not be intuitive. +TEST_F(PageTableHostMappingTest, MisalignedPageTable) +{ + SetSDR(MISALIGNED_PAGE_TABLE_BASE + 0x10000, PowerPC::PAGE_TABLE_MIN_SIZE - 1); + + AddHostSizedMapping(0x10a30000, 0x00230000, 4); + ExpectMapped(0x10a30000, 0x00230000); + + SetSDR(MISALIGNED_PAGE_TABLE_BASE, MISALIGNED_PAGE_TABLE_MASK); + + ExpectNotMapped(0x10a30000); + + AddHostSizedMapping(0x10230000, 0x00240000, 0); + AddHostSizedMapping(0x10630000, 0x00250000, 1); + AddHostSizedMapping(0x10a30000, 0x00260000, 2); + AddHostSizedMapping(0x10e30000, 0x00270000, 3); + + ExpectMapped(0x10230000, 0x00240000); + ExpectMapped(0x10630000, 0x00250000); + ExpectMapped(0x10a30000, 0x00260000); + ExpectMapped(0x10e30000, 0x00270000); + + // Exercise the code for falling back to a secondary PTE after removing a primary PTE. + AddHostSizedMapping(0x10a30000, 0x00270000, 10); + AddHostSizedMapping(0x10e30000, 0x00260000, 11); + RemoveHostSizedMapping(0x10a30000, 0x00260000, 2); + RemoveHostSizedMapping(0x10e30000, 0x00250000, 3); + + ExpectMapped(0x10230000, 0x00240000); + ExpectMapped(0x10630000, 0x00250000); + ExpectMapped(0x10a30000, 0x00270000); + ExpectMapped(0x10e30000, 0x00260000); + + SetSDR(MISALIGNED_PAGE_TABLE_BASE_ALIGNED, MISALIGNED_PAGE_TABLE_MASK); + + ExpectNotMapped(0x10230000); + ExpectMapped(0x10630000, 0x00250000); + ExpectMapped(0x10a30000, 0x00230000); + ExpectNotMapped(0x10e30000); + + SetSDR(ALIGNED_PAGE_TABLE_BASE, ALIGNED_PAGE_TABLE_MASK_SMALL); +} + +// Putting a zero in the middle of the page table mask's ones results in similar behavior +// to the scenario described above. +TEST_F(PageTableHostMappingTest, HoleInMask) +{ + SetSDR(HOLE_MASK_PAGE_TABLE_BASE + 0x10000, PowerPC::PAGE_TABLE_MIN_SIZE - 1); + + AddHostSizedMapping(0x10680000, 0x00280000, 4); + ExpectMapped(0x10680000, 0x00280000); + + SetSDR(HOLE_MASK_PAGE_TABLE_BASE, HOLE_MASK_PAGE_TABLE_MASK); + + ExpectNotMapped(0x10680000); + + AddHostSizedMapping(0x10280000, 0x00290000, 0); + AddHostSizedMapping(0x10680000, 0x002a0000, 1); + AddHostSizedMapping(0x10a80000, 0x002b0000, 2); + AddHostSizedMapping(0x10e80000, 0x002c0000, 3); + + ExpectMapped(0x10280000, 0x00290000); + ExpectMapped(0x10680000, 0x002a0000); + ExpectMapped(0x10a80000, 0x002b0000); + ExpectMapped(0x10e80000, 0x002c0000); + + // Exercise the code for falling back to a secondary PTE after removing a primary PTE. + AddHostSizedMapping(0x10a80000, 0x002c0000, 10); + AddHostSizedMapping(0x10e80000, 0x002b0000, 11); + RemoveHostSizedMapping(0x10a80000, 0x002b0000, 2); + RemoveHostSizedMapping(0x10e80000, 0x002c0000, 3); + + ExpectMapped(0x10280000, 0x00290000); + ExpectMapped(0x10680000, 0x002a0000); + ExpectMapped(0x10a80000, 0x002c0000); + ExpectMapped(0x10e80000, 0x002b0000); + + SetSDR(HOLE_MASK_PAGE_TABLE_BASE, HOLE_MASK_PAGE_TABLE_MASK_WITHOUT_HOLE); + + ExpectMapped(0x10280000, 0x00290000); + ExpectMapped(0x10680000, 0x00280000); + ExpectNotMapped(0x10a80000); + ExpectMapped(0x10e80000, 0x002b0000); + + SetSDR(ALIGNED_PAGE_TABLE_BASE, ALIGNED_PAGE_TABLE_MASK_SMALL); +} + +// If we combine the two scenarios above, both making the base misaligned and putting a hole in the +// mask, we get the same result as if we just make the base misaligned. +TEST_F(PageTableHostMappingTest, HoleInMaskMisalignedPageTable) +{ + SetSDR(MISALIGNED_PAGE_TABLE_BASE + 0x10000, PowerPC::PAGE_TABLE_MIN_SIZE - 1); + + AddHostSizedMapping(0x10ad0000, 0x002d0000, 4); + ExpectMapped(0x10ad0000, 0x002d0000); + + SetSDR(MISALIGNED_PAGE_TABLE_BASE, MISALIGNED_PAGE_TABLE_MASK); + + ExpectNotMapped(0x10ad0000); + + AddHostSizedMapping(0x102d0000, 0x002e0000, 0); + AddHostSizedMapping(0x106d0000, 0x002f0000, 1); + AddHostSizedMapping(0x10ad0000, 0x00300000, 2); + AddHostSizedMapping(0x10ed0000, 0x00310000, 3); + + ExpectMapped(0x102d0000, 0x002e0000); + ExpectMapped(0x106d0000, 0x002f0000); + ExpectMapped(0x10ad0000, 0x00300000); + ExpectMapped(0x10ed0000, 0x00310000); + + // Exercise the code for falling back to a secondary PTE after removing a primary PTE. + AddHostSizedMapping(0x10ad0000, 0x00310000, 10); + AddHostSizedMapping(0x10ed0000, 0x00300000, 11); + RemoveHostSizedMapping(0x10ad0000, 0x00300000, 2); + RemoveHostSizedMapping(0x10ed0000, 0x00310000, 3); + + ExpectMapped(0x102d0000, 0x002e0000); + ExpectMapped(0x106d0000, 0x002f0000); + ExpectMapped(0x10ad0000, 0x00310000); + ExpectMapped(0x10ed0000, 0x00300000); + + SetSDR(MISALIGNED_PAGE_TABLE_BASE_ALIGNED, MISALIGNED_PAGE_TABLE_MASK); + + ExpectNotMapped(0x102d0000); + ExpectMapped(0x106d0000, 0x002f0000); + ExpectMapped(0x10ad0000, 0x002d0000); + ExpectNotMapped(0x10ed0000); + + SetSDR(ALIGNED_PAGE_TABLE_BASE, ALIGNED_PAGE_TABLE_MASK_SMALL); +} + +TEST_F(PageTableHostMappingTest, MemChecks) +{ + AddHostSizedMapping(0x10320000, 0x00330000, 0); + AddHostSizedMapping(0x10330000, 0x00320000, 0); + ExpectMapped(0x10320000, 0x00330000); + ExpectMapped(0x10330000, 0x00320000); + + auto& memchecks = Core::System::GetInstance().GetPowerPC().GetMemChecks(); + TMemCheck memcheck; + memcheck.start_address = 0x10320000; + memcheck.end_address = 0x10320001; + memchecks.Add(std::move(memcheck)); + + ExpectNotMapped(0x10320000); + ExpectMapped(0x10330000, 0x00320000); + + memchecks.Remove(0x10320000); + + ExpectMapped(0x10320000, 0x00330000); + ExpectMapped(0x10330000, 0x00320000); +} diff --git a/Source/UnitTests/Core/StubJit.h b/Source/UnitTests/Core/StubJit.h new file mode 100644 index 00000000000..50a1e6efb54 --- /dev/null +++ b/Source/UnitTests/Core/StubJit.h @@ -0,0 +1,38 @@ +// Copyright 2026 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "Core/PowerPC/JitCommon/JitBase.h" +#include "Core/PowerPC/JitCommon/JitCache.h" + +class StubJit : public JitBase +{ +public: + explicit StubJit(Core::System& system) : JitBase(system) {} + + // CPUCoreBase methods + void Init() override {} + void Shutdown() override {} + void ClearCache() override {} + void Run() override {} + void SingleStep() override {} + const char* GetName() const override { return nullptr; } + // JitBase methods + JitBaseBlockCache* GetBlockCache() override { return nullptr; } + void Jit(u32) override {} + void EraseSingleBlock(const JitBlock&) override {} + std::vector GetMemoryStats() const override { return {}; } + std::size_t DisassembleNearCode(const JitBlock&, std::ostream&) const override { return 0; } + std::size_t DisassembleFarCode(const JitBlock&, std::ostream&) const override { return 0; } + const CommonAsmRoutinesBase* GetAsmRoutines() override { return nullptr; } + bool HandleFault(uintptr_t, SContext*) override { return false; } +}; + +class StubBlockCache : public JitBaseBlockCache +{ +public: + explicit StubBlockCache(JitBase& jit) : JitBaseBlockCache(jit) {} + + void WriteLinkBlock(const JitBlock::LinkData&, const JitBlock*) override {} +}; diff --git a/Source/UnitTests/UnitTests.vcxproj b/Source/UnitTests/UnitTests.vcxproj index 5ffba903d93..6b476dad940 100644 --- a/Source/UnitTests/UnitTests.vcxproj +++ b/Source/UnitTests/UnitTests.vcxproj @@ -32,6 +32,7 @@ + @@ -74,6 +75,7 @@ +