diff --git a/defects/dolphin-0001/patch/dolphin-0001.patch b/defects/dolphin-0001/patch/dolphin-0001.patch new file mode 100644 index 000000000..46fb20a1e --- /dev/null +++ b/defects/dolphin-0001/patch/dolphin-0001.patch @@ -0,0 +1,98 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/Source/Core/Core/PowerPC/BreakPoints.h ++++ b/Source/Core/Core/PowerPC/BreakPoints.h +@@ -7,6 +7,7 @@ + #include + #include + #include ++#include + #include + + #include "Common/BitSet.h" +@@ -60,11 +61,18 @@ public: + bool Remove(u32 address); + void Clear(); + void ClearTemporary(); ++ ++ // Rebuild the O(1) address index. Called after any mutation. ++ void RebuildIndex(); + + private: + TBreakPoints m_breakpoints; + std::optional m_temp_breakpoint; + Core::System& m_system; + bool m_breaking_enabled = true; ++ ++ // Address -> index into m_breakpoints for O(1) lookup. ++ // Invalidated and rebuilt on every Add/Remove/Clear. ++ std::unordered_map m_bp_index; + }; +--- a/Source/Core/Core/PowerPC/BreakPoints.cpp ++++ b/Source/Core/Core/PowerPC/BreakPoints.cpp +@@ -52,10 +52,17 @@ const TBreakPoint* BreakPoints::GetBreakpoint(u32 address) const + return GetRegularBreakpoint(address); + } + ++void BreakPoints::RebuildIndex() ++{ ++ m_bp_index.clear(); ++ m_bp_index.reserve(m_breakpoints.size()); ++ for (std::size_t i = 0; i < m_breakpoints.size(); ++i) ++ m_bp_index[m_breakpoints[i].address] = i; ++} ++ + const TBreakPoint* BreakPoints::GetRegularBreakpoint(u32 address) const + { +- auto bp = std::ranges::find(m_breakpoints, address, &TBreakPoint::address); +- +- if (bp == m_breakpoints.end()) ++ // O(1) hash lookup instead of O(N) linear scan over m_breakpoints. ++ auto it = m_bp_index.find(address); ++ if (it == m_bp_index.end()) + return nullptr; + +- return &*bp; ++ return &m_breakpoints[it->second]; + } + +@@ -115,7 +122,9 @@ void BreakPoints::Add(TBreakPoint bp) + if (IsAddressBreakPoint(bp.address)) + return; + + m_system.GetJitInterface().InvalidateICache(bp.address, 4, true); +- + m_breakpoints.emplace_back(std::move(bp)); ++ RebuildIndex(); + } + +@@ -133,6 +142,7 @@ void BreakPoints::Add(u32 address, bool break_on_hit, bool log_on_hit, + { + bp.is_enabled = iter->is_enabled; + *iter = std::move(bp); ++ RebuildIndex(); + } + else + { + m_breakpoints.emplace_back(std::move(bp)); ++ RebuildIndex(); + } + +@@ -181,7 +192,9 @@ bool BreakPoints::Remove(u32 address) + if (iter == m_breakpoints.cend()) + return false; + + m_breakpoints.erase(iter); ++ RebuildIndex(); + m_system.GetJitInterface().InvalidateICache(address, 4, true); + + return true; + } + +@@ -192,6 +205,7 @@ void BreakPoints::Clear() + m_system.GetJitInterface().InvalidateICache(bp.address, 4, true); + } + + m_breakpoints.clear(); ++ m_bp_index.clear(); + ClearTemporary(); + } diff --git a/defects/dolphin-0001/test/test_dolphin_breakpoints.cpp b/defects/dolphin-0001/test/test_dolphin_breakpoints.cpp new file mode 100644 index 000000000..17555d021 --- /dev/null +++ b/defects/dolphin-0001/test/test_dolphin_breakpoints.cpp @@ -0,0 +1,178 @@ +// Unit test for dolphin-0001: BreakPoints::GetRegularBreakpoint O(N) linear +// scan over std::vector -> O(1) unordered_map lookup. +// +// During a debug session, every CPU instruction executed in the interpreter +// (and every JIT block compiled) calls IsAddressBreakPoint -> GetBreakpoint -> +// GetRegularBreakpoint. With N breakpoints, that is O(N) per instruction. +// At 30 MHz emulated clock with N=50 breakpoints, this multiplies CPU time +// by 50x in the hot path. +// +// Fix: maintain an unordered_map index alongside m_breakpoints. +// Rebuild on every Add/Remove/Clear (rare). Lookup is O(1) amortized. + +#include +#include +#include +#include +#include +#include +#include + +// Minimal TBreakPoint mirroring Dolphin's struct. +struct TBreakPoint +{ + unsigned int address = 0; + bool is_enabled = false; + bool break_on_hit = false; + bool log_on_hit = false; +}; + +// Original implementation: O(N) linear scan. +struct BreakPointsOriginal +{ + std::vector m_breakpoints; + + void Add(unsigned int address) + { + TBreakPoint bp; + bp.address = address; + bp.is_enabled = true; + bp.break_on_hit = true; + m_breakpoints.push_back(bp); + } + + const TBreakPoint* GetRegularBreakpoint(unsigned int address) const + { + for (const auto& bp : m_breakpoints) + { + if (bp.address == address) + return &bp; + } + return nullptr; + } +}; + +// Patched implementation: O(1) hash index. +struct BreakPointsPatched +{ + std::vector m_breakpoints; + std::unordered_map m_bp_index; + + void RebuildIndex() + { + m_bp_index.clear(); + m_bp_index.reserve(m_breakpoints.size()); + for (std::size_t i = 0; i < m_breakpoints.size(); ++i) + m_bp_index[m_breakpoints[i].address] = i; + } + + void Add(unsigned int address) + { + TBreakPoint bp; + bp.address = address; + bp.is_enabled = true; + bp.break_on_hit = true; + m_breakpoints.push_back(bp); + RebuildIndex(); + } + + const TBreakPoint* GetRegularBreakpoint(unsigned int address) const + { + auto it = m_bp_index.find(address); + if (it == m_bp_index.end()) + return nullptr; + return &m_breakpoints[it->second]; + } +}; + +int main() +{ + const int N_BREAKPOINTS = 64; + const int N_LOOKUPS = 2000000; + + // --- Correctness --- + { + BreakPointsOriginal orig; + BreakPointsPatched patched; + + for (int i = 0; i < N_BREAKPOINTS; ++i) + { + unsigned int addr = 0x80000000u + i * 4; + orig.Add(addr); + patched.Add(addr); + } + + // Every address present in both implementations. + for (int i = 0; i < N_BREAKPOINTS; ++i) + { + unsigned int addr = 0x80000000u + i * 4; + const TBreakPoint* o = orig.GetRegularBreakpoint(addr); + const TBreakPoint* p = patched.GetRegularBreakpoint(addr); + assert(o != nullptr && "original: should find breakpoint"); + assert(p != nullptr && "patched: should find breakpoint"); + assert(o->address == addr); + assert(p->address == addr); + } + + // Address not in list returns nullptr in both. + assert(orig.GetRegularBreakpoint(0xDEADBEEFu) == nullptr); + assert(patched.GetRegularBreakpoint(0xDEADBEEFu) == nullptr); + + printf("Correctness: PASS\n"); + } + + // --- Performance --- + { + BreakPointsOriginal orig; + BreakPointsPatched patched; + + for (int i = 0; i < N_BREAKPOINTS; ++i) + { + unsigned int addr = 0x80000000u + i * 4; + orig.Add(addr); + patched.Add(addr); + } + + // Simulate per-instruction lookup: address not in breakpoints (common case). + unsigned int probe_miss = 0x90000000u; + // Also probe a hit address. + unsigned int probe_hit = 0x80000000u + (N_BREAKPOINTS / 2) * 4; + + auto t0 = std::chrono::steady_clock::now(); + volatile int sink_orig = 0; + for (int i = 0; i < N_LOOKUPS; ++i) + { + unsigned int addr = (i & 1) ? probe_hit : probe_miss; + const TBreakPoint* bp = orig.GetRegularBreakpoint(addr); + sink_orig += (bp != nullptr) ? 1 : 0; + } + auto t1 = std::chrono::steady_clock::now(); + volatile int sink_patched = 0; + for (int i = 0; i < N_LOOKUPS; ++i) + { + unsigned int addr = (i & 1) ? probe_hit : probe_miss; + const TBreakPoint* bp = patched.GetRegularBreakpoint(addr); + sink_patched += (bp != nullptr) ? 1 : 0; + } + auto t2 = std::chrono::steady_clock::now(); + + double ms_orig = + std::chrono::duration(t1 - t0).count(); + double ms_patched = + std::chrono::duration(t2 - t1).count(); + double ratio = ms_orig / ms_patched; + + assert(sink_orig == sink_patched && "hit counts must match"); + + printf("Original (O(N) vector scan): %.2f ms for %d lookups (N=%d)\n", + ms_orig, N_LOOKUPS, N_BREAKPOINTS); + printf("Patched (O(1) hash map): %.2f ms for %d lookups\n", + ms_patched, N_LOOKUPS); + printf("Speedup: %.1fx\n", ratio); + assert(ratio >= 2.0 && "patched must be at least 2x faster"); + printf("Performance: PASS\n"); + } + + printf("ALL TESTS PASSED\n"); + return 0; +}