IOS LLE: checkpoint NAND conformance and diagnostic tooling

Preserve formatted NAND boot and Wii Remote pairing, add DMA/SRAM conformance tests and a Wii comparison probe. Keep P1 profiling tools and native update A/B evidence; reject the unhelpful targeted SRAM shortcut.
This commit is contained in:
Yaya48 committed 2026-09-23 13:30:17 +02:00
1 parent b931671fde
commit c230971f07
64 files changed
+6488 -238

No files matched your search

+2
View File
@@ -122,6 +122,8 @@ add_library(common
OneShotEvent.h
PcapFile.cpp
PcapFile.h
PerformanceTrace.cpp
PerformanceTrace.h
Profiler.cpp
Profiler.h
Projection.h
+9 -1
View File
@@ -9,6 +9,7 @@
#include "Common/Assert.h"
#include "Common/CommonTypes.h"
#include "Common/MemoryUtil.h"
#include "Common/PerformanceTrace.h"
namespace Common
{
@@ -72,6 +73,8 @@ public:
void FreeCodeSpace()
{
ASSERT(!m_is_child);
if (region)
PerformanceTrace::CodeReset("Generated", this, region, region + total_region_size);
Common::FreeMemoryPages(region, total_region_size);
region = nullptr;
region_size = 0;
@@ -97,7 +100,12 @@ public:
{
Common::UnWriteProtectMemory(region, region_size, allow_execute);
}
void ResetCodePtr() { T::SetCodePtr(region, region + region_size); }
void ResetCodePtr()
{
if (region)
PerformanceTrace::CodeReset("Generated", this, region, region + region_size);
T::SetCodePtr(region, region + region_size);
}
size_t GetSpaceLeft() const
{
ASSERT(static_cast<size_t>(T::GetCodePtr() - region) < region_size);
+17
View File
@@ -11,6 +11,7 @@
#include "Common/CommonTypes.h"
#include "Common/IOFile.h"
#include "Common/PerformanceTrace.h"
#ifdef _WIN32
#include <process.h>
@@ -66,8 +67,24 @@ bool IsEnabled()
return s_is_enabled;
}
bool IsTraceEnabled()
{
return PerformanceTrace::Enabled();
}
void Register(const void* base_address, u32 code_size, const std::string& symbol_name)
{
// Block mappings (including invalidation) are emitted by JitCache itself.
if (!symbol_name.starts_with("JIT_PPC_"))
{
// Vertex decoding also uses JitRegister. Do not attribute it to the guest CPU.
const char* domain = symbol_name.starts_with("JIT_") ? "PPC" :
symbol_name.starts_with("VertexLoader") ? "VertexDecode" :
"OtherGenerated";
PerformanceTrace::CodeMap(domain, nullptr, base_address,
static_cast<const u8*>(base_address) + code_size, 0, 0,
symbol_name.c_str());
}
#ifdef USE_VTUNE
iJIT_Method_Load jmethod = {0};
jmethod.method_id = iJIT_GetNewMethodID();
+4 -2
View File
@@ -15,12 +15,14 @@ void Init(const std::string& perf_dir);
void Shutdown();
void Register(const void* base_address, u32 code_size, const std::string& symbol_name);
bool IsEnabled();
// ETW mappings can be requested without enabling perf-map/VTune registration.
bool IsTraceEnabled();
template <typename... Args>
inline void Register(const void* base_address, u32 code_size, fmt::format_string<Args...> format,
Args&&... args)
{
if (!IsEnabled())
if (!IsEnabled() && !IsTraceEnabled())
return;
Register(base_address, code_size, fmt::format(format, std::forward<Args>(args)...));
@@ -30,7 +32,7 @@ template <typename... Args>
inline void Register(const void* start, const void* end, fmt::format_string<Args...> format,
Args&&... args)
{
if (!IsEnabled())
if (!IsEnabled() && !IsTraceEnabled())
return;
u32 code_size = (u32)((const char*)end - (const char*)start);
+294
View File
@@ -0,0 +1,294 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Common/PerformanceTrace.h"
#include <atomic>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#include <Windows.h>
#include <TraceLoggingProvider.h>
#pragma comment(lib, "advapi32.lib")
TRACELOGGING_DEFINE_PROVIDER(s_dolphin_perf, "Dolphin.Performance",
(0x54ee9231, 0x28be, 0x4ab0, 0x98, 0x1e, 0xb2, 0x6f, 0x97, 0x9c, 0x56,
0xa4));
#endif
namespace Common::PerformanceTrace
{
namespace
{
constexpr u32 SCHEMA = 1;
constexpr u32 SAMPLE_MASK = 1023;
struct ThreadState
{
bool sampling = false;
u32 rng = 0x9e3779b9;
u64 roots = 0;
u64 selected = 0;
u64 last_snapshot = 0;
ScopeStack stack;
std::array<u64, static_cast<size_t>(Counter::Count)> counters{};
};
thread_local ThreadState s_thread;
#ifdef _WIN32
struct Registration
{
Registration() : status(TraceLoggingRegister(s_dolphin_perf)) {}
~Registration()
{
if (status == ERROR_SUCCESS)
TraceLoggingUnregister(s_dolphin_perf);
}
ULONG status;
};
#endif
} // namespace
bool ScopeStack::Begin(Region region, u64 now)
{
if (m_depth == m_stack.size() || static_cast<size_t>(region) >= m_timings.size())
{
++m_errors;
return false;
}
m_stack[m_depth++] = {region, now, 0};
return true;
}
bool ScopeStack::End(u64 now)
{
if (m_depth == 0)
{
++m_errors;
return false;
}
const Entry entry = m_stack[--m_depth];
if (now < entry.start || now - entry.start < entry.children)
{
++m_errors;
return false;
}
const u64 elapsed = now - entry.start;
auto& timing = m_timings[static_cast<size_t>(entry.region)];
++timing.calls;
timing.inclusive += elapsed;
timing.exclusive += elapsed - entry.children;
if (m_depth != 0)
m_stack[m_depth - 1].children += elapsed;
return true;
}
void ScopeStack::Reset()
{
m_depth = 0;
m_errors = 0;
m_timings = {};
}
bool Requested()
{
#ifdef _WIN32
static const bool requested = [] {
const char* value = std::getenv("DOLPHIN_PERF_TRACE");
return value && std::strcmp(value, "1") == 0;
}();
return requested;
#else
return false;
#endif
}
bool Enabled()
{
#ifdef _WIN32
if (!Requested())
return false;
static Registration registration;
return registration.status == ERROR_SUCCESS && TraceLoggingProviderEnabled(s_dolphin_perf, 0, 0);
#else
return false;
#endif
}
u64 Now()
{
#ifdef _WIN32
LARGE_INTEGER value;
QueryPerformanceCounter(&value);
return static_cast<u64>(value.QuadPart);
#else
return 0;
#endif
}
u64 Frequency()
{
#ifdef _WIN32
static const u64 frequency = [] {
LARGE_INTEGER value;
QueryPerformanceFrequency(&value);
return static_cast<u64>(value.QuadPart);
}();
return frequency;
#else
return 0;
#endif
}
void Add(Counter counter, u64 value)
{
if (Requested() && static_cast<size_t>(counter) < s_thread.counters.size())
s_thread.counters[static_cast<size_t>(counter)] += value;
}
u64 NewGeneration()
{
static std::atomic<u64> generation{0};
return Requested() ? generation.fetch_add(1, std::memory_order_relaxed) + 1 : 0;
}
SampleRoot::SampleRoot(bool force_for_test)
{
m_denominator = force_for_test ? 1 : SAMPLE_MASK + 1;
if (!Enabled() || s_thread.sampling)
return;
++s_thread.roots;
auto& x = s_thread.rng;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
if (!force_for_test && (x & SAMPLE_MASK) != 0)
return;
++s_thread.selected;
s_thread.stack.Reset();
m_start = Now();
m_active = s_thread.stack.Begin(Region::Slice, m_start);
s_thread.sampling = m_active;
}
SampleRoot::~SampleRoot()
{
if (!m_active)
return;
const u64 end = Now();
s_thread.stack.End(end);
s_thread.sampling = false;
#ifdef _WIN32
// Sampled wall times, NOT exact total CPU times. Nested regions are exclusive here.
for (size_t i = 0; i < s_thread.stack.Timings().size(); ++i)
{
const auto& timing = s_thread.stack.Timings()[i];
if (timing.calls == 0)
continue;
TraceLoggingWrite(s_dolphin_perf, "ScopeSample", TraceLoggingUInt32(SCHEMA, "Schema"),
TraceLoggingUInt64(m_start, "BeginQPC"), TraceLoggingUInt64(end, "EndQPC"),
TraceLoggingUInt64(s_thread.selected, "Sample"),
TraceLoggingUInt32(m_denominator, "SamplingDenominator"),
TraceLoggingUInt32(static_cast<u32>(i), "Region"),
TraceLoggingUInt64(timing.calls, "Calls"),
TraceLoggingUInt64(timing.inclusive, "InclusiveQPC"),
TraceLoggingUInt64(timing.exclusive, "ExclusiveQPC"),
TraceLoggingUInt64(s_thread.stack.Errors(), "AccountingErrors"));
}
#endif
}
Scope::Scope(Region region)
{
if (s_thread.sampling)
m_active = s_thread.stack.Begin(region, Now());
}
Scope::~Scope()
{
if (m_active)
s_thread.stack.End(Now());
}
void Snapshot(u64 core_id, u64 instructions, u64 native_instructions, u64 slow_reads,
u64 slow_writes, u64 compiles, u64 fallbacks, u32 pc, bool waiting)
{
if (!Enabled())
return;
const u64 now = Now();
if (now - s_thread.last_snapshot < Frequency())
return;
s_thread.last_snapshot = now;
#ifdef _WIN32
TraceLoggingWrite(
s_dolphin_perf, "Snapshot", TraceLoggingUInt32(SCHEMA, "Schema"),
TraceLoggingUInt64(now, "QPC"), TraceLoggingUInt64(Frequency(), "QPCFrequency"),
TraceLoggingUInt64(core_id, "CoreId"), TraceLoggingUInt64(instructions, "Instructions"),
TraceLoggingUInt64(native_instructions, "NativeInstructions"),
TraceLoggingUInt64(slow_reads, "SlowReads"), TraceLoggingUInt64(slow_writes, "SlowWrites"),
TraceLoggingUInt64(compiles, "Compiles"), TraceLoggingUInt64(fallbacks, "Fallbacks"),
TraceLoggingUInt32(pc, "GuestPC"), TraceLoggingBool(waiting, "Waiting"),
TraceLoggingUInt64(s_thread.roots, "Roots"),
TraceLoggingUInt64(s_thread.selected, "SelectedRoots"));
for (size_t i = 0; i < s_thread.counters.size(); ++i)
{
TraceLoggingWrite(s_dolphin_perf, "Counter", TraceLoggingUInt32(SCHEMA, "Schema"),
TraceLoggingUInt64(now, "QPC"),
TraceLoggingUInt32(static_cast<u32>(i), "Counter"),
TraceLoggingUInt64(s_thread.counters[i], "Value"));
}
#endif
}
void CodeMap(const char* domain, const void* owner, const void* begin, const void* end,
u32 guest_pc, u64 identity, const char* kind)
{
if (!begin || reinterpret_cast<uintptr_t>(end) <= reinterpret_cast<uintptr_t>(begin) ||
!Enabled())
return;
#ifdef _WIN32
TraceLoggingWrite(s_dolphin_perf, "CodeMap", TraceLoggingUInt32(SCHEMA, "Schema"),
TraceLoggingUInt64(Now(), "QPC"), TraceLoggingString(domain, "Domain"),
TraceLoggingPointer(owner, "Owner"), TraceLoggingPointer(begin, "Begin"),
TraceLoggingPointer(end, "End"), TraceLoggingUInt32(guest_pc, "GuestPC"),
TraceLoggingUInt64(identity, "Identity"), TraceLoggingString(kind, "Kind"));
#endif
}
void CodeReset(const char* domain, const void* owner, const void* begin, const void* end)
{
CodeMap(domain, owner, begin, end, 0, 0, "invalidate");
}
void Frame(const char* kind, s64 guest_ticks, u32 guest_frequency)
{
if (!Enabled())
return;
#ifdef _WIN32
TraceLoggingWrite(s_dolphin_perf, "Frame", TraceLoggingUInt32(SCHEMA, "Schema"),
TraceLoggingUInt64(Now(), "QPC"),
TraceLoggingUInt64(Frequency(), "QPCFrequency"),
TraceLoggingString(kind, "Kind"), TraceLoggingInt64(guest_ticks, "GuestTicks"),
TraceLoggingUInt32(guest_frequency, "GuestFrequency"));
#endif
}
void Marker(const char* name)
{
if (!Enabled())
return;
#ifdef _WIN32
TraceLoggingWrite(s_dolphin_perf, "Marker", TraceLoggingUInt32(SCHEMA, "Schema"),
TraceLoggingUInt64(Now(), "QPC"),
TraceLoggingUInt64(Frequency(), "QPCFrequency"),
TraceLoggingString(name, "Name"));
#endif
}
void Queue(const void* owner, u64 frames, u64 bytes, u64 oldest_qpc, u64 peak)
{
if (!Enabled())
return;
#ifdef _WIN32
TraceLoggingWrite(s_dolphin_perf, "USBQueue", TraceLoggingUInt32(SCHEMA, "Schema"),
TraceLoggingUInt64(Now(), "QPC"), TraceLoggingPointer(owner, "Owner"),
TraceLoggingUInt64(frames, "Frames"), TraceLoggingUInt64(bytes, "Bytes"),
TraceLoggingUInt64(oldest_qpc, "OldestQPC"),
TraceLoggingUInt64(peak, "PeakFrames"));
#endif
}
} // namespace Common::PerformanceTrace
+126
View File
@@ -0,0 +1,126 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <cstddef>
#include "Common/CommonTypes.h"
namespace Common::PerformanceTrace
{
// Opt-in diagnostic events, never guest state. Windows provider:
// Dolphin.Performance / {54ee9231-28be-4ab0-981e-b26f979c56a4}, schema 1.
// Set DOLPHIN_PERF_TRACE=1 before process creation and start ETW before launching.
enum class Region : u32
{
Slice,
ARM,
Jit,
Compile,
Devices,
NAND,
Journal,
JournalBuild,
JournalWrite,
JournalFlush,
NAT,
DMARead,
DMAWrite,
AES,
SHA,
Count
};
enum class Counter : u32
{
JournalTransactions,
JournalBytes,
JournalErrors,
NatRXFrames,
NatRXBytes,
NatTXFrames,
NatTXBytes,
NatPollErrors,
UsbRXFrames,
UsbRXBytes,
UsbDrops,
UsbDeliveredBytes,
NANDReads,
NANDPrograms,
NANDErases,
Count
};
struct Timing
{
u64 calls = 0;
u64 inclusive = 0;
u64 exclusive = 0;
};
// Clock-independent accounting, also exercised with deterministic timestamps in tests.
class ScopeStack
{
public:
bool Begin(Region region, u64 now);
bool End(u64 now);
void Reset();
size_t Depth() const { return m_depth; }
u64 Errors() const { return m_errors; }
const auto& Timings() const { return m_timings; }
private:
struct Entry
{
Region region;
u64 start;
u64 children;
};
std::array<Entry, 32> m_stack{};
std::array<Timing, static_cast<size_t>(Region::Count)> m_timings{};
size_t m_depth = 0;
u64 m_errors = 0;
};
bool Requested();
bool Enabled();
u64 Now();
u64 Frequency();
u64 NewGeneration();
void Add(Counter counter, u64 value = 1);
void Snapshot(u64 core_id, u64 instructions, u64 native_instructions, u64 slow_reads,
u64 slow_writes, u64 compiles, u64 fallbacks, u32 pc, bool waiting);
void CodeMap(const char* domain, const void* owner, const void* begin, const void* end,
u32 guest_pc, u64 identity, const char* kind);
void CodeReset(const char* domain, const void* owner, const void* begin, const void* end);
void Frame(const char* kind, s64 guest_ticks = 0, u32 guest_frequency = 0);
void Marker(const char* name);
void Queue(const void* owner, u64 frames, u64 bytes, u64 oldest_qpc, u64 peak);
// Only scopes inside a randomly selected root slice read the clock. No per-access timers.
class SampleRoot
{
public:
explicit SampleRoot(bool force_for_test = false);
~SampleRoot();
SampleRoot(const SampleRoot&) = delete;
SampleRoot& operator=(const SampleRoot&) = delete;
private:
bool m_active = false;
u64 m_start = 0;
u32 m_denominator = 1024;
};
class Scope
{
public:
explicit Scope(Region region);
~Scope();
Scope(const Scope&) = delete;
Scope& operator=(const Scope&) = delete;
private:
bool m_active = false;
};
} // namespace Common::PerformanceTrace
+2
View File
@@ -258,6 +258,8 @@ const Info<std::string> MAIN_GPU_DETERMINISM_MODE{{System::Main, "Core", "GPUDet
const Info<s32> MAIN_OVERRIDE_BOOT_IOS{{System::Main, "Core", "OverrideBootIOS"}, -1};
const Info<bool> MAIN_WII_IOS_LLE{{System::Main, "Core", "WiiIOSLLE"}, false};
const Info<bool> MAIN_WII_STARLET_JIT{{System::Main, "Core", "WiiStarletJIT"}, true};
const Info<bool> MAIN_WII_STARLET_JIT_MEMORY_PROFILING{
{System::Main, "Core", "WiiStarletJITMemoryProfiling"}, false};
const Info<bool> MAIN_WII_LLE_ETHERNET{{System::Main, "Core", "WiiLLEEthernet"}, false};
GPUDeterminismMode GetGPUDeterminismMode()
+1
View File
@@ -168,6 +168,7 @@ extern const Info<bool> MAIN_REAL_WII_REMOTE_REPEAT_REPORTS;
extern const Info<s32> MAIN_OVERRIDE_BOOT_IOS;
extern const Info<bool> MAIN_WII_IOS_LLE;
extern const Info<bool> MAIN_WII_STARLET_JIT;
extern const Info<bool> MAIN_WII_STARLET_JIT_MEMORY_PROFILING;
extern const Info<bool> MAIN_WII_LLE_ETHERNET;
extern const Info<std::string> MAIN_WII_NUS_SHOP_URL;
extern const Info<bool> MAIN_WII_WIILINK_ENABLE;
+13 -4
View File
@@ -12,6 +12,7 @@
#include <utility>
#include "Common/ChunkFile.h"
#include "Common/PerformanceTrace.h"
#if defined(_M_X86_64)
// ARMCore owns ARMJitX64 by value through unique_ptr construction in this translation unit, so a
// private JIT layout change must rebuild this file as well as the emitter.
@@ -32,7 +33,8 @@ constexpr u32 ModeValue(ARMCore::Mode mode)
}
} // namespace
ARMCore::ARMCore(ARMBus& bus) : m_bus(bus)
ARMCore::ARMCore(ARMBus& bus, bool profile_jit_memory)
: m_bus(bus), m_profile_jit_memory(profile_jit_memory)
{
Reset();
}
@@ -52,6 +54,7 @@ void ARMCore::SetJitEnabled(bool enabled)
void ARMCore::Reset(u32 reset_vector)
{
m_trace_generation = Common::PerformanceTrace::NewGeneration();
m_registers.fill(0);
m_user_r8_r14.fill(0);
m_fiq_r8_r14.fill(0);
@@ -237,9 +240,12 @@ u32 ARMCore::WalkPageTables(u32 modified_address, const u8** first_descriptor,
u32* first_descriptor_value, const u8** second_descriptor,
u32* second_descriptor_value) const
{
const auto read_descriptor = [&](u32 physical_address, const u8** host_pointer,
u32* host_value) {
const u8* const pointer = m_bus.GetDirectMemoryPointer(physical_address, sizeof(u32));
const auto read_descriptor = [&](u32 physical_address, const u8** host_pointer, u32* host_value) {
// Only native block validation needs a persistent descriptor pointer. Ordinary TLB
// misses need the bus value alone; do not resolve the same physical memory twice.
const u8* const pointer = (host_pointer || host_value) ?
m_bus.GetDirectMemoryPointer(physical_address, sizeof(u32)) :
nullptr;
if (host_pointer)
*host_pointer = pointer;
if (host_value)
@@ -791,6 +797,7 @@ u64 ARMCore::Run(u64 instruction_count)
u64 ARMCore::RunCycles(u64 cycle_budget)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::ARM);
u64 cycles = 0;
while (cycles < cycle_budget && !m_yield_requested)
{
@@ -1341,6 +1348,8 @@ std::vector<ARMCore::HotPCSample> ARMCore::TakeHotJitFallbackIntervalSamples(siz
void ARMCore::DoState(PointerWrap& p)
{
if (p.IsReadMode())
m_trace_generation = Common::PerformanceTrace::NewGeneration();
p.DoArray(m_registers);
p.Do(m_cpsr);
p.DoArray(m_user_r8_r14);
+6 -1
View File
@@ -112,7 +112,8 @@ public:
static constexpr u32 CPSR_T = 1U << 5;
static constexpr u32 CPSR_MODE_MASK = 0x1f;
explicit ARMCore(ARMBus& bus);
// Host-only diagnostic choice, fixed for this core's lifetime and not part of guest state.
explicit ARMCore(ARMBus& bus, bool profile_jit_memory = false);
~ARMCore();
void Reset(u32 reset_vector = 0);
@@ -165,6 +166,8 @@ public:
u64 GetExecutedInstructions() const { return m_executed_instructions; }
void SetJitEnabled(bool enabled);
bool IsJitEnabled() const { return m_jit_enabled; }
bool IsJitMemoryProfilingEnabled() const { return m_profile_jit_memory; }
u64 GetTraceGeneration() const { return m_trace_generation; }
u64 GetJitExecutedInstructions() const;
u64 GetJitNativeExecutedInstructions() const;
size_t GetJitCompiledBlockCount() const;
@@ -322,6 +325,8 @@ private:
void WriteCP15(u32 opcode1, u32 crn, u32 crm, u32 opcode2, u32 value);
ARMBus& m_bus;
const bool m_profile_jit_memory;
u64 m_trace_generation = 0; // Host diagnostics only; never serialized.
std::array<u32, 16> m_registers{};
u32 m_cpsr = 0;
+104 -84
View File
@@ -10,6 +10,7 @@
#include "Common/BitSet.h"
#include "Common/CommonTypes.h"
#include "Common/PerformanceTrace.h"
#include "Common/x64ABI.h"
#include "Core/HW/Memmap.h"
#include "Core/IOS/Starlet/ARMCore.h"
@@ -94,6 +95,7 @@ ARMJitX64::ARMJitX64(ARMCore& core) : m_core(core)
ARMJitX64::~ARMJitX64()
{
Common::PerformanceTrace::CodeReset("ARM", this, region, region + region_size);
FreeCodeSpace();
}
@@ -128,6 +130,7 @@ void ARMJitX64::RequestClear()
void ARMJitX64::ClearCodeCache()
{
Common::PerformanceTrace::CodeReset("ARM", this, m_block_code_begin, region + region_size);
m_discarded_block_count += m_blocks.size();
m_blocks.clear();
std::ranges::fill(m_fast_entries, FastEntry{});
@@ -150,6 +153,7 @@ void ARMJitX64::InvalidateTranslationContext()
u32 ARMJitX64::Run(u64 cycle_budget)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::Jit);
if (cycle_budget == 0 || !m_run_entry)
return 0;
@@ -405,6 +409,7 @@ void ARMJitX64::GenerateDispatcher()
RET();
m_block_code_begin = AlignCode16();
Common::PerformanceTrace::CodeMap("ARM", this, region, m_block_code_begin, 0, 0, "dispatcher");
}
u32 ARMJitX64::MakeFastEntryKey(u32 address, u32 control, u32 process_id)
@@ -562,13 +567,17 @@ ARMJitX64::Block* ARMJitX64::GetOrCompileBlock(u32 address, u32* physical_addres
return nullptr;
}
const u8* const trace_begin = GetCodePtr();
Block block = CompileBlock(virtual_address, thumb);
Common::PerformanceTrace::CodeMap("ARM", this, trace_begin, GetCodePtr(), virtual_address,
block_key, thumb ? "thumb" : "arm");
++m_lifetime_compiled_block_count;
return &m_blocks.emplace(block_key, block).first->second;
}
ARMJitX64::Block ARMJitX64::CompileBlock(u32 address, bool thumb)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::Compile);
const u8* const body = AlignCode16();
LoadRegisterCache();
MOV(8, MPCWritten(), Imm8(0));
@@ -2256,8 +2265,10 @@ void ARMJitX64::EmitFastmemAddress(std::vector<FixupBranch>* slow_paths, u32 acc
// Addresses above MEM2 are only directly accessible when they hit the high Starlet SRAM
// mirror. Everything else remains on the exact bus/MMIO path.
if (sram_access == SRAMFastmemAccess::None || !m_sram_base || !m_boot0_mapped ||
!m_sram_split_mode)
// SRAM writes are still bus-only. Reject them before testing the direct-read aperture;
// retain R9's physical address for the exact slow write without redundant SRAM checks.
if (sram_access == SRAMFastmemAccess::None || sram_access == SRAMFastmemAccess::Write ||
!m_sram_base || !m_boot0_mapped || !m_sram_split_mode)
{
slow_paths->push_back(J(Jump::Near));
SetJumpTarget(below_mem2);
@@ -2311,37 +2322,20 @@ void ARMJitX64::EmitFastmemAddress(std::vector<FixupBranch>* slow_paths, u32 acc
MOV(32, R(EAX), R(R9));
AND(32, R(EAX), Imm32(0x1ffff));
// Full-chain profiling gives a narrow safe aperture for reads. Direct writes to the measured
// IOS pages 0x00/0x19 passed isolated bounds tests, but reproducibly terminated BootMii during
// its hardware setup. Keep every SRAM write on the exact bus path until write coherency with
// the live boot chain is understood. Boot0, split-window holes and register-list transfers also
// remain on the bus.
// Limit scalar direct reads to pages measured across the boot chain and native IOS update.
// EDX is an aperture page in [0, 31]; the mask avoids a growing chain of comparisons. Keep
// ECX intact for the caller's unaligned ARM word rotation. Boot0 and split-window holes still
// use the bus. Writes and register-list transfers were rejected above: direct SRAM writes
// previously regressed BootMii despite passing isolated bounds tests.
MOV(32, R(EDX), R(EAX));
SHR(32, R(EDX), Imm8(12));
if (sram_access == SRAMFastmemAccess::Write)
{
slow_paths->push_back(J(Jump::Near));
}
else
{
CMP(32, R(EDX), Imm32(0x00));
const FixupBranch profiled_read_page_00 = J_CC(CC_E, Jump::Near);
CMP(32, R(EDX), Imm32(0x10));
const FixupBranch irq_vector_read_page_10 = J_CC(CC_E, Jump::Near);
CMP(32, R(EDX), Imm32(0x12));
const FixupBranch profiled_read_page_12 = J_CC(CC_E, Jump::Near);
CMP(32, R(EDX), Imm32(0x14));
const FixupBranch profiled_read_page_14 = J_CC(CC_E, Jump::Near);
CMP(32, R(EDX), Imm32(0x19));
const FixupBranch profiled_read_page_19 = J_CC(CC_E, Jump::Near);
CMP(32, R(EDX), Imm32(0x1e));
slow_paths->push_back(J_CC(CC_NE, Jump::Near));
SetJumpTarget(profiled_read_page_00);
SetJumpTarget(irq_vector_read_page_10);
SetJumpTarget(profiled_read_page_12);
SetJumpTarget(profiled_read_page_14);
SetJumpTarget(profiled_read_page_19);
}
constexpr u32 profiled_read_pages = (1U << 0x00) | (1U << 0x01) | (1U << 0x02) | (1U << 0x04) |
(1U << 0x10) | (1U << 0x12) | (1U << 0x13) | (1U << 0x14) |
(1U << 0x15) | (1U << 0x16) | (1U << 0x19) | (1U << 0x1a) |
(1U << 0x1c) | (1U << 0x1d) | (1U << 0x1e);
MOV(32, R(R8), Imm32(profiled_read_pages));
BT(32, R(R8), R(EDX));
slow_paths->push_back(J_CC(CC_NC, Jump::Near));
MOV(64, R(R8), ImmPtr(m_sram_split_mode));
CMP(8, MatR(R8), Imm8(0));
@@ -2408,17 +2402,25 @@ void ARMJitX64::EmitThumbMemorySlowPath(const std::vector<FixupBranch>& slow_pat
MOV(32, R(ABI_PARAM4), Imm32(0));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(ReadMemorySlow);
ABI_CallFunction(m_core.IsJitMemoryProfilingEnabled() ? ReadMemorySlow<true> :
ReadMemorySlow<false>);
if (sign_extend)
MOVSX(32, access_size * 8, EAX, R(EAX));
MOV(32, MStoredRegister(rd), R(EAX));
}
else
{
MOV(32, R(ABI_PARAM4), MStoredRegister(rd));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(32, R(ABI_PARAM3), MStoredRegister(rd));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(WriteMemorySlow);
if (access_size == 1)
ABI_CallFunction(m_core.IsJitMemoryProfilingEnabled() ? WriteMemorySlow<1, true> :
WriteMemorySlow<1, false>);
else if (access_size == 2)
ABI_CallFunction(m_core.IsJitMemoryProfilingEnabled() ? WriteMemorySlow<2, true> :
WriteMemorySlow<2, false>);
else
ABI_CallFunction(m_core.IsJitMemoryProfilingEnabled() ? WriteMemorySlow<4, true> :
WriteMemorySlow<4, false>);
}
if (access_size > 1)
{
@@ -2514,16 +2516,21 @@ void ARMJitX64::EmitARMMemorySlowPath(const std::vector<FixupBranch>& slow_paths
MOV(32, R(ABI_PARAM4), R(RBP));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(ReadMemorySlow);
ABI_CallFunction(m_core.IsJitMemoryProfilingEnabled() ? ReadMemorySlow<true> :
ReadMemorySlow<false>);
MOV(32, MStoredRegister(rd), R(EAX));
}
else
{
MOV(32, R(ABI_PARAM2), R(R9));
MOV(32, R(ABI_PARAM4), MStoredRegister(rd));
MOV(32, R(ABI_PARAM3), Imm32(access_size));
MOV(32, R(ABI_PARAM3), MStoredRegister(rd));
MOV(64, R(ABI_PARAM1), ImmPtr(this));
ABI_CallFunction(WriteMemorySlow);
if (byte)
ABI_CallFunction(m_core.IsJitMemoryProfilingEnabled() ? WriteMemorySlow<1, true> :
WriteMemorySlow<1, false>);
else
ABI_CallFunction(m_core.IsJitMemoryProfilingEnabled() ? WriteMemorySlow<4, true> :
WriteMemorySlow<4, false>);
}
if (writeback)
MOV(32, MStoredRegister(rn), R(RBX));
@@ -2959,29 +2966,35 @@ void ARMJitX64::ExceptionReturn(ARMJitX64* jit, u32 target)
core.WritePC(target);
}
template <bool profile>
u32 ARMJitX64::ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size,
u32 byte_offset)
{
++jit->m_slow_read_count;
if ((jit->m_slow_read_count & 0xff) == 0)
++jit->m_slow_memory_address_samples[physical_address & ~3ULL];
switch (ClassifySlowMemoryAddress(physical_address))
// The helper is selected at code generation, so disabling detailed diagnostics adds no
// per-access mode branch. Keep the aggregate counter and the exact bus transaction in both.
if constexpr (profile)
{
case SlowMemoryRegion::RAM:
++jit->m_slow_ram_read_count;
break;
case SlowMemoryRegion::SRAM:
++jit->m_slow_sram_read_count;
++(physical_address < 0xfff00000 ? jit->m_slow_sram_low_access_count :
jit->m_slow_sram_high_access_count);
++jit->m_slow_sram_page_read_access_count[(physical_address & 0x1ffff) >> 12];
break;
case SlowMemoryRegion::MMIO:
++jit->m_slow_mmio_read_count;
break;
case SlowMemoryRegion::Other:
++jit->m_slow_other_read_count;
break;
if ((jit->m_slow_read_count & 0xff) == 0)
++jit->m_slow_memory_address_samples[physical_address & ~3ULL];
switch (ClassifySlowMemoryAddress(physical_address))
{
case SlowMemoryRegion::RAM:
++jit->m_slow_ram_read_count;
break;
case SlowMemoryRegion::SRAM:
++jit->m_slow_sram_read_count;
++(physical_address < 0xfff00000 ? jit->m_slow_sram_low_access_count :
jit->m_slow_sram_high_access_count);
++jit->m_slow_sram_page_read_access_count[(physical_address & 0x1ffff) >> 12];
break;
case SlowMemoryRegion::MMIO:
++jit->m_slow_mmio_read_count;
break;
case SlowMemoryRegion::Other:
++jit->m_slow_other_read_count;
break;
}
}
ARMCore& core = jit->m_core;
if (access_size == 1)
@@ -2998,45 +3011,52 @@ u32 ARMJitX64::ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_s
return std::rotr(value, static_cast<int>((byte_offset & 3) * 8));
}
void ARMJitX64::WriteMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size, u32 value)
template <u32 access_size, bool profile>
void ARMJitX64::WriteMemorySlow(ARMJitX64* jit, u32 physical_address, u32 value)
{
// Select width while compiling the guest instruction, not on every dynamic bus write.
// Mapping, protection, MMIO side effects, endian conversion and aggregate counts stay live.
static_assert(access_size == 1 || access_size == 2 || access_size == 4);
++jit->m_slow_write_count;
if ((jit->m_slow_write_count & 0xff) == 0)
++jit->m_slow_memory_address_samples[(1ULL << 32) | (physical_address & ~3ULL)];
switch (ClassifySlowMemoryAddress(physical_address))
if constexpr (profile)
{
case SlowMemoryRegion::RAM:
++jit->m_slow_ram_write_count;
break;
case SlowMemoryRegion::SRAM:
++jit->m_slow_sram_write_count;
++(physical_address < 0xfff00000 ? jit->m_slow_sram_low_access_count :
jit->m_slow_sram_high_access_count);
++jit->m_slow_sram_page_write_access_count[(physical_address & 0x1ffff) >> 12];
break;
case SlowMemoryRegion::MMIO:
++jit->m_slow_mmio_write_count;
break;
case SlowMemoryRegion::Other:
++jit->m_slow_other_write_count;
break;
if ((jit->m_slow_write_count & 0xff) == 0)
++jit->m_slow_memory_address_samples[(1ULL << 32) | (physical_address & ~3ULL)];
switch (ClassifySlowMemoryAddress(physical_address))
{
case SlowMemoryRegion::RAM:
++jit->m_slow_ram_write_count;
break;
case SlowMemoryRegion::SRAM:
++jit->m_slow_sram_write_count;
++(physical_address < 0xfff00000 ? jit->m_slow_sram_low_access_count :
jit->m_slow_sram_high_access_count);
++jit->m_slow_sram_page_write_access_count[(physical_address & 0x1ffff) >> 12];
break;
case SlowMemoryRegion::MMIO:
++jit->m_slow_mmio_write_count;
break;
case SlowMemoryRegion::Other:
++jit->m_slow_other_write_count;
break;
}
}
ARMCore& core = jit->m_core;
if (access_size == 1)
if constexpr (access_size == 1)
{
core.m_bus.Write8(physical_address, static_cast<u8>(value));
return;
}
if (access_size == 2)
else if constexpr (access_size == 2)
{
const u16 halfword = static_cast<u16>(value);
core.m_bus.Write16(physical_address, core.m_big_endian ? halfword : std::byteswap(halfword));
return;
}
if (!core.m_big_endian)
value = std::byteswap(value);
core.m_bus.Write32(physical_address & ~3U, value);
else
{
if (!core.m_big_endian)
value = std::byteswap(value);
core.m_bus.Write32(physical_address & ~3U, value);
}
}
std::vector<std::pair<u64, u64>> ARMJitX64::TakeHotSlowMemorySamples(size_t maximum_count)
+3 -1
View File
@@ -227,8 +227,10 @@ private:
static void ExecuteUserBankBlockTransfer(ARMJitX64* jit, u32 instruction, u32 address);
static void ExecuteThumbPushPop(ARMJitX64* jit, u16 instruction, u32 address);
static void ExceptionReturn(ARMJitX64* jit, u32 target);
template <bool profile>
static u32 ReadMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size, u32 byte_offset);
static void WriteMemorySlow(ARMJitX64* jit, u32 physical_address, u32 access_size, u32 value);
template <u32 access_size, bool profile>
static void WriteMemorySlow(ARMJitX64* jit, u32 physical_address, u32 value);
static u32 TranslateAddress(ARMJitX64* jit, u32 address);
static void EnterUndefinedARM(ARMJitX64* jit, u32 instruction, u32 address);
static void EnterSVCARM(ARMJitX64* jit, u32 address);
+36
View File
@@ -11,6 +11,7 @@
#include "Common/ChunkFile.h"
#include "Common/Logging/Log.h"
#include "Common/PerformanceTrace.h"
namespace IOS::LLE
{
@@ -43,6 +44,8 @@ AX88772::AX88772(Transmit transmit) : m_transmit(std::move(transmit))
void AX88772::Reset()
{
m_trace_rx_times = {};
m_trace_rx_head = m_trace_rx_peak = 0;
m_address = m_configuration = 0;
m_control_valid = m_control_stalled = false;
m_control.clear();
@@ -77,6 +80,11 @@ bool AX88772::LinkUp() const
void AX88772::DoState(PointerWrap& p)
{
if (p.IsReadMode())
{
m_trace_rx_times = {};
m_trace_rx_head = m_trace_rx_peak = 0;
}
p.Do(m_mac);
p.Do(m_eeprom);
p.Do(m_phy);
@@ -403,6 +411,8 @@ bool AX88772::ApplyControl(std::span<const u8> data)
if (value & 1)
{
m_rx.clear();
m_trace_rx_times = {};
m_trace_rx_head = 0;
m_rx_offset = 0;
}
if (value & 2)
@@ -451,7 +461,10 @@ void AX88772::ReceiveFrame(std::span<const u8> frame)
{
if (!LinkUp() || (m_rx_control & 0x80) == 0 || (m_medium & 0x100) == 0 || frame.size() < 14 ||
frame.size() > MAX_FRAME || m_rx.size() >= MAX_QUEUE)
{
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::UsbDrops);
return;
}
const bool broadcast =
std::all_of(frame.begin(), frame.begin() + 6, [](u8 b) { return b == 0xff; });
const bool multicast = (frame[0] & 1) != 0;
@@ -465,7 +478,28 @@ void AX88772::ReceiveFrame(std::span<const u8> frame)
packet.insert(packet.end(), frame.begin(), frame.end());
if (length & 1)
packet.push_back(0);
if (Common::PerformanceTrace::Requested())
{
m_trace_rx_times[(m_trace_rx_head + m_rx.size()) % m_trace_rx_times.size()] =
Common::PerformanceTrace::Now();
m_trace_rx_peak = std::max(m_trace_rx_peak, m_rx.size() + 1);
}
m_rx.push_back(std::move(packet));
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::UsbRXFrames);
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::UsbRXBytes, frame.size());
}
void AX88772::TraceQueue() const
{
if (!Common::PerformanceTrace::Enabled())
return;
u64 bytes = 0;
for (const auto& packet : m_rx)
bytes += packet.size();
if (!m_rx.empty())
bytes -= m_rx_offset;
Common::PerformanceTrace::Queue(this, m_rx.size(), bytes,
m_rx.empty() ? 0 : m_trace_rx_times[m_trace_rx_head], m_trace_rx_peak);
}
AX88772::Result AX88772::Transfer(u8 endpoint, u8 direction, std::span<u8> buffer, size_t* actual)
@@ -560,9 +594,11 @@ AX88772::Result AX88772::Transfer(u8 endpoint, u8 direction, std::span<u8> buffe
*actual = std::min(buffer.size(), packet.size() - m_rx_offset);
std::copy_n(packet.begin() + m_rx_offset, *actual, buffer.begin());
m_rx_offset += *actual;
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::UsbDeliveredBytes, *actual);
if (m_rx_offset == packet.size())
{
m_rx.pop_front();
m_trace_rx_head = (m_trace_rx_head + 1) % m_trace_rx_times.size();
m_rx_offset = 0;
}
return Result::Completed;
+4
View File
@@ -36,6 +36,7 @@ public:
void ReceiveFrame(std::span<const u8> frame);
Result Transfer(u8 endpoint, u8 direction, std::span<u8> buffer, size_t* actual);
u8 GetAddress() const { return m_address; }
void TraceQueue() const;
private:
static constexpr u8 INTERRUPT_INTERVAL_MS = 10;
@@ -77,5 +78,8 @@ private:
// Diagnostics only; not part of guest-visible/savestate state.
u32 m_control_log_count = 0;
u64 m_interrupt_reports = 0;
std::array<u64, 128> m_trace_rx_times{};
size_t m_trace_rx_head = 0;
size_t m_trace_rx_peak = 0;
};
} // namespace IOS::LLE
+31 -12
View File
@@ -13,6 +13,7 @@
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/PerformanceTrace.h"
namespace IOS::LLE
{
@@ -137,31 +138,49 @@ bool NANDJournal::Open(const std::string& path, const Common::SHA1::Digest& sour
bool NANDJournal::Append(const Pages& pages, bool replace)
{
namespace Trace = Common::PerformanceTrace;
const Trace::Scope trace(Trace::Region::Journal);
if (!m_file || pages.size() > PAGE_COUNT)
return false;
if (pages.empty() && !replace)
return true;
std::vector<u8> record;
record.reserve(TRANSACTION_HEADER_SIZE + pages.size() * ENTRY_SIZE);
Put32(record, TRANSACTION_MAGIC);
Put32(record, replace ? 1 : 0);
Put32(record, static_cast<u32>(pages.size()));
for (const auto& [page, data] : pages)
Common::SHA1::Digest checksum;
{
if (page >= PAGE_COUNT)
return false;
Put32(record, page);
record.insert(record.end(), data.begin(), data.end());
const Trace::Scope build_trace(Trace::Region::JournalBuild);
record.reserve(TRANSACTION_HEADER_SIZE + pages.size() * ENTRY_SIZE);
Put32(record, TRANSACTION_MAGIC);
Put32(record, replace ? 1 : 0);
Put32(record, static_cast<u32>(pages.size()));
for (const auto& [page, data] : pages)
{
if (page >= PAGE_COUNT)
return false;
Put32(record, page);
record.insert(record.end(), data.begin(), data.end());
}
checksum = Common::SHA1::CalculateDigest(record);
}
const auto checksum = Common::SHA1::CalculateDigest(record);
if (!m_file.WriteArray(record.data(), record.size()) || !m_file.WriteArray(checksum) ||
!m_file.Flush())
bool success;
{
const Trace::Scope write_trace(Trace::Region::JournalWrite);
success = m_file.WriteArray(record.data(), record.size()) && m_file.WriteArray(checksum);
}
if (success)
{
const Trace::Scope flush_trace(Trace::Region::JournalFlush);
success = m_file.Flush();
}
if (!success)
{
Trace::Add(Trace::Counter::JournalErrors);
// Never acknowledge volatile writes as successful after a storage error.
ERROR_LOG_FMT(IOS, "Persistent NAND write failed; subsequent NAND writes are disabled");
m_file.Close();
return false;
}
Trace::Add(Trace::Counter::JournalTransactions);
Trace::Add(Trace::Counter::JournalBytes, record.size() + checksum.size());
return true;
}
} // namespace IOS::LLE
@@ -18,6 +18,7 @@
#include "Common/DynamicLibrary.h"
#include "Common/Logging/Log.h"
#include "Common/Network.h"
#include "Common/PerformanceTrace.h"
#include "Common/StringUtil.h"
namespace IOS::LLE
@@ -192,6 +193,8 @@ bool SlirpNetwork::Start(const std::string& directory, Receive receive, std::str
auto& self = *static_cast<Impl*>(opaque);
if (length >= 14 && length <= 1518 && self.receive)
{
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NatRXFrames);
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NatRXBytes, length);
if (++self.rx_frames <= 8)
INFO_LOG_FMT(IOS_NET, "Starlet NAT -> USB Ethernet: frame {} ({} bytes)", self.rx_frames,
length);
@@ -244,8 +247,11 @@ bool SlirpNetwork::Start(const std::string& directory, Receive receive, std::str
void SlirpNetwork::Input(std::span<const u8> frame)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::NAT);
if (m_impl && frame.size() >= 14 && frame.size() <= 1518)
{
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NatTXFrames);
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NatTXBytes, frame.size());
if (++m_impl->tx_frames <= 8)
INFO_LOG_FMT(IOS_NET, "Starlet USB Ethernet -> NAT: frame {} ({} bytes, type {:#06x})",
m_impl->tx_frames, frame.size(), (u16(frame[12]) << 8) | frame[13]);
@@ -256,6 +262,7 @@ void SlirpNetwork::Input(std::span<const u8> frame)
void SlirpNetwork::Poll(u64 arm_cycles)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::NAT);
if (!m_impl)
return;
auto& self = *m_impl;
@@ -285,6 +292,7 @@ void SlirpNetwork::Poll(u64 arm_cycles)
#endif
if (result < 0)
{
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NatPollErrors);
#ifdef _WIN32
const int error = WSAGetLastError();
#else
+64 -48
View File
@@ -9,6 +9,7 @@
#include "Common/ChunkFile.h"
#include "Common/GekkoDisassembler.h"
#include "Common/Logging/Log.h"
#include "Common/PerformanceTrace.h"
#include "Common/Timer.h"
#include "Core/Config/MainSettings.h"
#include "Core/CoreTiming.h"
@@ -43,7 +44,8 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
}
// ARMCore owns generation-tagged software TLB and instruction caches, so
// construct it only after the complete physical bus exists.
m_core = std::make_unique<ARMCore>(*m_memory);
m_core = std::make_unique<ARMCore>(*m_memory,
Config::Get(Config::MAIN_WII_STARLET_JIT_MEMORY_PROFILING));
#if defined(_M_X86_64)
m_core->SetJitEnabled(Config::Get(Config::MAIN_WII_STARLET_JIT));
#else
@@ -62,6 +64,8 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
m_system.GetCoreTiming().ScheduleEvent(0, m_run_event);
INFO_LOG_FMT(IOS, "Starlet LLE started at PC {:#010x} ({})", m_core->GetRegister(15),
m_core->IsJitEnabled() ? "JIT" : "interpreter");
INFO_LOG_FMT(IOS, "Starlet JIT detailed memory profiling={} (aggregate counts remain active)",
m_core->IsJitMemoryProfilingEnabled());
return true;
}
@@ -176,6 +180,7 @@ void Starlet::RunCallback(Core::System& system, u64, s64 cycles_late)
void Starlet::RunSlice(s64 cycles_late)
{
const Common::PerformanceTrace::SampleRoot trace_root;
if (!m_initialized)
return;
@@ -205,6 +210,13 @@ void Starlet::RunSlice(s64 cycles_late)
const u64 now_ms = check_realtime_diagnostic ? Common::Timer::NowMs() : 0;
if (check_realtime_diagnostic && now_ms >= m_next_realtime_diagnostic_ms)
{
Common::PerformanceTrace::Snapshot(
m_core->GetTraceGeneration(), m_core->GetExecutedInstructions(),
m_core->GetJitNativeExecutedInstructions(), m_core->GetJitSlowReadCount(),
m_core->GetJitSlowWriteCount(), m_core->GetJitLifetimeCompiledBlockCount(),
m_core->GetJitFallbackInstructionCount(), m_core->GetRegister(15),
m_core->IsWaitingForExternalEvent());
m_memory->TraceNetwork();
m_next_realtime_diagnostic_ms = now_ms + 1000;
const PowerPC::PowerPCState& ppc = m_system.GetPPCState();
const PowerPC::TryReadInstResult instruction = m_system.GetMMU().TryReadInstruction(ppc.pc);
@@ -282,18 +294,19 @@ void Starlet::RunSlice(s64 cycles_late)
INFO_LOG_FMT(IOS,
"Starlet live JIT: instructions={} native={:.1f}% fallbacks={} blocks={} "
"translations={} slow-reads={} "
"slow-writes={} slow-regions=ram:{}/sram:{}/mmio:{}/other:{} "
"slow-writes={} memory-profile={} slow-regions=ram:{}/sram:{}/mmio:{}/other:{} "
"interval-fallbacks={} idle-polls={} ipc-armctrl={:#04x} "
"PC={:#010x} "
"CPSR={:#010x} irqflag={:#010x} irqmask={:#010x} fiqmask={:#010x}",
jit_executed, jit_executed == 0 ? 0.0 : 100.0 * jit_native / jit_executed,
m_core->GetJitFallbackInstructionCount(), m_core->GetJitCompiledBlockCount(),
m_core->GetJitAddressTranslationCount(), m_core->GetJitSlowReadCount(),
m_core->GetJitSlowWriteCount(), m_core->GetJitSlowRAMAccessCount(),
m_core->GetJitSlowSRAMAccessCount(), m_core->GetJitSlowMMIOAccessCount(),
m_core->GetJitSlowOtherAccessCount(), interval_fallbacks,
m_core->GetMemoryPollEntryCount(), ipc_arm_ctrl, m_core->GetRegister(15),
m_core->GetCPSR(), m_system.GetWiiIPC().ReadStarletRegister(0x38),
m_core->GetJitSlowWriteCount(), m_core->IsJitMemoryProfilingEnabled(),
m_core->GetJitSlowRAMAccessCount(), m_core->GetJitSlowSRAMAccessCount(),
m_core->GetJitSlowMMIOAccessCount(), m_core->GetJitSlowOtherAccessCount(),
interval_fallbacks, m_core->GetMemoryPollEntryCount(), ipc_arm_ctrl,
m_core->GetRegister(15), m_core->GetCPSR(),
m_system.GetWiiIPC().ReadStarletRegister(0x38),
m_system.GetWiiIPC().ReadStarletRegister(0x3c),
m_system.GetWiiIPC().ReadStarletRegister(0x40));
INFO_LOG_FMT(IOS,
@@ -322,50 +335,53 @@ void Starlet::RunSlice(s64 cycles_late)
m_core->GetJitFastEntryReplacementCount(),
m_core->GetJitInstructionCacheClearCount(), m_core->GetJitCodeSpaceClearCount(),
m_core->GetJitDiscardedBlockCount());
std::array<u32, 4> hot_sram_read_pages{};
std::array<u64, 4> hot_sram_read_page_counts{};
std::array<u32, 4> hot_sram_write_pages{};
std::array<u64, 4> hot_sram_write_page_counts{};
const auto rank_sram_page = [](u32 page, u64 count, auto& pages, auto& counts) {
for (size_t rank = 0; rank < counts.size(); ++rank)
{
if (count <= counts[rank])
continue;
for (size_t move = counts.size() - 1; move > rank; --move)
if (m_core->IsJitMemoryProfilingEnabled())
{
std::array<u32, 4> hot_sram_read_pages{};
std::array<u64, 4> hot_sram_read_page_counts{};
std::array<u32, 4> hot_sram_write_pages{};
std::array<u64, 4> hot_sram_write_page_counts{};
const auto rank_sram_page = [](u32 page, u64 count, auto& pages, auto& counts) {
for (size_t rank = 0; rank < counts.size(); ++rank)
{
counts[move] = counts[move - 1];
pages[move] = pages[move - 1];
if (count <= counts[rank])
continue;
for (size_t move = counts.size() - 1; move > rank; --move)
{
counts[move] = counts[move - 1];
pages[move] = pages[move - 1];
}
counts[rank] = count;
pages[rank] = page;
break;
}
counts[rank] = count;
pages[rank] = page;
break;
};
for (u32 page = 0; page < 32; ++page)
{
rank_sram_page(page, m_core->GetJitSlowSRAMPageReadAccessCount(page), hot_sram_read_pages,
hot_sram_read_page_counts);
rank_sram_page(page, m_core->GetJitSlowSRAMPageWriteAccessCount(page), hot_sram_write_pages,
hot_sram_write_page_counts);
}
INFO_LOG_FMT(
IOS,
"Starlet slow SRAM aliases=low:{}/high:{} reads:{}/writes:{} "
"hot-reads={:#04x}:{}, {:#04x}:{}, {:#04x}:{}, {:#04x}:{} "
"hot-writes={:#04x}:{}, {:#04x}:{}, {:#04x}:{}, {:#04x}:{}",
m_core->GetJitSlowSRAMLowAccessCount(), m_core->GetJitSlowSRAMHighAccessCount(),
m_core->GetJitSlowSRAMReadAccessCount(), m_core->GetJitSlowSRAMWriteAccessCount(),
hot_sram_read_pages[0], hot_sram_read_page_counts[0], hot_sram_read_pages[1],
hot_sram_read_page_counts[1], hot_sram_read_pages[2], hot_sram_read_page_counts[2],
hot_sram_read_pages[3], hot_sram_read_page_counts[3], hot_sram_write_pages[0],
hot_sram_write_page_counts[0], hot_sram_write_pages[1], hot_sram_write_page_counts[1],
hot_sram_write_pages[2], hot_sram_write_page_counts[2], hot_sram_write_pages[3],
hot_sram_write_page_counts[3]);
for (const auto& [key, samples] : m_core->TakeHotJitSlowMemorySamples(8))
{
const bool write = (key >> 32) != 0;
INFO_LOG_FMT(IOS, "Starlet slow memory {} address={:#010x} samples={}",
write ? "write" : "read", static_cast<u32>(key), samples);
}
};
for (u32 page = 0; page < 32; ++page)
{
rank_sram_page(page, m_core->GetJitSlowSRAMPageReadAccessCount(page), hot_sram_read_pages,
hot_sram_read_page_counts);
rank_sram_page(page, m_core->GetJitSlowSRAMPageWriteAccessCount(page), hot_sram_write_pages,
hot_sram_write_page_counts);
}
INFO_LOG_FMT(IOS,
"Starlet slow SRAM aliases=low:{}/high:{} reads:{}/writes:{} "
"hot-reads={:#04x}:{}, {:#04x}:{}, {:#04x}:{}, {:#04x}:{} "
"hot-writes={:#04x}:{}, {:#04x}:{}, {:#04x}:{}, {:#04x}:{}",
m_core->GetJitSlowSRAMLowAccessCount(), m_core->GetJitSlowSRAMHighAccessCount(),
m_core->GetJitSlowSRAMReadAccessCount(), m_core->GetJitSlowSRAMWriteAccessCount(),
hot_sram_read_pages[0], hot_sram_read_page_counts[0], hot_sram_read_pages[1],
hot_sram_read_page_counts[1], hot_sram_read_pages[2], hot_sram_read_page_counts[2],
hot_sram_read_pages[3], hot_sram_read_page_counts[3], hot_sram_write_pages[0],
hot_sram_write_page_counts[0], hot_sram_write_pages[1],
hot_sram_write_page_counts[1], hot_sram_write_pages[2],
hot_sram_write_page_counts[2], hot_sram_write_pages[3],
hot_sram_write_page_counts[3]);
for (const auto& [key, samples] : m_core->TakeHotJitSlowMemorySamples(8))
{
const bool write = (key >> 32) != 0;
INFO_LOG_FMT(IOS, "Starlet slow memory {} address={:#010x} samples={}",
write ? "write" : "read", static_cast<u32>(key), samples);
}
const ARMCore::HotPCSample current = m_core->GetCurrentPCSample();
INFO_LOG_FMT(IOS, "Starlet current PC {:#010x} {} instruction={:#010x}", current.address,
+219 -79
View File
@@ -4,6 +4,7 @@
#include "Core/IOS/Starlet/StarletMemory.h"
#include <algorithm>
#include <bitset>
#include <cstring>
#include <limits>
#include <string_view>
@@ -15,8 +16,10 @@
#include "Common/FileUtil.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/PerformanceTrace.h"
#include "Common/SDCardUtil.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
#include "Core/Config/MainSettings.h"
#include "Core/HW/MMIO.h"
#include "Core/HW/Memmap.h"
@@ -873,6 +876,7 @@ std::optional<u32> StarletMemory::TryReadBroadwayResetInstruction(u32 address) c
void StarletMemory::AdvanceCycles(u64 cycles)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::Devices);
const u32 old_timer = GetTimer();
m_arm_cycles += cycles;
const u32 new_timer = GetTimer();
@@ -909,6 +913,12 @@ void StarletMemory::AdvanceCycles(u64 cycles)
}
}
void StarletMemory::TraceNetwork() const
{
if (m_ethernet)
m_ethernet->TraceQueue();
}
bool StarletMemory::IsIdlePollAddress(u32 address, u32 size) const
{
if (size == 0 || address > std::numeric_limits<u32>::max() - (size - 1))
@@ -1007,21 +1017,11 @@ u32 StarletMemory::GetBootROMOffset(u32 address) const
u32 StarletMemory::GetSRAMOffset(u32 address) const
{
const u32 window_offset = address & (SRAM_WINDOW_SIZE - 1);
if (!m_sram_split_mode)
{
if (window_offset < SRAM_A_SIZE)
return window_offset;
if (window_offset < SRAM_A_SIZE + SRAM_B_SIZE)
return SRAM_A_SIZE + window_offset - SRAM_A_SIZE;
return INVALID_SRAM_OFFSET;
}
if (window_offset < SRAM_B_SIZE)
return SRAM_A_SIZE + window_offset;
if (window_offset >= SRAM_A_SIZE)
return window_offset - SRAM_A_SIZE;
return INVALID_SRAM_OFFSET;
// SM swaps the two 64 KiB halves of the CPU aperture. Bank B occupies only 32 KiB;
// its remaining half is a hole in either mapping, never a wrap into bank A.
static_assert(SRAM_SIZE == SRAM_A_SIZE + SRAM_B_SIZE);
const u32 offset = (address & (SRAM_WINDOW_SIZE - 1)) ^ (m_sram_split_mode ? SRAM_A_SIZE : 0);
return offset < SRAM_SIZE ? offset : INVALID_SRAM_OFFSET;
}
u8 StarletMemory::ReadMapped8(u32 address) const
@@ -1092,6 +1092,56 @@ void StarletMemory::WriteDMA8(u32 address, u8 value)
WriteMapped8(address, value);
}
u8* StarletMemory::GetDMAMemoryPointer(u32 address, size_t size) const
{
// Only coalesce a transfer wholly inside ordinary backing memory. In particular, DMA's
// low SRAM aperture uses physical banks, independent of SM and the CPU boot0 overlay.
// Subtraction after checking the starting address also prevents 32-bit range wraparound.
if (address >= SRAM_BASE && address < SRAM_BASE + SRAM_SIZE &&
size <= SRAM_BASE + SRAM_SIZE - address)
{
return const_cast<u8*>(m_sram.data()) + (address - SRAM_BASE);
}
if ((address < Memory::MEM1_SIZE_RETAIL && size <= Memory::MEM1_SIZE_RETAIL - address) ||
(address >= 0x10000000 && address < 0x10000000 + Memory::MEM2_SIZE_RETAIL &&
size <= 0x10000000 + Memory::MEM2_SIZE_RETAIL - address))
{
const auto memory = m_system.GetMemory().GetSpanForAddress(address);
if (size <= memory.size())
return memory.data();
}
return nullptr;
}
void StarletMemory::ReadDMABuffer(u32 address, std::span<u8> buffer) const
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::DMARead);
if (buffer.empty())
return;
if (const u8* memory = GetDMAMemoryPointer(address, buffer.size()))
{
// Callers use separate device staging buffers, never an overlapping guest-memory span.
std::memcpy(buffer.data(), memory, buffer.size());
return;
}
for (size_t i = 0; i < buffer.size(); ++i)
buffer[i] = ReadDMA8(address + static_cast<u32>(i));
}
void StarletMemory::WriteDMABuffer(u32 address, std::span<const u8> buffer)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::DMAWrite);
if (buffer.empty())
return;
if (u8* memory = GetDMAMemoryPointer(address, buffer.size()))
{
std::memcpy(memory, buffer.data(), buffer.size());
return;
}
for (size_t i = 0; i < buffer.size(); ++i)
WriteDMA8(address + static_cast<u32>(i), buffer[i]);
}
void StarletMemory::ClearRegisterCache()
{
for (auto& entry : m_register_cache)
@@ -1633,17 +1683,17 @@ void StarletMemory::AdvanceOHCI(size_t controller, u64 cycles)
u32 StarletMemory::ReadOHCIMemory32(u32 address) const
{
return static_cast<u32>(ReadDMA8(address)) | static_cast<u32>(ReadDMA8(address + 1)) << 8 |
static_cast<u32>(ReadDMA8(address + 2)) << 16 |
static_cast<u32>(ReadDMA8(address + 3)) << 24;
std::array<u8, 4> bytes;
ReadDMABuffer(address, bytes);
return static_cast<u32>(bytes[0]) | static_cast<u32>(bytes[1]) << 8 |
static_cast<u32>(bytes[2]) << 16 | static_cast<u32>(bytes[3]) << 24;
}
void StarletMemory::WriteOHCIMemory32(u32 address, u32 value)
{
WriteDMA8(address, static_cast<u8>(value));
WriteDMA8(address + 1, static_cast<u8>(value >> 8));
WriteDMA8(address + 2, static_cast<u8>(value >> 16));
WriteDMA8(address + 3, static_cast<u8>(value >> 24));
const std::array<u8, 4> bytes = {static_cast<u8>(value), static_cast<u8>(value >> 8),
static_cast<u8>(value >> 16), static_cast<u8>(value >> 24)};
WriteDMABuffer(address, bytes);
}
std::vector<u8> StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end) const
@@ -1661,11 +1711,9 @@ std::vector<u8> StarletMemory::ReadOHCIBuffer(u32 current_buffer, u32 buffer_end
return {};
std::vector<u8> buffer(first_size + second_size);
for (size_t i = 0; i < first_size; ++i)
buffer[i] = ReadDMA8(current_buffer + static_cast<u32>(i));
ReadDMABuffer(current_buffer, std::span(buffer).first(first_size));
const u32 second_page = buffer_end & ~0xfffU;
for (size_t i = 0; i < second_size; ++i)
buffer[first_size + i] = ReadDMA8(second_page + static_cast<u32>(i));
ReadDMABuffer(second_page, std::span(buffer).subspan(first_size));
return buffer;
}
@@ -1678,15 +1726,13 @@ void StarletMemory::WriteOHCIBuffer(u32 current_buffer, u32 buffer_end, const u8
const size_t first_capacity =
same_page ? buffer_end - current_buffer + 1 : 0x1000 - (current_buffer & 0xfff);
const size_t first_size = std::min(size, first_capacity);
for (size_t i = 0; i < first_size; ++i)
WriteDMA8(current_buffer + static_cast<u32>(i), data[i]);
WriteDMABuffer(current_buffer, std::span(data, first_size));
if (size > first_size)
{
const u32 second_page = buffer_end & ~0xfffU;
const size_t second_size =
std::min(size - first_size, static_cast<size_t>((buffer_end & 0xfff) + 1));
for (size_t i = 0; i < second_size; ++i)
WriteDMA8(second_page + static_cast<u32>(i), data[first_size + i]);
WriteDMABuffer(second_page, std::span(data + first_size, second_size));
}
}
@@ -3081,15 +3127,13 @@ bool StarletMemory::TransferSDCardBlocks(bool read, u32 argument, u32 block_size
{
if (!m_sd_card.ReadBytes(data.data(), data.size()))
return false;
for (size_t i = 0; i < data.size(); ++i)
WriteDMA8(dma_address + static_cast<u32>(i), data[i]);
WriteDMABuffer(dma_address, data);
}
else
{
if (!Config::Get(Config::MAIN_ALLOW_SD_WRITES))
return false;
for (size_t i = 0; i < data.size(); ++i)
data[i] = ReadDMA8(dma_address + static_cast<u32>(i));
ReadDMABuffer(dma_address, data);
if (!m_sd_card.WriteBytes(data.data(), data.size()))
return false;
}
@@ -3479,16 +3523,28 @@ u8 StarletMemory::Read8(u32 address)
u16 StarletMemory::Read16(u32 address)
{
// Aligned reads stay inside one SRAM/boot0 mapping segment. Resolve the live mapping once,
// including its holes, before considering unrelated memory and peripheral ranges.
if ((address & 1) == 0 && IsSRAMWindowAddress(address))
{
if (IsBootROMAddress(address))
return Common::swap16(m_boot_rom.data() + GetBootROMOffset(address));
const u32 offset = GetSRAMOffset(address);
return offset == INVALID_SRAM_OFFSET ? 0 : Common::swap16(m_sram.data() + offset);
}
if (IsMemoryAddress(address) && IsMemoryAddress(address + 1))
return m_system.GetMemory().Read_U16(address);
if (IsBootROMAddress(address) && IsBootROMAddress(address + 1))
{
const u32 offset = GetBootROMOffset(address);
return static_cast<u16>((static_cast<u16>(m_boot_rom[offset]) << 8) | m_boot_rom[offset + 1]);
if (GetBootROMOffset(address + 1) == offset + 1)
return static_cast<u16>((static_cast<u16>(m_boot_rom[offset]) << 8) | m_boot_rom[offset + 1]);
}
if (IsSRAMWindowAddress(address) && IsSRAMWindowAddress(address + 1))
if (!IsBootROMAddress(address) && !IsBootROMAddress(address + 1) &&
IsSRAMWindowAddress(address) && IsSRAMWindowAddress(address + 1))
{
const u32 offset = GetSRAMOffset(address);
const u32 end_offset = GetSRAMOffset(address + 1);
@@ -3515,6 +3571,18 @@ u32 StarletMemory::Read32(u32 address)
{
return m_system.GetWiiIPC().ReadStarletRegister(address - HW_BASE);
}
// Keep timer/IPC reads first: SRAM optimization must not add range tests to their hot path.
if (IsSRAMWindowAddress(address))
{
if (IsBootROMAddress(address))
return Common::swap32(m_boot_rom.data() + GetBootROMOffset(address));
const u32 offset = GetSRAMOffset(address);
return offset == INVALID_SRAM_OFFSET ? 0 : Common::swap32(m_sram.data() + offset);
}
// IOS probes HW_VERSION frequently. It is ordinary register backing, with no read side
// effects: retain its live byte values without decoding the device range four times.
if (address == HW_VERSION)
return ReadRegister(address);
}
if ((address & 3) == 0 && IsDIAddress(address))
@@ -3534,12 +3602,18 @@ u32 StarletMemory::Read32(u32 address)
if (IsBootROMAddress(address) && IsBootROMAddress(address + 3))
{
const u32 offset = GetBootROMOffset(address);
return (static_cast<u32>(m_boot_rom[offset]) << 24) |
(static_cast<u32>(m_boot_rom[offset + 1]) << 16) |
(static_cast<u32>(m_boot_rom[offset + 2]) << 8) | m_boot_rom[offset + 3];
// Unaligned reads can cross the 8 KiB ROM mirror or the boot0/SRAM overlay boundary.
// Such reads must resolve each byte; indexing beyond m_boot_rom would read unrelated data.
if (GetBootROMOffset(address + 3) == offset + 3)
{
return (static_cast<u32>(m_boot_rom[offset]) << 24) |
(static_cast<u32>(m_boot_rom[offset + 1]) << 16) |
(static_cast<u32>(m_boot_rom[offset + 2]) << 8) | m_boot_rom[offset + 3];
}
}
if (IsSRAMWindowAddress(address) && IsSRAMWindowAddress(address + 3))
if (!IsBootROMAddress(address) && !IsBootROMAddress(address + 3) &&
IsSRAMWindowAddress(address) && IsSRAMWindowAddress(address + 3))
{
const u32 offset = GetSRAMOffset(address);
const u32 end_offset = GetSRAMOffset(address + 3);
@@ -3769,6 +3843,19 @@ void StarletMemory::Write8(u32 address, u8 value)
void StarletMemory::Write16(u32 address, u16 value)
{
// An aligned scalar cannot cross an SRAM bank, aperture or boot0-overlay boundary.
// Keep this on the bus (including live mapping/protection), but resolve it only once.
if ((address & 1) == 0 && IsSRAMWindowAddress(address))
{
if (!IsBootROMAddress(address))
{
const u32 offset = GetSRAMOffset(address);
if (offset != INVALID_SRAM_OFFSET)
Common::WriteSwap16(m_sram.data() + offset, value);
}
return;
}
if (IsMemoryAddress(address) && IsMemoryAddress(address + 1))
{
m_system.GetMemory().Write_U16(value, address);
@@ -3793,6 +3880,20 @@ void StarletMemory::Write16(u32 address, u16 value)
void StarletMemory::Write32(u32 address, u32 value)
{
// SRAM is ordinary byte-addressed storage, not MMIO. Aligned words stay inside one
// mapping segment, so there is no need to decode every device or map all four bytes.
// Unaligned/boundary accesses below still use the original byte-exact bus path.
if ((address & 3) == 0 && IsSRAMWindowAddress(address))
{
if (!IsBootROMAddress(address))
{
const u32 offset = GetSRAMOffset(address);
if (offset != INVALID_SRAM_OFFSET)
Common::WriteSwap32(m_sram.data() + offset, value);
}
return;
}
// Match the aligned read fast path above. WriteRegister keeps the byte-addressable backing image
// coherent, while the exact device handlers retain W1C interrupt flags, reset side effects and
// the Starlet/Broadway scheduling boundary of the four-byte Write8 path.
@@ -4005,6 +4106,7 @@ bool StarletMemory::ReadRawNANDPage(u32 page, NANDPage* raw)
bool StarletMemory::CommitNANDPages(const NANDJournal::Pages& pages)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::NAND);
if (m_nand_journal && !m_nand_journal->Append(pages))
return false;
for (const auto& [page, data] : pages)
@@ -4068,14 +4170,26 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
const char* const end = std::find(entry.name, std::end(entry.name), '\0');
return std::string_view(entry.name, static_cast<size_t>(end - entry.name));
};
const auto find_child = [&superblock, &entry_name](u16 parent, std::string_view name) {
if (parent >= superblock->fst.size())
bool invalid_directory = false;
const auto find_child = [&superblock, &entry_name, &invalid_directory](u16 parent,
std::string_view name) {
if (parent >= superblock->fst.size() ||
(superblock->fst[parent].mode & 3) !=
static_cast<u8>(DiscIO::NANDImporter::Type::Directory))
{
invalid_directory = true;
return std::optional<u16>{};
}
std::bitset<DiscIO::NANDImporter::FSTEntryCount> visited;
u16 child = superblock->fst[parent].sub;
while (child != NAND_INVALID_ENTRY)
{
if (child >= superblock->fst.size())
if (child >= superblock->fst.size() || visited.test(child))
{
invalid_directory = true;
return std::optional<u16>{};
}
visited.set(child);
if (entry_name(superblock->fst[child]) == name)
return std::optional<u16>{child};
child = superblock->fst[child].sib;
@@ -4089,8 +4203,17 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
entry = entry ? find_child(*entry, component) : std::nullopt;
if (!entry)
{
ERROR_LOG_FMT(IOS_WIIMOTE, "Raw NAND SFFS is missing /shared2/sys/SYSCONF");
return false;
if (invalid_directory)
{
ERROR_LOG_FMT(IOS_WIIMOTE, "Invalid raw NAND directory while looking for SYSCONF");
return false;
}
// Pairing injection is a convenience, not a prerequisite for boot0/BootMii.
// A freshly formatted NAND may not have SYSCONF yet. Leave it untouched:
// native software can create it, and controllers keep their default identities.
INFO_LOG_FMT(IOS_WIIMOTE, "Skipping Wii Remote auto-pairing: /shared2/sys/SYSCONF is absent; "
"continuing boot without modifying NAND");
return true;
}
}
@@ -4159,8 +4282,13 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
return false;
const std::string_view name(reinterpret_cast<const char*>(sysconf.data() + *raw_offset + 1),
name_size);
if (name == "BT.DINF" && type == 1)
if (name == "BT.DINF")
{
if (type != 1)
{
ERROR_LOG_FMT(IOS_WIIMOTE, "Raw NAND SYSCONF has an invalid BT.DINF type");
return false;
}
const std::optional<u16> size_minus_one = read_be16(value_header);
if (!size_minus_one)
return false;
@@ -4170,8 +4298,15 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
}
}
if (!pairing_offset)
{
INFO_LOG_FMT(IOS_WIIMOTE, "Skipping Wii Remote auto-pairing: SYSCONF has no BT.DINF entry; "
"continuing boot without modifying NAND");
return true;
}
const std::vector<u8> pairing = IOS::HLE::GetEmulatedWiimotePairingData();
if (!pairing_offset || !pairing_size || *pairing_size != pairing.size() ||
if (!pairing_size || *pairing_size != pairing.size() ||
*pairing_offset + *pairing_size > sysconf.size())
{
ERROR_LOG_FMT(IOS_WIIMOTE, "Raw NAND SYSCONF has no compatible BT.DINF section");
@@ -4223,8 +4358,8 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
std::ranges::copy(pairing, sysconf.begin() + static_cast<ptrdiff_t>(*pairing_offset));
// Re-encrypt only the file's SFFS clusters and place their raw pages in the
// persistent overlay. Preserve all spare metadata from the dump and replace only
// the ECC for the modified ciphertext.
// persistent overlay. Preserve unrelated spare metadata and regenerate the
// plaintext HMAC copies and ciphertext ECC for each modified cluster.
size_t file_offset = 0;
NANDJournal::Pages pairing_pages;
for (size_t cluster_number = 0; cluster_number < cluster_indices.size(); ++cluster_number)
@@ -4274,10 +4409,20 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
return false;
std::copy_n(encrypted_cluster.begin() + page_in_cluster * NAND_PAGE_DATA_SIZE,
NAND_PAGE_DATA_SIZE, raw.begin());
// SFFS keeps redundant HMACs at spare offset 0x0c in the seventh and
// eighth pages.
if (page_in_cluster >= 6)
std::ranges::copy(hmac, raw.begin() + NAND_PAGE_DATA_SIZE + 0x0c);
// Native SFFS stores a full HMAC at spare byte 1 in the seventh page.
// Its redundant copy spans that page's bytes 21-32 (first 12 bytes)
// and the eighth page's bytes 1-8 (last 8 bytes). Byte 0 is the bad-block
// marker. Writing two full copies at 0x0c leaves both native copies
// invalid, so IOS discards the updated SYSCONF and loses the pairings.
if (page_in_cluster == 6)
{
std::ranges::copy(hmac, raw.begin() + NAND_PAGE_DATA_SIZE + 1);
std::copy_n(hmac.begin(), 12, raw.begin() + NAND_PAGE_DATA_SIZE + 21);
}
else if (page_in_cluster == 7)
{
std::copy_n(hmac.begin() + 12, 8, raw.begin() + NAND_PAGE_DATA_SIZE + 1);
}
const auto ecc = CalculateNANDECC(raw.data());
std::ranges::copy(ecc, raw.begin() + NAND_PAGE_DATA_SIZE + 0x30);
pairing_pages.insert_or_assign(page, raw);
@@ -4293,6 +4438,7 @@ bool StarletMemory::InstallEmulatedWiimotePairings()
bool StarletMemory::ReadNANDPage(u32 command)
{
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NANDReads);
if (!(command & NAND_CTRL_READ) || !m_nand_read_pending)
return false;
@@ -4306,21 +4452,18 @@ bool StarletMemory::ReadNANDPage(u32 command)
if (!ReadRawNANDPage(m_nand_read_page, &raw))
return false;
for (u32 i = 0; i < length; ++i)
{
const u32 raw_offset = m_nand_read_column + i;
u32 destination = data_address + i;
if (m_nand_read_column < NAND_PAGE_DATA_SIZE && raw_offset >= NAND_PAGE_DATA_SIZE)
destination = spare_address + raw_offset - NAND_PAGE_DATA_SIZE;
WriteDMA8(destination, raw[raw_offset]);
}
const u32 data_size = m_nand_read_column < NAND_PAGE_DATA_SIZE ?
std::min(length, NAND_PAGE_DATA_SIZE - m_nand_read_column) :
length;
const auto transfer = std::span(raw).subspan(m_nand_read_column, length);
WriteDMABuffer(data_address, transfer.first(data_size));
WriteDMABuffer(spare_address, transfer.subspan(data_size));
if (command & NAND_CTRL_ECC)
{
const auto ecc = CalculateNANDECC(raw.data());
const u32 calculated_ecc_address = spare_address ^ 0x40U;
for (u32 i = 0; i < ecc.size(); ++i)
WriteDMA8(calculated_ecc_address + i, ecc[i]);
WriteDMABuffer(calculated_ecc_address, ecc);
}
return true;
}
@@ -4383,21 +4526,17 @@ bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
const u32 data_address = ReadRegister(NAND_DATA) & ~0xfU;
const u32 spare_address = ReadRegister(NAND_ECC) & ~0xfU;
for (u32 i = 0; i < length; ++i)
{
const u32 raw_offset = column + i;
u32 source = data_address + i;
if (column < NAND_PAGE_DATA_SIZE && raw_offset >= NAND_PAGE_DATA_SIZE)
source = spare_address + raw_offset - NAND_PAGE_DATA_SIZE;
m_nand_program_data[raw_offset] = ReadDMA8(source);
}
const u32 data_size =
column < NAND_PAGE_DATA_SIZE ? std::min(length, NAND_PAGE_DATA_SIZE - column) : length;
const auto transfer = std::span(m_nand_program_data).subspan(column, length);
ReadDMABuffer(data_address, transfer.first(data_size));
ReadDMABuffer(spare_address, transfer.subspan(data_size));
if ((command & NAND_CTRL_ECC) != 0 && column == 0 && length >= NAND_PAGE_DATA_SIZE)
{
const auto ecc = CalculateNANDECC(m_nand_program_data.data());
const u32 calculated_ecc_address = spare_address ^ 0x40U;
for (u32 i = 0; i < ecc.size(); ++i)
WriteDMA8(calculated_ecc_address + i, ecc[i]);
WriteDMABuffer(calculated_ecc_address, ecc);
m_nand_program_ecc_enabled = true;
}
return true;
@@ -4405,6 +4544,7 @@ bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
bool StarletMemory::CommitNANDProgram()
{
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NANDPrograms);
if (!m_nand_program_pending)
return false;
@@ -4452,6 +4592,7 @@ bool StarletMemory::StageNANDErase()
bool StarletMemory::CommitNANDErase()
{
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NANDErases);
if (!m_nand_erase_pending)
return false;
@@ -4557,6 +4698,7 @@ void StarletMemory::HandleAESWrite(u32 address, u32 value)
void StarletMemory::ExecuteAESCommand(u32 command)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::AES);
if (command == 0)
return;
if ((command & AES_CTRL_EXEC) == 0)
@@ -4567,8 +4709,7 @@ void StarletMemory::ExecuteAESCommand(u32 command)
const u32 destination = ReadRegister(AES_DEST) & ~0xfU;
std::vector<u8> input(size);
std::vector<u8> output(size);
for (size_t i = 0; i < size; ++i)
input[i] = ReadDMA8(source + static_cast<u32>(i));
ReadDMABuffer(source, input);
std::array<u8, 16> next_iv{};
bool succeeded = true;
@@ -4588,8 +4729,7 @@ void StarletMemory::ExecuteAESCommand(u32 command)
{
if ((command & AES_CTRL_ENABLE) != 0)
m_aes_iv = next_iv;
for (size_t i = 0; i < size; ++i)
WriteDMA8(destination + static_cast<u32>(i), output[i]);
WriteDMABuffer(destination, output);
WriteRegister(AES_SRC, source + static_cast<u32>(size));
WriteRegister(AES_DEST, destination + static_cast<u32>(size));
}
@@ -4667,6 +4807,7 @@ void StarletMemory::CompressSHA1(const u8* block)
void StarletMemory::ExecuteSHACommand(u32 command)
{
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::SHA);
if (!(command & (1U << 31)))
return;
@@ -4675,8 +4816,7 @@ void StarletMemory::ExecuteSHACommand(u32 command)
std::array<u8, 64> block{};
for (u32 i = 0; i < blocks; ++i)
{
for (u32 j = 0; j < block.size(); ++j)
block[j] = ReadDMA8(source + i * 64 + j);
ReadDMABuffer(source + i * 64, block);
CompressSHA1(block.data());
}
for (u32 i = 0; i < m_sha_state.size(); ++i)
@@ -11,6 +11,7 @@
#include <memory>
#include <mutex>
#include <optional>
#include <span>
#include <string>
#include <vector>
@@ -74,11 +75,16 @@ public:
const u8* GetDirectMemoryPointer(u32 address, u32 size) const override;
u64 GetCycles() const { return m_arm_cycles; }
void TraceNetwork() const;
std::optional<u32> TryReadBroadwayResetInstruction(u32 address) const;
void SetWiimoteSource(size_t index, WiimoteCommon::HIDWiimote* source);
bool EnableEthernetNAT(const std::string& runtime_directory, std::string* error);
private:
// Synthetic NAND fixtures exercise startup pairing without any console dumps or keys.
friend class StarletPairingTest;
friend class StarletDMATest;
static constexpr u32 NAND_PAGE_DATA_SIZE = 0x800;
static constexpr u32 NAND_PAGE_SPARE_SIZE = 0x40;
static constexpr u32 NAND_RAW_PAGE_SIZE = NAND_PAGE_DATA_SIZE + NAND_PAGE_SPARE_SIZE;
@@ -96,6 +102,9 @@ private:
void WriteMapped8(u32 address, u8 value);
u8 ReadDMA8(u32 address) const;
void WriteDMA8(u32 address, u8 value);
u8* GetDMAMemoryPointer(u32 address, size_t size) const;
void ReadDMABuffer(u32 address, std::span<u8> buffer) const;
void WriteDMABuffer(u32 address, std::span<const u8> buffer);
bool IsBootROMAddress(u32 address) const;
u32 GetBootROMOffset(u32 address) const;
u32 GetSRAMOffset(u32 address) const;
@@ -15,6 +15,7 @@
#include "Common/CommonTypes.h"
#include "Common/JitRegister.h"
#include "Common/PerformanceTrace.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/Host.h"
@@ -189,6 +190,10 @@ void JitBaseBlockCache::FinalizeBlock(JitBlock& block, bool block_link,
LinkBlock(block);
}
Common::PerformanceTrace::CodeMap("PPC", this, block.near_begin, block.near_end,
block.effectiveAddress, block.physicalAddress, "block-near");
Common::PerformanceTrace::CodeMap("PPC", this, block.far_begin, block.far_end,
block.effectiveAddress, block.physicalAddress, "block-far");
const Common::Symbol* symbol = nullptr;
if (Common::JitRegister::IsEnabled() &&
(symbol = m_jit.m_ppc_symbol_db.GetSymbolFromAddr(block.effectiveAddress)) != nullptr)
@@ -497,6 +502,8 @@ void JitBaseBlockCache::UnlinkBlock(const JitBlock& block)
void JitBaseBlockCache::DestroyBlock(JitBlock& block)
{
Common::PerformanceTrace::CodeReset("PPC", this, block.near_begin, block.near_end);
Common::PerformanceTrace::CodeReset("PPC", this, block.far_begin, block.far_end);
if (m_entry_points_ptr)
{
if (m_entry_points_ptr[block.fast_block_map_index] == block.normalEntry)
@@ -9,6 +9,7 @@
#include <implot.h>
#include "Common/HookableEvent.h"
#include "Common/PerformanceTrace.h"
#include "Core/Config/GraphicsSettings.h"
#include "Core/Core.h"
#include "VideoCommon/VideoConfig.h"
@@ -24,6 +25,7 @@ PerformanceMetrics::PerformanceMetrics()
void PerformanceMetrics::Reset()
{
Common::PerformanceTrace::Marker("PerformanceMetricsReset");
m_fps_counter.Reset();
m_vps_counter.Reset();
@@ -38,11 +40,13 @@ void PerformanceMetrics::Reset()
void PerformanceMetrics::CountFrame()
{
Common::PerformanceTrace::Frame("produced");
m_fps_counter.Count();
}
void PerformanceMetrics::CountVBlank()
{
Common::PerformanceTrace::Frame("vblank");
m_vps_counter.Count();
}
@@ -62,6 +66,7 @@ void PerformanceMetrics::AdjustClockSpeed(s64 ticks, u32 new_ppc_clock, u32 old_
void PerformanceMetrics::CountPerformanceMarker(s64 core_ticks, u32 ticks_per_second)
{
Common::PerformanceTrace::Frame("guest-clock", core_ticks, ticks_per_second);
const auto clock_time = Clock::now();
const auto work_time = clock_time - m_time_sleeping;
+7
View File
@@ -23,3 +23,10 @@ endmacro()
add_subdirectory(Common)
add_subdirectory(Core)
add_subdirectory(VideoCommon)
# Opt-in local trace qualification; never starts a console or accesses a NAND dump.
if(WIN32 AND _M_X86_64)
add_executable(TraceProbeP1 EXCLUDE_FROM_ALL
${CMAKE_SOURCE_DIR}/Tools/PerformanceTrace/TraceProbeP1.cpp StubHost.cpp)
target_link_libraries(TraceProbeP1 PRIVATE fmt::fmt core uicommon)
endif()
+1
View File
@@ -17,6 +17,7 @@ add_dolphin_test(FloatUtilsTest FloatUtilsTest.cpp)
add_dolphin_test(MathUtilTest MathUtilTest.cpp)
add_dolphin_test(MutexTest MutexTest.cpp)
add_dolphin_test(NandPathsTest NandPathsTest.cpp)
add_dolphin_test(PerformanceTraceTest PerformanceTraceTest.cpp)
add_dolphin_test(SettingsHandlerTest SettingsHandlerTest.cpp)
add_dolphin_test(SPSCQueueTest SPSCQueueTest.cpp)
add_dolphin_test(StringUtilTest StringUtilTest.cpp)
@@ -0,0 +1,56 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Common/PerformanceTrace.h"
#include <gtest/gtest.h>
using namespace Common::PerformanceTrace;
TEST(PerformanceTrace, NestedTimesAreExclusive)
{
ScopeStack stack;
ASSERT_TRUE(stack.Begin(Region::Slice, 100));
ASSERT_TRUE(stack.Begin(Region::ARM, 110));
ASSERT_TRUE(stack.Begin(Region::Journal, 120));
ASSERT_TRUE(stack.End(150));
ASSERT_TRUE(stack.End(180));
ASSERT_TRUE(stack.End(200));
const auto& t = stack.Timings();
EXPECT_EQ(t[static_cast<size_t>(Region::Slice)].inclusive, 100u);
EXPECT_EQ(t[static_cast<size_t>(Region::Slice)].exclusive, 30u);
EXPECT_EQ(t[static_cast<size_t>(Region::ARM)].exclusive, 40u);
EXPECT_EQ(t[static_cast<size_t>(Region::Journal)].exclusive, 30u);
EXPECT_EQ(stack.Errors(), 0u);
EXPECT_EQ(stack.Depth(), 0u);
}
TEST(PerformanceTrace, RecursiveRegionAndReset)
{
ScopeStack stack;
ASSERT_TRUE(stack.Begin(Region::ARM, 0));
ASSERT_TRUE(stack.Begin(Region::ARM, 10));
ASSERT_TRUE(stack.End(20));
ASSERT_TRUE(stack.End(30));
EXPECT_EQ(stack.Timings()[static_cast<size_t>(Region::ARM)].exclusive, 30u);
EXPECT_EQ(stack.Timings()[static_cast<size_t>(Region::ARM)].calls, 2u);
stack.Reset();
EXPECT_EQ(stack.Timings()[static_cast<size_t>(Region::ARM)].calls, 0u);
EXPECT_EQ(stack.Depth(), 0u);
}
TEST(PerformanceTrace, InvalidClockAndUnderflowAreReported)
{
ScopeStack stack;
EXPECT_FALSE(stack.End(0));
ASSERT_TRUE(stack.Begin(Region::ARM, 100));
EXPECT_FALSE(stack.End(99));
EXPECT_EQ(stack.Errors(), 2u);
EXPECT_EQ(stack.Timings()[static_cast<size_t>(Region::ARM)].inclusive, 0u);
}
TEST(PerformanceTrace, DepthOverflowIsBounded)
{
ScopeStack stack;
for (u64 i = 0; i < 32; ++i)
ASSERT_TRUE(stack.Begin(Region::ARM, i));
EXPECT_FALSE(stack.Begin(Region::ARM, 32));
for (u64 i = 0; i < 32; ++i)
ASSERT_TRUE(stack.End(40 + i));
EXPECT_EQ(stack.Depth(), 0u);
EXPECT_EQ(stack.Errors(), 1u);
}
+4
View File
@@ -15,7 +15,11 @@ add_dolphin_test(DSPAssemblyTest
add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
add_dolphin_test(StarletDMATest IOS/Starlet/DMATest.cpp)
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
add_dolphin_test(StarletNANDJournalTest IOS/Starlet/NANDJournalTest.cpp)
add_dolphin_test(StarletPairingTest IOS/Starlet/PairingTest.cpp)
add_dolphin_test(StarletEthernetTest IOS/Starlet/AX88772Test.cpp)
target_include_directories(StarletEthernetTest PRIVATE ${PROJECT_SOURCE_DIR}/Externals/libslirp/include)
@@ -4,6 +4,7 @@
#include <algorithm>
#include <array>
#include <bit>
#include <chrono>
#include <iostream>
#include <vector>
@@ -12,6 +13,7 @@
#include "Common/ChunkFile.h"
#include "Common/CommonTypes.h"
#include "Common/Swap.h"
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
@@ -176,6 +178,7 @@ public:
const u8* GetDirectMemoryPointer(u32 address, u32 size) const override
{
++m_direct_pointer_count;
const size_t offset = ToOffset(address);
if (size == 0 || offset > m_memory.size() || size > m_memory.size() - offset)
return nullptr;
@@ -192,7 +195,6 @@ public:
void SetSRAMFastmemEnabled(bool enabled) { m_sram_fastmem_enabled = enabled; }
void SetBoot0Mapped(bool mapped) { m_boot0_mapped = mapped; }
void SetSRAMSplitMode(bool split) { m_sram_split_mode = split; }
void WriteSRAM32(u32 offset, u32 value)
{
ASSERT_LT(offset + 3, SRAM_SIZE);
@@ -241,6 +243,7 @@ public:
u64 GetRead8Count() const { return m_read8_count; }
u64 GetRead16Count() const { return m_read16_count; }
u64 GetRead32Count() const { return m_read32_count; }
u64 GetDirectPointerCount() const { return m_direct_pointer_count; }
u32 GetMMIOWord() const { return m_mmio_word; }
void ResetReadCounts()
{
@@ -270,6 +273,7 @@ private:
u64 m_read8_count = 0;
u64 m_read16_count = 0;
u64 m_read32_count = 0;
mutable u64 m_direct_pointer_count = 0;
u32 m_mmio_word = 0;
bool m_idle_poll_safe = true;
bool m_slice_stable_poll_safe = false;
@@ -2662,7 +2666,7 @@ TEST(StarletARMCore, ThumbJitBusMemoryPreservesBoot0AndSRAMHoles)
{
SCOPED_TRACE(address);
TestBus bus;
ARMCore core(bus);
ARMCore core(bus, true);
bus.SetFastmemEnabled(true);
bus.SetSRAMFastmemEnabled(true);
bus.SetBoot0Mapped(true);
@@ -2735,7 +2739,7 @@ TEST(StarletARMCore, ARMJitProfilesSlowSRAMPagesByDirection)
{
#if defined(_M_X86_64)
TestBus bus;
ARMCore core(bus);
ARMCore core(bus, true);
bus.SetFastmemEnabled(true);
bus.SetSRAMFastmemEnabled(true);
bus.SetBoot0Mapped(false);
@@ -2764,7 +2768,7 @@ TEST(StarletARMCore, ARMJitKeepsSplitSRAMMirrorWritesOnExactBusPath)
{
#if defined(_M_X86_64)
TestBus bus;
ARMCore core(bus);
ARMCore core(bus, true);
bus.SetFastmemEnabled(true);
bus.SetSRAMFastmemEnabled(true);
bus.SetBoot0Mapped(false);
@@ -3377,6 +3381,56 @@ TEST(StarletARMCore, MMUSectionTranslation)
// The first fetch walks the section table; the second hits the 1 KiB
// translation cache.
EXPECT_EQ(bus.GetRead32Count(), 3u);
EXPECT_EQ(bus.GetDirectPointerCount(), 0u);
}
TEST(StarletARMCore, InterpreterPageWalksUseBusWithoutResolvingValidationPointers)
{
for (const bool big_endian : {false, true})
{
// Section; coarse large/small/extended-small; fine large/small/tiny pages.
for (const auto [first_type, second_type] :
{std::pair{2u, 0u}, {1u, 1u}, {1u, 2u}, {1u, 3u}, {3u, 1u}, {3u, 2u}, {3u, 3u}})
{
SCOPED_TRACE(testing::Message()
<< "be=" << big_endian << " first=" << first_type << " second=" << second_type);
TestBus bus(0x20000);
ARMCore core(bus);
core.SetBigEndian(big_endian);
const auto word = [&](u32 address, u32 value) {
bus.WriteARM(address, big_endian ? value : Common::swap32(value));
};
word(0, 0xe5921000); // ldr r1, [r2]
word(4, 0xe5923000); // ldr r3, [r2]: hit both TLB and instruction cache
word(0x4000, 0x00000c02); // Identity-map code.
constexpr u32 virtual_address = 0x80012344;
constexpr u32 physical_address = 0x12344;
if (first_type == 2)
{
word(0x6000, 0x00000c02);
}
else
{
word(0x6000, 0x8000 | first_type);
const u32 index =
first_type == 1 ? (virtual_address >> 10) & 0x3fc : (virtual_address >> 8) & 0xffc;
const u32 mask = second_type == 1 ? 0xffff0000u :
(first_type == 3 && second_type == 3) ? 0xfffffc00u :
0xfffff000u;
const u32 permissions = first_type == 3 && second_type == 3 ? 0x30 : 0xff0;
word(0x8000 + index, (physical_address & mask) | permissions | second_type);
}
word(physical_address, 0x12345678);
core.GetCP15State().translation_table_base = 0x4000;
core.GetCP15State().control |= 1;
core.SetRegister(2, virtual_address);
core.Run(2);
EXPECT_EQ(core.GetRegister(1), 0x12345678u);
EXPECT_EQ(core.GetRegister(3), 0x12345678u);
EXPECT_EQ(bus.GetDirectPointerCount(), 0u);
EXPECT_EQ(bus.GetRead32Count(), first_type == 2 ? 6u : 7u);
}
}
}
TEST(StarletARMCore, JitCachesProvisionalIdentityTranslationForFaultPages)
@@ -3402,6 +3456,7 @@ TEST(StarletARMCore, JitCachesProvisionalIdentityTranslationForFaultPages)
EXPECT_EQ(core.GetRegister(3), 0x12345678u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(core.GetJitNativeExecutedInstructions(), 3u);
EXPECT_GT(bus.GetDirectPointerCount(), 0u); // Native descriptor validation is still present.
#endif
}
} // namespace
@@ -0,0 +1,413 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <limits>
#include <memory>
#include <span>
#include <vector>
#include <gtest/gtest.h>
#include "Common/Config/Config.h"
#include "Common/Crypto/SHA1.h"
#include "Common/FileUtil.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/HW/Memmap.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
namespace IOS::LLE
{
// Compare the block helpers with the original byte bus, using synthetic memory only.
class StarletDMATest : public testing::Test
{
protected:
static void SetUpTestSuite()
{
Config::Init();
SConfig::Init();
auto& system = Core::System::GetInstance();
system.SetIsWii(true);
system.GetMemory().Init();
Core::DeclareAsCPUThread();
}
static void TearDownTestSuite()
{
auto& system = Core::System::GetInstance();
system.GetMemory().Shutdown();
system.SetIsWii(false);
Core::UndeclareAsCPUThread();
SConfig::Shutdown();
Config::Shutdown();
}
void SetUp() override
{
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
m_memory = std::make_unique<StarletMemory>(system);
}
void TearDown() override
{
m_memory.reset();
if (!m_temp_directory.empty())
File::DeleteDirRecursively(m_temp_directory); // Only this fixture's unique temporary SD.
}
std::vector<u8> ReadBytes(u32 address, size_t size)
{
std::vector<u8> result(size);
for (size_t i = 0; i < size; ++i)
result[i] = m_memory->ReadDMA8(address + static_cast<u32>(i));
return result;
}
void WriteBytes(u32 address, std::span<const u8> data)
{
for (size_t i = 0; i < data.size(); ++i)
m_memory->WriteDMA8(address + static_cast<u32>(i), data[i]);
}
void CompareRanges()
{
for (const bool split : {false, true})
{
for (const bool boot0 : {false, true})
{
m_memory->m_sram_split_mode = split;
m_memory->m_boot0_mapped = boot0;
for (const u32 address :
{0U, 1U, 0x10003U, 0x017ffff0U, 0x01800000U, 0x0fffffffU,
0x10000000U, 0x10000001U, 0x13fffff0U, 0x14000000U, 0x0d3ffff0U, 0x0d400000U,
0x0d400003U, 0x0d40fff0U, 0x0d410000U, 0x0d417ff0U, 0x0d418000U, 0x0d41fff0U,
0xfff00000U, 0xfff0fff0U, 0xfff17ff0U, 0xfffefff0U, 0xffff0000U, 0xffff7ff0U,
0xfffffff0U, 0x0d800020U})
{
for (const size_t size : {0U, 1U, 2U, 3U, 4U, 7U, 16U, 63U, 512U, 2048U, 8192U})
{
SCOPED_TRACE(testing::Message() << "address=" << address << " size=" << size
<< " split=" << split << " boot0=" << boot0);
std::vector<u8> initial(size + 32);
std::vector<u8> payload(size);
for (size_t i = 0; i < initial.size(); ++i)
initial[i] = static_cast<u8>((i * 37) ^ (i >> 8));
for (size_t i = 0; i < size; ++i)
payload[i] = static_cast<u8>((i * 13) ^ (i >> 7) ^ 0xab);
WriteBytes(address - 16, initial);
std::vector<u8> actual(size);
m_memory->ReadDMABuffer(address, actual);
ASSERT_EQ(actual, ReadBytes(address, size));
WriteBytes(address, payload);
const auto expected = ReadBytes(address - 16, size + 32);
const auto expected_sram = m_memory->m_sram;
WriteBytes(address - 16, initial);
m_memory->WriteDMABuffer(address, payload);
ASSERT_EQ(ReadBytes(address - 16, size + 32), expected);
ASSERT_EQ(m_memory->m_sram, expected_sram);
}
}
}
}
}
void CheckPointerGuards()
{
for (const u32 address : {0U, 0x10000000U, 0x0d400000U, 0x0d410000U})
{
EXPECT_NE(m_memory->GetDMAMemoryPointer(address, 16), nullptr);
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, std::numeric_limits<size_t>::max()),
nullptr);
}
for (const u32 address : {0x017ffff0U, 0x13fffff0U, 0x0d417ff0U})
{
EXPECT_NE(m_memory->GetDMAMemoryPointer(address, 16), nullptr);
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, 17), nullptr);
}
for (const u32 address : {0x0d418000U, 0x0d800038U, 0xfff00000U, 0xfffffff0U})
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, 4), nullptr);
}
void CheckOHCI()
{
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
{
// The second physical page can precede the first and need not be adjacent.
const u32 first = base + 0x3fe3;
const u32 end = base + 0x0017;
std::vector<u8> payload(70);
for (size_t i = 0; i < payload.size(); ++i)
payload[i] = static_cast<u8>(i + 1);
WriteBytes(first - 1, std::vector<u8>(100, 0xcc));
WriteBytes(base - 1, std::vector<u8>(100, 0xdd));
m_memory->WriteOHCIBuffer(first, end, payload.data(), payload.size());
payload.resize(29 + 24);
EXPECT_EQ(m_memory->ReadOHCIBuffer(first, end), payload);
EXPECT_EQ(ReadBytes(first - 1, 1)[0], 0xcc);
EXPECT_EQ(ReadBytes(first + 29, 1)[0], 0xcc);
EXPECT_EQ(ReadBytes(base - 1, 1)[0], 0xdd);
EXPECT_EQ(ReadBytes(end + 1, 1)[0], 0xdd);
// Short packet must not fill the remainder of either page.
m_memory->WriteOHCIBuffer(first, end, payload.data(), 3);
EXPECT_EQ(m_memory->ReadOHCIBuffer(first, first + 2), (std::vector<u8>{1, 2, 3}));
m_memory->WriteOHCIMemory32(base + 1, 0x12345678);
EXPECT_EQ(ReadBytes(base + 1, 4), (std::vector<u8>{0x78, 0x56, 0x34, 0x12}));
EXPECT_EQ(m_memory->ReadOHCIMemory32(base + 1), 0x12345678U);
}
EXPECT_TRUE(m_memory->ReadOHCIBuffer(0, 0x1234).empty());
EXPECT_TRUE(m_memory->ReadOHCIBuffer(0x1234, 0x1233).empty());
}
void CheckNAND()
{
StarletMemory::NANDPage raw;
for (size_t i = 0; i < raw.size(); ++i)
raw[i] = static_cast<u8>((i * 37) ^ (i >> 8));
m_memory->m_nand_overlay[7] = raw;
m_memory->m_nand_read_pending = true;
m_memory->m_nand_read_page = 7;
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
{
const u32 spare = base + 0x2000;
m_memory->WriteRegister(0x0d010010, base);
m_memory->WriteRegister(0x0d010014, spare);
m_memory->WriteRegister(0x0d01000c, 7);
for (const u32 column : {0U, 1U, 0x7f0U, 0x800U, 0x830U})
{
for (const u32 length : {1U, 0x840U - column})
{
SCOPED_TRACE(testing::Message()
<< "base=" << base << " column=" << column << " length=" << length);
WriteBytes(base, std::vector<u8>(0x900, 0xcc));
WriteBytes(spare, std::vector<u8>(0x100, 0xdd));
m_memory->m_nand_read_column = column;
ASSERT_TRUE(m_memory->ReadNANDPage(0x2000 | length));
// Independent old byte-address formula, including spare-only reads via NAND_DATA.
for (u32 i = 0; i < length; ++i)
{
const u32 address =
column < 0x800 && column + i >= 0x800 ? spare + column + i - 0x800 : base + i;
ASSERT_EQ(m_memory->ReadDMA8(address), raw[column + i]);
}
m_memory->WriteRegister(0x0d010008, column);
ASSERT_TRUE(m_memory->StageNANDProgram(0x4000 | length, false));
auto expected = raw;
std::fill(expected.begin(), expected.begin() + column, 0xff);
std::fill(expected.begin() + column + length, expected.end(), 0xff);
EXPECT_EQ(m_memory->m_nand_program_data, expected);
}
}
// ECC output remains separate from the transferred spare bytes.
m_memory->m_nand_read_column = 0;
ASSERT_TRUE(m_memory->ReadNANDPage(0x3000 | 0x840));
const auto ecc = StarletMemory::CalculateNANDECC(raw.data());
EXPECT_EQ(ReadBytes(spare ^ 0x40, ecc.size()), (std::vector<u8>(ecc.begin(), ecc.end())));
EXPECT_EQ(ReadBytes(spare, 0x40), (std::vector<u8>(raw.begin() + 0x800, raw.end())));
m_memory->WriteRegister(0x0d010008, 0);
ASSERT_TRUE(m_memory->StageNANDProgram(0x5800, false));
EXPECT_TRUE(m_memory->m_nand_program_ecc_enabled);
EXPECT_EQ(ReadBytes(spare ^ 0x40, ecc.size()), (std::vector<u8>(ecc.begin(), ecc.end())));
// Random spare input uses NAND_DATA and preserves the already staged page data.
WriteBytes(base, std::span(raw).subspan(0x800));
m_memory->WriteRegister(0x0d010008, 0x800);
ASSERT_TRUE(m_memory->StageNANDProgram(0x4040, true));
EXPECT_EQ(m_memory->m_nand_program_data, raw);
}
}
void CheckOverlappingAES()
{
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
{
for (const u32 offset : {0U, 16U})
{
std::vector<u8> input(2048);
for (size_t i = 0; i < input.size(); ++i)
input[i] = static_cast<u8>((i * 37) ^ (i >> 8));
WriteBytes(base, input);
m_memory->Write32(0x0d020004, base);
m_memory->Write32(0x0d020008, base + offset);
m_memory->Write32(0x0d020000, 0x8000007f);
EXPECT_EQ(ReadBytes(base + offset, input.size()), input);
EXPECT_EQ(m_memory->Read32(0x0d020004), base + input.size());
EXPECT_EQ(m_memory->Read32(0x0d020008), base + offset + input.size());
}
}
}
void CheckSHA()
{
std::vector<u8> input(257);
for (size_t i = 0; i < input.size(); ++i)
input[i] = static_cast<u8>(i * 37);
const auto digest = Common::SHA1::CalculateDigest(input);
input.resize(320, 0);
input[257] = 0x80;
input[318] = 0x08;
input[319] = 0x08; // 257 * 8 bits, big endian.
constexpr std::array<u32, 5> initial = {0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476,
0xc3d2e1f0};
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
{
WriteBytes(base, input);
for (u32 i = 0; i < initial.size(); ++i)
m_memory->Write32(0x0d030008 + i * 4, initial[i]);
m_memory->Write32(0x0d030004, base);
m_memory->Write32(0x0d030000, 0x80000004); // Five blocks.
EXPECT_EQ(ReadBytes(0x0d030008, digest.size()),
(std::vector<u8>(digest.begin(), digest.end())));
EXPECT_EQ(m_memory->Read32(0x0d030004), base + input.size());
}
}
void CheckSD()
{
m_temp_directory = File::CreateTempDir();
ASSERT_FALSE(m_temp_directory.empty());
ASSERT_TRUE(m_memory->m_sd_card.Open(m_temp_directory + "/dma-test.raw", "w+b"));
std::vector<u8> image(4096);
for (size_t i = 0; i < image.size(); ++i)
image[i] = static_cast<u8>((i * 37) ^ (i >> 8));
ASSERT_TRUE(m_memory->m_sd_card.WriteBytes(image.data(), image.size()));
ASSERT_TRUE(m_memory->m_sd_card.Flush());
m_memory->m_sd_card_inserted = true;
for (const bool high_capacity : {false, true})
{
m_memory->m_sd_card_high_capacity = high_capacity;
const u32 argument = high_capacity ? 1 : 512;
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
{
ASSERT_TRUE(m_memory->TransferSDCardBlocks(true, argument, 512, 3, base));
EXPECT_EQ(ReadBytes(base, 1536),
(std::vector<u8>(image.begin() + 512, image.begin() + 2048)));
EXPECT_EQ(m_memory->ReadRegister(0x0d070000), base + 1536);
Config::SetCurrent(Config::MAIN_ALLOW_SD_WRITES, false);
EXPECT_FALSE(m_memory->TransferSDCardBlocks(false, argument, 512, 3, base));
Config::SetCurrent(Config::MAIN_ALLOW_SD_WRITES, true);
auto changed = ReadBytes(base, 1536);
for (auto& byte : changed)
byte ^= 0x5a;
WriteBytes(base, changed);
ASSERT_TRUE(m_memory->TransferSDCardBlocks(false, argument, 512, 3, base));
std::copy(changed.begin(), changed.end(), image.begin() + 512);
ASSERT_TRUE(m_memory->m_sd_card.Seek(0, File::SeekOrigin::Begin));
std::vector<u8> actual(image.size());
ASSERT_TRUE(m_memory->m_sd_card.ReadBytes(actual.data(), actual.size()));
EXPECT_EQ(actual, image);
EXPECT_FALSE(m_memory->TransferSDCardBlocks(true, argument, 4096, 1, base));
}
}
}
void CheckCPUSRAMReads()
{
for (size_t i = 0; i < m_memory->m_boot_rom.size(); ++i)
m_memory->m_boot_rom[i] = static_cast<u8>((i * 29) ^ (i >> 5) ^ 0xa5);
for (size_t i = 0; i < m_memory->m_sram.size(); ++i)
m_memory->m_sram[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0x5a);
// The byte CPU path is unchanged by the aligned wide-read optimization. Use it as the
// reference, including nonzero boot ROM, all mirrors, live mapping changes and wrap to MEM1.
for (const bool split : {false, true, false})
{
for (const bool boot0 : {false, true, false})
{
m_memory->Write32(0x0d800060, split ? 0x20 : 0);
m_memory->Write32(0x0d80018c, boot0 ? 0 : 0x1000);
for (const u32 base : {0x0d400000U, 0xfff00000U, 0xfff20000U, 0xfff40000U, 0xfff60000U,
0xfff80000U, 0xfffa0000U, 0xfffc0000U, 0xfffe0000U})
{
for (u32 page = 0; page < 32; ++page)
{
for (const u32 offset : {0U, 1U, 2U, 3U, 0xffcU, 0xffdU, 0xffeU, 0xfffU})
{
const u32 address = base + page * 0x1000 + offset;
const u16 expected16 =
static_cast<u16>((m_memory->Read8(address) << 8) | m_memory->Read8(address + 1));
const u32 expected32 = (static_cast<u32>(expected16) << 16) |
(m_memory->Read8(address + 2) << 8) |
m_memory->Read8(address + 3);
ASSERT_EQ(m_memory->Read16(address), expected16)
<< "address=" << address << " split=" << split << " boot0=" << boot0;
ASSERT_EQ(m_memory->Read32(address), expected32)
<< "address=" << address << " split=" << split << " boot0=" << boot0;
}
}
}
}
}
}
void CheckVersionReads()
{
constexpr u32 version = 0x0d800214;
m_memory->Reset();
ASSERT_EQ(m_memory->Read32(version), 0x11U);
for (const u32 value : {0x12345678U, 0xdeadbeefU, 0U, 0xffffffffU})
{
m_memory->Write32(version, value);
for (u32 i = 0; i < 4; ++i)
EXPECT_EQ(m_memory->Read8(version + i), static_cast<u8>(value >> (24 - i * 8)));
EXPECT_EQ(m_memory->Read32(version), value);
m_memory->Write8(version + 1, 0x5a);
EXPECT_EQ(m_memory->Read32(version), (value & 0xff00ffffU) | 0x005a0000U);
}
m_memory->Reset();
EXPECT_EQ(m_memory->Read32(version), 0x11U);
}
std::unique_ptr<StarletMemory> m_memory;
std::string m_temp_directory;
};
TEST_F(StarletDMATest, BuffersMatchByteBusAcrossMappingsBoundariesAndWrap)
{
CompareRanges();
}
TEST_F(StarletDMATest, DirectRangesExcludeHolesRegistersMirrorsAndOverflow)
{
CheckPointerGuards();
}
TEST_F(StarletDMATest, OHCIUsesTwoPhysicalPagesAndLittleEndianDescriptors)
{
CheckOHCI();
}
TEST_F(StarletDMATest, NANDSeparatesDataSpareAndECCIncludingPartialTransfers)
{
CheckNAND();
}
TEST_F(StarletDMATest, AESStagesAllInputBeforeWritingOverlappingOutput)
{
CheckOverlappingAES();
}
TEST_F(StarletDMATest, SHAMultipleBlocksMatchDigestInEveryPhysicalBank)
{
CheckSHA();
}
TEST_F(StarletDMATest, SDTransfersPreserveOffsetsContentsAndWriteProtection)
{
CheckSD();
}
TEST_F(StarletDMATest, CPUWideSRAMReadsMatchBytesWithBootROMAndEveryMirror)
{
CheckCPUSRAMReads();
}
TEST_F(StarletDMATest, VersionWordReadsPreserveLiveByteBackingAndReset)
{
CheckVersionReads();
}
} // namespace IOS::LLE
@@ -0,0 +1,276 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <memory>
#include <span>
#include <string_view>
#include <vector>
#include <gtest/gtest.h>
#include "Common/Crypto/AES.h"
#include "Common/Crypto/HMAC.h"
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/IOS/USB/Bluetooth/BTBase.h"
#include "Core/System.h"
#include "DiscIO/NANDImporter.h"
namespace IOS::LLE
{
class StarletPairingTest : public testing::Test
{
protected:
using Superblock = DiscIO::NANDImporter::NANDSuperblock;
static constexpr u32 SUPERBLOCK_START = 0x1fc00000;
static constexpr u16 SYSCONF_CLUSTER = 0x20;
void SetUp() override
{
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
m_memory = std::make_unique<StarletMemory>(system);
m_memory->Reset();
m_superblock = std::make_unique<Superblock>();
m_superblock->magic = {'S', 'F', 'F', 'S'};
m_superblock->version = 1;
for (auto& entry : m_superblock->fst)
{
entry.sub = 0xffff;
entry.sib = 0xffff;
}
m_superblock->fst[0].mode = 2;
m_superblock->fat[SYSCONF_CLUSTER] = 0xfffb;
// All sixteen raw superblock copies can be read, but only copy zero is valid.
const std::vector<u8> empty(16 * sizeof(Superblock));
PutLogicalData(SUPERBLOCK_START, empty);
}
void PutLogicalData(u32 offset, std::span<const u8> bytes)
{
ASSERT_EQ(offset % 0x800, 0u);
ASSERT_EQ(bytes.size() % 0x800, 0u);
for (u32 i = 0; i < bytes.size(); i += 0x800)
{
NANDJournal::Page page{};
std::copy_n(bytes.begin() + i, 0x800, page.begin());
m_memory->m_nand_overlay.insert_or_assign((offset + i) / 0x800, page);
}
}
void AddPath(u32 depth)
{
constexpr std::array<std::string_view, 3> names = {"shared2", "sys", "SYSCONF"};
for (u32 i = 0; i < depth; ++i)
{
m_superblock->fst[i].sub = static_cast<u16>(i + 1);
auto& child = m_superblock->fst[i + 1];
std::ranges::copy(names[i], child.name);
child.mode = i == 2 ? 1 : 2;
}
}
void SetSysconf(bool with_pairings)
{
AddPath(3);
m_superblock->fst[3].sub = SYSCONF_CLUSTER;
m_superblock->fst[3].size = 0x4000;
std::vector<u8> plain(0x4000);
std::ranges::copy(std::array{'S', 'C', 'v', '0'}, plain.begin());
if (with_pairings)
{
// Valid but empty BT.DINF: this takes the existing auto-pairing write path.
const auto pairing = IOS::HLE::GetEmulatedWiimotePairingData();
plain[5] = 1;
plain[7] = 0x10;
plain[0x10] = 0x26;
constexpr std::string_view name = "BT.DINF";
std::ranges::copy(name, plain.begin() + 0x11);
plain[0x18] = static_cast<u8>((pairing.size() - 1) >> 8);
plain[0x19] = static_cast<u8>(pairing.size() - 1);
}
const std::array<u8, 16> synthetic_key{};
auto aes = Common::AES::CreateContextEncrypt(synthetic_key.data());
std::vector<u8> encrypted(plain.size());
ASSERT_TRUE(aes->CryptIvZero(plain.data(), encrypted.data(), plain.size()));
PutLogicalData(SYSCONF_CLUSTER * 0x4000, encrypted);
}
void StoreSuperblock(u32 copy = 0)
{
PutLogicalData(SUPERBLOCK_START + copy * sizeof(Superblock),
{reinterpret_cast<const u8*>(m_superblock.get()), sizeof(Superblock)});
}
void CheckWithoutWrites(bool expected)
{
StoreSuperblock();
const auto before = m_memory->m_nand_overlay;
EXPECT_EQ(m_memory->InstallEmulatedWiimotePairings(), expected);
EXPECT_TRUE(m_memory->m_nand_overlay == before);
}
void RemoveFirstSuperblockPage() { m_memory->m_nand_overlay.erase(SUPERBLOCK_START / 0x800); }
void UseUnwritableJournal() { m_memory->m_nand_journal = std::make_unique<NANDJournal>(); }
bool Install() { return m_memory->InstallEmulatedWiimotePairings(); }
NANDJournal::Page ReadSysconfPage(u32 page)
{
NANDJournal::Page raw{};
EXPECT_TRUE(m_memory->ReadRawNANDPage(SYSCONF_CLUSTER * 8 + page, &raw));
return raw;
}
static std::array<u8, 16> CalculateECC(const NANDJournal::Page& page)
{
return StarletMemory::CalculateNANDECC(page.data());
}
void MarkSysconfSpare()
{
for (u32 page = 0; page < 8; ++page)
{
auto& raw = m_memory->m_nand_overlay.at(SYSCONF_CLUSTER * 8 + page);
for (size_t i = 0; i < 0x30; ++i)
raw[0x800 + i] = i == 0 ? 0xff : static_cast<u8>(0x40 + i + page);
}
}
std::unique_ptr<StarletMemory> m_memory;
std::unique_ptr<Superblock> m_superblock;
};
TEST_F(StarletPairingTest, MissingShared2DoesNotPreventBootOrWriteNAND)
{
CheckWithoutWrites(true);
}
TEST_F(StarletPairingTest, MissingSysDoesNotPreventBootOrWriteNAND)
{
AddPath(1);
CheckWithoutWrites(true);
}
TEST_F(StarletPairingTest, MissingSysconfDoesNotPreventBootOrWriteNAND)
{
AddPath(2);
CheckWithoutWrites(true);
}
TEST_F(StarletPairingTest, AbsentBluetoothEntryDoesNotPreventBootOrWriteNAND)
{
SetSysconf(false);
CheckWithoutWrites(true);
}
TEST_F(StarletPairingTest, UnreadableNANDRemainsAnError)
{
StoreSuperblock();
RemoveFirstSuperblockPage();
EXPECT_FALSE(Install());
}
TEST_F(StarletPairingTest, MalformedDirectoryRemainsAnError)
{
m_superblock->fst[0].sub = DiscIO::NANDImporter::FSTEntryCount;
CheckWithoutWrites(false);
}
TEST_F(StarletPairingTest, CyclicSiblingListFailsInsteadOfHanging)
{
m_superblock->fst[0].sub = 1;
m_superblock->fst[1].sib = 1;
CheckWithoutWrites(false);
}
TEST_F(StarletPairingTest, NewestFormattedSuperblockDoesNotRestoreOlderPairings)
{
SetSysconf(true);
StoreSuperblock();
// The formatter's newest root has no shared2, although an old copy still has it.
m_superblock->version = 2;
m_superblock->fst[0].sub = 0xffff;
StoreSuperblock(1);
// Keep the older copy and all raw data intact while selecting the newer root.
EXPECT_TRUE(Install());
}
TEST_F(StarletPairingTest, EmptyExistingSysconfRemainsAnError)
{
AddPath(3);
CheckWithoutWrites(false);
}
TEST_F(StarletPairingTest, FailedPairingWriteRemainsAnErrorAndDoesNotAlterOverlay)
{
SetSysconf(true);
UseUnwritableJournal();
CheckWithoutWrites(false);
}
TEST_F(StarletPairingTest, ExistingEmptyPairingRecordIsStillPopulated)
{
SetSysconf(true);
StoreSuperblock();
EXPECT_TRUE(Install());
// Once installed, the trusted identities are reused without another NAND write.
CheckWithoutWrites(true);
}
TEST_F(StarletPairingTest, PairingWriteUsesNativeSFFSAuthenticationLayout)
{
SetSysconf(true);
m_superblock->fst[3].uid = 0x12345678;
m_superblock->fst[3].x3 = 0x9abcdef0;
StoreSuperblock();
MarkSysconfSpare();
std::array<NANDJournal::Page, 8> before;
for (u32 page = 0; page < 8; ++page)
before[page] = ReadSysconfPage(page);
ASSERT_TRUE(Install());
std::array<NANDJournal::Page, 8> after;
std::vector<u8> ciphertext(0x4000);
for (u32 page = 0; page < 8; ++page)
{
after[page] = ReadSysconfPage(page);
std::copy_n(after[page].begin(), 0x800, ciphertext.begin() + page * 0x800);
}
const std::array<u8, 16> aes_key{};
auto aes = Common::AES::CreateContextDecrypt(aes_key.data());
std::vector<u8> authenticated(0x4040);
ASSERT_TRUE(aes->CryptIvZero(ciphertext.data(), authenticated.data() + 0x40, ciphertext.size()));
const auto pairing = IOS::HLE::GetEmulatedWiimotePairingData();
EXPECT_TRUE(
std::ranges::equal(pairing, std::span(authenticated).subspan(0x40 + 0x1a, pairing.size())));
// Independent fixture salt: UID, file name, cluster ordinal, FST index and x3.
const std::array<u8, 0x1c> salt = {0x12, 0x34, 0x56, 0x78, 'S', 'Y', 'S', 'C', 'O', 'N',
'F', 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 3, 0x9a, 0xbc, 0xde, 0xf0};
std::ranges::copy(salt, authenticated.begin());
const std::array<u8, 20> hmac_key{};
std::array<u8, 20> hmac{};
ASSERT_TRUE(Common::HMAC::HMACWithSHA1(hmac_key, authenticated, hmac.data()));
// Native SFFS puts the full HMAC at spare byte 1 in page 6. The second
// copy is split: 12 bytes at page 6 byte 21, then 8 at page 7 byte 1.
auto expected = before;
std::ranges::copy(hmac, expected[6].begin() + 0x801);
std::copy_n(hmac.begin(), 12, expected[6].begin() + 0x815);
std::copy_n(hmac.begin() + 12, 8, expected[7].begin() + 0x801);
for (u32 page = 0; page < 8; ++page)
{
SCOPED_TRACE(page);
EXPECT_TRUE(std::ranges::equal(std::span(after[page]).subspan(0x800, 0x30),
std::span(expected[page]).subspan(0x800, 0x30)));
EXPECT_TRUE(
std::ranges::equal(CalculateECC(after[page]), std::span(after[page]).subspan(0x830, 16)));
}
}
} // namespace IOS::LLE
@@ -0,0 +1,441 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <iomanip>
#include <memory>
#include <optional>
#include <gtest/gtest.h>
#include "Common/Swap.h"
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/ARMCore.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
namespace IOS::LLE
{
#if defined(_M_X86_64)
namespace
{
// Only code and page tables use synthetic RAM. Data reads exercise the actual SRAM/boot0 bus,
// without initializing any firmware, NAND image, profile, or live peripheral scheduler.
class SRAMReadBus final : public ARMBus
{
public:
SRAMReadBus() : memory(Core::System::GetInstance())
{
memory.Reset();
for (u32 i = 0; i < 0x18000; ++i)
memory.GetFastmemSRAMBase()[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0xa5);
}
u8 Read8(u32 address) override
{
if (address < ram.size())
return ram[address];
++data_reads;
return memory.Read8(address);
}
u16 Read16(u32 address) override
{
if (address < ram.size())
return ARMBus::Read16(address);
++data_reads;
return memory.Read16(address);
}
u32 Read32(u32 address) override
{
if (address < ram.size())
return ARMBus::Read32(address);
++data_reads;
return memory.Read32(address);
}
void Write8(u32 address, u8 value) override
{
if (address < ram.size())
ram[address] = value;
else
memory.Write8(address, value);
}
void Write16(u32 address, u16 value) override
{
if (address < ram.size())
ARMBus::Write16(address, value);
else
memory.Write16(address, value);
}
void Write32(u32 address, u32 value) override
{
if (address < ram.size())
ARMBus::Write32(address, value);
else
memory.Write32(address, value);
}
u8* GetFastmemBase() const override { return const_cast<u8*>(ram.data()); }
u8* GetFastmemSRAMBase() const override { return memory.GetFastmemSRAMBase(); }
const bool* GetFastmemBoot0Mapped() const override { return memory.GetFastmemBoot0Mapped(); }
const bool* GetFastmemSRAMSplitMode() const override { return memory.GetFastmemSRAMSplitMode(); }
const u8* GetDirectMemoryPointer(u32 address, u32 size) const override
{
if (address < ram.size() && size <= ram.size() - address)
return ram.data() + address;
return memory.GetDirectMemoryPointer(address, size);
}
void SetMapping(bool split, bool boot0)
{
memory.Write32(0x0d800060, split ? 0x20 : 0); // HW_SRNPROT
memory.Write32(0x0d80018c, boot0 ? 0 : 0x1000); // HW_BOOT0
}
void InstallReads(bool thumb, bool big_endian)
{
if (thumb)
{
constexpr std::array<u16, 6> code = {
0x6830, // ldr r0, [r6]
0x7831, // ldrb r1, [r6]
0x8832, // ldrh r2, [r6]
0x57f3, // ldrsb r3, [r6, r7]
0x5ff4, // ldrsh r4, [r6, r7]
0xe7fe, // b .
};
for (u32 i = 0; i < code.size(); ++i)
Write16(i * 2, big_endian ? code[i] : Common::swap16(code[i]));
}
else
{
constexpr std::array<u32, 6> code = {
0xe5960000, // ldr r0, [r6]
0xe5d61000, // ldrb r1, [r6]
0xe1d620b0, // ldrh r2, [r6]
0xe1d630d0, // ldrsb r3, [r6]
0xe1d640f0, // ldrsh r4, [r6]
0xeafffffe, // b .
};
for (u32 i = 0; i < code.size(); ++i)
Write32(i * 4, big_endian ? code[i] : Common::swap32(code[i]));
}
}
void InstallStores(bool thumb, bool big_endian)
{
if (thumb)
{
constexpr std::array<u16, 4> code = {
0x6030, // str r0, [r6]
0x7131, // strb r1, [r6, #4]
0x80f2, // strh r2, [r6, #6]
0xe7fe, // b .
};
for (u32 i = 0; i < code.size(); ++i)
Write16(i * 2, big_endian ? code[i] : Common::swap16(code[i]));
}
else
{
constexpr std::array<u32, 4> code = {
0xe5860000, // str r0, [r6]
0xe5c61004, // strb r1, [r6, #4]
0xe1c620b6, // strh r2, [r6, #6]
0xeafffffe, // b .
};
for (u32 i = 0; i < code.size(); ++i)
Write32(i * 4, big_endian ? code[i] : Common::swap32(code[i]));
}
}
StarletMemory memory;
std::array<u8, 0x8000> ram{};
u64 data_reads = 0;
};
class StarletSRAMRead : public testing::TestWithParam<bool>
{
protected:
void SetUp() override
{
Core::DeclareAsCPUThread();
Core::System::GetInstance().GetWiiIPC().Reset();
}
static void Prepare(ARMCore& core, bool thumb, bool big_endian, u32 address)
{
if (core.IsBigEndian() != big_endian)
core.SetBigEndian(big_endian);
core.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | (thumb ? ARMCore::CPSR_T : 0));
for (u32 i = 0; i < 16; ++i)
core.SetRegister(i, 0);
core.SetRegister(6, address);
}
};
TEST_P(StarletSRAMRead, NewlyProfiledPagesAvoidDataBusCalls)
{
for (const bool thumb : {false, true})
{
for (const bool big_endian : {false, true})
{
auto bus = std::make_unique<SRAMReadBus>();
bus->SetMapping(true, false);
bus->InstallReads(thumb, big_endian);
ARMCore core(*bus, GetParam());
core.SetJitEnabled(true);
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfffe0000u})
{
for (const u32 page : {0x01u, 0x02u, 0x04u, 0x13u, 0x15u, 0x16u, 0x1au, 0x1cu, 0x1du})
{
const u32 address = base + page * 0x1000;
SCOPED_TRACE(testing::Message() << "thumb=" << thumb << " be=" << big_endian
<< " address=" << std::hex << address);
Prepare(core, thumb, big_endian, address);
bus->data_reads = 0;
ASSERT_EQ(core.RunCycles(6), 6u);
ASSERT_EQ(core.GetLastUndefinedInstruction(), 0u);
ASSERT_EQ(bus->data_reads, 0u);
EXPECT_EQ(core.GetRegister(0), big_endian ? bus->memory.Read32(address) :
Common::swap32(bus->memory.Read32(address)));
}
}
EXPECT_EQ(core.GetJitSlowReadCount(), 0u);
EXPECT_EQ(core.GetJitFallbackInstructionCount(), 0u);
EXPECT_EQ(core.GetJitCompiledBlockCount(), 1u);
}
}
}
TEST_P(StarletSRAMRead, SizedNativeStoresMatchInterpreterAcrossMappings)
{
for (const bool thumb : {false, true})
{
for (const bool big_endian : {false, true})
{
auto reference_bus = std::make_unique<SRAMReadBus>();
auto native_bus = std::make_unique<SRAMReadBus>();
reference_bus->InstallStores(thumb, big_endian);
native_bus->InstallStores(thumb, big_endian);
ARMCore reference(*reference_bus);
ARMCore native(*native_bus, GetParam());
native.SetJitEnabled(true);
for (const bool split : {false, true, false})
{
for (const bool boot0 : {false, true, false})
{
reference_bus->SetMapping(split, boot0);
native_bus->SetMapping(split, boot0);
for (const u32 base : {0x0d400000U, 0xfff00000U, 0xfffe0000U})
{
for (u32 page = 0; page < 32; ++page)
{
for (const u32 offset : {0U, 1U, 2U, 3U, 0xff8U, 0xffcU, 0xffeU, 0xfffU})
{
const u32 address = base + page * 0x1000 + offset;
if (address > 0xfffffff7U)
continue; // Do not wrap a test data store into synthetic guest code.
SCOPED_TRACE(testing::Message()
<< "thumb=" << thumb << " be=" << big_endian << " split=" << split
<< " boot0=" << boot0 << " address=" << std::hex << address);
for (ARMCore* core : {&reference, &native})
{
Prepare(*core, thumb, big_endian, address);
core->SetRegister(0, 0x12345678);
core->SetRegister(1, 0x9abcdef0);
core->SetRegister(2, 0x7e3dac12);
ASSERT_EQ(core->RunCycles(4), 4U);
ASSERT_EQ(core->GetLastUndefinedInstruction(), 0U);
}
for (u32 reg = 0; reg < 16; ++reg)
ASSERT_EQ(native.GetRegister(reg), reference.GetRegister(reg));
ASSERT_EQ(native.GetCPSR(), reference.GetCPSR());
ASSERT_TRUE(std::equal(reference_bus->memory.GetFastmemSRAMBase(),
reference_bus->memory.GetFastmemSRAMBase() + 0x18000,
native_bus->memory.GetFastmemSRAMBase()));
for (u32 byte = 0; byte < 8; ++byte)
ASSERT_EQ(native_bus->Read8(address + byte),
reference_bus->Read8(address + byte));
}
}
}
}
}
}
}
}
TEST_P(StarletSRAMRead, MatchesInterpreterAcrossLiveMappingsAliasesAndBoundaries)
{
for (const bool thumb : {false, true})
{
for (const bool big_endian : {false, true})
{
auto interpreter_bus = std::make_unique<SRAMReadBus>();
auto jit_bus = std::make_unique<SRAMReadBus>();
interpreter_bus->InstallReads(thumb, big_endian);
jit_bus->InstallReads(thumb, big_endian);
ARMCore interpreter(*interpreter_bus);
ARMCore jit(*jit_bus, GetParam());
jit.SetJitEnabled(true);
std::array<u8, 0x18000> original;
std::copy_n(jit_bus->GetFastmemSRAMBase(), original.size(), original.begin());
std::optional<u64> compiled_blocks;
// Toggle the hardware state while reusing the same compiled code: guards must consult
// live boot0/split latches, not their values at compilation time.
for (const bool split : {false, true, false})
{
for (const bool boot0 : {false, true, false})
{
interpreter_bus->SetMapping(split, boot0);
jit_bus->SetMapping(split, boot0);
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfffe0000u})
{
for (u32 page = 0; page < 32; ++page)
{
for (const u32 offset :
{0u, 1u, 2u, 3u, 0x3fdu, 0x3feu, 0x3ffu, 0xffcu, 0xffdu, 0xffeu, 0xfffu})
{
const u32 address = base + page * 0x1000 + offset;
if (address > 0xfffffffcu)
continue; // Exclude address-space wrap into the synthetic code buffer.
SCOPED_TRACE(testing::Message()
<< "thumb=" << thumb << " be=" << big_endian << " split=" << split
<< " boot0=" << boot0 << " address=" << std::hex << address);
Prepare(interpreter, thumb, big_endian, address);
Prepare(jit, thumb, big_endian, address);
ASSERT_EQ(interpreter.RunCycles(6), 6u);
ASSERT_EQ(jit.RunCycles(6), 6u);
ASSERT_EQ(jit.GetLastUndefinedInstruction(), 0u);
for (u32 reg = 0; reg < 16; ++reg)
ASSERT_EQ(jit.GetRegister(reg), interpreter.GetRegister(reg)) << "r" << reg;
ASSERT_EQ(jit.GetCPSR(), interpreter.GetCPSR());
}
}
}
// Unaligned fallbacks also compile continuations. Once the first boundary sweep has
// warmed them, changing the hardware mapping must reuse those blocks, not rebuild them.
if (!compiled_blocks)
compiled_blocks = jit.GetJitLifetimeCompiledBlockCount();
else
EXPECT_EQ(jit.GetJitLifetimeCompiledBlockCount(), *compiled_blocks);
}
}
EXPECT_TRUE(std::equal(original.begin(), original.end(), jit_bus->GetFastmemSRAMBase()));
EXPECT_TRUE(
std::equal(original.begin(), original.end(), interpreter_bus->GetFastmemSRAMBase()));
}
}
}
TEST_P(StarletSRAMRead, PreservedBlockUsesRemappedSRAMAfterGuestTLBInvalidation)
{
auto bus = std::make_unique<SRAMReadBus>();
bus->SetMapping(true, false);
bus->Write32(0, 0xe5960000); // ldr r0, [r6]
bus->Write32(4, 0xeafffffe); // b .
bus->Write32(0x40, 0xee080f17); // mcr p15, 0, r0, c8, c7, 0 (invalidate unified TLB)
bus->Write32(0x4000, 0x00000c02); // Identity section for code/page tables.
bus->Write32(0x4c00, 0x00005001); // VA 0x30000000 -> coarse page table at 0x5000.
bus->Write32(0x5000, 0x0d404ff2); // Small page -> SRAM B, aperture page 04.
bus->memory.Write32(0x0d404000, 0x12345678);
bus->memory.Write32(0xffff6000, 0x87654321);
ARMCore core(*bus, GetParam());
core.GetCP15State().translation_table_base = 0x4000;
core.GetCP15State().domain_access_control = 3;
core.GetCP15State().control |= 1;
core.SetRegister(6, 0x30000000);
core.SetJitEnabled(true);
ASSERT_EQ(core.RunCycles(2), 2u);
ASSERT_EQ(core.GetRegister(0), 0x12345678u);
const auto old_blocks = core.GetJitCompiledBlockCount();
bus->Write32(0x5000, 0xffff6ff2); // Remap to SRAM A, aperture page 16.
core.SetRegister(15, 0x40);
ASSERT_EQ(core.RunCycles(1), 1u);
core.SetRegister(15, 0);
ASSERT_EQ(core.RunCycles(2), 2u);
EXPECT_EQ(core.GetRegister(0), 0x87654321u);
EXPECT_EQ(core.GetJitCompiledBlockCount(), old_blocks + 1);
EXPECT_EQ(core.GetJitSlowReadCount(), 0u);
}
TEST_P(StarletSRAMRead, OptionalProfilingPreservesBusCountsResultsAndReset)
{
for (const bool thumb : {false, true})
{
for (const bool big_endian : {false, true})
{
for (const u32 address : {0xfff1f000U, 0x0d800214U})
{
auto bus = std::make_unique<SRAMReadBus>();
auto reference_bus = std::make_unique<SRAMReadBus>();
for (SRAMReadBus* target : {bus.get(), reference_bus.get()})
{
target->SetMapping(true, false);
target->memory.Write32(address, 0x89abcdef);
if (thumb)
{
target->Write16(0, big_endian ? 0x6830 : Common::swap16(0x6830)); // ldr r0,[r6]
target->Write16(2, big_endian ? 0x6030 : Common::swap16(0x6030)); // str r0,[r6]
target->Write16(4, big_endian ? 0xe7fc : Common::swap16(0xe7fc)); // b 0
}
else
{
target->Write32(0, big_endian ? 0xe5960000 : Common::swap32(0xe5960000));
target->Write32(4, big_endian ? 0xe5860000 : Common::swap32(0xe5860000));
target->Write32(8, big_endian ? 0xeafffffc : Common::swap32(0xeafffffc));
}
}
ARMCore core(*bus, GetParam());
ARMCore reference(*reference_bus);
EXPECT_FALSE(reference.IsJitMemoryProfilingEnabled());
core.SetJitEnabled(true);
// Recompile after reset and endian changes with the same host-only profiling choice.
for (int reset = 0; reset < 2; ++reset)
{
core.Reset();
reference.Reset();
EXPECT_EQ(core.IsJitMemoryProfilingEnabled(), GetParam());
const u64 reads_before = core.GetJitSlowReadCount();
const u64 writes_before = core.GetJitSlowWriteCount();
for (ARMCore* cpu : {&core, &reference})
{
Prepare(*cpu, thumb, big_endian, address);
ASSERT_EQ(cpu->RunCycles(768), 768U);
}
EXPECT_EQ(core.GetJitSlowReadCount() - reads_before, 256U);
EXPECT_EQ(core.GetJitSlowWriteCount() - writes_before, 256U);
for (u32 reg = 0; reg < 16; ++reg)
EXPECT_EQ(core.GetRegister(reg), reference.GetRegister(reg));
EXPECT_EQ(core.GetCPSR(), reference.GetCPSR());
EXPECT_EQ(bus->memory.Read32(address), reference_bus->memory.Read32(address));
EXPECT_EQ(bus->memory.Read32(address), 0x89abcdefU);
const auto samples = core.TakeHotJitSlowMemorySamples(8);
if (GetParam())
{
ASSERT_EQ(samples.size(), 2U);
for (const auto& [key, count] : samples)
{
EXPECT_EQ(static_cast<u32>(key), address);
EXPECT_EQ(count, 1U);
}
EXPECT_GT(address == 0xfff1f000 ? core.GetJitSlowSRAMAccessCount() :
core.GetJitSlowMMIOAccessCount(),
0U);
}
else
{
EXPECT_TRUE(samples.empty());
EXPECT_EQ(core.GetJitSlowSRAMAccessCount(), 0U);
EXPECT_EQ(core.GetJitSlowMMIOAccessCount(), 0U);
EXPECT_EQ(core.GetJitSlowSRAMPageAccessCount(0x1f), 0U);
}
}
}
}
}
}
INSTANTIATE_TEST_SUITE_P(MemoryProfiling, StarletSRAMRead, testing::Bool());
} // namespace
#endif
} // namespace IOS::LLE
@@ -0,0 +1,128 @@
// Copyright 2026 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <iomanip>
#include <memory>
#include <optional>
#include <gtest/gtest.h>
#include "Core/Core.h"
#include "Core/HW/WII_IPC.h"
#include "Core/IOS/Starlet/StarletMemory.h"
#include "Core/System.h"
namespace IOS::LLE
{
namespace
{
// Deliberately branch-based reference, independent of the optimized aperture-bit swap.
std::optional<u32> MapSRAMByte(u32 address, bool split, bool boot0)
{
if (!(address >= 0x0d400000 && address < 0x0d420000) && address < 0xfff00000)
return std::nullopt;
const u32 window = address & 0x1ffff;
if (boot0 && address >= 0xfffe0000 && (split ? window < 0x10000 : window >= 0x10000))
return std::nullopt;
if (!split)
return window < 0x18000 ? std::optional(window) : std::nullopt;
if (window < 0x8000)
return 0x10000 + window;
if (window >= 0x10000)
return window - 0x10000;
return std::nullopt;
}
class StarletSRAMWrite : public testing::Test
{
protected:
void SetUp() override
{
Core::DeclareAsCPUThread();
Core::System::GetInstance().GetWiiIPC().Reset();
}
static void SetMapping(StarletMemory& memory, bool split, bool boot0)
{
memory.Write32(0x0d800060, split ? 0x20 : 0);
memory.Write32(0x0d80018c, boot0 ? 0 : 0x1000);
}
};
TEST_F(StarletSRAMWrite, EveryApertureByteMatchesIndependentMapping)
{
auto memory = std::make_unique<StarletMemory>(Core::System::GetInstance());
memory->Reset();
u8* const sram = memory->GetFastmemSRAMBase();
for (u32 i = 0; i < StarletMemory::SRAM_SIZE; ++i)
sram[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0xa5);
for (const bool split : {false, true})
{
for (const bool boot0 : {false, true})
{
SetMapping(*memory, split, boot0);
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfffe0000u})
{
for (u32 i = 0; i < 0x20000; ++i)
{
const u32 address = base + i;
const auto offset = MapSRAMByte(address, split, boot0);
// Reset leaves the synthetic boot ROM zero-filled. Holes also read zero.
ASSERT_EQ(memory->Read8(address), offset ? sram[*offset] : 0)
<< "address=" << std::hex << address << " split=" << split << " boot0=" << boot0;
}
}
}
}
}
TEST_F(StarletSRAMWrite, WideStoresMatchByteOracleAcrossLiveMappingsAndAllMirrors)
{
auto memory = std::make_unique<StarletMemory>(Core::System::GetInstance());
memory->Reset();
std::array<u8, StarletMemory::SRAM_SIZE> expected;
for (u32 i = 0; i < expected.size(); ++i)
expected[i] = static_cast<u8>((i * 13) ^ (i >> 8) ^ 0xa5);
std::copy(expected.begin(), expected.end(), memory->GetFastmemSRAMBase());
// Toggle back, too: no cached host pointer may survive changes to SM/boot0.
for (const bool split : {false, true, false})
{
for (const bool boot0 : {false, true, false})
{
SetMapping(*memory, split, boot0);
for (const u32 base : {0x0d400000u, 0xfff00000u, 0xfff20000u, 0xfff40000u, 0xfff60000u,
0xfff80000u, 0xfffa0000u, 0xfffc0000u, 0xfffe0000u})
{
for (u32 page = 0; page < 32; ++page)
{
for (const u32 in_page : {0u, 1u, 2u, 3u, 0xffcu, 0xffdu, 0xffeu, 0xfffu})
{
const u32 address = base + page * 0x1000 + in_page;
for (const u32 width : {2u, 4u})
{
if (address > 0xffffffffu - (width - 1))
continue; // Do not wrap into uninitialized MEM1.
const u32 value = (address * 0x12345u) ^ (width == 2 ? 0x89abcdefu : 0x76543210u);
for (u32 byte = 0; byte < width; ++byte)
{
if (const auto offset = MapSRAMByte(address + byte, split, boot0))
expected[*offset] = static_cast<u8>(value >> ((width - 1 - byte) * 8));
}
if (width == 2)
memory->Write16(address, static_cast<u16>(value));
else
memory->Write32(address, value);
ASSERT_TRUE(
std::equal(expected.begin(), expected.end(), memory->GetFastmemSRAMBase()))
<< "address=" << std::hex << address << " width=" << width << " split=" << split
<< " boot0=" << boot0;
}
}
}
}
}
}
}
} // namespace
} // namespace IOS::LLE