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:
1 parent
b931671fde
commit
c230971f07
64 files changed
+6488
-238
No files matched your search
@@ -122,6 +122,8 @@ add_library(common
|
||||
OneShotEvent.h
|
||||
PcapFile.cpp
|
||||
PcapFile.h
|
||||
PerformanceTrace.cpp
|
||||
PerformanceTrace.h
|
||||
Profiler.cpp
|
||||
Profiler.h
|
||||
Projection.h
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user