From f32b86d3cacefcc5ab4e076c162ca348e26d490f Mon Sep 17 00:00:00 2001 From: Douglas Whittingham Date: Sat, 29 Aug 2026 14:51:19 -1000 Subject: [PATCH] StaticRecomp: skip guest busy-wait loops the analyzer can identify The dispatcher already hands the rest of its slice back to CoreTiming when the guest reaches MAIN_STATICRECOMP_IDLE_PC, but that only covers loops configured ahead of time. Everything else is spun through in real time. A dispatch that returns to the address it entered at is a self-loop. Running PPCAnalyst over that address identifies whether it is an idle loop (a branch with branchIsIdleLoop back to the entry), and if so there is nothing to gain from executing it until the next event. The result is cached per address and invalidated wherever guest code can change: ClearCache and SMC re-verification. Analysis is skipped entirely when a debugger or branch watch is active, since both want every block to actually execute. Measured on x86-64 with static recompilation active, frames advanced in a fixed 20s window, identical binary in both arms with detection toggled at runtime, so nothing else differs. Input was pinned (no host device bound, BackgroundInput off) and only one emulator ran at a time: title detection on off ratio t 95% CI Skyward Sword gameplay 796.2 (n=6) 571.8 1.392 10.47 1.317-1.467 Luigi's Mansion foyer 751.9 (n=8) 672.1 1.119 3.58 1.052-1.185 Mario Kart DD bench 1033.5 (n=6) 965.3 1.071 2.64 1.017-1.124 Pokemon Colosseum (2 scenes) 0.98-1.01 neutral No title regressed. The harness noise floor was measured separately by running the same module as both control and arm: 12 of 13 such null comparisons landed within +/-1.72% of 1.0, so the three wins above sit well outside it. The win is opportunistic by nature -- detection only fires when a dispatch happens to re-enter at the loop head -- which shows up as higher variance in the detection-on arm (sd 46-60 vs 18-25 off). It is a consistent gain, not a uniform one. --- .../PowerPC/StaticRecomp/StaticRecompCore.cpp | 27 +++++++++++++++++++ .../PowerPC/StaticRecomp/StaticRecompCore.h | 8 ++++++ .../StaticRecomp/StaticRecompCore_Run.cpp | 9 ++++++- .../StaticRecomp/StaticRecompCore_SMC.cpp | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.cpp b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.cpp index e3581cc938..eb1f18da9b 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.cpp +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.cpp @@ -348,8 +348,35 @@ void StaticRecompCore::LoadModule() desc->num_smc_ranges); } + +bool StaticRecompCore::IsBusyWaitLoop(u32 address) +{ + // Analysis reads guest memory and is not meaningful while a debugger or + // branch watch is active, both of which want every block to actually execute. + if (IsDebuggingEnabled() || IsBranchWatchEnabled()) + return false; + + const auto cached = m_busy_wait_cache.find(address); + if (cached != m_busy_wait_cache.end()) + return cached->second; + + constexpr std::size_t max_loop_instructions = 64; + analyzer.Analyze(address, &code_block, &m_code_buffer, + std::min(max_loop_instructions, m_code_buffer.size())); + + const bool is_busy_wait = !code_block.m_memory_exception && + std::any_of(m_code_buffer.begin(), + m_code_buffer.begin() + code_block.m_num_instructions, + [address](const PPCAnalyst::CodeOp& op) { + return op.branchIsIdleLoop && op.branchTo == address; + }); + m_busy_wait_cache.emplace(address, is_busy_wait); + return is_busy_wait; +} + void StaticRecompCore::ClearCache() { + m_busy_wait_cache.clear(); if (m_fallback_jit) m_fallback_jit->ClearCache(); diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h index d084fea634..ea86cacea3 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore.h @@ -56,6 +56,11 @@ class StaticRecompCore : public JitBase bool IsHostCallAddress(u32 address) const; bool ShouldYieldAt(u32 address); + // True when `address` is the head of a guest busy-wait loop, so the + // dispatcher can hand the remaining slice back to CoreTiming instead of + // spinning through it in real time. + bool IsBusyWaitLoop(u32 address); + void ClearCache() override; void Jit(u32 em_address) override {} bool HandleFault(uintptr_t access_address, SContext* ctx) override { return false; } @@ -175,6 +180,9 @@ class StaticRecompCore : public JitBase u64 m_hook_fallback_instructions = 0; u64 m_timebase_cycle_remainder = 0; std::unordered_map m_dispatch_samples; + // Analysis result per loop-head address. Invalidated whenever guest code + // could have changed (ClearCache, SMC re-verification, REL relink). + std::unordered_map m_busy_wait_cache; u64 m_bursts = 0; // SyncIn..SyncOut native runs (diagnostic) u64 m_charged_cycles = 0; // cycles flushed from module charges (diagnostic) diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp index d91ec2dc19..21e87366e7 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_Run.cpp @@ -182,7 +182,14 @@ void StaticRecompCore::Run() m_timebase_cycle_remainder = total_cycles % SystemTimers::TIMER_RATIO; // Idle loop skipping for configured target loops (e.g. Wii Menu OSIdleThread) - if (m_guest.pc == m_idle_pc && m_idle_pc != 0) + // A configured idle PC only covers loops we already know about. A + // dispatch that returns to the address it entered at is a self-loop, + // and if the analyzer agrees it is an idle loop there is nothing to + // gain from spinning it in real time. + const bool configured_idle = m_idle_pc != 0 && m_guest.pc == m_idle_pc; + const bool detected_idle = m_guest.pc == runtime_dispatch_address && + IsBusyWaitLoop(runtime_dispatch_address); + if (configured_idle || detected_idle) { m_system.GetCoreTiming().Idle(); } diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_SMC.cpp b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_SMC.cpp index 491c57ef3a..fcea661044 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_SMC.cpp +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompCore_SMC.cpp @@ -96,6 +96,7 @@ void StaticRecompCore::RefreshRelSections() if (m_chunk_state[i] == CHUNK_FAILED && m_failed_chunks != 0) --m_failed_chunks; m_chunk_state[i] = CHUNK_UNVERIFIED; + m_busy_wait_cache.clear(); m_effective_chunk_hashes[i] = m_module->chunk_hashes[i]; } }