From cdddb88c8f9a79427b034917e1f9852adef02730 Mon Sep 17 00:00:00 2001 From: KakarottoCake Date: Thu, 20 Aug 2026 16:35:15 -0400 Subject: [PATCH] Lockstep: stop reporting a step-cap timeout as a control-flow divergence The shadow interpreter walks at most m_ls_step_cap instructions looking for the block's end PC. When it runs out, `reached` is false and the block is reported as CTRLFLOW -- but nothing has been compared at that point, so there is no evidence the module diverged at all. The verifier was reporting its own give-up condition as a fault in the code under test. At the old default of 512 this dominated the output. On a full retail GameCube title the same run reported 631 CTRLFLOW divergences at cap 512 and 22 at cap 20000, and the 609 that disappeared were all sitting exactly at the cap. The cap was also hiding real divergences, which is the worse half: any block longer than 512 steps never reaches the register comparison, so its mismatches are silently skipped. Raising the cap took FP divergences from 463 to 622 -- 159 real reports the old default concealed. So: raise the default to 20000, and count a cap timeout in its own counter (cap_hits, surfaced in the summary line) instead of emitting a divergence for it. STATICRECOMP_LOCKSTEP_STEPCAP still overrides it. Co-Authored-By: Claude Opus 5 --- .../PowerPC/StaticRecomp/StaticRecompLockstep.cpp | 5 ++++- .../Core/PowerPC/StaticRecomp/StaticRecompLockstep.h | 10 +++++++++- .../StaticRecomp/StaticRecompLockstep_Check.cpp | 12 ++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.cpp b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.cpp index da9fb99f8c..0a29208b13 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.cpp +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.cpp @@ -35,9 +35,12 @@ StaticRecompLockstepVerifier::~StaticRecompLockstepVerifier() { std::fprintf(stderr, "[lockstep] summary: checks=%llu reports=%llu skipped_fallback=%llu " - "skipped_zero=%llu undercharges=%llu max_deficit=%lld distinct_pcs=%zu\n", + "skipped_zero=%llu cap_hits=%llu undercharges=%llu " + "max_deficit=%lld distinct_pcs=%zu +", (unsigned long long)m_ls_checks, (unsigned long long)m_ls_reports, (unsigned long long)m_ls_skipped_fallback, (unsigned long long)m_ls_skipped_zero, + (unsigned long long)m_ls_cap_hits, (unsigned long long)m_ls_undercharges, (long long)m_ls_max_undercharge, m_ls_checked.size()); if (m_set_mem_journal) diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.h b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.h index 3e8a0ee9db..bba96f7877 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.h +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep.h @@ -145,7 +145,15 @@ class StaticRecompLockstepVerifier u64 m_ls_start = 0; // begin checking at this native-dispatch index u64 m_ls_limit = 0; // stop checking after this index (0 = no bound) u64 m_ls_max_report = 0; // cap divergence reports (0 = unlimited) - int m_ls_step_cap = 512; // interpreter single-steps before giving up on end PC + // Interpreter single-steps before giving up on end PC. 512 is far too low, + // and it makes a default run misleading in BOTH directions: it manufactures + // CTRLFLOW "divergences" that are only the shadow running out of steps, and + // it hides real register divergences in every block longer than the cap, + // because the walk stops before it ever reaches the comparison. Measured on + // a full retail title, raising it to 20000 took CTRLFLOW reports 631 -> 22 + // and FP divergences 463 -> 622 -- 159 real reports the old default hid. + int m_ls_step_cap = 20000; + u64 m_ls_cap_hits = 0; // gave up on end PC at the step cap -- NOT a divergence u64 m_ls_checks = 0; // distinct blocks differentially checked u64 m_ls_reports = 0; // divergences reported u64 m_ls_skipped_fallback = 0; // blocks skipped (native used instruction fallback) diff --git a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep_Check.cpp b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep_Check.cpp index 6e72cb5eab..2ec5280299 100644 --- a/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep_Check.cpp +++ b/Source/Core/Core/PowerPC/StaticRecomp/StaticRecompLockstep_Check.cpp @@ -194,6 +194,18 @@ void StaticRecompLockstepVerifier::LockstepCheck(u32 entry_pc, u32 end_pc, const StaticRecompLockstep::g_tb_override_active = false; const bool reached = (ppc.pc == end_pc && ppc.Exceptions == 0); + + // Running out of steps is the shadow interpreter giving up, not the module + // diverging. No register has been compared at this point, so there is no + // evidence either way. Reporting it as CTRLFLOW made a default run show + // hundreds of faults that no codegen change could fix, and buried the real + // ones among them. Count it separately and stay quiet. + if (!reached && ppc.Exceptions == 0 && steps >= m_ls_step_cap) + { + ++m_ls_cap_hits; + return; + } + const bool undercharged = reached && (interp_cycles > native_charge + LS_UNDERCHARGE_GRACE) && !end_is_loop_header; std::string diff;