mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
CTC2's default arm stored all 32 bits of the source GPR, so two cases diverged from the console capture on the interpreter only: vi01-vi15 are 16-bit integer registers and read back as 0000FFFF after a write of FFFFFFFF; the interpreter kept the full word. vi16 STATUS has a writable sticky field of 0xFC0. The 0x3F current-flag field belongs to the FMAC pipeline and survives a CTC2, so a write of FFFFFFFF reads back as 00000FC0 with the live cause nibble intact. Both recompilers already did this (microVU_Macro.inl recCTC2 emits a 16-bit store and the 0xFC0/0x3F merge; iCOP2-arm64.cpp recCOP2_CTC2 emits Strh and the same merge). The recompilers additionally broadcast the denormalized STATUS into micro_statusflags; the interpreter does not need to, because vu0ExecMicro re-copies VI[REG_STATUS_FLAG] into the micro instances at program start -- COP2 cannot execute while a microprogram is running, which is the comment's stated reason for doing the copy there. CLIP, I and Q still land in the default arm and stay 32-bit. vi18/vi27/vi31 are left recorded in kCtc2Divergences: CLIP is 24-bit and CMSAR0/CMSAR1 are 16-bit on silicon, and REG_CMSAR1 kicks VU1 without storing at all. Those are wrong on BOTH engines, so DISABLED_AllCtc2WriteMasksMatchConsole stays disabled -- its failure count drops from 8 to 6. The STATUS mask also graduates three interpreter rows in the sticky suite, which is what "clears the sticky field and leaves the cause nibble standing" means in practice. Idea by pstef.
281 lines
8.5 KiB
C++
281 lines
8.5 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
/* TODO
|
|
-Fix the flags Proper as they aren't handle now..
|
|
-Add BC Table opcodes
|
|
-Add Interlock in QMFC2,QMTC2,CFC2,CTC2
|
|
-Finish instruction set
|
|
-Bug Fixes!!!
|
|
*/
|
|
|
|
#include "Common.h"
|
|
|
|
#include <cmath>
|
|
|
|
#include "R5900OpcodeTables.h"
|
|
#include "VUmicro.h"
|
|
#include "Vif_Dma.h"
|
|
#include "MTVU.h"
|
|
|
|
#define _Ft_ _Rt_
|
|
#define _Fs_ _Rd_
|
|
#define _Fd_ _Sa_
|
|
|
|
#define _Fsf_ ((cpuRegs.code >> 21) & 0x03)
|
|
#define _Ftf_ ((cpuRegs.code >> 23) & 0x03)
|
|
|
|
using namespace R5900;
|
|
|
|
// ---- pcsx2-eerunner --vu0diff per-COP2-read capture hooks (DIAGNOSTIC) ----------
|
|
// Null in production (zero overhead). pcsx2-eerunner installs a sink so that, with
|
|
// the EE pinned to interp in both passes, every EE-interpreter COP2 *read* (QMFC2
|
|
// reads VF[fs], CFC2 reads VI[fs]) of a freshly-run VU0 program is recorded in
|
|
// execution order. Diffing the VU0-jit pass vs the VU0-interp pass read-streams
|
|
// pins the FIRST VU0 program output the micro JIT computes differently from the
|
|
// interpreter — a live, in-context VU0-jit-vs-interp value diff the offline
|
|
// capture-replay harness can't produce (no real EE<->VU0 interleave). op:
|
|
// 0=QMFC2(VF), 1=CFC2(VI). NOTE: single-arch jit-vs-interp; valid for an
|
|
// arithmetic value bug, but a pipeline/flag/cycle-instance divergence here is
|
|
// usually shared-with-x86 noise —
|
|
// confirm arch-specificity with an arm64-jit-vs-x86-jit diff before trusting it.
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
typedef void (*Cop2ReadHook)(u32 ee_pc, u32 op, u32 fs, const u32* lanes);
|
|
Cop2ReadHook g_cop2ReadHook = nullptr;
|
|
|
|
// Companion: VU0 pipeline/flag state at the read (TPC=last micro PC, Q=DIV result,
|
|
// MAC/STATUS/CLIP flags). Lets the harness tell whether a divergent VF read is driven
|
|
// by a wrong Q (broadcast-scalar pipeline) or a flag-instance handoff.
|
|
typedef void (*Cop2StateHook)(u32 tpc, u32 q, u32 mac, u32 status, u32 clip);
|
|
Cop2StateHook g_cop2StateHook = nullptr;
|
|
#endif
|
|
|
|
void COP2_BC2() { Int_COP2BC2PrintTable[_Rt_]();}
|
|
void COP2_SPECIAL() { _vu0FinishMicro(); Int_COP2SPECIAL1PrintTable[_Funct_]();}
|
|
|
|
void COP2_SPECIAL2() {
|
|
Int_COP2SPECIAL2PrintTable[(cpuRegs.code & 0x3) | ((cpuRegs.code >> 4) & 0x7c)]();
|
|
}
|
|
|
|
void COP2_Unknown()
|
|
{
|
|
CPU_LOG("Unknown COP2 opcode called");
|
|
}
|
|
|
|
//****************************************************************************
|
|
|
|
__fi void _vu0run(bool breakOnMbit, bool addCycles, bool sync_only, bool runAhead) {
|
|
|
|
if (!(VU0.VI[REG_VPU_STAT].UL & 1)) return;
|
|
|
|
//VU0 is ahead of the EE and M-Bit is already encountered, so no need to wait for it, just catch up the EE
|
|
if ((VU0.flags & VUFLAG_MFLAGSET) && breakOnMbit && (s64)(cpuRegs.cycle - VU0.cycle) <= 0)
|
|
{
|
|
cpuRegs.cycle = VU0.cycle;
|
|
return;
|
|
}
|
|
|
|
if(!EmuConfig.Cpu.Recompiler.EnableEE)
|
|
intUpdateCPUCycles();
|
|
|
|
u64 startcycle = cpuRegs.cycle;
|
|
s32 runCycles = 0x7fffffff;
|
|
|
|
if (sync_only)
|
|
{
|
|
runCycles = (s64)(cpuRegs.cycle - VU0.cycle);
|
|
|
|
if (runCycles < 0)
|
|
return;
|
|
|
|
// Run-ahead (non-interlocked COP2 sync only): dispatching a tiny VU0
|
|
// catch-up (e.g. 3 cycles) pays the full mVU dispatch envelope to run
|
|
// almost nothing. When the sync isn't interlocked it's fine to overshoot
|
|
// the EE by a few cycles — the next sync sees a negative delta and
|
|
// no-ops until the EE catches back up, so several round-trips collapse
|
|
// into one. Mirrors upstream CalculateMinRunCycles(delta, /*accurate*/
|
|
// false) (commit 6dc5087cb "VU: Run sync ahead on small blocks"); the
|
|
// arm64 COP2 path syncs via vu0Sync rather than ExecuteBlockJIT, so the
|
|
// floor lives here instead.
|
|
if (runAhead && runCycles < 16)
|
|
runCycles = 16;
|
|
}
|
|
|
|
do { // Run VU until it finishes or M-Bit
|
|
CpuVU0->Execute(runCycles);
|
|
} while ((VU0.VI[REG_VPU_STAT].UL & 1) // E-bit Termination
|
|
&& !sync_only && (!breakOnMbit || (!(VU0.flags & VUFLAG_MFLAGSET) && (s32)(cpuRegs.cycle - VU0.cycle) > 0))); // M-bit Break
|
|
|
|
// Add cycles if called from EE's COP2
|
|
if (addCycles)
|
|
{
|
|
cpuRegs.cycle += (VU0.cycle - startcycle);
|
|
CpuVU1->ExecuteBlock(0); // Catch up VU1 as it's likely fallen behind
|
|
|
|
if(VU0.VI[REG_VPU_STAT].UL & 1)
|
|
cpuSetNextEventDelta(4);
|
|
}
|
|
}
|
|
|
|
void _vu0WaitMicro() { _vu0run(1, 1, 0, 0); } // Runs VU0 Micro Until E-bit or M-Bit End
|
|
void _vu0FinishMicro() { _vu0run(0, 1, 0, 0); } // Runs VU0 Micro Until E-Bit End
|
|
void vu0Finish() { _vu0run(0, 0, 0, 0); } // Runs VU0 Micro Until E-Bit End (doesn't stall EE)
|
|
void vu0Sync() { _vu0run(0, 0, 1, 0); } // Runs VU0 until it catches up (exact)
|
|
void vu0SyncRunAhead() { _vu0run(0, 0, 1, 1); } // Catches up, but runs a 16-cycle minimum (non-interlocked)
|
|
|
|
namespace R5900 {
|
|
namespace Interpreter{
|
|
namespace OpcodeImpl
|
|
{
|
|
void LQC2() {
|
|
vu0Sync();
|
|
u32 addr = cpuRegs.GPR.r[_Rs_].UL[0] + (s16)cpuRegs.code;
|
|
if (_Ft_) {
|
|
memRead128(addr, VU0.VF[_Ft_].UQ);
|
|
} else {
|
|
u128 val;
|
|
memRead128(addr, val);
|
|
}
|
|
}
|
|
|
|
// Asadr.Changed
|
|
//TODO: check this
|
|
// HUH why ? doesn't make any sense ...
|
|
void SQC2() {
|
|
vu0Sync();
|
|
u32 addr = _Imm_ + cpuRegs.GPR.r[_Rs_].UL[0];
|
|
memWrite128(addr, VU0.VF[_Ft_].UQ);
|
|
}
|
|
}}}
|
|
|
|
|
|
void QMFC2() {
|
|
vu0Sync();
|
|
|
|
if (cpuRegs.code & 1) {
|
|
_vu0FinishMicro();
|
|
}
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
if (g_cop2ReadHook && g_cop2StateHook) // diagnostic hooks (recompiler test harness); null in production
|
|
{
|
|
g_cop2ReadHook(cpuRegs.pc, 0, _Fs_, VU0.VF[_Fs_].UL);
|
|
g_cop2StateHook(VU0.VI[REG_TPC].UL, VU0.VI[REG_Q].UL, VU0.VI[REG_MAC_FLAG].UL,
|
|
VU0.VI[REG_STATUS_FLAG].UL, VU0.VI[REG_CLIP_FLAG].UL);
|
|
}
|
|
#endif
|
|
|
|
if (_Rt_ == 0) return;
|
|
cpuRegs.GPR.r[_Rt_].UD[0] = VU0.VF[_Fs_].UD[0];
|
|
cpuRegs.GPR.r[_Rt_].UD[1] = VU0.VF[_Fs_].UD[1];
|
|
}
|
|
|
|
void QMTC2() {
|
|
vu0Sync();
|
|
|
|
if (cpuRegs.code & 1) {
|
|
_vu0WaitMicro();
|
|
}
|
|
|
|
if (_Fs_ == 0) return;
|
|
VU0.VF[_Fs_].UD[0] = cpuRegs.GPR.r[_Rt_].UD[0];
|
|
VU0.VF[_Fs_].UD[1] = cpuRegs.GPR.r[_Rt_].UD[1];
|
|
}
|
|
|
|
void CFC2() {
|
|
vu0Sync();
|
|
|
|
if (cpuRegs.code & 1) {
|
|
_vu0FinishMicro();
|
|
}
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
if (g_cop2ReadHook && g_cop2StateHook) // diagnostic hooks (recompiler test harness); null in production
|
|
{
|
|
g_cop2ReadHook(cpuRegs.pc, 1, _Fs_, &VU0.VI[_Fs_].UL);
|
|
g_cop2StateHook(VU0.VI[REG_TPC].UL, VU0.VI[REG_Q].UL, VU0.VI[REG_MAC_FLAG].UL,
|
|
VU0.VI[REG_STATUS_FLAG].UL, VU0.VI[REG_CLIP_FLAG].UL);
|
|
}
|
|
#endif
|
|
|
|
if (_Rt_ == 0) return;
|
|
|
|
if (_Fs_ == REG_R)
|
|
cpuRegs.GPR.r[_Rt_].UL[0] = VU0.VI[REG_R].UL & 0x7FFFFF;
|
|
else
|
|
{
|
|
cpuRegs.GPR.r[_Rt_].UL[0] = VU0.VI[_Fs_].UL;
|
|
|
|
if (VU0.VI[_Fs_].UL & 0x80000000)
|
|
cpuRegs.GPR.r[_Rt_].UL[1] = 0xffffffff;
|
|
else
|
|
cpuRegs.GPR.r[_Rt_].UL[1] = 0;
|
|
}
|
|
|
|
}
|
|
|
|
void CTC2() {
|
|
vu0Sync();
|
|
|
|
if (cpuRegs.code & 1) {
|
|
_vu0WaitMicro();
|
|
}
|
|
|
|
if (_Fs_ == 0) return;
|
|
|
|
switch(_Fs_) {
|
|
case REG_MAC_FLAG: // read-only
|
|
case REG_TPC: // read-only
|
|
case REG_VPU_STAT: // read-only
|
|
break;
|
|
case REG_R:
|
|
VU0.VI[REG_R].UL = ((cpuRegs.GPR.r[_Rt_].UL[0] & 0x7FFFFF) | 0x3F800000);
|
|
break;
|
|
case REG_FBRST:
|
|
VU0.VI[REG_FBRST].UL = cpuRegs.GPR.r[_Rt_].UL[0] & 0x0C0C;
|
|
if (cpuRegs.GPR.r[_Rt_].UL[0] & 0x1) { // VU0 Force Break
|
|
Console.Error("fixme: VU0 Force Break");
|
|
}
|
|
if (cpuRegs.GPR.r[_Rt_].UL[0] & 0x2) { // VU0 Reset
|
|
//Console.WriteLn("fixme: VU0 Reset");
|
|
vu0ResetRegs();
|
|
}
|
|
if (cpuRegs.GPR.r[_Rt_].UL[0] & 0x100) { // VU1 Force Break
|
|
Console.Error("fixme: VU1 Force Break");
|
|
}
|
|
if (cpuRegs.GPR.r[_Rt_].UL[0] & 0x200) { // VU1 Reset
|
|
// Console.WriteLn("fixme: VU1 Reset");
|
|
vu1ResetRegs();
|
|
}
|
|
break;
|
|
case REG_CMSAR1: // REG_CMSAR1
|
|
vu1Finish(true);
|
|
vu1ExecMicro(cpuRegs.GPR.r[_Rt_].US[0]); // Execute VU1 Micro SubRoutine
|
|
break;
|
|
case REG_STATUS_FLAG:
|
|
// Only the sticky field (0xFC0) is writable; the current-flag
|
|
// field (0x3F) belongs to the FMAC pipeline and survives the
|
|
// write. Both recompilers do exactly this (microVU_Macro.inl
|
|
// recCTC2, iCOP2-arm64.cpp recCOP2_CTC2). They additionally
|
|
// broadcast the denormalized value into micro_statusflags; the
|
|
// interpreter does not need to, because vu0ExecMicro re-copies
|
|
// VI[REG_STATUS_FLAG] into the micro instances at program start.
|
|
VU0.VI[REG_STATUS_FLAG].UL = (VU0.VI[REG_STATUS_FLAG].UL & 0x3F) |
|
|
(cpuRegs.GPR.r[_Rt_].UL[0] & 0xFC0);
|
|
break;
|
|
case REG_CLIP_FLAG:
|
|
VU0.clipflag = cpuRegs.GPR.r[_Rt_].UL[0];
|
|
[[fallthrough]];
|
|
default:
|
|
// VI01-VI15 are 16-bit integer registers, so only the low half is
|
|
// stored (the recompilers emit a 16-bit store here too). The
|
|
// control registers that land in this arm — CLIP, I, Q and the
|
|
// rest — are full 32-bit words.
|
|
if (_Fs_ < REG_STATUS_FLAG)
|
|
VU0.VI[_Fs_].US[0] = cpuRegs.GPR.r[_Rt_].US[0];
|
|
else
|
|
VU0.VI[_Fs_].UL = cpuRegs.GPR.r[_Rt_].UL[0];
|
|
break;
|
|
}
|
|
}
|