IOS: checkpoint Starlet JIT, PPC cache and Wii timing fixes

Save the remaining ARM JIT and MMU/cache optimizations, accurate Starlet timer and Wiimote report cadence, opt-in PPC event tracing, and full texture hashing in the LLE launcher. Include regression coverage and exclude local profiling artifacts.

Validation: 127 targeted tests from 15 suites passed, with one disabled test. Includes the current user-tested source state following the persistent NAND milestone.
This commit is contained in:
2026-09-08 11:45:51 +02:00
parent 31760e8bce
commit ebb753d09a
25 changed files with 2661 additions and 125 deletions
+1
View File
@@ -26,6 +26,7 @@ if(_M_X86_64)
PowerPC/DivUtilsTest.cpp
PowerPC/PageTableHostMappingTest.cpp
PowerPC/Jit64Common/ConvertDoubleToSingle.cpp
PowerPC/Jit64Common/DCache.cpp
PowerPC/Jit64Common/Fres.cpp
PowerPC/Jit64Common/Frsqrte.cpp
)
@@ -10,6 +10,7 @@
#include <gtest/gtest.h>
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
@@ -171,6 +172,14 @@ public:
return m_sram_fastmem_enabled ? &m_sram_split_mode : nullptr;
}
const u8* GetDirectMemoryPointer(u32 address, u32 size) const override
{
const size_t offset = ToOffset(address);
if (size == 0 || offset > m_memory.size() || size > m_memory.size() - offset)
return nullptr;
return m_memory.data() + offset;
}
void SetIdlePollSafe(bool safe) { m_idle_poll_safe = safe; }
void SetSliceStablePollAddress(u32 address)
{
@@ -376,7 +385,7 @@ TEST(StarletTimer, ZeroDelayAlarmMatchesImmediatelyAndUsesIRQW1C)
memory.Write8(address + 3, static_cast<u8>(value));
};
memory.AdvanceCycles(405);
memory.AdvanceCycles(32 * 128);
ASSERT_EQ(read_word(timer), 32u);
write_word(alarm, read_word(timer));
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
@@ -388,12 +397,155 @@ TEST(StarletTimer, ZeroDelayAlarmMatchesImmediatelyAndUsesIRQW1C)
write_word(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
memory.AdvanceCycles(404);
memory.AdvanceCycles(32 * 128 - 1);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
memory.AdvanceCycles(1);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
}
TEST(StarletRegisters, WideTimerAndInterruptAccessesMatchHardwareSemantics)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 timer = hardware_base + 0x10;
constexpr u32 arm_irq_flag = hardware_base + 0x38;
constexpr u32 arm_irq_mask = hardware_base + 0x3c;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.AdvanceCycles(32 * 128);
EXPECT_EQ(memory.Read32(timer), 32u);
memory.Write32(timer, 64);
EXPECT_EQ(memory.Read32(timer), 64u);
system.GetWiiIPC().SetStarletInterrupt(INT_CAUSE_TIMER, true);
EXPECT_EQ(memory.Read32(arm_irq_flag) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
memory.Write32(arm_irq_flag, INT_CAUSE_TIMER);
EXPECT_EQ(memory.Read32(arm_irq_flag) & INT_CAUSE_TIMER, 0u);
memory.Write32(arm_irq_mask, 0x800619ef);
EXPECT_EQ(memory.Read32(arm_irq_mask), 0x800619efu);
}
TEST(StarletTimer, RunsAtOneTickPer128ARMCycles)
{
constexpr u32 timer = 0x0d800010;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.AdvanceCycles(127);
EXPECT_EQ(memory.Read32(timer), 0u);
memory.AdvanceCycles(1);
EXPECT_EQ(memory.Read32(timer), 1u);
memory.AdvanceCycles(243'000'000 - 128);
EXPECT_EQ(memory.Read32(timer), 1'898'437u);
memory.AdvanceCycles(243'000'000);
EXPECT_EQ(memory.Read32(timer), 3'796'875u);
}
TEST(StarletTimer, SchedulerSlicePartitionDoesNotChangeClock)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
constexpr u64 total_cycles = 243'000'000;
// Active, IPC, and idle scheduler slices must use the same clock, with no
// fractional timer ticks lost at the end of a slice.
for (const u64 slice : {256u, 4096u, 24300u})
{
memory.Reset();
for (u64 elapsed = 0; elapsed < total_cycles;)
{
const u64 step = std::min(slice, total_cycles - elapsed);
memory.AdvanceCycles(step);
elapsed += step;
}
EXPECT_EQ(memory.Read32(0x0d800010), 1'898'437u) << "slice=" << slice;
EXPECT_EQ(memory.GetCycles(), total_cycles);
}
}
TEST(StarletTimer, CounterWritesDoNotRewindPeripheralClock)
{
constexpr u32 timer = 0x0d800010;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
memory.AdvanceCycles(1025);
for (const u32 value : {1u, 0xffffffffu, 0u, 0x12345678u})
{
memory.Write32(timer, value);
EXPECT_EQ(memory.Read32(timer), value);
EXPECT_EQ(memory.GetCycles(), 1025u);
}
// Reprogramming HW_TIMER leaves the free-running /128 clock phase intact.
memory.AdvanceCycles(126);
EXPECT_EQ(memory.Read32(timer), 0x12345678u);
memory.AdvanceCycles(1);
EXPECT_EQ(memory.Read32(timer), 0x12345679u);
}
TEST(StarletTimer, ByteAssembledCounterWritesMatchWideWrites)
{
constexpr u32 timer = 0x0d800010;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
memory.AdvanceCycles(1280);
memory.Write8(timer, 0x12);
memory.Write8(timer + 1, 0x34);
memory.Write8(timer + 2, 0x56);
memory.Write8(timer + 3, 0x78);
EXPECT_EQ(memory.Read32(timer), 0x12345678u);
EXPECT_EQ(memory.GetCycles(), 1280u);
memory.AdvanceCycles(128);
EXPECT_EQ(memory.Read32(timer), 0x12345679u);
}
TEST(StarletTimer, AlarmFiresAcrossCounterWrap)
{
constexpr u32 timer = 0x0d800010;
constexpr u32 alarm = 0x0d800014;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
memory.Write32(timer, 0xfffffffe);
memory.Write32(alarm, 1);
memory.AdvanceCycles(3 * 128 - 1);
EXPECT_EQ(memory.Read32(timer), 0u);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, 0u);
memory.AdvanceCycles(1);
EXPECT_EQ(memory.Read32(timer), 1u);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
}
TEST(StarletTimer, ResetClearsCounterOffset)
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
memory.AdvanceCycles(512);
memory.Write32(0x0d800010, 0x12345678);
memory.Reset();
EXPECT_EQ(memory.GetCycles(), 0u);
EXPECT_EQ(memory.Read32(0x0d800010), 0u);
memory.AdvanceCycles(128);
EXPECT_EQ(memory.Read32(0x0d800010), 1u);
}
TEST(StarletNAND, HardwareResetPreservesProgrammedFlash)
{
Core::DeclareAsCPUThread();
@@ -426,6 +578,36 @@ TEST(StarletNAND, HardwareResetPreservesProgrammedFlash)
EXPECT_EQ(memory.Read32(sram), 0x12345678u);
}
TEST(StarletTimer, StateRoundTripPreservesOffsetAndDividerPhase)
{
constexpr u32 timer = 0x0d800010;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
StarletMemory memory(system);
memory.Reset();
memory.AdvanceCycles(1025);
memory.Write32(timer, 0x12345678);
std::vector<u8> state_buffer(1024 * 1024);
u8* state_pointer = state_buffer.data();
PointerWrap writer(&state_pointer, state_buffer.size(), PointerWrap::Mode::Write);
memory.DoState(writer);
ASSERT_TRUE(writer.IsWriteMode());
const size_t state_size = state_pointer - state_buffer.data();
memory.Reset();
state_pointer = state_buffer.data();
PointerWrap reader(&state_pointer, state_size, PointerWrap::Mode::Read);
memory.DoState(reader);
ASSERT_TRUE(reader.IsReadMode());
EXPECT_EQ(memory.GetCycles(), 1025u);
EXPECT_EQ(memory.Read32(timer), 0x12345678u);
memory.AdvanceCycles(126);
EXPECT_EQ(memory.Read32(timer), 0x12345678u);
memory.AdvanceCycles(1);
EXPECT_EQ(memory.Read32(timer), 0x12345679u);
}
TEST(StarletGPIO, InterruptFlagIsWriteOneToClear)
{
constexpr u32 hardware_base = 0x0d800000;
@@ -984,6 +1166,35 @@ TEST(StarletARMCore, JitCompilesDrainWriteBufferNatively)
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 1u);
}
TEST(StarletARMCore, JitCompilesHotCP15MaintenanceNatively)
{
#if defined(_M_X86_64)
TestBus interpreter_bus;
TestBus jit_bus;
ARMCore interpreter(interpreter_bus);
ARMCore jit(jit_bus);
jit.SetJitEnabled(true);
const auto install_program = [](TestBus& bus) {
bus.WriteARM(0x00, 0xee033f10); // mcr p15, 0, r3, c3, c0, 0 (DACR)
bus.WriteARM(0x04, 0xee070f36); // mcr p15, 0, r0, c7, c6, 1
bus.WriteARM(0x08, 0xee070f3a); // mcr p15, 0, r0, c7, c10, 1
bus.WriteARM(0x0c, 0xeafffffe); // b .
};
install_program(interpreter_bus);
install_program(jit_bus);
interpreter.SetRegister(3, 0x55555555);
jit.SetRegister(3, 0x55555555);
ASSERT_EQ(interpreter.RunCycles(4), 4u);
ASSERT_EQ(jit.RunCycles(4), 4u);
EXPECT_EQ(jit.GetCP15State().domain_access_control,
interpreter.GetCP15State().domain_access_control);
EXPECT_EQ(jit.GetRegister(15), interpreter.GetRegister(15));
EXPECT_EQ(jit.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(jit.GetJitNativeExecutedInstructions(), 4u);
#endif
}
TEST(StarletARMCore, JitDefersCP15CacheInvalidationUntilTheHostBlockReturns)
{
TestBus bus;
@@ -1041,18 +1252,324 @@ TEST(StarletARMCore, JitPreservedBlocksUseCurrentTLBGenerationForFastmem)
EXPECT_EQ(core.RunCycles(2), 2u);
ASSERT_EQ(core.GetRegister(1), 0x11223344u);
ASSERT_EQ(core.GetJitFallbackInstructionCount(), 1u);
ASSERT_EQ(core.GetJitFallbackInstructionCount(), 0u);
ASSERT_EQ(core.GetJitCompiledBlockCount(), 1u);
core.SetRegister(1, 0);
core.SetRegister(15, 0x80000000);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(1), 0x11223344u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 1u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(core.GetJitCompiledBlockCount(), 1u);
#endif
}
TEST(StarletARMCore, JitTLBRevalidationIsSharedByNativeBlocksOnTheSamePage)
{
#if defined(_M_X86_64)
TestBus bus(0x10000);
ARMCore core(bus);
bus.WriteARM(0x0000, 0xe3a01001); // mov r1, #1
bus.WriteARM(0x0020, 0xe3a02002); // mov r2, #2
bus.WriteARM(0x0040, 0xee080f17); // invalidate unified TLB
bus.WriteARM(0x6000, 0x00000c02); // VA 0x80000000 section -> PA 0
core.GetCP15State().translation_table_base = 0x4000;
core.GetCP15State().domain_access_control = 3;
core.GetCP15State().control |= 1;
core.SetJitEnabled(true);
core.SetRegister(15, 0x80000000);
EXPECT_EQ(core.RunCycles(1), 1u);
core.SetRegister(15, 0x80000020);
EXPECT_EQ(core.RunCycles(1), 1u);
core.SetRegister(15, 0x80000040);
EXPECT_EQ(core.RunCycles(1), 1u);
const u64 dispatches_before_revalidation = core.GetJitDispatchSlowCount();
core.SetRegister(15, 0x80000000);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetJitDispatchSlowCount(), dispatches_before_revalidation);
// The first block revalidated the unchanged page-table descriptor directly in generated code.
// A second native block on that physical page must likewise avoid a page-table walk and C++
// block-map lookup.
core.SetRegister(15, 0x80000020);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetJitDispatchSlowCount(), dispatches_before_revalidation);
#endif
}
TEST(StarletARMCore, JitSharedTLBRefillRejectsRemappedPhysicalCode)
{
#if defined(_M_X86_64)
TestBus bus(0x110000);
ARMCore core(bus);
bus.WriteARM(0x000000, 0xe3a01001); // old page: mov r1, #1
bus.WriteARM(0x000020, 0xe3a02002); // old page: mov r2, #2
bus.WriteARM(0x000040, 0xee080f17); // invalidate unified TLB
bus.WriteARM(0x100000, 0xe3a01003); // new page: mov r1, #3
bus.WriteARM(0x100020, 0xe3a02004); // new page: mov r2, #4
bus.WriteARM(0x006000, 0x00000c02); // VA 0x80000000 section -> PA 0
core.GetCP15State().translation_table_base = 0x4000;
core.GetCP15State().domain_access_control = 3;
core.GetCP15State().control |= 1;
core.SetJitEnabled(true);
core.SetRegister(15, 0x80000000);
EXPECT_EQ(core.RunCycles(1), 1u);
core.SetRegister(15, 0x80000020);
EXPECT_EQ(core.RunCycles(1), 1u);
const size_t old_block_count = core.GetJitCompiledBlockCount();
// Change the page table under the still-valid TLB, then execute the architectural invalidation
// through the old mapping. The following dispatch must discover the new physical page.
bus.WriteARM(0x006000, 0x00100c02);
core.SetRegister(15, 0x80000040);
EXPECT_EQ(core.RunCycles(1), 1u);
core.SetRegister(15, 0x80000000);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(1), 3u);
core.SetRegister(15, 0x80000020);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(2), 4u);
EXPECT_EQ(core.GetJitCompiledBlockCount(), old_block_count + 3);
#endif
}
TEST(StarletARMCore, JitRetainsPhysicalAliasesAcrossAddressSpaceSwitches)
{
#if defined(_M_X86_64)
TestBus bus(0x110000);
ARMCore core(bus);
bus.WriteARM(0x000000, 0xe3a01001); // physical mapping 0: mov r1, #1
bus.WriteARM(0x000020, 0xe3a02002); // physical mapping 0: mov r2, #2
bus.WriteARM(0x000040, 0xee080f17); // invalidate unified TLB
bus.WriteARM(0x100000, 0xe3a01003); // physical mapping 1: mov r1, #3
bus.WriteARM(0x100020, 0xe3a02004); // physical mapping 1: mov r2, #4
bus.WriteARM(0x100040, 0xee080f17); // invalidate unified TLB
bus.WriteARM(0x006000, 0x00000c02); // VA 0x80000000 section -> PA 0
core.GetCP15State().translation_table_base = 0x4000;
core.GetCP15State().domain_access_control = 3;
core.GetCP15State().control |= 1;
core.SetJitEnabled(true);
for (const u32 address : {0x80000000U, 0x80000020U})
{
core.SetRegister(15, address);
EXPECT_EQ(core.RunCycles(1), 1u);
}
bus.WriteARM(0x006000, 0x00100c02); // Same virtual section -> PA 1 MiB.
core.SetRegister(15, 0x80000040);
EXPECT_EQ(core.RunCycles(1), 1u);
for (const u32 address : {0x80000000U, 0x80000020U})
{
core.SetRegister(15, address);
EXPECT_EQ(core.RunCycles(1), 1u);
}
// Return to the first address space. The first block refills the shared page translation; the
// second must immediately find its retained (MVA, physical page) entry instead of overwriting a
// single virtual-key slot and falling back to C++ again.
bus.WriteARM(0x006000, 0x00000c02);
core.SetRegister(15, 0x80000040);
EXPECT_EQ(core.RunCycles(1), 1u);
const u64 dispatches_before_refill = core.GetJitDispatchSlowCount();
core.SetRegister(15, 0x80000000);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(1), 1u);
core.SetRegister(15, 0x80000020);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(2), 2u);
EXPECT_EQ(core.GetJitDispatchSlowCount(), dispatches_before_refill + 1);
#endif
}
TEST(StarletARMCore, JitFastBlockCacheRetainsFourCollidingHotBlocks)
{
#if defined(_M_X86_64)
TestBus bus(0xd0000);
ARMCore core(bus);
core.SetJitEnabled(true);
// These ARM addresses deliberately have the same upper 16 bits after multiplying by the JIT
// cache's 0x9e3779b1 hash constant. They therefore occupy the four ways of one cache set.
constexpr std::array<u32, 5> addresses = {0x000014, 0x04cb94, 0x07e168, 0x099714, 0x0cace8};
for (const u32 address : addresses)
bus.WriteARM(address, 0xe3a01001); // mov r1, #1
for (size_t i = 0; i < 4; ++i)
{
const u32 address = addresses[i];
core.SetRegister(15, address);
EXPECT_EQ(core.RunCycles(1), 1u);
}
const u64 dispatches_after_fill = core.GetJitDispatchSlowCount();
const u64 collisions_after_fill = core.GetJitDispatchCollisionCount();
for (size_t i = 0; i < 4; ++i)
{
const u32 address = addresses[i];
core.SetRegister(15, address);
EXPECT_EQ(core.RunCycles(1), 1u);
}
EXPECT_EQ(core.GetJitDispatchSlowCount(), dispatches_after_fill);
EXPECT_EQ(core.GetJitDispatchCollisionCount(), collisions_after_fill);
// A fifth distinct key proves that the set is actually full and exercises bounded replacement.
core.SetRegister(15, addresses.back());
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetJitDispatchSlowCount(), dispatches_after_fill + 1);
EXPECT_EQ(core.GetJitDispatchCollisionCount(), collisions_after_fill + 1);
#endif
}
TEST(StarletARMCore, JitCachesFallbackOnlyBlocks)
{
#if defined(_M_X86_64)
TestBus bus(0x10000);
ARMCore core(bus);
// MUL uses the exact interpreter helper in this JIT. A block beginning with it therefore has
// zero directly emitted ARM instructions, but its generated fallback wrapper is still reusable.
bus.WriteARM(0x0000, 0xe0010190); // mul r1, r0, r1
core.SetJitEnabled(true);
core.SetRegister(0, 3);
core.SetRegister(1, 4);
core.SetRegister(15, 0);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(1), 12u);
const u64 dispatches_after_compile = core.GetJitDispatchSlowCount();
const u64 fallbacks_after_compile = core.GetJitFallbackInstructionCount();
core.SetRegister(1, 5);
core.SetRegister(15, 0);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(1), 15u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), fallbacks_after_compile + 1);
EXPECT_EQ(core.GetJitDispatchSlowCount(), dispatches_after_compile);
#endif
}
TEST(StarletARMCore, ARMJitCompilesLogicalImmediateAndShiftCarry)
{
#if defined(_M_X86_64)
TestBus interpreter_bus(0x1000);
TestBus jit_bus(0x1000);
ARMCore interpreter(interpreter_bus);
ARMCore jit(jit_bus);
jit.SetJitEnabled(true);
const auto install_program = [](TestBus& bus) {
bus.WriteARM(0x00, 0xe3180701); // tst r8, #0x40000; rotated immediate supplies C
bus.WriteARM(0x04, 0xeafffffe); // b .
bus.WriteARM(0x20, 0xe1b02820); // movs r2, r0, lsr #16; bit 15 supplies C
bus.WriteARM(0x24, 0xeafffffe); // b .
};
install_program(interpreter_bus);
install_program(jit_bus);
for (ARMCore* core : {&interpreter, &jit})
{
core->SetCPSR(static_cast<u32>(ARMCore::Mode::System) | ARMCore::CPSR_C | ARMCore::CPSR_V);
core->SetRegister(8, 0x40000);
core->SetRegister(15, 0);
}
ASSERT_EQ(interpreter.RunCycles(2), 2u);
ASSERT_EQ(jit.RunCycles(2), 2u);
EXPECT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
for (ARMCore* core : {&interpreter, &jit})
{
core->SetCPSR(static_cast<u32>(ARMCore::Mode::System) | ARMCore::CPSR_V);
core->SetRegister(0, 0x80018000);
core->SetRegister(2, 0);
core->SetRegister(15, 0x20);
}
ASSERT_EQ(interpreter.RunCycles(2), 2u);
ASSERT_EQ(jit.RunCycles(2), 2u);
EXPECT_EQ(jit.GetRegister(2), interpreter.GetRegister(2));
EXPECT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
EXPECT_EQ(jit.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(jit.GetJitNativeExecutedInstructions(), 4u);
#endif
}
TEST(StarletARMCore, ARMJitCompilesIRQVectorLoadPCWithInterworking)
{
#if defined(_M_X86_64)
TestBus bus;
ARMCore core(bus);
bus.SetFastmemEnabled(true);
bus.WriteARM(0x00, 0xe59ff018); // ldr pc, [pc, #0x18] -> 0x20
bus.WriteARM(0x20, 0x00000101); // enter Thumb at 0x100
core.SetRegister(15, 0);
core.SetJitEnabled(true);
EXPECT_EQ(core.RunCycles(1), 1u);
EXPECT_EQ(core.GetRegister(15), 0x100u);
EXPECT_NE(core.GetCPSR() & ARMCore::CPSR_T, 0u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(core.GetJitSlowReadCount(), 0u);
EXPECT_EQ(core.GetJitNativeExecutedInstructions(), 1u);
EXPECT_TRUE(bus.SRAMCanariesIntact());
#endif
}
TEST(StarletARMCore, ARMJitCompilesLongMultiplyFamily)
{
#if defined(_M_X86_64)
struct Case
{
u32 instruction;
u32 cpsr;
u32 rm;
u32 rs;
u32 rd_hi;
u32 rd_lo;
};
constexpr std::array cases = {
Case{0xe0834291, static_cast<u32>(ARMCore::Mode::System) | ARMCore::CPSR_C, 0x10000, 0x10001,
0, 0}, // UMULL
Case{0xe0c34291, static_cast<u32>(ARMCore::Mode::System) | ARMCore::CPSR_V, 0xfffffff0, 0x10,
0, 0}, // SMULL
Case{0xe0b34291, static_cast<u32>(ARMCore::Mode::System) | ARMCore::CPSR_C | ARMCore::CPSR_V,
0xffffffff, 1, 0, 1}, // UMLALS, result wraps to zero and preserves CV
Case{0x10834291, static_cast<u32>(ARMCore::Mode::System) | ARMCore::CPSR_Z, 7, 9, 0x11223344,
0x55667788}, // UMULLNE, predicate fails
};
for (const Case& test : cases)
{
TestBus interpreter_bus;
TestBus jit_bus;
ARMCore interpreter(interpreter_bus);
ARMCore jit(jit_bus);
interpreter_bus.WriteARM(0, test.instruction);
interpreter_bus.WriteARM(4, 0xeafffffe); // b .
jit_bus.WriteARM(0, test.instruction);
jit_bus.WriteARM(4, 0xeafffffe); // b .
for (ARMCore* core : {&interpreter, &jit})
{
core->SetCPSR(test.cpsr);
core->SetRegister(1, test.rm);
core->SetRegister(2, test.rs);
core->SetRegister(3, test.rd_hi);
core->SetRegister(4, test.rd_lo);
core->SetRegister(15, 0);
}
jit.SetJitEnabled(true);
ASSERT_EQ(interpreter.RunCycles(2), 2u);
ASSERT_EQ(jit.RunCycles(2), 2u);
EXPECT_EQ(jit.GetRegister(3), interpreter.GetRegister(3));
EXPECT_EQ(jit.GetRegister(4), interpreter.GetRegister(4));
EXPECT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
EXPECT_EQ(jit.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(jit.GetJitNativeExecutedInstructions(), 2u);
}
#endif
}
TEST(StarletARMCore, FCSESwitchPreservesTaggedTLBTranslations)
{
TestBus bus(0x10000);
@@ -0,0 +1,316 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <memory>
#include "Common/x64ABI.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/Memmap.h"
#include "Core/PowerPC/Jit64/Jit.h"
#include "Core/PowerPC/Jit64Common/Jit64AsmCommon.h"
#include "Core/PowerPC/Jit64Common/Jit64Constants.h"
#include "Core/PowerPC/MMU.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include <gtest/gtest.h>
namespace
{
using namespace Gen;
// Execute the real SafeLoad/SafeWrite emitter, including its fallback and register contract.
// Testing only the C++ MMU helpers cannot detect corruption introduced at the native call site.
class DCacheCode : public CommonAsmRoutines
{
public:
explicit DCacheCode(Core::System& system) : CommonAsmRoutines(jit), jit(system)
{
jit.jo = {};
jit.js = {};
AllocCodeSpace(512 * 1024);
old_read = dcache32_read_hit_dbat;
old_write = dcache32_write_hit_dbat;
dcache32_read_hit_dbat = AlignCode4();
GenDCache32Hit(false);
dcache32_write_hit_dbat = AlignCode4();
GenDCache32Hit(true);
}
~DCacheCode() override
{
dcache32_read_hit_dbat = old_read;
dcache32_write_hit_dbat = old_write;
}
void Access(bool write, u32 address, u32 value, X64Reg address_reg, X64Reg value_reg,
s32 offset = 0, int flags = 0)
{
run = reinterpret_cast<void (*)()>(AlignCode4());
ABI_PushRegistersAndAdjustStack(ABI_ALL_CALLEE_SAVED, 8);
MOV(64, R(RPPCSTATE), ImmPtr(reinterpret_cast<u8*>(&jit.m_ppc_state) + 0x80));
MOV(32, R(RDX), Imm32(0x12345678));
MOV(32, R(RCX), Imm32(0x87654321));
MOV(32, R(address_reg), Imm32(address));
if (write)
MOV(32, R(value_reg), Imm32(value));
const BitSet32 live{RCX, RDX, R8, R9};
if (flags & SAFE_LOADSTORE_NO_PROLOG)
SUB(64, R(RSP), Imm8(8));
if (write)
SafeWriteRegToReg(value_reg, address_reg, 32, offset, live, flags);
else
SafeLoadToReg(value_reg, R(address_reg), 32, offset, live, false, flags);
if (flags & SAFE_LOADSTORE_NO_PROLOG)
ADD(64, R(RSP), Imm8(8));
MOV(64, R(R11), ImmPtr(result.data()));
for (const auto reg : {RAX, RCX, RDX, R8, R9})
MOV(64, MDisp(R11, static_cast<int>(reg) * sizeof(u64)), R(reg));
ABI_PopRegistersAndAdjustStack(ABI_ALL_CALLEE_SAVED, 8);
RET();
ASSERT_FALSE(HasWriteFailed());
run();
}
std::array<u64, 16> result{};
void (*run)() = nullptr;
Jit64 jit;
const u8* old_read;
const u8* old_write;
};
class Jit64DCache : public testing::Test
{
protected:
static void SetUpTestSuite()
{
SConfig::Init();
auto& system = Core::System::GetInstance();
system.SetIsWii(true);
system.GetMemory().Init();
Core::DeclareAsCPUThread();
system.GetPPCState().dCache.Init(system.GetMemory());
}
static void TearDownTestSuite()
{
auto& system = Core::System::GetInstance();
system.GetPPCState().m_enable_dcache = false;
system.GetMemory().Shutdown();
system.SetIsWii(false);
Core::UndeclareAsCPUThread();
SConfig::Shutdown();
}
void SetUp() override
{
state.dCache.Reset();
state.m_enable_dcache = true;
state.msr.DR = 1;
state.feature_flags = FEATURE_FLAG_MSR_DR;
state.Exceptions = 0;
state.spr[SPR_HID0] = 0;
bats.fill(0);
bats[0x80000000 >> PowerPC::BAT_INDEX_SHIFT] = PowerPC::BAT_MAPPED_BIT;
bats[0x90000000 >> PowerPC::BAT_INDEX_SHIFT] = 0x10000000 | PowerPC::BAT_MAPPED_BIT;
code = std::make_unique<DCacheCode>(system);
}
void TearDown() override { code.reset(); }
Core::System& system = Core::System::GetInstance();
PowerPC::PowerPCState& state = system.GetPPCState();
PowerPC::MMU& mmu = system.GetMMU();
Memory::MemoryManager& memory = system.GetMemory();
PowerPC::BatTable& bats = const_cast<PowerPC::BatTable&>(mmu.GetDBATTable());
std::unique_ptr<DCacheCode> code;
};
TEST_F(Jit64DCache, StoreMissRetainsAddressInRDX)
{
// The data is deliberately also a mapped address: a broken fallback writes there instead.
memory.Write_U32(0, 0x1000);
memory.Write_U32(0, 0x4000);
code->Access(true, 0x80001000, 0x80004000, RDX, RCX);
EXPECT_EQ(mmu.ReadForJit<u32>(0x80001000), 0x80004000);
EXPECT_EQ(mmu.ReadForJit<u32>(0x80004000), 0U);
EXPECT_EQ(code->result[RDX], 0x80001000);
EXPECT_EQ(code->result[RCX], 0x80004000);
}
TEST_F(Jit64DCache, LoadMissRetainsAddressInRDX)
{
memory.Write_U32(0x89abcdef, 0x1000);
code->Access(false, 0x80001000, 0, RDX, R8);
EXPECT_EQ(code->result[R8], 0x89abcdef);
EXPECT_EQ(code->result[RDX], 0x80001000);
}
TEST_F(Jit64DCache, HitPreservesLiveScratchRegisters)
{
mmu.WriteForJit<u32>(0xffffffff, 0x80001000);
code->Access(false, 0x80001000, 0, R9, R8);
EXPECT_EQ(code->result[R8], 0xffffffff);
EXPECT_EQ(code->result[RDX], 0x12345678U);
EXPECT_EQ(code->result[RCX], 0x87654321U);
code->Access(true, 0x80001000, 0x11223344, R9, R8);
EXPECT_EQ(mmu.ReadForJit<u32>(0x80001000), 0x11223344U);
EXPECT_EQ(code->result[RDX], 0x12345678U);
EXPECT_EQ(code->result[RCX], 0x87654321U);
}
TEST_F(Jit64DCache, PhysicalAccessIgnoresDBAT)
{
state.msr.DR = 0;
state.feature_flags = CPUEmuFeatureFlags{};
bats[0] = 0x20000 | PowerPC::BAT_MAPPED_BIT;
mmu.WriteForJit<u32>(0x11111111, 0x1000);
mmu.WriteForJit<u32>(0x22222222, 0x21000);
code->Access(false, 0x1000, 0, R9, R8);
EXPECT_EQ(code->result[R8], 0x11111111U);
code->Access(true, 0x1000, 0x33333333, R9, R8);
EXPECT_EQ(mmu.ReadForJit<u32>(0x1000), 0x33333333U);
EXPECT_EQ(mmu.ReadForJit<u32>(0x21000), 0x22222222U);
}
TEST_F(Jit64DCache, LoadRegisterAndOffsetCombinations)
{
for (const bool hit : {false, true})
{
for (const auto address_reg : {RAX, RCX, RDX, R8, R9})
{
for (const auto value_reg : {RAX, RCX, RDX, R8, R9})
{
for (const s32 offset : {0, 4, -4})
{
SCOPED_TRACE(testing::Message()
<< hit << ' ' << address_reg << ' ' << value_reg << ' ' << offset);
state.dCache.Reset();
memory.Write_U32(0xfedcba98, 0x1020);
if (hit)
ASSERT_EQ(mmu.ReadForJit<u32>(0x80001020), 0xfedcba98);
code->Access(false, 0x80001020 - offset, 0, address_reg, value_reg, offset);
EXPECT_EQ(code->result[value_reg], 0xfedcba98);
}
}
}
}
}
TEST_F(Jit64DCache, StoreRegisterAndOffsetCombinations)
{
for (const bool hit : {false, true})
{
for (const auto address_reg : {RAX, RCX, RDX, R8, R9})
{
for (const auto value_reg : {RCX, RDX, R8, R9})
{
if (address_reg == value_reg)
continue;
for (const s32 offset : {0, 4, -4})
{
SCOPED_TRACE(testing::Message()
<< hit << ' ' << address_reg << ' ' << value_reg << ' ' << offset);
state.dCache.Reset();
memory.Write_U32(0, 0x1020);
if (hit)
ASSERT_EQ(mmu.ReadForJit<u32>(0x80001020), 0U);
code->Access(true, 0x80001020 - offset, 0xfedcba98, address_reg, value_reg, offset,
EmuCodeBlock::SAFE_LOADSTORE_CLOBBER_RSCRATCH_INSTEAD_OF_ADDR);
EXPECT_EQ(mmu.ReadForJit<u32>(0x80001020), 0xfedcba98);
EXPECT_EQ(code->result[value_reg], 0xfedcba98);
}
}
}
}
}
TEST_F(Jit64DCache, SplitLineAndPageAccess)
{
for (const u32 offset : {29U, 30U, 31U, 4093U, 4094U, 4095U})
{
SCOPED_TRACE(offset);
const u32 address = 0x80001000 + offset;
mmu.WriteForJit<u32>(0x11223344, address);
code->Access(false, address, 0, RDX, R8);
EXPECT_EQ(code->result[R8], 0x11223344U);
code->Access(true, address, 0x55667788, RDX, RCX);
EXPECT_EQ(mmu.ReadForJit<u32>(address), 0x55667788U);
}
}
TEST_F(Jit64DCache, InhibitedBATBypassesCachedData)
{
mmu.WriteForJit<u32>(0x11111111, 0x80001000);
memory.Write_U32(0x22222222, 0x1000);
bats[0x80000000 >> PowerPC::BAT_INDEX_SHIFT] |= PowerPC::BAT_WI_BIT;
code->Access(false, 0x80001000, 0, RDX, R8);
EXPECT_EQ(code->result[R8], 0x22222222U);
code->Access(true, 0x80001000, 0x33333333, RDX, RCX);
EXPECT_EQ(memory.Read_U32(0x1000), 0x33333333U);
bats[0x80000000 >> PowerPC::BAT_INDEX_SHIFT] &= ~PowerPC::BAT_WI_BIT;
EXPECT_EQ(mmu.ReadForJit<u32>(0x80001000), 0x11111111U);
}
TEST_F(Jit64DCache, SharedRoutineStackAndDRFlag)
{
// Shared paired-load routines are emitted without a known block's feature flags and run
// with a return address on the stack. DR_ON is their explicit translation contract.
state.feature_flags = CPUEmuFeatureFlags{};
constexpr int flags = EmuCodeBlock::SAFE_LOADSTORE_NO_PROLOG |
EmuCodeBlock::SAFE_LOADSTORE_NO_UPDATE_PC |
EmuCodeBlock::SAFE_LOADSTORE_DR_ON;
memory.Write_U32(0x89abcdef, 0x1000);
for (int repeat = 0; repeat < 2; ++repeat)
{
code->Access(false, 0x80001000, 0, RDX, R8, 0, flags);
EXPECT_EQ(code->result[R8], 0x89abcdef);
code->Access(true, 0x80001000, 0x89abcdef, RDX, RCX, 0, flags);
EXPECT_EQ(mmu.ReadForJit<u32>(0x80001000), 0x89abcdef);
}
}
TEST_F(Jit64DCache, NativePLRUAndDirtyStateMatchMMU)
{
for (const u32 base : {0x80000000U, 0x90000000U})
{
state.dCache.Reset();
constexpr u32 set = 126;
for (u32 way = 0; way < PowerPC::CACHE_WAYS; ++way)
mmu.WriteForJit<u32>(0xffffffff, base + way * 4096 + set * 32);
for (u32 way = 0; way < PowerPC::CACHE_WAYS; ++way)
{
const u32 address = base + way * 4096 + set * 32;
for (const bool write : {false, true})
{
code->Access(write, address, 0xffffffff, RDX, R8);
for (u32 old_plru = 0; old_plru < 128; ++old_plru)
{
SCOPED_TRACE(testing::Message() << base << ' ' << way << ' ' << write << ' ' << old_plru);
auto& cache = state.dCache;
cache.plru[set] = static_cast<u8>(old_plru);
cache.modified[set] = 0x80;
if (write)
ASSERT_TRUE(mmu.TryWriteDCache32ForJit(0xffffffff, address));
else
ASSERT_EQ(mmu.TryReadDCache32ForJit(address), 0x100000000ULL);
const auto expected_plru = cache.plru[set];
const auto expected_dirty = cache.modified[set];
const auto expected_data = cache.data[set];
cache.plru[set] = static_cast<u8>(old_plru);
cache.modified[set] = 0x80;
code->run();
EXPECT_EQ(cache.plru[set], expected_plru);
EXPECT_EQ(cache.modified[set], expected_dirty);
EXPECT_EQ(cache.data[set], expected_data);
if (!write)
EXPECT_EQ(code->result[R8], 0xffffffff);
}
}
}
}
}
} // namespace