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>
395 lines
11 KiB
C++
395 lines
11 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#pragma once
|
|
|
|
#include "Config.h"
|
|
#include "GS/MultiISA.h"
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
#include "vu_capture.h"
|
|
#endif
|
|
|
|
//------------------------------------------------------------------
|
|
// Dispatcher Functions
|
|
//------------------------------------------------------------------
|
|
static bool mvuNeedsFPCRUpdate(mV)
|
|
{
|
|
// always update on the vu1 thread
|
|
if (isVU1 && THREAD_VU1)
|
|
return true;
|
|
|
|
// otherwise only emit when it's different to the EE
|
|
return EmuConfig.Cpu.FPUFPCR.bitmask != (isVU0 ? EmuConfig.Cpu.VU0FPCR.bitmask : EmuConfig.Cpu.VU1FPCR.bitmask);
|
|
}
|
|
|
|
// Generates the code for entering/exit recompiled blocks
|
|
void mVUdispatcherAB(mV)
|
|
{
|
|
mVU.startFunct = xGetAlignedCallTarget();
|
|
|
|
{
|
|
xScopedStackFrame frame(false, true);
|
|
|
|
// = The caller has already put the needed parameters in ecx/edx:
|
|
if (!isVU1) xFastCall((void*)mVUexecuteVU0, arg1reg, arg2reg);
|
|
else xFastCall((void*)mVUexecuteVU1, arg1reg, arg2reg);
|
|
|
|
// Load VU's MXCSR state
|
|
if (mvuNeedsFPCRUpdate(mVU))
|
|
xLDMXCSR(ptr32[isVU0 ? &EmuConfig.Cpu.VU0FPCR.bitmask : &EmuConfig.Cpu.VU1FPCR.bitmask]);
|
|
|
|
// Load Regs
|
|
xMOVAPS (xmmT1, ptr128[&mVU.regs().VI[REG_P].UL]);
|
|
xMOVAPS (xmmPQ, ptr128[&mVU.regs().VI[REG_Q].UL]);
|
|
xMOVDZX (xmmT2, ptr32[&mVU.regs().pending_q]);
|
|
xSHUF.PS(xmmPQ, xmmT1, 0); // wzyx = PPQQ
|
|
//Load in other Q instance
|
|
xPSHUF.D(xmmPQ, xmmPQ, 0xe1);
|
|
xMOVSS(xmmPQ, xmmT2);
|
|
xPSHUF.D(xmmPQ, xmmPQ, 0xe1);
|
|
|
|
if (isVU1)
|
|
{
|
|
//Load in other P instance
|
|
xMOVDZX(xmmT2, ptr32[&mVU.regs().pending_p]);
|
|
xPSHUF.D(xmmPQ, xmmPQ, 0x1B);
|
|
xMOVSS(xmmPQ, xmmT2);
|
|
xPSHUF.D(xmmPQ, xmmPQ, 0x1B);
|
|
}
|
|
|
|
xMOVAPS(xmmT1, ptr128[&mVU.regs().micro_macflags]);
|
|
xMOVAPS(ptr128[mVU.macFlag], xmmT1);
|
|
|
|
|
|
xMOVAPS(xmmT1, ptr128[&mVU.regs().micro_clipflags]);
|
|
xMOVAPS(ptr128[mVU.clipFlag], xmmT1);
|
|
|
|
xMOV(gprF0, ptr32[&mVU.regs().micro_statusflags[0]]);
|
|
xMOV(gprF1, ptr32[&mVU.regs().micro_statusflags[1]]);
|
|
xMOV(gprF2, ptr32[&mVU.regs().micro_statusflags[2]]);
|
|
xMOV(gprF3, ptr32[&mVU.regs().micro_statusflags[3]]);
|
|
|
|
// Jump to Recompiled Code Block
|
|
xJMP(rax);
|
|
|
|
mVU.exitFunct = x86Ptr;
|
|
|
|
// Load EE's MXCSR state
|
|
if (mvuNeedsFPCRUpdate(mVU))
|
|
xLDMXCSR(ptr32[&EmuConfig.Cpu.FPUFPCR.bitmask]);
|
|
|
|
// = The first two DWORD or smaller arguments are passed in ECX and EDX registers;
|
|
// all other arguments are passed right to left.
|
|
if (!isVU1) xFastCall((void*)mVUcleanUpVU0);
|
|
else xFastCall((void*)mVUcleanUpVU1);
|
|
}
|
|
|
|
xRET();
|
|
|
|
Perf::any.Register(mVU.startFunct, static_cast<u32>(xGetPtr() - mVU.startFunct),
|
|
mVU.index ? "VU1StartFunc" : "VU0StartFunc");
|
|
}
|
|
|
|
// Generates the code for resuming/exit xgkick
|
|
void mVUdispatcherCD(mV)
|
|
{
|
|
mVU.startFunctXG = xGetAlignedCallTarget();
|
|
|
|
{
|
|
xScopedStackFrame frame(false, true);
|
|
|
|
// Load VU's MXCSR state
|
|
if (mvuNeedsFPCRUpdate(mVU))
|
|
xLDMXCSR(ptr32[isVU0 ? &EmuConfig.Cpu.VU0FPCR.bitmask : &EmuConfig.Cpu.VU1FPCR.bitmask]);
|
|
|
|
mVUrestoreRegs(mVU);
|
|
xMOV(gprF0, ptr32[&mVU.regs().micro_statusflags[0]]);
|
|
xMOV(gprF1, ptr32[&mVU.regs().micro_statusflags[1]]);
|
|
xMOV(gprF2, ptr32[&mVU.regs().micro_statusflags[2]]);
|
|
xMOV(gprF3, ptr32[&mVU.regs().micro_statusflags[3]]);
|
|
|
|
// Jump to Recompiled Code Block
|
|
xJMP(ptrNative[&mVU.resumePtrXG]);
|
|
|
|
mVU.exitFunctXG = x86Ptr;
|
|
|
|
// Backup Status Flag (other regs were backed up on xgkick)
|
|
xMOV(ptr32[&mVU.regs().micro_statusflags[0]], gprF0);
|
|
xMOV(ptr32[&mVU.regs().micro_statusflags[1]], gprF1);
|
|
xMOV(ptr32[&mVU.regs().micro_statusflags[2]], gprF2);
|
|
xMOV(ptr32[&mVU.regs().micro_statusflags[3]], gprF3);
|
|
|
|
// Load EE's MXCSR state
|
|
if (mvuNeedsFPCRUpdate(mVU))
|
|
xLDMXCSR(ptr32[&EmuConfig.Cpu.FPUFPCR.bitmask]);
|
|
}
|
|
|
|
xRET();
|
|
|
|
Perf::any.Register(mVU.startFunctXG, static_cast<u32>(xGetPtr() - mVU.startFunctXG),
|
|
mVU.index ? "VU1StartFuncXG" : "VU0StartFuncXG");
|
|
}
|
|
|
|
static void mVUGenerateWaitMTVU(mV)
|
|
{
|
|
mVU.waitMTVU = xGetAlignedCallTarget();
|
|
|
|
int num_xmms = 0, num_gprs = 0;
|
|
|
|
for (int i = 0; i < static_cast<int>(iREGCNT_GPR); i++)
|
|
{
|
|
if (!xRegister32::IsCallerSaved(i) || i == rsp.GetId())
|
|
continue;
|
|
|
|
// T1 often contains the address we're loading when waiting for VU1.
|
|
// T2 isn't used until afterwards, so don't bother saving it.
|
|
if (i == gprT2.GetId())
|
|
continue;
|
|
|
|
xPUSH(xRegister64(i));
|
|
num_gprs++;
|
|
}
|
|
|
|
for (int i = 0; i < static_cast<int>(iREGCNT_XMM); i++)
|
|
{
|
|
if (!xRegisterSSE::IsCallerSaved(i))
|
|
continue;
|
|
|
|
num_xmms++;
|
|
}
|
|
|
|
// We need 16 byte alignment on the stack.
|
|
// Since the stack is unaligned at entry to this function, we add 8 when it's even, not odd.
|
|
const int stack_size = (num_xmms * sizeof(u128)) + ((~num_gprs & 1) * sizeof(u64)) + SHADOW_STACK_SIZE;
|
|
int stack_offset = SHADOW_STACK_SIZE;
|
|
|
|
if (stack_size > 0)
|
|
{
|
|
xSUB(rsp, stack_size);
|
|
for (int i = 0; i < static_cast<int>(iREGCNT_XMM); i++)
|
|
{
|
|
if (!xRegisterSSE::IsCallerSaved(i))
|
|
continue;
|
|
|
|
xMOVAPS(ptr128[rsp + stack_offset], xRegisterSSE(i));
|
|
stack_offset += sizeof(u128);
|
|
}
|
|
}
|
|
|
|
xFastCall((void*)mVUwaitMTVU);
|
|
|
|
stack_offset = (num_xmms - 1) * sizeof(u128) + SHADOW_STACK_SIZE;
|
|
for (int i = static_cast<int>(iREGCNT_XMM - 1); i >= 0; i--)
|
|
{
|
|
if (!xRegisterSSE::IsCallerSaved(i))
|
|
continue;
|
|
|
|
xMOVAPS(xRegisterSSE(i), ptr128[rsp + stack_offset]);
|
|
stack_offset -= sizeof(u128);
|
|
}
|
|
xADD(rsp, stack_size);
|
|
|
|
for (int i = static_cast<int>(iREGCNT_GPR - 1); i >= 0; i--)
|
|
{
|
|
if (!xRegister32::IsCallerSaved(i) || i == rsp.GetId())
|
|
continue;
|
|
|
|
if (i == gprT2.GetId())
|
|
continue;
|
|
|
|
xPOP(xRegister64(i));
|
|
}
|
|
|
|
xRET();
|
|
|
|
Perf::any.Register(mVU.waitMTVU, static_cast<u32>(xGetPtr() - mVU.waitMTVU),
|
|
mVU.index ? "VU1WaitMTVU" : "VU0WaitMTVU");
|
|
}
|
|
|
|
static void mVUGenerateCopyPipelineState(mV)
|
|
{
|
|
mVU.copyPLState = xGetAlignedCallTarget();
|
|
|
|
xLoadFarAddr(rdx, reinterpret_cast<u8*>(&mVU.prog.lpState));
|
|
|
|
if (g_cpu.vectorISA >= ProcessorFeatures::VectorISA::AVX)
|
|
{
|
|
xMOVAPS(ymm0, ptr[rax]);
|
|
xMOVAPS(ymm1, ptr[rax + 32u]);
|
|
xMOVAPS(ymm2, ptr[rax + 64u]);
|
|
|
|
xMOVUPS(ptr[rdx], ymm0);
|
|
xMOVUPS(ptr[rdx + 32u], ymm1);
|
|
xMOVUPS(ptr[rdx + 64u], ymm2);
|
|
|
|
xVZEROUPPER();
|
|
}
|
|
else
|
|
{
|
|
xMOVAPS(xmm0, ptr[rax]);
|
|
xMOVAPS(xmm1, ptr[rax + 16u]);
|
|
xMOVAPS(xmm2, ptr[rax + 32u]);
|
|
xMOVAPS(xmm3, ptr[rax + 48u]);
|
|
xMOVAPS(xmm4, ptr[rax + 64u]);
|
|
xMOVAPS(xmm5, ptr[rax + 80u]);
|
|
|
|
xMOVUPS(ptr[rdx], xmm0);
|
|
xMOVUPS(ptr[rdx + 16u], xmm1);
|
|
xMOVUPS(ptr[rdx + 32u], xmm2);
|
|
xMOVUPS(ptr[rdx + 48u], xmm3);
|
|
xMOVUPS(ptr[rdx + 64u], xmm4);
|
|
xMOVUPS(ptr[rdx + 80u], xmm5);
|
|
}
|
|
|
|
xRET();
|
|
|
|
Perf::any.Register(mVU.copyPLState, static_cast<u32>(xGetPtr() - mVU.copyPLState),
|
|
mVU.index ? "VU1CopyPLState" : "VU0CopyPLState");
|
|
}
|
|
|
|
//------------------------------------------------------------------
|
|
// Micro VU - Custom Quick Search
|
|
//------------------------------------------------------------------
|
|
|
|
// Generates a custom optimized block-search function
|
|
// Note: Structs must be 16-byte aligned! (GCC doesn't guarantee this)
|
|
static void mVUGenerateCompareState(mV)
|
|
{
|
|
mVU.compareStateF = xGetAlignedCallTarget();
|
|
|
|
if (g_cpu.vectorISA < ProcessorFeatures::VectorISA::AVX2)
|
|
{
|
|
xMOVAPS (xmm0, ptr32[arg1reg]);
|
|
xPCMP.EQD(xmm0, ptr32[arg2reg]);
|
|
xMOVAPS (xmm1, ptr32[arg1reg + 0x10]);
|
|
xPCMP.EQD(xmm1, ptr32[arg2reg + 0x10]);
|
|
xPAND (xmm0, xmm1);
|
|
|
|
xMOVMSKPS(eax, xmm0);
|
|
xXOR (eax, 0xf);
|
|
xForwardJNZ8 exitPoint;
|
|
|
|
xMOVAPS (xmm0, ptr32[arg1reg + 0x20]);
|
|
xPCMP.EQD(xmm0, ptr32[arg2reg + 0x20]);
|
|
xMOVAPS (xmm1, ptr32[arg1reg + 0x30]);
|
|
xPCMP.EQD(xmm1, ptr32[arg2reg + 0x30]);
|
|
xPAND (xmm0, xmm1);
|
|
|
|
xMOVAPS (xmm1, ptr32[arg1reg + 0x40]);
|
|
xPCMP.EQD(xmm1, ptr32[arg2reg + 0x40]);
|
|
xMOVAPS (xmm2, ptr32[arg1reg + 0x50]);
|
|
xPCMP.EQD(xmm2, ptr32[arg2reg + 0x50]);
|
|
xPAND (xmm1, xmm2);
|
|
xPAND (xmm0, xmm1);
|
|
|
|
xMOVMSKPS(eax, xmm0);
|
|
xXOR(eax, 0xf);
|
|
|
|
exitPoint.SetTarget();
|
|
}
|
|
else
|
|
{
|
|
// We have to use unaligned loads here, because the blocks are only 16 byte aligned.
|
|
xMOVUPS(ymm0, ptr[arg1reg]);
|
|
xPCMP.EQD(ymm0, ymm0, ptr[arg2reg]);
|
|
xPMOVMSKB(eax, ymm0);
|
|
xXOR(eax, 0xffffffff);
|
|
xForwardJNZ8 exitPoint;
|
|
|
|
xMOVUPS(ymm0, ptr[arg1reg + 0x20]);
|
|
xMOVUPS(ymm1, ptr[arg1reg + 0x40]);
|
|
xPCMP.EQD(ymm0, ymm0, ptr[arg2reg + 0x20]);
|
|
xPCMP.EQD(ymm1, ymm1, ptr[arg2reg + 0x40]);
|
|
xPAND(ymm0, ymm0, ymm1);
|
|
|
|
xPMOVMSKB(eax, ymm0);
|
|
xNOT(eax);
|
|
|
|
exitPoint.SetTarget();
|
|
xVZEROUPPER();
|
|
}
|
|
|
|
xRET();
|
|
}
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
// Execution Functions
|
|
//------------------------------------------------------------------
|
|
|
|
// Executes for number of cycles
|
|
_mVUt void* mVUexecute(u32 startPC, u32 cycles)
|
|
{
|
|
|
|
microVU& mVU = mVUx;
|
|
u32 vuLimit = vuIndex ? 0x3ff8 : 0xff8;
|
|
if (startPC > vuLimit + 7)
|
|
{
|
|
DevCon.Warning("microVU%x Warning: startPC = 0x%x, cycles = 0x%x", vuIndex, startPC, cycles);
|
|
}
|
|
|
|
mVU.cycles = cycles;
|
|
mVU.totalCycles = cycles;
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
// Records the microprogram plus the entry register/memory state at dispatch
|
|
// so the captured program can be replayed offline by the capture test tooling.
|
|
vu_capture::MaybeCapture(static_cast<int>(vuIndex), startPC & vuLimit, cycles,
|
|
mVU.regs().Micro, mVU.microMemSize,
|
|
mVU.regs().Mem, mVU.microMemSize,
|
|
mVU.regs());
|
|
#endif
|
|
|
|
xSetTextPtr(mVU.textPtr());
|
|
xSetPtr(mVU.prog.x86ptr); // Set x86ptr to where last program left off
|
|
return mVUsearchProg<vuIndex>(startPC & vuLimit, (uptr)&mVU.prog.lpState); // Find and set correct program
|
|
}
|
|
|
|
//------------------------------------------------------------------
|
|
// Cleanup Functions
|
|
//------------------------------------------------------------------
|
|
|
|
_mVUt void mVUcleanUp()
|
|
{
|
|
microVU& mVU = mVUx;
|
|
|
|
mVU.prog.x86ptr = x86Ptr;
|
|
|
|
if ((xGetPtr() < mVU.prog.x86start) || (xGetPtr() >= mVU.prog.x86end))
|
|
{
|
|
Console.WriteLn(vuIndex ? Color_Orange : Color_Magenta, "microVU%d: Program cache limit reached.", mVU.index);
|
|
mVUreset(mVU, false);
|
|
}
|
|
|
|
mVU.cycles = mVU.totalCycles - std::max(0, mVU.cycles);
|
|
mVU.regs().cycle += mVU.cycles;
|
|
|
|
if (!vuIndex || !THREAD_VU1)
|
|
{
|
|
u32 cycles_passed = std::min(mVU.cycles, 3000) * EmuConfig.Speedhacks.EECycleSkip;
|
|
if (cycles_passed > 0)
|
|
{
|
|
s64 vu0_offset = VU0.cycle - cpuRegs.cycle;
|
|
cpuRegs.cycle += cycles_passed;
|
|
|
|
// VU0 needs to stay in sync with the CPU otherwise things get messy
|
|
// So we need to adjust when VU1 skips cycles also
|
|
if (!vuIndex)
|
|
VU0.cycle = cpuRegs.cycle + vu0_offset;
|
|
else
|
|
VU0.cycle += cycles_passed;
|
|
}
|
|
}
|
|
mVU.profiler.Print();
|
|
}
|
|
|
|
//------------------------------------------------------------------
|
|
// Caller Functions
|
|
//------------------------------------------------------------------
|
|
|
|
void* mVUexecuteVU0(u32 startPC, u32 cycles) { return mVUexecute<0>(startPC, cycles); }
|
|
void* mVUexecuteVU1(u32 startPC, u32 cycles) { return mVUexecute<1>(startPC, cycles); }
|
|
void mVUcleanUpVU0() { mVUcleanUp<0>(); }
|
|
void mVUcleanUpVU1() { mVUcleanUp<1>(); }
|