2026-07-19 07:37:15 -07:00
|
|
|
// SPDX-FileCopyrightText: 2026 yaps2 Dev Team
|
2026-06-20 20:27:55 -07:00
|
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include "Config.h"
|
|
|
|
|
#include "common/FileSystem.h"
|
|
|
|
|
#include "common/Path.h"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Messages Called at Execution Time
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
static inline void mVUbadOp0 (u32 prog, u32 pc) { Console.Error("microVU0 Warning: Bad opcode [%04x] [%03d]", pc, prog); }
|
|
|
|
|
static inline void mVUbadOp1 (u32 prog, u32 pc) { Console.Error("microVU1 Warning: Bad opcode [%04x] [%03d]", pc, prog); }
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Program Range Checking
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
__fi void mVUcheckIsSame(mV)
|
|
|
|
|
{
|
|
|
|
|
if (mVU.prog.isSame == -1)
|
|
|
|
|
mVU.prog.isSame = !memcmp((u8*)mVUcurProg.data, mVU.regs().Micro, mVU.microMemSize);
|
|
|
|
|
if (mVU.prog.isSame == 0)
|
|
|
|
|
{
|
|
|
|
|
mVUcacheProg(mVU, *mVU.prog.cur);
|
|
|
|
|
mVU.prog.isSame = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void mVUsetupRange(microVU& mVU, s32 pc, bool isStartPC)
|
|
|
|
|
{
|
|
|
|
|
std::deque<microRange>*& ranges = mVUcurProg.ranges;
|
|
|
|
|
if (pc > (s64)mVU.microMemSize)
|
|
|
|
|
{
|
|
|
|
|
Console.Error("microVU%d: PC outside of VU memory PC=0x%04x", mVU.index, pc);
|
|
|
|
|
pxFail("microVU: PC out of VU memory");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const s32 cur_pc = (!isStartPC && mVUrange.start > pc && pc == 0) ? mVU.microMemSize : pc;
|
|
|
|
|
|
|
|
|
|
if (isStartPC)
|
|
|
|
|
{
|
|
|
|
|
for (auto it = ranges->begin(); it != ranges->end(); ++it)
|
|
|
|
|
{
|
|
|
|
|
if ((cur_pc >= it->start) && (cur_pc <= it->end))
|
|
|
|
|
{
|
|
|
|
|
if (it->start != it->end)
|
|
|
|
|
{
|
|
|
|
|
microRange mRange = {it->start, it->end};
|
|
|
|
|
ranges->erase(it);
|
|
|
|
|
ranges->push_front(mRange);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (mVUrange.end >= cur_pc)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (doWholeProgCompare)
|
|
|
|
|
mVUcheckIsSame(mVU);
|
|
|
|
|
|
|
|
|
|
if (isStartPC)
|
|
|
|
|
{
|
|
|
|
|
microRange mRange = {cur_pc, -1};
|
|
|
|
|
ranges->push_front(mRange);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mVUrange.start <= cur_pc)
|
|
|
|
|
{
|
|
|
|
|
mVUrange.end = cur_pc;
|
|
|
|
|
s32 rStart = mVUrange.start;
|
|
|
|
|
s32 rEnd = mVUrange.end;
|
|
|
|
|
for (auto it = ranges->begin() + 1; it != ranges->end();)
|
|
|
|
|
{
|
|
|
|
|
if (((it->start >= rStart) && (it->start <= rEnd)) ||
|
|
|
|
|
((it->end >= rStart) && (it->end <= rEnd)))
|
|
|
|
|
{
|
|
|
|
|
mVUrange.start = rStart = std::min(it->start, rStart);
|
|
|
|
|
mVUrange.end = rEnd = std::max(it->end, rEnd);
|
|
|
|
|
it = ranges->erase(it);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
it++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mVUrange.end = mVU.microMemSize;
|
|
|
|
|
microRange mRange = {0, cur_pc};
|
|
|
|
|
ranges->push_front(mRange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!doWholeProgCompare)
|
|
|
|
|
mVUcacheProg(mVU, *mVU.prog.cur);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Pipeline State Helpers (platform-independent)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
__fi u8 optimizeReg(u8 rState) { return (rState == 1) ? 0 : rState; }
|
|
|
|
|
__fi u8 calcCycles(u8 reg, u8 x) { return ((reg > x) ? (reg - x) : 0); }
|
|
|
|
|
__fi u8 tCycles(u8 dest, u8 src) { return std::max(dest, src); }
|
|
|
|
|
__fi void incP(mV) { mVU.p ^= 1; }
|
|
|
|
|
__fi void incQ(mV) { mVU.q ^= 1; }
|
|
|
|
|
|
|
|
|
|
// Optimizes the end pipeline state — collapses cycles-remaining==1 to 0, since
|
|
|
|
|
// mVU's block loop auto-decrements at entry so 1 is equivalent to 0. Without
|
|
|
|
|
// this, pipeline-state-hashed blocks get distinct variants for every cycle
|
|
|
|
|
// delta, exploding the block cache. Ported verbatim from x86 microVU_Compile.inl.
|
|
|
|
|
void mVUoptimizePipeState(mV)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < 32; i++)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.VF[i].x = optimizeReg(mVUregs.VF[i].x);
|
|
|
|
|
mVUregs.VF[i].y = optimizeReg(mVUregs.VF[i].y);
|
|
|
|
|
mVUregs.VF[i].z = optimizeReg(mVUregs.VF[i].z);
|
|
|
|
|
mVUregs.VF[i].w = optimizeReg(mVUregs.VF[i].w);
|
|
|
|
|
}
|
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.VI[i] = optimizeReg(mVUregs.VI[i]);
|
|
|
|
|
}
|
|
|
|
|
if (mVUregs.q) { mVUregs.q = optimizeReg(mVUregs.q); if (!mVUregs.q) { incQ(mVU); } }
|
|
|
|
|
if (mVUregs.p) { mVUregs.p = optimizeReg(mVUregs.p); if (!mVUregs.p) { incP(mVU); } }
|
|
|
|
|
mVUregs.r = 0; // No stalls on R-reg — safe to discard.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Advance pipeline cycles by x instructions. Ported verbatim from x86.
|
|
|
|
|
void mVUincCycles(mV, int x)
|
|
|
|
|
{
|
|
|
|
|
mVUcycles += x;
|
|
|
|
|
// VF[0] is a constant (0,0,0,1) — skip.
|
|
|
|
|
for (int z = 31; z > 0; z--)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.VF[z].x = calcCycles(mVUregs.VF[z].x, x);
|
|
|
|
|
mVUregs.VF[z].y = calcCycles(mVUregs.VF[z].y, x);
|
|
|
|
|
mVUregs.VF[z].z = calcCycles(mVUregs.VF[z].z, x);
|
|
|
|
|
mVUregs.VF[z].w = calcCycles(mVUregs.VF[z].w, x);
|
|
|
|
|
}
|
|
|
|
|
// VI[0] is constant (0) — skip.
|
|
|
|
|
for (int z = 15; z > 0; z--)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.VI[z] = calcCycles(mVUregs.VI[z], x);
|
|
|
|
|
}
|
|
|
|
|
if (mVUregs.q)
|
|
|
|
|
{
|
|
|
|
|
if (mVUregs.q > 4)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.q = calcCycles(mVUregs.q, x);
|
|
|
|
|
if (mVUregs.q <= 4)
|
|
|
|
|
mVUinfo.doDivFlag = 1;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mVUregs.q = calcCycles(mVUregs.q, x);
|
|
|
|
|
}
|
|
|
|
|
if (!mVUregs.q)
|
|
|
|
|
incQ(mVU);
|
|
|
|
|
}
|
|
|
|
|
if (mVUregs.p)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.p = calcCycles(mVUregs.p, x);
|
|
|
|
|
if (!mVUregs.p || mVUregsTemp.p)
|
|
|
|
|
incP(mVU);
|
|
|
|
|
}
|
|
|
|
|
if (mVUregs.xgkick)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.xgkick = calcCycles(mVUregs.xgkick, x);
|
|
|
|
|
if (!mVUregs.xgkick)
|
|
|
|
|
{
|
|
|
|
|
mVUinfo.doXGKICK = 1;
|
|
|
|
|
mVUinfo.XGKICKPC = xPC;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mVUregs.r = calcCycles(mVUregs.r, x);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: set xVar to 1 if VFreg1 and VFreg2 reference the same VF reg and
|
|
|
|
|
// any of the X/Y/Z/W components are touched by both. Ported from x86.
|
|
|
|
|
static __fi void cmpVFregs(microVFreg& VFreg1, microVFreg& VFreg2, bool& xVar)
|
|
|
|
|
{
|
|
|
|
|
if (VFreg1.reg == VFreg2.reg)
|
|
|
|
|
{
|
|
|
|
|
if ((VFreg1.x && VFreg2.x) || (VFreg1.y && VFreg2.y)
|
|
|
|
|
|| (VFreg1.z && VFreg2.z) || (VFreg1.w && VFreg2.w))
|
|
|
|
|
{
|
|
|
|
|
xVar = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void mVUsetCycles(mV)
|
|
|
|
|
{
|
|
|
|
|
mVUincCycles(mVU, mVUstall);
|
|
|
|
|
|
|
|
|
|
// If upper Op && lower Op write to same VF reg: either make Lower skip its
|
|
|
|
|
// VF write (noWriteVF) or mark it a NOP entirely when Lower has no other
|
|
|
|
|
// side effects.
|
|
|
|
|
if ((mVUregsTemp.VFreg[0] == mVUregsTemp.VFreg[1]) && mVUregsTemp.VFreg[0])
|
|
|
|
|
{
|
|
|
|
|
if (mVUregsTemp.r || mVUregsTemp.VI)
|
|
|
|
|
mVUlow.noWriteVF = true;
|
|
|
|
|
else
|
|
|
|
|
mVUlow.isNOP = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If Lower reads a VF reg that Upper writes, Upper's semantic output must
|
|
|
|
|
// be visible to Lower → run Lower first (swapOps).
|
|
|
|
|
if ((mVUlow.VF_read[0].reg || mVUlow.VF_read[1].reg) && mVUup.VF_write.reg)
|
|
|
|
|
{
|
|
|
|
|
cmpVFregs(mVUup.VF_write, mVUlow.VF_read[0], mVUinfo.swapOps);
|
|
|
|
|
cmpVFregs(mVUup.VF_write, mVUlow.VF_read[1], mVUinfo.swapOps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If swapOps is set AND Upper also reads a VF reg that Lower writes,
|
|
|
|
|
// snapshot the VF reg before Lower runs so Upper sees pre-Lower
|
|
|
|
|
// state (backupVF).
|
|
|
|
|
if (mVUinfo.swapOps && ((mVUup.VF_read[0].reg || mVUup.VF_read[1].reg) && mVUlow.VF_write.reg))
|
|
|
|
|
{
|
|
|
|
|
cmpVFregs(mVUlow.VF_write, mVUup.VF_read[0], mVUinfo.backupVF);
|
|
|
|
|
cmpVFregs(mVUlow.VF_write, mVUup.VF_read[1], mVUinfo.backupVF);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[0]].x = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].x, mVUregsTemp.VF[0].x);
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[0]].y = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].y, mVUregsTemp.VF[0].y);
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[0]].z = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].z, mVUregsTemp.VF[0].z);
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[0]].w = tCycles(mVUregs.VF[mVUregsTemp.VFreg[0]].w, mVUregsTemp.VF[0].w);
|
|
|
|
|
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[1]].x = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].x, mVUregsTemp.VF[1].x);
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[1]].y = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].y, mVUregsTemp.VF[1].y);
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[1]].z = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].z, mVUregsTemp.VF[1].z);
|
|
|
|
|
mVUregs.VF[mVUregsTemp.VFreg[1]].w = tCycles(mVUregs.VF[mVUregsTemp.VFreg[1]].w, mVUregsTemp.VF[1].w);
|
|
|
|
|
|
|
|
|
|
mVUregs.VI[mVUregsTemp.VIreg] = tCycles(mVUregs.VI[mVUregsTemp.VIreg], mVUregsTemp.VI);
|
|
|
|
|
|
|
|
|
|
mVUregs.q = tCycles(mVUregs.q, mVUregsTemp.q);
|
|
|
|
|
mVUregs.p = tCycles(mVUregs.p, mVUregsTemp.p);
|
|
|
|
|
mVUregs.r = tCycles(mVUregs.r, mVUregsTemp.r);
|
|
|
|
|
mVUregs.xgkick = tCycles(mVUregs.xgkick, mVUregsTemp.xgkick);
|
|
|
|
|
memset(&mVUregsTemp, 0, sizeof(mVUregsTemp));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Flag-Pass Analysis (ported from x86 microVU_Flags.inl)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Scans forward through instructions to determine which pipeline flags
|
|
|
|
|
// (sFlag/mFlag/cFlag) the next block reads in its first ~4 instructions.
|
|
|
|
|
// Sets mVUregs.needExactMatch bits (1/2/4) so block lookup can require
|
|
|
|
|
// an exact pipeline-state match for correctness.
|
|
|
|
|
|
|
|
|
|
#define shortBranchPass() \
|
|
|
|
|
{ \
|
|
|
|
|
if ((branch == 3) || (branch == 4)) /* Branches */ \
|
|
|
|
|
{ \
|
|
|
|
|
_mVUflagPass(mVU, aBranchAddr, sCount + found, found, v); \
|
|
|
|
|
if (branch == 3) /* Non-conditional Branch */ \
|
|
|
|
|
break; \
|
|
|
|
|
branch = 0; \
|
|
|
|
|
} \
|
|
|
|
|
else if (branch == 5) /* JR/JARL */ \
|
|
|
|
|
{ \
|
|
|
|
|
if (sCount + found < 4) \
|
|
|
|
|
mVUregs.needExactMatch |= 7; \
|
|
|
|
|
break; \
|
|
|
|
|
} \
|
|
|
|
|
else /* E-Bit End */ \
|
2026-07-18 19:16:38 -07:00
|
|
|
{ \
|
|
|
|
|
/* The successor ends the program, and mVUendProgram finalises the */ \
|
|
|
|
|
/* flags into VI[REG_*_FLAG]. Without this the lookahead reports */ \
|
|
|
|
|
/* "successor reads no flags" and this block elides the very FMAC */ \
|
|
|
|
|
/* flag writes the E-bit will store, so the finalisation reads a */ \
|
|
|
|
|
/* never-written ring instance (findFlagInst's all-(-1) -> slot 0). */ \
|
|
|
|
|
/* eBitPass1 applies the same rule for an E-bit in the block being */ \
|
|
|
|
|
/* compiled; the lookahead just never carried it across the edge. */ \
|
|
|
|
|
mVU.needFlagFinalize = true; \
|
2026-06-20 20:27:55 -07:00
|
|
|
break; \
|
2026-07-18 19:16:38 -07:00
|
|
|
} \
|
2026-06-20 20:27:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Scan instructions at startPC and check if they read any pipeline flags.
|
|
|
|
|
// Uses pass4 (recPass=3) on each Upper/Lower op to accumulate needExactMatch bits.
|
|
|
|
|
void _mVUflagPass(mV, u32 startPC, u32 sCount, u32 found, std::vector<u32>& v)
|
|
|
|
|
{
|
|
|
|
|
for (u32 i = 0; i < v.size(); i++)
|
|
|
|
|
{
|
|
|
|
|
if (v[i] == startPC)
|
|
|
|
|
return; // Prevent infinite recursion
|
|
|
|
|
}
|
|
|
|
|
v.push_back(startPC);
|
|
|
|
|
|
|
|
|
|
int oldPC = iPC;
|
|
|
|
|
int oldBranch = mVUbranch;
|
|
|
|
|
int aBranchAddr = 0;
|
|
|
|
|
iPC = startPC / 4;
|
|
|
|
|
mVUbranch = 0;
|
|
|
|
|
for (int branch = 0; sCount < 4; sCount += found)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.needExactMatch &= 7;
|
|
|
|
|
incPC(1);
|
|
|
|
|
mVUopU(mVU, 3);
|
|
|
|
|
found |= (mVUregs.needExactMatch & 8) >> 3;
|
|
|
|
|
mVUregs.needExactMatch &= 7;
|
|
|
|
|
if (curI & _Ebit_)
|
|
|
|
|
{
|
|
|
|
|
branch = 1;
|
|
|
|
|
}
|
|
|
|
|
if (curI & _Tbit_)
|
|
|
|
|
{
|
|
|
|
|
branch = 6;
|
|
|
|
|
}
|
|
|
|
|
if ((curI & _Dbit_) && doDBitHandling)
|
|
|
|
|
{
|
|
|
|
|
branch = 6;
|
|
|
|
|
}
|
|
|
|
|
if (!(curI & _Ibit_))
|
|
|
|
|
{
|
|
|
|
|
incPC(-1);
|
|
|
|
|
mVUopL(mVU, 3);
|
|
|
|
|
incPC(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (branch >= 2)
|
|
|
|
|
{
|
|
|
|
|
shortBranchPass();
|
|
|
|
|
}
|
|
|
|
|
else if (branch == 1)
|
|
|
|
|
{
|
|
|
|
|
branch = 2;
|
|
|
|
|
}
|
|
|
|
|
if (mVUbranch)
|
|
|
|
|
{
|
|
|
|
|
branch = ((mVUbranch > 8) ? (5) : ((mVUbranch < 3) ? 3 : 4));
|
|
|
|
|
incPC(-1);
|
|
|
|
|
aBranchAddr = branchAddr(mVU);
|
|
|
|
|
incPC(1);
|
|
|
|
|
mVUbranch = 0;
|
|
|
|
|
}
|
|
|
|
|
incPC(1);
|
|
|
|
|
if ((mVUregs.needExactMatch & 7) == 7)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
iPC = oldPC;
|
|
|
|
|
mVUbranch = oldBranch;
|
|
|
|
|
mVUregs.needExactMatch &= 7;
|
|
|
|
|
setCode();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void mVUflagPass(mV, u32 startPC, u32 sCount = 0, u32 found = 0)
|
|
|
|
|
{
|
|
|
|
|
std::vector<u32> v;
|
|
|
|
|
_mVUflagPass(mVU, startPC, sCount, found, v);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checks if the first ~4 instructions of the successor block(s) read flags,
|
|
|
|
|
// and sets needExactMatch bits accordingly so block lookup requires exact state.
|
|
|
|
|
void mVUsetFlagInfo(mV)
|
|
|
|
|
{
|
|
|
|
|
if (noFlagOpts)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.needExactMatch = 0x7;
|
|
|
|
|
mVUregs.flagInfo = 0x0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (mVUbranch <= 2) // B/BAL
|
|
|
|
|
{
|
|
|
|
|
incPC(-1);
|
|
|
|
|
mVUflagPass(mVU, branchAddr(mVU));
|
|
|
|
|
incPC(1);
|
|
|
|
|
|
|
|
|
|
mVUregs.needExactMatch &= 0x7;
|
|
|
|
|
}
|
|
|
|
|
else if (mVUbranch <= 8) // Conditional Branch
|
|
|
|
|
{
|
|
|
|
|
incPC(-1); // Branch Taken
|
|
|
|
|
mVUflagPass(mVU, branchAddr(mVU));
|
|
|
|
|
int backupFlagInfo = mVUregs.needExactMatch;
|
|
|
|
|
mVUregs.needExactMatch = 0;
|
|
|
|
|
|
|
|
|
|
incPC(4); // Branch Not Taken
|
|
|
|
|
mVUflagPass(mVU, xPC);
|
|
|
|
|
incPC(-3);
|
|
|
|
|
|
|
|
|
|
mVUregs.needExactMatch |= backupFlagInfo;
|
|
|
|
|
mVUregs.needExactMatch &= 0x7;
|
|
|
|
|
}
|
|
|
|
|
else // JR/JALR
|
|
|
|
|
{
|
|
|
|
|
if (!doConstProp || !mVUlow.constJump.isValid)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.needExactMatch |= 0x7;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mVUflagPass(mVU, (mVUlow.constJump.regValue * 8) & (mVU.microMemSize - 8));
|
|
|
|
|
}
|
|
|
|
|
mVUregs.needExactMatch &= 0x7;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 18:16:40 -07:00
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// VU0 spin-wait fast-forward
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// EE-handshake busy-wait loops — a conditional VI branch whose entire loop
|
|
|
|
|
// body is architectural NOPs — dominate VU0 execution in macro-handshake
|
|
|
|
|
// titles (UYA gameplay: 89% of all VU0 micro cycles are three such loops).
|
|
|
|
|
// While the spin condition holds, every iteration is bit-identical AND
|
|
|
|
|
// nothing can change the condition mid-grant (the EE thread is stalled
|
|
|
|
|
// inside Execute; only CTC2/interp writes the VI), so the whole remaining
|
|
|
|
|
// cycle grant can be consumed in one step: zero mVU.cycles and let
|
|
|
|
|
// mVUtestCycles' budget-exhaust path bank VU0.cycle += grant and park the
|
|
|
|
|
// VE-07 resume at this very block. The next grant re-enters the 3-insn
|
|
|
|
|
// condition check directly.
|
|
|
|
|
//
|
|
|
|
|
// Two recognized shapes (all non-branch slots must be exact NOP encodings,
|
|
|
|
|
// so no flags/Q/P/regs/XGKICK/E/M/D/T/I side effects exist by construction):
|
|
|
|
|
// 4-pair: {IBEQ/IBNE viA,viB,+N | NOP; NOP|NOP; B ->head |NOP; NOP|NOP}
|
|
|
|
|
// (spins on fallthrough; branch-taken = loop exit)
|
|
|
|
|
// 2-pair: {IBEQ/IBNE viA,viB,->self | NOP; NOP|NOP}
|
|
|
|
|
// (spins while taken; fallthrough = loop exit)
|
|
|
|
|
struct mVUSpinLoop
|
|
|
|
|
{
|
|
|
|
|
bool found;
|
|
|
|
|
bool exitOnEq; // loop-exit condition: VI[viA] == VI[viB] (else !=)
|
|
|
|
|
u32 viA, viB;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static mVUSpinLoop mVUdetectSpinLoop(mV, u32 startPC)
|
|
|
|
|
{
|
|
|
|
|
mVUSpinLoop r = {};
|
|
|
|
|
if (!isVU0 || startPC + 32 > mVU.microMemSize)
|
|
|
|
|
return r;
|
|
|
|
|
const u32* mem = (const u32*)(mVU.regs().Micro + startPC);
|
|
|
|
|
constexpr u32 upNOP = 0x000002FF, loNOP = 0x8000033C;
|
|
|
|
|
if (mem[1] != upNOP || mem[2] != loNOP || mem[3] != upNOP)
|
|
|
|
|
return r; // branch upper + delay-slot pair must be exact NOPs
|
|
|
|
|
const u32 lo0 = mem[0];
|
|
|
|
|
const u32 op = lo0 >> 25;
|
|
|
|
|
const bool ibeq = (op == 0x28), ibne = (op == 0x29);
|
|
|
|
|
if (!ibeq && !ibne)
|
|
|
|
|
return r;
|
|
|
|
|
r.viA = (lo0 >> 16) & 0x1F;
|
|
|
|
|
r.viB = (lo0 >> 11) & 0x1F;
|
|
|
|
|
const s32 imm11 = (s32)(lo0 << 21) >> 21;
|
|
|
|
|
if (startPC + 8 + imm11 * 8 == (s32)startPC)
|
|
|
|
|
{
|
|
|
|
|
// 2-pair self-loop: spins while taken; exit on the opposite sense.
|
|
|
|
|
r.found = true;
|
|
|
|
|
r.exitOnEq = ibne;
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
// 4-pair shape: fallthrough pair2 must be `B ->head` with NOP upper,
|
|
|
|
|
// pair3 (its delay slot) all-NOP.
|
|
|
|
|
const u32 lo2 = mem[4];
|
|
|
|
|
if (mem[5] != upNOP || mem[6] != loNOP || mem[7] != upNOP)
|
|
|
|
|
return r;
|
|
|
|
|
if ((lo2 >> 25) != 0x20) // B
|
|
|
|
|
return r;
|
|
|
|
|
const s32 imm2 = (s32)(lo2 << 21) >> 21;
|
|
|
|
|
if (startPC + 16 + 8 + imm2 * 8 != (s32)startPC)
|
|
|
|
|
return r;
|
|
|
|
|
r.found = true;
|
|
|
|
|
r.exitOnEq = ibeq; // spins on fallthrough; taken (the branch sense) exits
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Emitted at the very top of a spin block, ahead of mVUtestCycles: if the
|
|
|
|
|
// spin condition still holds, zero the cycle budget so mVUtestCycles takes
|
|
|
|
|
// its existing budget-exhaust exit (grant fully consumed, resume parked).
|
|
|
|
|
static void mVUemitSpinFF(mV, const mVUSpinLoop& spin)
|
|
|
|
|
{
|
|
|
|
|
a64::Label noSpin;
|
|
|
|
|
armAsm->Ldrh(a64::w9, mVUstateMem(offsetof(VURegs, VI) + spin.viA * sizeof(REG_VI)));
|
|
|
|
|
if (spin.viB == 0)
|
|
|
|
|
armAsm->Cmp(a64::w9, 0);
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
armAsm->Ldrh(a64::w10, mVUstateMem(offsetof(VURegs, VI) + spin.viB * sizeof(REG_VI)));
|
|
|
|
|
armAsm->Cmp(a64::w9, a64::w10);
|
|
|
|
|
}
|
|
|
|
|
armAsm->B(&noSpin, spin.exitOnEq ? a64::eq : a64::ne);
|
|
|
|
|
armAsm->Str(a64::wzr, mVUfieldMem(mVU, &mVU.cycles));
|
|
|
|
|
armAsm->Bind(&noSpin);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 20:27:55 -07:00
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Cycle Test (emits code to check remaining cycles)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Test remaining cycles; if insufficient, save block state via copyPLState +
|
|
|
|
|
// mVUendProgram(0) and exit to the dispatcher. Otherwise deduct cycles and
|
|
|
|
|
// continue into the block. Ported from x86 microVU_Compile.inl:449.
|
|
|
|
|
// The copyPLState + mVUendProgram(0) on early-exit is required so that
|
|
|
|
|
// a cycle-timeout block has its pipeline state saved; without it, the block
|
|
|
|
|
// manager would see stale pState on re-entry and create a new variant.
|
|
|
|
|
static void mVUtestCycles(mV, microFlagCycles& mFC)
|
|
|
|
|
{
|
|
|
|
|
iPC = mVUstartPC;
|
|
|
|
|
|
|
|
|
|
if (isVU0 && EmuConfig.Speedhacks.EECycleRate != 0 && (!EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Speedhacks.EECycleRate < 0))
|
|
|
|
|
{
|
|
|
|
|
switch (std::min(static_cast<int>(EmuConfig.Speedhacks.EECycleRate), static_cast<int>(mVUcycles)))
|
|
|
|
|
{
|
|
|
|
|
case -3: mVUcycles *= 2.0f; break;
|
|
|
|
|
case -2: mVUcycles *= 1.6666667f; break;
|
|
|
|
|
case -1: mVUcycles *= 1.3333333f; break;
|
|
|
|
|
case 1: mVUcycles /= 1.3f; break;
|
|
|
|
|
case 2: mVUcycles /= 1.8f; break;
|
|
|
|
|
case 3: mVUcycles /= 3.0f; break;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 17:20:50 -07:00
|
|
|
armAsm->Ldr(a64::w9, mVUfieldMem(mVU, &mVU.cycles));
|
2026-06-20 20:27:55 -07:00
|
|
|
if (EmuConfig.Gamefixes.VUSyncHack)
|
|
|
|
|
armAsm->Subs(a64::w9, a64::w9, mVUcycles);
|
|
|
|
|
else
|
|
|
|
|
armAsm->Subs(a64::w9, a64::w9, 1);
|
|
|
|
|
|
|
|
|
|
// If (cycles - check) is non-negative, there is budget — skip the early exit.
|
|
|
|
|
a64::Label skip;
|
|
|
|
|
armAsm->B(&skip, a64::pl); // pl = N clear = non-negative
|
|
|
|
|
|
|
|
|
|
// Early exit path: save pipeline state then exit via mVUendProgram(0).
|
2026-07-13 12:49:54 -07:00
|
|
|
// The resume variant additionally parks this block's hostEntry in
|
|
|
|
|
// mVU.resumeEntry — a budget break resumes at this very block (the
|
|
|
|
|
// state saved here IS this block's entry state), so the next dispatch
|
|
|
|
|
// can skip mVUlookupProg (VE-07).
|
2026-06-20 20:27:55 -07:00
|
|
|
armMoveAddressToReg(a64::x0, &mVUpBlock->pState);
|
2026-07-13 12:49:54 -07:00
|
|
|
armEmitCall(mVU.copyPLStateResume);
|
2026-06-20 20:27:55 -07:00
|
|
|
if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack)
|
|
|
|
|
{
|
|
|
|
|
armAsm->Mov(a64::w9, mVUcycles);
|
|
|
|
|
armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, nextBlockCycles)));
|
|
|
|
|
}
|
|
|
|
|
mVUendProgram(mVU, &mFC, 0);
|
|
|
|
|
|
|
|
|
|
armAsm->Bind(&skip);
|
|
|
|
|
|
|
|
|
|
// Budget remains — deduct block cycles from mVU.cycles and fall through
|
2026-07-12 17:20:50 -07:00
|
|
|
// into the block body. w9 still holds the Subs result from the test
|
|
|
|
|
// above (the early-exit path never reaches here), so the deducted value
|
|
|
|
|
// is derivable without reloading: under VUSyncHack it IS cycles -
|
|
|
|
|
// mVUcycles; otherwise it's cycles - 1, one short of the full deduction.
|
|
|
|
|
if (!EmuConfig.Gamefixes.VUSyncHack && mVUcycles != 1)
|
|
|
|
|
armAsm->Sub(a64::w9, a64::w9, mVUcycles - 1);
|
|
|
|
|
armAsm->Str(a64::w9, mVUfieldMem(mVU, &mVU.cycles));
|
2026-06-20 20:27:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Execute VU Instruction (Upper + Lower)
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
// Pre-populate NEON/GPR caches with VF/VI registers the next few ops will
|
|
|
|
|
// read. Ported from pcsx2/x86/microVU_Compile.inl:603-690. Runs once at
|
|
|
|
|
// the start of pass 2; iterates forward through mVUinfo until caches are
|
|
|
|
|
// nearly full, or an XGKICK / branch is encountered. Purely an
|
|
|
|
|
// optimization — skips pre-loaded regs on allocReg so subsequent ops
|
|
|
|
|
// reuse the cached data instead of re-loading from memory.
|
|
|
|
|
static void mvuPreloadRegisters(microVU& mVU, u32 endCount)
|
|
|
|
|
{
|
|
|
|
|
static constexpr const int REQUIRED_FREE_NEON = 3;
|
|
|
|
|
static constexpr const int REQUIRED_FREE_GPRS = 1;
|
|
|
|
|
|
|
|
|
|
u32 vfs_loaded = 0;
|
|
|
|
|
u32 vis_loaded = 0;
|
|
|
|
|
|
|
|
|
|
for (int reg = 0; reg < mVU.regAlloc->getNeonCount(); reg++)
|
|
|
|
|
{
|
|
|
|
|
const int vf = mVU.regAlloc->getRegVF(reg);
|
|
|
|
|
if (vf >= 0)
|
|
|
|
|
vfs_loaded |= (1u << vf);
|
|
|
|
|
}
|
|
|
|
|
for (int reg = 0; reg < mVU.regAlloc->getGPRCount(); reg++)
|
|
|
|
|
{
|
|
|
|
|
const int vi = mVU.regAlloc->getRegVI(reg);
|
|
|
|
|
if (vi >= 0)
|
|
|
|
|
vis_loaded |= (1u << vi);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const u32 orig_pc = iPC;
|
|
|
|
|
const u32 orig_code = mVU.code;
|
|
|
|
|
int free_regs = mVU.regAlloc->getFreeNeonCount();
|
|
|
|
|
int free_gprs = mVU.regAlloc->getFreeGPRCount();
|
|
|
|
|
|
|
|
|
|
auto preloadVF = [&mVU, &vfs_loaded, &free_regs](u8 reg)
|
|
|
|
|
{
|
|
|
|
|
if (free_regs <= REQUIRED_FREE_NEON || reg == 0 || (vfs_loaded & (1u << reg)) != 0)
|
|
|
|
|
return;
|
|
|
|
|
mVU.regAlloc->clearNeeded(mVU.regAlloc->allocReg(reg));
|
|
|
|
|
vfs_loaded |= (1u << reg);
|
|
|
|
|
free_regs--;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto preloadVI = [&mVU, &vis_loaded, &free_gprs](u8 reg)
|
|
|
|
|
{
|
|
|
|
|
if (free_gprs <= REQUIRED_FREE_GPRS || reg == 0 || (vis_loaded & (1u << reg)) != 0)
|
|
|
|
|
return;
|
|
|
|
|
mVU.regAlloc->clearNeeded(mVU.regAlloc->allocGPR(reg));
|
|
|
|
|
vis_loaded |= (1u << reg);
|
|
|
|
|
free_gprs--;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto canPreload = [&free_regs, &free_gprs]() {
|
|
|
|
|
return (free_regs >= REQUIRED_FREE_NEON || free_gprs >= REQUIRED_FREE_GPRS);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (u32 x = 0; x < endCount && canPreload(); x++)
|
|
|
|
|
{
|
|
|
|
|
incPC(1);
|
|
|
|
|
|
|
|
|
|
const microOp* info = &mVUinfo;
|
|
|
|
|
if (info->doXGKICK)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
for (u32 i = 0; i < 2; i++)
|
|
|
|
|
{
|
|
|
|
|
preloadVF(info->uOp.VF_read[i].reg);
|
|
|
|
|
preloadVF(info->lOp.VF_read[i].reg);
|
|
|
|
|
if (info->lOp.VI_read[i].used)
|
|
|
|
|
preloadVI(info->lOp.VI_read[i].reg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const microVFreg& uvfr = info->uOp.VF_write;
|
|
|
|
|
if (uvfr.reg != 0 && (!uvfr.x || !uvfr.y || !uvfr.z || !uvfr.w))
|
|
|
|
|
preloadVF(uvfr.reg);
|
|
|
|
|
|
|
|
|
|
const microVFreg& lvfr = info->lOp.VF_write;
|
|
|
|
|
if (lvfr.reg != 0 && (!lvfr.x || !lvfr.y || !lvfr.z || !lvfr.w))
|
|
|
|
|
preloadVF(lvfr.reg);
|
|
|
|
|
|
|
|
|
|
if (info->lOp.branch)
|
|
|
|
|
break;
|
2026-06-23 18:25:05 -07:00
|
|
|
|
|
|
|
|
// Stop at the block's true end. endCount is the whole micro-memory size
|
|
|
|
|
// (microMemSize/8), not the block length — the analysis loop above only
|
|
|
|
|
// clears + populates IRinfo.info[] for the block's own instructions and
|
|
|
|
|
// breaks at isEOB. Without the matching isEOB break here, an E-bit-
|
|
|
|
|
// terminated block (no lOp.branch) walks the preload past its own end
|
|
|
|
|
// into info[] entries left over from a PRIOR compile, preloading VF/VI
|
|
|
|
|
// the program never touches. Harmless at runtime (an unused reg load),
|
|
|
|
|
// but it makes the emitted shape depend on compile history — non-
|
|
|
|
|
// deterministic codegen that the persisted-JIT ABI digest must not see.
|
|
|
|
|
// In a cold cache those stale entries read zero (reg 0 → skipped), so
|
|
|
|
|
// this only suppresses the spurious warm-state preloads; the cold shape
|
|
|
|
|
// (what the digest pins) is unchanged. Diverges from x86, which has the
|
|
|
|
|
// same latent over-read but no on-disk cache that needs deterministic
|
|
|
|
|
// emit. Mirrors the flagInfo "clear each compile" fix in mVUinitFirstPass.
|
|
|
|
|
if (info->isEOB)
|
|
|
|
|
break;
|
2026-06-20 20:27:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
iPC = orig_pc;
|
|
|
|
|
mVU.code = orig_code;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 14:09:45 -07:00
|
|
|
__ri void doUpperOp(mV) { mVUopU(mVU, 1); mVUdivSet(mVU); }
|
2026-06-20 20:27:55 -07:00
|
|
|
__ri void doLowerOp(mV) { incPC(-1); mVUopL(mVU, 1); incPC(1); }
|
|
|
|
|
__ri void flushRegs(mV) { if (!doRegAlloc) mVU.regAlloc->flushAll(); }
|
|
|
|
|
|
|
|
|
|
void doIbit(mV)
|
|
|
|
|
{
|
|
|
|
|
if (mVUup.iBit)
|
|
|
|
|
{
|
|
|
|
|
incPC(-1);
|
2026-08-17 22:21:27 +02:00
|
|
|
if (EmuConfig.Gamefixes.IbitHack)
|
|
|
|
|
{
|
|
|
|
|
// mVUsetupRange leaves this word out of the range the program
|
|
|
|
|
// compare covers, and mVUclear is range-aware, so a game that
|
|
|
|
|
// rewrites the I immediate in place keeps running the block already
|
|
|
|
|
// compiled for the old one. Read the constant from micro memory at
|
|
|
|
|
// run time instead of baking it in. Left unclamped, like x86 — the
|
|
|
|
|
// operand clamp at the use site covers it.
|
|
|
|
|
armLoadPtr(gprT1, &curI);
|
|
|
|
|
armAsm->Str(gprT1, mVUstateMem(offsetof(VURegs, VI) + REG_I * sizeof(REG_VI)));
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
u32 tempI = curI;
|
|
|
|
|
if (CHECK_VU_OVERFLOW(mVU.index) && ((curI & 0x7fffffff) >= 0x7f800000))
|
|
|
|
|
tempI = (0x80000000 & curI) | 0x7f7fffff;
|
2026-06-20 20:27:55 -07:00
|
|
|
|
2026-08-17 22:21:27 +02:00
|
|
|
armAsm->Mov(gprT1, tempI);
|
|
|
|
|
armAsm->Str(gprT1, mVUstateMem(offsetof(VURegs, VI) + REG_I * sizeof(REG_VI)));
|
|
|
|
|
}
|
2026-06-20 20:27:55 -07:00
|
|
|
incPC(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ported from x86 microVU_Compile.inl:doSwapOp — runs Lower before Upper, and
|
|
|
|
|
// when Upper reads a VF reg Lower writes, snapshots the pre-Lower VF value via
|
|
|
|
|
// an XOR-swap so Upper observes the original value.
|
|
|
|
|
static void doSwapOp(mV)
|
|
|
|
|
{
|
|
|
|
|
if (mVUinfo.backupVF && !mVUlow.noWriteVF)
|
|
|
|
|
{
|
|
|
|
|
DevCon.WriteLn(Color_Green, "microVU%d: Backing Up VF Reg [%04x]", getIndex, xPC);
|
|
|
|
|
|
|
|
|
|
// Alloc t1 = current value of Lower's VF_write reg (pre-Lower).
|
|
|
|
|
const a64::VRegister t1 = mVU.regAlloc->allocReg(mVUlow.VF_write.reg);
|
|
|
|
|
const a64::VRegister t2 = mVU.regAlloc->allocReg();
|
|
|
|
|
armAsm->Mov(t2.V16B(), t1.V16B()); // t2 = pre-Lower value
|
|
|
|
|
mVU.regAlloc->clearNeeded(t1);
|
|
|
|
|
|
|
|
|
|
mVUopL(mVU, 1); // Lower writes new value to VF_write
|
|
|
|
|
|
|
|
|
|
// XOR-swap: t2 gets new value, VF_write reg (via t3) gets old value,
|
|
|
|
|
// so Upper sees the pre-Lower state.
|
|
|
|
|
const a64::VRegister t3 = mVU.regAlloc->allocReg(mVUlow.VF_write.reg, mVUlow.VF_write.reg, 0xf, false);
|
|
|
|
|
armAsm->Eor(t2.V16B(), t2.V16B(), t3.V16B());
|
|
|
|
|
armAsm->Eor(t3.V16B(), t3.V16B(), t2.V16B());
|
|
|
|
|
armAsm->Eor(t2.V16B(), t2.V16B(), t3.V16B());
|
|
|
|
|
mVU.regAlloc->clearNeeded(t3);
|
|
|
|
|
|
|
|
|
|
incPC(1);
|
|
|
|
|
doUpperOp(mVU); // Upper reads VF_write reg with old value
|
|
|
|
|
|
|
|
|
|
// Write the new value (held in t2) back to VF_write reg.
|
|
|
|
|
const a64::VRegister t4 = mVU.regAlloc->allocReg(-1, mVUlow.VF_write.reg, 0xf);
|
|
|
|
|
armAsm->Mov(t4.V16B(), t2.V16B());
|
|
|
|
|
mVU.regAlloc->clearNeeded(t4);
|
|
|
|
|
mVU.regAlloc->clearNeeded(t2);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mVUopL(mVU, 1);
|
|
|
|
|
incPC(1);
|
|
|
|
|
flushRegs(mVU);
|
|
|
|
|
doUpperOp(mVU);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Runtime D-bit handler: if VU0/VU1 FBRST has the D-interrupt bit set, raise
|
|
|
|
|
// VPU_STAT and INTCINTERRUPT flags, end the program, otherwise fall through.
|
|
|
|
|
// Mirrors x86 microVU_Compile.inl:560-576.
|
|
|
|
|
static void mVUDoDBit(microVU& mVU, microFlagCycles* mFC)
|
|
|
|
|
{
|
|
|
|
|
// Flush regalloc before the conditional skip — mVUDTendProgram's internal
|
|
|
|
|
// flushAll emits the stores INSIDE the branch, so the silent path would
|
|
|
|
|
// otherwise drop dirty regs (the lower op of the D-bit pair). Same pattern
|
|
|
|
|
// as the branch-side D-bit handler in microVU_Branch-arm64.inl:540.
|
|
|
|
|
mVU.regAlloc->flushAll(false);
|
|
|
|
|
|
|
|
|
|
a64::Label noDBit;
|
|
|
|
|
armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1)
|
|
|
|
|
? (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL);
|
|
|
|
|
armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8));
|
|
|
|
|
armAsm->Tst(a64::w9, isVU1 ? 0x400 : 0x4);
|
|
|
|
|
armAsm->B(&noDBit, a64::eq);
|
|
|
|
|
|
|
|
|
|
if (!isVU1 || !THREAD_VU1)
|
|
|
|
|
{
|
|
|
|
|
armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL);
|
|
|
|
|
armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8));
|
|
|
|
|
armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x200 : 0x2);
|
|
|
|
|
armAsm->Str(a64::w9, a64::MemOperand(a64::x8));
|
|
|
|
|
|
|
|
|
|
armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags)));
|
|
|
|
|
armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT);
|
|
|
|
|
armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
incPC(1);
|
|
|
|
|
mVUDTendProgram(mVU, mFC, 1);
|
|
|
|
|
incPC(-1);
|
|
|
|
|
|
|
|
|
|
armAsm->Bind(&noDBit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Runtime T-bit handler. Same pattern as mVUDoDBit but tests the T bit.
|
|
|
|
|
// Mirrors x86 microVU_Compile.inl:578-595.
|
|
|
|
|
static void mVUDoTBit(microVU& mVU, microFlagCycles* mFC)
|
|
|
|
|
{
|
|
|
|
|
// Flush regalloc before the conditional skip — mVUDTendProgram's internal
|
|
|
|
|
// flushAll emits the stores INSIDE the branch, so the silent path would
|
|
|
|
|
// otherwise drop dirty regs (the lower op of the T-bit pair). Same pattern
|
|
|
|
|
// as the branch-side T-bit handler in microVU_Branch-arm64.inl:569.
|
|
|
|
|
mVU.regAlloc->flushAll(false);
|
|
|
|
|
|
|
|
|
|
a64::Label noTBit;
|
|
|
|
|
armMoveAddressToReg(a64::x8, (mVU.index && THREAD_VU1)
|
|
|
|
|
? (void*)&vu1Thread.vuFBRST : (void*)&VU0.VI[REG_FBRST].UL);
|
|
|
|
|
armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8));
|
|
|
|
|
armAsm->Tst(a64::w9, isVU1 ? 0x800 : 0x8);
|
|
|
|
|
armAsm->B(&noTBit, a64::eq);
|
|
|
|
|
|
|
|
|
|
if (!isVU1 || !THREAD_VU1)
|
|
|
|
|
{
|
|
|
|
|
armMoveAddressToReg(a64::x8, &VU0.VI[REG_VPU_STAT].UL);
|
|
|
|
|
armAsm->Ldr(a64::w9, a64::MemOperand(a64::x8));
|
|
|
|
|
armAsm->Orr(a64::w9, a64::w9, isVU1 ? 0x400 : 0x4);
|
|
|
|
|
armAsm->Str(a64::w9, a64::MemOperand(a64::x8));
|
|
|
|
|
|
|
|
|
|
armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags)));
|
|
|
|
|
armAsm->Orr(a64::w9, a64::w9, VUFLAG_INTCINTERRUPT);
|
|
|
|
|
armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
incPC(1);
|
|
|
|
|
mVUDTendProgram(mVU, mFC, 1);
|
|
|
|
|
incPC(-1);
|
|
|
|
|
|
|
|
|
|
armAsm->Bind(&noTBit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void mVUexecuteInstruction(mV)
|
|
|
|
|
{
|
|
|
|
|
if (mVUlow.isNOP)
|
|
|
|
|
{
|
|
|
|
|
incPC(1);
|
|
|
|
|
doUpperOp(mVU);
|
|
|
|
|
flushRegs(mVU);
|
|
|
|
|
doIbit(mVU);
|
|
|
|
|
}
|
|
|
|
|
else if (!mVUinfo.swapOps)
|
|
|
|
|
{
|
|
|
|
|
incPC(1);
|
|
|
|
|
doUpperOp(mVU);
|
|
|
|
|
flushRegs(mVU);
|
|
|
|
|
doLowerOp(mVU);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
doSwapOp(mVU);
|
|
|
|
|
}
|
|
|
|
|
flushRegs(mVU);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Init helpers
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
__fi void startLoop(mV)
|
|
|
|
|
{
|
|
|
|
|
memset(&mVUinfo, 0, sizeof(mVUinfo));
|
|
|
|
|
memset(&mVUregsTemp, 0, sizeof(mVUregsTemp));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
__fi void mVUinitConstValues(microVU& mVU)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < 16; i++)
|
|
|
|
|
{
|
|
|
|
|
mVUconstReg[i].isValid = 0;
|
|
|
|
|
mVUconstReg[i].regValue = 0;
|
|
|
|
|
}
|
|
|
|
|
mVUconstReg[15].isValid = mVUregs.vi15v;
|
|
|
|
|
mVUconstReg[15].regValue = mVUregs.vi15v ? mVUregs.vi15 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
__fi void mVUinitFirstPass(mV, uptr pState, u8* thisPtr)
|
|
|
|
|
{
|
|
|
|
|
mVUstartPC = iPC;
|
|
|
|
|
mVUbranch = 0;
|
|
|
|
|
mVUcount = 0;
|
|
|
|
|
mVUcycles = 0;
|
|
|
|
|
mVU.p = 0;
|
|
|
|
|
mVU.q = 0;
|
|
|
|
|
|
|
|
|
|
if ((uptr)&mVUregs != pState)
|
|
|
|
|
memcpy((u8*)&mVUregs, (u8*)pState, sizeof(microRegInfo));
|
|
|
|
|
if ((uptr)&mVU.prog.lpState != pState)
|
|
|
|
|
memcpy((u8*)&mVU.prog.lpState, (u8*)pState, sizeof(microRegInfo));
|
|
|
|
|
|
|
|
|
|
mVUblock.x86ptrStart = thisPtr;
|
|
|
|
|
// hostEntry mirrors x86ptrStart: the JIT stores its code-cache slot in both.
|
|
|
|
|
// The indirection is kept so an alternate code source can repoint hostEntry
|
|
|
|
|
// at a prepared block without disturbing the code-cache slot tracking.
|
|
|
|
|
mVUblock.hostEntry = thisPtr;
|
|
|
|
|
|
|
|
|
|
// Create block manager if needed, then add this block. Both conditions are
|
|
|
|
|
// invariant violations — the caller (mVUcompile) unconditionally proceeds
|
|
|
|
|
// into the first-pass loop and dereferences mVUpBlock, so a bare return
|
|
|
|
|
// here would just defer a NULL/stale deref into UB. Fail fast instead.
|
|
|
|
|
pxAssertRel(mVU.prog.cur, "microVU: mVUinitFirstPass with NULL mVU.prog.cur");
|
|
|
|
|
blockCreate(mVUstartPC / 2);
|
|
|
|
|
mVUpBlock = mVUblocks[mVUstartPC / 2]->add(mVU, &mVUblock);
|
|
|
|
|
pxAssertRel(mVUpBlock, "microVU: mVUpBlock NULL after blockManager::add");
|
|
|
|
|
// Register this block (manager copy + host entry) with the VU program-cache
|
|
|
|
|
// recorder so the emitted code can be persisted and reloaded across runs.
|
|
|
|
|
mVUPersist::OnBlockCompiled(mVU, mVUpBlock, thisPtr, mVUstartPC * 4);
|
|
|
|
|
mVUregs.needExactMatch = (mVUpBlock->pState.blockType) ? 7 : 0;
|
2026-07-18 19:16:38 -07:00
|
|
|
mVU.needFlagFinalize = false; // compile-scoped; see microVU-arm64.h
|
2026-06-20 20:27:55 -07:00
|
|
|
mVUregs.blockType = 0;
|
|
|
|
|
mVUregs.viBackUp = 0;
|
|
|
|
|
mVUregs.flagInfo = 0; // Must be cleared each compile: mVUsetFlags OR-updates
|
|
|
|
|
// flagInfo at end of compile, so stale bits accumulate
|
|
|
|
|
// across blocks, making every compile hash to a new
|
|
|
|
|
// pipeline state and create a new variant.
|
|
|
|
|
mVUsFlagHack = CHECK_VU_FLAGHACK;
|
|
|
|
|
|
|
|
|
|
mVUinitConstValues(mVU);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
__fi void mVUcheckBadOp(mV)
|
|
|
|
|
{
|
|
|
|
|
if (mVUinfo.isBadOp && mVU.code != 0x8000033c)
|
|
|
|
|
{
|
|
|
|
|
mVUinfo.isEOB = true;
|
|
|
|
|
DevCon.Warning("microVU Warning: Block contains an illegal opcode...");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
__fi void eBitPass1(mV, int& branch)
|
|
|
|
|
{
|
|
|
|
|
if (mVUregs.blockType != 1)
|
|
|
|
|
{
|
|
|
|
|
branch = 1;
|
|
|
|
|
mVUup.eBit = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
__fi void branchWarning(mV)
|
|
|
|
|
{
|
|
|
|
|
incPC(-2);
|
|
|
|
|
if (mVUup.eBit && mVUbranch)
|
|
|
|
|
{
|
|
|
|
|
incPC(2);
|
|
|
|
|
mVUlow.isNOP = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
incPC(2);
|
|
|
|
|
|
|
|
|
|
if (mVUinfo.isBdelay && !mVUlow.evilBranch)
|
|
|
|
|
{
|
|
|
|
|
if (mVUlow.VI_write.reg && mVUlow.VI_write.used && !mVUlow.readFlags)
|
|
|
|
|
{
|
|
|
|
|
mVUlow.backupVI = true;
|
|
|
|
|
mVUregs.viBackUp = mVUlow.VI_write.reg;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
__ri void eBitWarning(mV)
|
|
|
|
|
{
|
|
|
|
|
incPC(2);
|
|
|
|
|
if (curI & _Ebit_)
|
|
|
|
|
mVUregs.blockType = 1;
|
|
|
|
|
incPC(-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void mVUdebugPrintBlocks(mV, bool isEndPC) {}
|
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
// Main Compile Function
|
|
|
|
|
//------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
void* mVUcompile(microVU& mVU, u32 startPC, uptr pState)
|
|
|
|
|
{
|
|
|
|
|
microFlagCycles mFC;
|
|
|
|
|
// armAsm is managed by mVUexecute/mVUcompileJIT — must be active here.
|
|
|
|
|
pxAssert(armAsm);
|
|
|
|
|
u8* thisPtr = armGetCurrentCodePointer();
|
|
|
|
|
|
|
|
|
|
const u32 endCount = (((microRegInfo*)pState)->blockType) ? 1 : (mVU.microMemSize / 8);
|
|
|
|
|
|
|
|
|
|
// === First Pass (Analysis) ===
|
|
|
|
|
iPC = startPC / 4;
|
|
|
|
|
mVUsetupRange(mVU, startPC, 1);
|
|
|
|
|
mVU.regAlloc->reset(false);
|
2026-07-12 17:37:15 -07:00
|
|
|
mVUclearBranchCondCarry(mVU);
|
2026-06-20 20:27:55 -07:00
|
|
|
mVUinitFirstPass(mVU, pState, thisPtr);
|
|
|
|
|
mVUbranch = 0;
|
|
|
|
|
|
|
|
|
|
for (int branch = 0; mVUcount < endCount;)
|
|
|
|
|
{
|
|
|
|
|
incPC(1);
|
|
|
|
|
startLoop(mVU);
|
|
|
|
|
mVUincCycles(mVU, 1);
|
|
|
|
|
mVUopU(mVU, 0); // Upper analysis
|
|
|
|
|
mVUcheckBadOp(mVU);
|
|
|
|
|
|
|
|
|
|
if (curI & _Ebit_)
|
|
|
|
|
{
|
|
|
|
|
eBitPass1(mVU, branch);
|
2026-07-18 19:16:38 -07:00
|
|
|
// End-of-program flags must be valid: mVUendProgram finalises them into
|
|
|
|
|
// VI[REG_*_FLAG], so an FMAC in this block whose flag write got elided
|
|
|
|
|
// would be finalised from a stale ring instance. Upstream gates this on
|
|
|
|
|
// isVU0 (COP2 reads VU0's flags back - State of Emergency 2, Driving
|
|
|
|
|
// Emotion Type-S; x86 microVU_Compile.inl:711-717), but VU1 needs it
|
|
|
|
|
// too: mVUdispatcherA reloads the ring from VI[REG_*_FLAG] at program
|
|
|
|
|
// entry, so a stale instance survives into the next VU1 program.
|
|
|
|
|
mVU.needFlagFinalize = true;
|
2026-06-20 20:27:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// M-bit: VU0 sync point with EE. If the previous instruction was also
|
|
|
|
|
// M-bit, skip — no need to re-sync. Mirrors x86 microVU_Compile.inl:720-735.
|
|
|
|
|
if ((curI & _Mbit_) && isVU0)
|
|
|
|
|
{
|
|
|
|
|
if (xPC > 0)
|
|
|
|
|
{
|
|
|
|
|
incPC(-2);
|
|
|
|
|
if (!(curI & _Mbit_))
|
|
|
|
|
{
|
|
|
|
|
incPC(2);
|
|
|
|
|
mVUup.mBit = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
incPC(2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mVUup.mBit = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (curI & _Ibit_)
|
|
|
|
|
{
|
|
|
|
|
mVUlow.isNOP = true;
|
|
|
|
|
mVUup.iBit = true;
|
|
|
|
|
if (EmuConfig.Gamefixes.IbitHack)
|
|
|
|
|
{
|
|
|
|
|
mVUsetupRange(mVU, xPC, false);
|
|
|
|
|
if (branch < 2)
|
|
|
|
|
mVUsetupRange(mVU, xPC + 4, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
incPC(-1);
|
|
|
|
|
if (EmuConfig.Gamefixes.IbitHack)
|
|
|
|
|
{
|
|
|
|
|
// Ignore IADDI/IADDIU/ISUBU/ILW/ISW/LQ/SQ on the lower slot when
|
|
|
|
|
// IbitHack is active. Matches x86 microVU_Compile.inl:751-765.
|
|
|
|
|
const u32 upper = (mVU.code >> 25);
|
|
|
|
|
if (upper == 0x1 || upper == 0x0 || upper == 0x4 || upper == 0x5
|
|
|
|
|
|| upper == 0x8 || upper == 0x9
|
|
|
|
|
|| (upper == 0x40 && (mVU.code & 0x3F) == 0x32))
|
|
|
|
|
{
|
|
|
|
|
incPC(1);
|
|
|
|
|
mVUsetupRange(mVU, xPC, false);
|
|
|
|
|
if (branch < 2)
|
|
|
|
|
mVUsetupRange(mVU, xPC + 2, true);
|
|
|
|
|
incPC(-1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
mVUopL(mVU, 0);
|
|
|
|
|
incPC(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (curI & _Dbit_) { mVUup.dBit = true; }
|
|
|
|
|
if (curI & _Tbit_) { mVUup.tBit = true; }
|
|
|
|
|
mVUsetCycles(mVU);
|
|
|
|
|
|
|
|
|
|
if (!mVUlow.isKick)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.xgkickcycles += 1 + mVUstall;
|
|
|
|
|
if (mVUlow.isMemWrite) { mVUlow.kickcycles = mVUregs.xgkickcycles; mVUregs.xgkickcycles = 0; }
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mVUregs.xgkickcycles = 1;
|
|
|
|
|
mVUlow.kickcycles = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mVUinfo.readQ = mVU.q;
|
|
|
|
|
mVUinfo.writeQ = !mVU.q;
|
|
|
|
|
mVUinfo.readP = mVU.p && isVU1;
|
|
|
|
|
mVUinfo.writeP = !mVU.p && isVU1;
|
|
|
|
|
mVUcount++;
|
|
|
|
|
|
|
|
|
|
if (branch >= 2)
|
|
|
|
|
{
|
|
|
|
|
mVUinfo.isEOB = true;
|
|
|
|
|
if (branch == 3)
|
|
|
|
|
mVUinfo.isBdelay = true;
|
|
|
|
|
branchWarning(mVU);
|
|
|
|
|
if (mVUregs.xgkickcycles)
|
|
|
|
|
{
|
|
|
|
|
mVUlow.kickcycles = mVUregs.xgkickcycles;
|
|
|
|
|
mVUregs.xgkickcycles = 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
else if (branch == 1)
|
|
|
|
|
{
|
|
|
|
|
branch = 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mVUbranch) { mVUsetFlagInfo(mVU); eBitWarning(mVU); branch = 3; mVUbranch = 0; }
|
|
|
|
|
|
|
|
|
|
if (mVUup.mBit && !branch && !mVUup.eBit)
|
|
|
|
|
{
|
|
|
|
|
mVUregs.needExactMatch |= 7;
|
|
|
|
|
if (mVUregs.xgkickcycles)
|
|
|
|
|
{
|
|
|
|
|
mVUlow.kickcycles = mVUregs.xgkickcycles;
|
|
|
|
|
mVUregs.xgkickcycles = 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mVUinfo.isEOB)
|
|
|
|
|
{
|
|
|
|
|
if (mVUregs.xgkickcycles)
|
|
|
|
|
{
|
|
|
|
|
mVUlow.kickcycles = mVUregs.xgkickcycles;
|
|
|
|
|
mVUregs.xgkickcycles = 0;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
incPC(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mVUregs.vi15 = 0;
|
|
|
|
|
mVUregs.vi15v = 0;
|
|
|
|
|
mVUsetFlags(mVU, mFC);
|
|
|
|
|
mVUoptimizePipeState(mVU);
|
2026-07-18 18:16:40 -07:00
|
|
|
{
|
|
|
|
|
const mVUSpinLoop spin = mVUdetectSpinLoop(mVU, startPC);
|
|
|
|
|
if (spin.found)
|
|
|
|
|
mVUemitSpinFF(mVU, spin);
|
|
|
|
|
}
|
2026-06-20 20:27:55 -07:00
|
|
|
mVUtestCycles(mVU, mFC);
|
|
|
|
|
|
|
|
|
|
// === Second Pass (Codegen) ===
|
|
|
|
|
iPC = mVUstartPC;
|
|
|
|
|
setCode();
|
|
|
|
|
mVUbranch = 0;
|
|
|
|
|
u32 x = 0;
|
|
|
|
|
|
|
|
|
|
mvuPreloadRegisters(mVU, endCount);
|
|
|
|
|
|
|
|
|
|
for (; x < endCount; x++)
|
|
|
|
|
{
|
|
|
|
|
if (mVUinfo.isEOB) { x = 0xffff; }
|
|
|
|
|
|
|
|
|
|
// M-bit: signal the EE-visible M-flag so VU0 micro-mode can break/sync
|
|
|
|
|
// to the EE (VU0.cpp gates the M-bit Break on VURegs.flags & MFLAGSET).
|
|
|
|
|
// Mirrors x86 microVU_Compile.inl:890-893; VURegs.flags is always
|
|
|
|
|
// memory-resident, so no regalloc flush is needed (same as x86's
|
|
|
|
|
// direct memory xOR). Matches the VUFLAG_INTCINTERRUPT D/T-bit pattern.
|
|
|
|
|
if (mVUup.mBit)
|
|
|
|
|
{
|
|
|
|
|
armAsm->Ldr(a64::w9, mVUstateMem(offsetof(VURegs, flags)));
|
|
|
|
|
armAsm->Orr(a64::w9, a64::w9, VUFLAG_MFLAGSET);
|
|
|
|
|
armAsm->Str(a64::w9, mVUstateMem(offsetof(VURegs, flags)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isVU1 && mVUlow.kickcycles && CHECK_XGKICKHACK)
|
|
|
|
|
mVU_XGKICK_SYNC(mVU, false);
|
|
|
|
|
|
|
|
|
|
mVUexecuteInstruction(mVU);
|
|
|
|
|
|
|
|
|
|
#ifdef PCSX2_RECOMPILER_TESTS
|
|
|
|
|
// Per-op state-snapshot hook (test builds only). When enabled, flush all
|
|
|
|
|
// dirty allocator state to vuRegs[N] memory and emit a brk whose imm16
|
|
|
|
|
// encodes the op index; a test harness's SIGTRAP handler captures
|
|
|
|
|
// vuRegs[N] for that index and skips the brk. Release builds emit no
|
|
|
|
|
// per-op probe into the block.
|
|
|
|
|
if (mvu_divtrace::g_enabled.load(std::memory_order_relaxed))
|
|
|
|
|
{
|
2026-07-12 17:37:15 -07:00
|
|
|
// The brk hands control to the divtrace SIGTRAP handler — don't
|
|
|
|
|
// trust pool temps across it.
|
|
|
|
|
mVUclearBranchCondCarry(mVU);
|
|
|
|
|
|
2026-06-20 20:27:55 -07:00
|
|
|
mvu_divtrace::OpMeta meta{};
|
|
|
|
|
meta.op_idx = static_cast<u16>(mvu_divtrace::g_meta.size());
|
|
|
|
|
meta.microvu_pc = xPC;
|
|
|
|
|
// Raw 64-bit microvu instruction (lower word + upper word) at xPC.
|
|
|
|
|
std::memcpy(&meta.opcode, &mVU.regs().Micro[xPC], sizeof(u32));
|
|
|
|
|
meta.host_lo = armGetCurrentCodePointer();
|
|
|
|
|
meta.alloc = mVU.regAlloc->snapshotMaps();
|
|
|
|
|
|
|
|
|
|
mVU.regAlloc->flushAll(true);
|
|
|
|
|
|
|
|
|
|
// Flush qmmPQ (host-resident Q/P pipeline) to vuRegs.VI[REG_Q]/[REG_P]
|
|
|
|
|
// + pending_q/pending_p so vi22/vi23 are meaningful at the brk.
|
|
|
|
|
// The current lane is not known at codegen-of-op-N (mVU.q is
|
|
|
|
|
// the post-analyze final value, not the per-op value), so dump both:
|
|
|
|
|
// VI[REG_Q] := qmmPQ[0], pending_q := qmmPQ[1]. The driver compares
|
|
|
|
|
// JIT and interp Q as a multiset {VI[Q], pending_q} to tolerate the
|
|
|
|
|
// JIT/interp lane-index disagreement.
|
|
|
|
|
armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_Q * sizeof(REG_VI));
|
|
|
|
|
armAsm->St1(qmmPQ.V4S(), 0, a64::MemOperand(a64::x8));
|
|
|
|
|
armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, pending_q));
|
|
|
|
|
armAsm->St1(qmmPQ.V4S(), 1, a64::MemOperand(a64::x8));
|
|
|
|
|
if (isVU1)
|
|
|
|
|
{
|
|
|
|
|
armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, VI) + REG_P * sizeof(REG_VI));
|
|
|
|
|
armAsm->St1(qmmPQ.V4S(), 2, a64::MemOperand(a64::x8));
|
|
|
|
|
armAsm->Add(a64::x8, gprVUState, offsetof(VURegs, pending_p));
|
|
|
|
|
armAsm->St1(qmmPQ.V4S(), 3, a64::MemOperand(a64::x8));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
armAsm->Brk(meta.op_idx);
|
|
|
|
|
meta.host_hi = armGetCurrentCodePointer();
|
|
|
|
|
mvu_divtrace::g_meta.push_back(meta);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// T/D/M-bit per-instruction handling (excluding branch delay slots;
|
|
|
|
|
// those are handled after the branch itself emits). Mirrors x86
|
|
|
|
|
// microVU_Compile.inl:901-923.
|
|
|
|
|
if (!mVUinfo.isBdelay && !mVUlow.branch)
|
|
|
|
|
{
|
|
|
|
|
if (mVUup.tBit)
|
|
|
|
|
{
|
|
|
|
|
mVUDoTBit(mVU, &mFC);
|
|
|
|
|
}
|
|
|
|
|
else if (mVUup.dBit && doDBitHandling)
|
|
|
|
|
{
|
|
|
|
|
mVUDoDBit(mVU, &mFC);
|
|
|
|
|
}
|
|
|
|
|
else if (mVUup.mBit && !mVUup.eBit && !mVUinfo.isEOB)
|
|
|
|
|
{
|
|
|
|
|
// Flags must be exact: Gungrave does FCAND/FMAND with M-bit
|
|
|
|
|
// back-to-back. setupBranch sorts flag instances.
|
|
|
|
|
mVUsetupBranch(mVU, mFC);
|
|
|
|
|
// Emit a runtime snapshot of the current pipeline state into
|
|
|
|
|
// mVU.prog.lpState. Matches x86's xMOV(ptr32[lpS], cpS[0])
|
|
|
|
|
// loop: the values are compile-time constants (from mVUregs)
|
|
|
|
|
// baked into the emitted store instructions.
|
|
|
|
|
{
|
|
|
|
|
const u32* cpS = reinterpret_cast<const u32*>(&mVUregs);
|
|
|
|
|
const size_t nWords = (sizeof(microRegInfo) - 4) / 4;
|
|
|
|
|
armMoveAddressToReg(a64::x8, &mVU.prog.lpState);
|
|
|
|
|
for (size_t i = 0; i < nWords; i++)
|
|
|
|
|
{
|
|
|
|
|
armAsm->Mov(a64::w9, cpS[i]);
|
|
|
|
|
armAsm->Str(a64::w9, a64::MemOperand(a64::x8, static_cast<s32>(i * 4)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
incPC(2);
|
|
|
|
|
mVUsetupRange(mVU, xPC, false);
|
|
|
|
|
// VUSyncHack: clear nextBlockCycles (mVUendProgram only emits
|
|
|
|
|
// this for the E-bit/isEbit!=0 paths, so the M-bit case must
|
|
|
|
|
// store it explicitly). Mirrors x86 microVU_Compile.inl:926-927.
|
|
|
|
|
if (EmuConfig.Gamefixes.VUSyncHack || EmuConfig.Gamefixes.FullVU0SyncHack)
|
|
|
|
|
armAsm->Str(a64::wzr, mVUstateMem(offsetof(VURegs, nextBlockCycles)));
|
|
|
|
|
// endProgram + normBranchCompile run at iPC+2 so the saved TPC is
|
|
|
|
|
// the continuation PC (not the already-executed M-bit instruction)
|
|
|
|
|
// and the continuation block is compiled/linked into the cache.
|
|
|
|
|
// incPC(-2) is deferred until after, matching x86:928-930.
|
|
|
|
|
mVUendProgram(mVU, &mFC, 0);
|
|
|
|
|
normBranchCompile(mVU, xPC);
|
|
|
|
|
incPC(-2);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (mVUinfo.doXGKICK)
|
|
|
|
|
mVU_XGKICK_DELAY(mVU);
|
|
|
|
|
|
|
|
|
|
if (isEvilBlock)
|
|
|
|
|
{
|
|
|
|
|
mVUsetupRange(mVU, xPC + 8, false);
|
|
|
|
|
normJumpCompile(mVU, mFC, true);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
}
|
|
|
|
|
else if (!mVUinfo.isBdelay)
|
|
|
|
|
{
|
|
|
|
|
if ((xPC + 8) == mVU.microMemSize)
|
|
|
|
|
{
|
|
|
|
|
mVUsetupRange(mVU, xPC + 8, false);
|
|
|
|
|
mVUsetupRange(mVU, 0, 1);
|
|
|
|
|
}
|
|
|
|
|
incPC(1);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
incPC(1);
|
|
|
|
|
mVUsetupRange(mVU, xPC, false);
|
|
|
|
|
incPC(-4); // Go back to branch opcode
|
|
|
|
|
|
|
|
|
|
switch (mVUlow.branch)
|
|
|
|
|
{
|
|
|
|
|
case 1: case 2: // B/BAL
|
|
|
|
|
normBranch(mVU, mFC);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
case 9: case 10: // JR/JALR
|
|
|
|
|
normJump(mVU, mFC);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
case 3: // IBEQ
|
|
|
|
|
condBranch(mVU, mFC, a64::eq);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
case 4: // IBGEZ
|
|
|
|
|
condBranch(mVU, mFC, a64::ge);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
case 5: // IBGTZ
|
|
|
|
|
condBranch(mVU, mFC, a64::gt);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
case 6: // IBLEQ
|
|
|
|
|
condBranch(mVU, mFC, a64::le);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
case 7: // IBLTZ
|
|
|
|
|
condBranch(mVU, mFC, a64::lt);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
case 8: // IBNE
|
|
|
|
|
condBranch(mVU, mFC, a64::ne);
|
|
|
|
|
goto perf_and_return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// E-bit end
|
|
|
|
|
mVUsetupRange(mVU, xPC, false);
|
|
|
|
|
mVUendProgram(mVU, &mFC, 1);
|
|
|
|
|
|
|
|
|
|
perf_and_return:
|
2026-06-23 20:18:41 -07:00
|
|
|
// Register the program-entry compile only, not every continuation block
|
|
|
|
|
// compiled at a mid-program PC (start_pc stays the program entry for
|
|
|
|
|
// branch-target sub-blocks; it's only re-pointed on the dispatch/indirect-
|
|
|
|
|
// jump path). Mirrors x86 microVU_Compile.inl (a5ed24ca8) — one clean
|
|
|
|
|
// jitdump symbol per VU program rather than a symbol per linked sub-block.
|
|
|
|
|
if (mVU.regs().start_pc == startPC)
|
2026-06-20 20:27:55 -07:00
|
|
|
{
|
|
|
|
|
u8* endPtr = armGetCurrentCodePointer();
|
|
|
|
|
if (mVU.index)
|
|
|
|
|
Perf::vu1.RegisterPC(thisPtr, static_cast<u32>(endPtr - thisPtr), startPC);
|
|
|
|
|
else
|
|
|
|
|
Perf::vu0.RegisterPC(thisPtr, static_cast<u32>(endPtr - thisPtr), startPC);
|
|
|
|
|
}
|
|
|
|
|
return thisPtr;
|
|
|
|
|
}
|