mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
DiffJitVsInterp harness (EE/IOP/VU synthetic JIT-vs-interpreter tests) plus the shared capture/divergence-localizer infrastructure (vu_capture, ee_divtrace, microVU_Divtrace, VU1Trace) and the test hooks they install in interp/GIF/COP2 paths. All hook sites are guarded by PCSX2_RECOMPILER_TESTS and compile out of release builds. Co-Authored-By: Ryan Walklin <ryan@testtoast.com> Co-Authored-By: Brian Degenhardt <bmd@bmdhacks.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
336 lines
7.9 KiB
C++
336 lines
7.9 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "Common.h"
|
|
|
|
#include "VUmicro.h"
|
|
#include "VU1Trace.h"
|
|
#include "GS.h"
|
|
#include "Gif_Unit.h"
|
|
#include "MTVU.h"
|
|
#include "microVU_Divtrace.h"
|
|
#include "vu_capture.h"
|
|
|
|
#include <atomic>
|
|
#include <cfenv>
|
|
|
|
extern void _vuFlushAll(VURegs* VU);
|
|
extern void _vuXGKICKFlush(VURegs* VU);
|
|
|
|
void _vu1ExecUpper(VURegs* VU, u32* ptr)
|
|
{
|
|
VU->code = ptr[1];
|
|
IdebugUPPER(VU1);
|
|
VU1_UPPER_OPCODE[VU->code & 0x3f]();
|
|
}
|
|
|
|
void _vu1ExecLower(VURegs* VU, u32* ptr)
|
|
{
|
|
VU->code = ptr[0];
|
|
IdebugLOWER(VU1);
|
|
VU1_LOWER_OPCODE[VU->code >> 25]();
|
|
}
|
|
|
|
int vu1branch = 0;
|
|
|
|
static void _vu1Exec(VURegs* VU)
|
|
{
|
|
_VURegsNum lregs;
|
|
_VURegsNum uregs;
|
|
u32* ptr;
|
|
|
|
ptr = (u32*)&VU->Micro[VU->VI[REG_TPC].UL];
|
|
VU->VI[REG_TPC].UL += 8;
|
|
|
|
if (ptr[1] & 0x40000000) // E flag
|
|
{
|
|
VU->ebit = 2;
|
|
}
|
|
if (ptr[1] & 0x10000000) // D flag
|
|
{
|
|
if (VU0.VI[REG_FBRST].UL & 0x400)
|
|
{
|
|
VU0.VI[REG_VPU_STAT].UL |= 0x200;
|
|
hwIntcIrq(INTC_VU1);
|
|
VU->ebit = 1;
|
|
}
|
|
}
|
|
if (ptr[1] & 0x08000000) // T flag
|
|
{
|
|
if (VU0.VI[REG_FBRST].UL & 0x800)
|
|
{
|
|
VU0.VI[REG_VPU_STAT].UL |= 0x400;
|
|
hwIntcIrq(INTC_VU1);
|
|
VU->ebit = 1;
|
|
}
|
|
}
|
|
|
|
//VUM_LOG("VU->cycle = %d (flags st=%x;mac=%x;clip=%x,q=%f)", VU->cycle, VU->statusflag, VU->macflag, VU->clipflag, VU->q.F);
|
|
|
|
VU->code = ptr[1];
|
|
VU1regs_UPPER_OPCODE[VU->code & 0x3f](&uregs);
|
|
|
|
u32 cyclesBeforeOp = VU1.cycle-1;
|
|
|
|
_vuTestUpperStalls(VU, &uregs);
|
|
|
|
/* check upper flags */
|
|
if (ptr[1] & 0x80000000) // I Flag (Lower op is a float)
|
|
{
|
|
_vuTestPipes(VU);
|
|
|
|
if (VU->VIBackupCycles > 0)
|
|
VU->VIBackupCycles -= std::min((u8)(VU1.cycle - cyclesBeforeOp), VU->VIBackupCycles);
|
|
|
|
_vu1ExecUpper(VU, ptr);
|
|
|
|
VU->VI[REG_I].UL = ptr[0];
|
|
//Lower not used, set to 0 to fill in the FMAC stall gap
|
|
//Could probably get away with just running upper stalls, but lets not tempt fate.
|
|
memset(&lregs, 0, sizeof(lregs));
|
|
}
|
|
else
|
|
{
|
|
VECTOR _VF;
|
|
VECTOR _VFc;
|
|
REG_VI _VI;
|
|
REG_VI _VIc;
|
|
int vfreg = 0;
|
|
int vireg = 0;
|
|
int discard = 0;
|
|
|
|
VU->code = ptr[0];
|
|
lregs.cycles = 0;
|
|
VU1regs_LOWER_OPCODE[VU->code >> 25](&lregs);
|
|
|
|
_vuTestLowerStalls(VU, &lregs);
|
|
_vuTestPipes(VU);
|
|
|
|
if (VU->VIBackupCycles > 0)
|
|
VU->VIBackupCycles-= std::min((u8)(VU1.cycle- cyclesBeforeOp), VU->VIBackupCycles);
|
|
|
|
if (uregs.VFwrite)
|
|
{
|
|
if (lregs.VFwrite == uregs.VFwrite)
|
|
{
|
|
//Console.Warning("*PCSX2*: Warning, VF write to the same reg in both lower/upper cycle pc=%x", VU->VI[REG_TPC].UL);
|
|
discard = 1;
|
|
}
|
|
if (lregs.VFread0 == uregs.VFwrite ||
|
|
lregs.VFread1 == uregs.VFwrite)
|
|
{
|
|
//Console.WriteLn("saving reg %d at pc=%x", uregs.VFwrite, VU->VI[REG_TPC].UL);
|
|
_VF = VU->VF[uregs.VFwrite];
|
|
vfreg = uregs.VFwrite;
|
|
}
|
|
}
|
|
if (uregs.VIwrite & (1 << REG_CLIP_FLAG))
|
|
{
|
|
if (lregs.VIwrite & (1 << REG_CLIP_FLAG))
|
|
{
|
|
//Console.Warning("*PCSX2*: Warning, VI write to the same reg in both lower/upper cyclepc=%x", VU->VI[REG_TPC].UL);
|
|
discard = 1;
|
|
}
|
|
if (lregs.VIread & (1 << REG_CLIP_FLAG))
|
|
{
|
|
//Console.Warning("*PCSX2*: Warning, VI read same cycle as write pc=%x", VU->VI[REG_TPC].UL);
|
|
_VI = VU->VI[REG_CLIP_FLAG];
|
|
vireg = REG_CLIP_FLAG;
|
|
}
|
|
}
|
|
|
|
_vu1ExecUpper(VU, ptr);
|
|
|
|
if (discard == 0)
|
|
{
|
|
if (vfreg)
|
|
{
|
|
_VFc = VU->VF[vfreg];
|
|
VU->VF[vfreg] = _VF;
|
|
}
|
|
if (vireg)
|
|
{
|
|
_VIc = VU->VI[vireg];
|
|
VU->VI[vireg] = _VI;
|
|
}
|
|
|
|
_vu1ExecLower(VU, ptr);
|
|
|
|
if (vfreg)
|
|
{
|
|
VU->VF[vfreg] = _VFc;
|
|
}
|
|
if (vireg)
|
|
{
|
|
VU->VI[vireg] = _VIc;
|
|
}
|
|
}
|
|
}
|
|
// Clear an FMAC read for use
|
|
if (uregs.pipe == VUPIPE_FMAC || lregs.pipe == VUPIPE_FMAC)
|
|
_vuClearFMAC(VU);
|
|
|
|
_vuAddUpperStalls(VU, &uregs);
|
|
_vuAddLowerStalls(VU, &lregs);
|
|
|
|
if (VU->branch > 0)
|
|
{
|
|
if (VU->branch-- == 1)
|
|
{
|
|
VU->VI[REG_TPC].UL = VU->branchpc;
|
|
|
|
if (VU->takedelaybranch)
|
|
{
|
|
//DevCon.Warning("VU1 - Branch/Jump in Delay Slot");
|
|
VU->branch = 1;
|
|
VU->branchpc = VU->delaybranchpc;
|
|
VU->takedelaybranch = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (VU->ebit > 0)
|
|
{
|
|
if (VU->ebit-- == 1)
|
|
{
|
|
VU->VIBackupCycles = 0;
|
|
_vuFlushAll(VU);
|
|
VU0.VI[REG_VPU_STAT].UL &= ~0x100;
|
|
vif1Regs.stat.VEW = false;
|
|
|
|
if(VU1.xgkickenable)
|
|
_vuXGKICKTransfer(0, true);
|
|
// In instant VU mode, VU1 goes WAY ahead of the CPU, making the XGKick fall way behind
|
|
// We also have some code to update it in VIF Unpacks too, since in some games (Aggressive Inline) overwrite the XGKick data
|
|
// VU currently flushes XGKICK on end, so this isn't needed, yet
|
|
if (INSTANT_VU1)
|
|
VU1.xgkicklastcycle = cpuRegs.cycle;
|
|
}
|
|
}
|
|
|
|
// Progress the write position of the FMAC pipeline by one place
|
|
if (uregs.pipe == VUPIPE_FMAC || lregs.pipe == VUPIPE_FMAC)
|
|
VU->fmacwritepos = (VU->fmacwritepos + 1) & 3;
|
|
}
|
|
|
|
void vu1Exec(VURegs* VU)
|
|
{
|
|
VU->cycle++;
|
|
_vu1Exec(VU);
|
|
|
|
if (VU->VI[0].UL != 0)
|
|
DbgCon.Error("VI[0] != 0!!!!\n");
|
|
if (VU->VF[0].f.x != 0.0f)
|
|
DbgCon.Error("VF[0].x != 0.0!!!!\n");
|
|
if (VU->VF[0].f.y != 0.0f)
|
|
DbgCon.Error("VF[0].y != 0.0!!!!\n");
|
|
if (VU->VF[0].f.z != 0.0f)
|
|
DbgCon.Error("VF[0].z != 0.0!!!!\n");
|
|
if (VU->VF[0].f.w != 1.0f)
|
|
DbgCon.Error("VF[0].w != 1.0!!!!\n");
|
|
}
|
|
|
|
InterpVU1 CpuIntVU1;
|
|
|
|
InterpVU1::InterpVU1()
|
|
{
|
|
m_Idx = 1;
|
|
IsInterpreter = true;
|
|
}
|
|
|
|
void InterpVU1::Reset()
|
|
{
|
|
DevCon.Warning("VU1 Int Reset");
|
|
VU1.fmacwritepos = 0;
|
|
VU1.fmacreadpos = 0;
|
|
VU1.fmaccount = 0;
|
|
VU1.ialuwritepos = 0;
|
|
VU1.ialureadpos = 0;
|
|
VU1.ialucount = 0;
|
|
}
|
|
|
|
void InterpVU1::SetStartPC(u32 startPC)
|
|
{
|
|
VU1.start_pc = startPC;
|
|
}
|
|
|
|
void InterpVU1::Step()
|
|
{
|
|
VU1.VI[REG_TPC].UL &= VU1_PROGMASK;
|
|
vu1Exec(&VU1);
|
|
}
|
|
|
|
void InterpVU1::Execute(u32 cycles)
|
|
{
|
|
const FPControlRegisterBackup fpcr_backup(EmuConfig.Cpu.VU1FPCR);
|
|
|
|
VU1.VI[REG_TPC].UL <<= 3;
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
vu1_trace::Entry* trace = vu1_trace::g_enabled.load(std::memory_order_relaxed)
|
|
? vu1_trace::begin('i', VU1.VI[REG_TPC].UL, cycles)
|
|
: nullptr;
|
|
#endif
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
// Live-game capture probe — mirror of the mVU JIT-side probe in
|
|
// mVUexecute (microVU-arm64.cpp). Lets a full-interpreter boot still dump
|
|
// VU programs from games that crash under JIT before reaching VU-heavy
|
|
// code, so the captures can be replayed through pcsx2-vurunner --diff.
|
|
// No-op unless PCSX2_VU_CAPTURE_DIR / PCSX2_VU_RANK_OUT is set. REG_TPC is
|
|
// already byte-PC here (shifted at function entry above); mask to match the JIT key.
|
|
vu_capture::MaybeCapture(1, VU1.VI[REG_TPC].UL & 0x3ff8, cycles,
|
|
(const u8*)VU1.Micro, VU1_PROGSIZE,
|
|
(const u8*)VU1.Mem, VU1_MEMSIZE, VU1);
|
|
#endif
|
|
|
|
u64 startcycles = VU1.cycle;
|
|
|
|
while ((VU1.cycle - startcycles) < cycles)
|
|
{
|
|
if (!(VU0.VI[REG_VPU_STAT].UL & 0x100))
|
|
{
|
|
if (VU1.branch == 1)
|
|
{
|
|
VU1.VI[REG_TPC].UL = VU1.branchpc;
|
|
VU1.branch = 0;
|
|
}
|
|
break;
|
|
}
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
// Capture xPC of the op we're ABOUT to execute, for divtrace alignment.
|
|
// REG_TPC was shifted to byte-PC at function entry,
|
|
// so it already holds the byte address — do NOT shift again.
|
|
const u32 dt_pre_xPC = VU1.VI[REG_TPC].UL;
|
|
#endif
|
|
Step();
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
// vudivtrace: snapshot VU1 architectural state after each interp op
|
|
// so the driver can compare against the JIT's per-op snapshot stream.
|
|
// Test-hook-only; release builds drop the per-op probe entirely.
|
|
if (mvu_divtrace::g_enabled.load(std::memory_order_relaxed)
|
|
&& mvu_divtrace::g_vu_index == 1
|
|
&& mvu_divtrace::g_interp_op_idx < mvu_divtrace::g_interp_fps.size())
|
|
{
|
|
const u32 idx = mvu_divtrace::g_interp_op_idx;
|
|
mvu_divtrace::g_interp_fps[idx] = mvu_divtrace::FingerprintRegs(vuRegs[1]);
|
|
mvu_divtrace::g_interp_xpc[idx] = dt_pre_xPC;
|
|
if (idx >= mvu_divtrace::g_full_lo && idx < mvu_divtrace::g_full_hi)
|
|
{
|
|
auto& snap = mvu_divtrace::g_interp_snaps[idx - mvu_divtrace::g_full_lo];
|
|
std::memcpy(&snap.regs, &vuRegs[1], sizeof(VURegs));
|
|
snap.meta_idx = 0xFFFF;
|
|
snap.pre_xPC = dt_pre_xPC;
|
|
}
|
|
++mvu_divtrace::g_interp_op_idx;
|
|
}
|
|
#endif
|
|
}
|
|
VU1.VI[REG_TPC].UL >>= 3;
|
|
VU1.nextBlockCycles = (VU1.cycle - cpuRegs.cycle) + 1;
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
vu1_trace::finish(trace);
|
|
#endif
|
|
}
|