From 06445dd641401bff0e29967660e572e89d63bce8 Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Tue, 28 Jul 2026 21:00:49 -0700 Subject: [PATCH] eerunner: narrow --rec-fallback to a single VU macro op MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once a hunt reaches `cop2vu` it stops: that group is one dispatch bit covering the whole VU macro-mode instruction set, and there was no next axis. Finding which of them miscompiles meant hand-editing the classifier and rebuilding per hypothesis. Two additions close that gap. `--rec-fallback cop2vu:` selects individual macro ops by name, over a flat 256-entry id space covering all three dispatch tables (BC2 by rt, SPECIAL1 by funct, SPECIAL2 by its packed index). And a compile-time census, printed after --mkstate, lists the macro ops the run actually emitted — an op that never compiles cannot be the bug, so it turns a 100-way search into a bisect over the handful a given game really uses. On NASCAR Thunder 2002 the census reported 58 distinct ops and the bisect reached one of them in eleven runs, no rebuilds. --- pcsx2-eerunner/Main.cpp | 17 +++- pcsx2/arm64/iR5900-arm64.cpp | 150 ++++++++++++++++++++++++++++++- pcsx2/arm64/iR5900-arm64.h | 29 +++++- pcsx2/arm64/iR5900Misc-arm64.cpp | 3 + 4 files changed, 190 insertions(+), 9 deletions(-) diff --git a/pcsx2-eerunner/Main.cpp b/pcsx2-eerunner/Main.cpp index 071a164d8d..c87f28c6c9 100644 --- a/pcsx2-eerunner/Main.cpp +++ b/pcsx2-eerunner/Main.cpp @@ -152,6 +152,7 @@ static std::vector s_set_overrides; // --set Section/Key=Value (rep static u32 s_rec_fallback_groups = 0; // --rec-fallback : EE opcode groups forced to interp #if defined(ARCH_ARM64) static u32 s_rec_fallback_reg_masks[EERecFallback::kCop2MoveOpCount] = {~0u, ~0u, ~0u, ~0u}; // per-COP2-move-op register filters +static u64 s_rec_fallback_vu_mask[EERecFallback::kCop2VuIdCount / 64] = {~0ull, ~0ull, ~0ull, ~0ull}; // per-VU-macro-op filter #endif bool EERunner::InitializeConfig() @@ -554,7 +555,11 @@ static void PrintCommandLineHelp(const char* progname) std::fprintf(stderr, " the group that makes the symptom disappear contains the bug. Groups: fpu, cop2,\n"); std::fprintf(stderr, " mmi, multdiv, shift, arith, loadstore, move, cop0, branch (plus all / none).\n"); std::fprintf(stderr, " cop2 narrows further into cop2move (QMFC2/CFC2/QMTC2/CTC2), cop2vu (the VU\n"); - std::fprintf(stderr, " macro ops) and cop2ls (LQC2/SQC2).\n"); + std::fprintf(stderr, " macro ops) and cop2ls (LQC2/SQC2). The four move ops take a ':' filter\n"); + std::fprintf(stderr, " (ctc2:27, ctc2:16:21); cop2vu takes a ':' one (cop2vu:vmaddaw,\n"); + std::fprintf(stderr, " cop2vu:vmulax:vmaddaz) — cop2vu is ~100 emitters, so don't stop the funnel\n"); + std::fprintf(stderr, " there. --mkstate prints a COP2VU CENSUS of the macro ops actually compiled;\n"); + std::fprintf(stderr, " bisect over that list, since an op never compiled cannot be the bug.\n"); std::fprintf(stderr, " Pairs well with --mkstate --renderer sw for a visual oracle. arm64 only.\n"); std::fprintf(stderr, " --gsdump [:]: with --liverun, record a .gs dump of the GIF stream (default 1\n"); std::fprintf(stderr, " frame) starting after --gsdump-at frames (default 30, so the scene has settled).\n"); @@ -784,13 +789,15 @@ bool EERunner::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& pa std::string err; u32 mask = 0; u32 reg_masks[EERecFallback::kCop2MoveOpCount] = {}; - if (!EERecFallback::ParseGroups(list, &mask, reg_masks, &err)) + u64 vu_mask[EERecFallback::kCop2VuIdCount / 64] = {}; + if (!EERecFallback::ParseGroups(list, &mask, reg_masks, vu_mask, &err)) { Console.Error(err.c_str()); return false; } s_rec_fallback_groups = mask; std::memcpy(s_rec_fallback_reg_masks, reg_masks, sizeof(reg_masks)); + std::memcpy(s_rec_fallback_vu_mask, vu_mask, sizeof(vu_mask)); #else Console.Error("--rec-fallback is implemented for the arm64 EE recompiler only."); return false; @@ -1094,6 +1101,7 @@ void EERunner::SettingsOverride() // before any block is compiled, and stays put for the whole run. EERecFallback::g_groups = s_rec_fallback_groups; std::memcpy(EERecFallback::g_cop2RegMask, s_rec_fallback_reg_masks, sizeof(s_rec_fallback_reg_masks)); + std::memcpy(EERecFallback::g_cop2VuMask, s_rec_fallback_vu_mask, sizeof(s_rec_fallback_vu_mask)); if (s_rec_fallback_groups != 0) { Console.WriteLn(fmt::format("EE REC FALLBACK: forcing to interpreter -> {}", @@ -3027,6 +3035,11 @@ static int RunMkState() VMManager::WaitForSaveStateFlush(); Console.WriteLn(fmt::format("MKSTATE: {} after {} frames -> {}", ok ? "wrote state" : "FAILED", s_frames, s_mkstate_path)); +#if defined(ARCH_ARM64) + // Which VU macro ops this run actually compiled — the candidate list for a + // `--rec-fallback cop2vu:` bisect. + Console.WriteLn(fmt::format("COP2VU CENSUS: {}", EERecFallback::DescribeCop2VuCensus())); +#endif return ok ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/pcsx2/arm64/iR5900-arm64.cpp b/pcsx2/arm64/iR5900-arm64.cpp index c7b89e8f58..24f9b1ba70 100644 --- a/pcsx2/arm64/iR5900-arm64.cpp +++ b/pcsx2/arm64/iR5900-arm64.cpp @@ -1695,6 +1695,92 @@ namespace EERecFallback { u32 g_groups = 0; u32 g_cop2RegMask[kCop2MoveOpCount] = {~0u, ~0u, ~0u, ~0u}; + u64 g_cop2VuMask[kCop2VuIdCount / 64] = {~0ull, ~0ull, ~0ull, ~0ull}; + + // Mnemonics parallel to recCOP2_BC2t / recCOP2SPECIAL1t / recCOP2SPECIAL2t + // (iR5900Misc-arm64.cpp). nullptr marks a hole in the encoding. + static const char* const kBc2Names[4] = {"vbc2f", "vbc2t", "vbc2fl", "vbc2tl"}; + + static const char* const kSpec1Names[64] = { + "vaddx", "vaddy", "vaddz", "vaddw", "vsubx", "vsuby", "vsubz", "vsubw", + "vmaddx", "vmaddy", "vmaddz", "vmaddw", "vmsubx", "vmsuby", "vmsubz", "vmsubw", + "vmaxx", "vmaxy", "vmaxz", "vmaxw", "vminix", "vminiy", "vminiz", "vminiw", + "vmulx", "vmuly", "vmulz", "vmulw", "vmulq", "vmaxi", "vmuli", "vminii", + "vaddq", "vmaddq", "vaddi", "vmaddi", "vsubq", "vmsubq", "vsubi", "vmsubi", + "vadd", "vmadd", "vmul", "vmax", "vsub", "vmsub", "vopmsub", "vmini", + "viadd", "visub", "viaddi", nullptr, "viand", "vior", nullptr, nullptr, + "vcallms", "vcallmsr", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, + }; + + static const char* const kSpec2Names[128] = { + "vaddax", "vadday", "vaddaz", "vaddaw", "vsubax", "vsubay", "vsubaz", "vsubaw", + "vmaddax", "vmadday", "vmaddaz", "vmaddaw", "vmsubax", "vmsubay", "vmsubaz", "vmsubaw", + "vitof0", "vitof4", "vitof12", "vitof15", "vftoi0", "vftoi4", "vftoi12", "vftoi15", + "vmulax", "vmulay", "vmulaz", "vmulaw", "vmulaq", "vabs", "vmulai", "vclip", + "vaddaq", "vmaddaq", "vaddai", "vmaddai", "vsubaq", "vmsubaq", "vsubai", "vmsubai", + "vadda", "vmadda", "vmula", nullptr, "vsuba", "vmsuba", "vopmula", "vnop", + "vmove", "vmr32", nullptr, nullptr, "vlqi", "vsqi", "vlqd", "vsqd", + "vdiv", "vsqrt", "vrsqrt", "vwaitq", "vmtir", "vmfir", "vilwr", "viswr", + "vrnext", "vrget", "vrinit", "vrxor", nullptr, nullptr, nullptr, nullptr, + // 0x48-0x7F are holes. + }; + + int Cop2VuOpId(u32 code) + { + if ((code >> 26) != 0x12) + return -1; + const u32 rs = (code >> 21) & 0x1F; + if (rs == 0x08) // BC2 + return static_cast((code >> 16) & 0x1F); + if (rs < 0x10) // QMFC2/CFC2/QMTC2/CTC2 and holes are not VU macro ops + return -1; + + const u32 funct = code & 0x3F; + if (funct >= 0x3C) // SPECIAL2 escape + return 0x80 + static_cast((code & 0x3) | ((code >> 4) & 0x7C)); + return 0x40 + static_cast(funct); + } + + const char* Cop2VuOpName(int id) + { + if (id < 0 || id >= kCop2VuIdCount) + return nullptr; + if (id < 0x20) + return (id < 4) ? kBc2Names[id] : nullptr; + if (id >= 0x40 && id < 0x80) + return kSpec1Names[id - 0x40]; + if (id >= 0x80) + return kSpec2Names[id - 0x80]; + return nullptr; + } + + static u32 s_cop2VuCensus[kCop2VuIdCount]; + + void NoteCop2VuCompiled(u32 code) + { + const int id = Cop2VuOpId(code); + if (id >= 0) + s_cop2VuCensus[id]++; + } + + std::string DescribeCop2VuCensus() + { + std::string s; + u32 total = 0; + for (int id = 0; id < kCop2VuIdCount; id++) + { + if (s_cop2VuCensus[id] == 0) + continue; + total += s_cop2VuCensus[id]; + const char* name = Cop2VuOpName(id); + if (!s.empty()) + s += " "; + s += fmt::format("{}={}", name ? name : fmt::format("id0x{:02x}", id), s_cop2VuCensus[id]); + } + if (s.empty()) + return "no VU macro ops compiled"; + return fmt::format("{} (total {})", s, total); + } bool Selected(u32 code) { @@ -1773,6 +1859,15 @@ namespace EERecFallback break; } + if (group == Cop2Vu) + { + // Per-op filter: an unnamed/unmapped encoding is never selected, so a + // mnemonic filter can't accidentally widen to the whole group. + const int id = Cop2VuOpId(code); + if (id < 0 || (g_cop2VuMask[id >> 6] & (1ull << (id & 63))) == 0) + return false; + } + return group != 0 && (g_groups & group) != 0; } @@ -1797,10 +1892,13 @@ namespace EERecFallback } } - bool ParseGroups(const std::string_view& list, u32* out, u32* reg_masks, std::string* error) + bool ParseGroups(const std::string_view& list, u32* out, u32* reg_masks, + u64* cop2vu_mask, std::string* error) { u32 local_reg_masks[kCop2MoveOpCount] = {~0u, ~0u, ~0u, ~0u}; bool reg_filtered[kCop2MoveOpCount] = {false, false, false, false}; + u64 local_vu_mask[kCop2VuIdCount / 64] = {~0ull, ~0ull, ~0ull, ~0ull}; + bool vu_filtered = false; u32 mask = 0; size_t pos = 0; while (pos <= list.size()) @@ -1842,7 +1940,47 @@ namespace EERecFallback found = true; mask |= g.bit; - if (!regs.empty()) + if (!regs.empty() && g.bit == Cop2Vu) + { + // "cop2vu:vmulaw:vmaddaz" — narrow to named macro ops. + size_t rp = 0; + while (rp <= regs.size()) + { + const size_t rc = regs.find(':', rp); + const size_t rend = (rc == std::string_view::npos) ? regs.size() : rc; + const std::string_view mnem = regs.substr(rp, rend - rp); + int id = -1; + for (int i = 0; i < kCop2VuIdCount; i++) + { + const char* n = Cop2VuOpName(i); + if (n && mnem == n) + { + id = i; + break; + } + } + if (id < 0) + { + if (error) + { + *error = fmt::format("unknown VU macro op '{}' in EE rec fallback " + "filter '{}'", std::string(mnem), std::string(tok)); + } + return false; + } + if (!vu_filtered) + { + for (auto& w : local_vu_mask) + w = 0; + vu_filtered = true; + } + local_vu_mask[id >> 6] |= (1ull << (id & 63)); + if (rc == std::string_view::npos) + break; + rp = rc + 1; + } + } + else if (!regs.empty()) { const int slot = MoveOpSlotForGroup(g.bit); if (slot < 0) @@ -1850,7 +1988,8 @@ namespace EERecFallback if (error) { *error = fmt::format("EE rec fallback group '{}' does not take a register " - "filter (only qmfc2/cfc2/qmtc2/ctc2 do)", std::string(name)); + "filter (only qmfc2/cfc2/qmtc2/ctc2 take ':', cop2vu takes " + "':')", std::string(name)); } return false; } @@ -1891,7 +2030,8 @@ namespace EERecFallback *error = fmt::format("unknown EE rec fallback group '{}'; valid: none, all, " "fpu, cop2, mmi, multdiv, shift, arith, loadstore, move, cop0, branch, " "cop2move, cop2vu, cop2ls, qmfc2, cfc2, qmtc2, ctc2 (the last four accept " - "a ':' filter, e.g. ctc2:27)", + "a ':' filter, e.g. ctc2:27; cop2vu accepts a ':' filter, " + "e.g. cop2vu:vmaddaw)", std::string(name)); } return false; @@ -1907,6 +2047,8 @@ namespace EERecFallback *out = mask; for (int i = 0; i < kCop2MoveOpCount; i++) reg_masks[i] = local_reg_masks[i]; + for (int i = 0; i < kCop2VuIdCount / 64; i++) + cop2vu_mask[i] = local_vu_mask[i]; return true; } diff --git a/pcsx2/arm64/iR5900-arm64.h b/pcsx2/arm64/iR5900-arm64.h index 6bc104337c..c4721413bc 100644 --- a/pcsx2/arm64/iR5900-arm64.h +++ b/pcsx2/arm64/iR5900-arm64.h @@ -962,14 +962,37 @@ namespace EERecFallback enum Cop2MoveOp { kQmfc2 = 0, kCfc2, kQmtc2, kCtc2, kCop2MoveOpCount }; extern u32 g_cop2RegMask[kCop2MoveOpCount]; + // Per-VU-macro-op filter, the next bisect axis once `cop2vu` is implicated. + // That group is 100+ distinct emitters, so narrowing it by name is the + // difference between one run and a hand-written binary search. Ids form a + // flat 256-entry space so one bitset covers all three dispatch tables: + // 0x00-0x1F BC2 (by rt), 0x40-0x7F SPECIAL1 (by funct), 0x80-0xFF SPECIAL2 + // (by its packed index). All bits set (the default) means "every op". + inline constexpr int kCop2VuIdCount = 256; + extern u64 g_cop2VuMask[kCop2VuIdCount / 64]; + + // Canonical id for a VU macro op, or -1 if `code` is not one. + int Cop2VuOpId(u32 code); + // Mnemonic for an id ("vmulaw"), or nullptr for an unassigned slot. + const char* Cop2VuOpName(int id); + + // Compile-time census of the VU macro ops the recompiler actually emitted. + // An op that is never compiled cannot be the bug, so this prunes a 100-way + // bisect down to the handful a given game really uses. + void NoteCop2VuCompiled(u32 code); + std::string DescribeCop2VuCensus(); + // True when `code` belongs to a selected group (and passes its register filter). bool Selected(u32 code); // Parse a comma-separated group list ("fpu,mmi" / "all" / "none"). A COP2 move // group may carry a register filter: "ctc2:27" or "ctc2:16:21" selects only - // those fs values. Returns false and leaves the outputs untouched on a parse - // error. `reg_masks` must point at kCop2MoveOpCount entries. - bool ParseGroups(const std::string_view& list, u32* out, u32* reg_masks, std::string* error); + // those fs values; "cop2vu" takes a mnemonic filter: "cop2vu:vmulaw:vmaddaz". + // Returns false and leaves the outputs untouched on a parse error. + // `reg_masks` must point at kCop2MoveOpCount entries, `cop2vu_mask` at + // kCop2VuIdCount/64. + bool ParseGroups(const std::string_view& list, u32* out, u32* reg_masks, + u64* cop2vu_mask, std::string* error); // Human-readable rendering of a mask, for run banners. std::string DescribeGroups(u32 groups); diff --git a/pcsx2/arm64/iR5900Misc-arm64.cpp b/pcsx2/arm64/iR5900Misc-arm64.cpp index 26e7d08fb7..c10bf7819b 100644 --- a/pcsx2/arm64/iR5900Misc-arm64.cpp +++ b/pcsx2/arm64/iR5900Misc-arm64.cpp @@ -426,6 +426,9 @@ static void recCOP2_SPEC2() { recCOP2SPECIAL2t[(cpuRegs.code & 0x3) | ((cpuRegs. void recCOP2() { +#ifdef PCSX2_RECOMPILER_TESTS + EERecFallback::NoteCop2VuCompiled(cpuRegs.code); +#endif recCOP2t[_Rs_](); }