mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
eerunner: --rec-fallback opcode-group interpreter bisect switch
EE JIT divergence hunts start at "this game miscomputes something under the EE recompiler" with no idea which emitter is at fault, and the existing funnel (stepdiff/contmem) localizes to a frame and a RAM region — which chaotic float amplification makes almost unreadable by end of frame. Add a harness-only switch that routes whole EE opcode groups through recCall(interpret) instead of their native emitter, everything else still JIT. The interpreters are the known-good baseline, so the group that makes the symptom disappear contains the bug, and each probe is one run instead of a rebuild per hypothesis. Groups: fpu, cop2, mmi, multdiv, shift, arith, loadstore, move, cop0, branch, plus all/none. cop2 narrows into cop2move / cop2vu / cop2ls and then into qmfc2 / cfc2 / qmtc2 / ctc2, and those four accept a `:<reg>` destination filter (`ctc2:0`) since their fs field names 32 registers with wildly different semantics. Gated on PCSX2_RECOMPILER_TESTS so it never reaches a shipping build; the non-hooks path compiles to `constexpr bool forcedInterp = false`. A forced fallback also feeds delaySlotNeedsBranchBracket, matching the conservatism already applied to genuine !opcode.recompile fallbacks, so the switch cannot itself perturb delay-slot exception semantics. Pairs with `--mkstate --renderer sw`, whose savestate carries a rendered screenshot, to give a headless visual oracle for graphical bugs. Together these took the Xenosaga zoom bug from "somewhere in the EE JIT" to the exact instruction (CTC2 to vi00) in about ten runs.
This commit is contained in:
@@ -77,6 +77,10 @@
|
||||
|
||||
#include "pcsx2/ee_divtrace.h"
|
||||
|
||||
#if defined(ARCH_ARM64)
|
||||
#include "pcsx2/arm64/iR5900-arm64.h"
|
||||
#endif
|
||||
|
||||
#include "svnrev.h"
|
||||
|
||||
namespace EERunner
|
||||
@@ -133,6 +137,10 @@ struct SetOverride
|
||||
std::string section, key, value;
|
||||
};
|
||||
static std::vector<SetOverride> s_set_overrides; // --set Section/Key=Value (repeatable)
|
||||
static u32 s_rec_fallback_groups = 0; // --rec-fallback <groups>: 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
|
||||
#endif
|
||||
|
||||
bool EERunner::InitializeConfig()
|
||||
{
|
||||
@@ -528,6 +536,14 @@ static void PrintCommandLineHelp(const char* progname)
|
||||
std::fprintf(stderr, " --localize / --repro: aliases of --stepdiff (the old jittery frame-boundary funnel was removed).\n");
|
||||
std::fprintf(stderr, " --selfcheck: Characterize run-to-run determinism (interp only). Expected to flag benign ~10-cycle\n");
|
||||
std::fprintf(stderr, " pause-point sampling jitter; use it to understand the noise floor, not as a gate.\n");
|
||||
std::fprintf(stderr, " --rec-fallback <groups>: comma-separated EE opcode groups to force through the INTERPRETER\n");
|
||||
std::fprintf(stderr, " instead of native codegen, with everything else still JIT. Bisects \"the EE JIT\n");
|
||||
std::fprintf(stderr, " miscomputes something\" down to one emitter family without a rebuild per hypothesis:\n");
|
||||
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, " Pairs well with --mkstate --renderer sw for a visual oracle. arm64 only.\n");
|
||||
std::fprintf(stderr, " --renderer <null|vk|ogl|sw>: GS renderer (default null). Use vk on Intel GPUs / boxes where\n");
|
||||
std::fprintf(stderr, " the auto-check declines Vulkan and the surfaceless GL path fails to open GS.\n");
|
||||
std::fprintf(stderr, " --savestate <file>: Savestate to load after Initialize (required).\n");
|
||||
@@ -709,6 +725,26 @@ bool EERunner::ParseCommandLineArgs(int argc, char* argv[], VMBootParameters& pa
|
||||
s_set_overrides.push_back({kv.substr(0, slash), kv.substr(slash + 1, eq - slash - 1), kv.substr(eq + 1)});
|
||||
continue;
|
||||
}
|
||||
else if (CHECK_ARG_PARAM("--rec-fallback"))
|
||||
{
|
||||
const std::string_view list = StringUtil::StripWhitespace(argv[++i]);
|
||||
#if defined(ARCH_ARM64)
|
||||
std::string err;
|
||||
u32 mask = 0;
|
||||
u32 reg_masks[EERecFallback::kCop2MoveOpCount] = {};
|
||||
if (!EERecFallback::ParseGroups(list, &mask, reg_masks, &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));
|
||||
#else
|
||||
Console.Error("--rec-fallback is implemented for the arm64 EE recompiler only.");
|
||||
return false;
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
else if (CHECK_ARG_PARAM("--renderer"))
|
||||
{
|
||||
const std::string_view r = StringUtil::StripWhitespace(argv[++i]);
|
||||
@@ -978,6 +1014,19 @@ void EERunner::SettingsOverride()
|
||||
s_settings_interface.SetStringValue(so.section.c_str(), so.key.c_str(), so.value.c_str());
|
||||
Console.WriteLn(fmt::format("SettingsOverride: --set [{}] {} = {}", so.section, so.key, so.value));
|
||||
}
|
||||
|
||||
#if defined(ARCH_ARM64)
|
||||
// --rec-fallback: route whole EE opcode groups through the interpreter. Not a
|
||||
// setting (it lives in the recompiler, not EmuConfig), so it is applied here,
|
||||
// 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));
|
||||
if (s_rec_fallback_groups != 0)
|
||||
{
|
||||
Console.WriteLn(fmt::format("EE REC FALLBACK: forcing to interpreter -> {}",
|
||||
EERecFallback::DescribeGroups(s_rec_fallback_groups)));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Snapshot live cpuRegs/fpuRegs into a FullSnap (frame-boundary capture; pc/cycle
|
||||
|
||||
@@ -1863,6 +1863,246 @@ static bool recSuperblockLivenessBarrier(u32 addr)
|
||||
// INTC/DMAC/TIMR fire only at event tests (block boundary, branch==0 there),
|
||||
// and AdEL (RaiseAddressError) is a stub. Pinned by ee_rec_traps_tests.cpp
|
||||
// delay-slot raiser tests + AluDelaySlotBranchSemanticsSurviveWithoutBracket.
|
||||
#ifdef PCSX2_RECOMPILER_TESTS
|
||||
|
||||
namespace EERecFallback
|
||||
{
|
||||
u32 g_groups = 0;
|
||||
u32 g_cop2RegMask[kCop2MoveOpCount] = {~0u, ~0u, ~0u, ~0u};
|
||||
|
||||
bool Selected(u32 code)
|
||||
{
|
||||
if (g_groups == 0)
|
||||
return false;
|
||||
|
||||
const u32 primary = code >> 26;
|
||||
u32 group = 0;
|
||||
|
||||
switch (primary)
|
||||
{
|
||||
case 0x00: // SPECIAL
|
||||
{
|
||||
const u32 funct = code & 0x3F;
|
||||
if (funct == 0x08 || funct == 0x09) // JR / JALR
|
||||
group = Branch;
|
||||
else if (funct == 0x0A || funct == 0x0B || (funct >= 0x10 && funct <= 0x13))
|
||||
group = Move; // MOVZ/MOVN, MFHI/MTHI/MFLO/MTLO
|
||||
else if (funct >= 0x18 && funct <= 0x1B)
|
||||
group = MultDiv;
|
||||
else if (funct <= 0x07 || funct == 0x14 || funct == 0x16 || funct == 0x17 ||
|
||||
funct == 0x38 || funct == 0x3A || funct == 0x3B || funct == 0x3C ||
|
||||
funct == 0x3E || funct == 0x3F)
|
||||
group = Shift;
|
||||
else if ((funct >= 0x20 && funct <= 0x2F) || funct == 0x28 || funct == 0x29)
|
||||
group = Arith; // ADD..NOR, SLT/SLTU, DADD..DSUBU, MFSA/MTSA
|
||||
break;
|
||||
}
|
||||
case 0x01: // REGIMM — branches (trap immediates stay native)
|
||||
{
|
||||
const u32 rt = (code >> 16) & 0x1F;
|
||||
if (rt <= 0x03 || (rt >= 0x10 && rt <= 0x13))
|
||||
group = Branch;
|
||||
break;
|
||||
}
|
||||
case 0x02: case 0x03: // J / JAL
|
||||
case 0x04: case 0x05: case 0x06: case 0x07: // BEQ/BNE/BLEZ/BGTZ
|
||||
case 0x14: case 0x15: case 0x16: case 0x17: // *L variants
|
||||
group = Branch;
|
||||
break;
|
||||
case 0x08: case 0x09: case 0x0A: case 0x0B: // ADDI/ADDIU/SLTI/SLTIU
|
||||
case 0x0C: case 0x0D: case 0x0E: case 0x0F: // ANDI/ORI/XORI/LUI
|
||||
case 0x18: case 0x19: // DADDI/DADDIU
|
||||
group = Arith;
|
||||
break;
|
||||
case 0x10: group = Cop0; break;
|
||||
case 0x11: group = Fpu; break; // COP1
|
||||
case 0x12: // COP2 — split by rs so the VU macro ops can be isolated
|
||||
{
|
||||
const u32 rs = (code >> 21) & 0x1F;
|
||||
int moveOp = -1;
|
||||
switch (rs)
|
||||
{
|
||||
case 0x01: group = Qmfc2; moveOp = kQmfc2; break;
|
||||
case 0x02: group = Cfc2; moveOp = kCfc2; break;
|
||||
case 0x05: group = Qmtc2; moveOp = kQmtc2; break;
|
||||
case 0x06: group = Ctc2; moveOp = kCtc2; break;
|
||||
default: group = Cop2Vu; break; // BC2 + CO=1 macro ops
|
||||
}
|
||||
// Register filter: fs/rd is bits 15-11 for all four move ops.
|
||||
if (moveOp >= 0 && (g_cop2RegMask[moveOp] & (1u << ((code >> 11) & 0x1F))) == 0)
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case 0x1C: group = Mmi; break;
|
||||
case 0x31: case 0x39: group = Fpu; break; // LWC1 / SWC1
|
||||
case 0x36: case 0x3E: group = Cop2Ls; break; // LQC2 / SQC2
|
||||
case 0x1A: case 0x1B: // LDL / LDR
|
||||
case 0x1E: case 0x1F: // LQ / SQ
|
||||
case 0x37: case 0x3F: // LD / SD
|
||||
group = LoadStore;
|
||||
break;
|
||||
default:
|
||||
if (primary >= 0x20 && primary <= 0x2E) // LB..SWR
|
||||
group = LoadStore;
|
||||
break;
|
||||
}
|
||||
|
||||
return group != 0 && (g_groups & group) != 0;
|
||||
}
|
||||
|
||||
static const struct { const char* name; u32 bit; } kGroupNames[] = {
|
||||
{"fpu", Fpu}, {"cop2", Cop2All}, {"mmi", Mmi}, {"multdiv", MultDiv},
|
||||
{"shift", Shift}, {"arith", Arith}, {"loadstore", LoadStore},
|
||||
{"move", Move}, {"cop0", Cop0}, {"branch", Branch},
|
||||
{"cop2move", Cop2Move}, {"cop2vu", Cop2Vu}, {"cop2ls", Cop2Ls},
|
||||
{"qmfc2", Qmfc2}, {"cfc2", Cfc2}, {"qmtc2", Qmtc2}, {"ctc2", Ctc2},
|
||||
};
|
||||
|
||||
// Maps a group bit to its COP2 move-op register-filter slot, or -1.
|
||||
static int MoveOpSlotForGroup(u32 bit)
|
||||
{
|
||||
switch (bit)
|
||||
{
|
||||
case Qmfc2: return kQmfc2;
|
||||
case Cfc2: return kCfc2;
|
||||
case Qmtc2: return kQmtc2;
|
||||
case Ctc2: return kCtc2;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseGroups(const std::string_view& list, u32* out, u32* reg_masks, std::string* error)
|
||||
{
|
||||
u32 local_reg_masks[kCop2MoveOpCount] = {~0u, ~0u, ~0u, ~0u};
|
||||
bool reg_filtered[kCop2MoveOpCount] = {false, false, false, false};
|
||||
u32 mask = 0;
|
||||
size_t pos = 0;
|
||||
while (pos <= list.size())
|
||||
{
|
||||
const size_t comma = list.find(',', pos);
|
||||
const size_t end = (comma == std::string_view::npos) ? list.size() : comma;
|
||||
std::string_view tok = list.substr(pos, end - pos);
|
||||
while (!tok.empty() && tok.front() == ' ') tok.remove_prefix(1);
|
||||
while (!tok.empty() && tok.back() == ' ') tok.remove_suffix(1);
|
||||
|
||||
if (!tok.empty())
|
||||
{
|
||||
if (tok == "none")
|
||||
{
|
||||
mask = 0;
|
||||
}
|
||||
else if (tok == "all")
|
||||
{
|
||||
for (const auto& g : kGroupNames)
|
||||
mask |= g.bit;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Optional "<group>:<reg>[:<reg>...]" register filter.
|
||||
std::string_view name = tok;
|
||||
std::string_view regs;
|
||||
const size_t colon = tok.find(':');
|
||||
if (colon != std::string_view::npos)
|
||||
{
|
||||
name = tok.substr(0, colon);
|
||||
regs = tok.substr(colon + 1);
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
for (const auto& g : kGroupNames)
|
||||
{
|
||||
if (name != g.name)
|
||||
continue;
|
||||
found = true;
|
||||
mask |= g.bit;
|
||||
|
||||
if (!regs.empty())
|
||||
{
|
||||
const int slot = MoveOpSlotForGroup(g.bit);
|
||||
if (slot < 0)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*error = fmt::format("EE rec fallback group '{}' does not take a register "
|
||||
"filter (only qmfc2/cfc2/qmtc2/ctc2 do)", std::string(name));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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 rtok = regs.substr(rp, rend - rp);
|
||||
const std::optional<u32> parsed = StringUtil::FromChars<u32>(rtok, 10);
|
||||
const u32 reg = parsed.value_or(0);
|
||||
if (rtok.empty() || !parsed.has_value() || reg > 31)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*error = fmt::format("bad register '{}' in EE rec fallback filter "
|
||||
"'{}' (expected 0-31)", std::string(rtok), std::string(tok));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!reg_filtered[slot])
|
||||
{
|
||||
local_reg_masks[slot] = 0;
|
||||
reg_filtered[slot] = true;
|
||||
}
|
||||
local_reg_masks[slot] |= (1u << reg);
|
||||
if (rc == std::string_view::npos)
|
||||
break;
|
||||
rp = rc + 1;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
if (error)
|
||||
{
|
||||
*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 ':<reg>' filter, e.g. ctc2:27)",
|
||||
std::string(name));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (comma == std::string_view::npos)
|
||||
break;
|
||||
pos = comma + 1;
|
||||
}
|
||||
|
||||
*out = mask;
|
||||
for (int i = 0; i < kCop2MoveOpCount; i++)
|
||||
reg_masks[i] = local_reg_masks[i];
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string DescribeGroups(u32 groups)
|
||||
{
|
||||
if (groups == 0)
|
||||
return "none (full native codegen)";
|
||||
std::string s;
|
||||
for (const auto& g : kGroupNames)
|
||||
{
|
||||
if (groups & g.bit)
|
||||
{
|
||||
if (!s.empty()) s += ",";
|
||||
s += g.name;
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
} // namespace EERecFallback
|
||||
|
||||
#endif // PCSX2_RECOMPILER_TESTS
|
||||
|
||||
static bool delaySlotNeedsBranchBracket(u32 code, const R5900::OPCODE& op)
|
||||
{
|
||||
if (op.flags & (IS_LOAD | IS_STORE | IS_MEMORY))
|
||||
@@ -1871,6 +2111,11 @@ static bool delaySlotNeedsBranchBracket(u32 code, const R5900::OPCODE& op)
|
||||
return true; // SYSCALL + branch-in-delay-slot interpreter fallback
|
||||
if (!op.recompile)
|
||||
return true; // conservative: interpreter fallback may raise
|
||||
#ifdef PCSX2_RECOMPILER_TESTS
|
||||
if (EERecFallback::Selected(code))
|
||||
return true; // same conservatism for a harness-forced interpreter fallback,
|
||||
// so the bisect switch cannot itself change exception semantics
|
||||
#endif
|
||||
const u32 primary = code >> 26;
|
||||
if (primary == 0x00)
|
||||
{
|
||||
@@ -2073,7 +2318,13 @@ void recompileNextInstruction(bool delayslot, bool swapped_delay_slot)
|
||||
// Guard: branch/jump in a delay slot would cause infinite
|
||||
// compile-time recursion. Use interpreter for the instruction.
|
||||
const bool isBranchInDelaySlot = delayslot && (opcode.flags & IS_BRANCH);
|
||||
if (isBranchInDelaySlot || !opcode.recompile)
|
||||
#ifdef PCSX2_RECOMPILER_TESTS
|
||||
// Harness-only: --rec-fallback bisect switch (see EERecFallback).
|
||||
const bool forcedInterp = EERecFallback::Selected(cpuRegs.code);
|
||||
#else
|
||||
constexpr bool forcedInterp = false;
|
||||
#endif
|
||||
if (isBranchInDelaySlot || !opcode.recompile || forcedInterp)
|
||||
{
|
||||
if ((opcode.flags & IS_BRANCH) && !isBranchInDelaySlot)
|
||||
recBranchCall(opcode.interpret);
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
#include "VU.h"
|
||||
#include "arm64/iCore-arm64.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
// Per-category interpreter fallback toggles.
|
||||
// Comment out a define to enable native ARM64 codegen for that category.
|
||||
// #define FORCE_INTERP_BRANCH 1
|
||||
@@ -905,3 +908,72 @@ void eeRecompileCodeRC2_MEM(R5900FNPTR constcode, R5900FNPTR_INFO noconstcode, i
|
||||
|
||||
int eeRecompileCodeXMM(int xmminfo);
|
||||
void eeFPURecompileCode(R5900FNPTR_INFO xmmcode, R5900FNPTR fpucode, int xmminfo);
|
||||
|
||||
#ifdef PCSX2_RECOMPILER_TESTS
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Opcode-group interpreter-fallback bisect switch (harness-only).
|
||||
//
|
||||
// Divergence hunts start with "the EE JIT miscomputes something in this game" and
|
||||
// no idea which emitter. Selecting a group here routes every opcode in it through
|
||||
// recCall(interpret) instead of its native emitter, so a handful of runs against a
|
||||
// visual/memory oracle bisects the bug down to one emitter family without a rebuild
|
||||
// per hypothesis. The interpreters are the known-good baseline, so a group that
|
||||
// makes the symptom disappear contains the bug.
|
||||
//
|
||||
// Gated on PCSX2_RECOMPILER_TESTS: never compiled into a shipping build.
|
||||
namespace EERecFallback
|
||||
{
|
||||
enum Group : u32
|
||||
{
|
||||
Fpu = 1u << 0, // COP1 + LWC1/SWC1
|
||||
// bit 1 free (COP2 is split into sub-groups below)
|
||||
Mmi = 1u << 2, // MMI (128-bit multimedia)
|
||||
MultDiv = 1u << 3, // SPECIAL MULT/MULTU/DIV/DIVU
|
||||
Shift = 1u << 4, // SPECIAL shifts (incl. doubleword)
|
||||
Arith = 1u << 5, // SPECIAL + immediate integer ALU
|
||||
LoadStore = 1u << 6, // integer loads/stores (incl. LQ/SQ, unaligned)
|
||||
Move = 1u << 7, // MFHI/MTHI/MFLO/MTLO/MOVZ/MOVN
|
||||
Cop0 = 1u << 8, // COP0
|
||||
Branch = 1u << 9, // branches and jumps
|
||||
|
||||
// COP2 sub-groups, for narrowing once `cop2` has been implicated.
|
||||
Cop2Vu = 1u << 11, // the VU macro-mode instructions themselves (CO=1) + BC2
|
||||
Cop2Ls = 1u << 12, // LQC2 / SQC2
|
||||
|
||||
// Individual COP2 move ops, for the last step of the narrowing.
|
||||
Qmfc2 = 1u << 13, // VF -> EE GPR (128-bit)
|
||||
Cfc2 = 1u << 14, // VI -> EE GPR (32-bit)
|
||||
Qmtc2 = 1u << 15, // EE GPR -> VF (128-bit)
|
||||
Ctc2 = 1u << 16, // EE GPR -> VI (32-bit)
|
||||
|
||||
Cop2Move = Qmfc2 | Cfc2 | Qmtc2 | Ctc2,
|
||||
Cop2All = Cop2Move | Cop2Vu | Cop2Ls,
|
||||
};
|
||||
|
||||
// Bitmask of groups to force to the interpreter. 0 = full native codegen.
|
||||
extern u32 g_groups;
|
||||
|
||||
// Per-COP2-move-op register filters. The four EE<->VU move ops each carry an
|
||||
// fs/rd field naming one of 32 registers whose semantics differ wildly (VI01
|
||||
// is a plain integer, REG_I is a broadcast constant, CMSAR0 a micro-program
|
||||
// address, FBRST a reset port). Once a hunt has narrowed to one of these ops,
|
||||
// filtering by register is the next bisect axis. Index by Cop2MoveOp; a mask
|
||||
// of ~0u (the default) means "every register".
|
||||
enum Cop2MoveOp { kQmfc2 = 0, kCfc2, kQmtc2, kCtc2, kCop2MoveOpCount };
|
||||
extern u32 g_cop2RegMask[kCop2MoveOpCount];
|
||||
|
||||
// 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);
|
||||
|
||||
// Human-readable rendering of a mask, for run banners.
|
||||
std::string DescribeGroups(u32 groups);
|
||||
} // namespace EERecFallback
|
||||
|
||||
#endif // PCSX2_RECOMPILER_TESTS
|
||||
|
||||
Reference in New Issue
Block a user