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
@@ -71,3 +71,7 @@ CMakeUserPresets.json
|
||||
/.codex-tools/
|
||||
/.starlet-profile/
|
||||
/yaya48-starlet-log.txt
|
||||
/Run-Wii-Formatter-Test.ps1
|
||||
|
||||
# Generated bytecode from the performance analysis helpers
|
||||
/Tools/PerformanceTrace/__pycache__/
|
||||
+13
-2
@@ -5,10 +5,14 @@ param(
|
||||
|
||||
[switch]$DisableJIT,
|
||||
|
||||
[switch]$ProfileJitMemory,
|
||||
|
||||
[switch]$SafeTextureCache = $true,
|
||||
|
||||
[switch]$Ethernet,
|
||||
|
||||
[switch]$PerformanceTrace,
|
||||
|
||||
[switch]$Wait
|
||||
)
|
||||
|
||||
@@ -27,6 +31,7 @@ $dolphinArguments = @(
|
||||
'-u', $userPath,
|
||||
'-n', '0000000100000002',
|
||||
'-C', "Dolphin.Core.WiiStarletJIT=$(-not $DisableJIT)",
|
||||
'-C', "Dolphin.Core.WiiStarletJITMemoryProfiling=$($ProfileJitMemory.IsPresent)",
|
||||
'-C', "Dolphin.Core.WiiLLEEthernet=$($Ethernet.IsPresent)",
|
||||
# Keep the next BootMii run diagnostic rather than observational. BootMii reports its loader
|
||||
# stages and panic codes through GPIO bits 16-23; the Starlet bus logs those writes under IOS.
|
||||
@@ -47,8 +52,14 @@ if ($Ethernet) {
|
||||
$dolphinArguments += @('-C', 'Logger.Logs.IOS_USB=True', '-C', 'Logger.Logs.IOS_NET=True')
|
||||
}
|
||||
|
||||
$dolphin = Start-Process -FilePath $dolphinPath -ArgumentList $dolphinArguments `
|
||||
-WorkingDirectory $repoRoot -PassThru
|
||||
$priorPerformanceTrace = [Environment]::GetEnvironmentVariable('DOLPHIN_PERF_TRACE', 'Process')
|
||||
try {
|
||||
if ($PerformanceTrace) { $env:DOLPHIN_PERF_TRACE = '1' }
|
||||
$dolphin = Start-Process -FilePath $dolphinPath -ArgumentList $dolphinArguments `
|
||||
-WorkingDirectory $repoRoot -PassThru
|
||||
} finally {
|
||||
[Environment]::SetEnvironmentVariable('DOLPHIN_PERF_TRACE', $priorPerformanceTrace, 'Process')
|
||||
}
|
||||
|
||||
$dolphin.PriorityClass = 'High'
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,157 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$CaptureDirectory,
|
||||
[Parameter(Mandatory)][string]$OutputDirectory,
|
||||
[string]$PythonExecutable,
|
||||
[ValidateRange(0,2147483647)][int]$NativePID=0,
|
||||
[string]$NativePDB,
|
||||
[string]$XperfRangeStart,
|
||||
[string]$XperfRangeEnd
|
||||
)
|
||||
# Read-only input: qualify a synthetic capture, or inspect one native Dolphin PID.
|
||||
# Output is a new directory; no emulator, NAND or WPR session is touched.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (($NativePID -eq 0) -ne (-not $NativePDB)) {
|
||||
throw 'For native analysis pass both -NativePID and -NativePDB.'
|
||||
}
|
||||
if ([bool]$XperfRangeStart -ne [bool]$XperfRangeEnd) {
|
||||
throw 'Pass both -XperfRangeStart and -XperfRangeEnd, or neither.'
|
||||
}
|
||||
if ($XperfRangeStart -and -not $NativePID) {
|
||||
throw 'Time-window export is only supported for exploratory native analysis.'
|
||||
}
|
||||
if ($PythonExecutable) {
|
||||
$p1Python = (Resolve-Path -LiteralPath $PythonExecutable).Path
|
||||
} else {
|
||||
$p1Candidates = @(Get-Command python.exe -All -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Source -notlike '*\WindowsApps\*' } |
|
||||
Select-Object -ExpandProperty Source)
|
||||
$p1Candidates += 'C:\msys64\mingw64\bin\python.exe'
|
||||
foreach ($p1Candidate in $p1Candidates) {
|
||||
if ($p1Candidate -and (Test-Path -LiteralPath $p1Candidate)) {
|
||||
$p1Python = $p1Candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $p1Python) {
|
||||
throw 'Python was not found before analysis. Pass -PythonExecutable with the path to a working python.exe.'
|
||||
}
|
||||
& $p1Python --version | Out-Null
|
||||
if ($LASTEXITCODE) { throw "Python could not start: $p1Python" }
|
||||
function ConvertTo-P1WindowsArgument([string]$Value) {
|
||||
$p1Quoted = '"'
|
||||
$p1Slashes = 0
|
||||
foreach ($p1Char in $Value.ToCharArray()) {
|
||||
if ($p1Char -eq '\') { ++$p1Slashes; continue }
|
||||
if ($p1Char -eq '"') {
|
||||
$p1Quoted += ('\' * (2 * $p1Slashes + 1)) + '"'
|
||||
$p1Slashes = 0
|
||||
continue
|
||||
}
|
||||
$p1Quoted += ('\' * $p1Slashes) + $p1Char
|
||||
$p1Slashes = 0
|
||||
}
|
||||
return $p1Quoted + ('\' * (2 * $p1Slashes)) + '"'
|
||||
}
|
||||
$p1Repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||
$p1Input = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($CaptureDirectory)) {
|
||||
$CaptureDirectory
|
||||
} else {
|
||||
Join-Path (Get-Location).ProviderPath $CaptureDirectory
|
||||
}))
|
||||
$p1Output = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($OutputDirectory)) {
|
||||
$OutputDirectory
|
||||
} else {
|
||||
Join-Path $p1Repo $OutputDirectory
|
||||
}))
|
||||
if (Test-Path -LiteralPath $p1Output) { throw 'Use a NEW analysis directory; no results are overwritten.' }
|
||||
$p1ETL = Join-Path $p1Input 'capture.etl'
|
||||
$p1ManifestPath = Join-Path $p1Input 'manifest.json'
|
||||
$p1StdoutPath = Join-Path $p1Input 'probe.stdout.log'
|
||||
$p1Decoder = Join-Path $p1Repo '.starlet-profile/P1-tools/DecodeP1.exe'
|
||||
$p1RequiredInputs = @($p1ETL, $p1ManifestPath, $p1Decoder)
|
||||
if ($NativePID) {
|
||||
$NativePDB = (Resolve-Path -LiteralPath $NativePDB).Path
|
||||
$p1RequiredInputs += $NativePDB
|
||||
} else {
|
||||
$p1RequiredInputs += $p1StdoutPath
|
||||
}
|
||||
foreach ($p1Required in $p1RequiredInputs) {
|
||||
if (-not (Test-Path -LiteralPath $p1Required)) { throw "Missing analysis input: $p1Required" }
|
||||
}
|
||||
$p1Drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($p1Output))
|
||||
$p1RequiredFree = if ($NativePID) { 20GB } else { 7GB }
|
||||
if ($p1Drive.AvailableFreeSpace -lt $p1RequiredFree) {
|
||||
throw "Need at least $([math]::Round($p1RequiredFree / 1GB)) GiB free for offline analysis."
|
||||
}
|
||||
New-Item -ItemType Directory -Path $p1Output | Out-Null
|
||||
Copy-Item -LiteralPath $p1ManifestPath -Destination $p1Output
|
||||
if (-not $NativePID) { Copy-Item -LiteralPath $p1StdoutPath -Destination $p1Output }
|
||||
$p1Manifest = Get-Content -LiteralPath $p1ManifestPath -Raw | ConvertFrom-Json
|
||||
$p1TargetPID = if ($NativePID) { $NativePID } else { $p1Manifest.ProbePID }
|
||||
if (-not $p1TargetPID) { throw 'Target PID missing from capture manifest.' }
|
||||
$p1Native = Join-Path $p1Output 'native-events.jsonl'
|
||||
$p1DecodeText = & $p1Decoder $p1ETL $p1Native
|
||||
$p1DecodeExit = $LASTEXITCODE
|
||||
$p1DecodeText | Out-File -LiteralPath (Join-Path $p1Output 'decoder-summary.json') -Encoding utf8
|
||||
if ($p1DecodeExit) { throw 'TDH native decode or ETW loss check failed; see decoder-summary.json.' }
|
||||
|
||||
$p1Csv = Join-Path $p1Output 'xperf-symbols.csv'
|
||||
$p1PriorSymbols = [Environment]::GetEnvironmentVariable('_NT_SYMBOL_PATH', 'Process')
|
||||
$p1PriorCache = [Environment]::GetEnvironmentVariable('_NT_SYMCACHE_PATH', 'Process')
|
||||
try {
|
||||
$env:_NT_SYMBOL_PATH = if ($NativePID) { Split-Path $NativePDB -Parent } else {
|
||||
Split-Path $p1Manifest.ProbePDB -Parent
|
||||
}
|
||||
$env:_NT_SYMCACHE_PATH = Join-Path $p1Output 'symcache'
|
||||
$p1Info = [Diagnostics.ProcessStartInfo]::new((Get-Command xperf.exe).Source)
|
||||
$p1Info.UseShellExecute = $false
|
||||
$p1Info.CreateNoWindow = $true
|
||||
$p1Info.RedirectStandardOutput = $true
|
||||
$p1Info.RedirectStandardError = $true
|
||||
$p1Info.WorkingDirectory = $p1Repo
|
||||
$p1XperfArgs = @('-i', $p1ETL, '-o', $p1Csv, '-symbols', '-target',
|
||||
'machine', '-quiet', '-a', 'dumper')
|
||||
if ($XperfRangeStart) {
|
||||
$p1XperfArgs += @('-range', $XperfRangeStart, $XperfRangeEnd)
|
||||
[pscustomobject]@{ Start=$XperfRangeStart; End=$XperfRangeEnd;
|
||||
Note='Xperf CPU samples are limited to this window; native scopes and counters span the full ETL.' } |
|
||||
ConvertTo-Json | Out-File -LiteralPath (Join-Path $p1Output 'analysis-range.json') -Encoding utf8
|
||||
}
|
||||
$p1Info.Arguments = ($p1XperfArgs | ForEach-Object {
|
||||
ConvertTo-P1WindowsArgument $_
|
||||
}) -join ' '
|
||||
$p1Process = [Diagnostics.Process]::Start($p1Info)
|
||||
$p1Stdout = $p1Process.StandardOutput.ReadToEndAsync()
|
||||
$p1Stderr = $p1Process.StandardError.ReadToEndAsync()
|
||||
$p1TimeoutMs = if ($NativePID) { 900000 } else { 180000 }
|
||||
if (-not $p1Process.WaitForExit($p1TimeoutMs)) {
|
||||
$p1Process.Kill() # Only the xperf process created by this analysis script.
|
||||
$p1Process.WaitForExit()
|
||||
throw "Owned Xperf decoder exceeded $($p1TimeoutMs / 1000) seconds; no input ETL was modified."
|
||||
}
|
||||
$p1Stdout.GetAwaiter().GetResult() | Out-File `
|
||||
-LiteralPath (Join-Path $p1Output 'xperf-stdout.log') -Encoding utf8
|
||||
$p1Stderr.GetAwaiter().GetResult() | Out-File `
|
||||
-LiteralPath (Join-Path $p1Output 'xperf-warnings.log') -Encoding utf8
|
||||
if ($p1Process.ExitCode) { throw 'Xperf kernel decode failed; see xperf-warnings.log.' }
|
||||
} finally {
|
||||
[Environment]::SetEnvironmentVariable('_NT_SYMBOL_PATH', $p1PriorSymbols, 'Process')
|
||||
[Environment]::SetEnvironmentVariable('_NT_SYMCACHE_PATH', $p1PriorCache, 'Process')
|
||||
}
|
||||
$p1Combined = Join-Path $p1Output 'combined.jsonl'
|
||||
$p1XperfSummary = Join-Path $p1Output 'xperf-summary.json'
|
||||
& $p1Python (Join-Path $PSScriptRoot 'xperf_to_p1.py') $p1Csv $p1Native `
|
||||
--pid $p1TargetPID --output $p1Combined --summary $p1XperfSummary
|
||||
if ($LASTEXITCODE) { throw 'Xperf/native clock alignment failed.' }
|
||||
if ($NativePID) {
|
||||
& $p1Python (Join-Path $PSScriptRoot 'analyze_p1.py') $p1Combined `
|
||||
--pid $p1TargetPID --output (Join-Path $p1Output 'native-report.json')
|
||||
if ($LASTEXITCODE) { throw 'Native offline analysis failed.' }
|
||||
Write-Output "Exploratory native report: $p1Output\native-report.json (NOT qualified as a complete Wii-update bottleneck report)."
|
||||
} else {
|
||||
& $p1Python (Join-Path $PSScriptRoot 'qualify_p1.py') $p1Output `
|
||||
--events $p1Combined --xperf-summary $p1XperfSummary
|
||||
if ($LASTEXITCODE) { throw 'Local WPR qualification failed; see qualification.json.' }
|
||||
Write-Output "Local WPR qualification finished: $p1Output\qualification.json"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
param([Parameter(Mandatory)][string]$OutputDirectory)
|
||||
$ErrorActionPreference='Stop'
|
||||
$decoderOut=[IO.Path]::GetFullPath($(if([IO.Path]::IsPathRooted($OutputDirectory)){
|
||||
$OutputDirectory
|
||||
}else{
|
||||
Join-Path (Get-Location).ProviderPath $OutputDirectory
|
||||
}))
|
||||
New-Item -ItemType Directory -Force -Path $decoderOut | Out-Null
|
||||
$decoderSource=Join-Path $PSScriptRoot 'DecodeP1.cpp'
|
||||
$decoderSetup='call "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\Tools\VsDevCmd.bat" -arch=x64 -host_arch=x64 >nul'
|
||||
$decoderCommand=$decoderSetup+' && cl /nologo /EHsc /O2 /std:c++20 /DUNICODE /D_UNICODE "'+$decoderSource+'" /Fo"'+(Join-Path $decoderOut 'DecodeP1.obj')+'" /Fe"'+(Join-Path $decoderOut 'DecodeP1.exe')+'" /link tdh.lib advapi32.lib'
|
||||
cmd.exe /d /s /c $decoderCommand
|
||||
if($LASTEXITCODE) { throw 'Decoder compilation failed' }
|
||||
@@ -0,0 +1,48 @@
|
||||
param([switch]$ConfigureOnly, [switch]$CleanBeforeBuild)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$p1Repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||
$p1Build = Join-Path $p1Repo '.starlet_diag_p1'
|
||||
$p1VS = 'C:\Program Files\Microsoft Visual Studio\18\Community'
|
||||
$p1CMake = Join-Path $p1VS 'Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
|
||||
$p1Ninja = Join-Path $p1VS 'Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja\ninja.exe'
|
||||
$p1Setup = 'set VSLANG=1033&& call "' + (Join-Path $p1VS 'Common7\Tools\VsDevCmd.bat') + '" -arch=x64 -host_arch=x64 >nul'
|
||||
$p1Configure = $p1Setup + ' && "' + $p1CMake + '" -S "' + $p1Repo + '" -B "' + $p1Build + '" -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_MAKE_PROGRAM="' + $p1Ninja + '" -DENABLE_QT=OFF -DENABLE_NOGUI=ON -DENABLE_TESTS=ON -DENABLE_VULKAN=OFF -DENABLE_LTO=OFF -DENABLE_LLVM=OFF -DUSE_SYSTEM_LIBS=OFF'
|
||||
cmd.exe /d /s /c $p1Configure
|
||||
if ($LASTEXITCODE) { throw 'P1 configuration failed.' }
|
||||
|
||||
# CMake can decode French MSVC /showIncludes through the OEM code page even though
|
||||
# cl prints UTF-8 nonbreaking spaces. Then Ninja silently records no header deps.
|
||||
$p1PrefixFixed = $false
|
||||
foreach ($p1Info in Get-ChildItem -LiteralPath (Join-Path $p1Build 'CMakeFiles') -Recurse -File -Filter 'CMake*Compiler.cmake') {
|
||||
$p1Text = [IO.File]::ReadAllText($p1Info.FullName)
|
||||
$p1Corrected = $p1Text.Replace('Remarque : inclusion du fichier : ',
|
||||
'Remarque' + [char]0xa0 + ': inclusion du fichier' + [char]0xa0 + ': ')
|
||||
if ($p1Corrected -cne $p1Text) {
|
||||
[IO.File]::WriteAllText($p1Info.FullName, $p1Corrected, [Text.UTF8Encoding]::new($false))
|
||||
$p1PrefixFixed = $true
|
||||
}
|
||||
}
|
||||
if ($p1PrefixFixed) {
|
||||
cmd.exe /d /s /c $p1Configure
|
||||
if ($LASTEXITCODE) { throw 'P1 dependency prefix repair failed.' }
|
||||
}
|
||||
if ($ConfigureOnly) { return }
|
||||
if ($CleanBeforeBuild -and [IO.Path]::GetFullPath($p1Build) -cne
|
||||
[IO.Path]::GetFullPath((Join-Path $p1Repo '.starlet_diag_p1'))) {
|
||||
throw 'Unexpected build target: refusing clean.'
|
||||
}
|
||||
$p1Clean = if ($CleanBeforeBuild) { ' --clean-first' } else { '' }
|
||||
$p1Compile = $p1Setup + ' && "' + $p1CMake + '" --build "' + $p1Build + '"' + $p1Clean + ' --target dolphin-nogui tests TraceProbeP1 -j 6'
|
||||
cmd.exe /d /s /c $p1Compile
|
||||
if ($LASTEXITCODE) { throw 'P1 build failed.' }
|
||||
$p1NetworkTarget = Join-Path $p1Build 'Binaries/Network'
|
||||
if (-not (Test-Path -LiteralPath $p1NetworkTarget)) {
|
||||
$p1NetworkSource = Join-Path $p1Repo '.starlet_msvc2/Binaries/Network'
|
||||
if (-not (Test-Path -LiteralPath $p1NetworkSource)) {
|
||||
throw 'Local libslirp runtime absent; the network probe cannot run.'
|
||||
}
|
||||
New-Item -ItemType Directory -Path $p1NetworkTarget | Out-Null
|
||||
Get-ChildItem -LiteralPath $p1NetworkSource -File | Copy-Item -Destination $p1NetworkTarget
|
||||
}
|
||||
& $p1Ninja -C $p1Build -t deps 'Source/Core/Common/CMakeFiles/common.dir/PerformanceTrace.cpp.obj' |
|
||||
Select-Object -First 8
|
||||
@@ -0,0 +1,17 @@
|
||||
param([Parameter(Mandatory)][string]$OutputDirectory)
|
||||
# One-command, firmware-free WPR qualification. Run in an elevated PowerShell.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$p1Repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||
$p1Out = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($OutputDirectory)) {
|
||||
$OutputDirectory
|
||||
} else {
|
||||
Join-Path $p1Repo $OutputDirectory
|
||||
}))
|
||||
$p1Probe = Join-Path $p1Repo '.starlet_diag_p1/Binaries/Tests/TraceProbeP1.exe'
|
||||
$p1Network = Join-Path $p1Repo '.starlet_diag_p1/Binaries/Network'
|
||||
foreach ($p1Input in @($p1Probe, $p1Network)) {
|
||||
if (-not (Test-Path -LiteralPath $p1Input)) { throw "Missing local probe dependency: $p1Input" }
|
||||
}
|
||||
& (Join-Path $PSScriptRoot 'Capture-P1.ps1') `
|
||||
-OutputDirectory $p1Out -Seconds 60 -ProbeExecutable $p1Probe `
|
||||
-ProbeArguments @((Join-Path $p1Out 'synthetic.journal'), $p1Network)
|
||||
@@ -0,0 +1,58 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][ValidateSet('P1-trace-005','P1-trace-006')][string]$Name,
|
||||
[Parameter(Mandatory=$true)][int]$DolphinProcessId,
|
||||
[ValidateRange(2,60)][int]$IntervalSeconds=10,
|
||||
[ValidateRange(1,120)][int]$MaxMinutes=60
|
||||
)
|
||||
|
||||
# Low-overhead, read-only host progress samples for the paired native update.
|
||||
# No debugger, ETW session, guest-memory reads, or emulator input is involved.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$p1Repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||
$p1Root = Join-Path $p1Repo '.starlet_update_ab_20260913'
|
||||
$p1Profile = Join-Path $p1Root $Name
|
||||
$p1ExpectedExe = Join-Path $p1Root "build-$Name/Binaries/DolphinNoGUI.exe"
|
||||
$p1Journal = Join-Path $p1Profile 'Starlet/0238188DE97894AE3AF93A92C4F238C0572731A7.nand-journal'
|
||||
$p1Process = Get-Process -Id $DolphinProcessId -ErrorAction Stop
|
||||
if ($p1Process.ProcessName -ne 'DolphinNoGUI') {
|
||||
throw "Process $DolphinProcessId is not DolphinNoGUI."
|
||||
}
|
||||
# The restricted sampler cannot always read another process's executable path.
|
||||
# Check it when available, and require the isolated journal to be live as well.
|
||||
if ($p1Process.Path -and
|
||||
-not $p1Process.Path.Equals($p1ExpectedExe, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Process $DolphinProcessId is not the frozen build for $Name."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $p1Journal -PathType Leaf)) {
|
||||
throw "Missing isolated NAND journal: $p1Journal"
|
||||
}
|
||||
$p1Started = $p1Process.StartTime
|
||||
if ((Get-Item -LiteralPath $p1Journal).LastWriteTime -lt $p1Started) {
|
||||
throw "The isolated journal has not been written by the current launch."
|
||||
}
|
||||
$p1Capture = Join-Path $p1Profile ('Capture/host-progress-' + (Get-Date -Format 'yyyyMMdd-HHmmss') + '.jsonl')
|
||||
$p1Stream = [IO.File]::Open($p1Capture, [IO.FileMode]::CreateNew,
|
||||
[IO.FileAccess]::Write, [IO.FileShare]::Read)
|
||||
$p1Writer = [IO.StreamWriter]::new($p1Stream, [Text.UTF8Encoding]::new($false))
|
||||
$p1Writer.AutoFlush = $true
|
||||
$p1Clock = [Diagnostics.Stopwatch]::StartNew()
|
||||
Write-Output "Capturing $Name (PID $DolphinProcessId) to $p1Capture"
|
||||
try {
|
||||
while ($p1Clock.Elapsed.TotalMinutes -lt $MaxMinutes) {
|
||||
$p1Process = Get-Process -Id $DolphinProcessId -ErrorAction SilentlyContinue
|
||||
if (-not $p1Process -or $p1Process.StartTime -ne $p1Started) { break }
|
||||
$p1Process.Refresh()
|
||||
[ordered]@{
|
||||
UTC=[datetime]::UtcNow.ToString('o')
|
||||
ElapsedSeconds=[math]::Round($p1Clock.Elapsed.TotalSeconds,3)
|
||||
ProcessCPUSeconds=[math]::Round($p1Process.TotalProcessorTime.TotalSeconds,3)
|
||||
JournalBytes=(Get-Item -LiteralPath $p1Journal).Length
|
||||
PrivateMemoryBytes=$p1Process.PrivateMemorySize64
|
||||
} | ConvertTo-Json -Compress | ForEach-Object { $p1Writer.WriteLine($_) }
|
||||
Start-Sleep -Seconds $IntervalSeconds
|
||||
}
|
||||
$p1Writer.WriteLine(('CAPTURE_END_UTC=' + [datetime]::UtcNow.ToString('o')))
|
||||
} finally {
|
||||
$p1Writer.Dispose()
|
||||
}
|
||||
Write-Output "Saved $p1Capture"
|
||||
@@ -0,0 +1,144 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$OutputDirectory,
|
||||
[ValidateRange(5,900)][int]$Seconds=60,
|
||||
[string]$ProbeExecutable,
|
||||
[string[]]$ProbeArguments=@(),
|
||||
[ValidateRange(0,9223372036854775807)][long]$ProbeAffinityMask=0
|
||||
)
|
||||
# Runs an owned, uniquely named ETW session. Does not launch or stop Dolphin.
|
||||
# Native use requires a separately launched, frozen diagnostic build and manifest.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
function ConvertTo-P1WindowsArgument([string]$Value) {
|
||||
# Windows PowerShell 5.1 uses .NET Framework, where ProcessStartInfo has no
|
||||
# ArgumentList. Quote paths using the standard Windows argv backslash rules.
|
||||
$p1Quoted = '"'
|
||||
$p1Slashes = 0
|
||||
foreach ($p1Char in $Value.ToCharArray()) {
|
||||
if ($p1Char -eq '\') { ++$p1Slashes; continue }
|
||||
if ($p1Char -eq '"') {
|
||||
$p1Quoted += ('\' * (2 * $p1Slashes + 1)) + '"'
|
||||
$p1Slashes = 0
|
||||
continue
|
||||
}
|
||||
$p1Quoted += ('\' * $p1Slashes) + $p1Char
|
||||
$p1Slashes = 0
|
||||
}
|
||||
return $p1Quoted + ('\' * (2 * $p1Slashes)) + '"'
|
||||
}
|
||||
$p1Out = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($OutputDirectory)) {
|
||||
$OutputDirectory
|
||||
} else {
|
||||
Join-Path (Get-Location).ProviderPath $OutputDirectory
|
||||
}))
|
||||
if (Test-Path -LiteralPath $p1Out) { throw 'Use a NEW capture directory; results are never overwritten.' }
|
||||
if ($ProbeExecutable) {
|
||||
$ProbeExecutable = (Resolve-Path -LiteralPath $ProbeExecutable).Path
|
||||
if ([IO.Path]::GetFileName($ProbeExecutable) -ne 'TraceProbeP1.exe') {
|
||||
throw 'Automatic launch is restricted to the local qualification probe, not Dolphin.'
|
||||
}
|
||||
}
|
||||
if ($ProbeAffinityMask -ne 0) {
|
||||
$p1Allowed = [Diagnostics.Process]::GetCurrentProcess().ProcessorAffinity.ToInt64()
|
||||
if (($ProbeAffinityMask -band $p1Allowed) -ne $ProbeAffinityMask) {
|
||||
throw "Probe affinity mask $ProbeAffinityMask is not available to this process."
|
||||
}
|
||||
}
|
||||
$p1Drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($p1Out))
|
||||
if ($p1Drive.AvailableFreeSpace -lt 9GB) { throw 'Need 4 GiB trace budget plus 5 GiB reserve.' }
|
||||
New-Item -ItemType Directory -Path $p1Out | Out-Null
|
||||
$p1Name = 'DolphinP1-' + [guid]::NewGuid().ToString('N')
|
||||
$p1Temp = Join-Path (Split-Path $p1Out -Parent) ($p1Name + '-temporary-etl')
|
||||
New-Item -ItemType Directory -Path $p1Temp | Out-Null
|
||||
$p1Profile = Join-Path $PSScriptRoot 'DolphinP1.wprp'
|
||||
$p1ETL = Join-Path $p1Out 'capture.etl'
|
||||
$p1PDB = if ($ProbeExecutable) {
|
||||
Join-Path (Split-Path (Split-Path (Split-Path $ProbeExecutable -Parent) -Parent) -Parent) 'pdb/TraceProbeP1.pdb'
|
||||
} else { $null }
|
||||
$p1Started = $false
|
||||
$p1Reason = 'duration'
|
||||
$p1Probe = $null
|
||||
$p1Stdout = $null
|
||||
$p1Stderr = $null
|
||||
$p1Manifest = [ordered]@{
|
||||
Schema=1; Collection='WPRSystemAndNative'; Session=$p1Name
|
||||
StartUTC=[datetime]::UtcNow.ToString('o')
|
||||
Seconds=$Seconds; TemporaryTraceDirectory=$p1Temp
|
||||
Profile=$p1Profile; ProfileSHA256=(Get-FileHash -LiteralPath $p1Profile).Hash
|
||||
Probe=$ProbeExecutable; ProbeSHA256=$(if ($ProbeExecutable) { (Get-FileHash -LiteralPath $ProbeExecutable).Hash } else { $null })
|
||||
ProbeAffinityMask=$ProbeAffinityMask
|
||||
ProbePDB=$p1PDB; ProbePDBSHA256=$(if ($p1PDB -and (Test-Path -LiteralPath $p1PDB)) { (Get-FileHash -LiteralPath $p1PDB).Hash } else { $null })
|
||||
NativeQualificationPassed=$false
|
||||
}
|
||||
$p1Manifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath (Join-Path $p1Out 'manifest.json') -Encoding utf8
|
||||
try {
|
||||
& wpr.exe -start ($p1Profile + '!DolphinP1') -filemode -recordtempto $p1Temp -instancename $p1Name *> (Join-Path $p1Out 'start.log')
|
||||
if ($LASTEXITCODE) { throw "ETW start failed; see $p1Out\start.log. No other session was stopped." }
|
||||
$p1Started = $true
|
||||
if (-not $ProbeExecutable) {
|
||||
Write-Output "Native collector armed for $Seconds seconds: $p1Name. Launch Dolphin separately with -PerformanceTrace now."
|
||||
}
|
||||
if ($ProbeExecutable) {
|
||||
# Environment of this child only; never persistently modify the user's environment.
|
||||
$p1Info = [Diagnostics.ProcessStartInfo]::new($ProbeExecutable)
|
||||
$p1Info.UseShellExecute = $false
|
||||
$p1Info.CreateNoWindow = $true
|
||||
$p1Info.RedirectStandardOutput = $true
|
||||
$p1Info.RedirectStandardError = $true
|
||||
$p1Info.Environment['DOLPHIN_PERF_TRACE'] = '1'
|
||||
if ($p1Info.PSObject.Properties['ArgumentList'] -and $p1Info.ArgumentList) {
|
||||
foreach ($p1Arg in $ProbeArguments) { $p1Info.ArgumentList.Add($p1Arg) }
|
||||
} else {
|
||||
$p1Info.Arguments = ($ProbeArguments | ForEach-Object {
|
||||
ConvertTo-P1WindowsArgument $_
|
||||
}) -join ' '
|
||||
}
|
||||
$p1Probe = [Diagnostics.Process]::Start($p1Info)
|
||||
if ($ProbeAffinityMask -ne 0) {
|
||||
try {
|
||||
$p1Probe.ProcessorAffinity = [IntPtr]$ProbeAffinityMask
|
||||
} catch {
|
||||
$p1Probe.Kill() # Only the synthetic probe started by this script.
|
||||
$p1Probe.WaitForExit()
|
||||
throw "Could not pin owned synthetic probe to mask ${ProbeAffinityMask}: $_"
|
||||
}
|
||||
}
|
||||
$p1Manifest.ProbeObservedAffinityMask = $p1Probe.ProcessorAffinity.ToInt64()
|
||||
$p1Stdout = $p1Probe.StandardOutput.ReadToEndAsync()
|
||||
$p1Stderr = $p1Probe.StandardError.ReadToEndAsync()
|
||||
$p1Manifest.ProbePID = $p1Probe.Id
|
||||
$p1Manifest.ProbeStartUTC = $p1Probe.StartTime.ToUniversalTime().ToString('o')
|
||||
}
|
||||
$p1Clock = [Diagnostics.Stopwatch]::StartNew()
|
||||
while ($p1Clock.Elapsed.TotalSeconds -lt $Seconds) {
|
||||
Start-Sleep -Milliseconds 1000
|
||||
$p1Bytes = (Get-ChildItem -LiteralPath $p1Temp -File -Recurse | Measure-Object Length -Sum).Sum
|
||||
if ($p1Drive.AvailableFreeSpace -lt 5GB -or $p1Bytes -gt 4GB) { $p1Reason='storage-budget'; break }
|
||||
if ($p1Probe -and $p1Probe.HasExited) { $p1Reason='probe-exit'; break }
|
||||
}
|
||||
} finally {
|
||||
if ($p1Started) {
|
||||
& wpr.exe -status collectors -details -instancename $p1Name *> (Join-Path $p1Out 'collector-status.log')
|
||||
& wpr.exe -stop $p1ETL -instancename $p1Name *> (Join-Path $p1Out 'stop.log')
|
||||
$p1StopExit = $LASTEXITCODE
|
||||
$p1Manifest.EndUTC = [datetime]::UtcNow.ToString('o')
|
||||
$p1Manifest.StopReason = $p1Reason
|
||||
$p1Manifest.StopExitCode = $p1StopExit
|
||||
$p1Manifest.ProbeExitCode = if ($p1Probe -and $p1Probe.HasExited) { $p1Probe.ExitCode } else { $null }
|
||||
if ($p1Probe -and $p1Probe.HasExited) {
|
||||
$p1Stdout.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Out 'probe.stdout.log') -Encoding utf8
|
||||
$p1Stderr.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Out 'probe.stderr.log') -Encoding utf8
|
||||
$p1Manifest.ProbeCPUSeconds = $p1Probe.TotalProcessorTime.TotalSeconds
|
||||
$p1Manifest.ProbeWallSeconds = ($p1Probe.ExitTime - $p1Probe.StartTime).TotalSeconds
|
||||
}
|
||||
$p1Manifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath (Join-Path $p1Out 'manifest.json') -Encoding utf8
|
||||
if ($p1StopExit) { throw 'Owned ETW session could not be saved. See stop.log; no broad cancel attempted.' }
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $p1ETL)) { throw 'WPR reported success but produced no capture.etl.' }
|
||||
if ($p1Probe -and -not $p1Probe.HasExited) {
|
||||
throw "Capture stopped at $p1Reason, but the owned local probe is still running (PID $($p1Probe.Id)). No process was forcibly stopped."
|
||||
}
|
||||
if ($p1Probe -and $p1Probe.ExitCode -ne 0) {
|
||||
throw "Local probe failed with exit code $($p1Probe.ExitCode); see probe.stderr.log. The ETL was kept."
|
||||
}
|
||||
Write-Output "Saved $p1ETL. Qualification is NOT automatically passed; inspect losses, attribution and overhead."
|
||||
@@ -0,0 +1,106 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$OutputDirectory,
|
||||
[ValidateRange(30,300)][int]$TimeoutSeconds=120,
|
||||
[ValidateRange(0,9223372036854775807)][long]$ProbeAffinityMask=0,
|
||||
[switch]$NoTrace
|
||||
)
|
||||
# Synthetic user-mode ETW qualification. This does not sample CPU or scheduling.
|
||||
$ErrorActionPreference='Stop'
|
||||
function ConvertTo-P1WindowsArgument([string]$Value) {
|
||||
$p1Quoted = '"'
|
||||
$p1Slashes = 0
|
||||
foreach ($p1Char in $Value.ToCharArray()) {
|
||||
if ($p1Char -eq '\') { ++$p1Slashes; continue }
|
||||
if ($p1Char -eq '"') {
|
||||
$p1Quoted += ('\' * (2 * $p1Slashes + 1)) + '"'
|
||||
$p1Slashes = 0
|
||||
continue
|
||||
}
|
||||
$p1Quoted += ('\' * $p1Slashes) + $p1Char
|
||||
$p1Slashes = 0
|
||||
}
|
||||
return $p1Quoted + ('\' * (2 * $p1Slashes)) + '"'
|
||||
}
|
||||
$p1Repo=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||
if ($ProbeAffinityMask -ne 0) {
|
||||
$p1Allowed = [Diagnostics.Process]::GetCurrentProcess().ProcessorAffinity.ToInt64()
|
||||
if (($ProbeAffinityMask -band $p1Allowed) -ne $ProbeAffinityMask) {
|
||||
throw "Probe affinity mask $ProbeAffinityMask is not available to this process."
|
||||
}
|
||||
}
|
||||
$p1Output=[IO.Path]::GetFullPath($(if([IO.Path]::IsPathRooted($OutputDirectory)){
|
||||
$OutputDirectory
|
||||
}else{
|
||||
Join-Path (Get-Location).ProviderPath $OutputDirectory
|
||||
}))
|
||||
$p1Probe=Join-Path $p1Repo '.starlet_diag_p1/Binaries/Tests/TraceProbeP1.exe'
|
||||
$p1PDB=Join-Path $p1Repo '.starlet_diag_p1/pdb/TraceProbeP1.pdb'
|
||||
$p1Network=Join-Path $p1Repo '.starlet_diag_p1/Binaries/Network'
|
||||
$p1Decoder=Join-Path $p1Repo '.starlet-profile/P1-tools/DecodeP1.exe'
|
||||
foreach($p1Input in @($p1Probe,$p1PDB,$p1Network,$p1Decoder)) {
|
||||
if(-not (Test-Path -LiteralPath $p1Input)) { throw "Missing local qualification dependency: $p1Input" }
|
||||
}
|
||||
if(Test-Path -LiteralPath $p1Output) { throw 'Use a NEW directory; trace evidence is never overwritten.' }
|
||||
New-Item -ItemType Directory -Path $p1Output | Out-Null
|
||||
$p1Journal=Join-Path $p1Output 'synthetic.journal'
|
||||
$p1ETL=Join-Path $p1Output 'native.etl'
|
||||
$p1Events=Join-Path $p1Output 'events.jsonl'
|
||||
$p1Info=[Diagnostics.ProcessStartInfo]::new($p1Probe)
|
||||
$p1Info.UseShellExecute=$false
|
||||
$p1Info.CreateNoWindow=$true
|
||||
$p1Info.RedirectStandardOutput=$true
|
||||
$p1Info.RedirectStandardError=$true
|
||||
$p1Info.WorkingDirectory=$p1Repo
|
||||
if($NoTrace) { $p1Info.Environment.Remove('DOLPHIN_PERF_TRACE') | Out-Null }
|
||||
else { $p1Info.Environment['DOLPHIN_PERF_TRACE']='1' }
|
||||
$p1Arguments=if($NoTrace) { @($p1Journal,$p1Network) } else { @($p1Journal,$p1Network,$p1ETL) }
|
||||
if($p1Info.PSObject.Properties['ArgumentList'] -and $p1Info.ArgumentList) {
|
||||
foreach($p1Arg in $p1Arguments) { $p1Info.ArgumentList.Add($p1Arg) }
|
||||
} else {
|
||||
$p1Info.Arguments=($p1Arguments | ForEach-Object {
|
||||
ConvertTo-P1WindowsArgument $_
|
||||
}) -join ' '
|
||||
}
|
||||
$p1Process=[Diagnostics.Process]::Start($p1Info)
|
||||
if ($ProbeAffinityMask -ne 0) {
|
||||
try {
|
||||
$p1Process.ProcessorAffinity = [IntPtr]$ProbeAffinityMask
|
||||
} catch {
|
||||
$p1Process.Kill() # Only the synthetic probe started by this script.
|
||||
$p1Process.WaitForExit()
|
||||
throw "Could not pin owned synthetic probe to mask ${ProbeAffinityMask}: $_"
|
||||
}
|
||||
}
|
||||
$p1ObservedAffinity = $p1Process.ProcessorAffinity.ToInt64()
|
||||
$p1Stdout=$p1Process.StandardOutput.ReadToEndAsync()
|
||||
$p1Stderr=$p1Process.StandardError.ReadToEndAsync()
|
||||
if(-not $p1Process.WaitForExit($TimeoutSeconds*1000)) {
|
||||
throw "Local probe exceeded $TimeoutSeconds seconds. Its PID is $($p1Process.Id); no process was forcibly stopped."
|
||||
}
|
||||
$p1Stdout.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Output 'probe.stdout.log') -Encoding utf8
|
||||
$p1Stderr.GetAwaiter().GetResult() | Out-File -LiteralPath (Join-Path $p1Output 'probe.stderr.log') -Encoding utf8
|
||||
$p1Manifest=[ordered]@{
|
||||
Schema=1; Collection=$(if($NoTrace){'NoTrace'}else{'PrivateUserMode'}); Probe=$p1Probe
|
||||
ProbeSHA256=(Get-FileHash -LiteralPath $p1Probe).Hash
|
||||
ProbePDB=$p1PDB; ProbePDBSHA256=(Get-FileHash -LiteralPath $p1PDB).Hash
|
||||
ProbePID=$p1Process.Id; ProbeExitCode=$p1Process.ExitCode
|
||||
ProbeAffinityMask=$ProbeAffinityMask; ProbeObservedAffinityMask=$p1ObservedAffinity
|
||||
ProbeStartUTC=$p1Process.StartTime.ToUniversalTime().ToString('o')
|
||||
ProbeEndUTC=$p1Process.ExitTime.ToUniversalTime().ToString('o')
|
||||
ProbeWallSeconds=($p1Process.ExitTime-$p1Process.StartTime).TotalSeconds
|
||||
ProbeCPUSeconds=$p1Process.TotalProcessorTime.TotalSeconds
|
||||
NativeQualificationPassed=$false
|
||||
}
|
||||
$p1Manifest | ConvertTo-Json -Depth 5 | Out-File -LiteralPath (Join-Path $p1Output 'manifest.json') -Encoding utf8
|
||||
if($p1Process.ExitCode -ne 0) { throw 'Local probe failed; see probe.stderr.log.' }
|
||||
if($NoTrace) {
|
||||
Write-Output "No-trace fixed-work baseline completed: $p1Output"
|
||||
return
|
||||
}
|
||||
if(-not (Test-Path -LiteralPath $p1ETL)) { throw 'Local probe produced no ETL.' }
|
||||
$p1DecodeText=& $p1Decoder $p1ETL $p1Events
|
||||
$p1DecodeExit=$LASTEXITCODE
|
||||
$p1DecodeText | Out-File -LiteralPath (Join-Path $p1Output 'decoder-summary.json') -Encoding utf8
|
||||
if($p1DecodeExit) { throw 'ETL decode/loss check failed; see decoder-summary.json.' }
|
||||
Write-Output "Private user-mode trace decoded: $p1Output"
|
||||
Write-Output 'This capture has no CPU samples, context switches or disk wait events.'
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Offline ETL decoder. No process attach, memory read or thread suspension.
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cwchar>
|
||||
#include <evntrace.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tdh.h>
|
||||
#include <vector>
|
||||
|
||||
static constexpr GUID NATIVE = {
|
||||
0x54ee9231, 0x28be, 0x4ab0, {0x98, 0x1e, 0xb2, 0x6f, 0x97, 0x9c, 0x56, 0xa4}};
|
||||
static FILE* output;
|
||||
static unsigned long long decoded = 0, errors = 0, total = 0;
|
||||
static std::map<std::string, std::vector<unsigned char>> schemas;
|
||||
static bool inspect_system = false;
|
||||
static unsigned inspected_schemas = 0, failed_schemas = 0;
|
||||
static std::string UTF8(const wchar_t* value)
|
||||
{
|
||||
if (!value || !*value)
|
||||
return {};
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, value, -1, nullptr, 0, nullptr, nullptr);
|
||||
std::string text(n, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value, -1, text.data(), n, nullptr, nullptr);
|
||||
text.pop_back();
|
||||
return text;
|
||||
}
|
||||
static std::string Quote(const std::string& value)
|
||||
{
|
||||
std::string result = "\"";
|
||||
for (unsigned char c : value)
|
||||
{
|
||||
if (c == '"' || c == '\\')
|
||||
{
|
||||
result += '\\';
|
||||
result += c;
|
||||
}
|
||||
else if (c < 32)
|
||||
{
|
||||
char escaped[7];
|
||||
std::snprintf(escaped, sizeof(escaped), "\\u%04x", c);
|
||||
result += escaped;
|
||||
}
|
||||
else
|
||||
result += c;
|
||||
}
|
||||
return result + '"';
|
||||
}
|
||||
static void WINAPI Event(PEVENT_RECORD record)
|
||||
{
|
||||
++total;
|
||||
const bool native = IsEqualGUID(record->EventHeader.ProviderId, NATIVE);
|
||||
std::string key(reinterpret_cast<const char*>(&record->EventHeader.ProviderId), sizeof(GUID));
|
||||
key.append(reinterpret_cast<const char*>(&record->EventHeader.EventDescriptor),
|
||||
sizeof(EVENT_DESCRIPTOR));
|
||||
auto& buffer = schemas[key];
|
||||
const bool first_schema = buffer.empty();
|
||||
if (native || buffer.empty())
|
||||
{
|
||||
ULONG bytes = 0;
|
||||
const ULONG status = TdhGetEventInformation(record, 0, nullptr, nullptr, &bytes);
|
||||
if (status != ERROR_INSUFFICIENT_BUFFER)
|
||||
{
|
||||
if (native)
|
||||
++errors;
|
||||
else if (inspect_system && failed_schemas++ < 20)
|
||||
std::fprintf(stderr, "TDH missing: event=%u opcode=%u status=%lu\n",
|
||||
record->EventHeader.EventDescriptor.Id,
|
||||
record->EventHeader.EventDescriptor.Opcode, status);
|
||||
return;
|
||||
}
|
||||
buffer.resize(bytes);
|
||||
if (TdhGetEventInformation(record, 0, nullptr,
|
||||
reinterpret_cast<PTRACE_EVENT_INFO>(buffer.data()),
|
||||
&bytes) != ERROR_SUCCESS)
|
||||
{
|
||||
if (native)
|
||||
++errors;
|
||||
buffer.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto* info = reinterpret_cast<PTRACE_EVENT_INFO>(buffer.data());
|
||||
const auto text = [&](ULONG offset) -> std::string {
|
||||
return offset ? UTF8(reinterpret_cast<wchar_t*>(buffer.data() + offset)) : "";
|
||||
};
|
||||
std::string name = text(info->EventNameOffset);
|
||||
if (name.empty())
|
||||
name = text(info->TaskNameOffset) + "/" + text(info->OpcodeNameOffset);
|
||||
if (inspect_system && !native && first_schema && inspected_schemas++ < 300)
|
||||
{
|
||||
std::fprintf(stderr, "SCHEMA event=%u opcode=%u name=%s fields=",
|
||||
record->EventHeader.EventDescriptor.Id,
|
||||
record->EventHeader.EventDescriptor.Opcode, name.c_str());
|
||||
for (ULONG i = 0; i < info->TopLevelPropertyCount && i < 30; ++i)
|
||||
std::fprintf(stderr, "%s%s", i ? "," : "", text(info->EventPropertyInfoArray[i].NameOffset).c_str());
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
bool interesting = native;
|
||||
// Keep scheduling and instruction pointer records for independent qualification.
|
||||
for (ULONG i = 0; i < info->TopLevelPropertyCount; ++i)
|
||||
{
|
||||
const auto field = text(info->EventPropertyInfoArray[i].NameOffset);
|
||||
if (field == "InstructionPointer" || field == "OldThreadId" || field == "TThreadId")
|
||||
interesting = true;
|
||||
}
|
||||
if (!interesting)
|
||||
return;
|
||||
std::string fields;
|
||||
for (ULONG i = 0; i < info->TopLevelPropertyCount; ++i)
|
||||
{
|
||||
const auto& property = info->EventPropertyInfoArray[i];
|
||||
if (property.Flags & PropertyStruct)
|
||||
continue;
|
||||
const auto field = text(property.NameOffset);
|
||||
PROPERTY_DATA_DESCRIPTOR descriptor{};
|
||||
descriptor.PropertyName = reinterpret_cast<ULONGLONG>(buffer.data() + property.NameOffset);
|
||||
descriptor.ArrayIndex = ULONG_MAX;
|
||||
ULONG size = 0;
|
||||
if (TdhGetPropertySize(record, 0, nullptr, 1, &descriptor, &size) != ERROR_SUCCESS ||
|
||||
size > 65536)
|
||||
{
|
||||
if (native)
|
||||
++errors;
|
||||
continue;
|
||||
}
|
||||
std::vector<unsigned char> bytes(size + 2, 0);
|
||||
if (TdhGetProperty(record, 0, nullptr, 1, &descriptor, size, bytes.data()) != ERROR_SUCCESS)
|
||||
{
|
||||
if (native)
|
||||
++errors;
|
||||
continue;
|
||||
}
|
||||
std::string value;
|
||||
const auto type = property.nonStructType.InType;
|
||||
if (type == TDH_INTYPE_UNICODESTRING)
|
||||
value = Quote(UTF8(reinterpret_cast<wchar_t*>(bytes.data())));
|
||||
else if (type == TDH_INTYPE_ANSISTRING)
|
||||
value = Quote(reinterpret_cast<char*>(bytes.data()));
|
||||
else if (size == 1 || size == 2 || size == 4 || size == 8)
|
||||
{
|
||||
unsigned long long number = 0;
|
||||
std::memcpy(&number, bytes.data(), size);
|
||||
value = std::to_string(number);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (native)
|
||||
++errors;
|
||||
continue;
|
||||
}
|
||||
if (!fields.empty())
|
||||
fields += ',';
|
||||
fields += Quote(field) + ':' + value;
|
||||
}
|
||||
std::fprintf(output,
|
||||
"{\"native\":%s,\"event\":%s,\"qpc\":%lld,\"pid\":%lu,\"tid\":%lu,\"cpu\":%u,"
|
||||
"\"opcode\":%u,\"data\":{%s}}\n",
|
||||
native ? "true" : "false", Quote(name).c_str(),
|
||||
record->EventHeader.TimeStamp.QuadPart, record->EventHeader.ProcessId,
|
||||
record->EventHeader.ThreadId, record->BufferContext.ProcessorNumber,
|
||||
record->EventHeader.EventDescriptor.Opcode, fields.c_str());
|
||||
++decoded;
|
||||
}
|
||||
int wmain(int argc, wchar_t** argv)
|
||||
{
|
||||
if (argc != 3 && (argc != 4 || std::wcscmp(argv[3], L"--inspect-system") != 0))
|
||||
{
|
||||
std::fprintf(stderr, "DecodeP1 input.etl NEW-output.jsonl [--inspect-system]\n");
|
||||
return 2;
|
||||
}
|
||||
inspect_system = argc == 4;
|
||||
if (GetFileAttributesW(argv[2]) != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
std::fprintf(stderr, "Refusing overwrite\n");
|
||||
return 2;
|
||||
}
|
||||
if (_wfopen_s(&output, argv[2], L"wb"))
|
||||
return 2;
|
||||
EVENT_TRACE_LOGFILEW logfile{};
|
||||
logfile.LogFileName = argv[1];
|
||||
logfile.ProcessTraceMode = PROCESS_TRACE_MODE_EVENT_RECORD | PROCESS_TRACE_MODE_RAW_TIMESTAMP;
|
||||
logfile.EventRecordCallback = Event;
|
||||
TRACEHANDLE trace = OpenTraceW(&logfile);
|
||||
if (trace == INVALID_PROCESSTRACE_HANDLE)
|
||||
{
|
||||
std::fclose(output);
|
||||
return 3;
|
||||
}
|
||||
const ULONG result = ProcessTrace(&trace, 1, nullptr, nullptr);
|
||||
CloseTrace(trace);
|
||||
std::fclose(output);
|
||||
std::printf("{\"status\":%lu,\"total\":%llu,\"decoded\":%llu,\"native_decode_errors\":%llu,"
|
||||
"\"events_lost\":%lu,\"buffers_lost\":%lu,\"qpc_frequency\":%lld}\n",
|
||||
result, total, decoded, errors, logfile.LogfileHeader.EventsLost,
|
||||
logfile.LogfileHeader.BuffersLost, logfile.LogfileHeader.PerfFreq.QuadPart);
|
||||
return result != ERROR_SUCCESS || errors != 0 || logfile.LogfileHeader.EventsLost != 0 ||
|
||||
logfile.LogfileHeader.BuffersLost != 0;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<WindowsPerformanceRecorder Version="1.0" Author="Dolphin" Company="Dolphin">
|
||||
<Profiles>
|
||||
<SystemCollector Id="P1Kernel" Name="DolphinP1Kernel">
|
||||
<BufferSize Value="256"/>
|
||||
<Buffers Value="128"/>
|
||||
</SystemCollector>
|
||||
<EventCollector Id="P1Events" Name="DolphinP1Events">
|
||||
<BufferSize Value="128"/>
|
||||
<Buffers Value="64"/>
|
||||
</EventCollector>
|
||||
<SystemProvider Id="P1System">
|
||||
<Keywords>
|
||||
<Keyword Value="ProcessThread"/>
|
||||
<Keyword Value="Loader"/>
|
||||
<Keyword Value="SampledProfile"/>
|
||||
<Keyword Value="CSwitch"/>
|
||||
<Keyword Value="ReadyThread"/>
|
||||
<Keyword Value="DiskIO"/>
|
||||
<Keyword Value="DiskIOInit"/>
|
||||
<Keyword Value="FileIO"/>
|
||||
<Keyword Value="FileIOInit"/>
|
||||
<Keyword Value="Filename"/>
|
||||
<Keyword Value="HardFaults"/>
|
||||
<Keyword Value="CpuConfig"/>
|
||||
<Keyword Value="Power"/>
|
||||
<Keyword Value="NetworkTrace"/>
|
||||
</Keywords>
|
||||
<Stacks>
|
||||
<Stack Value="SampledProfile"/>
|
||||
<Stack Value="CSwitch"/>
|
||||
</Stacks>
|
||||
</SystemProvider>
|
||||
<EventProvider Id="P1Native" Name="54ee9231-28be-4ab0-981e-b26f979c56a4" Level="5"/>
|
||||
<Profile Id="DolphinP1.Verbose.File" Name="DolphinP1" Description="CPU, scheduling, file IO and versioned Dolphin diagnostic events" LoggingMode="File" DetailLevel="Verbose">
|
||||
<Collectors>
|
||||
<SystemCollectorId Value="P1Kernel"><SystemProviderId Value="P1System"/></SystemCollectorId>
|
||||
<EventCollectorId Value="P1Events"><EventProviders><EventProviderId Value="P1Native"/></EventProviders></EventCollectorId>
|
||||
</Collectors>
|
||||
</Profile>
|
||||
</Profiles>
|
||||
</WindowsPerformanceRecorder>
|
||||
@@ -0,0 +1,79 @@
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$OutputDirectory,
|
||||
[ValidateRange(4,8)][int]$Pairs = 4,
|
||||
[string]$PythonExecutable,
|
||||
[ValidateRange(0,9223372036854775807)][long]$ProbeAffinityMask = 0,
|
||||
[switch]$AnalyzeOnly
|
||||
)
|
||||
# One controlled batch of OFF/WPR-ON fixed-work synthetic runs; no Wii/NAND.
|
||||
# WPR requires an elevated PowerShell. Every run gets a unique directory.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$p1Repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||
$p1Root = [IO.Path]::GetFullPath($(if ([IO.Path]::IsPathRooted($OutputDirectory)) {
|
||||
$OutputDirectory
|
||||
} else {
|
||||
Join-Path $p1Repo $OutputDirectory
|
||||
}))
|
||||
if ($PythonExecutable) {
|
||||
$p1Python = (Resolve-Path -LiteralPath $PythonExecutable).Path
|
||||
} else {
|
||||
$p1Candidates = @(Get-Command python.exe -All -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Source -notlike '*\WindowsApps\*' } |
|
||||
Select-Object -ExpandProperty Source)
|
||||
$p1Candidates += 'C:\msys64\mingw64\bin\python.exe'
|
||||
foreach ($p1Candidate in $p1Candidates) {
|
||||
if ($p1Candidate -and (Test-Path -LiteralPath $p1Candidate)) {
|
||||
$p1Python = $p1Candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $p1Python) {
|
||||
throw 'Python was not found before capture. Pass -PythonExecutable with the path to a working python.exe.'
|
||||
}
|
||||
& $p1Python --version | Out-Null
|
||||
if ($LASTEXITCODE) { throw "Python could not start: $p1Python" }
|
||||
$p1AlreadyExists = Test-Path -LiteralPath $p1Root
|
||||
if ($AnalyzeOnly -and -not $p1AlreadyExists) { throw "Existing batch not found: $p1Root" }
|
||||
if (-not $AnalyzeOnly -and $p1AlreadyExists) {
|
||||
throw 'Use a NEW batch directory for capture. Pass -AnalyzeOnly to compare existing captures.'
|
||||
}
|
||||
$p1Probe = Join-Path $p1Repo '.starlet_diag_p1/Binaries/Tests/TraceProbeP1.exe'
|
||||
$p1Network = Join-Path $p1Repo '.starlet_diag_p1/Binaries/Network'
|
||||
$p1Decoder = Join-Path $p1Repo '.starlet-profile/P1-tools/DecodeP1.exe'
|
||||
if (-not $AnalyzeOnly) {
|
||||
foreach ($p1Required in @($p1Probe, $p1Network, $p1Decoder)) {
|
||||
if (-not (Test-Path -LiteralPath $p1Required)) { throw "Missing dependency: $p1Required" }
|
||||
}
|
||||
$p1Drive = [IO.DriveInfo]::new([IO.Path]::GetPathRoot($p1Root))
|
||||
if ($p1Drive.AvailableFreeSpace -lt 9GB) { throw 'Need 4 GiB batch budget plus 5 GiB reserve.' }
|
||||
New-Item -ItemType Directory -Path $p1Root | Out-Null
|
||||
}
|
||||
$p1Runs = @()
|
||||
for ($p1Index = 1; $p1Index -le $Pairs; ++$p1Index) {
|
||||
$p1Off = Join-Path $p1Root "off-$p1Index"
|
||||
$p1On = Join-Path $p1Root "wpr-on-$p1Index"
|
||||
if (-not $AnalyzeOnly) {
|
||||
Write-Output "Pair $p1Index/${Pairs}: fixed-work OFF"
|
||||
& (Join-Path $PSScriptRoot 'Capture-Private-P1.ps1') -OutputDirectory $p1Off -NoTrace `
|
||||
-ProbeAffinityMask $ProbeAffinityMask
|
||||
Write-Output "Pair $p1Index/${Pairs}: fixed-work WPR ON"
|
||||
& (Join-Path $PSScriptRoot 'Capture-P1.ps1') -OutputDirectory $p1On -Seconds 60 `
|
||||
-ProbeExecutable $p1Probe `
|
||||
-ProbeArguments @((Join-Path $p1On 'synthetic.journal'), $p1Network) `
|
||||
-ProbeAffinityMask $ProbeAffinityMask
|
||||
$p1Decoded = & $p1Decoder (Join-Path $p1On 'capture.etl') `
|
||||
(Join-Path $p1On 'native-events.jsonl')
|
||||
$p1DecodeExit = $LASTEXITCODE
|
||||
$p1Decoded | Out-File -LiteralPath (Join-Path $p1On 'decoder-summary.json') -Encoding utf8
|
||||
if ($p1DecodeExit) { throw "Pair $p1Index has ETW loss or native decode errors." }
|
||||
}
|
||||
$p1Runs += $p1Off, $p1On
|
||||
}
|
||||
$p1Comparison = Join-Path $p1Root 'comparison.json'
|
||||
if (Test-Path -LiteralPath $p1Comparison) {
|
||||
throw "Comparison already exists and will not be overwritten: $p1Comparison"
|
||||
}
|
||||
& $p1Python (Join-Path $PSScriptRoot 'compare_p1.py') @p1Runs --output $p1Comparison
|
||||
if ($LASTEXITCODE) { throw 'Fixed-work comparison failed; no Wii run was started.' }
|
||||
Write-Output "Overhead batch finished: $p1Comparison"
|
||||
@@ -0,0 +1,110 @@
|
||||
param(
|
||||
[ValidatePattern('^P1-trace-[0-9]{3}$')][string]$Name='P1-trace-001',
|
||||
[string]$BuildDirectory='.starlet_diag_p1',
|
||||
[string]$RuntimeDirectory='.starlet_msvc2',
|
||||
[Parameter(Mandatory=$true)][string]$PythonExecutable
|
||||
)
|
||||
# Create an independent, verified pre-update NAND and frozen diagnostic build.
|
||||
# Never write to the user's normal NAND, the seed, or an existing experiment.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$p1Repo = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||
$p1Root = Join-Path $p1Repo '.starlet_update_ab_20260913'
|
||||
$p1Profile = Join-Path $p1Root $Name
|
||||
$p1Build = Join-Path $p1Root "build-$Name"
|
||||
$p1SourceBuild = [IO.Path]::GetFullPath((Join-Path $p1Repo $BuildDirectory))
|
||||
$p1RuntimeBuild = [IO.Path]::GetFullPath((Join-Path $p1Repo $RuntimeDirectory))
|
||||
$p1SourceExe = Join-Path $p1SourceBuild 'Binaries/DolphinNoGUI.exe'
|
||||
$p1SourcePDB = Join-Path $p1SourceBuild 'pdb/DolphinNoGUI.pdb'
|
||||
$p1Seed = Join-Path $p1Root 'pre-update.nand-journal'
|
||||
$p1JournalRelative = 'Starlet\0238188DE97894AE3AF93A92C4F238C0572731A7.nand-journal'
|
||||
$p1Manifest = Get-Content -LiteralPath (Join-Path $p1Root 'manifest.json') -Raw | ConvertFrom-Json
|
||||
$p1Expected = Get-Content -LiteralPath (Join-Path $p1Root 'initial-files-B.json') -Raw | ConvertFrom-Json
|
||||
$p1Python = (Resolve-Path -LiteralPath $PythonExecutable).Path
|
||||
if (Get-Process DolphinNoGUI,Dolphin -ErrorAction SilentlyContinue) {
|
||||
throw 'Close the existing Dolphin instance before preparing a native-update run.'
|
||||
}
|
||||
foreach ($p1Target in @($p1Profile, $p1Build)) {
|
||||
if (Test-Path -LiteralPath $p1Target) { throw "Refusing to overwrite $p1Target" }
|
||||
}
|
||||
foreach ($p1Required in @($p1Seed, $p1SourceExe, $p1SourcePDB,
|
||||
(Join-Path $p1RuntimeBuild 'Binaries/Sys'),
|
||||
(Join-Path $p1SourceBuild 'Binaries/Network'))) {
|
||||
if (-not (Test-Path -LiteralPath $p1Required)) { throw "Missing input: $p1Required" }
|
||||
}
|
||||
if ((Get-FileHash -LiteralPath $p1Seed -Algorithm SHA256).Hash -ne $p1Manifest.SeedSHA256) {
|
||||
throw 'The preserved pre-update NAND journal differs from its manifest.'
|
||||
}
|
||||
$p1InputBytes = ($p1Expected | Measure-Object -Property Bytes -Sum).Sum
|
||||
$p1InputBytes += (Get-Item -LiteralPath $p1SourceExe).Length + (Get-Item -LiteralPath $p1SourcePDB).Length
|
||||
foreach ($p1Folder in @('Sys','Network')) {
|
||||
$p1FolderSource = if ($p1Folder -eq 'Sys') { $p1RuntimeBuild } else { $p1SourceBuild }
|
||||
$p1InputBytes += (Get-ChildItem -LiteralPath (Join-Path $p1FolderSource "Binaries/$p1Folder") `
|
||||
-File -Recurse | Measure-Object -Property Length -Sum).Sum
|
||||
}
|
||||
if ((Get-PSDrive C).Free -lt ($p1InputBytes + 5GB)) {
|
||||
throw 'Insufficient free space for copies, update growth, and a 5 GiB reserve.'
|
||||
}
|
||||
$p1Copies = foreach ($p1File in $p1Expected) {
|
||||
$p1Target = [IO.Path]::GetFullPath((Join-Path $p1Profile $p1File.Relative))
|
||||
if (-not $p1Target.StartsWith($p1Profile + '\', [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw 'Invalid relative path in the initial-file manifest.'
|
||||
}
|
||||
$p1Sources = if ($p1File.Relative -eq $p1JournalRelative) { @($p1Seed) } else {
|
||||
@((Join-Path (Join-Path $p1Root 'B') $p1File.Relative),
|
||||
(Join-Path (Join-Path $p1Repo '.starlet_formatter_test_20260913') $p1File.Relative))
|
||||
}
|
||||
$p1Selected = $null
|
||||
foreach ($p1Source in $p1Sources) {
|
||||
if ((Test-Path -LiteralPath $p1Source -PathType Leaf) -and
|
||||
(Get-FileHash -LiteralPath $p1Source -Algorithm SHA256).Hash -eq $p1File.SHA256) {
|
||||
$p1Selected = $p1Source
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $p1Selected) { throw "No byte-identical initial source for $($p1File.Relative)" }
|
||||
[pscustomobject]@{ Relative=$p1File.Relative; Source=$p1Selected; Target=$p1Target; SHA256=$p1File.SHA256 }
|
||||
}
|
||||
New-Item -ItemType Directory -Path $p1Profile,(Join-Path $p1Build 'Binaries'),
|
||||
(Join-Path $p1Build 'pdb') | Out-Null
|
||||
foreach ($p1Copy in $p1Copies) {
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $p1Copy.Target -Parent) | Out-Null
|
||||
Copy-Item -LiteralPath $p1Copy.Source -Destination $p1Copy.Target
|
||||
if ((Get-FileHash -LiteralPath $p1Copy.Target -Algorithm SHA256).Hash -ne $p1Copy.SHA256) {
|
||||
throw "Copied input verification failed: $($p1Copy.Relative)"
|
||||
}
|
||||
}
|
||||
foreach ($p1Folder in @('Sys','Network')) {
|
||||
$p1FolderSource = if ($p1Folder -eq 'Sys') { $p1RuntimeBuild } else { $p1SourceBuild }
|
||||
Copy-Item -LiteralPath (Join-Path $p1FolderSource "Binaries/$p1Folder") `
|
||||
-Destination (Join-Path $p1Build 'Binaries') -Recurse
|
||||
}
|
||||
Copy-Item -LiteralPath $p1SourceExe -Destination (Join-Path $p1Build 'Binaries/DolphinNoGUI.exe')
|
||||
Copy-Item -LiteralPath $p1SourcePDB -Destination (Join-Path $p1Build 'pdb/DolphinNoGUI.pdb')
|
||||
foreach ($p1Folder in @('Logs','Perf','Capture')) {
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $p1Profile $p1Folder) | Out-Null
|
||||
}
|
||||
$p1ConfigPath = Join-Path $p1Profile 'Config/Dolphin.ini'
|
||||
$p1OriginalConfig = [IO.File]::ReadAllText($p1ConfigPath)
|
||||
$p1OldPrefix = (Join-Path $p1Root 'B').Replace('\','/') + '/'
|
||||
$p1NewPrefix = $p1Profile.Replace('\','/') + '/'
|
||||
if (-not $p1OriginalConfig.Contains($p1OldPrefix)) {
|
||||
throw 'The source configuration has no expected baseline profile path.'
|
||||
}
|
||||
$p1NewConfig = $p1OriginalConfig.Replace($p1OldPrefix, $p1NewPrefix)
|
||||
if ($p1NewConfig.Replace($p1NewPrefix, $p1OldPrefix) -cne $p1OriginalConfig) {
|
||||
throw 'The isolated configuration differs beyond its profile paths.'
|
||||
}
|
||||
[IO.File]::WriteAllText($p1ConfigPath, $p1NewConfig, [Text.UTF8Encoding]::new($false))
|
||||
& $p1Python -B (Join-Path $p1Root '../.starlet-profile/VerifyJournalCheckpoint.py') `
|
||||
(Join-Path $p1Profile $p1JournalRelative) $p1Manifest.PrefixBytes `
|
||||
> (Join-Path $p1Profile 'Capture/initial-journal-verification.txt') 2>&1
|
||||
if ($LASTEXITCODE) { throw 'Preserved NAND checkpoint verification failed; no emulator was launched.' }
|
||||
[pscustomobject]@{
|
||||
CreatedUTC=[datetime]::UtcNow.ToString('o'); Profile=$p1Profile; Build=$p1Build
|
||||
InitialFiles=$p1Copies.Count; SeedSHA256=$p1Manifest.SeedSHA256
|
||||
ExeSHA256=(Get-FileHash -LiteralPath (Join-Path $p1Build 'Binaries/DolphinNoGUI.exe')).Hash
|
||||
PdbSHA256=(Get-FileHash -LiteralPath (Join-Path $p1Build 'pdb/DolphinNoGUI.pdb')).Hash
|
||||
SourceBuild=$p1SourceBuild; RuntimeBuild=$p1RuntimeBuild
|
||||
PerformanceTraceRequestedAtLaunch=$false
|
||||
} | ConvertTo-Json -Depth 4 | Out-File -LiteralPath (Join-Path $p1Profile 'Capture/prepare-manifest.json') -Encoding utf8
|
||||
Write-Output "Prepared isolated profile $p1Profile and frozen build $p1Build. No Dolphin instance was launched."
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// A local, user-mode-only trace for validating the provider without WPR rights.
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <evntrace.h>
|
||||
|
||||
#include "Common/PerformanceTrace.h"
|
||||
|
||||
class PrivateETW
|
||||
{
|
||||
public:
|
||||
explicit PrivateETW(const char* output)
|
||||
{
|
||||
if (!output)
|
||||
return;
|
||||
const auto path = std::filesystem::absolute(output);
|
||||
if (std::filesystem::exists(path))
|
||||
throw std::runtime_error("Private ETL target already exists");
|
||||
if (!Common::PerformanceTrace::Requested())
|
||||
throw std::runtime_error("Set DOLPHIN_PERF_TRACE=1 for a private trace");
|
||||
Common::PerformanceTrace::Enabled(); // Register before starting the session.
|
||||
constexpr wchar_t name[] = L"DolphinP1LocalProbe";
|
||||
const std::wstring filename = path.wstring();
|
||||
const size_t name_size = sizeof(name);
|
||||
const size_t filename_size = (filename.size() + 1) * sizeof(wchar_t);
|
||||
m_storage.resize(sizeof(EVENT_TRACE_PROPERTIES) + name_size + filename_size);
|
||||
m_properties = reinterpret_cast<EVENT_TRACE_PROPERTIES*>(m_storage.data());
|
||||
std::memset(m_properties, 0, m_storage.size());
|
||||
m_properties->Wnode.BufferSize = static_cast<ULONG>(m_storage.size());
|
||||
m_properties->Wnode.Guid = PERF_PROVIDER;
|
||||
m_properties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
|
||||
m_properties->Wnode.ClientContext = 1; // QPC
|
||||
m_properties->BufferSize = 64;
|
||||
m_properties->MinimumBuffers = 2;
|
||||
m_properties->MaximumBuffers = 16;
|
||||
m_properties->LogFileMode = EVENT_TRACE_FILE_MODE_SEQUENTIAL | EVENT_TRACE_PRIVATE_LOGGER_MODE |
|
||||
EVENT_TRACE_PRIVATE_IN_PROC;
|
||||
m_properties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
|
||||
m_properties->LogFileNameOffset =
|
||||
static_cast<ULONG>(sizeof(EVENT_TRACE_PROPERTIES) + name_size);
|
||||
std::memcpy(m_storage.data() + m_properties->LoggerNameOffset, name, name_size);
|
||||
std::memcpy(m_storage.data() + m_properties->LogFileNameOffset, filename.c_str(),
|
||||
filename_size);
|
||||
const ULONG started = StartTraceW(&m_handle, name, m_properties);
|
||||
if (started != ERROR_SUCCESS)
|
||||
throw std::runtime_error("Private ETW StartTrace error " + std::to_string(started));
|
||||
m_started = true;
|
||||
const ULONG enabled =
|
||||
EnableTraceEx2(m_handle, &PERF_PROVIDER, EVENT_CONTROL_CODE_ENABLE_PROVIDER,
|
||||
TRACE_LEVEL_VERBOSE, 0, 0, 0, nullptr);
|
||||
if (enabled != ERROR_SUCCESS || !Common::PerformanceTrace::Enabled())
|
||||
{
|
||||
Close();
|
||||
throw std::runtime_error("Private ETW enable error " + std::to_string(enabled));
|
||||
}
|
||||
}
|
||||
|
||||
~PrivateETW() { Close(); }
|
||||
PrivateETW(const PrivateETW&) = delete;
|
||||
PrivateETW& operator=(const PrivateETW&) = delete;
|
||||
|
||||
bool Close()
|
||||
{
|
||||
if (!m_started)
|
||||
return true;
|
||||
m_started = false;
|
||||
return ControlTraceW(m_handle, L"DolphinP1LocalProbe", m_properties,
|
||||
EVENT_TRACE_CONTROL_STOP) == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr GUID PERF_PROVIDER = {
|
||||
0x54ee9231, 0x28be, 0x4ab0, {0x98, 0x1e, 0xb2, 0x6f, 0x97, 0x9c, 0x56, 0xa4}};
|
||||
TRACEHANDLE m_handle = 0;
|
||||
bool m_started = false;
|
||||
EVENT_TRACE_PROPERTIES* m_properties = nullptr;
|
||||
std::vector<u8> m_storage;
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
# Diagnostic P1 : statut et exécution locale
|
||||
|
||||
Le protocole et les critères de décision sont dans
|
||||
[`docs/Wii_LLE_Performance_Diagnostic_Protocol.md`](../../docs/Wii_LLE_Performance_Diagnostic_Protocol.md).
|
||||
Tous les artefacts volumineux vont dans `.starlet-profile` (ignoré par Git), jamais
|
||||
dans la NAND source ni dans le profil utilisateur courant.
|
||||
|
||||
1. Construire le profil isolé avec `./Tools/PerformanceTrace/Build-P1.ps1`.
|
||||
Après modification d'une option de compilation, `-CleanBeforeBuild` vérifie tous
|
||||
les objets. Le script répare le préfixe français `/showIncludes` si cette
|
||||
installation MSVC l'a mal décodé ; un objet avec `#deps 0` invalide la build.
|
||||
2. Vérifier les tests ciblés de `Binaries/Tests/tests.exe` et le décodeur avec
|
||||
`./Tools/PerformanceTrace/Build-Decoder.ps1 -OutputDirectory .starlet-profile/P1-tools`.
|
||||
3. Sur une session Windows sans droit de profilage noyau, valider d'abord le
|
||||
fournisseur dans une session ETW privée :
|
||||
|
||||
```powershell
|
||||
./Tools/PerformanceTrace/Capture-Private-P1.ps1 -OutputDirectory .starlet-profile/P1-private-NEW
|
||||
python Tools/PerformanceTrace/qualify_p1.py .starlet-profile/P1-private-NEW
|
||||
```
|
||||
|
||||
Le résultat attendu est `local_provider_passed=true` et
|
||||
`full_collector_passed=false`. Cette session ne contient aucun échantillon
|
||||
CPU, changement de thread ou attente disque. Le mode `-NoTrace` exécute
|
||||
exactement la même charge pour une comparaison locale alternée. Exemple
|
||||
avec quatre répertoires neufs, à exécuter dans cet ordre :
|
||||
|
||||
```powershell
|
||||
./Tools/PerformanceTrace/Capture-Private-P1.ps1 -OutputDirectory .starlet-profile/P1-off-1 -NoTrace
|
||||
./Tools/PerformanceTrace/Capture-Private-P1.ps1 -OutputDirectory .starlet-profile/P1-on-1
|
||||
./Tools/PerformanceTrace/Capture-Private-P1.ps1 -OutputDirectory .starlet-profile/P1-off-2 -NoTrace
|
||||
./Tools/PerformanceTrace/Capture-Private-P1.ps1 -OutputDirectory .starlet-profile/P1-on-2
|
||||
python Tools/PerformanceTrace/compare_p1.py .starlet-profile/P1-off-1 .starlet-profile/P1-on-1 .starlet-profile/P1-off-2 .starlet-profile/P1-on-2 --output .starlet-profile/P1-private-overhead-NEW.json
|
||||
```
|
||||
|
||||
Ce comparateur conserve les quatre mesures et la dispersion ; il ne peut
|
||||
**pas** valider le budget de 2 % de la capture WPR complète.
|
||||
4. Capture synthétique WPR complète **dans une session administrateur**, avec
|
||||
un **nouveau** chemin de sortie :
|
||||
|
||||
```powershell
|
||||
./Tools/PerformanceTrace/Capture-Local-P1.ps1 -OutputDirectory .starlet-profile/P1-local-NEW
|
||||
```
|
||||
|
||||
Ce programme exécute une boucle Thumb réelle, un sommeil hôte connu, 128
|
||||
transactions de journal synthétique et 32 échanges ARP avec la passerelle
|
||||
locale libslirp. Le journal synthétique reste dans ce répertoire neuf.
|
||||
Aucun serveur externe n'est contacté par l'échange ARP.
|
||||
5. Après la capture, décoder et qualifier hors ligne (sans droit administrateur)
|
||||
dans un **nouveau** répertoire :
|
||||
|
||||
```powershell
|
||||
./Tools/PerformanceTrace/Analyze-Local-P1.ps1 `
|
||||
-CaptureDirectory .starlet-profile/P1-local-NEW `
|
||||
-OutputDirectory .starlet-profile/P1-local-NEW-analysis
|
||||
```
|
||||
|
||||
Le lecteur TDH extrait les événements natifs ; Xperf décode les événements
|
||||
noyau classiques, résout les PDB locaux et les deux flux sont alignés par
|
||||
les QPC des marqueurs ou des snapshots périodiques. Le CSV, l'ETL et les données de profil peuvent contenir
|
||||
des noms de processus ou chemins : ils restent locaux et ignorés par Git.
|
||||
Le répertoire de sortie doit être neuf à chaque tentative.
|
||||
6. Mesurer le surcoût WPR avec une seule commande administrateur et un nouveau
|
||||
répertoire. Elle automatise quatre paires alternées OFF/ON du **même** travail
|
||||
synthétique (huit lancements locaux, aucun démarrage Wii ni NAND utilisateur) :
|
||||
|
||||
```powershell
|
||||
./Tools/PerformanceTrace/Measure-P1-Overhead.ps1 -OutputDirectory .starlet-profile/P1-overhead-001
|
||||
```
|
||||
|
||||
`comparison.json` conserve les durées brutes, la dispersion des bases OFF,
|
||||
les écarts par paire et une borne supérieure de confiance. Le seuil de 2 %
|
||||
ne passe que si les données restent stables et les trois mesures ARM/mur/CPU
|
||||
satisfont ce seuil. Python est vérifié **avant** toute capture ; si un lot
|
||||
s'est interrompu uniquement au calcul final, `-AnalyzeOnly` relit les huit
|
||||
captures existantes sans en relancer aucune. `-PythonExecutable` permet de
|
||||
préciser le chemin de `python.exe` si le PowerShell administrateur n'a pas
|
||||
le même `PATH`. Un résultat négatif ou bruité n'autorise aucun diagnostic
|
||||
de vitesse Wii.
|
||||
|
||||
`qualification.json` détaille chaque gate. Une sonde locale réussie ne constitue
|
||||
pas encore une analyse de mise à jour Wii : le cas PPC, les piles hôte et le
|
||||
surcoût du collecteur restent à qualifier. La capture native n'est prévue
|
||||
qu'après ces vérifications. Le profil WPR comporte une session au nom unique et
|
||||
le script arrête seulement cette session.
|
||||
|
||||
Le 22 septembre, deux traces privées ont validé les 13 contrôles du fournisseur
|
||||
(`P1-private-004`, `P1-private-006`). Le jeton `CodexSandboxUsers` ne pouvait
|
||||
pas lancer WPR (`0xc5585011`), mais la capture synthétique administrateur
|
||||
`P1-local-004` a réussi. Sous Windows PowerShell 5.1, le premier essai avait
|
||||
révélé `ProcessStartInfo.ArgumentList` nul et une résolution de chemin relatif
|
||||
depuis le répertoire de processus plutôt que celui affiché par `cd` ; les deux
|
||||
cas sont corrigés et testés.
|
||||
|
||||
L'analyse reproductible `P1-local-004-reanalysis2` passe 13 contrôles du
|
||||
fournisseur et 3 contrôles système : 9 427 échantillons CPU, 694 changements
|
||||
de contexte, dix ancres QPC alignées à cinq ticks près, sommeil connu de
|
||||
752 ms avec un switch `Waiting`. Les 112 événements classiques déclarés
|
||||
invalides par Xperf concernent une métadonnée `ImageID` hors du PID de la
|
||||
sonde ; zéro événement cible invalide et zéro perte ETW. Les symboles du PDB
|
||||
local résolvent les fonctions hôte. Cela valide la **sonde**, pas la capture
|
||||
native : coût du collecteur WPR, cas PPC et chemin critique Wii restent ouverts.
|
||||
[Identification du GUID ImageID dans le code Microsoft PerfView](https://github.com/microsoft/perfview/blob/main/src/TraceEvent/BPerf/BPerfEventSource.cs).
|
||||
|
||||
Le lot `P1-overhead-001` du 22 septembre a produit les huit captures complètes,
|
||||
mais son PowerShell administrateur ne trouvait pas Python lors de la comparaison.
|
||||
Les données conservées ont été comparées hors ligne, sans recapture : les bases
|
||||
OFF varient de 34,5 % pour la phase ARM et 26,8 % sur le temps total. Les écarts
|
||||
ARM par paire sont +9,7 %, +25,0 %, -0,5 % et +1,0 %. Le surcoût WPR ne peut
|
||||
donc pas être isolé de la variabilité de cette série et le budget de 2 % reste
|
||||
**non qualifié**. Le rapport brut est
|
||||
`.starlet-profile/P1-overhead-001/comparison.json` ; aucune mesure de vitesse
|
||||
Wii n'en est déduite. Les traces ON montrent `cpptools.exe` consommant environ
|
||||
8–9 s de CPU par essai et des sous-processus Dell intermittents ; ces charges
|
||||
hôte sont une piste de contamination, pas une attribution causale chiffrée des
|
||||
écarts ON/OFF. Dans `wpr-on-2`, 76 768 événements `ReadyThread` système ont été
|
||||
enregistrés. Le profil conserve ces événements et leurs horodatages mais ne
|
||||
collecte plus leur pile, afin de réduire le coût potentiel de cette source
|
||||
système très fréquente ; le profil modifié doit être requalifié séparément.
|
||||
|
||||
Le lot `P1-overhead-002`, après cet allègement, n'a pas perdu d'événements
|
||||
mais garde 36,5 % d'étendue ARM entre bases OFF. Ses quatre écarts ON/OFF sont
|
||||
+4,6 %, -11,0 %, +10,2 % et -3,5 % ; leur moyenne proche de zéro ne qualifie
|
||||
**pas** le budget. L'analyse des échantillons ETW révèle un changement de
|
||||
placement CPU : part sur les processeurs logiques 0–3 de 78,7 %, 45,4 %, 28,9 %,
|
||||
46,5 % pour des phases ARM de 7,59 s, 8,82 s, 10,33 s, 8,65 s. L'ETL identifie
|
||||
un Core i7-1255U et SMT sur 0–3 ; [Intel indique 2 P-cores et 8 E-cores](https://www.intel.com/content/www/us/en/products/sku/226259/intel-core-i71255u-processor-12m-cache-up-to-4-70-ghz/specifications.html).
|
||||
Ce placement est fortement associé à la durée, sans prouver qu'il explique à
|
||||
lui seul tout l'écart. Le lancement Wii existant fixe déjà Dolphin sur ces
|
||||
processeurs ; la **sonde synthétique**, elle, n'était pas fixée.
|
||||
|
||||
Les captures de sonde acceptent maintenant `-ProbeAffinityMask`, enregistrent
|
||||
le masque réellement appliqué, et le comparateur refuse les masques différents.
|
||||
Quatre bases locales sans WPR avec `-ProbeAffinityMask 15` ont fini leur phase
|
||||
ARM en 7,03, 6,65, 7,20 et 6,97 s (masque observé 15 à chaque fois). Le
|
||||
placement est contrôlé, mais une variabilité résiduelle de 8,3 % subsiste :
|
||||
aucun nouveau lot administrateur n'est demandé avant de traiter cette limite
|
||||
du protocole ou de passer à un diagnostic qualitatif explicitement borné.
|
||||
|
||||
## Première capture d'une vraie mise à jour (exploratoire)
|
||||
|
||||
`Prepare-Native-Update-P1.ps1` crée un profil NAND pré-mise-à-jour indépendant,
|
||||
vérifie les 31 fichiers et le journal, puis fige l'exécutable et son PDB. Il
|
||||
refuse tout nom déjà utilisé et ne lance pas Dolphin. L'expérience préparée
|
||||
ici est `P1-trace-002` ; le profil courant `.starlet_user3` et les résultats A–F
|
||||
ne sont pas modifiés. La capture ETW requiert un PowerShell administrateur.
|
||||
La préparation dépend d'un seed NAND local ignoré par Git et demande désormais
|
||||
un `-PythonExecutable` explicite ; elle n'est pas une procédure de création de
|
||||
NAND dans un clone neuf.
|
||||
|
||||
```powershell
|
||||
# Terminal administrateur : démarrer d'abord, puis laisser ouvert.
|
||||
cd C:\Users\tpetkovic\Desktop\dolphin
|
||||
.\Tools\PerformanceTrace\Capture-P1.ps1 -OutputDirectory .starlet-profile/P1-native-update-001 -Seconds 600
|
||||
|
||||
# Dès « Native collector armed », dans un autre PowerShell :
|
||||
cd C:\Users\tpetkovic\Desktop\dolphin
|
||||
.\Run-Wii-IOS-LLE.ps1 `
|
||||
-BuildDirectory .starlet_update_ab_20260913/build-P1-trace-002 `
|
||||
-UserDirectory .starlet_update_ab_20260913/P1-trace-002 `
|
||||
-Ethernet -PerformanceTrace
|
||||
```
|
||||
|
||||
Noter le PID imprimé par le lanceur, lancer la mise à jour dans la Wii, puis
|
||||
laisser terminer la capture. Analyser ensuite l'ETL hors ligne dans un nouveau
|
||||
répertoire :
|
||||
|
||||
```powershell
|
||||
.\Tools\PerformanceTrace\Analyze-Local-P1.ps1 `
|
||||
-CaptureDirectory .starlet-profile/P1-native-update-001 `
|
||||
-OutputDirectory .starlet-profile/P1-native-update-001-analysis `
|
||||
-NativePID <PID_DOLPHIN> `
|
||||
-NativePDB .starlet_update_ab_20260913/build-P1-trace-002/pdb/DolphinNoGUI.pdb
|
||||
```
|
||||
|
||||
Le rapport `native-report.json` reste exploratoire : perte ETW et alignement
|
||||
QPC sont contrôlés, mais piles, attentes critiques, NAT et surcoût de capture
|
||||
doivent encore être qualifiés avant d'attribuer le ralentissement ou d'annoncer
|
||||
un gain de FPS. La mise à jour nécessite une action Wii réelle et un réseau
|
||||
disponible ; ne pas essayer de la rejouer sur le même profil après succès.
|
||||
|
||||
Windows exige un droit de profilage pour le profil noyau ; un logger privé
|
||||
du processus fonctionne avec un compte peu privilégié.
|
||||
[Sessions privées ETW](https://learn.microsoft.com/en-us/windows/win32/etw/configuring-and-starting-a-private-logger-session),
|
||||
[droits de StartTrace](https://learn.microsoft.com/en-us/windows/win32/api/evntrace/nf-evntrace-starttracea).
|
||||
|
||||
## Décision après A/B natif du 23 septembre
|
||||
|
||||
Le [rapport P1](../../docs/Wii_LLE_Update_P1_Trace_2026-09-23.md) contient les
|
||||
deux mises à jour NAND isolées et vérifiées. Le raccourci SRAM ciblé a été
|
||||
rejeté : sur ce passage, le témoin a terminé les mêmes seuils de journal plus
|
||||
vite et a été jugé plus fluide. Le raccourci a été retiré du source ; ses deux
|
||||
binaires et journaux figés restent locaux pour audit. Les outils de ce dossier
|
||||
servent au diagnostic, pas à justifier une modification matérielle sans test
|
||||
de conformité sur Wii réelle.
|
||||
@@ -0,0 +1,190 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Local qualification only: synthetic ARM code/data, synthetic NAND journal, no
|
||||
// firmware/profile.
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
#include "Common/Network.h"
|
||||
#include "Common/PerformanceTrace.h"
|
||||
#include "Core/IOS/Starlet/ARMCore.h"
|
||||
#include "Core/IOS/Starlet/AX88772.h"
|
||||
#include "Core/IOS/Starlet/NANDJournal.h"
|
||||
#include "Core/IOS/Starlet/SlirpNetwork.h"
|
||||
#include "PrivateETW.h"
|
||||
|
||||
namespace Trace = Common::PerformanceTrace;
|
||||
using namespace IOS::LLE;
|
||||
class ProbeBus final : public ARMBus
|
||||
{
|
||||
public:
|
||||
u8 Read8(u32 address) override { return bytes.at(address); }
|
||||
void Write8(u32 address, u8 value) override { bytes.at(address) = value; }
|
||||
const u8* GetDirectMemoryPointer(u32 address, u32 size) const override
|
||||
{
|
||||
return address < bytes.size() && size <= bytes.size() - address ? bytes.data() + address :
|
||||
nullptr;
|
||||
}
|
||||
void Op(u32 offset, u16 op)
|
||||
{
|
||||
bytes[offset] = static_cast<u8>(op >> 8);
|
||||
bytes[offset + 1] = static_cast<u8>(op);
|
||||
}
|
||||
std::array<u8, 8192> bytes{};
|
||||
};
|
||||
void Check(bool value)
|
||||
{
|
||||
if (!value)
|
||||
throw std::runtime_error("P1 local probe validation failed");
|
||||
}
|
||||
void LocalNAT(const std::string& directory)
|
||||
{
|
||||
SlirpNetwork network;
|
||||
AX88772 device([&](std::span<const u8> frame) { network.Input(frame); });
|
||||
std::string error;
|
||||
if (!network.Start(
|
||||
directory, [&](std::span<const u8> frame) { device.ReceiveFrame(frame); }, &error))
|
||||
throw std::runtime_error(error);
|
||||
const auto control = [&](u8 type, u8 request, u16 value) {
|
||||
std::array<u8, 8> setup{
|
||||
type, request, static_cast<u8>(value), static_cast<u8>(value >> 8), 0, 0, 0, 0};
|
||||
size_t actual = 0;
|
||||
Check(device.Transfer(0, 0, setup, &actual) == AX88772::Result::Completed);
|
||||
Check(device.Transfer(0, 2, {}, &actual) == AX88772::Result::Completed);
|
||||
};
|
||||
control(0, 9, 1);
|
||||
control(0x40, 0x10, 0x88);
|
||||
control(0x40, 0x1b, 0x306);
|
||||
device.SetLink(true);
|
||||
// ARP reaches only libslirp's virtual gateway: no DNS, Internet or remote server.
|
||||
Common::MACAddress mac{2, 0x44, 0x4f, 0x4c, 0, 1};
|
||||
Common::ARPPacket request(Common::MACAddress{255, 255, 255, 255, 255, 255}, mac);
|
||||
request.arp_header.opcode = htons(1);
|
||||
request.arp_header.sender_address = mac;
|
||||
request.arp_header.sender_ip = htonl(0x0a00020f);
|
||||
request.arp_header.target_ip = htonl(0x0a000202);
|
||||
const auto frame = request.Build();
|
||||
const u16 length = static_cast<u16>(frame.size());
|
||||
std::vector<u8> framed{static_cast<u8>(length), static_cast<u8>(length >> 8),
|
||||
static_cast<u8>(~length), static_cast<u8>(~length >> 8)};
|
||||
framed.insert(framed.end(), frame.begin(), frame.end());
|
||||
Trace::Marker("LocalNATBegin");
|
||||
for (u32 i = 0; i < 32; ++i)
|
||||
{
|
||||
Trace::SampleRoot root(true);
|
||||
size_t actual = 0;
|
||||
Check(device.Transfer(3, 1, framed, &actual) == AX88772::Result::Completed);
|
||||
network.Poll(243000);
|
||||
device.TraceQueue();
|
||||
std::array<u8, 2048> response{};
|
||||
Check(device.Transfer(2, 2, response, &actual) == AX88772::Result::Completed);
|
||||
Check(actual >= 46);
|
||||
const auto arp =
|
||||
Common::PacketView(response.data() + 4, response[0] | (response[1] << 8)).GetARPPacket();
|
||||
Check(arp && ntohs(arp->arp_header.opcode) == 2 &&
|
||||
arp->arp_header.sender_ip == htonl(0x0a000202));
|
||||
device.TraceQueue();
|
||||
}
|
||||
Trace::Marker("LocalNATEnd");
|
||||
}
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (argc != 3 && argc != 4)
|
||||
throw std::runtime_error("Usage: TraceProbeP1.exe NEW-synthetic-journal-path "
|
||||
"libslirp-runtime-directory [NEW-private-ETL]");
|
||||
const std::string journal_path = argv[1];
|
||||
Check(!std::filesystem::exists(journal_path));
|
||||
PrivateETW private_trace(argc == 4 ? argv[3] : nullptr);
|
||||
Trace::Marker("ProbeBegin");
|
||||
ProbeBus bus;
|
||||
bus.Op(0, 0x6810); // ldr r0,[r2]
|
||||
bus.Op(2, 0x6851); // ldr r1,[r2,#4]
|
||||
bus.Op(4, 0x46c0); // nop
|
||||
bus.Op(6, 0xe7fb); // b 0
|
||||
bus.Write32(4096, 0x12345678);
|
||||
bus.Write32(4100, 0x87654321);
|
||||
ARMCore cpu(bus);
|
||||
cpu.SetCPSR(static_cast<u32>(ARMCore::Mode::Supervisor) | ARMCore::CPSR_T);
|
||||
cpu.SetRegister(2, 4096);
|
||||
cpu.SetJitEnabled(true);
|
||||
Trace::Marker("ARMWorkBegin");
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
// Fixed work, not a duration-based loop: suitable for ON/OFF cost
|
||||
// comparisons.
|
||||
constexpr u64 slices = 160000;
|
||||
for (u64 i = 0; i < slices; ++i)
|
||||
{
|
||||
Trace::SampleRoot root;
|
||||
Check(cpu.RunCycles(4096) == 4096);
|
||||
if ((i & 1023) == 0)
|
||||
Trace::Snapshot(cpu.GetTraceGeneration(), cpu.GetExecutedInstructions(),
|
||||
cpu.GetJitNativeExecutedInstructions(), cpu.GetJitSlowReadCount(),
|
||||
cpu.GetJitSlowWriteCount(), cpu.GetJitLifetimeCompiledBlockCount(),
|
||||
cpu.GetJitFallbackInstructionCount(), cpu.GetRegister(15), false);
|
||||
}
|
||||
const double ms =
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
||||
Check(cpu.GetRegister(0) == 0x12345678 && cpu.GetRegister(1) == 0x87654321);
|
||||
Check(cpu.GetExecutedInstructions() == slices * 4096);
|
||||
Trace::Marker("ARMWorkEnd");
|
||||
Trace::Marker("KnownSleepBegin");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(750));
|
||||
Trace::Marker("KnownSleepEnd");
|
||||
{
|
||||
Trace::Marker("SyntheticJournalBegin");
|
||||
NANDJournal journal;
|
||||
NANDJournal::Pages pages;
|
||||
Common::SHA1::Digest synthetic_identity{};
|
||||
std::string error;
|
||||
Check(journal.Open(journal_path, synthetic_identity, &pages, &error));
|
||||
for (u32 i = 0; i < 128; ++i)
|
||||
{
|
||||
Trace::SampleRoot root(true);
|
||||
NANDJournal::Pages update;
|
||||
update[i].fill(static_cast<u8>(i));
|
||||
Check(journal.Append(update));
|
||||
}
|
||||
Trace::Marker("SyntheticJournalEnd");
|
||||
}
|
||||
{
|
||||
NANDJournal verifier;
|
||||
NANDJournal::Pages pages;
|
||||
Common::SHA1::Digest synthetic_identity{};
|
||||
std::string error;
|
||||
Check(verifier.Open(journal_path, synthetic_identity, &pages, &error));
|
||||
Check(pages.size() == 128);
|
||||
for (u32 i = 0; i < 128; ++i)
|
||||
for (u8 value : pages.at(i))
|
||||
Check(value == static_cast<u8>(i));
|
||||
}
|
||||
LocalNAT(std::filesystem::absolute(argv[2]).string());
|
||||
cpu.Reset();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1100));
|
||||
Trace::Snapshot(cpu.GetTraceGeneration(), cpu.GetExecutedInstructions(),
|
||||
cpu.GetJitNativeExecutedInstructions(), cpu.GetJitSlowReadCount(),
|
||||
cpu.GetJitSlowWriteCount(), cpu.GetJitLifetimeCompiledBlockCount(),
|
||||
cpu.GetJitFallbackInstructionCount(), cpu.GetRegister(15), false);
|
||||
Trace::Marker("ProbeEnd");
|
||||
const bool trace_enabled = Trace::Enabled();
|
||||
Check(private_trace.Close());
|
||||
std::printf("P1Probe exact=OK trace-requested=%d trace-enabled=%d "
|
||||
"arm-ms=%.3f instructions=%llu journal-pages=128 nat-arp-replies=32\n",
|
||||
Trace::Requested(), trace_enabled, ms, slices * 4096);
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception& error)
|
||||
{
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
"""Offline P1 event analysis. Unknowns fail qualification rather than becoming zero cost."""
|
||||
import argparse
|
||||
from bisect import bisect_left
|
||||
from collections import Counter, defaultdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
|
||||
REGIONS = ("Slice", "ARM", "Jit", "Compile", "Devices", "NAND", "Journal",
|
||||
"JournalBuild", "JournalWrite", "JournalFlush", "NAT", "DMARead", "DMAWrite", "AES", "SHA")
|
||||
|
||||
|
||||
class CodeMap:
|
||||
def __init__(self):
|
||||
self.ranges = []
|
||||
|
||||
def event(self, data):
|
||||
begin, end = int(data["Begin"]), int(data["End"])
|
||||
if not begin or end <= begin:
|
||||
raise ValueError("Invalid generated-code interval")
|
||||
# Reuse replaces overlapping mappings at this timestamp. Never use the final
|
||||
# mapping to interpret samples collected before an invalidation/recompilation.
|
||||
kept = []
|
||||
for lo, hi, domain, pc in self.ranges:
|
||||
if hi <= begin or lo >= end:
|
||||
kept.append((lo, hi, domain, pc))
|
||||
else:
|
||||
if lo < begin:
|
||||
kept.append((lo, begin, domain, pc))
|
||||
if hi > end:
|
||||
kept.append((end, hi, domain, pc))
|
||||
if data["Kind"] != "invalidate":
|
||||
kept.append((begin, end, data["Domain"], data["GuestPC"]))
|
||||
self.ranges = kept
|
||||
|
||||
def lookup(self, ip):
|
||||
for lo, hi, domain, pc in reversed(self.ranges):
|
||||
if lo <= ip < hi:
|
||||
return domain, pc
|
||||
return "UnresolvedNative", None
|
||||
|
||||
|
||||
class SampledCodeMap:
|
||||
"""Time-ordered mappings only for host PCs actually sampled by ETW.
|
||||
|
||||
Native updates can emit hundreds of thousands of JIT map events. Keeping
|
||||
split intervals and scanning them for every sample makes offline analysis
|
||||
quadratic. The report only needs the finite set of sampled host addresses.
|
||||
"""
|
||||
|
||||
def __init__(self, sampled_addresses):
|
||||
self.addresses = sorted(set(sampled_addresses))
|
||||
self.mappings = {}
|
||||
|
||||
def event(self, data):
|
||||
begin, end = int(data["Begin"]), int(data["End"])
|
||||
if not begin or end <= begin:
|
||||
raise ValueError("Invalid generated-code interval")
|
||||
lo = bisect_left(self.addresses, begin)
|
||||
hi = bisect_left(self.addresses, end)
|
||||
if data["Kind"] == "invalidate":
|
||||
for address in self.addresses[lo:hi]:
|
||||
self.mappings.pop(address, None)
|
||||
else:
|
||||
value = (data["Domain"], data["GuestPC"])
|
||||
for address in self.addresses[lo:hi]:
|
||||
self.mappings[address] = value
|
||||
|
||||
def lookup(self, ip):
|
||||
return self.mappings.get(ip, ("UnresolvedNative", None))
|
||||
|
||||
|
||||
def percentile(values, fraction):
|
||||
if not values:
|
||||
return None
|
||||
values = sorted(values)
|
||||
position = (len(values) - 1) * fraction
|
||||
low = int(position)
|
||||
high = min(low + 1, len(values) - 1)
|
||||
return values[low] + (values[high] - values[low]) * (position - low)
|
||||
|
||||
|
||||
def read_relevant_events(path, pid):
|
||||
# ETW is host-wide. Keep this process's telemetry and all its CPU samples;
|
||||
# rendering/network threads can be hot without emitting native telemetry.
|
||||
native = []
|
||||
with path.open(encoding="utf-8") as source:
|
||||
for line in source:
|
||||
e = json.loads(line)
|
||||
if e["native"] and e["pid"] == pid:
|
||||
native.append(e)
|
||||
if not native:
|
||||
raise ValueError("No native events for requested process")
|
||||
native_tids = {e["tid"] for e in native}
|
||||
begin, end = min(e["qpc"] for e in native), max(e["qpc"] for e in native)
|
||||
rows = native
|
||||
with path.open(encoding="utf-8") as source:
|
||||
for line in source:
|
||||
e = json.loads(line)
|
||||
if e["native"] or not begin <= e["qpc"] <= end:
|
||||
continue
|
||||
if e["event"] == "SampledProfile" and e["pid"] == pid:
|
||||
rows.append(e)
|
||||
sampled_tids = {e["data"].get("ThreadId", e["tid"]) for e in rows
|
||||
if not e["native"] and e["event"] == "SampledProfile"}
|
||||
process_tids = native_tids | sampled_tids
|
||||
with path.open(encoding="utf-8") as source:
|
||||
for line in source:
|
||||
e = json.loads(line)
|
||||
if e["native"] or e["event"] != "CSwitch" or not begin <= e["qpc"] <= end:
|
||||
continue
|
||||
d = e["data"]
|
||||
if d.get("OldThreadId") in process_tids or d.get("NewThreadId") in process_tids:
|
||||
rows.append(e)
|
||||
return rows
|
||||
|
||||
|
||||
def analyze(rows, pid):
|
||||
rows = sorted(rows, key=lambda event: event["qpc"])
|
||||
native = [e for e in rows if e["native"] and e["pid"] == pid]
|
||||
if not native:
|
||||
raise ValueError("No native events for requested process")
|
||||
if any(e["data"].get("Schema") != 1 for e in native):
|
||||
raise ValueError("Unknown or missing native schema")
|
||||
native_tids = {e["tid"] for e in native}
|
||||
sampled_tids = {e["data"].get("ThreadId", e["tid"]) for e in rows
|
||||
if not e["native"] and e["event"] == "SampledProfile" and e["pid"] == pid}
|
||||
tids = native_tids | sampled_tids
|
||||
frequencies = {e["data"]["QPCFrequency"] for e in native if "QPCFrequency" in e["data"]}
|
||||
if len(frequencies) != 1 or not next(iter(frequencies)):
|
||||
raise ValueError("Missing or inconsistent QPC frequency")
|
||||
frequency = next(iter(frequencies))
|
||||
mappings = SampledCodeMap(e["data"]["InstructionPointer"] for e in rows
|
||||
if not e["native"] and "InstructionPointer" in e["data"])
|
||||
samples = Counter()
|
||||
hot_pc = Counter()
|
||||
hot_static = Counter()
|
||||
phases = {}
|
||||
scope_samples = defaultdict(list)
|
||||
frames = defaultdict(list)
|
||||
counter_last = {}
|
||||
counter_errors = []
|
||||
snapshots = []
|
||||
cswitches = []
|
||||
queues = []
|
||||
accounting_errors = 0
|
||||
for e in rows:
|
||||
data = e["data"]
|
||||
if e["native"] and e["pid"] == pid:
|
||||
if e["event"] == "CodeMap":
|
||||
mappings.event(data)
|
||||
elif e["event"] == "Marker":
|
||||
phases[data["Name"]] = e["qpc"]
|
||||
if data["Name"] == "PerformanceMetricsReset":
|
||||
# Keep intervals per generation, never bridge a reset/reboot.
|
||||
for events in frames.values():
|
||||
events.append(None)
|
||||
elif e["event"] == "ScopeSample":
|
||||
accounting_errors += data["AccountingErrors"]
|
||||
scope_samples[(e["tid"], data["Sample"])].append(data)
|
||||
elif e["event"] == "Frame":
|
||||
frames[data["Kind"]].append((data["QPC"], data))
|
||||
elif e["event"] == "Counter":
|
||||
key = (e["tid"], data["Counter"])
|
||||
if data["Value"] < counter_last.get(key, 0):
|
||||
counter_errors.append({"tid": e["tid"], "counter": data["Counter"], "qpc": e["qpc"]})
|
||||
counter_last[key] = data["Value"]
|
||||
elif e["event"] == "Snapshot":
|
||||
snapshots.append(data)
|
||||
elif e["event"] == "USBQueue":
|
||||
queues.append(data)
|
||||
elif not e["native"]:
|
||||
tid = data.get("ThreadId", data.get("ThreadID", e["tid"]))
|
||||
if "InstructionPointer" in data and tid in tids:
|
||||
domain, pc = mappings.lookup(data["InstructionPointer"])
|
||||
weight = data.get("Count", 1)
|
||||
if domain == "UnresolvedNative" and "!" in data.get("Symbol", ""):
|
||||
symbol = data["Symbol"]
|
||||
module = symbol.split("!", 1)[0].strip('"').lower()
|
||||
if module in ("traceprobep1.exe", "dolphinnogui.exe", "dolphin.exe"):
|
||||
domain = "HostC++"
|
||||
elif module and module != "unknown":
|
||||
domain = "SystemOrLibrary"
|
||||
if domain != "UnresolvedNative":
|
||||
hot_static[symbol] += weight
|
||||
samples[domain] += weight
|
||||
if pc is not None:
|
||||
hot_pc[(domain, pc)] += weight
|
||||
if "OldThreadId" in data and (data["OldThreadId"] in tids or data.get("NewThreadId") in tids):
|
||||
cswitches.append(e)
|
||||
timing = defaultdict(lambda: {"calls": 0, "inclusive_ms": 0., "exclusive_ms": 0.})
|
||||
for group in scope_samples.values():
|
||||
roots = [d for d in group if d["Region"] == 0]
|
||||
if len(roots) != 1 or sum(d["ExclusiveQPC"] for d in group) != roots[0]["InclusiveQPC"]:
|
||||
accounting_errors += 1
|
||||
for data in group:
|
||||
region = REGIONS[data["Region"]] + "/sample-1-in-" + str(data["SamplingDenominator"])
|
||||
# Actual sampled time only. Do not present extrapolated duration as exact CPU.
|
||||
stat = timing[region]
|
||||
stat["calls"] += data["Calls"]
|
||||
stat["inclusive_ms"] += 1000 * data["InclusiveQPC"] / frequency
|
||||
stat["exclusive_ms"] += 1000 * data["ExclusiveQPC"] / frequency
|
||||
snapshot_errors = []
|
||||
previous = {}
|
||||
for data in snapshots:
|
||||
key = data["CoreId"]
|
||||
if key in previous:
|
||||
for field in ("Instructions", "NativeInstructions", "SlowReads", "SlowWrites", "Compiles", "Fallbacks"):
|
||||
if data[field] < previous[key][field]:
|
||||
snapshot_errors.append({"generation": key, "field": field})
|
||||
previous[key] = data
|
||||
durations = {}
|
||||
for kind, events in frames.items():
|
||||
deltas = [(b[0] - a[0]) * 1000 / frequency for a, b in zip(events, events[1:]) if a and b]
|
||||
durations[kind] = {"intervals": len(deltas), "p50_ms": percentile(deltas, .5),
|
||||
"p95_ms": percentile(deltas, .95), "p99_ms": percentile(deltas, .99)}
|
||||
sleep = None
|
||||
if "KnownSleepBegin" in phases and "KnownSleepEnd" in phases:
|
||||
begin, end = phases["KnownSleepBegin"], phases["KnownSleepEnd"]
|
||||
changes = [e for e in cswitches if begin <= e["qpc"] <= end]
|
||||
waits = [e for e in changes if e["data"]["OldThreadId"] in tids and e["data"].get("OldThreadState") == 5]
|
||||
sleep = {"wall_ms": (end - begin) * 1000 / frequency,
|
||||
"cswitch_events": len(changes), "waiting_switches": len(waits),
|
||||
"wait_evidence_present": bool(waits)}
|
||||
return {"schema": 1, "pid": pid, "qpc_frequency": frequency,
|
||||
"native_events": len(native), "native_threads": sorted(native_tids),
|
||||
"sampled_process_threads": sorted(sampled_tids),
|
||||
"cpu_leaf_samples": dict(samples),
|
||||
"hot_generated_pcs": [{"domain": domain, "pc": hex(pc), "samples": count}
|
||||
for (domain, pc), count in hot_pc.most_common(20)],
|
||||
"hot_static_symbols": [{"symbol": symbol, "samples": count}
|
||||
for symbol, count in hot_static.most_common(20)],
|
||||
"sampled_scope_wall_times_not_cpu": dict(timing), "scope_samples": len(scope_samples),
|
||||
"accounting_errors": accounting_errors, "counter_regressions": counter_errors,
|
||||
"snapshot_regressions": snapshot_errors, "core_generations": sorted(previous),
|
||||
"last_counters_by_thread": [{"tid": tid, "counter": counter, "value": value}
|
||||
for (tid, counter), value in sorted(counter_last.items())],
|
||||
"usb_queue_events": len(queues),
|
||||
"usb_queue_peak": max((q["PeakFrames"] for q in queues), default=None),
|
||||
"frame_intervals": durations, "known_sleep": sleep, "markers": phases,
|
||||
"native_update_qualified": False,
|
||||
"remaining_gates": ["Resolve static C++/system stacks and critical-path waits in ETL",
|
||||
"Qualify PPC and check local NAT counters", "Check ETW losses and full collector overhead"],
|
||||
"note": "Partial offline qualification, not a native update bottleneck report. CPU leaf samples include all sampled threads of the target process within native telemetry bounds. Leaf sample counts are not exact CPU durations. Scopes are sampled wall times, not CPU. Never compare differently sampled totals."}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("events", type=Path)
|
||||
parser.add_argument("--pid", required=True, type=int)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
result = analyze(read_relevant_events(args.events, args.pid), args.pid)
|
||||
with args.output.open("x", encoding="utf-8") as output:
|
||||
json.dump(result, output, indent=2)
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -0,0 +1,98 @@
|
||||
"""Compare alternating fixed-work local runs without claiming a native gain."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import statistics
|
||||
|
||||
|
||||
def load(directory):
|
||||
manifest = json.loads((directory / "manifest.json").read_text(encoding="utf-8-sig"))
|
||||
log = (directory / "probe.stdout.log").read_text(encoding="utf-8-sig")
|
||||
match = re.search(r"arm-ms=([0-9.]+) instructions=(\d+) journal-pages=128 nat-arp-replies=32", log)
|
||||
if not match or manifest["ProbeExitCode"] != 0 or int(match.group(2)) != 655360000:
|
||||
raise ValueError(f"Probe did not finish the exact work: {directory}")
|
||||
collection = manifest.get("Collection", "WPRSystemAndNative" if "Profile" in manifest else None)
|
||||
requested = collection != "NoTrace"
|
||||
if f"trace-requested={int(requested)} trace-enabled={int(requested)}" not in log:
|
||||
raise ValueError(f"Probe trace state does not match collection: {directory}")
|
||||
if collection == "WPRSystemAndNative":
|
||||
decoder = json.loads((directory / "decoder-summary.json").read_text(encoding="utf-8-sig"))
|
||||
if any(decoder.get(key) != 0 for key in
|
||||
("status", "native_decode_errors", "events_lost", "buffers_lost")):
|
||||
raise ValueError(f"WPR trace has decode errors or lost events: {directory}")
|
||||
return {"directory": str(directory), "collection": collection,
|
||||
"hash": manifest["ProbeSHA256"], "pdb_hash": manifest["ProbePDBSHA256"],
|
||||
"affinity_mask": manifest.get("ProbeAffinityMask", 0),
|
||||
"observed_affinity_mask": manifest.get("ProbeObservedAffinityMask"),
|
||||
"arm_ms": float(match.group(1)), "wall_ms": 1000 * manifest["ProbeWallSeconds"],
|
||||
"cpu_ms": 1000 * manifest["ProbeCPUSeconds"]}
|
||||
|
||||
|
||||
def upper_95(values):
|
||||
# Two-sided Student 95% upper bound; report but never equate with a
|
||||
# guarantee for a different (native Wii) workload.
|
||||
critical = {4: 3.182, 5: 2.776, 6: 2.571, 7: 2.447, 8: 2.365}
|
||||
return statistics.mean(values) + critical[len(values)] * statistics.stdev(values) / math.sqrt(len(values))
|
||||
|
||||
|
||||
def compare(directories):
|
||||
runs = [load(path) for path in directories]
|
||||
on_mode = runs[1]["collection"] if len(runs) >= 2 else None
|
||||
if (on_mode not in ("PrivateUserMode", "WPRSystemAndNative") or
|
||||
len(runs) < 4 or len(runs) > 16 or len(runs) % 2 or
|
||||
any(r["collection"] != kind for r, kind in
|
||||
zip(runs, ("NoTrace", on_mode) * (len(runs) // 2)))):
|
||||
raise ValueError("Provide at least two OFF/ON pairs in alternating order")
|
||||
if len({(r["hash"], r["pdb_hash"]) for r in runs}) != 1:
|
||||
raise ValueError("The compared executables or PDBs differ")
|
||||
if len({r["affinity_mask"] for r in runs}) != 1 or any(
|
||||
r["affinity_mask"] and r["observed_affinity_mask"] != r["affinity_mask"]
|
||||
for r in runs):
|
||||
raise ValueError("Probe CPU affinity differs across compared runs")
|
||||
pairs = []
|
||||
for off, on in zip(runs[::2], runs[1::2]):
|
||||
pairs.append({"off": off["directory"], "on": on["directory"],
|
||||
"arm_phase_percent": 100 * (on["arm_ms"] / off["arm_ms"] - 1),
|
||||
"whole_probe_percent": 100 * (on["wall_ms"] / off["wall_ms"] - 1),
|
||||
"cpu_percent": 100 * (on["cpu_ms"] / off["cpu_ms"] - 1),
|
||||
"non_arm_wall_delta_ms": (on["wall_ms"] - on["arm_ms"]) -
|
||||
(off["wall_ms"] - off["arm_ms"])})
|
||||
arm_differences = [pair["arm_phase_percent"] for pair in pairs]
|
||||
wall_differences = [pair["whole_probe_percent"] for pair in pairs]
|
||||
cpu_differences = [pair["cpu_percent"] for pair in pairs]
|
||||
baseline_range = 100 * (max(r["arm_ms"] for r in runs[::2]) /
|
||||
min(r["arm_ms"] for r in runs[::2]) - 1)
|
||||
wall_baseline_range = 100 * (max(r["wall_ms"] for r in runs[::2]) /
|
||||
min(r["wall_ms"] for r in runs[::2]) - 1)
|
||||
bounds = (dict(arm_phase_percent=upper_95(arm_differences),
|
||||
whole_probe_percent=upper_95(wall_differences),
|
||||
cpu_percent=upper_95(cpu_differences)) if len(pairs) >= 4 else None)
|
||||
qualified = (on_mode == "WPRSystemAndNative" and bounds is not None and
|
||||
baseline_range <= 2 and wall_baseline_range <= 2 and
|
||||
all(value <= 2 for value in bounds.values()))
|
||||
return {"schema": 1, "on_collection": on_mode, "runs": runs, "pairs": pairs,
|
||||
"mean_arm_phase_percent": statistics.mean(arm_differences),
|
||||
"off_arm_phase_range_percent": baseline_range,
|
||||
"off_wall_range_percent": wall_baseline_range,
|
||||
"upper_95_percent": bounds,
|
||||
"mean_non_arm_wall_delta_ms": statistics.mean(p["non_arm_wall_delta_ms"] for p in pairs),
|
||||
"two_percent_budget_qualified": qualified,
|
||||
"note": "Fixed-work local collector cost only, not a native Wii speed estimate. Four or more OFF/ON pairs, low baseline spread, and upper bounds below 2% are required for this conservative local gate."}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("captures", nargs="+", type=Path)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
result = compare(args.captures)
|
||||
with args.output.open("x", encoding="utf-8") as target:
|
||||
json.dump(result, target, indent=2)
|
||||
print(json.dumps({key: result[key] for key in
|
||||
("on_collection", "pairs", "mean_arm_phase_percent",
|
||||
"off_arm_phase_range_percent", "off_wall_range_percent",
|
||||
"upper_95_percent", "mean_non_arm_wall_delta_ms",
|
||||
"two_percent_budget_qualified")}, indent=2))
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Check a local P1 trace against the probe's known work; never qualify a Wii run."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from analyze_p1 import analyze, read_relevant_events
|
||||
|
||||
|
||||
def sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest().upper()
|
||||
|
||||
|
||||
def qualify(directory, events_path=None, xperf_summary_path=None):
|
||||
manifest = json.loads((directory / "manifest.json").read_text(encoding="utf-8-sig"))
|
||||
decoder = json.loads((directory / "decoder-summary.json").read_text(encoding="utf-8-sig"))
|
||||
source = events_path or directory / "events.jsonl"
|
||||
report = analyze(read_relevant_events(source, manifest["ProbePID"]),
|
||||
manifest["ProbePID"])
|
||||
log = (directory / "probe.stdout.log").read_text(encoding="utf-8-sig")
|
||||
events = read_relevant_events(source, manifest["ProbePID"])
|
||||
xperf_path = xperf_summary_path or directory / "xperf-summary.json"
|
||||
xperf = json.loads(xperf_path.read_text(encoding="utf-8-sig")) if xperf_path.exists() else {}
|
||||
counters = {(c["tid"], c["counter"]): c["value"]
|
||||
for c in report["last_counters_by_thread"]}
|
||||
# The local probe has one worker thread. Values are cumulative and exact.
|
||||
worker = next(e["tid"] for e in events if e["native"] and e["event"] == "Marker"
|
||||
and e["data"].get("Name") == "ProbeBegin")
|
||||
checks = {
|
||||
"probe_exit_success": manifest.get("ProbeExitCode") == 0,
|
||||
"probe_self_checks": "P1Probe exact=OK" in log and "journal-pages=128 nat-arp-replies=32" in log,
|
||||
"trace_requested_and_enabled": "trace-requested=1 trace-enabled=1" in log,
|
||||
"probe_hash_unchanged": sha256(Path(manifest["Probe"])) == manifest["ProbeSHA256"],
|
||||
"probe_pdb_present_and_unchanged": bool(manifest.get("ProbePDBSHA256")) and
|
||||
sha256(Path(manifest["ProbePDB"])) == manifest["ProbePDBSHA256"],
|
||||
"etl_decoded_without_loss": decoder.get("status") == 0
|
||||
and decoder.get("events_lost") == 0 and decoder.get("buffers_lost") == 0
|
||||
and decoder.get("native_decode_errors") == 0,
|
||||
"qpc_frequency_agrees": decoder.get("qpc_frequency") == report["qpc_frequency"],
|
||||
"arm_jit_mapping": any(e["native"] and e["event"] == "CodeMap"
|
||||
and e["data"].get("Domain") == "ARM"
|
||||
and e["data"].get("Kind") == "thumb" for e in events),
|
||||
"nested_scope_consistency": report["scope_samples"] >= 128 and
|
||||
report["accounting_errors"] == 0,
|
||||
"no_counter_or_snapshot_regression": not report["counter_regressions"] and
|
||||
not report["snapshot_regressions"],
|
||||
"reset_distinct_generation": len(report["core_generations"]) >= 2,
|
||||
"journal_exact": counters.get((worker, 0)) == 128 and
|
||||
counters.get((worker, 1)) == 128 * (12 + 4 + 0x840 + 20) and
|
||||
counters.get((worker, 2)) == 0,
|
||||
"local_nat_exact": counters.get((worker, 3)) == 32 and
|
||||
counters.get((worker, 5)) == 32 and
|
||||
counters.get((worker, 7)) == 0 and
|
||||
counters.get((worker, 8)) == 32 and
|
||||
counters.get((worker, 10)) == 0 and
|
||||
report["usb_queue_events"] >= 64,
|
||||
}
|
||||
system_checks = {
|
||||
"arm_cpu_sample": report["cpu_leaf_samples"].get("ARM", 0) > 0,
|
||||
"known_sleep_classified_waiting": bool(report["known_sleep"] and
|
||||
report["known_sleep"]["wait_evidence_present"]),
|
||||
"xperf_clock_and_target_events": xperf.get("anchors", 0) >= 2 and
|
||||
xperf.get("max_anchor_error_qpc", float("inf")) <=
|
||||
report["qpc_frequency"] / 100_000 and
|
||||
xperf.get("xperf_invalid_target_events") == 0,
|
||||
}
|
||||
return {
|
||||
"schema": 1, "local_provider_passed": all(checks.values()),
|
||||
"full_collector_passed": all(checks.values()) and all(system_checks.values()),
|
||||
"native_update_ready": False, "provider_checks": checks,
|
||||
"system_checks": system_checks,
|
||||
"remaining": ["Qualify real PPC JIT workload and native C++ stacks",
|
||||
"Quantify full collector overhead on alternating fixed-work runs",
|
||||
"Validate CPU domain coverage and critical path on a native trace"],
|
||||
"analysis": report,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("capture", type=Path)
|
||||
parser.add_argument("--events", type=Path,
|
||||
help="Combined native and xperf JSONL; defaults to capture/events.jsonl")
|
||||
parser.add_argument("--xperf-summary", type=Path,
|
||||
help="Summary from xperf_to_p1.py for the same combined events")
|
||||
parser.add_argument("--output", type=Path,
|
||||
help="New qualification JSON path; defaults to capture/qualification.json")
|
||||
args = parser.parse_args()
|
||||
result = qualify(args.capture, args.events, args.xperf_summary)
|
||||
output = args.output or args.capture / "qualification.json"
|
||||
with output.open("x", encoding="utf-8") as target:
|
||||
json.dump(result, target, indent=2)
|
||||
print(json.dumps({"local_provider_passed": result["local_provider_passed"],
|
||||
"full_collector_passed": result["full_collector_passed"],
|
||||
"provider_checks": result["provider_checks"],
|
||||
"system_checks": result["system_checks"], "output": str(output)}, indent=2))
|
||||
raise SystemExit(0 if result["local_provider_passed"] else 1)
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Summarize CPU leaf samples per thread from an already-aligned P1 capture.
|
||||
|
||||
These are sample counts, not exact CPU times or a critical-path attribution.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import Counter, defaultdict
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from analyze_p1 import percentile
|
||||
|
||||
|
||||
def summarize(path, pid):
|
||||
by_thread = defaultdict(Counter)
|
||||
processors = defaultdict(Counter)
|
||||
sample_begin = None
|
||||
sample_end = None
|
||||
with path.open(encoding="utf-8") as source:
|
||||
for line in source:
|
||||
event = json.loads(line)
|
||||
if event["native"] or event["pid"] != pid or event["event"] != "SampledProfile":
|
||||
continue
|
||||
data = event["data"]
|
||||
tid = int(data["ThreadId"])
|
||||
weight = int(data.get("Count", 1))
|
||||
by_thread[tid][data.get("Symbol", "") or "<unresolved>"] += weight
|
||||
processors[tid][int(data["Processor"])] += weight
|
||||
sample_begin = event["qpc"] if sample_begin is None else min(sample_begin, event["qpc"])
|
||||
sample_end = event["qpc"] if sample_end is None else max(sample_end, event["qpc"])
|
||||
if sample_begin is None:
|
||||
raise ValueError("No target CPU samples in the selected window")
|
||||
frequency = None
|
||||
previous_frame = {}
|
||||
frame_deltas = defaultdict(list)
|
||||
snapshots = {}
|
||||
with path.open(encoding="utf-8") as source:
|
||||
for line in source:
|
||||
event = json.loads(line)
|
||||
if not event["native"] or event["pid"] != pid:
|
||||
continue
|
||||
qpc = event["qpc"]
|
||||
if not sample_begin <= qpc <= sample_end:
|
||||
continue
|
||||
data = event["data"]
|
||||
if event["event"] == "Marker" and data["Name"] == "PerformanceMetricsReset":
|
||||
previous_frame.clear()
|
||||
elif event["event"] == "Frame":
|
||||
if frequency is None:
|
||||
frequency = int(data["QPCFrequency"])
|
||||
elif frequency != int(data["QPCFrequency"]):
|
||||
raise ValueError("Inconsistent frame QPC frequency")
|
||||
kind = data["Kind"]
|
||||
if kind in previous_frame:
|
||||
frame_deltas[kind].append(1000 * (qpc - previous_frame[kind]) / frequency)
|
||||
previous_frame[kind] = qpc
|
||||
elif event["event"] == "Snapshot":
|
||||
core = int(data["CoreId"])
|
||||
if core not in snapshots:
|
||||
snapshots[core] = [data, data]
|
||||
else:
|
||||
snapshots[core][1] = data
|
||||
frames = {kind: {"intervals": len(deltas), "p50_ms": percentile(deltas, .5),
|
||||
"p95_ms": percentile(deltas, .95), "p99_ms": percentile(deltas, .99)}
|
||||
for kind, deltas in frame_deltas.items()}
|
||||
return {
|
||||
"pid": pid,
|
||||
"sample_count": sum(sum(symbols.values()) for symbols in by_thread.values()),
|
||||
"sample_window_qpc": {"begin": sample_begin, "end": sample_end,
|
||||
"duration_seconds": (sample_end - sample_begin) / frequency
|
||||
if frequency else None},
|
||||
"frame_intervals_in_sample_window": frames,
|
||||
"snapshot_deltas_in_sample_window": {
|
||||
str(core): {field: last[field] - first[field]
|
||||
for field in ("Instructions", "NativeInstructions", "SlowReads",
|
||||
"SlowWrites", "Compiles", "Fallbacks")}
|
||||
for core, (first, last) in snapshots.items()
|
||||
},
|
||||
"note": "Leaf sample counts and host frame intervals within the same selected window; no exact CPU duration or critical-path claim.",
|
||||
"threads": [
|
||||
{"tid": tid, "samples": sum(symbols.values()),
|
||||
"processors": dict(sorted(processors[tid].items())),
|
||||
"top_symbols": [{"symbol": symbol, "samples": count}
|
||||
for symbol, count in symbols.most_common(12)]}
|
||||
for tid, symbols in sorted(by_thread.items(),
|
||||
key=lambda item: -sum(item[1].values()))
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("events", type=Path)
|
||||
parser.add_argument("--pid", required=True, type=int)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
result = summarize(args.events, args.pid)
|
||||
with args.output.open("x", encoding="utf-8") as target:
|
||||
json.dump(result, target, indent=2)
|
||||
print(f"{result['sample_count']} CPU samples from {len(result['threads'])} threads")
|
||||
@@ -0,0 +1,195 @@
|
||||
import unittest
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from analyze_p1 import CodeMap, SampledCodeMap, analyze, percentile, read_relevant_events
|
||||
from xperf_to_p1 import convert, parse_pid
|
||||
|
||||
|
||||
class AnalysisTests(unittest.TestCase):
|
||||
def test_temporal_address_reuse(self):
|
||||
m = CodeMap()
|
||||
def event(begin, end, kind, pc=0):
|
||||
m.event(dict(Begin=begin, End=end, Kind=kind, GuestPC=pc, Domain="ARM"))
|
||||
event(100, 200, "thumb", 4)
|
||||
self.assertEqual(m.lookup(125), ("ARM", 4))
|
||||
event(120, 180, "invalidate")
|
||||
self.assertEqual(m.lookup(125)[0], "UnresolvedNative")
|
||||
self.assertEqual(m.lookup(190), ("ARM", 4))
|
||||
event(120, 180, "thumb", 8)
|
||||
self.assertEqual(m.lookup(125), ("ARM", 8))
|
||||
m.event(dict(Begin=100, End=200, Kind="block-near", GuestPC=0x8000, Domain="PPC"))
|
||||
self.assertEqual(m.lookup(125), ("PPC", 0x8000))
|
||||
self.assertEqual(m.lookup(190), ("PPC", 0x8000))
|
||||
|
||||
def test_invalid_range(self):
|
||||
with self.assertRaises(ValueError):
|
||||
CodeMap().event(dict(Begin=10, End=9))
|
||||
|
||||
def test_sampled_map_preserves_temporal_overlap_semantics(self):
|
||||
sampled = SampledCodeMap([125, 190, 250])
|
||||
sampled.event(dict(Begin=100, End=200, Kind="thumb", Domain="ARM", GuestPC=4))
|
||||
self.assertEqual(sampled.lookup(125), ("ARM", 4))
|
||||
sampled.event(dict(Begin=120, End=180, Kind="invalidate", Domain="ARM", GuestPC=0))
|
||||
self.assertEqual(sampled.lookup(125), ("UnresolvedNative", None))
|
||||
self.assertEqual(sampled.lookup(190), ("ARM", 4))
|
||||
sampled.event(dict(Begin=120, End=180, Kind="thumb", Domain="ARM", GuestPC=8))
|
||||
self.assertEqual(sampled.lookup(125), ("ARM", 8))
|
||||
self.assertEqual(sampled.lookup(250), ("UnresolvedNative", None))
|
||||
|
||||
def test_percentiles_are_frame_intervals(self):
|
||||
self.assertEqual(percentile([10, 20, 30], .5), 20)
|
||||
self.assertIsNone(percentile([], .5))
|
||||
|
||||
def test_missing_telemetry_rejected(self):
|
||||
with self.assertRaises(ValueError):
|
||||
analyze([], 12)
|
||||
|
||||
def test_unknown_schema_rejected(self):
|
||||
with self.assertRaises(ValueError):
|
||||
analyze([dict(native=True, pid=12, tid=1, qpc=0, data=dict(Schema=99))], 12)
|
||||
|
||||
@staticmethod
|
||||
def native(event, qpc, **data):
|
||||
return dict(native=True, pid=12, tid=1, qpc=qpc, event=event, data=dict(Schema=1, **data))
|
||||
|
||||
def test_exclusive_accounting_does_not_sum_inclusive_regions(self):
|
||||
rows = [self.native("Marker", 0, QPCFrequency=1000, Name="ProbeBegin")]
|
||||
for region, inclusive, exclusive in ((0, 100, 20), (1, 80, 50), (6, 30, 30)):
|
||||
rows.append(self.native("ScopeSample", 100, Region=region, Sample=1,
|
||||
SamplingDenominator=1, Calls=1, AccountingErrors=0,
|
||||
InclusiveQPC=inclusive, ExclusiveQPC=exclusive))
|
||||
result = analyze(rows, 12)
|
||||
self.assertEqual(result["accounting_errors"], 0)
|
||||
rows[-1]["data"]["ExclusiveQPC"] = 31
|
||||
self.assertEqual(analyze(rows, 12)["accounting_errors"], 1)
|
||||
|
||||
def test_frames_do_not_bridge_reset(self):
|
||||
rows = [self.native("Marker", 0, QPCFrequency=1000, Name="ProbeBegin")]
|
||||
for t in (10, 20):
|
||||
rows.append(self.native("Frame", t, QPC=t, Kind="produced"))
|
||||
rows.append(self.native("Marker", 100, Name="PerformanceMetricsReset"))
|
||||
for t in (1000, 1010):
|
||||
rows.append(self.native("Frame", t, QPC=t, Kind="produced"))
|
||||
self.assertEqual(analyze(rows, 12)["frame_intervals"]["produced"],
|
||||
dict(intervals=2, p50_ms=10, p95_ms=10, p99_ms=10))
|
||||
|
||||
def test_counter_regression_fails_even_with_good_scopes(self):
|
||||
rows = [self.native("Marker", 0, QPCFrequency=1000, Name="ProbeBegin"),
|
||||
self.native("Counter", 1, Counter=0, Value=5),
|
||||
self.native("Counter", 2, Counter=0, Value=4)]
|
||||
self.assertEqual(len(analyze(rows, 12)["counter_regressions"]), 1)
|
||||
|
||||
def test_kernel_sample_and_known_sleep_are_separate_evidence(self):
|
||||
rows = [self.native("Marker", 0, QPCFrequency=1000, Name="ProbeBegin"),
|
||||
self.native("CodeMap", 1, Begin=100, End=200, Domain="ARM",
|
||||
GuestPC=4, Kind="thumb"),
|
||||
dict(native=False, pid=0, tid=0, qpc=10, event="SampledProfile",
|
||||
data=dict(ThreadId=1, InstructionPointer=125)),
|
||||
self.native("Marker", 20, Name="KnownSleepBegin"),
|
||||
dict(native=False, pid=0, tid=0, qpc=30, event="CSwitch",
|
||||
data=dict(OldThreadId=1, NewThreadId=2, OldThreadState=5)),
|
||||
self.native("Marker", 800, Name="KnownSleepEnd")]
|
||||
result = analyze(rows, 12)
|
||||
self.assertEqual(result["cpu_leaf_samples"], {"ARM": 1})
|
||||
self.assertEqual(result["hot_generated_pcs"][0]["pc"], "0x4")
|
||||
self.assertTrue(result["known_sleep"]["wait_evidence_present"])
|
||||
self.assertEqual(result["known_sleep"]["wall_ms"], 780)
|
||||
|
||||
def test_jit_sample_after_invalidation_is_not_attributed_to_old_block(self):
|
||||
rows = [self.native("Marker", 0, QPCFrequency=1000, Name="ProbeBegin"),
|
||||
self.native("CodeMap", 1, Begin=100, End=200, Domain="ARM",
|
||||
GuestPC=4, Kind="thumb"),
|
||||
dict(native=False, pid=0, tid=1, qpc=2, event="SampledProfile",
|
||||
data=dict(InstructionPointer=125)),
|
||||
self.native("CodeMap", 3, Begin=100, End=200, Domain="ARM",
|
||||
GuestPC=0, Kind="invalidate"),
|
||||
dict(native=False, pid=0, tid=1, qpc=4, event="SampledProfile",
|
||||
data=dict(InstructionPointer=125))]
|
||||
self.assertEqual(analyze(rows, 12)["cpu_leaf_samples"],
|
||||
{"ARM": 1, "UnresolvedNative": 1})
|
||||
|
||||
def test_static_module_is_not_mistaken_for_unmapped_jit(self):
|
||||
rows = [self.native("Marker", 0, QPCFrequency=1000, Name="ProbeBegin"),
|
||||
dict(native=False, pid=12, tid=1, qpc=1, event="SampledProfile",
|
||||
data=dict(ThreadId=1, InstructionPointer=999, Count=3,
|
||||
Symbol="TraceProbeP1.exe!IOS::LLE::ARMCore::Read32"))]
|
||||
report = analyze(rows, 12)
|
||||
self.assertEqual(report["cpu_leaf_samples"], {"HostC++": 3})
|
||||
self.assertEqual(report["hot_static_symbols"][0]["samples"], 3)
|
||||
|
||||
def test_xperf_classic_events_align_to_native_qpc(self):
|
||||
native = [self.native("Marker", 1001000, QPCFrequency=10000000, Name="ProbeBegin"),
|
||||
self.native("CodeMap", 1001500, Begin=100, End=200, Domain="ARM",
|
||||
GuestPC=4, Kind="thumb"),
|
||||
self.native("Marker", 1011000, Name="KnownSleepBegin"),
|
||||
self.native("Marker", 1111000, Name="KnownSleepEnd")]
|
||||
rows = [["Dolphin.Performance/Marker/", "100000", "TraceProbeP1.exe (12)",
|
||||
"1"] + [""] * 8 + ["ProbeBegin"],
|
||||
["Dolphin.Performance/Marker/", "101000", "TraceProbeP1.exe (12)",
|
||||
"1"] + [""] * 8 + ["KnownSleepBegin"],
|
||||
["Dolphin.Performance/Marker/", "111000", "TraceProbeP1.exe (12)",
|
||||
"1"] + [""] * 8 + ["KnownSleepEnd"],
|
||||
["SampledProfile", "100100", "TraceProbeP1.exe (12)", "1",
|
||||
"0x7d", "0", "", "", "2", "Unbatched"],
|
||||
["CSwitch", "101100", "Idle (0)", "2", "", "", "", "",
|
||||
"TraceProbeP1.exe (12)", "1", "", "", "Waiting"]]
|
||||
merged, summary = convert(rows, native, 12)
|
||||
report = analyze(merged, 12)
|
||||
self.assertEqual(summary["max_anchor_error_qpc"], 0)
|
||||
self.assertEqual(summary["cpu_sample_count"], 2)
|
||||
self.assertEqual(summary["cpu_samples_by_processor"], {0: 2})
|
||||
self.assertEqual(report["cpu_leaf_samples"], {"ARM": 2})
|
||||
self.assertTrue(report["known_sleep"]["wait_evidence_present"])
|
||||
self.assertEqual(parse_pid("Idle ( 0)"), 0)
|
||||
|
||||
def test_xperf_snapshots_align_without_named_markers(self):
|
||||
native = [self.native("Snapshot", 1001000, QPC=1000999,
|
||||
QPCFrequency=10000000, CoreId=1),
|
||||
self.native("Snapshot", 2001000, QPC=2000999,
|
||||
QPCFrequency=10000000, CoreId=1)]
|
||||
def snapshot(us, payload_qpc):
|
||||
row = ["Dolphin.Performance/Snapshot/", str(us), "DolphinNoGUI.exe (12)",
|
||||
"1"] + [""] * 8
|
||||
row[10] = str(payload_qpc)
|
||||
return row
|
||||
rows = [snapshot(100000, 1000999), snapshot(200000, 2000999),
|
||||
["SampledProfile", "150000", "DolphinNoGUI.exe (12)", "1",
|
||||
"0x7d", "0", "", "", "2", "DolphinNoGUI.exe!ARMCore::Run"]]
|
||||
merged, summary = convert(rows, native, 12)
|
||||
self.assertEqual(summary["anchors"], 2)
|
||||
self.assertEqual(summary["max_anchor_error_qpc"], 0)
|
||||
self.assertEqual(summary["cpu_sample_count"], 2)
|
||||
self.assertEqual(merged[1]["qpc"], 1501000)
|
||||
|
||||
def test_uninstrumented_process_thread_is_counted(self):
|
||||
native = [self.native("Snapshot", 1001000, QPC=1000999,
|
||||
QPCFrequency=10000000, CoreId=1, Instructions=1,
|
||||
NativeInstructions=1, SlowReads=0, SlowWrites=0,
|
||||
Compiles=0, Fallbacks=0),
|
||||
self.native("Snapshot", 2001000, QPC=2000999,
|
||||
QPCFrequency=10000000, CoreId=1, Instructions=2,
|
||||
NativeInstructions=2, SlowReads=0, SlowWrites=0,
|
||||
Compiles=0, Fallbacks=0)]
|
||||
rows = []
|
||||
for us, payload in ((100000, 1000999), (200000, 2000999)):
|
||||
row = ["Dolphin.Performance/Snapshot/", str(us), "DolphinNoGUI.exe (12)",
|
||||
"1"] + [""] * 8
|
||||
row[10] = str(payload)
|
||||
rows.append(row)
|
||||
rows.append(["SampledProfile", "150000", "DolphinNoGUI.exe (12)", "9",
|
||||
"0x7d", "0", "", "DolphinNoGUI.exe!Render", "3", "Unbatched"])
|
||||
merged, summary = convert(rows, native, 12)
|
||||
self.assertEqual(summary["cpu_sample_threads"], 1)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "events.jsonl"
|
||||
path.write_text("".join(json.dumps(event) + "\n" for event in merged),
|
||||
encoding="utf-8")
|
||||
report = analyze(read_relevant_events(path, 12), 12)
|
||||
self.assertEqual(report["native_threads"], [1])
|
||||
self.assertEqual(report["sampled_process_threads"], [9])
|
||||
self.assertEqual(report["hot_static_symbols"][0]["samples"], 3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,45 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from compare_p1 import compare
|
||||
|
||||
|
||||
class ComparisonTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def runs(on_mode, on_multiplier=1.01, off_values=(100, 100, 100, 100)):
|
||||
result = []
|
||||
for index, off in enumerate(off_values):
|
||||
for mode, value in (("NoTrace", off), (on_mode, off * on_multiplier)):
|
||||
result.append(dict(directory=f"run-{index}-{mode}", collection=mode,
|
||||
hash="same-exe", pdb_hash="same-pdb",
|
||||
affinity_mask=15, observed_affinity_mask=15, arm_ms=value,
|
||||
wall_ms=value * 1.2, cpu_ms=value * .95))
|
||||
return result
|
||||
|
||||
def test_repeated_low_noise_wpr_pairs_can_pass_local_budget(self):
|
||||
runs = self.runs("WPRSystemAndNative")
|
||||
with patch("compare_p1.load", side_effect=runs):
|
||||
report = compare(list(range(len(runs))))
|
||||
self.assertTrue(report["two_percent_budget_qualified"])
|
||||
self.assertAlmostEqual(report["upper_95_percent"]["arm_phase_percent"], 1)
|
||||
|
||||
def test_private_logger_never_qualifies_full_wpr_budget(self):
|
||||
runs = self.runs("PrivateUserMode")
|
||||
with patch("compare_p1.load", side_effect=runs):
|
||||
self.assertFalse(compare(list(range(len(runs))))["two_percent_budget_qualified"])
|
||||
|
||||
def test_large_baseline_spread_keeps_budget_unqualified(self):
|
||||
runs = self.runs("WPRSystemAndNative", off_values=(100, 104, 100, 104))
|
||||
with patch("compare_p1.load", side_effect=runs):
|
||||
self.assertFalse(compare(list(range(len(runs))))["two_percent_budget_qualified"])
|
||||
|
||||
def test_mismatched_affinity_rejected(self):
|
||||
runs = self.runs("WPRSystemAndNative")
|
||||
runs[3]["observed_affinity_mask"] = 255
|
||||
with patch("compare_p1.load", side_effect=runs):
|
||||
with self.assertRaisesRegex(ValueError, "affinity"):
|
||||
compare(list(range(len(runs))))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Merge xperf's classic kernel events with native TDH events on their QPC clock.
|
||||
|
||||
TDH may return ERROR_NOT_FOUND for classic kernel schemas on the analysis host.
|
||||
Xperf can still decode those MOF events from the same ETL. This is an offline
|
||||
adapter only: it never qualifies a native Wii update on its own.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import csv
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import statistics
|
||||
|
||||
|
||||
def parse_pid(label):
|
||||
match = re.search(r"\(\s*(\d+)\s*\)$", label.strip())
|
||||
return int(match.group(1)) if match else None
|
||||
|
||||
|
||||
def convert(rows, native, pid):
|
||||
native_markers = {e["data"]["Name"]: e["qpc"] for e in native
|
||||
if e["native"] and e["pid"] == pid and e["event"] == "Marker"}
|
||||
native_payload_qpc = {
|
||||
(e["event"], int(e["data"]["QPC"])): e["qpc"] for e in native
|
||||
if e["native"] and e["pid"] == pid and e["event"] in ("Marker", "Snapshot", "Frame")
|
||||
and "QPC" in e["data"]
|
||||
}
|
||||
frequencies = {e["data"]["QPCFrequency"] for e in native
|
||||
if e["native"] and e["pid"] == pid and "QPCFrequency" in e["data"]}
|
||||
if len(frequencies) != 1:
|
||||
raise ValueError("Missing or inconsistent native QPC frequency")
|
||||
frequency = frequencies.pop()
|
||||
native_tids = {e["tid"] for e in native if e["native"] and e["pid"] == pid}
|
||||
if not native_tids:
|
||||
raise ValueError("No native threads for requested PID")
|
||||
|
||||
# Xperf timestamps are trace-relative microseconds. Match the QPC payload
|
||||
# of periodic native events to their TDH event-header QPC. A real Dolphin
|
||||
# run need not emit two named markers, but normally emits many snapshots.
|
||||
anchors = []
|
||||
raw_samples = []
|
||||
raw_switches = []
|
||||
invalid_events = 0
|
||||
invalid_target_events = 0
|
||||
for row in rows:
|
||||
if len(row) < 2:
|
||||
continue
|
||||
kind = row[0].strip()
|
||||
if kind.startswith("InvalidEvent/"):
|
||||
invalid_events += 1
|
||||
if len(row) > 2 and parse_pid(row[2]) == pid:
|
||||
invalid_target_events += 1
|
||||
if not row[1].strip().isdigit(): # xperf's per-event header line
|
||||
continue
|
||||
if kind in ("Dolphin.Performance/Marker/", "Dolphin.Performance/Snapshot/",
|
||||
"Dolphin.Performance/Frame/") and len(row) >= 12 and parse_pid(row[2]) == pid:
|
||||
event = kind.split("/")[1]
|
||||
payload_qpc = row[10].strip()
|
||||
if payload_qpc.isdigit() and (event, int(payload_qpc)) in native_payload_qpc:
|
||||
anchors.append((int(row[1]), native_payload_qpc[(event, int(payload_qpc))],
|
||||
f"{event}:{payload_qpc}"))
|
||||
elif event == "Marker" and len(row) >= 13:
|
||||
# Older synthetic captures also support named-marker matching.
|
||||
name = row[-1].strip()
|
||||
if name in native_markers:
|
||||
anchors.append((int(row[1]), native_markers[name], name))
|
||||
elif kind == "SampledProfile" and len(row) >= 9 and parse_pid(row[2]) == pid:
|
||||
tid = int(row[3])
|
||||
# Xperf does not CSV-quote C++ template commas in symbol names.
|
||||
# Count/type are the final two columns; rejoin the leaf symbol.
|
||||
# All threads of this PID matter: GPU/network threads may be hot
|
||||
# without ever emitting a native Dolphin.Performance event.
|
||||
raw_samples.append((int(row[1]), tid, int(row[4], 16), int(row[5]),
|
||||
int(row[-2]), ",".join(row[7:-2]).strip()))
|
||||
elif kind == "CSwitch" and len(row) >= 13:
|
||||
old_tid, new_tid = int(row[9]), int(row[3])
|
||||
if (old_tid in native_tids or new_tid in native_tids or
|
||||
parse_pid(row[2]) == pid or parse_pid(row[8]) == pid):
|
||||
raw_switches.append((int(row[1]), old_tid, new_tid, row[12].strip()))
|
||||
|
||||
if len(anchors) < 2:
|
||||
raise ValueError("Need at least two matching native/xperf QPC anchors")
|
||||
scale = frequency / 1_000_000
|
||||
offset = statistics.median(qpc - us * scale for us, qpc, _ in anchors)
|
||||
max_anchor_error = max(abs(round(us * scale + offset) - qpc) for us, qpc, _ in anchors)
|
||||
if max_anchor_error > frequency / 100_000: # More than 10 microseconds.
|
||||
raise ValueError(f"Native/xperf clocks do not agree: {max_anchor_error} QPC ticks")
|
||||
|
||||
def qpc(us):
|
||||
return round(us * scale + offset)
|
||||
|
||||
system = [dict(native=False, event="SampledProfile", qpc=qpc(us), pid=pid, tid=tid,
|
||||
data=dict(ThreadId=tid, InstructionPointer=ip, Processor=processor,
|
||||
Count=count, Symbol=symbol))
|
||||
for us, tid, ip, processor, count, symbol in raw_samples]
|
||||
system.extend(dict(native=False, event="CSwitch", qpc=qpc(us), pid=0, tid=0,
|
||||
data=dict(OldThreadId=old_tid, NewThreadId=new_tid,
|
||||
OldThreadState=5 if state == "Waiting" else -1,
|
||||
OldState=state))
|
||||
for us, old_tid, new_tid, state in raw_switches)
|
||||
samples_by_processor = Counter()
|
||||
for _, _, _, processor, count, _ in raw_samples:
|
||||
samples_by_processor[processor] += count
|
||||
summary = dict(schema=1, pid=pid, qpc_frequency=frequency, anchors=len(anchors),
|
||||
max_anchor_error_qpc=max_anchor_error, cpu_sample_records=len(raw_samples),
|
||||
cpu_sample_count=sum(count for _, _, _, _, count, _ in raw_samples),
|
||||
cpu_sample_threads=len({tid for _, tid, _, _, _, _ in raw_samples}),
|
||||
cpu_samples_by_processor=dict(sorted(samples_by_processor.items())),
|
||||
context_switches=len(raw_switches), xperf_invalid_events=invalid_events,
|
||||
xperf_invalid_target_events=invalid_target_events,
|
||||
note="Classic kernel events decoded by xperf; invalid events and missing symbols remain separate quality gates.")
|
||||
return sorted(native + system, key=lambda e: e["qpc"]), summary
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("xperf_csv", type=Path)
|
||||
parser.add_argument("native_jsonl", type=Path)
|
||||
parser.add_argument("--pid", required=True, type=int)
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
parser.add_argument("--summary", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
with args.native_jsonl.open(encoding="utf-8") as source:
|
||||
native_events = [json.loads(line) for line in source]
|
||||
with args.xperf_csv.open(encoding="utf-8", errors="replace", newline="") as source:
|
||||
events, result = convert(csv.reader(source, skipinitialspace=True), native_events, args.pid)
|
||||
with args.output.open("x", encoding="utf-8") as target:
|
||||
for event in events:
|
||||
target.write(json.dumps(event, separators=(",", ":")) + "\n")
|
||||
with args.summary.open("x", encoding="utf-8") as target:
|
||||
json.dump(result, target, indent=2)
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -0,0 +1,11 @@
|
||||
build/
|
||||
__pycache__/
|
||||
*.elf
|
||||
*.map
|
||||
/build/
|
||||
/apps/
|
||||
/captures/
|
||||
/__pycache__/
|
||||
/wii-conformance.dol
|
||||
/wii-conformance.elf
|
||||
/wii-conformance.map
|
||||
@@ -0,0 +1,45 @@
|
||||
.SUFFIXES:
|
||||
|
||||
ifeq ($(strip $(DEVKITPPC)),)
|
||||
$(error Set DEVKITPPC to your devkitPPC installation)
|
||||
endif
|
||||
|
||||
include $(DEVKITPPC)/wii_rules
|
||||
|
||||
TARGET := wii-conformance
|
||||
BUILD := build
|
||||
SOURCES := source
|
||||
|
||||
CFLAGS := -O2 -g -Wall -Wextra $(MACHDEP) $(INCLUDE)
|
||||
LDFLAGS = -g $(MACHDEP) -Wl,-Map,$(OUTPUT).map
|
||||
LIBS := -lwiiuse -lbte -lfat -logc -lm
|
||||
|
||||
ifneq ($(BUILD),$(notdir $(CURDIR)))
|
||||
export OUTPUT := $(CURDIR)/$(TARGET)
|
||||
export VPATH := $(CURDIR)/source
|
||||
export DEPSDIR := $(CURDIR)/$(BUILD)
|
||||
export OFILES := main.o
|
||||
export INCLUDE := -I$(LIBOGC_INC)
|
||||
export LIBPATHS := -L$(LIBOGC_LIB)
|
||||
export LD := $(CC)
|
||||
|
||||
.PHONY: all clean
|
||||
all: | $(BUILD)
|
||||
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
|
||||
@mkdir -p apps/wii-conformance
|
||||
@cp $(TARGET).dol apps/wii-conformance/boot.dol
|
||||
@cp meta.xml apps/wii-conformance/meta.xml
|
||||
|
||||
$(BUILD):
|
||||
@mkdir -p $@
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILD) $(OUTPUT).elf $(OUTPUT).dol $(OUTPUT).elf.map
|
||||
else
|
||||
DEPENDS := $(OFILES:.o=.d)
|
||||
|
||||
$(OUTPUT).dol: $(OUTPUT).elf
|
||||
$(OUTPUT).elf: $(OFILES)
|
||||
|
||||
-include $(DEPENDS)
|
||||
endif
|
||||
@@ -0,0 +1,59 @@
|
||||
# Wii IOS conformance probe
|
||||
|
||||
This is the first black-box comparison probe for the original IOS running on a
|
||||
real Wii and under this fork's Starlet LLE mode. Build **one** DOL and run that
|
||||
same file in both places. It records 64 rounds of IPC-backed resource-manager
|
||||
open/close calls in all four open modes, missing-device errors, root directory
|
||||
counts, installed-title counts, and an SD write/read/hash/cleanup round trip.
|
||||
The output contains 1,860 numbered observations. The probe requests no NAND
|
||||
writes, installs no IOS, and does not blindly scan MMIO registers. It creates and
|
||||
removes only `sd:/wii-conformance-data.bin` on the SD card.
|
||||
|
||||
## Build
|
||||
|
||||
Install devkitPPC and libogc/libfat using devkitPro, set `DEVKITPRO` and
|
||||
`DEVKITPPC` in the build shell, then run `make` in this directory. The output is
|
||||
`wii-conformance.dol`. On this Windows machine, the following command was used
|
||||
successfully from this directory in PowerShell:
|
||||
|
||||
```powershell
|
||||
& 'C:\devkitPro\msys2\usr\bin\bash.exe' -c 'export DEVKITPRO=/c/devkitPro DEVKITPPC=/c/devkitPro/devkitPPC; export PATH=/c/devkitPro/devkitPPC/bin:/c/devkitPro/tools/bin:/usr/bin:/bin; make'
|
||||
```
|
||||
|
||||
The ready-to-copy Homebrew Channel folder is `apps/wii-conformance/`, containing
|
||||
`boot.dol` and `meta.xml`. This build has not yet been run on a physical Wii.
|
||||
|
||||
## Capture
|
||||
|
||||
1. Use a Wii and a Dolphin LLE profile with the same IOS version and revision.
|
||||
For exact directory/title counts, use the same NAND snapshot, starting state,
|
||||
and loaded title. The SD cards need enough free space for a 4 KiB test file
|
||||
and the CSV output.
|
||||
2. Copy `apps/wii-conformance/` to the `apps/` folder on each SD card and launch
|
||||
it through the Homebrew Channel. Use the exact same `boot.dol` on both runs.
|
||||
Dolphin LLE requires a configured writable virtual SD image.
|
||||
3. After the program returns, copy `sd:/wii-conformance.csv` from each SD card
|
||||
to separate files such as `wii.csv` and `dolphin.csv`.
|
||||
4. Compare them with `python compare.py wii.csv dolphin.csv`. Exit code 0 means
|
||||
all recorded observations agree, 1 reports differences, and 2 means the
|
||||
captures are invalid or use different IOS versions.
|
||||
|
||||
The CSV contains no keys or NAND contents. An allocated IOS file descriptor is
|
||||
normalized to success/failure because its numeric value depends on unrelated
|
||||
open handles. Call return codes and output counts remain exact. Time is not
|
||||
compared in this first suite because scheduling and output I/O introduce noise.
|
||||
`captures/` is kept local and ignored by Git until each capture's console or
|
||||
emulator provenance, IOS revision, probe binary hash, and initial NAND state
|
||||
are documented. A filename alone is not evidence that a capture came from a
|
||||
physical Wii.
|
||||
|
||||
## Interpretation and next suites
|
||||
|
||||
A mismatch identifies a reproducible behavior to investigate; it does not by
|
||||
itself locate a bad register. Trace the first failing request inside Dolphin's
|
||||
Wii IPC, Starlet memory bus, and the relevant device. The next test suites
|
||||
should use explicit stimulus/response cases for IPC flag acknowledgements,
|
||||
shared-memory cache visibility, DMA, IRQ ordering, and individual devices.
|
||||
PPC homebrew cannot inspect every Starlet register while Nintendo IOS is
|
||||
running. A separate Starlet-side probe under MINI/BootMii can test raw
|
||||
hardware registers, but its results must be labeled as MINI-mode tests.
|
||||
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare two runs of the identical Wii conformance DOL."""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
METADATA = ("schema", "rounds", "observations", "ios_version", "ios_revision")
|
||||
HEADER = ["seq", "test", "round", "value", "extra"]
|
||||
|
||||
|
||||
def read_capture(path: Path):
|
||||
with path.open(newline="", encoding="utf-8") as stream:
|
||||
rows = csv.reader(stream)
|
||||
metadata = {}
|
||||
for key in METADATA:
|
||||
row = next(rows, None)
|
||||
if row is None or len(row) != 2 or row[0] != key:
|
||||
raise ValueError(f"{path}: expected metadata row {key!r}")
|
||||
metadata[key] = int(row[1])
|
||||
if next(rows, None) != HEADER:
|
||||
raise ValueError(f"{path}: invalid observation header")
|
||||
observations = []
|
||||
for line, row in enumerate(rows, start=7):
|
||||
if len(row) != 5:
|
||||
raise ValueError(f"{path}:{line}: expected 5 columns")
|
||||
seq, test, round_number, value, extra = row
|
||||
if int(seq) != len(observations):
|
||||
raise ValueError(f"{path}:{line}: sequence is not contiguous")
|
||||
observations.append((test, int(round_number), int(value), int(extra)))
|
||||
if len(observations) != metadata["observations"]:
|
||||
raise ValueError(
|
||||
f"{path}: expected {metadata['observations']} observations, "
|
||||
f"found {len(observations)}"
|
||||
)
|
||||
return metadata, observations
|
||||
|
||||
|
||||
def compare(reference, candidate, limit=20):
|
||||
reference_meta, reference_rows = reference
|
||||
candidate_meta, candidate_rows = candidate
|
||||
for key in METADATA:
|
||||
if reference_meta[key] != candidate_meta[key]:
|
||||
print(f"Incomparable runs: {key}: Wii={reference_meta[key]}, Dolphin={candidate_meta[key]}")
|
||||
return 2
|
||||
|
||||
differences = 0
|
||||
for index in range(max(len(reference_rows), len(candidate_rows))):
|
||||
expected = reference_rows[index] if index < len(reference_rows) else None
|
||||
actual = candidate_rows[index] if index < len(candidate_rows) else None
|
||||
if expected == actual:
|
||||
continue
|
||||
differences += 1
|
||||
if differences <= limit:
|
||||
print(f"#{index} Wii={expected} Dolphin={actual}")
|
||||
|
||||
if differences:
|
||||
print(f"{differences} differences across {max(len(reference_rows), len(candidate_rows))} observations")
|
||||
return 1
|
||||
print(f"Identical: {len(reference_rows)} observations")
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("wii", type=Path, help="CSV from the real Wii")
|
||||
parser.add_argument("dolphin", type=Path, help="CSV from Dolphin")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return compare(read_capture(args.wii), read_capture(args.dolphin))
|
||||
except (OSError, ValueError) as error:
|
||||
print(error, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<app version="1">
|
||||
<name>Wii IOS Conformance Probe</name>
|
||||
<coder>Dolphin Emulator Project</coder>
|
||||
<version>0.1</version>
|
||||
<short_description>Compare IOS behavior on Wii and Dolphin</short_description>
|
||||
<long_description>Runs repeatable IPC, IOS, and SD tests and saves wii-conformance.csv to the SD card.</long_description>
|
||||
</app>
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <fat.h>
|
||||
#include <gccore.h>
|
||||
#include <ogc/es.h>
|
||||
#include <ogc/ios.h>
|
||||
#include <ogc/ipc.h>
|
||||
#include <ogc/isfs.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// The same DOL must be run on the console and in Dolphin. Do not depend on
|
||||
// timings or on allocated IOS file descriptor numbers in the comparison.
|
||||
#define SCHEMA_VERSION 1
|
||||
#define ROUNDS 64
|
||||
#define RECORDS_PER_ROUND 29
|
||||
#define SKIPPED (-32768)
|
||||
#define SD_TEST_FILE "sd:/wii-conformance-data.bin"
|
||||
|
||||
static FILE* output;
|
||||
static unsigned int sequence;
|
||||
|
||||
static void record(const char* test, unsigned int round, int value, int extra)
|
||||
{
|
||||
if (output)
|
||||
fprintf(output, "%u,%s,%u,%d,%d\n", sequence, test, round, value, extra);
|
||||
++sequence;
|
||||
}
|
||||
|
||||
static void probe_open(const char* name, const char* path, unsigned int round, unsigned int mode)
|
||||
{
|
||||
char test[48];
|
||||
const s32 fd = IOS_Open(path, mode);
|
||||
snprintf(test, sizeof(test), "open_%s_mode%u", name, mode);
|
||||
// File descriptor allocation can legitimately differ between runs.
|
||||
record(test, round, fd >= 0 ? 0 : fd, 0);
|
||||
snprintf(test, sizeof(test), "close_%s_mode%u", name, mode);
|
||||
record(test, round, fd >= 0 ? IOS_Close(fd) : SKIPPED, 0);
|
||||
}
|
||||
|
||||
static void probe_sd(unsigned int round)
|
||||
{
|
||||
static u8 expected[4096] __attribute__((aligned(32)));
|
||||
static u8 actual[4096] __attribute__((aligned(32)));
|
||||
const size_t length = 1 + ((round * 67) % sizeof(expected));
|
||||
for (size_t i = 0; i < length; ++i)
|
||||
expected[i] = (u8)(round * 131 + i * 17);
|
||||
|
||||
FILE* file = fopen(SD_TEST_FILE, "wb");
|
||||
record("sd_open_write", round, file ? 0 : -1, (int)length);
|
||||
record("sd_write_bytes", round, file ? (int)fwrite(expected, 1, length, file) : SKIPPED,
|
||||
(int)length);
|
||||
record("sd_close_write", round, file ? fclose(file) : SKIPPED, 0);
|
||||
|
||||
file = fopen(SD_TEST_FILE, "rb");
|
||||
record("sd_open_read", round, file ? 0 : -1, 0);
|
||||
const size_t read_count = file ? fread(actual, 1, length, file) : 0;
|
||||
record("sd_read_bytes", round, file ? (int)read_count : SKIPPED, (int)length);
|
||||
u32 hash = 2166136261u;
|
||||
for (size_t i = 0; i < read_count; ++i)
|
||||
hash = (hash ^ actual[i]) * 16777619u;
|
||||
record("sd_read_hash", round, file ? (int)hash : SKIPPED, 0);
|
||||
record("sd_data_equal", round,
|
||||
file ? (read_count == length && memcmp(expected, actual, length) == 0) : SKIPPED, 0);
|
||||
record("sd_close_read", round, file ? fclose(file) : SKIPPED, 0);
|
||||
record("sd_remove", round, remove(SD_TEST_FILE), 0);
|
||||
}
|
||||
|
||||
static void init_display(void)
|
||||
{
|
||||
VIDEO_Init();
|
||||
GXRModeObj* mode = VIDEO_GetPreferredMode(NULL);
|
||||
void* framebuffer = MEM_K0_TO_K1(SYS_AllocateFramebuffer(mode));
|
||||
console_init(framebuffer, 20, 20, mode->fbWidth - 40, mode->xfbHeight - 40,
|
||||
mode->fbWidth * VI_DISPLAY_PIX_SZ);
|
||||
VIDEO_Configure(mode);
|
||||
VIDEO_SetNextFramebuffer(framebuffer);
|
||||
VIDEO_SetBlack(FALSE);
|
||||
VIDEO_Flush();
|
||||
VIDEO_WaitVSync();
|
||||
if (mode->viTVMode & VI_NON_INTERLACE)
|
||||
VIDEO_WaitVSync();
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
init_display();
|
||||
printf("Wii IOS conformance probe v%d\n", SCHEMA_VERSION);
|
||||
|
||||
if (fatInitDefault())
|
||||
output = fopen("sd:/wii-conformance.csv", "wb");
|
||||
if (!output)
|
||||
{
|
||||
printf("Cannot write sd:/wii-conformance.csv\n");
|
||||
printf("Insert a writable SD card and restart.\n");
|
||||
for (int i = 0; i < 600; ++i)
|
||||
VIDEO_WaitVSync();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Metadata is checked before comparing individual observations.
|
||||
fprintf(output, "schema,%d\n", SCHEMA_VERSION);
|
||||
fprintf(output, "rounds,%d\n", ROUNDS);
|
||||
fprintf(output, "observations,%d\n", 4 + ROUNDS * RECORDS_PER_ROUND);
|
||||
fprintf(output, "ios_version,%d\n", IOS_GetVersion());
|
||||
fprintf(output, "ios_revision,%d\n", IOS_GetRevision());
|
||||
fprintf(output, "seq,test,round,value,extra\n");
|
||||
|
||||
const s32 fs_init = ISFS_Initialize();
|
||||
record("isfs_init", 0, fs_init, 0);
|
||||
const s32 es_init = __ES_Init();
|
||||
record("es_init", 0, es_init, 0);
|
||||
|
||||
for (unsigned int round = 0; round < ROUNDS; ++round)
|
||||
{
|
||||
// A missing resource is a useful stable IPC error-path test.
|
||||
probe_open("missing", "/dev/wii_conformance_probe_absent", round, IPC_OPEN_NONE);
|
||||
|
||||
for (unsigned int mode = IPC_OPEN_NONE; mode <= IPC_OPEN_RW; ++mode)
|
||||
{
|
||||
probe_open("fs", "/dev/fs", round, mode);
|
||||
probe_open("es", "/dev/es", round, mode);
|
||||
}
|
||||
|
||||
if (fs_init >= 0)
|
||||
{
|
||||
u32 count __attribute__((aligned(32))) = 0;
|
||||
const s32 result = ISFS_ReadDir("/", NULL, &count);
|
||||
record("isfs_root_count", round, result, result >= 0 ? (int)count : 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
record("isfs_root_count", round, SKIPPED, 0);
|
||||
}
|
||||
|
||||
if (es_init >= 0)
|
||||
{
|
||||
u32 title_count __attribute__((aligned(32))) = 0;
|
||||
const s32 title_result = ES_GetNumTitles(&title_count);
|
||||
record("es_title_count", round, title_result,
|
||||
title_result >= 0 ? (int)title_count : 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
record("es_title_count", round, SKIPPED, 0);
|
||||
}
|
||||
|
||||
probe_sd(round);
|
||||
}
|
||||
|
||||
if (fs_init >= 0)
|
||||
record("isfs_deinit", 0, ISFS_Deinitialize(), 0);
|
||||
else
|
||||
record("isfs_deinit", 0, SKIPPED, 0);
|
||||
if (es_init >= 0)
|
||||
record("es_deinit", 0, __ES_Close(), 0);
|
||||
else
|
||||
record("es_deinit", 0, SKIPPED, 0);
|
||||
|
||||
const int write_error = ferror(output);
|
||||
const int close_error = fclose(output);
|
||||
printf("%u observations. CSV: sd:/wii-conformance.csv\n", sequence);
|
||||
printf("Write status: %d, close status: %d\n", write_error, close_error);
|
||||
printf("Returning to loader in about 10 seconds.\n");
|
||||
for (int i = 0; i < 600; ++i)
|
||||
VIDEO_WaitVSync();
|
||||
return write_error || close_error ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import contextlib
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from compare import compare, read_capture
|
||||
|
||||
|
||||
def capture(version=58, value=0):
|
||||
return ("schema,1\nrounds,64\nobservations,1\nios_version,%d\nios_revision,6176\n"
|
||||
"seq,test,round,value,extra\n0,open_fs_mode0,0,%d,0\n" % (version, value))
|
||||
|
||||
|
||||
class CompareTest(unittest.TestCase):
|
||||
def test_identical_and_first_difference(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
wii = Path(directory) / "wii.csv"
|
||||
dolphin = Path(directory) / "dolphin.csv"
|
||||
wii.write_text(capture(), encoding="utf-8")
|
||||
dolphin.write_text(capture(), encoding="utf-8")
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(compare(read_capture(wii), read_capture(dolphin)), 0)
|
||||
dolphin.write_text(capture(value=-6), encoding="utf-8")
|
||||
output = io.StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
self.assertEqual(compare(read_capture(wii), read_capture(dolphin)), 1)
|
||||
self.assertIn("#0", output.getvalue())
|
||||
|
||||
def test_wrong_ios_version_is_incomparable(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
wii = Path(directory) / "wii.csv"
|
||||
dolphin = Path(directory) / "dolphin.csv"
|
||||
wii.write_text(capture(), encoding="utf-8")
|
||||
dolphin.write_text(capture(version=80), encoding="utf-8")
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(compare(read_capture(wii), read_capture(dolphin)), 2)
|
||||
|
||||
def test_truncated_capture_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "short.csv"
|
||||
path.write_text(capture().replace("observations,1", "observations,2"), encoding="utf-8")
|
||||
with self.assertRaises(ValueError):
|
||||
read_capture(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -30,6 +30,15 @@ CPUThread = True
|
||||
WiiIOSLLEDumpPath = C:/absolute/path/to/dumps
|
||||
```
|
||||
|
||||
Detailed JIT memory profiling is optional and disabled by default. To collect slow-access
|
||||
regions, SRAM page counts and sampled hot addresses, set `WiiStarletJITMemoryProfiling = True`
|
||||
under `[Core]` before starting emulation. The launch script exposes the same choice through
|
||||
`-ProfileJitMemory` and explicitly disables it when that switch is absent. Aggregate slow-read,
|
||||
slow-write and instruction counters remain active in both modes. A log field of
|
||||
`memory-profile=false` means the detailed region counts are unavailable, not that there were no
|
||||
accesses. The setting is fixed for the core's lifetime and does not alter guest memory, timing,
|
||||
interrupts, or NAND persistence. It is host instrumentation and is not serialized in savestates.
|
||||
|
||||
The selected directory must contain the three matching files from one console:
|
||||
|
||||
| File | Exact size | Purpose |
|
||||
@@ -105,6 +114,29 @@ X2 and immediately submit the next request with X1 before Starlet is scheduled a
|
||||
|
||||
## Implemented hardware
|
||||
|
||||
### Freshly formatted NAND and controller pairing
|
||||
|
||||
Automatic Wii Remote pairing is optional at startup when `/shared2/sys/SYSCONF` or
|
||||
its `BT.DINF` entry is absent. This permits boot0/BootMii to run after a formatter
|
||||
without the host creating configuration files or restoring data from an older
|
||||
SFFS superblock. Native Wii software remains responsible for initializing its NAND;
|
||||
the emulated remotes use their default identities until pairing is established.
|
||||
Existing pairing records are still reused or populated as before. Unreadable NAND,
|
||||
malformed directories/configuration and failed journal writes remain errors.
|
||||
This removes a host-side startup blocker; it does not guarantee that the formatter
|
||||
installed a complete, bootable Wii system.
|
||||
|
||||
When native first-boot setup creates a new SYSCONF after formatting, a full Dolphin
|
||||
restart lets the existing startup pairing routine populate its empty BT.DINF.
|
||||
The write must use the native SFFS authentication layout: the complete 20-byte HMAC
|
||||
starts at spare byte 1 of cluster page 6, with a second copy split between page 6
|
||||
bytes 21-32 and page 7 bytes 1-8. Bad-block markers and unrelated spare bytes are
|
||||
preserved, and ECC is regenerated for the ciphertext. The pairing regression test
|
||||
checks both HMAC copies and spare/ECC preservation; valid decryption alone does not
|
||||
prove IOS will accept the modified file.
|
||||
|
||||
### Devices and execution
|
||||
|
||||
- ARM and Thumb instruction interpretation needed by the observed boot, including banked CPU
|
||||
modes, user-bank block transfers, SPSRs, high Starlet exception vectors, IRQ/FIQ entry,
|
||||
ARM/Thumb interworking, ARMv5TE branches/multiplies, and CP15 registers.
|
||||
@@ -506,6 +538,19 @@ frequency, guest scheduling/idle detection and device-access cost.
|
||||
| Configuration | `Core/Config/MainSettings.{h,cpp}` |
|
||||
| Starlet CPU/device unit tests | `UnitTests/Core/IOS/Starlet/ARMCoreTest.cpp` |
|
||||
|
||||
## Conformance work
|
||||
|
||||
The current priority is behavioral agreement with a real Wii, not a fixed FPS
|
||||
target. [`Tools/WiiConformanceProbe`](../Tools/WiiConformanceProbe/README.md)
|
||||
builds one PPC homebrew binary to run against native IOS on both systems and
|
||||
compares numbered return codes and outputs. Keep the probe binary identical,
|
||||
record the IOS revision and starting NAND state, and label physical-Wii versus
|
||||
emulator captures explicitly. Local captures are not automatically treated as
|
||||
hardware ground truth. When a result differs, add a minimal deterministic unit
|
||||
test for the relevant IPC, memory, timing, or device behavior before changing
|
||||
the emulator. Performance optimizations must preserve those observations and
|
||||
the BootMii → Wii boot path.
|
||||
|
||||
## Safe development rules
|
||||
|
||||
1. Treat all dumps as private console material and never print keys or ROM/NAND bytes in logs.
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
# Protocole P1 — attribuer les ralentissements Wii LLE
|
||||
|
||||
Date : 19 septembre 2026. Statut au 22 septembre : **capture noyau locale validée ;
|
||||
coût du collecteur, cas PPC et chemin critique Wii non qualifiés**.
|
||||
Aucune nouvelle mise à jour n'est lancée par la rédaction de ce document.
|
||||
Il remplace la stratégie « modifier, refaire une mise à jour, comparer les FPS ».
|
||||
|
||||
## 1. Question et règle de décision
|
||||
|
||||
Sur les passages réellement lents, déterminer **quel travail ou quelle attente retarde
|
||||
la progression de l'émulation**, dans quelles fonctions, et pour combien de temps.
|
||||
Le résultat attendu n'est pas « un cœur à 100 % », mais une attribution vérifiable.
|
||||
|
||||
Une seule exécution de diagnostic est prévue après qualification du dispositif.
|
||||
Aucun nouveau correctif de performance avant un rapport d'attribution exploitable.
|
||||
Une capture incomplète ne justifie pas un correctif au hasard : on répare le dispositif,
|
||||
et on précise quelle information manque avant d'envisager une capture complémentaire.
|
||||
Une seule cause est ensuite traitée ; la validation porte d'abord sur sa charge locale.
|
||||
|
||||
## 2. Pourquoi A–F ne suffisent pas
|
||||
|
||||
- Les instantanés espacés d'environ 10 secondes ne décrivent ni les pics ni leurs piles.
|
||||
- Les FPS échantillonnés ne sont pas une distribution des temps de frame.
|
||||
- Un nombre d'accès mémoire ou d'instructions JIT ne mesure pas leur coût CPU.
|
||||
- `ARMCore::RunCycles` contient également `m_bus.AdvanceCycles` : son temps inclusif
|
||||
ne doit pas être présenté intégralement comme du calcul ARM.
|
||||
- `AdvanceCycles` appelle les périphériques, dont OHCI et le réseau : additionner
|
||||
ces temps imbriqués compterait plusieurs fois le même travail.
|
||||
- `Sampler.cpp` utilise `SuspendThread`/`ResumeThread` ; ses observations historiques
|
||||
ne deviennent pas une référence de temps sans perturbation.
|
||||
- Le lecteur F ne résout pas certaines adresses JIT. Une annotation absente n'est
|
||||
ni zéro travail JIT ni une preuve que tous les autres offsets sont incorrects.
|
||||
- Les 600–2 700 Mo de journal sont des repères approchés, pas une charge identique.
|
||||
- La durée entre messages utilisateur n'est pas la durée exacte de la mise à jour.
|
||||
- Les conditions hôte/serveur diffèrent. F est observé plus lent que E, mais la cause
|
||||
n'est pas isolée. Répéter simplement F avec un autre réglage ne résout pas cela.
|
||||
|
||||
## 3. Geler le scénario, sans toucher aux NAND existantes
|
||||
|
||||
Le diagnostic concerne le ralentissement actuel : source actuelle figée correspondant
|
||||
au comportement F, profilage détaillé des accès désactivé, **sans nouvelle optimisation**.
|
||||
Créer une build de diagnostic propre, non incrémentale, et ne pas remplacer les builds
|
||||
figées D/E/F. Conserver le patch et les empreintes des sources, y compris les fichiers
|
||||
non suivis utilisés, options de compilation, compilateur, EXE, PDB et paramètres.
|
||||
Vérifier l'identité EXE/PDB ; cela ne suffit pas, à elle seule, à valider un lecteur de types.
|
||||
|
||||
Utiliser un nouveau profil depuis la graine pré-mise à jour déjà vérifiée :
|
||||
SHA256 `07b2f4bc543cfd4203efc448538505e9d503b2727c16aaff8d8c0b286d8b4f0c`.
|
||||
Conserver les mêmes réglages Wii, réseau NAT, rendu, vitesse 100 %, horloges invitées,
|
||||
SD et persistance. Noter les différences de chemins et rien d'autre.
|
||||
Un démarrage du processus n'implique pas un cache disque Windows froid ; ne pas vider
|
||||
les caches système, changer l'affinité ou désactiver l'antivirus pour fabriquer un résultat.
|
||||
|
||||
Relever au départ et pendant la capture : charge des autres processus, CPU logique utilisé,
|
||||
état d'alimentation, fréquence disponible, mémoire/pression de pagination, disque et GPU.
|
||||
Ne pas lancer de compilation, benchmark, hachage massif, nettoyage ou autre test en parallèle.
|
||||
Ne pas fermer des applications utilisateur sans accord. Si un concurrent perturbe une fenêtre,
|
||||
la signaler et mesurer sa contribution plutôt que l'effacer du rapport.
|
||||
|
||||
## 4. Deux couches de mesure sur la même horloge
|
||||
|
||||
### A. Profil système Windows, indépendant des layouts C++
|
||||
|
||||
WPR, Xperf et WPA sont présents sur cette machine ; `wpr -profiles` a été vérifié.
|
||||
Employer un profil ETW ciblé, validé avant le test :
|
||||
|
||||
- CPU échantillonné avec piles, cible initiale 1 ms, intervalle réellement utilisé consigné ;
|
||||
- changements de contexte et réveils, pour séparer **Running / Ready / Waiting** par thread ;
|
||||
- chargement des modules, événements disque/fichiers, fautes matérielles de pagination ;
|
||||
- métadonnées d'activité réseau, sans contenu des paquets ;
|
||||
- fréquence/puissance si disponibles. Une donnée absente reste « indisponible ».
|
||||
|
||||
Les vues CPU échantillonné et CPU précis répondent à des questions différentes : localisation
|
||||
du calcul d'une part, ordonnancement/attentes d'autre part. Les combiner, sans confondre une
|
||||
pile d'attente avec une preuve d'attente disque. [Documentation Microsoft](https://learn.microsoft.com/en-us/troubleshoot/windows-server/support-tools/support-tools-xperf-wpa-wpr)
|
||||
|
||||
Ne plus utiliser de suspension de thread ou d'attachement au débogueur pour chronométrer.
|
||||
Pas de profilage heap complet, trace de chaque accès mémoire, ni capture de paquets par défaut.
|
||||
Filtrer l'analyse sur le PID/TID exact et conserver les processus concurrents pour expliquer
|
||||
le temps Ready. Ne pas supposer que le PID suffit après un redémarrage : enregistrer sa date
|
||||
de création. Aucun arrêt/annulation d'une session ETW tierce ; vérifier les sessions avant démarrage.
|
||||
|
||||
### B. Télémétrie native légère, versionnée
|
||||
|
||||
Publier les compteurs depuis le code qui les possède ; ne plus les déduire d'offsets PDB
|
||||
via `ReadProcessMemory`. Un schéma versionné comporte `run_id`, version, QPC/fréquence,
|
||||
PID/TID, génération du cœur/rechargement IOS et des compteurs monotones.
|
||||
L'export asynchrone est borné ; les débordements sont comptés. Aucune écriture de log
|
||||
synchrone à chaque instruction ou accès. Ne pas ajouter de verrou partagé au chemin mémoire.
|
||||
Récupérer les données sur leur thread propriétaire, sans lecture C++ concurrente non synchronisée.
|
||||
|
||||
- Temps invité écoulé et cycles ARM/PPC, séparés du temps hôte QPC.
|
||||
- Temps de frames réellement produites : histogramme p50/p95/p99 et longues frames,
|
||||
rapporté au mode vidéo du profil (50 ou 60 Hz). Une médiane de FPS n'est pas ce calcul.
|
||||
- Agrégats toutes les secondes ; chronologie d'analyse en fenêtres de 250 ms quand
|
||||
les événements le permettent. Marquer les fenêtres qui n'ont pas assez de données.
|
||||
- JIT : compilations, invalidations, instructions natives/fallback, accès lents existants,
|
||||
entrées en attente et sorties de slice, avec identifiant de génération après reset.
|
||||
- NAND : lectures/programmes/effacements, transactions, octets et erreurs ; séparer
|
||||
sérialisation/hash, appels d'écriture, `IOFile::Flush` et attente hôte observée.
|
||||
- Réseau : octets/trames hôte reçus, mise en file NAT, remise USB à IOS, occupation maximale
|
||||
et âge des files. Le débit reçu ne prouve pas le débit consommé par IOS.
|
||||
|
||||
Les chronomètres de régions fréquentes sont échantillonnés et calibrés, jamais posés sur
|
||||
chaque accès ARM. Les frontières plus rares (rechargement, compilations, gros transferts)
|
||||
peuvent produire des événements. Éviter un échantillonnage périodique aligné sur la boucle
|
||||
invitée ; conserver le taux de sélection et distinguer estimation et total exact.
|
||||
|
||||
### C. Rendre les échantillons JIT attribuables
|
||||
|
||||
Un PDB décrit le C++ compilé, pas automatiquement le code généré à l'exécution.
|
||||
Exporter, à compilation/invalidation, les intervalles d'adresses **hôte** du JIT ARM et
|
||||
du JIT PPC, dispatcher/trampolines compris, avec QPC et génération ; pour les blocs,
|
||||
indiquer PC invité, ARM/Thumb et identité du mapping. Ne pas extraire ni publier le firmware.
|
||||
Une adresse réutilisée après vidage du cache doit se résoudre selon la date de l'échantillon.
|
||||
|
||||
Classer l'IP feuille même si la pile ne traverse pas correctement une transition JIT/C++.
|
||||
Ne pas inventer de parent de pile manquant. Garder « pile tronquée » et « code non attribué »
|
||||
visibles ; ajouter une instrumentation ciblée si ces trous empêchent la décision.
|
||||
Le support `Common::JitRegister` existant côté PPC est un point d'intégration à examiner,
|
||||
pas une garantie que le JIT ARM ou l'unwinding Windows sont déjà couverts.
|
||||
|
||||
## 5. Points d'attribution dans ce dépôt
|
||||
|
||||
| Domaine | Points à observer | Preuve recherchée |
|
||||
|---|---|---|
|
||||
| Ordonnancement ARM | `Starlet::RunSlice`, `ARMCore::RunCycles` | Coût de dispatch, quanta/s, transitions, temps invité/hôte |
|
||||
| JIT ARM | `ARMJitX64::Run`, compilation, code généré, helpers | Part CPU feuille, blocs invités chauds, recompilations |
|
||||
| Mémoire ARM | `ReadMemorySlow`, `WriteMemorySlow`, traduction, bus | CPU exclusif des helpers et classes d'accès, pas leur seul nombre |
|
||||
| Périphériques | `StarletMemory::AdvanceCycles`, DMA, AES/SHA, OHCI/EHCI | CPU exclusif par fonction ; attente simulée séparée du travail hôte |
|
||||
| NAND | `CommitNANDPages`, `NANDJournal::Append`, `IOFile::Flush` | CPU de préparation vs appels fichiers vs attente disque réelle |
|
||||
| Réseau | `SlirpNetwork::Poll`, AX88772, remise OHCI | CPU NAT/USB, files, progression hôte versus progression invitée |
|
||||
| PPC / rendu | JIT PPC, threads vidéo, présentation | Concurrence réelle et dépendances de l'émulation, pas seulement FPS |
|
||||
| Hors émulation | Limiteur, audio, logs, collecteur, Windows | Attente voulue, contention, CPU de diagnostic, temps Ready |
|
||||
|
||||
Sur un même thread, présenter les **temps exclusifs** des régions imbriquées.
|
||||
Présenter aussi leurs temps inclusifs, mais dans une autre colonne, non additionnable.
|
||||
Ne pas additionner le CPU de plusieurs threads comme une durée murale de mise à jour.
|
||||
Pour les attentes, suivre la dépendance qui retarde le thread utile ; un thread GPU qui
|
||||
attend l'émulation n'est pas une cause de ralentissement GPU.
|
||||
`fflush` n'est pas une preuve de vidage physique sur disque : le niveau de persistance
|
||||
actuel reste inchangé et les attentes sont établies par les événements hôte.
|
||||
|
||||
## 6. Qualification obligatoire, sans demander de mise à jour à l'utilisateur
|
||||
|
||||
Avant de dire « lance la mise à jour », un contrôle local court doit démontrer :
|
||||
|
||||
1. EXE/PDB identifiés ; compteur versionné lu correctement ; horloges alignées ; un reset
|
||||
ne produit pas de deltas négatifs ; pas de perte silencieuse d'événements.
|
||||
2. Une boucle JIT connue est correctement attribuée ARM/PPC ; son coût CPU augmente
|
||||
comme prévu. Un cas d'attente connu est classé Waiting, pas « calcul JIT ».
|
||||
3. Les scopes imbriqués d'un test ont des temps exclusifs cohérents, sans double comptage.
|
||||
4. Un cas de journal synthétique et de réseau local, sans service Nintendo, est visible.
|
||||
5. Mesurer le surcoût ON/OFF du dispositif complet sur la même charge locale, dans le même
|
||||
binaire et en ordre alterné. Budget initial : **2 %** de temps mural/CPU, bruit affiché.
|
||||
Ce seuil est une règle de projet, pas une garantie d'ETW. S'il est dépassé ou non résolu
|
||||
par le bruit, alléger les fournisseurs/samples avant le test. Le contrôle ne garantit
|
||||
pas le même surcoût sur toutes les phases natives : relever aussi la charge du collecteur.
|
||||
|
||||
Pas de nouveau comportement invité, pas de changement de NAT, timers, JIT, NAND ou SVC.
|
||||
Les boucles SVC/polling éventuelles sont d'abord attribuées à du CPU/PC invité ; aucune
|
||||
émulation accélérée d'appel IOS ne découle d'un simple comptage d'instructions.
|
||||
|
||||
## 7. Une capture native, puis analyse hors ligne
|
||||
|
||||
1. Vérifier réserve disque et absence de test/build actif. Enregistrer le manifeste.
|
||||
2. Armer ETW et la télémétrie **avant** de lancer le profil isolé ; confirmer une fenêtre
|
||||
visible et des mesures valides. Ne pas se baser sur les messages utilisateur pour armer.
|
||||
3. L'utilisateur lance une seule mise à jour. Garder 10 s de référence avant l'activité.
|
||||
Tout marquage manuel reste approximatif ; les phases sont reconstruites à partir des
|
||||
événements synchronisés, files réseau, travail invité et commits NAND.
|
||||
4. Téléchargement, calcul et installation peuvent se chevaucher : ne pas leur imposer
|
||||
trois phases exclusives artificielles. Une file vide ne prouve pas une attente serveur.
|
||||
5. Garder environ 10 s après la réussite signalée, puis arrêter uniquement la capture.
|
||||
Ne pas arrêter Dolphin, modifier la NAND ou cliquer à la place de l'utilisateur.
|
||||
6. Garde-fous initiaux : 15 min de capture, budget disque 4 Gio et réserve 5 Gio ; le pilote
|
||||
doit arrêter proprement **sa** capture si une limite approche, conserver le préfixe et
|
||||
déclarer les phases manquantes. Une trace incomplète n'est pas un échec de la mise à jour.
|
||||
7. Vérifier les événements perdus et le coût du profilage. Ne pas charger WPA ni effectuer
|
||||
une grosse analyse/hachage pendant la mesure. Fermer ensuite Dolphin avec l'utilisateur
|
||||
pour les vérifications NAND, comme pour F.
|
||||
|
||||
Les traces en mode fichier sont adaptées à ce scénario contrôlé, mais ajoutent des E/S :
|
||||
les distinguer du journal NAND, relever le volume et ne pas supposer leur coût nul.
|
||||
Les pertes d'événements rendent certaines conclusions impossibles ; les vérifier explicitement.
|
||||
[Sessions ETW/WPR — Microsoft](https://learn.microsoft.com/en-us/windows-hardware/test/wpt/sessions)
|
||||
|
||||
## 8. Rapport exigé avant tout correctif
|
||||
|
||||
Analyser la capture entière et, au minimum, trois fenêtres lentes non chevauchantes de
|
||||
10 s si elles existent, ainsi qu'une fenêtre rapide de la même exécution. Une fenêtre lente
|
||||
est définie avant analyse : vitesse invitée moyenne < 90 % pendant au moins 2 s ; analyser
|
||||
aussi les longues frames indépendamment. Rapporter l'absence de fenêtres admissibles.
|
||||
Comparer des types d'activité similaires ; ne pas comparer une pause réseau à une installation.
|
||||
|
||||
Livrer :
|
||||
|
||||
- Chronologie superposant vitesse/frames, CPU Running/Ready/Waiting, NAND et files réseau.
|
||||
- Top fonctions/piles, part CPU exclusive/inclusive, nombre d'échantillons, contexte invité.
|
||||
- Temps critique perdu en calcul, Ready et attentes expliquées ; catégories inconnues visibles.
|
||||
- Coût par unité de travail quand disponible : CPU/transaction, CPU/Mio traité, compilations
|
||||
par instruction, etc. Un compteur n'a de sens que pour la même opération/sémantique.
|
||||
- Liste des observations qui réfutent les autres pistes ; distinguer preuve et hypothèse.
|
||||
|
||||
Gates de qualité initiaux : zéro perte sur les flux indispensables à l'intervalle analysé,
|
||||
au moins 95 % des échantillons CPU Dolphin attribués à un domaine, compteurs intègres et
|
||||
coût de collecte qualifié. Ce sont des critères d'analyse, non des promesses de précision.
|
||||
Une pile incomplète peut permettre de classer un domaine sans identifier sa cause interne :
|
||||
on ne valide alors pas un correctif visant une fonction non résolue. Pas de fausse certitude
|
||||
statistique obtenue en traitant des échantillons temporels corrélés comme des essais indépendants.
|
||||
|
||||
Un correctif est admissible seulement si :
|
||||
|
||||
1. La cause apparaît dans plusieurs fenêtres lentes avec une signature explicable.
|
||||
2. Sa part sur le chemin qui retarde l'émulation est chiffrée, avec son incertitude.
|
||||
3. Un cas local représentatif reproduit cette charge ou cette attente.
|
||||
4. Le gain global plausible est estimé avant modification. Par exemple, supprimer tout un
|
||||
poste responsable de 5 % du temps critique ne retire au mieux que 5 % de cette durée.
|
||||
5. Le correctif garde le comportement matériel et passe les tests ciblés/interpréteur.
|
||||
|
||||
Décision pratique : si le CPU est dans les helpers, travailler ces helpers ; s'il est
|
||||
dans le code invité, examiner les blocs et le travail IOS ; si Ready domine, étudier
|
||||
la contention hôte ; si les attentes d'E/S dominent, traiter leur dépendance réelle.
|
||||
Si les files NAT sont pleines mais peu consommées, ne pas accuser le serveur ; si elles
|
||||
sont vides, confirmer l'attente avec les échanges/états disponibles ou laisser la cause ouverte.
|
||||
Si aucun poste ne domine, le rapport le dit : on ne fabrique pas un unique responsable.
|
||||
|
||||
## 9. Validation minimale après diagnostic
|
||||
|
||||
D'abord une validation locale répétable du poste identifié, résultat invité exact et même
|
||||
charge. Puis une validation native ciblée du scénario corrigé, pas une série de réglages.
|
||||
La trace de diagnostic peut localiser une cause, mais son temps n'est pas automatiquement
|
||||
une référence équitable face à une build sans instrumentation. Pour revendiquer un gain
|
||||
global, conserver un dispositif équivalent et répéter seulement si la variabilité observée
|
||||
empêche la conclusion. Annoncer cette nécessité et sa raison, pas un nouveau test à l'aveugle.
|
||||
L'objectif est de réduire les essais utilisateur ; garantir à l'avance qu'un seul essai
|
||||
prouvera toute causalité serait trompeur.
|
||||
|
||||
## 10. Artefacts et limites
|
||||
|
||||
Conserver dans un nouveau répertoire local ignoré : manifeste, capture ETL, télémétrie
|
||||
versionnée, mapping JIT daté, coût de collecte, tableaux de fenêtres, piles chaudes,
|
||||
rapport des attentes, événements perdus et décision. Le rapport lisible va dans `docs`.
|
||||
Pas de clés NAND, mémoire brute invitée, mots de passe ou contenu réseau dans les exports.
|
||||
Les ETL peuvent contenir des chemins et noms de processus : conservation locale, pas de push.
|
||||
|
||||
État au 22 septembre 2026 : instrumentation opt-in `DOLPHIN_PERF_TRACE=1`, mapping
|
||||
temporel JIT ARM/PPC, scopes/counters versionnés, profil WPR ciblé, décodeur ETL et
|
||||
sonde locale ont été ajoutés. La build isolée passe et la sonde privée a validé
|
||||
13 contrôles locaux sur 13 sur deux captures, avec zéro perte d'événement.
|
||||
Sur deux paires alternées, la phase ARM avec trace privée dépasse la phase sans
|
||||
trace d'environ 1,7 % en moyenne, mais la dispersion est supérieure à cet écart.
|
||||
Le budget de 2 % n'est donc pas qualifié. Le démarrage/arrêt du logger privé
|
||||
ajoute environ 0,34 s à ces exécutions courtes ; ce coût ne mesure pas le
|
||||
surcoût du collecteur WPR pendant une mise à jour Wii.
|
||||
La session WPR ne démarrait pas dans le jeton Windows de cette tâche
|
||||
(`0xc5585011`, droit de profilage système absent). Une capture synthétique
|
||||
administrateur a depuis produit l'ETL `P1-local-004`, sans perte. L'analyse
|
||||
hors ligne, reproductible sous PowerShell 5.1, valide 9 427 échantillons CPU,
|
||||
694 context-switches, dix ancres QPC alignées à cinq ticks près, et le sommeil
|
||||
connu effectivement classé `Waiting`. Le PDB local résout les fonctions C++;
|
||||
le mapping temporel attribue les IP du JIT ARM. Les 112 événements Xperf
|
||||
invalides sont des métadonnées `ImageID` hors du PID cible. Voir
|
||||
[`Tools/PerformanceTrace/README.md`](../Tools/PerformanceTrace/README.md).
|
||||
Le lot WPR `P1-overhead-001` a terminé ses huit charges synthétiques sans perte
|
||||
ETW ; seul son calcul final a échoué faute de Python dans le PowerShell
|
||||
administrateur. Le calcul récupéré hors ligne donne quatre écarts ARM ON/OFF de
|
||||
+9,7 %, +25,0 %, -0,5 % et +1,0 %, tandis que les quatre bases OFF ont une
|
||||
étendue de 34,5 %. Cette variabilité interdit d'annoncer un coût fiable du
|
||||
collecteur ou de qualifier le seuil de 2 %. Le script vérifie maintenant Python
|
||||
avant toute capture et peut analyser un lot déjà enregistré avec `-AnalyzeOnly`.
|
||||
Les traces ON montrent en outre un indexeur C++ hôte (`cpptools.exe`) actif
|
||||
pendant chaque essai et des services Dell intermittents. Le profil a été allégé
|
||||
des piles `ReadyThread`, tout en conservant leurs événements ; sa nouvelle
|
||||
version n'est pas encore qualifiée par un lot ON/OFF stable.
|
||||
Le second lot avec ce profil (`P1-overhead-002`) a conservé zéro perte ETW mais
|
||||
36,5 % d'étendue entre bases OFF. L'analyse par processeur montre que la sonde
|
||||
rapide passe 78,7 % de ses échantillons sur les logiques 0–3 contre 28,9 % pour
|
||||
la sonde la plus lente ; les temps ARM correspondants sont 7,59 et 10,33 s.
|
||||
L'ETL identifie un i7-1255U hybride avec SMT sur 0–3. Le lanceur Wii fixait
|
||||
déjà Dolphin à ces processeurs, mais les sondes OFF/ON ne l'étaient pas.
|
||||
L'affinité a été ajoutée aux sondes et vérifiée localement (masque 15 observé).
|
||||
Quatre bases fixées gardent toutefois 8,3 % de dispersion ARM ; elles ne
|
||||
valident pas encore le seuil initial de 2 %. Il serait trompeur de demander un
|
||||
troisième lot administrateur sans résoudre cette variabilité résiduelle.
|
||||
Les événements natifs ne sont pas activés dans les profils utilisateur par défaut.
|
||||
Une build et une sonde locale réussies ne valident pas le budget de 2 % ni le
|
||||
comportement d'une mise à jour native. Le vérificateur local conserve
|
||||
`native_update_ready=false` tant que les gates PPC, chemin critique natif et
|
||||
coût du collecteur n'ont pas été validées.
|
||||
**Ne pas demander de nouvel essai utilisateur tant que ces gates ne passent pas.**
|
||||
@@ -0,0 +1,182 @@
|
||||
# Wii IOS LLE : première trace CPU native de mise à jour
|
||||
|
||||
Date : 2026-09-23 (Europe/Paris). Cible : atteindre à terme environ 60 FPS
|
||||
pendant une mise à jour système, sans raccourci HLE ni régression BootMii/HBC.
|
||||
Ce document rapporte une **mesure exploratoire**, pas un gain de performance.
|
||||
|
||||
## Périmètre et qualité
|
||||
|
||||
- NAND : copie isolée `.starlet_update_ab_20260913/P1-trace-002`, créée depuis
|
||||
le journal pré-mise-à-jour vérifié (31 fichiers, SHA du seed
|
||||
`07B2F4BC543CFD4203EFC448538505E9D503B2727C16AAFF8D8C0B286D8B4F0C`).
|
||||
`.starlet_user3` et les anciens essais A–F n'ont pas été modifiés.
|
||||
- Build diagnostic figée : `.starlet_update_ab_20260913/build-P1-trace-002` ;
|
||||
exécutable SHA-256 `D809E04E2E8E900DB06D3813A67805E66EADB2BB79924CB8E805CB724360EAD4`.
|
||||
- ETL source : `.starlet-profile/P1-native-update-001/capture.etl` (3,88 Gio),
|
||||
session WPR de 600 s. Le champ `EndUTC` du manifest inclut environ 100 s de
|
||||
fusion de la trace : ce n'est **pas** la fin de la collecte.
|
||||
- Analyse : `.starlet-profile/P1-native-update-001-update-window/`, Xperf limité
|
||||
à 2026-09-22 22:57:00–23:00:33 UTC. Les échantillons cibles couvrent en fait
|
||||
**114,15 s** seulement, jusqu'à l'arrêt de la collecte vers 22:58:52 UTC.
|
||||
Les premières écritures NAND de mise à jour sont observées vers 22:57:23 UTC.
|
||||
La fenêtre CPU comprend donc aussi environ 23 s avant cette première écriture.
|
||||
La fin de la mise à jour, confirmée par l'utilisateur, n'est donc pas profilée.
|
||||
- TDH : 780 781 événements natifs décodés, zéro perte de buffer/événement et
|
||||
zéro erreur de décodage. Fusion Xperf : 25 419 ancres QPC, erreur maximale
|
||||
cinq ticks (QPC 10 MHz), 128 329 échantillons CPU sur 18 threads, 549 607
|
||||
changements de contexte retenus, zéro événement cible invalide. Les 57
|
||||
événements Xperf invalides sont hors PID cible.
|
||||
- Le surcoût du collecteur WPR en mise à jour native n'est pas qualifié par le
|
||||
lot synthétique précédent ; ces FPS ne constituent pas un A/B sans trace.
|
||||
|
||||
## Observations dans la fenêtre CPU retenue
|
||||
|
||||
| Mesure | Valeur |
|
||||
| --- | ---: |
|
||||
| Images « produced », intervalle médian | 21,16 ms (≈47,3 FPS) |
|
||||
| Images « produced », p95 | 35,33 ms (≈28,3 FPS) |
|
||||
| Images « produced », p99 | 68,58 ms (≈14,6 FPS) |
|
||||
| Thread principal TID 28212 | 95 154 / 128 329 échantillons (74,2 %) |
|
||||
| Thread GPU TID 13804 | 16 125 / 128 329 (12,6 %) |
|
||||
| Thread audio TID 22388 | 5 095 / 128 329 (4,0 %) |
|
||||
| JIT ARM / JIT PPC, échantillons de feuilles | 27 817 / 16 450 |
|
||||
| Instructions ARM, delta de snapshots | 4 566 790 374 |
|
||||
| Instructions ARM JIT natives, delta | 4 522 113 258 |
|
||||
| Accès lents JIT lecture / écriture, deltas | 59 486 963 / 218 257 365 |
|
||||
| Fallbacks ARM, delta | 42 433 964 |
|
||||
|
||||
Les symboles hôte les plus visibles sur le thread principal comprennent
|
||||
`StarletMemory::Read32` (3 246 échantillons),
|
||||
`ARMCore::TranslateVirtualAddress` (2 909), `StarletMemory::Write32` (2 613),
|
||||
`PowerPC::MMU::WriteToHardware` (2 420) et
|
||||
`StarletMemory::CalculateNANDECC` (1 918). Ce sont des **feuilles** : leur somme
|
||||
ne décrit pas une pile ni un chemin critique. Le thread GPU occupe aussi du
|
||||
CPU, mais il ne domine pas ce relevé. Les 218 millions d'écritures lentes
|
||||
JIT sont un compteur d'appels, pas 218 millions d'accès NAND.
|
||||
|
||||
Les compteurs de scopes échantillonnés distinguent NAND, journal, NAT, DMA,
|
||||
ARM/JIT et périphériques, avec zéro erreur de comptabilité. Ils mesurent du
|
||||
temps mur de tranches sélectionnées 1/1024, pas une durée CPU exhaustive ; ils
|
||||
ne justifient donc pas d'affirmer qu'un autre poste est nul. Les événements
|
||||
`Frame` et `Snapshot` natifs couvrent toute la session alors que les
|
||||
échantillons CPU Xperf sont filtrés ; les chiffres de FPS ci-dessus viennent
|
||||
de `cpu-threads-phase2.json`, qui borne **les deux** à la même fenêtre QPC.
|
||||
|
||||
## Décision
|
||||
|
||||
La première minute de mise à jour est surtout limitée par le thread principal
|
||||
d'émulation, mais aucun helper unique mesuré ne démontre à lui seul les ~21 %
|
||||
de réduction de temps par image nécessaires pour passer de la médiane observée
|
||||
à 60 FPS. Un simple gain de microbenchmark ECC ou TLB ne doit pas être présenté
|
||||
comme un correctif 60 FPS. Les écritures lentes sont le candidat à qualifier :
|
||||
le fast path direct SRAM écrit avait déjà régressé BootMii, donc élargir son
|
||||
masque sans savoir quelles adresses dominent serait risqué.
|
||||
|
||||
Prochain essai borné : la nouvelle copie initiale `P1-trace-003` est préparée
|
||||
et vérifiée. Activer `-ProfileJitMemory` dans cette copie seulement et relever
|
||||
dans `dolphin.log`
|
||||
les compteurs RAM/SRAM/MMIO et les adresses les plus fréquentes pendant le même
|
||||
début de mise à jour. Ce mode ajoute du coût et ne sert qu'à **classer les
|
||||
adresses**, pas à comparer ses FPS à la présente trace. Ensuite seulement,
|
||||
choisir une optimisation limitée à une famille d'adresses, ajouter des tests de
|
||||
fidélité BootMii/HBC/NAND et réaliser un A/B sans instrumentation sur deux NAND
|
||||
identiques. L'objectif de 60 FPS reste ouvert.
|
||||
|
||||
Le lancement du relevé ciblé est manuel depuis un PowerShell interactif, car
|
||||
une instance créée par l'outil de commande de l'agent s'est retrouvée invisible
|
||||
sur le bureau utilisateur :
|
||||
|
||||
```powershell
|
||||
cd C:\Users\tpetkovic\Desktop\dolphin
|
||||
.\Run-Wii-IOS-LLE.ps1 `
|
||||
-BuildDirectory .starlet_update_ab_20260913/build-P1-trace-003 `
|
||||
-UserDirectory .starlet_update_ab_20260913/P1-trace-003 `
|
||||
-Ethernet -ProfileJitMemory
|
||||
```
|
||||
|
||||
Fichiers de résultat locaux : `decoder-summary.json`, `xperf-summary.json`,
|
||||
`native-report.json`, `cpu-threads-phase2.json` dans le répertoire d'analyse
|
||||
indiqué ci-dessus. Un export Xperf complet, interrompu car disproportionné,
|
||||
a été supprimé ; l'ETL source et l'export ciblé sont conservés.
|
||||
|
||||
## Relevé ciblé `P1-trace-003` : écritures SRAM
|
||||
|
||||
La copie isolée `P1-trace-003` a été démarrée avec `-ProfileJitMemory`. La mise
|
||||
à jour est allée jusqu'au redémarrage/menu Wii (confirmé par l'utilisateur).
|
||||
Le journal a cessé de croître à 2 763 867 232 octets. L'analyse en lecture
|
||||
seule valide ses 651 690 transactions et 165 568 pages finales superposées.
|
||||
Le SYSCONF est valide ; les 9 contenus du menu v514 et les 15 d'IOS80 v6944
|
||||
correspondent à leurs empreintes TMD. SHA-256 du journal :
|
||||
`49b19327a9b0d7c3aa08c4d59e81365aa17b82ef7452939c07281cbd499b5b56`.
|
||||
Cela ne constitue pas une vérification exhaustive de tous les titres.
|
||||
|
||||
Entre les snapshots JIT de 11:57:06.905 (début des écritures de mise à jour)
|
||||
et 12:07:12.622 (fin des écritures), soit environ 606 s de temps mur :
|
||||
|
||||
| Compteur d'accès ARM lents | Delta |
|
||||
| --- | ---: |
|
||||
| SRAM | 1 024 033 027 (78,74 % des accès classés) |
|
||||
| MMIO | 255 829 918 |
|
||||
| Autres | 20 598 539 |
|
||||
| RAM | 0 |
|
||||
| Lectures, toutes régions | 292 651 794 |
|
||||
| Écritures, toutes régions | 1 007 809 690 |
|
||||
| Écritures SRAM | 971 531 207 (96,4 % des écritures lentes) |
|
||||
|
||||
Les pages d'aperture SRAM `0x00` et `0x19` portent respectivement
|
||||
556 332 042 et 348 178 857 écritures lentes, soit **93,1 % des écritures
|
||||
SRAM** de cette fenêtre. L'alias bas n'augmente pas : ce trafic passe par
|
||||
l'alias haut. Les adresses qui reviennent le plus dans l'échantillonnage
|
||||
1/256 sont notamment `0xffff97a0` et `0xfffe03c0` en écriture ; les lectures
|
||||
MMIO comprennent `0x0d800038` et `0x0d80003c`. Le classement par page est
|
||||
exact dans les compteurs, tandis que les adresses individuelles ne sont que
|
||||
des échantillons (huit adresses au maximum par snapshot).
|
||||
|
||||
**Conséquence :** les écritures SRAM sont un candidat ciblé, pas un gain de
|
||||
FPS déjà établi. Ce run est instrumenté et ne doit pas être comparé en vitesse
|
||||
au run natif `P1-trace-002`. Le JIT conserve volontairement les écritures
|
||||
SRAM sur le bus exact : une ancienne écriture directe, même limitée aux pages
|
||||
`0x00`/`0x19`, avait bloqué BootMii à son initialisation matérielle. Avant de
|
||||
réintroduire un fast path, il faut expliquer cette divergence et vérifier
|
||||
boot0/split/alias/endianness ainsi que le cycle BootMii → menu → HBC. Ensuite,
|
||||
seul un A/B non instrumenté sur deux copies identiques peut qualifier le gain
|
||||
sur la mise à jour réelle et la cible de 60 FPS.
|
||||
|
||||
## Essai SRAM ciblé : rejeté après A/B natif
|
||||
|
||||
Un candidat a court-circuité le bus dans le helper JIT uniquement pour les
|
||||
stores alignés de 32 bits sur les pages SRAM hautes `0x00` et `0x19`, avec
|
||||
gardes boot0/split et chemin bus conservé ailleurs. Le test dédié des gardes
|
||||
et **153 tests** `Starlet*` ont passé. BootMii puis Wii ont démarré dans le
|
||||
profil isolé `P1-trace-004`. HBC est absent de cette NAND ; ce trajet n'a pas
|
||||
été testé. Le binaire candidat figé (SHA-256
|
||||
`B5F3C3BA1D6EE2C86E5A3EB685F3B3D5FCEF2674C51EA137B697903F9E210006`)
|
||||
reste dans `build-P1-trace-005` pour reproduire l'essai, mais le raccourci a
|
||||
été **retiré du source courant** après les mesures ci-dessous.
|
||||
|
||||
`P1-trace-005` (candidat) et `P1-trace-006` (témoin, exécutable SHA-256
|
||||
`D809E04E2E8E900DB06D3813A67805E66EADB2BB79924CB8E805CB724360EAD4`)
|
||||
partaient des mêmes 31 fichiers, d'un journal vérifié de 533 705 344 octets
|
||||
et de la même configuration, hormis les chemins isolés. Le profilage détaillé
|
||||
était désactivé dans les deux builds. Une mesure hôte légère a relevé toutes
|
||||
les 10 s la taille du journal et le temps CPU du processus, sans lire la
|
||||
mémoire invitée ni injecter d'entrée. Les durées ci-dessous interpolent les
|
||||
franchissements des **mêmes** seuils de 600 Mo et 2,7 Go ; l'incertitude
|
||||
temporelle reste de l'ordre de l'intervalle d'échantillonnage.
|
||||
|
||||
| Mesure 600 Mo → 2,7 Go | Candidat | Témoin |
|
||||
| --- | ---: | ---: |
|
||||
| Temps mur interpolé | 434,7 s | 402,4 s |
|
||||
| Temps CPU du processus | 501,5 s | 459,8 s |
|
||||
| Journal final | 2 763 867 232 octets | 2 733 507 520 octets |
|
||||
|
||||
Les deux mises à jour ont abouti et les journaux passent la vérification
|
||||
complète des transactions. Le SYSCONF est valide ; le menu v514 et IOS80 v6944
|
||||
ont respectivement leurs 9 et 15 contenus cohérents avec les TMD. L'utilisateur
|
||||
a aussi trouvé le témoin plus fluide. Aucun flux FPS précis n'a été enregistré
|
||||
pour cet A/B. C'est un seul passage par variante, avec une taille finale de
|
||||
journal légèrement différente : il ne démontre pas la taille exacte d'une
|
||||
régression, mais **ne montre aucun gain**. Le nombre d'accès SRAM n'était donc
|
||||
pas, à lui seul, une justification suffisante pour ce raccourci. Ne pas le
|
||||
promouvoir. Les captures brutes restent dans `P1-trace-005/Capture` et
|
||||
`P1-trace-006/Capture` pour réanalyse.
|
||||
@@ -0,0 +1,802 @@
|
||||
# Étude des performances de la mise à jour Wii native
|
||||
|
||||
Date : 13 septembre 2026. Profil étudié : `.starlet_formatter_test_20260913`.
|
||||
La réussite de la mise à jour est confirmée par l'utilisateur.
|
||||
|
||||
**Méthode à suivre depuis le bilan F du 19 septembre :**
|
||||
[protocole de diagnostic P1](Wii_LLE_Performance_Diagnostic_Protocol.md).
|
||||
Les propositions d'essais ci-dessous constituent l'historique, pas une instruction de
|
||||
relancer une série de mises à jour. L'attribution du coût doit précéder le prochain correctif.
|
||||
|
||||
## Conclusion
|
||||
|
||||
La priorité est le chemin mémoire du JIT ARM, pas un changement de moteur JIT ou
|
||||
une désactivation de la persistance NAND. Un premier prototype conservateur réduit
|
||||
le temps d'une boucle d'écritures SRAM d'environ **9,5 %**. Les lectures SRAM lentes
|
||||
offrent un potentiel plus important, mais leur élargissement nécessite une validation
|
||||
matérielle et de la chaîne de démarrage. Le journal NAND peut être beaucoup plus compact,
|
||||
sans que la réduction de sa taille se traduise proportionnellement en temps gagné.
|
||||
|
||||
Lors de l'étude initiale, aucun correctif de production n'était déployé : la build interactive,
|
||||
la NAND mise à jour et les réglages de vitesse restaient inchangés. Les sections 5 et 6 décrivent
|
||||
les essais ultérieurs, après compilation des correctifs SRAM. Les prototypes de format de journal
|
||||
restent des exécutables distincts et ne sont pas utilisés par les builds interactives.
|
||||
|
||||
## 1. Mesures pendant la mise à jour
|
||||
|
||||
- PID 27108, lancé à 00:52:47, Ethernet AX88772 et NAT actifs.
|
||||
- À 01:00, 1 226 trames reçues supplémentaires en environ onze secondes ; aucune
|
||||
erreur de polling NAT. Cela prouve une progression, pas l'absence de toute limitation réseau.
|
||||
- Thread principal : 76 à 83 % d'un cœur selon les fenêtres mesurées.
|
||||
- Thread de rendu : environ 88 % des échantillons en attente.
|
||||
- Vitesse d'émulation : phases à environ 65–76 %, alternant avec des phases à pleine vitesse.
|
||||
- Environ 99,6 % des instructions ARM comptées par le JIT sont natives. Cette mesure
|
||||
ne signifie pas que les accès mémoire associés évitent les appels C++.
|
||||
- Les 21,58 % d'échantillons dans `NtWaitForSingleObject` ne sont pas attribués au disque :
|
||||
sans piles d'appels, le limiteur de vitesse et d'autres attentes restent des explications possibles.
|
||||
|
||||
La capture CPU et les compteurs sont conservés dans
|
||||
`.starlet-profile/update-profile-20260913-0100.txt` et
|
||||
`.starlet-profile/update-diagnosis-20260913.md`.
|
||||
|
||||
## 2. Journal NAND réel : beaucoup d'octets, mais pas une preuve de domination du disque
|
||||
|
||||
Une analyse binaire en lecture seule a vérifié les sommes de contrôle de toutes les
|
||||
transactions, sans déchiffrer la NAND. Le journal final représente **2 928 558 112 octets**.
|
||||
Le seuil de départ est sa taille observée avant la session, **533 705 344 octets**.
|
||||
Les chiffres suivants couvrent donc la session après ce seuil, pas un chronométrage
|
||||
strict des seuls appels de l'interface de mise à jour.
|
||||
|
||||
| Catégorie | Transactions | Octets |
|
||||
|---|---:|---:|
|
||||
| Programmation d'une page | 555 504 | 1 193 222 592 |
|
||||
| Effacement d'un bloc de 64 pages | 8 871 | 1 201 630 176 |
|
||||
| Total après le seuil | 564 375 | 2 394 852 768 |
|
||||
|
||||
Chaque effacement sérialise actuellement 64 pages entièrement remplies de `0xff`,
|
||||
soit **135 456 octets** avec les métadonnées et la somme de contrôle. Un enregistrement
|
||||
décrivant exactement le bloc effacé pourrait représenter la même opération en **36 octets**.
|
||||
L'estimation sur cette session passe ainsi de 2,395 Go à **1,194 Go**, environ **50 % de moins**.
|
||||
|
||||
Il n'y a que 7 048 réécritures de page strictement identiques parmi 1 123 248 enregistrements
|
||||
de page après le seuil, soit environ 0,63 %. La simple suppression des doublons n'est donc
|
||||
pas le meilleur premier levier sur cette charge.
|
||||
|
||||
### Comparaison synthétique avec la bibliothèque de production
|
||||
|
||||
Même charge par essai : 128 effacements, chacun suivi de 64 programmations ; 8 320 transactions.
|
||||
Le code de production `NANDJournal::Append` est appelé directement pour la référence.
|
||||
Les trois variantes conservent un `fflush` par transaction. Trois répétitions, ordre tournant.
|
||||
Les opérations synthétiques sont relues indépendamment : sommes de contrôle et pages finales
|
||||
identiques. Les fichiers synthétiques sont ensuite supprimés individuellement.
|
||||
|
||||
| Variante | Temps médian | Taille |
|
||||
|---|---:|---:|
|
||||
| Production actuelle | 86,377 ms | 34 934 816 octets |
|
||||
| Un seul `WriteArray` avec record + checksum | 94,858 ms | 34 934 816 octets |
|
||||
| Effacements compacts + écriture regroupée | 69,166 ms | 17 601 056 octets |
|
||||
|
||||
Le regroupement seul n'est pas un gain démontré. L'encodage compact réduit d'environ
|
||||
20 % le temps de ce banc d'essai, et de 49,6 % le volume. Les chiffres absolus sont ceux
|
||||
du cache disque et du processeur de cette machine ; ils n'incluent pas les calculs et
|
||||
la planification d'IOS, les contrôleurs NAND/AES/ECC, ni les échanges réseau. On ne peut
|
||||
pas transformer ces pourcentages en promesse de gain global sur la mise à jour.
|
||||
|
||||
**Le prototype compact n'est pas un format de journal déployable** : il utilise un marqueur
|
||||
expérimental non reconnu par le lecteur actuel. Une intégration réclame un format versionné,
|
||||
une lecture des anciens journaux, une migration sauvegardée et des tests de transactions
|
||||
interrompues, corruption et retour à une ancienne build. `fflush` est conservé à l'identique ;
|
||||
ces essais ne prouvent pas une garantie supplémentaire contre une panne électrique de l'hôte.
|
||||
|
||||
## 3. Mémoire ARM : origine concrète des appels lents
|
||||
|
||||
Dans `ARMJitX64::EmitFastmemAddress` :
|
||||
|
||||
- Les lectures SRAM directes sont limitées aux pages logiques `00, 10, 12, 14, 19, 1e`.
|
||||
- Les écritures SRAM sont toutes rejetées vers le bus exact, après plusieurs vérifications
|
||||
qui ne peuvent actuellement conduire à une écriture directe.
|
||||
- Les transferts de listes de registres sont exclus du chemin SRAM direct.
|
||||
- Des pages très sollicitées dans les compteurs de mise à jour, telles que `04, 16, 15`,
|
||||
ne font pas partie de la liste des lectures directes.
|
||||
- Les chemins lents paient les appels généré → C++, la classification du bus, le calcul des
|
||||
alias SRAM, et l'instrumentation. Il ne suffit donc pas d'afficher « 99,6 % natif ».
|
||||
|
||||
Un banc d'essai utilise le véritable `ARMCore`/JIT et le véritable bus `StarletMemory` pour
|
||||
les données SRAM. Seul le petit programme Thumb est placé dans une RAM synthétique.
|
||||
Il ne charge aucun dump et n'initialise aucun profil. Les registres et la mémoire finale sont vérifiés.
|
||||
|
||||
Pour un même programme de lecture et la même adresse `0xffff4000`, autoriser le pointeur
|
||||
direct déjà validé prend environ **14 ms**, contre **70 ms** en forçant le bus exact,
|
||||
pour 8 millions d'instructions : environ **5 fois moins de temps sur cette boucle**.
|
||||
Les lectures de la page non admise `0xffff6000` et les écritures restent sur le chemin lent.
|
||||
Ce microbenchmark utilise une MMU désactivée, un programme homogène et pas de périphériques
|
||||
en activité : le facteur cinq **n'est pas** un facteur cinq sur IOS ou sur la mise à jour.
|
||||
|
||||
### Prototype conservateur : rejoindre plus tôt le même chemin lent
|
||||
|
||||
Le prototype ajoute seulement `sram_access == Write` au rejet initial SRAM, puisque ces
|
||||
écritures sont déjà rejetées inconditionnellement plus loin. Il ne permet aucune écriture
|
||||
directe nouvelle, ne supprime aucun appel au bus et ne modifie pas le comportement d'IOS.
|
||||
|
||||
Comparaison alternée **A/B/B/A/A/B**, trois répétitions par exécutable, affinité sur le même
|
||||
processeur logique 0, 32 millions d'instructions par boucle, compilation initiale exclue :
|
||||
|
||||
| Boucle d'écritures SRAM | Médiane, 9 échantillons |
|
||||
|---|---:|
|
||||
| Production | 267,285 ms |
|
||||
| Rejet anticipé | 241,842 ms |
|
||||
|
||||
Soit **9,5 % de temps en moins** sur cette boucle. Les 16 millions d'écritures par mesure
|
||||
passent toujours par le chemin lent et le résultat architectural reste identique.
|
||||
Les fluctuations de fréquence/ordonnancement subsistent ; les résultats ne constituent
|
||||
pas un gain global mesuré. Le prototype lié aux tests existants passe **176 tests sur 22 suites**,
|
||||
y compris les tests NAT avec le runtime présent ; deux benchmarks préexistants sont désactivés.
|
||||
La validation interactive BootMii → IOS → menu → HBC → rechargement IOS reste à effectuer.
|
||||
|
||||
## 4. Ordre proposé pour les correctifs
|
||||
|
||||
1. **Rejet anticipé des écritures SRAM** : petite modification, gain local mesuré, même bus,
|
||||
tests ciblés passés. Valider une build séparée sur toute la chaîne avant de l'adopter.
|
||||
2. **Lectures SRAM chaudes** : caractériser précisément les pages et leurs alias, puis étendre
|
||||
la voie directe par classe d'accès avec tests interpréteur/JIT, deux endiannesses,
|
||||
boot0 actif/inactif, split, trous, frontières et rechargements IOS. Potentiel plus élevé.
|
||||
3. **Journal compact pour les effacements** : format versionné et récupération testée,
|
||||
mêmes opérations invitées et même niveau de persistance avant acquittement.
|
||||
4. **Chronométrage par phase dans une exécution comparable** : temps ARM, attente/limiteur,
|
||||
appels du bus, contrôle NAND et journal, débit réseau ; comparer des copies d'un même
|
||||
état pré-mise à jour. Ne pas reformater ni rétrograder la NAND actuelle pour fabriquer ce test.
|
||||
|
||||
Ne pas réactiver globalement les écritures SRAM directes : le code documente une précédente
|
||||
régression BootMii malgré des tests de bornes isolés. Ne pas désactiver les contrôles IOS,
|
||||
modifier les horloges Wii, ou différer arbitrairement les sauvegardes pour afficher plus de FPS.
|
||||
|
||||
## 5. Extension des lectures SRAM mesurées (13 septembre, 23 h)
|
||||
|
||||
Le rejet anticipé des écritures a été compilé puis lancé sur le profil mis à jour ; l'utilisateur
|
||||
a confirmé que ce premier essai fonctionnait. La chaîne complète de rechargements reste un
|
||||
critère de validation distinct, pas une conséquence des tests unitaires.
|
||||
|
||||
La trace arrêtée à 22:52 contient 73 117 662 lectures SRAM lentes. Les pages `04, 16, 15, 1d`
|
||||
en représentent 47 810 655, soit environ 65,4 %. Avec `1c`, également observée pendant l'étude
|
||||
de la mise à jour, elles sont désormais admises pour les lectures scalaires directes.
|
||||
La liste totale est `00, 04, 10, 12, 14, 15, 16, 19, 1c, 1d, 1e`.
|
||||
Un masque de 32 bits remplace la chaîne de comparaisons. Les états boot0/split sont toujours
|
||||
lus à l'exécution ; trous, bornes, écritures et listes de registres gardent leur traitement existant.
|
||||
Ni les horloges, ni les instructions IOS, ni le journal NAND ne sont modifiés par ce correctif.
|
||||
|
||||
### Validation hors firmware
|
||||
|
||||
Le nouveau `SRAMReadTest.cpp` utilise le vrai `StarletMemory` pour les données, sans initialiser
|
||||
de dump ou de profil. Il vérifie :
|
||||
|
||||
- les lectures ARM/Thumb de mots, octets, demi-mots et valeurs signées, dans les deux endiannesses ;
|
||||
- 37 908 combinaisons d'alias, pages, alignements/frontières et changements boot0/split,
|
||||
en comparant les 16 registres et CPSR à l'interpréteur, sans modifier les 96 Kio de SRAM ;
|
||||
- la réutilisation des blocs compilés lors des changements de mapping matériel ;
|
||||
- un remappage de page SRAM après une invalidation TLB réellement exécutée par le code invité ;
|
||||
- zéro appel de lecture de données au bus pour les nouveaux accès alignés autorisés.
|
||||
|
||||
Avant le correctif, les tests de chemins rapides échouaient comme attendu sur les compteurs,
|
||||
alors que les résultats mémoire correspondaient. Après le correctif, **179 tests sur 23 suites**
|
||||
passent, dont les tests existants de protection boot0, écritures SRAM, NAND, appairage et NAT.
|
||||
Deux benchmarks préexistants restent désactivés.
|
||||
|
||||
### Comparaison A/B
|
||||
|
||||
Comparaison alternée A/B/B/A/A/B après la compilation, sur le même processeur logique 0,
|
||||
avec neuf échantillons par cas, 32 millions d'instructions et compilation initiale exclue :
|
||||
|
||||
| Boucle | Avant, médiane | Après, médiane |
|
||||
|---|---:|---:|
|
||||
| Lecture page `16` (`0xffff6000`), nouvellement directe | 263,97 ms | 43,44 ms |
|
||||
| Lecture page `14`, déjà directe | 45,39 ms | 45,31 ms |
|
||||
| Lecture page `14`, bus forcé | 245,27 ms | 224,46 ms |
|
||||
| Écriture SRAM, toujours par le bus | 206,77 ms | 215,03 ms |
|
||||
|
||||
Le cas ciblé passe de 16 millions à zéro lecture lente et prend environ **6 fois moins de temps**.
|
||||
Les valeurs finales sont vérifiées pour tous les échantillons. Les cas de contrôle montrent
|
||||
encore la variabilité de l'hôte ; ne pas interpréter leurs écarts comme des correctifs supplémentaires.
|
||||
Ce banc homogène sans MMU ni périphériques actifs **ne mesure pas** la durée d'une mise à jour
|
||||
Wii, ni les FPS de la chaîne complète. Ces gains réels restent à mesurer sur la build interactive.
|
||||
Le premier chronométrage effectué pendant la compilation est exclu de cette comparaison.
|
||||
|
||||
La build précédente et le journal courant (3 080 124 160 octets) ont été sauvegardés dans
|
||||
`.starlet-profile/before-sram-read-20260913-230752/`. Les SHA-256 de la copie et du journal
|
||||
actif ont été comparés et sont identiques. Aucun compactage ni retour arrière de la NAND
|
||||
n'a été réalisé. La nouvelle build reste en validation interactive, pas un jalon final validé.
|
||||
|
||||
## 6. Préparation d'une vraie comparaison de mise à jour
|
||||
|
||||
À la demande de l'utilisateur, deux profils indépendants ont été préparés dans
|
||||
`.starlet_update_ab_20260913/A` et `B`. La NAND de départ est le préfixe de 533 705 344 octets
|
||||
du journal archivé : exactement la taille observée avant la session de mise à jour initiale.
|
||||
Extraction dans un nouveau fichier uniquement ; aucune troncature/restauration d'un journal actif.
|
||||
Ses 125 881 transactions, le SYSCONF et les 24 contenus du menu/IOS80 ont été revérifiés.
|
||||
SHA-256 du préfixe : `07b2f4bc543cfd4203efc448538505e9d503b2727c16aaff8d8c0b286d8b4f0c`.
|
||||
|
||||
Les fichiers des deux profils sont identiques, sauf leurs chemins de sortie isolés. SD et runtime
|
||||
sont copiés ; executables et PDB sont figés séparément. A inclut déjà le rejet anticipé des écritures,
|
||||
B ajoute les lectures scalaires étendues : cette comparaison isole donc cette seconde optimisation.
|
||||
Les journaux des profils habituels ont été contrôlés inchangés après préparation.
|
||||
|
||||
A a été lancé le 13 septembre à 23:26:56 (PID 1696), Ethernet actif, avec une capture en lecture
|
||||
seule des performances, compteurs NAT/CPU/I/O et taille du journal toutes les dix secondes.
|
||||
Le coût de chaque interrogation est enregistré : les premières lectures réussies coûtent environ
|
||||
0,7 seconde de temps mural, ce qui impose de garder la même instrumentation dans les deux essais
|
||||
et de ne pas négliger son impact potentiel. La première lecture, trop précoce pendant l'initialisation,
|
||||
a échoué sans affecter Dolphin ; les suivantes fonctionnent. La capture n'injecte pas d'entrées,
|
||||
n'écrit pas en mémoire et ne suspend pas les threads.
|
||||
|
||||
Cet essai du 13 septembre n'a pas de confirmation de fin. Le 16 septembre, son dossier complet
|
||||
a été conservé sous `A-interrupted-20260913`, puis A recréé à l'identique depuis B encore intact.
|
||||
Les empreintes de tous les fichiers du nouveau A correspondent à son manifeste initial.
|
||||
|
||||
## 7. Résultats de la paire native A/B du 16 septembre
|
||||
|
||||
L'utilisateur confirme la réussite des deux mises à jour. Aucun réglage d'horloge, accès réseau,
|
||||
contrôle IOS ou format du journal n'a été modifié entre les deux exécutables figés.
|
||||
|
||||
| Mesure | A, avant extension des lectures | B, après extension |
|
||||
|---|---:|---:|
|
||||
| Durée entre signalements début/fin | 13 min 29 s | 11 min 33 s |
|
||||
| FPS médians des relevés | 22,79 | 28,33 |
|
||||
| Vitesse d'émulation médiane | 46,3 % | 55,0 % |
|
||||
| Plus bas FPS échantillonné | 8,42 | 7,28 |
|
||||
| Échantillons sous 90 % de vitesse | 68 / 73 | 56 / 63 |
|
||||
| Accroissement du journal entre premiers/derniers relevés sélectionnés | 2 190 232 512 octets | 2 190 232 512 octets |
|
||||
| Erreurs de polling NAT | 0 | 0 |
|
||||
|
||||
Sur cette paire, **115,5 secondes de moins, soit environ 14,3 % de réduction de durée** sont
|
||||
observées. Ce n'est ni un gain global de 6×, ni un fonctionnement garanti à 50–60 FPS. Les chutes
|
||||
à 7–9 FPS demeurent. Les FPS sont des relevés discrets, pas une distribution pondérée par image.
|
||||
|
||||
Les heures de début/fin proviennent des signalements et inspections, pas d'un chronométrage
|
||||
exact du clic et de l'écran de réussite. Pour recouper, les franchissements échantillonnés de
|
||||
600 Mo à 2700 Mo de journal prennent 685,6 s dans A et 582,6 s dans B (environ 15 % de moins).
|
||||
Ils ont une granularité d'environ onze secondes et ne désignent pas exactement les mêmes
|
||||
transactions. Conditions serveur, cache/température/charge hôte et capture restent des facteurs
|
||||
de variation ; une paire séquentielle ne mesure pas à elle seule l'effet causal exact du correctif.
|
||||
|
||||
### Profilage complémentaire pendant la chute de B
|
||||
|
||||
À la demande de diagnostic, un échantillonnage des pointeurs d'instruction de 15 secondes a été
|
||||
ajouté entre 17:16:47 et 17:17:03. Il suspend/reprend brièvement les threads. **Ce surcoût n'était
|
||||
pas présent sur A**, et reste inclus dans la durée observée de B ; il ne faut pas soustraire
|
||||
arbitrairement quinze secondes. La fenêtre est marquée dans les résultats.
|
||||
|
||||
- Thread d'émulation : 14,000 secondes CPU en 15 secondes, soit environ 93,3 % d'un cœur.
|
||||
- Allocation identifiée du JIT ARM : 393 / 953 échantillons du thread, environ 41,2 %.
|
||||
- Accès `StarletMemory`, traduction d'adresses, fonctions mémoire et quelques chemins interprétés
|
||||
apparaissent également parmi les points chauds.
|
||||
- Thread de rendu : environ 88,9 % des échantillons en attente, 1,438 seconde CPU en 15 secondes.
|
||||
- Sur 3,828 secondes couvertes par cinq compteurs de cette fenêtre : environ un million
|
||||
d'écritures SRAM lentes par seconde. Il s'agit d'accès mémoire ARM, pas d'appels d'écriture disque,
|
||||
et le nombre d'accès n'est pas une part de temps CPU.
|
||||
|
||||
Le frein observé est principalement côté exécution CPU/mémoire, pas le thread de rendu.
|
||||
Les données ne démontrent pas que le journal NAND domine le temps. Prochaines cibles à mesurer :
|
||||
écritures SRAM via le bus, traduction, dispatch et chemins de repli, en conservant les protections
|
||||
et la cohérence. Ne pas réactiver globalement les écritures directes qui avaient régressé BootMii.
|
||||
|
||||
### Intégrité à l'arrêt et conservation
|
||||
|
||||
Après fermeture confirmée, les journaux ont été relus intégralement : **651 682 transactions valides
|
||||
pour A, 651 690 pour B**. Les deux SYSCONF sont valides ; les 24 contenus du menu v514 et
|
||||
de l'IOS80 v6944 correspondent à leurs empreintes TMD. Il ne s'agit pas d'un audit exhaustif
|
||||
de tous les titres installés. Les journaux finaux font respectivement 2 763 850 048 et
|
||||
2 763 867 232 octets ; leurs empreintes et preuves sont conservées dans chaque dossier `Capture`.
|
||||
Les profils habituels, les dumps d'origine et la graine pré-mise à jour n'ont pas été remplacés.
|
||||
|
||||
## 8. Optimisation du bus SRAM et des marches de tables (16–19 septembre)
|
||||
|
||||
Le profilage de B a conduit à deux changements ciblés, sans nouveau chemin d'écriture directe
|
||||
dans le JIT et sans modification des horloges, du NAT ou du journal NAND :
|
||||
|
||||
- Les écritures SRAM alignées de 16/32 bits sont traitées avant le décodage MMIO. Un accès aligné
|
||||
reste dans un seul segment ; les protections boot0 et le bit SM sont relus à chaque accès.
|
||||
Les accès non alignés conservent le chemin précédent, y compris aux limites des banques.
|
||||
- Le calcul de banque SRAM utilise l'échange du bit d'aperture, suivi du contrôle des 96 Kio
|
||||
physiques. Les trous ne deviennent pas des alias valides.
|
||||
- Les marches de tables ordinaires ne demandent plus un pointeur de descripteur inutilisé.
|
||||
Le JIT continue à obtenir ces pointeurs pour valider ses blocs après maintenance du TLB.
|
||||
Les lectures de descripteurs et les règles de traduction ne changent pas.
|
||||
|
||||
### Vérification hors firmware
|
||||
|
||||
**182 tests passent**. Les nouveaux tests confrontent chaque octet de trois apertures au modèle
|
||||
de mapping indépendant (1 572 864 lectures), puis les écritures larges à un oracle octet par octet
|
||||
sur toutes les fenêtres miroirs, les limites, les modes SM/boot0 et leur retour à l'état initial.
|
||||
Ils comparent toute la SRAM après chaque écriture. La traduction est vérifiée en big/little-endian
|
||||
pour les sections et les tables coarse/fine (pages large, small et tiny). Les tests existants de
|
||||
DMA, IRQ, MMU, changement de contexte JIT, NAND et réseau restent inclus.
|
||||
|
||||
### Mesure synthétique avant/après
|
||||
|
||||
Même programme ARM/Thumb, résultats vérifiés, bibliothèque Release, CPU logique 0, exécutables
|
||||
avant/après distincts. Deux séries alternées, ABBAAB puis BAABBA, donnent chacune neuf mesures
|
||||
par variante et par cas. Aucun émulateur ni compilateur n'est lancé pendant la mesure.
|
||||
Le PC montre une forte variabilité ; les durées CPU du thread sont aussi enregistrées mais leur
|
||||
granularité est limitée. Les valeurs suivantes sont les médianes de durée murale en millisecondes :
|
||||
|
||||
| Cas | Avant, série 1 | Après, série 1 | Avant, série 2 | Après, série 2 |
|
||||
|---|---:|---:|---:|---:|
|
||||
| 16 millions d'écritures SRAM, alias `ffff9000` | 271,06 | 149,17 | 273,50 | 156,40 |
|
||||
| Même charge, alias bas `0d419000` | 307,16 | 154,17 | 383,38 | 176,50 |
|
||||
| Lecture déjà directe, contrôle `ffff4000` | 56,82 | 56,55 | 74,31 | 71,17 |
|
||||
| Marches de tables forcées, charge interprétée | 174,94 | 124,29 | 318,02 | 136,92 |
|
||||
|
||||
Le cas principal d'écriture montre environ **43–45 % de durée en moins sur ce microbanc**.
|
||||
Les 16 millions d'écritures passent toujours par le bus, avec les mêmes résultats ; aucune n'est
|
||||
supprimée. La variabilité, surtout pour la traduction, interdit une extrapolation précise.
|
||||
Ce résultat ne signifie pas 43–45 % de moins sur une mise à jour complète, ni 50–60 FPS garantis.
|
||||
La validation native de cette nouvelle optimisation est décrite en section 9 ;
|
||||
les mises à jour A/B de la section 7 concernaient uniquement l'extension des lectures.
|
||||
Les profils et journaux réels ne sont pas remplacés par ces tests.
|
||||
|
||||
### Préparation du test natif C
|
||||
|
||||
Le 19 septembre, un troisième profil `C` et un exécutable figé `build-C` sont créés sans écraser A/B.
|
||||
Tous les fichiers du profil initial correspondent aux empreintes du manifeste initial B, hormis
|
||||
les chemins de sortie redirigés vers C. Les copies intactes de TimePlayed, SRAM GameCube et SD brute
|
||||
sont récupérées depuis le profil de test du formateur, sans le modifier. La NAND de départ est à
|
||||
nouveau vérifiée intégralement. Premier lancement à 11:48 Paris, capture de compteurs active.
|
||||
Sa fenêtre étant invisible pour l'utilisateur dans le contexte isolé, il est relancé sur le bureau
|
||||
visible à 11:52, après confirmation que le journal n'avait pas changé. L'utilisateur ferme ensuite
|
||||
cette instance après le démarrage natif ; elle est relancée à 11:55 en conservant ses changements,
|
||||
sans réinitialiser la NAND. Le journal avant cette dernière relance est vérifié : 536 348 704 octets,
|
||||
126 491 transactions valides et contenus menu/IOS80 intacts. Seule la capture du dernier PID 15540
|
||||
est utilisée pour mesurer la mise à jour.
|
||||
Les essais ont lieu à des dates différentes, ce qui ajoute des variations possibles de charge
|
||||
hôte et de conditions serveur.
|
||||
|
||||
## 9. Résultat natif C du 19 septembre
|
||||
|
||||
L'utilisateur confirme la fin de la mise à jour, puis ferme Dolphin pour la validation finale.
|
||||
|
||||
| Mesure | B, extension des lectures | C, bus SRAM et marches de tables optimisés |
|
||||
|---|---:|---:|
|
||||
| Durée entre signalements début/fin | 11 min 33 s | 9 min 07 s |
|
||||
| FPS médians des relevés | 28,33 | 41,61 |
|
||||
| Plus bas FPS échantillonné | 7,28 | 13,17 |
|
||||
| Vitesse d'émulation médiane | 55,0 % | 86,8 % |
|
||||
| Relevés sous 90 % de vitesse | 56 / 63 | 27 / 51 |
|
||||
| Temps CPU du processus entre relevés sélectionnés | 796,91 s | 604,77 s |
|
||||
| Erreurs de polling NAT pendant la fenêtre | 0 | 0 |
|
||||
|
||||
La durée observée diminue de **146,4 secondes, environ 21,1 %**, avec une nette amélioration des
|
||||
FPS échantillonnés. En recoupement, le passage échantillonné de 600 Mo à 2700 Mo de journal prend
|
||||
439,8 s dans C contre 582,6 s dans B, environ 24,5 % de moins. Ces seuils sont espacés d'environ
|
||||
onze secondes et ne représentent pas exactement les mêmes transactions.
|
||||
|
||||
Ce n'est pas une mesure causale isolée : les signalements sont des repères approximatifs,
|
||||
les journées/charges hôte et conditions serveur diffèrent, C a été redémarré avant la mise à jour,
|
||||
et B comporte une fenêtre supplémentaire de profilage hôte de quinze secondes. Les relevés FPS
|
||||
ne sont pas pondérés par image ; 13,17 FPS est le minimum échantillonné, pas le minimum garanti
|
||||
sur toutes les images. **L'objectif de 50–60 FPS constants n'est pas encore atteint.**
|
||||
|
||||
Après fermeture, le journal C est intégralement valide : **651 746 transactions**, 2 763 987 520
|
||||
octets, 165 120 pages superposées, génération 821 612. SYSCONF est valide et les 24 contenus du
|
||||
menu v514/IOS80 v6944 correspondent aux empreintes TMD. Les autres titres ne sont pas vérifiés
|
||||
par ce contrôleur. SHA256 final :
|
||||
`81b402d72c7c5fbe38e77a234212125488e0442560b5d0ba73c7a2da800fde56`.
|
||||
La NAND C et ses mesures sont conservées sans remplacer les résultats A/B, la graine initiale
|
||||
ou les profils habituels. Aucun commit ni push n'a été effectué pour cette nouvelle étape.
|
||||
|
||||
## 10. Copies DMA par blocs — candidat D du 19 septembre
|
||||
|
||||
Les phases lentes de C restent notamment visibles pendant le transfert et l'installation :
|
||||
les relevés entre 10:04:51 et 10:06:06 UTC descendent vers 13–17 FPS, alors que le journal progresse.
|
||||
Le code des périphériques copiait encore les buffers DMA octet par octet. En MEM1/MEM2, chaque
|
||||
octet repassait par la résolution de la mémoire hôte. Le profil hôte de B avait déjà échantillonné
|
||||
`GetPointerForRange`, `Read_U8` et les chemins associés ; cela motive une mesure ciblée, mais ne
|
||||
quantifie pas leur contribution exacte à C.
|
||||
|
||||
Le candidat D résout une plage contiguë une seule fois, puis utilise une copie par blocs pour
|
||||
AES, SHA, les données/spare/ECC NAND, les buffers/descripteurs OHCI et les transferts SD.
|
||||
Seules les plages entièrement contenues dans MEM1, MEM2 ou les 96 Kio physiques de SRAM basse
|
||||
sont accélérées. La SRAM DMA ignore toujours SM et le recouvrement boot0 du CPU. Les trous,
|
||||
miroirs hauts, registres, franchissements de limites et débordements d'adresse restent sur le
|
||||
chemin octet par octet. Les tampons temporaires restent séparés de la mémoire invitée ; AES
|
||||
capture toujours toute son entrée avant d'écrire une sortie éventuellement superposée.
|
||||
Ni les temporisations, ni le format/flush du journal NAND, ni les traitements réseau ne changent.
|
||||
|
||||
### Microbanc et validation
|
||||
|
||||
Le banc utilise de vraies commandes MMIO du moteur AES en **mode copie, chiffrement désactivé**,
|
||||
pour isoler le coût DMA, sans NAND, firmware, SD ou réseau. Chaque cas transfère 64 Mio et vérifie
|
||||
tous les octets de sortie ainsi que les registres source/destination/commande. Ordre ABBAAB,
|
||||
trois répétitions par exécution, neuf mesures par cas/version, même cœur hôte fixé.
|
||||
|
||||
| Banque / taille par commande | Médiane avant | Médiane après |
|
||||
|---|---:|---:|
|
||||
| MEM1 / 2 Kio | 1708,93 ms | 27,42 ms |
|
||||
| MEM1 / 16 Kio | 1669,52 ms | 10,95 ms |
|
||||
| MEM2 / 2 Kio | 2247,41 ms | 26,41 ms |
|
||||
| MEM2 / 16 Kio | 2173,30 ms | 10,57 ms |
|
||||
| SRAM / 2 Kio | 301,39 ms | 28,91 ms |
|
||||
| SRAM / 16 Kio | 265,90 ms | 12,37 ms |
|
||||
|
||||
La variabilité hôte est importante, même à affinité constante. Les temps CPU sont enregistrés,
|
||||
mais leur granularité de 15,625 ms limite la précision des cas accélérés. Ces chiffres ne sont
|
||||
**ni une mesure du chiffrement AES, ni une accélération annoncée de la mise à jour complète**.
|
||||
Le gain natif du candidat D reste à mesurer.
|
||||
|
||||
Les **189 tests ciblés passent**. Sept nouveaux tests couvrent : comparaison au bus octet par
|
||||
octet sur 1 144 combinaisons de plage/mode, canaris et SRAM entière, limites/overflow, pages OHCI
|
||||
non contiguës, endian des descripteurs, colonnes NAND et spare/ECC séparés, entrée aléatoire NAND,
|
||||
AES avec entrée/sortie superposées, SHA multiblocs sur les trois banques, SD/SDHC synthétique et
|
||||
protection des écritures. Ces sept tests passent aussi trois répétitions avec ordre mélangé.
|
||||
Les tests antérieurs d'AES chiffré/déchiffré, de SHA en SRAM split, de JIT/MMU et de persistance
|
||||
NAND restent dans la sélection. Les seules suppressions de fichiers des nouveaux tests concernent
|
||||
leurs images SD temporaires uniques ; aucun profil utilisateur n'est utilisé.
|
||||
|
||||
Le test natif D est préparé dans un nouveau profil depuis la graine initiale contrôlée, avec
|
||||
les mêmes fichiers initiaux que B (chemins de sortie isolés). A/B/C et les profils habituels ne
|
||||
sont pas remplacés. Build figée SHA256 :
|
||||
`1ab2746044256533185836a49e7644817f8f2d7f523c6a9c0a6199773574151c`.
|
||||
|
||||
Le premier essai D est interrompu pendant l'absence de l'utilisateur ; il n'est pas utilisé
|
||||
pour comparer la durée complète. Sa NAND de 855 883 744 octets et ses logs sont conservés dans
|
||||
`D`, avec un marqueur `Capture/interrupted.json`. À sa demande, le 19 septembre au soir, un
|
||||
nouvel essai est lancé dans `D-repeat-20260919` avec **la même build-D**, depuis les fichiers
|
||||
initiaux et la graine de NAND vérifiés à nouveau. Ses captures et repères début/fin sont distincts
|
||||
du premier essai. Aucune NAND existante n'est supprimée ni remplacée.
|
||||
|
||||
## 11. Résultat natif D répété, 19 septembre au soir
|
||||
|
||||
Le nouvel essai `D-repeat-20260919`, PID 11548, part de la graine initiale vérifiée avec la build D
|
||||
inchangée. L'utilisateur signale le départ à 18:23:04,771 UTC, puis la fin à 18:31:37,196 UTC en
|
||||
précisant qu'elle remonte peut-être à deux minutes. **Le délai entre ces deux signalements n'est
|
||||
donc pas retenu comme durée de mise à jour**, et les relevés après stabilisation sont exclus du
|
||||
bilan actif. Le premier essai D interrompu reste conservé séparément et n'entre pas dans ce bilan.
|
||||
|
||||
Les compteurs réseau atteignent leur valeur terminale au relevé de 18:29:51,774 UTC. Le journal
|
||||
atteint sa taille terminale au relevé suivant, 18:30:02,220 UTC, puis reste stable jusqu'au
|
||||
signalement. Ce repère situe la fin de l'activité observée environ **6 min 47 à 6 min 57 après le
|
||||
signalement de départ**, soit environ sept minutes. Il ne prouve pas l'instant exact d'apparition
|
||||
de l'écran de réussite ; le signalement de départ est lui aussi approximatif. Le signalement
|
||||
final arrive environ 95 secondes après le premier relevé entièrement stabilisé.
|
||||
|
||||
Pour comparer sans utiliser ce signalement final tardif, les trois essais sont recalculés sur
|
||||
les mêmes seuils échantillonnés de taille de journal : 600 Mo vers 2700 Mo, unités décimales.
|
||||
|
||||
| Portion de progression comparable | B | C | D répété |
|
||||
|---|---:|---:|---:|
|
||||
| Durée échantillonnée | 9 min 43 s | 7 min 20 s | 5 min 49 s |
|
||||
| Nombre de relevés | 54 | 42 | 34 |
|
||||
| FPS médians | 27,84 | 39,03 | 47,32 |
|
||||
| Plus bas FPS échantillonné | 7,28 | 13,17 | 18,40 |
|
||||
| Vitesse d'émulation médiane | 53,2 % | 82,2 % | 93,1 % |
|
||||
| Temps CPU du processus | 687,09 s | 511,27 s | 354,88 s |
|
||||
| Erreurs de polling NAT | 0 | 0 | 0 |
|
||||
|
||||
Sur cette portion, D demande **environ 20,7 % de temps réel et 30,6 % de temps CPU de moins que C**.
|
||||
Les seuils ne désignent pas exactement les mêmes transactions : C commence à 644 516 788 octets
|
||||
et finit à 2 728 169 380, D à 610 411 264 et 2 700 960 352. Les instantanés sont espacés d'environ
|
||||
10–11 secondes, les conditions hôte/serveur diffèrent et il ne s'agit que d'un essai par version.
|
||||
Le coût médian d'un instantané est de 697 ms dans cette portion de C et 555 ms dans D. Les
|
||||
différences observées ne constituent donc pas un effet causal précisément isolé du seul patch.
|
||||
|
||||
Sur toute la fenêtre du signalement de départ au premier plateau terminal, D a 40 relevés :
|
||||
47,64 FPS médians, 18,40 FPS au plus bas échantillonné, vitesse médiane 96,2 %, 14 relevés sous
|
||||
90 %. Les baisses persistantes se concentrent notamment vers la fin des transferts. Les valeurs
|
||||
FPS restent des instantanés, non pondérés par image. **50–60 FPS constants ne sont pas acquis.**
|
||||
|
||||
Après confirmation de fermeture, le journal complet est vérifié : **644 569 transactions**,
|
||||
2 733 507 520 octets, 165 440 pages superposées, génération 821 569, SYSCONF valide. Les neuf contenus
|
||||
du menu v514 et quinze contenus d'IOS80 v6944 correspondent aux empreintes TMD ; les autres titres
|
||||
ne sont pas contrôlés par ce vérificateur. SHA256 final :
|
||||
`4018d66489f2bfda3577a82a206abf52b152cf7a83e9c1bced0f4f4f49ec665e`.
|
||||
NAND et résultats restent dans le profil de répétition, sans suppression ni remplacement des
|
||||
essais précédents. Aucun commit/push supplémentaire n'est effectué.
|
||||
|
||||
## 12. Candidat E : lectures SRAM et écritures scalaires spécialisées
|
||||
|
||||
Le profil du dernier essai D relève notamment 116 619 902 lectures SRAM lentes et
|
||||
806 261 862 écritures SRAM. Parmi les lectures, les pages 01, 13, 1a et 02 représentent
|
||||
respectivement environ 49,99, 12,35, 11,71 et 7,53 millions d'accès. Ce sont des compteurs
|
||||
d'accès, **pas une attribution du temps CPU**. Ils motivent une optimisation ciblée,
|
||||
mesurée séparément avant de relancer une mise à jour native.
|
||||
|
||||
Le candidat E apporte quatre changements :
|
||||
|
||||
- Les lectures scalaires du JIT peuvent utiliser le chemin direct existant pour ces quatre
|
||||
pages supplémentaires. Les contrôles vivants boot0/split, trous et alignement restent actifs.
|
||||
Les transferts de listes de registres et les écritures SRAM restent sur le bus matériel.
|
||||
- Les lectures CPU alignées de 16/32 bits en SRAM résolvent une fois la banque et l'overlay ROM,
|
||||
au lieu de refaire le décodage octet par octet. Les cas timer/IPC existants gardent la priorité.
|
||||
`HW_VERSION` est lu par mot depuis son registre vivant, sans valeur forcée.
|
||||
- Un nouveau test avec une ROM non nulle révèle un défaut préexistant aux limites : une lecture
|
||||
large non alignée pouvait ignorer une partie de l'overlay boot0 ou dépasser son tableau à la
|
||||
frontière d'un miroir. Le chemin large n'est désormais retenu que si toute sa plage est
|
||||
contiguë dans la même banque ; sinon, le bus résout chaque octet.
|
||||
- Les helpers d'écriture lente du JIT sont spécialisés à la compilation pour 1, 2 ou 4 octets.
|
||||
La largeur connue de l'instruction n'est plus transmise puis testée à chaque appel. Les mêmes
|
||||
appels `ARMBus::Write8/16/32`, compteurs, conversions d'endian et effets matériels subsistent.
|
||||
Aucun timer, délai d'interruption ou engagement de persistance NAND n'est modifié.
|
||||
|
||||
La première variante limitée aux lectures accélérait les cas ciblés, mais ralentissait le
|
||||
témoin d'écriture d'environ 7 % dans les deux ordres de comparaison. Elle n'est pas la version
|
||||
retenue pour l'essai natif. La spécialisation des écritures corrige ce recul et donne les
|
||||
résultats finaux ci-dessous.
|
||||
|
||||
### Validation et mesures isolées
|
||||
|
||||
Les **192 tests de 25 suites passent** ; les 13 tests DMA/lectures SRAM passent aussi trois fois
|
||||
en ordre mélangé. Le nouveau test mémoire compare 41 472 lectures larges à l'oracle octet par
|
||||
octet, avec ROM/SRAM non nulles, changements vivants de mapping et frontières de miroirs.
|
||||
Les nouveaux tests d'écritures natives comparent JIT et interpréteur en ARM/Thumb, petit/grand
|
||||
endian et différents mappings : registres, CPSR, SRAM entière et octets voisins doivent coïncider.
|
||||
|
||||
Le banc Thumb utilise le vrai `StarletMemory`, sans firmware ni NAND utilisateur, avec 64 millions
|
||||
d'instructions et 32 millions d'accès mémoire par cas. Les builds avant/après sont alternées en
|
||||
ABBAAB puis BAABBA, neuf échantillons par cas/version dans chaque série, sur le même cœur hôte.
|
||||
Le tableau donne les médianes murales ; les registres, valeurs et compteurs sont aussi vérifiés.
|
||||
|
||||
| Cas | D, ordre 1 | E, ordre 1 | D, ordre inverse | E, ordre inverse |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Lecture page 01 | 329,96 ms | 63,35 ms | 341,51 ms | 63,78 ms |
|
||||
| Lecture page 02 | 331,58 ms | 63,80 ms | 341,56 ms | 64,18 ms |
|
||||
| Lecture page 13 | 334,95 ms | 68,83 ms | 338,39 ms | 69,31 ms |
|
||||
| Lecture page 1a | 333,46 ms | 69,76 ms | 336,22 ms | 69,52 ms |
|
||||
| Lecture forcée par le bus | 318,91 ms | 258,61 ms | 323,60 ms | 260,80 ms |
|
||||
| Lecture déjà directe, témoin | 67,92 ms | 68,24 ms | 68,96 ms | 68,75 ms |
|
||||
| Écriture SRAM par le bus, témoin | 221,92 ms | 201,56 ms | 224,79 ms | 203,47 ms |
|
||||
| Lecture VERSION et mot suivant | 1417,86 ms | 874,05 ms | 1458,66 ms | 887,22 ms |
|
||||
|
||||
Les lectures nouvellement directes suppriment les 32 millions d'appels au bus de chaque cas.
|
||||
Le cas de lecture forcée et le témoin d'écriture conservent chacun leurs 32 millions d'appels.
|
||||
Le témoin de lecture déjà directe est pratiquement inchangé ; celui d'écriture demande environ
|
||||
9 % de temps en moins. La mesure VERSION inclut aussi le mot suivant, pas uniquement VERSION.
|
||||
Les conditions hôte peuvent varier ; une courte répétition de tests intervient vers la fin de
|
||||
la série inverse. Les temps CPU sont enregistrés mais leur granularité limite leur précision.
|
||||
**Ces gains synthétiques ne prédisent ni la durée totale de mise à jour ni des FPS constants.**
|
||||
|
||||
### Essai natif préparé
|
||||
|
||||
Le profil E repart des mêmes fichiers initiaux que B et de la même graine NAND, contrôlés avant
|
||||
la réécriture des seuls chemins isolés. Le journal initial comporte 125 881 transactions ;
|
||||
SYSCONF et les 24 contenus du menu/IOS80 sont vérifiés. Les profils A/B/C/D, la répétition D et
|
||||
les profils habituels sont conservés. Build E figée SHA256 :
|
||||
`9cca944ecece148b1ffa0cc77a160d5762804d79fc3817f24189e5954d9935fb`.
|
||||
Le gain natif de E reste à mesurer avec la même capture que D.
|
||||
|
||||
## 13. Résultat natif E, 19 septembre au soir
|
||||
|
||||
L'utilisateur confirme la réussite de la mise à jour E, PID 3308, à 19:17:49 UTC.
|
||||
Le départ signalé à 19:10:31 UTC est tardif par rapport aux premiers échanges réseau et
|
||||
écritures du journal. Les valeurs observées avant le signalement final atteignent leur
|
||||
dernier état à 19:17:41,598 UTC, soit environ 7 min 11 après le signalement de départ.
|
||||
Ce délai **n'est pas la durée exacte de la mise à jour**. L'activité reprend après le
|
||||
signalement final ; cette activité ultérieure est exclue du bilan de mise à jour.
|
||||
|
||||
La comparaison principale reprend les mêmes seuils échantillonnés de journal que pour D :
|
||||
|
||||
| Portion de journal 600 Mo vers 2700 Mo | D répété | E |
|
||||
|---|---:|---:|
|
||||
| Durée échantillonnée | 348,91 s | 350,34 s |
|
||||
| Nombre de relevés | 34 | 34 |
|
||||
| FPS médians | 47,32 | 48,70 |
|
||||
| Plus bas FPS échantillonné | 18,40 | 20,13 |
|
||||
| Vitesse d'émulation médiane | 93,05 % | 95,25 % |
|
||||
| Relevés sous 90 % de vitesse | 14 | 16 |
|
||||
| Temps CPU du processus | 354,88 s | 353,86 s |
|
||||
| Coût médian d'un instantané | 555,37 ms | 597,96 ms |
|
||||
| Erreurs de polling NAT | 0 | 0 |
|
||||
|
||||
Le temps réel augmente de 0,4 % et le temps CPU diminue de 0,3 % : **aucune accélération
|
||||
globale convaincante n'est démontrée**. La légère amélioration des FPS médians et minimaux
|
||||
ne suffit pas à établir un gain reproductible. Les seuils sont franchis à des transactions
|
||||
différentes : D couvre 610 411 264 à 2 700 960 352 octets, E 655 167 424 à 2 732 944 480.
|
||||
Les conditions hôte/serveur diffèrent, les mesures restent espacées d'environ 10–11 secondes,
|
||||
et il ne s'agit que d'un essai par version. Les gains du banc ciblé ne se traduisent donc
|
||||
pas par un nouveau gain comparable à C vers D dans cet essai natif.
|
||||
|
||||
La fenêtre du signalement de départ au dernier état pré-signalement contient 41 relevés :
|
||||
49,25 FPS médians, 20,13 au minimum, vitesse médiane 96,6 %, 16 relevés sous 90 % et 409,77 s
|
||||
de temps CPU. Les relevés après le signalement final ne sont pas inclus. Le journal atteint
|
||||
2 733 507 520 octets avant ce signalement puis continue à évoluer lors de l'activité suivante.
|
||||
Après confirmation de fermeture, la capture se termine à 19:52:07,295 UTC. Le journal final
|
||||
de **2 798 712 064 octets contient 659 844 transactions valides**, 165 568 pages superposées
|
||||
et une génération de système de fichiers de 821 657. SYSCONF est valide ; les neuf contenus
|
||||
du menu v514 et quinze contenus d'IOS80 v6944 correspondent aux empreintes TMD. Les autres
|
||||
titres ne sont pas contrôlés par ce vérificateur. Empreinte SHA256 finale :
|
||||
`5002e06eb368fbc9b55c4e7d1c377391c734a2ccdad8a7ea7df3b3dac352fb16`.
|
||||
|
||||
Le préfixe enregistré juste avant le signalement de fin est également vérifié en lecture
|
||||
seule, **sans tronquer ni restaurer le journal** : 2 733 507 520 octets, 644 569 transactions,
|
||||
165 440 pages et génération 821 569. SYSCONF et les mêmes 24 contenus sont valides. Son SHA256
|
||||
est `bae4e8b6f7060139f21a1968c67a5d38199ee96109e873b3467ae59d15f37d17`.
|
||||
La différence avec le journal final provient des écritures ultérieures, exclues du bilan de
|
||||
mise à jour. L'ensemble reste conservé dans le profil E ; aucune NAND ni build antérieure
|
||||
n'est remplacée. Aucun commit/push supplémentaire n'est effectué.
|
||||
|
||||
## 14. Candidat F : profilage mémoire détaillé à la demande
|
||||
|
||||
Après E, la durée de mise à jour ne baisse pas de façon convaincante. Le candidat F isole
|
||||
donc le coût du diagnostic, sans modifier les transactions invitées. Les chemins mémoire
|
||||
lents incrémentaient systématiquement plusieurs compteurs de région/page, et alimentaient
|
||||
une table de hachage d'adresses tous les 256 accès, même en usage normal.
|
||||
|
||||
`WiiStarletJITMemoryProfiling` est désormais un réglage hôte optionnel, désactivé par défaut.
|
||||
Le choix est fixé à la construction du cœur et le JIT sélectionne un helper spécialisé lors
|
||||
de la génération de code : aucun test dynamique du réglage n'est ajouté par accès. Le mode
|
||||
normal conserve les **totaux** de lectures/écritures lentes et les compteurs d'instructions ;
|
||||
il évite la classification, les compteurs détaillés et la table d'adresses. Le mode diagnostic
|
||||
les conserve. Les logs identifient explicitement `memory-profile=false` pour que des compteurs
|
||||
détaillés indisponibles ne soient pas interprétés comme une absence d'accès.
|
||||
|
||||
Le script de lancement expose `-ProfileJitMemory`. Le réglage n'est pas sérialisé dans l'état
|
||||
invité, demeure identique après reset et rechargement des caches, et ne modifie ni endian,
|
||||
protection boot0, mapping SRAM, interruptions, horloges ni persistance NAND. Les lectures et
|
||||
écritures continuent à appeler les mêmes méthodes du bus. Les classements de pages/adresses
|
||||
ne sont calculés dans les logs que lorsque le profilage détaillé est activé.
|
||||
|
||||
### Comparaison dans un même exécutable
|
||||
|
||||
Le banc reprend le vrai bus `StarletMemory`, sans firmware ni profil utilisateur. Un **même
|
||||
exécutable figé** est lancé avec le profilage activé/désactivé, en ordre ON/OFF/OFF/ON/ON/OFF,
|
||||
puis dans l'ordre inverse. Chaque série comporte neuf échantillons par mode/cas, trois tours
|
||||
par processus, affinité identique et compilation initiale exclue. Chaque mesure exécute
|
||||
64 millions d'instructions et 32 millions d'accès ; les valeurs finales et totaux d'accès
|
||||
sont vérifiés. Aucun build ni test unitaire ne tourne pendant ces deux séries.
|
||||
|
||||
| Cas, médianes murales | ON, ordre 1 | OFF, ordre 1 | ON, inverse | OFF, inverse |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Lecture SRAM déjà directe, page 01 | 58,85 ms | 59,01 ms | 59,92 ms | 59,15 ms |
|
||||
| Lecture SRAM forcée sur le bus | 256,89 ms | 234,06 ms | 262,07 ms | 234,36 ms |
|
||||
| Lecture SRAM déjà directe, page 14 | 64,12 ms | 63,94 ms | 64,90 ms | 64,15 ms |
|
||||
| Écriture SRAM sur le bus | 202,66 ms | 167,61 ms | 205,49 ms | 168,06 ms |
|
||||
| Lecture VERSION et mot suivant | 964,43 ms | 919,85 ms | 968,57 ms | 928,59 ms |
|
||||
|
||||
Le coût diminue d'environ **17–18 % pour les écritures SRAM lentes**, **9–11 % pour les
|
||||
lectures SRAM forcées sur le bus** et **4–5 % sur le cas MMIO**. Les témoins déjà directs
|
||||
restent proches. Les temps CPU sont enregistrés, avec la limite habituelle de granularité.
|
||||
La variabilité hôte demeure ; ces chiffres comparent les deux modes du candidat F dans le
|
||||
même binaire, pas des temps historiques de builds différents. **Ils ne démontrent pas encore
|
||||
un gain de durée sur une mise à jour native.**
|
||||
|
||||
### Validation
|
||||
|
||||
Les **195 tests de 27 suites passent**, couvrant Starlet, IPC, lecteurs TMD, systèmes de
|
||||
fichiers et CoreTiming. Les quatre tests de lectures/écritures SRAM réelles sont désormais
|
||||
paramétrés pour tourner avec et sans profilage : ARM/Thumb, deux endiannesses, mappings
|
||||
vivants, frontières et invalidations TLB restent comparés à l'interpréteur. Un nouveau test
|
||||
vérifie dans les deux modes les résultats SRAM/MMIO, les totaux exacts de 256 lectures et
|
||||
256 écritures par phase, l'échantillonnage détaillé et son absence lorsqu'il est désactivé,
|
||||
puis recommence après reset. Les 20 tests ciblés SRAM/DMA/IPC passent aussi trois répétitions
|
||||
dans un ordre mélangé. Deux benchmarks historiques restent désactivés.
|
||||
|
||||
Build compilée SHA256 :
|
||||
`bdfb27dbf23065c7894640323f615220140f6cda2fe250814cb59cc22f858a9d`.
|
||||
Exécutable du banc ON/OFF SHA256 :
|
||||
`0e20fda45f75464cba4414c4884ddca13a77c0adc221a91622f7a45aa18692a3`.
|
||||
La validation native reste à faire. Au moment de la préparation, environ 4,2 Go restent sur
|
||||
C:, insuffisants pour un nouveau profil depuis la graine, la croissance prévue du journal
|
||||
et la réserve de sécurité du protocole. Aucun essai natif F n'est lancé et aucune ancienne
|
||||
NAND n'est supprimée pour contourner ce manque d'espace.
|
||||
|
||||
L'espace est ensuite libéré indépendamment du test : environ 81 Gio sont disponibles avant
|
||||
préparation. À la demande de l'utilisateur, le profil F est créé depuis les mêmes fichiers
|
||||
initiaux vérifiés que B ; les chemins de sortie sont les seules modifications du profil.
|
||||
Le journal initial de 533 705 344 octets, ses 125 881 transactions, SYSCONF et les 24 contenus
|
||||
menu/IOS80 sont vérifiés. La build F et ses symboles sont figés avec les empreintes testées.
|
||||
L'instance F démarre le 19 septembre à **21:17:04 UTC**, PID 32008, capture PID 20080, avec
|
||||
le profilage mémoire détaillé désactivé. Aucun résultat de performance natif F n'est encore
|
||||
conclu ; les signalements de départ et fin de mise à jour sont attendus dans `F/Capture`.
|
||||
|
||||
À **21:21:25 UTC**, le collecteur est remplacé (PID 33736) sans arrêter Dolphin :
|
||||
le lecteur initial échoue sur l'annotation facultative d'une adresse interne du JIT.
|
||||
Le lecteur local `LiveStateF` signale explicitement `JIT_METADATA_UNAVAILABLE` et poursuit
|
||||
les lectures obligatoires FPS/CPU, E/S, NAT et registres de contrôle AES. La cause exacte
|
||||
de l'adresse invalide dans les symboles incrémentaux n'est pas établie. Ce changement
|
||||
concerne uniquement l'observateur, pas l'émulation ni les compteurs du candidat.
|
||||
Les nouvelles captures terminent avec `SNAPSHOT_EXIT=0` ; les anciennes captures en échec
|
||||
sont conservées, mais doivent être exclues de la comparaison native. Les mesures valides
|
||||
sont dans `metrics-20260919-232125-32008.txt`, avec le coût de chaque capture enregistré.
|
||||
`reader-fix.json` et `launch-before-reader-fix.json` conservent la trace du remplacement.
|
||||
En attente du lancement de la mise à jour, les lectures montrent environ 50 FPS à vitesse
|
||||
100 % ; cela ne mesure pas encore le gain pendant une mise à jour.
|
||||
|
||||
### Résultat natif F : succès fonctionnel, gain non validé
|
||||
|
||||
L'utilisateur signale le départ, relevé à **21:26:45 UTC**, puis la réussite, relevée à
|
||||
**21:35:53 UTC**. Ces repères ne sont pas les instants exacts de clic ou d'affichage ;
|
||||
l'activité avait déjà commencé avant le signalement du départ. La comparaison conserve
|
||||
les mêmes seuils échantillonnés du journal (600 à 2 700 Mo), sans inclure l'activité après
|
||||
le signalement de fin.
|
||||
|
||||
| Mesure sur ces seuils | D répété | E | F |
|
||||
|---|---:|---:|---:|
|
||||
| Temps mural | 348,91 s | 350,34 s | 442,04 s |
|
||||
| Temps CPU du processus | 354,88 s | 353,86 s | 522,23 s |
|
||||
| FPS médian des échantillons | 47,32 | 48,70 | 40,985 |
|
||||
| FPS minimal échantillonné | 18,40 | 20,13 | 11,08 |
|
||||
| Vitesse médiane | 93,05 % | 95,25 % | 80,3 % |
|
||||
| Échantillons sous 90 % | 14/34 | 16/34 | 25/42 |
|
||||
| Coût médian d'une capture | 555,37 ms | 597,96 ms | 749,90 ms |
|
||||
| Erreurs du polling NAT supplémentaires | 0 | 0 | 0 |
|
||||
|
||||
F prend **26,2 % de temps mural et 47,6 % de temps CPU supplémentaires par rapport à E**.
|
||||
Les quatre sous-plages de progression sont également plus lentes. Ce résultat ne valide
|
||||
donc pas l'optimisation sur la mise à jour réelle, malgré le gain des microbenchmarks.
|
||||
Il ne suffit pas à attribuer la régression à la désactivation des compteurs : les essais
|
||||
ne sont pas alternés dans les mêmes conditions hôte/serveur, et les seuils ne correspondent
|
||||
pas exactement aux mêmes transactions. Le coût du lecteur est aussi plus élevé ; les
|
||||
compteurs CPU du processus Dolphin n'incluent pas le CPU du lecteur externe.
|
||||
L'absence d'erreur de polling NAT n'exclut pas une attente du serveur.
|
||||
|
||||
La prochaine expérience utile serait une comparaison native ON/OFF alternée du **même
|
||||
binaire F**, depuis la même graine, avec un lecteur fiable commun et des conditions hôte
|
||||
relevées. Elle n'est pas lancée automatiquement à la clôture de F. Aucun correctif
|
||||
supplémentaire de l'émulation n'est appliqué pendant cet essai.
|
||||
|
||||
Après fermeture demandée à l'utilisateur, le collecteur se termine à **21:37:36 UTC**.
|
||||
La dernière capture échoue pendant la disparition du processus et est exclue des calculs.
|
||||
Le journal final est conservé : **2 783 208 736 octets**, **656 226 transactions valides**,
|
||||
165 440 pages superposées, génération 821638, SYSCONF valide. Les 24 contenus du menu v514
|
||||
et d'IOS80 v6944 correspondent à leurs empreintes TMD ; les autres titres ne sont pas
|
||||
vérifiés par ce contrôle. Empreinte SHA256 du journal :
|
||||
`6d0341773c613aab63194153f44541b12d75541d0cab53b94b2c014544e9aab0`.
|
||||
Les anciennes NAND et le profil habituel sont inchangés.
|
||||
|
||||
## Reproduction / fichiers de travail
|
||||
|
||||
- `.starlet-profile/StudyUpdateJournal.py` : analyse du journal réel en lecture seule.
|
||||
- `.starlet-profile/update-journal-study-20260913.json` : totaux et vérification des transactions.
|
||||
- `.starlet-profile/UpdateStudyBench.cpp` et `BuildUpdateStudy.ps1` : banc d'essai séparé.
|
||||
- `.starlet-profile/ARMJitStudy.cpp` : copie expérimentale du JIT, pas le fichier de production.
|
||||
- `.starlet-profile/update-study-bench-20260913.txt` : première comparaison des journaux et des accès.
|
||||
- `.starlet-profile/update-study-arm-ab-summary-20260913.json` : échantillons A/B d'écriture.
|
||||
- `.starlet-profile/update-study-tests-with-nat-20260913.txt` : résultat des 176 tests du prototype ARM.
|
||||
- `.starlet-profile/CompareSRAMReads.ps1` et `sram-read-ab-samples.json` : comparaison des lectures.
|
||||
- `.starlet-profile/sram-read-all-tests.txt` : les 179 tests après extension des lectures.
|
||||
- `.starlet-profile/PrepareUpdateAB.ps1`, `RunUpdateAB.ps1`, `CaptureWiiUpdate.ps1` : essais isolés.
|
||||
- `.starlet_update_ab_20260913/manifest.json` : provenance, empreintes des deux builds et des NAND.
|
||||
- `.starlet_update_ab_20260913/A/Capture` et `B/Capture` : mesures de chaque exécution.
|
||||
- `.starlet-profile/SummarizeUpdateCapture.ps1` : synthèse des fenêtres horodatées.
|
||||
- `.starlet_update_ab_20260913/journal-progress-comparison.json` : repères automatiques de progression.
|
||||
- `.starlet-profile/CompareBusWrites20260919.ps1` : comparaisons alternées du nouveau bus.
|
||||
- `.starlet-profile/bus-write-ab-samples-20260919.json` et `bus-write-ab-samples-20260919-repeat.json` : mesures brutes.
|
||||
- `.starlet-profile/tests-bus-writes-final-20260919.log` : validation finale des 182 tests.
|
||||
- `.starlet-profile/PrepareUpdateC20260919.ps1`, `.starlet_update_ab_20260913/manifest-C-20260919.json`
|
||||
et `C/Capture` : provenance et enregistrement du troisième essai natif.
|
||||
- `.starlet-profile/DMAStudy.cpp`, `BuildDMAStudy.ps1`, `DMABefore20260919.exe`,
|
||||
`DMAAfter20260919.exe` : banc de copie MMIO et exécutables comparés.
|
||||
- `.starlet-profile/dma-ab-samples-20260919.json`, `dma-ab-summary-20260919.json` : 108 mesures DMA.
|
||||
- `.starlet-profile/tests-dma-final-20260919.log`, `tests-dma-repeat-20260919.log` : tests du candidat D.
|
||||
- `.starlet-profile/PrepareUpdateD20260919.ps1`, `.starlet_update_ab_20260913/manifest-D-20260919.json`
|
||||
et `D/Capture` : préparation et mesures du quatrième essai natif.
|
||||
- `.starlet-profile/PrepareUpdateDRepeat20260919.ps1`, `AnalyzeUpdateDRepeat20260919.ps1` et
|
||||
`.starlet_update_ab_20260913/D-repeat-20260919/Capture` : répétition depuis la graine, repères
|
||||
d'activité, comparaison des seuils, vérification NAND et compte rendu final `completion.json`.
|
||||
|
||||
- `.starlet-profile/ReadStudyE.cpp`, `BuildReadStudyE.ps1`, `CompareReadStudyESized.ps1` :
|
||||
banc réel de lectures/écritures scalaires et comparaison alternée de D/E.
|
||||
- `.starlet-profile/read-e-sized-ab-samples-20260919.json` et
|
||||
`read-e-sized-ab-samples-20260919-reverse.json` : mesures finales dans les deux ordres.
|
||||
- `.starlet-profile/tests-E-sized-final-20260919.log`, `tests-E-sized-repeat-20260919.log` :
|
||||
tests complets et répétitions ciblées du candidat E.
|
||||
- `.starlet-profile/PrepareUpdateE20260919.ps1`, `.starlet_update_ab_20260913/manifest-E-20260919.json`
|
||||
et `E/Capture` : provenance, profil et captures du cinquième essai natif.
|
||||
- `.starlet-profile/AnalyzeUpdateE20260919.ps1`, `E/Capture/completion-report.json`,
|
||||
`journal-progress-comparison.json`, `delta-vs-D.json` : repère de réussite et bilan natif E.
|
||||
- `E/Capture/final-journal-verification.txt`, `update-completion-prefix-verification.txt`
|
||||
et `completion.json` : contrôles après fermeture, empreintes et compte rendu final E.
|
||||
- `.starlet-profile/ReadStudyF.cpp`, `ReadStudyF20260919.exe`, `CompareMemoryProfileF.ps1` :
|
||||
même banc réel avec choix ON/OFF au démarrage, sans changer de binaire.
|
||||
- `.starlet-profile/memory-profile-F-20260919.json`, `memory-profile-F-20260919-reverse.json` :
|
||||
180 mesures brutes du candidat F.
|
||||
- `.starlet-profile/tests-F-expanded-20260919.log`, `tests-F-repeat-20260919.log` et
|
||||
`build-F-memory-profile-20260919.log` : tests, répétitions et compilation.
|
||||
- `.starlet-profile/PrepareUpdateF20260919.ps1`, `.starlet_update_ab_20260913/manifest-F-20260919.json`
|
||||
et `F/Capture/launch.json` : provenance et lancement du sixième essai natif isolé.
|
||||
- `.starlet-profile/AnalyzeUpdateF20260919.ps1`, `F/Capture/report-window-summary.json`,
|
||||
`journal-progress-comparison.json`, `journal-progress-phases.json`, `deltas-vs-D-E.json` :
|
||||
comparaison native, limitée aux captures complètes du lecteur corrigé et à la fenêtre du test.
|
||||
- `F/Capture/completion-report.json`, `final-journal-verification.txt`,
|
||||
`update-completion-prefix-verification.txt`, `completion.json` : fin signalée et contrôles NAND.
|
||||
|
||||
Les prototypes et données de mesure restent dans les répertoires locaux ignorés.
|
||||
Ce rapport est la seule nouvelle documentation destinée au dépôt ; les modifications
|
||||
préexistantes du correctif d'appairage Wiimote sont conservées.
|
||||
Reference in new issue
Block a user