// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-FileCopyrightText: 2026 isztld
// 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(nVifMask[0][offX]) - reinterpret_cast(nVifMask)));
armAsm->Ldr(a64::q30, a64::MemOperand(RXVIXLSCRATCH, reinterpret_cast(nVifMask[1][offX]) - reinterpret_cast(nVifMask)));
armAsm->Ldr(a64::q31, a64::MemOperand(RXVIXLSCRATCH, reinterpret_cast(nVifMask[2][offX]) - reinterpret_cast(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();
}