Files
ARMSX2/pcsx2/arm64/Vif_UnpackNEON.cpp
Brian Degenhardt 3e077eff9b Merge yaps2: arm64 JIT transplant + test/perf/libretro infrastructure
Merges yaps2/main (github.com/yaps2/yaps2, c16b88cb7) into ARMSX2,
replacing the arm64 recompiler family with the yaps2 JITs and importing
the yaps2 testing, perf, and libretro infrastructure. Common ancestor is
upstream PCSX2 342db5152 (2026-06-19); git auto-merged all but 38 files.

Replaced (deleted in this merge, recoverable from history):
- arm64/aR5900*, aR3000A*, aVU* -> arm64/iR5900*/iR3000A*/microVU*-arm64:
  EE static-pin register file with lazy dirty tracking, dual-residence
  allocator, IOP block linking, native COP2 macro ops, inline unaligned
  fastmem, persisted VU program cache, call-ret shadow ring, VU0 spin
  fast-forward.
- MVU_DIFF shadow-run hooks in shared VU interpreter TUs (superseded by
  the offline vurunner JIT-vs-interp oracle).

Imported from yaps2:
- tests/ctest/core/recompilers: ~80 gtest suites (EE/IOP/VU differential
  harnesses, fuzzers, ABI digest tripwire, capture format pins) plus the
  gs_vertex_tests kernel oracle.
- pcsx2-vurunner / pcsx2-eerunner headless capture-replay runners.
- tools/perf counter-based A/B rigs, perf jitdump productionization,
  PmuCounters, clang-perf/clang-handheld presets.
- pcsx2-libretro core (ENABLE_LIBRETRO, default OFF; rename pending).
- GS vertex-kick fast path (GV series): TBL-based packed parse,
  register-resident kick, scalar-outcode cull, fused draw-rect/FindMinMax.
- Null renderer, VK_KHR_display direct WSI, swapchain PresentStats.
- SPU2 NEON mixer vectorization, EE timer read clamp (NFL 2K5 hang),
  IOP ioman signed-compare fix, assorted UB fixes.

Kept from ARMSX2 in the both-touched files:
- iOS dual-map W^X and fastmem-unavailable resilience (Memory, HostSys,
  vtlb). The split data/code area model is retained; both areas now take
  fixed VA hints so cached VU JIT code stays deterministic on Linux.
- Android thread-affinity model, VMState shutdown early-outs, all
  platform frontends, branding, CI, RetroAchievements identity/policy.
- GSDeviceVK: ARMSX2's push-descriptor decision logic (Mali crash gate,
  proprietary-vs-turnip Adreno split) merged with yaps2's descriptor-pool
  exhaustion recovery (flush + render-pass restart instead of dropped
  binds). Vendor feature policy is the union: Mali fbfetch policy with
  MediaTek/G57/Xclipse gates from ARMSX2; Adreno stencil/ROV/
  test-and-sample-depth hang avoidance and no_ps2_z_quantization from
  yaps2.

Build-system notes:
- The Qt debugger is now gated behind ENABLE_QT_DEBUGGER (default off on
  arm64) so handheld builds drop the KDDockWidgets dependency.
- GSDeviceNone and remaining yaps2 GS code were ported to the newer
  upstream GSTexture Usage-flags API.

The replaced backend's interpreter-fallback glue (intExecuteOneInst,
AndroidEEOpHist) and the EEDiffVerify runtime differ are retained for
now; dead pieces will be removed in a follow-up commit.
2026-07-19 10:24:29 -07:00

433 lines
9.7 KiB
C++

// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-FileCopyrightText: 2026 isztld <https://isztld.com/>
// SPDX-License-Identifier: GPL-3.0
#include "Vif_UnpackNEON.h"
#include "common/Perf.h"
namespace a64 = vixl::aarch64;
// =====================================================================================================
// VifUnpackSSE_Base Section
// =====================================================================================================
VifUnpackNEON_Base::VifUnpackNEON_Base()
: usn(false)
, doMask(false)
, UnpkLoopIteration(0)
, UnpkNoOfIterations(0)
, IsAligned(0)
, dstIndirect(a64::MemOperand(RXARG1))
, srcIndirect(a64::MemOperand(RXARG2))
, workReg(a64::q1)
, destReg(a64::q0)
, workGprW(a64::w4)
{
}
void VifUnpackNEON_Base::xMovDest() const
{
if (!IsWriteProtectedOp())
{
if (IsUnmaskedOp())
armAsm->Str(destReg, dstIndirect);
else
doMaskWrite(destReg);
}
}
void VifUnpackNEON_Base::xShiftR(const vixl::aarch64::VRegister& regX, int n) const
{
if (usn)
armAsm->Ushr(regX.V4S(), regX.V4S(), n);
else
armAsm->Sshr(regX.V4S(), regX.V4S(), n);
}
void VifUnpackNEON_Base::xPMOVXX8(const vixl::aarch64::VRegister& regX) const
{
// TODO(Stenzek): Check this
armAsm->Ldr(regX.S(), srcIndirect);
if (usn)
{
armAsm->Ushll(regX.V8H(), regX.V8B(), 0);
armAsm->Ushll(regX.V4S(), regX.V4H(), 0);
}
else
{
armAsm->Sshll(regX.V8H(), regX.V8B(), 0);
armAsm->Sshll(regX.V4S(), regX.V4H(), 0);
}
}
void VifUnpackNEON_Base::xPMOVXX16(const vixl::aarch64::VRegister& regX) const
{
armAsm->Ldr(regX.D(), srcIndirect);
if (usn)
armAsm->Ushll(regX.V4S(), regX.V4H(), 0);
else
armAsm->Sshll(regX.V4S(), regX.V4H(), 0);
}
void VifUnpackNEON_Base::xUPK_S_32() const
{
if (UnpkLoopIteration == 0)
armAsm->Ldr(workReg, srcIndirect);
if (IsInputMasked())
return;
switch (UnpkLoopIteration)
{
case 0:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 0);
break;
case 1:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 1);
break;
case 2:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 2);
break;
case 3:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 3);
break;
}
}
void VifUnpackNEON_Base::xUPK_S_16() const
{
if (UnpkLoopIteration == 0)
xPMOVXX16(workReg);
if (IsInputMasked())
return;
switch (UnpkLoopIteration)
{
case 0:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 0);
break;
case 1:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 1);
break;
case 2:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 2);
break;
case 3:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 3);
break;
}
}
void VifUnpackNEON_Base::xUPK_S_8() const
{
if (UnpkLoopIteration == 0)
xPMOVXX8(workReg);
if (IsInputMasked())
return;
switch (UnpkLoopIteration)
{
case 0:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 0);
break;
case 1:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 1);
break;
case 2:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 2);
break;
case 3:
armAsm->Dup(destReg.V4S(), workReg.V4S(), 3);
break;
}
}
// The V2 + V3 unpacks have freaky behaviour, the manual claims "indeterminate".
// After testing on the PS2, it's very much determinate in 99% of cases
// and games like Lemmings, And1 Streetball rely on this data to be like this!
// I have commented after each shuffle to show what data is going where - Ref
void VifUnpackNEON_Base::xUPK_V2_32() const
{
if (UnpkLoopIteration == 0)
{
armAsm->Ldr(workReg, srcIndirect);
if (IsInputMasked())
return;
armAsm->Dup(destReg.V2D(), workReg.V2D(), 0); //v1v0v1v0
if (IsAligned)
armAsm->Ins(destReg.V4S(), 3, a64::wzr); //zero last word - tested on ps2
}
else
{
if (IsInputMasked())
return;
armAsm->Dup(destReg.V2D(), workReg.V2D(), 1); //v3v2v3v2
if (IsAligned)
armAsm->Ins(destReg.V4S(), 3, a64::wzr); //zero last word - tested on ps2
}
}
void VifUnpackNEON_Base::xUPK_V2_16() const
{
if (UnpkLoopIteration == 0)
{
xPMOVXX16(workReg);
if (IsInputMasked())
return;
armAsm->Dup(destReg.V2D(), workReg.V2D(), 0); //v1v0v1v0
}
else
{
if (IsInputMasked())
return;
armAsm->Dup(destReg.V2D(), workReg.V2D(), 1); //v3v2v3v2
}
}
void VifUnpackNEON_Base::xUPK_V2_8() const
{
if (UnpkLoopIteration == 0)
{
xPMOVXX8(workReg);
if (IsInputMasked())
return;
armAsm->Dup(destReg.V2D(), workReg.V2D(), 0); //v1v0v1v0
}
else
{
if (IsInputMasked())
return;
armAsm->Dup(destReg.V2D(), workReg.V2D(), 1); //v3v2v3v2
}
}
void VifUnpackNEON_Base::xUPK_V3_32() const
{
if (IsInputMasked())
return;
armAsm->Ldr(destReg, srcIndirect);
if (UnpkLoopIteration != IsAligned)
armAsm->Ins(destReg.V4S(), 3, a64::wzr);
}
void VifUnpackNEON_Base::xUPK_V3_16() const
{
if (IsInputMasked())
return;
xPMOVXX16(destReg);
//With V3-16, it takes the first vector from the next position as the W vector
//However - IF the end of this iteration of the unpack falls on a quadword boundary, W becomes 0
//IsAligned is the position through the current QW in the vif packet
//Iteration counts where we are in the packet.
int result = (((UnpkLoopIteration / 4) + 1 + (4 - IsAligned)) & 0x3);
if ((UnpkLoopIteration & 0x1) == 0 && result == 0)
armAsm->Ins(destReg.V4S(), 3, a64::wzr); //zero last word on QW boundary if whole 32bit word is used - tested on ps2
}
void VifUnpackNEON_Base::xUPK_V3_8() const
{
if (IsInputMasked())
return;
xPMOVXX8(destReg);
if (UnpkLoopIteration != IsAligned)
armAsm->Ins(destReg.V4S(), 3, a64::wzr);
}
void VifUnpackNEON_Base::xUPK_V4_32() const
{
if (IsInputMasked())
return;
armAsm->Ldr(destReg.Q(), a64::MemOperand(srcIndirect));
}
void VifUnpackNEON_Base::xUPK_V4_16() const
{
if (IsInputMasked())
return;
xPMOVXX16(destReg);
}
void VifUnpackNEON_Base::xUPK_V4_8() const
{
if (IsInputMasked())
return;
xPMOVXX8(destReg);
}
void VifUnpackNEON_Base::xUPK_V4_5() const
{
if (IsInputMasked())
return;
armAsm->Ldrh(workGprW, srcIndirect);
armAsm->Lsl(workGprW, workGprW, 3); // ABG|R5.000
armAsm->Dup(destReg.V4S(), workGprW); // x|x|x|R
armAsm->Lsr(workGprW, workGprW, 8); // ABG
armAsm->Lsl(workGprW, workGprW, 3); // AB|G5.000
armAsm->Ins(destReg.V4S(), 1, workGprW); // x|x|G|R
armAsm->Lsr(workGprW, workGprW, 8); // AB
armAsm->Lsl(workGprW, workGprW, 3); // A|B5.000
armAsm->Ins(destReg.V4S(), 2, workGprW); // x|B|G|R
armAsm->Lsr(workGprW, workGprW, 8); // A
armAsm->Lsl(workGprW, workGprW, 7); // A.0000000
armAsm->Ins(destReg.V4S(), 3, workGprW); // A|B|G|R
// Zero the upper 24 bits of each lane (lanes carry per-channel
// Lsr/Lsl residue from the unpack chain above). Shl+Ushr is 2
// NEON insns; an AND with a pre-built mask is the same count
// (Movi #0xFF + And) so no win — keep the shift form.
armAsm->Shl(destReg.V4S(), destReg.V4S(), 24);
armAsm->Ushr(destReg.V4S(), destReg.V4S(), 24);
}
void VifUnpackNEON_Base::xUnpack(int upknum) const
{
switch (upknum)
{
case 0:
xUPK_S_32();
break;
case 1:
xUPK_S_16();
break;
case 2:
xUPK_S_8();
break;
case 4:
xUPK_V2_32();
break;
case 5:
xUPK_V2_16();
break;
case 6:
xUPK_V2_8();
break;
case 8:
xUPK_V3_32();
break;
case 9:
xUPK_V3_16();
break;
case 10:
xUPK_V3_8();
break;
case 12:
xUPK_V4_32();
break;
case 13:
xUPK_V4_16();
break;
case 14:
xUPK_V4_8();
break;
case 15:
xUPK_V4_5();
break;
case 3:
case 7:
case 11:
// TODO: Needs hardware testing.
// Dynasty Warriors 5: Empire - Player 2 chose a character menu.
Console.Warning("Vpu/Vif: Invalid Unpack %d", upknum);
break;
}
}
// =====================================================================================================
// VifUnpackSSE_Simple
// =====================================================================================================
VifUnpackNEON_Simple::VifUnpackNEON_Simple(bool usn_, bool domask_, int curCycle_)
{
curCycle = curCycle_;
usn = usn_;
doMask = domask_;
IsAligned = true;
}
void VifUnpackNEON_Simple::doMaskWrite(const vixl::aarch64::VRegister& regX) const
{
armAsm->Ldr(a64::q7, dstIndirect);
int offX = std::min(curCycle, 3);
armMoveAddressToReg(RXVIXLSCRATCH, nVifMask);
armAsm->Ldr(a64::q29, a64::MemOperand(RXVIXLSCRATCH, reinterpret_cast<const u8*>(nVifMask[0][offX]) - reinterpret_cast<const u8*>(nVifMask)));
armAsm->Ldr(a64::q30, a64::MemOperand(RXVIXLSCRATCH, reinterpret_cast<const u8*>(nVifMask[1][offX]) - reinterpret_cast<const u8*>(nVifMask)));
armAsm->Ldr(a64::q31, a64::MemOperand(RXVIXLSCRATCH, reinterpret_cast<const u8*>(nVifMask[2][offX]) - reinterpret_cast<const u8*>(nVifMask)));
armAsm->And(regX.V16B(), regX.V16B(), a64::q29.V16B());
armAsm->And(a64::q7.V16B(), a64::q7.V16B(), a64::q30.V16B());
armAsm->Orr(regX.V16B(), regX.V16B(), a64::q31.V16B());
armAsm->Orr(regX.V16B(), regX.V16B(), a64::q7.V16B());
armAsm->Str(regX, dstIndirect);
}
// ecx = dest, edx = src
static void nVifGen(int usn, int mask, int curCycle)
{
int usnpart = usn * 2 * 16;
int maskpart = mask * 16;
VifUnpackNEON_Simple vpugen(!!usn, !!mask, curCycle);
for (int i = 0; i < 16; ++i)
{
nVifCall& ucall(nVifUpk[((usnpart + maskpart + i) * 4) + curCycle]);
ucall = NULL;
if (nVifT[i] == 0)
continue;
ucall = (nVifCall)armStartBlock();
vpugen.xUnpack(i);
vpugen.xMovDest();
armAsm->Ret();
armEndBlock();
}
}
void VifUnpackSSE_Init()
{
DevCon.WriteLn("Generating NEON-optimized unpacking functions for VIF interpreters...");
HostSys::BeginCodeWrite();
armSetAsmPtr(SysMemory::GetVIFUnpackRec(), SysMemory::GetVIFUnpackRecEnd() - SysMemory::GetVIFUnpackRec(), nullptr);
for (int a = 0; a < 2; a++)
{
for (int b = 0; b < 2; b++)
{
for (int c = 0; c < 4; c++)
{
nVifGen(a, b, c);
}
}
}
Perf::any.Register(SysMemory::GetVIFUnpackRec(), armGetAsmPtr() - SysMemory::GetVIFUnpackRec(), "VIF Unpack");
HostSys::EndCodeWrite();
}