Files
pstef a521b590c4 EE: drop QFSRV's adjacent-source fast path
Rs == Rt+1 puts the two sources next to each other in the GPR array, so
a 16-byte read at &GPR.r[Rt] + sa funnelled them without a TBL. It was
the slower of the two sequences on both cores at every sa, and worst on
the twelve adjacent pairs that have a pinned source - the register range
a funnel-shift memcpy loop uses.

armFlushEEGPRPin had no other caller.

The sa mask stays, but it no longer guards a host address. Every writer
of SA - MTSA, MTSAB, MTSAH - already keeps four bits, so the two
oversized-sa tests could not fail; one goes with the path it named and
the other says what it covers.
2026-08-09 11:21:33 +02:00

1504 lines
49 KiB
C++

// SPDX-FileCopyrightText: 2026 yaps2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// ARM64 EE MMI (Multimedia Instructions) Codegen — NEON-based
//
// All MMI instructions are 128-bit SIMD operations on the EE's 128-bit GPRs.
// NEON Q registers are used throughout: load from cpuRegs.GPR, operate, store back.
#include "arm64/iR5900-arm64.h"
#include "arm64/AsmHelpers.h"
#include "common/Assertions.h"
namespace a64 = vixl::aarch64;
namespace R5900 {
namespace Dynarec {
namespace OpcodeImpl {
namespace MMI {
namespace Interp = R5900::Interpreter::OpcodeImpl::MMI;
// ============================================================================
// Helpers for 128-bit GPR load/store
// ============================================================================
// Flush any const propagation state for a register and invalidate allocations.
// Must be called before reading a register's 128-bit value from memory,
// since const prop only tracks the lower 64 bits.
static void mmiFlushReg(int reg)
{
if (reg == 0) return;
if (GPR_IS_CONST1(reg))
{
// Const prop only has lower 64 bits — flush to memory so upper 64 bits
// are preserved alongside the correct lower 64 bits.
_flushEEreg(reg);
}
_deleteEEreg(reg, 1);
}
// Prepare destination: invalidate const/alloc state (the full 128 bits will be overwritten)
static void mmiInvalidateDest(int reg)
{
if (reg == 0) return;
_deleteEEreg(reg, 0);
GPR_DEL_CONST(reg);
}
// Load 128-bit GPR into a NEON Q register
static void mmiLoadReg(const a64::VRegister& qreg, int gpr)
{
if (gpr == 0)
{
// r0 is always zero
armAsm->Movi(qreg.V16B(), 0);
}
else
{
armAsm->Ldr(qreg, armCpuRegMem(&cpuRegs.GPR.r[gpr].UQ));
// Lazy-dirty / residency: merge a dirty pin OR scalar slot over the
// possibly-stale lower half.
armMergeEEResidentIntoQuad(qreg, gpr);
}
}
// Store 128-bit NEON Q register to GPR
static void mmiStoreReg(int gpr, const a64::VRegister& qreg)
{
pxAssert(gpr != 0);
armStoreEEGPRQuad(qreg, gpr);
}
// Standard 3-operand MMI: rd = rs OP rt (128-bit).
//
// Routes through eeRecompileCodeXMM so consecutive MMI ops on the same guest
// register stay register-resident in the allocator-managed NEON pool instead
// of bouncing through memory. Allocator handles const tracking, GPR-side
// eviction, NaN-zero of r0, and "already in NEON" reuse.
//
// Each user gets three locals:
// qs — VRegister view of EEREC_S (Rs input, MODE_READ)
// qt — VRegister view of EEREC_T (Rt input, MODE_READ)
// qd — VRegister view of EEREC_D (Rd output, MODE_WRITE)
//
// Functions that need a fourth temp can use RQSCRATCH / RQSCRATCH2 — both are
// outside the allocator pool. Do NOT clobber qs or qt
// before the final write to qd, otherwise the allocator's MODE_READ state
// for them is invalidated.
#define MMI_3OP_SETUP() \
if (!_Rd_) return; \
int info = eeRecompileCodeXMM(XMMINFO_READS | XMMINFO_READT | XMMINFO_WRITED); \
const a64::VRegister qs = armQRegister(EEREC_S); \
const a64::VRegister qt = armQRegister(EEREC_T); \
const a64::VRegister qd = armQRegister(EEREC_D); \
(void)info
// 2-operand: rd = OP(rt).
// qt — VRegister view of EEREC_T (Rt input, MODE_READ)
// qd — VRegister view of EEREC_D (Rd output, MODE_WRITE)
#define MMI_2OP_SETUP() \
if (!_Rd_) return; \
int info = eeRecompileCodeXMM(XMMINFO_READT | XMMINFO_WRITED); \
const a64::VRegister qt = armQRegister(EEREC_T); \
const a64::VRegister qd = armQRegister(EEREC_D); \
(void)info
// ============================================================================
// Logical Operations (128-bit)
// ============================================================================
void recPAND()
{
MMI_3OP_SETUP();
armAsm->And(qd.V16B(), qs.V16B(), qt.V16B());
}
void recPOR()
{
if (!_Rd_)
return;
// `por rd, r0, rt` is the canonical PS2 128-bit register-move idiom and is
// common. Special-case an r0 operand to avoid allocating r0 into a NEON reg
// and materialize a zero just to OR it in (conditional XMMINFO,
// Movi when both r0, register-copy when one is r0).
const bool s_zero = (_Rs_ == 0);
const bool t_zero = (_Rt_ == 0);
int info = eeRecompileCodeXMM((s_zero ? 0 : XMMINFO_READS) | (t_zero ? 0 : XMMINFO_READT) | XMMINFO_WRITED);
const a64::VRegister qd = armQRegister(EEREC_D);
if (s_zero && t_zero)
armAsm->Movi(qd.V2D(), 0);
else if (s_zero)
armAsm->Mov(qd.V16B(), armQRegister(EEREC_T).V16B());
else if (t_zero)
armAsm->Mov(qd.V16B(), armQRegister(EEREC_S).V16B());
else
armAsm->Orr(qd.V16B(), armQRegister(EEREC_S).V16B(), armQRegister(EEREC_T).V16B());
}
void recPXOR()
{
MMI_3OP_SETUP();
armAsm->Eor(qd.V16B(), qs.V16B(), qt.V16B());
}
void recPNOR()
{
MMI_3OP_SETUP();
armAsm->Orr(qd.V16B(), qs.V16B(), qt.V16B());
armAsm->Not(qd.V16B(), qd.V16B());
}
// ============================================================================
// Packed Arithmetic — Signed
// ============================================================================
void recPADDW()
{
MMI_3OP_SETUP();
armAsm->Add(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPSUBW()
{
MMI_3OP_SETUP();
armAsm->Sub(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPADDH()
{
MMI_3OP_SETUP();
armAsm->Add(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPSUBH()
{
MMI_3OP_SETUP();
armAsm->Sub(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPADDB()
{
MMI_3OP_SETUP();
armAsm->Add(qd.V16B(), qs.V16B(), qt.V16B());
}
void recPSUBB()
{
MMI_3OP_SETUP();
armAsm->Sub(qd.V16B(), qs.V16B(), qt.V16B());
}
// ============================================================================
// Packed Arithmetic — Unsigned
// ============================================================================
void recPADDUW()
{
MMI_3OP_SETUP();
armAsm->Uqadd(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPSUBUW()
{
MMI_3OP_SETUP();
armAsm->Uqsub(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPADDUH()
{
MMI_3OP_SETUP();
armAsm->Uqadd(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPSUBUH()
{
MMI_3OP_SETUP();
armAsm->Uqsub(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPADDUB()
{
MMI_3OP_SETUP();
armAsm->Uqadd(qd.V16B(), qs.V16B(), qt.V16B());
}
void recPSUBUB()
{
MMI_3OP_SETUP();
armAsm->Uqsub(qd.V16B(), qs.V16B(), qt.V16B());
}
// ============================================================================
// Packed Arithmetic — Saturating Signed
// ============================================================================
void recPADDSW()
{
MMI_3OP_SETUP();
armAsm->Sqadd(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPSUBSW()
{
MMI_3OP_SETUP();
armAsm->Sqsub(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPADDSH()
{
MMI_3OP_SETUP();
armAsm->Sqadd(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPSUBSH()
{
MMI_3OP_SETUP();
armAsm->Sqsub(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPADDSB()
{
MMI_3OP_SETUP();
armAsm->Sqadd(qd.V16B(), qs.V16B(), qt.V16B());
}
void recPSUBSB()
{
MMI_3OP_SETUP();
armAsm->Sqsub(qd.V16B(), qs.V16B(), qt.V16B());
}
// ============================================================================
// Packed Compare — Greater Than (signed)
// ============================================================================
void recPCGTW()
{
MMI_3OP_SETUP();
armAsm->Cmgt(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPCGTH()
{
MMI_3OP_SETUP();
armAsm->Cmgt(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPCGTB()
{
MMI_3OP_SETUP();
armAsm->Cmgt(qd.V16B(), qs.V16B(), qt.V16B());
}
// ============================================================================
// Packed Compare — Equal
// ============================================================================
void recPCEQW()
{
MMI_3OP_SETUP();
armAsm->Cmeq(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPCEQH()
{
MMI_3OP_SETUP();
armAsm->Cmeq(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPCEQB()
{
MMI_3OP_SETUP();
armAsm->Cmeq(qd.V16B(), qs.V16B(), qt.V16B());
}
// ============================================================================
// Packed Min/Max (signed)
// ============================================================================
void recPMAXW()
{
MMI_3OP_SETUP();
armAsm->Smax(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPMINW()
{
MMI_3OP_SETUP();
armAsm->Smin(qd.V4S(), qs.V4S(), qt.V4S());
}
void recPMAXH()
{
MMI_3OP_SETUP();
armAsm->Smax(qd.V8H(), qs.V8H(), qt.V8H());
}
void recPMINH()
{
MMI_3OP_SETUP();
armAsm->Smin(qd.V8H(), qs.V8H(), qt.V8H());
}
// ============================================================================
// Packed Absolute Value (signed)
// ============================================================================
void recPABSW()
{
MMI_2OP_SETUP();
// PS2 PABSW saturates INT_MIN → INT_MAX (per MMI.cpp _PABSW). NEON Abs
// preserves INT_MIN; Sqabs is the saturating form that matches.
armAsm->Sqabs(qd.V4S(), qt.V4S());
}
void recPABSH()
{
MMI_2OP_SETUP();
// Mirror of PABSW for halfword lanes.
armAsm->Sqabs(qd.V8H(), qt.V8H());
}
// ============================================================================
// Register Copy / Move
// ============================================================================
// PCPYLD: rd = { rs.UD[0], rt.UD[0] } — copy (concatenate) lower doubleword of each source.
void recPCPYLD()
{
MMI_3OP_SETUP();
armAsm->Zip1(qd.V2D(), qt.V2D(), qs.V2D());
}
// PCPYUD: rd = {rt[127:64], rs[127:64]} — upper doublewords interleaved
void recPCPYUD()
{
MMI_3OP_SETUP();
armAsm->Zip2(qd.V2D(), qs.V2D(), qt.V2D());
}
// PCPYH: rd = {rt.UH[4] x4, rt.UH[0] x4} — replicate halfwords.
// Register-resident via the allocator (MMI_2OP_SETUP) instead of a
// memory-bounce (Ldr q from Rt + Str q to Rd + const flush), matching
// sibling single-source MMI ops (PABSW/PCPYLD). Saves a full-width load
// + store per execution.
// Broadcast rt.H[4] into scratch FIRST so the qd==qt aliased case stays correct
// (a qd write would otherwise clobber rt before H[4] is read).
void recPCPYH()
{
MMI_2OP_SETUP();
armAsm->Dup(RQSCRATCH.V8H(), qt.V8H(), 4); // rt.H[4] x8 (read qt before qd write)
armAsm->Dup(qd.V8H(), qt.V8H(), 0); // qd = rt.H[0] x8
armAsm->Mov(qd.V2D(), 1, RQSCRATCH.V2D(), 0); // upper 64 <- rt.H[4] x4
}
// PMFHI: rd = HI (128-bit)
void recPMFHI()
{
if (!_Rd_) return;
mmiInvalidateDest(_Rd_);
armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ));
mmiStoreReg(_Rd_, RQSCRATCH);
}
// PMFLO: rd = LO (128-bit)
void recPMFLO()
{
if (!_Rd_) return;
mmiInvalidateDest(_Rd_);
armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
mmiStoreReg(_Rd_, RQSCRATCH);
}
// PMTHI: HI = rs (128-bit). Take Rs from its NEON slot when allocated and store
// straight to HI memory. HI is never NEON-resident in the EE rec (no opcode
// passes XMMINFO_*HI — see iR5900Templates-arm64.cpp), so no allocator
// invalidation is needed.
void recPMTHI()
{
int info = eeRecompileCodeXMM(XMMINFO_READS);
(void)info;
armAsm->Str(armQRegister(EEREC_S), armCpuRegMem(&cpuRegs.HI.UQ));
}
// PMTLO: LO = rs (128-bit). LO is never NEON-resident (see recPMTHI).
void recPMTLO()
{
int info = eeRecompileCodeXMM(XMMINFO_READS);
(void)info;
armAsm->Str(armQRegister(EEREC_S), armCpuRegMem(&cpuRegs.LO.UQ));
}
// ============================================================================
// Packed Shifts (by immediate sa field)
// ============================================================================
// GE-16: allocator-resident single-insn shifts through the XMM template
// (rd = SHIFT(rt)) — the old shape memory-bounced via mmiFlush+Ldr+op+Str
// and evicted residency at every immediate shift. sa==0 degenerates to a
// register move (skipped when the allocator aliased rd onto rt).
void recPSLLW()
{
MMI_2OP_SETUP();
if (_Sa_ == 0)
{
if (!qd.Is(qt))
armAsm->Mov(qd.V16B(), qt.V16B());
return;
}
armAsm->Shl(qd.V4S(), qt.V4S(), _Sa_);
}
void recPSRLW()
{
MMI_2OP_SETUP();
if (_Sa_ == 0)
{
if (!qd.Is(qt))
armAsm->Mov(qd.V16B(), qt.V16B());
return;
}
armAsm->Ushr(qd.V4S(), qt.V4S(), _Sa_);
}
void recPSRAW()
{
MMI_2OP_SETUP();
if (_Sa_ == 0)
{
if (!qd.Is(qt))
armAsm->Mov(qd.V16B(), qt.V16B());
return;
}
armAsm->Sshr(qd.V4S(), qt.V4S(), _Sa_);
}
// Halfword shifts: interp uses (_Sa_ & 0xf) per MMI.cpp:228/240/252 —
// only 4 of the 5 sa bits are live since the lane is 16-bit. vixl
// Shl/Ushr/Sshr V8H require shift ∈ [0,15]; mask up front to match
// interp and stay inside the encoder's range.
void recPSLLH()
{
MMI_2OP_SETUP();
const u32 sa = _Sa_ & 0xf;
if (sa == 0)
{
if (!qd.Is(qt))
armAsm->Mov(qd.V16B(), qt.V16B());
return;
}
armAsm->Shl(qd.V8H(), qt.V8H(), sa);
}
void recPSRLH()
{
MMI_2OP_SETUP();
const u32 sa = _Sa_ & 0xf;
if (sa == 0)
{
if (!qd.Is(qt))
armAsm->Mov(qd.V16B(), qt.V16B());
return;
}
armAsm->Ushr(qd.V8H(), qt.V8H(), sa);
}
void recPSRAH()
{
MMI_2OP_SETUP();
const u32 sa = _Sa_ & 0xf;
if (sa == 0)
{
if (!qd.Is(qt))
armAsm->Mov(qd.V16B(), qt.V16B());
return;
}
armAsm->Sshr(qd.V8H(), qt.V8H(), sa);
}
// ============================================================================
// Pack / Unpack (Extend / Compress)
// ============================================================================
// PEXTLW: interleave lower 32-bit words of rs and rt
// rd = {rs.UL[1], rt.UL[1], rs.UL[0], rt.UL[0]}
void recPEXTLW()
{
MMI_3OP_SETUP();
armAsm->Zip1(qd.V4S(), qt.V4S(), qs.V4S());
}
// PEXTUW: interleave upper 32-bit words of rs and rt
// rd = {rs.UL[3], rt.UL[3], rs.UL[2], rt.UL[2]}
void recPEXTUW()
{
MMI_3OP_SETUP();
armAsm->Zip2(qd.V4S(), qt.V4S(), qs.V4S());
}
// PEXTLH: interleave lower 16-bit halfwords
void recPEXTLH()
{
if (!_Rd_)
return;
// rs==0 fast path: the odd output halfwords are all zero, so this
// is just a zero-extend of rt's lower 4 halfwords to words. Skip requesting
// XMMINFO_READS — otherwise the allocator pins a callee-saved NEON reg and
// materializes a zero vector for r0 just to Zip it in. Zip1(qt,qt) duplicates
// each halfword, then Ushr clears the high half of every word lane.
if (_Rs_ == 0)
{
int info = eeRecompileCodeXMM(XMMINFO_READT | XMMINFO_WRITED);
const a64::VRegister qt = armQRegister(EEREC_T);
const a64::VRegister qd = armQRegister(EEREC_D);
(void)info;
armAsm->Zip1(qd.V8H(), qt.V8H(), qt.V8H()); // {H0,H0,H1,H1,H2,H2,H3,H3}
armAsm->Ushr(qd.V4S(), qd.V4S(), 16); // {0:H0, 0:H1, 0:H2, 0:H3}
return;
}
MMI_3OP_SETUP();
armAsm->Zip1(qd.V8H(), qt.V8H(), qs.V8H());
}
// PEXTUH: interleave upper 16-bit halfwords
void recPEXTUH()
{
MMI_3OP_SETUP();
armAsm->Zip2(qd.V8H(), qt.V8H(), qs.V8H());
}
// PEXTLB: interleave lower bytes
void recPEXTLB()
{
MMI_3OP_SETUP();
armAsm->Zip1(qd.V16B(), qt.V16B(), qs.V16B());
}
// PEXTUB: interleave upper bytes
void recPEXTUB()
{
MMI_3OP_SETUP();
armAsm->Zip2(qd.V16B(), qt.V16B(), qs.V16B());
}
// PPACW: pack words — {rs.UL[2], rs.UL[0], rt.UL[2], rt.UL[0]}
void recPPACW()
{
MMI_3OP_SETUP();
armAsm->Uzp1(qd.V4S(), qt.V4S(), qs.V4S());
}
// PPACH: pack halfwords — even halfwords from rs and rt
void recPPACH()
{
MMI_3OP_SETUP();
armAsm->Uzp1(qd.V8H(), qt.V8H(), qs.V8H());
}
// PPACB: pack bytes — even bytes from rs and rt
void recPPACB()
{
MMI_3OP_SETUP();
armAsm->Uzp1(qd.V16B(), qt.V16B(), qs.V16B());
}
// PADSBH: rd.UH[0..3] = rs.UH[0..3] - rt.UH[0..3], rd.UH[4..7] = rs.UH[4..7] + rt.UH[4..7]
// Lower 4 halfwords: subtract. Upper 4 halfwords: add.
void recPADSBH()
{
MMI_3OP_SETUP();
// Compute the add into a scratch FIRST. If Rd aliases Rs or Rt, the
// allocator hands qd back as the same Q-reg as qs/qt — writing qd in
// the sub step would clobber the source still needed for the add.
armAsm->Add(RQSCRATCH.V8H(), qs.V8H(), qt.V8H());
// qd = sub result (all 8 halfwords); safe to clobber qs/qt now.
armAsm->Sub(qd.V8H(), qs.V8H(), qt.V8H());
// Blend: keep lower 64 bits of sub in qd, upper 64 bits from add.
armAsm->Mov(qd.V2D(), 1, RQSCRATCH.V2D(), 1);
}
// ============================================================================
// Interleave halfwords
// ============================================================================
// PINTH: rd.US[2k]=Rt.US[k], rd.US[2k+1]=Rs.US[k+4], k=0..3 — interleave low 4
// halfwords of Rt with high 4 of Rs.
void recPINTH()
{
MMI_3OP_SETUP();
// Move rs upper 64 → low position of scratch (don't clobber qs).
armAsm->Dup(RQSCRATCH.V2D(), qs.V2D(), 1); // tmp = {rs.UD[1], rs.UD[1]}
// zip1.8h of rt(lower) and rs_upper(lower) gives interleaved result.
armAsm->Zip1(qd.V8H(), qt.V8H(), RQSCRATCH.V8H());
}
// PINTEH: rd = {rs.UH[6],rt.UH[6], rs.UH[4],rt.UH[4], rs.UH[2],rt.UH[2], rs.UH[0],rt.UH[0]}
// Interleave even halfwords
void recPINTEH()
{
MMI_3OP_SETUP();
// Extract even halfwords from each into scratch — never touch qs/qt.
armAsm->Uzp1(RQSCRATCH.V8H(), qs.V8H(), qs.V8H()); // rs evens in lower 64
armAsm->Uzp1(RQSCRATCH2.V8H(), qt.V8H(), qt.V8H()); // rt evens in lower 64
// Zip the lower 64 bits of each into qd.
armAsm->Zip1(qd.V8H(), RQSCRATCH2.V8H(), RQSCRATCH.V8H());
}
// ============================================================================
// Shuffles / Permutations
// ============================================================================
// PEXEW: rd = {rt[2], rt[1], rt[0], rt[3]} (lane order) — swap words 0 and 2.
// 2-op idiom (Rev64 + Ext) instead of a scratch snapshot + full copy + 2 lane
// inserts. Both ops read qt fully before writing, so it is alias-safe when the
// allocator hands back qd == qt.
void recPEXEW()
{
MMI_2OP_SETUP();
armAsm->Rev64(qd.V4S(), qt.V4S()); // {rt[1],rt[0],rt[3],rt[2]}
armAsm->Ext(qd.V16B(), qd.V16B(), qd.V16B(), 12); // {rt[2],rt[1],rt[0],rt[3]}
}
// PEXEH: swap halfwords 0↔2 in each 64-bit lane
// rd = {H[2],H[1],H[0],H[3], H[6],H[5],H[4],H[7]}
void recPEXEH()
{
MMI_2OP_SETUP();
armAsm->Mov(RQSCRATCH.V16B(), qt.V16B());
armAsm->Mov(qd.V8H(), RQSCRATCH.V8H());
armAsm->Mov(qd.V8H(), 0, RQSCRATCH.V8H(), 2);
armAsm->Mov(qd.V8H(), 2, RQSCRATCH.V8H(), 0);
armAsm->Mov(qd.V8H(), 4, RQSCRATCH.V8H(), 6);
armAsm->Mov(qd.V8H(), 6, RQSCRATCH.V8H(), 4);
}
// PREVH: reverse halfwords within each 64-bit lane
// rd = {H[3],H[2],H[1],H[0], H[7],H[6],H[5],H[4]}
void recPREVH()
{
MMI_2OP_SETUP();
armAsm->Rev64(qd.V8H(), qt.V8H());
}
// PROT3W: rotate lower 3 words: rd = {rt[1], rt[2], rt[0], rt[3]} (lane order).
// 3-op shuffle (Rev64 + Ext + Zip1) instead of a scratch snapshot + full copy
// + 3 lane inserts. Rev64 and Ext read qt into scratches
// first, so Zip1 → qd is alias-safe when qd == qt.
// rev = {rt[1],rt[0],rt[3],rt[2]}
// ext8 = {rt[2],rt[3],rt[0],rt[1]}
// Zip1(rev,ext8) = {rev[0],ext8[0],rev[1],ext8[1]} = {rt[1],rt[2],rt[0],rt[3]}
void recPROT3W()
{
MMI_2OP_SETUP();
armAsm->Rev64(RQSCRATCH.V4S(), qt.V4S());
armAsm->Ext(RQSCRATCH2.V16B(), qt.V16B(), qt.V16B(), 8);
armAsm->Zip1(qd.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S());
}
// PEXCW: swap words 1 and 2: rd = {rt[0], rt[2], rt[1], rt[3]} (lane order).
// 2-op idiom (Rev64 + Uzp1) instead of a scratch snapshot + full copy + 2 lane
// inserts. Both read their sources fully before
// writing qd, so it is alias-safe when qd == qt.
// rev = {rt[1],rt[0],rt[3],rt[2]}
// Uzp1(qt,rev) = {qt[0],qt[2],rev[0],rev[2]} = {rt[0],rt[2],rt[1],rt[3]}
void recPEXCW()
{
MMI_2OP_SETUP();
armAsm->Rev64(RQSCRATCH.V4S(), qt.V4S());
armAsm->Uzp1(qd.V4S(), qt.V4S(), RQSCRATCH.V4S());
}
// PEXCH: swap halfwords 1↔2 within each 64-bit lane
// {H[0],H[2],H[1],H[3], H[4],H[6],H[5],H[7]}
void recPEXCH()
{
MMI_2OP_SETUP();
armAsm->Mov(RQSCRATCH.V16B(), qt.V16B());
armAsm->Mov(qd.V8H(), RQSCRATCH.V8H());
armAsm->Mov(qd.V8H(), 1, RQSCRATCH.V8H(), 2);
armAsm->Mov(qd.V8H(), 2, RQSCRATCH.V8H(), 1);
armAsm->Mov(qd.V8H(), 5, RQSCRATCH.V8H(), 6);
armAsm->Mov(qd.V8H(), 6, RQSCRATCH.V8H(), 5);
}
// PEXT5: expand each 32-bit lane's PS2 RGB1555 field into BGRA8 layout.
// Per-lane:
// rd = ((rt & 0x001F) << 3) // R bits [4:0] -> [7:3]
// | ((rt & 0x03E0) << 6) // G bits [9:5] -> [15:11]
// | ((rt & 0x7C00) << 9) // B bits [14:10] -> [23:19]
// | ((rt & 0x8000) << 16); // A bit [15] -> [31]
void recPEXT5()
{
MMI_2OP_SETUP();
// Preserve qt in case allocator assigned qd == qt — rt is needed for all
// four shift+mask passes below, but the first write to qd would clobber
// it if they share a slot.
armAsm->Mov(RQSCRATCH3.V16B(), qt.V16B());
// Field 0: (rt << 3) & 0x000000F8 -> qd
armAsm->Shl(qd.V4S(), RQSCRATCH3.V4S(), 3);
armAsm->Movi(RQSCRATCH.V4S(), 0xF8);
armAsm->And(qd.V16B(), qd.V16B(), RQSCRATCH.V16B());
// Field 1: (rt << 6) & 0x0000F800 -> qd
armAsm->Shl(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 6);
armAsm->Movi(RQSCRATCH.V4S(), 0xF8, vixl::aarch64::LSL, 8);
armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B());
armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B());
// Field 2: (rt << 9) & 0x00F80000 -> qd
armAsm->Shl(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 9);
armAsm->Movi(RQSCRATCH.V4S(), 0xF8, vixl::aarch64::LSL, 16);
armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B());
armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B());
// Field 3: (rt << 16) & 0x80000000 -> qd
armAsm->Shl(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 16);
armAsm->Movi(RQSCRATCH.V4S(), 0x80, vixl::aarch64::LSL, 24);
armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B());
armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B());
}
// PPAC5: pack BGRA8-style 32-bit lanes back into PS2 RGB1555 16-bit layout
// (the inverse of PEXT5). Upper 16 bits of each lane left as garbage from
// the shifted-source — interp does not mask them either.
// Per-lane:
// rd = ((rt >> 3) & 0x001F)
// | ((rt >> 6) & 0x03E0)
// | ((rt >> 9) & 0x7C00)
// | ((rt >> 16) & 0x8000);
void recPPAC5()
{
MMI_2OP_SETUP();
armAsm->Mov(RQSCRATCH3.V16B(), qt.V16B());
// Field 0: (rt >> 3) & 0x0000001F -> qd
armAsm->Ushr(qd.V4S(), RQSCRATCH3.V4S(), 3);
armAsm->Movi(RQSCRATCH.V4S(), 0x1F);
armAsm->And(qd.V16B(), qd.V16B(), RQSCRATCH.V16B());
// Field 1: (rt >> 6) & 0x000003E0 -> qd
// 0x3E0 has two non-zero bytes; vixl's Movi macro materializes it via
// Mov scratch_w + Dup (2 host insns) rather than the single LSL form.
armAsm->Ushr(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 6);
armAsm->Movi(RQSCRATCH.V4S(), 0x3E0);
armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B());
armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B());
// Field 2: (rt >> 9) & 0x00007C00 -> qd
armAsm->Ushr(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 9);
armAsm->Movi(RQSCRATCH.V4S(), 0x7C, vixl::aarch64::LSL, 8);
armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B());
armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B());
// Field 3: (rt >> 16) & 0x00008000 -> qd
armAsm->Ushr(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), 16);
armAsm->Movi(RQSCRATCH.V4S(), 0x80, vixl::aarch64::LSL, 8);
armAsm->And(RQSCRATCH2.V16B(), RQSCRATCH2.V16B(), RQSCRATCH.V16B());
armAsm->Orr(qd.V16B(), qd.V16B(), RQSCRATCH2.V16B());
}
// ============================================================================
// Variable shifts — operate on words 0 and 2 only, sign-extend to 64
// ============================================================================
// PSLLVW: rd.SD[0] = sign_ext(rt.UL[0] << (rs.UL[0] & 0x1F))
// rd.SD[1] = sign_ext(rt.UL[2] << (rs.UL[2] & 0x1F))
void recPSLLVW()
{
if (!_Rd_) return;
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
mmiInvalidateDest(_Rd_);
// Word 0
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[0]);
armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[0]);
armAsm->And(a64::w1, a64::w1, 0x1F);
armAsm->Lsl(a64::w0, a64::w0, a64::w1);
armAsm->Sxtw(a64::x0, a64::w0);
armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[0]);
// Word 2
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[2]);
armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[2]);
armAsm->And(a64::w1, a64::w1, 0x1F);
armAsm->Lsl(a64::w0, a64::w0, a64::w1);
armAsm->Sxtw(a64::x0, a64::w0);
armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[1]);
}
void recPSRLVW()
{
if (!_Rd_) return;
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
mmiInvalidateDest(_Rd_);
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[0]);
armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[0]);
armAsm->And(a64::w1, a64::w1, 0x1F);
armAsm->Lsr(a64::w0, a64::w0, a64::w1);
armAsm->Sxtw(a64::x0, a64::w0);
armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[0]);
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[2]);
armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[2]);
armAsm->And(a64::w1, a64::w1, 0x1F);
armAsm->Lsr(a64::w0, a64::w0, a64::w1);
armAsm->Sxtw(a64::x0, a64::w0);
armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[1]);
}
void recPSRAVW()
{
if (!_Rd_) return;
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
mmiInvalidateDest(_Rd_);
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[0]);
armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[0]);
armAsm->And(a64::w1, a64::w1, 0x1F);
armAsm->Asr(a64::w0, a64::w0, a64::w1);
armAsm->Sxtw(a64::x0, a64::w0);
armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[0]);
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rt_].UL[2]);
armLoadEERegPtr(a64::w1, &cpuRegs.GPR.r[_Rs_].UL[2]);
armAsm->And(a64::w1, a64::w1, 0x1F);
armAsm->Asr(a64::w0, a64::w0, a64::w1);
armAsm->Sxtw(a64::x0, a64::w0);
armStoreEERegPtr(a64::x0, &cpuRegs.GPR.r[_Rd_].UD[1]);
}
// ============================================================================
// Multiply / Divide / MAC - PDIV* stay interp (x86 parity); PMADD*/PMSUBW native
// ============================================================================
// PDIVW / PDIVBW / PDIVUW stay as REC_FUNC because production x86 does too:
// its MMI2_RECOMPILE "native" path for these is itself `recCall(Interp::PDIV*)`
// after a targeted `_deleteEEreg(_Rd_, 0)`, so there is no codegen to port, and
// AArch64 NEON has no integer divide to build one from either.
//
// (MMI2_RECOMPILE itself is defined unconditionally at Config.h:1650 — an
// earlier version of this comment called it "never-defined", which made the
// rest of pcsx2/x86/iMMI.cpp look like dead reference code when it is in fact
// the shipping x86 implementation.)
//
// PMADDUW gets a native impl below — its interp is plain u64 arithmetic (no
// errata), so a NEON port matches interp bit-for-bit.
REC_FUNC(PDIVW);
REC_FUNC(PDIVBW);
REC_FUNC(PDIVUW);
// One PMADDW/PMSUBW lane pair (dd = dest half, ss = source word index).
//
// The accumulator is one 64-bit quantity per lane, not two 32-bit halves:
// acc = LO.UL[ss] | HI.UL[ss] << 32
// result = acc ± (s64)Rs.SL[ss] * Rt.SL[ss] (full 64-bit, carry kept)
// LO.SD[dd] = (s32)result HI.SD[dd] = (s32)(result >> 32)
// Rd.UD[dd] = LO.UL[dd*2] | HI.UL[dd*2] << 32, i.e. `result` itself
//
// This replaces a lane-split version that emitted an SDIV by 0xFFFFFFFF and a
// conditional +0x70000000 lane-0 addend to reproduce the PS2 multiply errata.
// Those were wrong: against the ps2autotests console captures the errata form
// scores 54/64 and this one 64/64 (see MMI.cpp _PMADDW). The interpreter was
// changed in the same commit, so the two engines still agree.
//
// Scratches w8/w9/w10 are outside the allocatable GPR pool and armCpuRegMem is
// a pure [RSTATE, #imm] MemOperand (no address scratch). Lane 1 reads word 2 of
// each source, which lane 0's SD[0]/UD[0] stores never touch, so sequential
// per-lane commit matches the interp's ordering under every Rd/Rs/Rt aliasing.
static void recPMADDWLane(int dd, int ss, bool isSub)
{
// armLoadEERegPtr: lane 0 (word 0) substitutes a pinned reg's mirror —
// required under lazy-dirty (memory lower half may be stale) and a free
// Ldr→Mov under write-through; lane 1 (word 2) is upper-half → memory is
// always canonical there and the helper falls through to the plain Ldr.
armLoadEERegPtr(a64::w8, &cpuRegs.GPR.r[_Rs_].SL[ss]);
armLoadEERegPtr(a64::w9, &cpuRegs.GPR.r[_Rt_].SL[ss]);
armAsm->Smull(a64::x10, a64::w8, a64::w9);
// Both loads are 32-bit, so the upper halves are zeroed and a shifted Orr
// splices the accumulator without masking.
armAsm->Ldr(a64::w8, armCpuRegMem(&cpuRegs.HI.UL[ss]));
armAsm->Ldr(a64::w9, armCpuRegMem(&cpuRegs.LO.UL[ss]));
armAsm->Orr(a64::x9, a64::x9, a64::Operand(a64::x8, a64::LSL, 32));
if (!isSub)
armAsm->Add(a64::x9, a64::x9, a64::x10);
else
armAsm->Sub(a64::x9, a64::x9, a64::x10);
// An Asr of the 64-bit result by 32 IS the sign-extended high word, so HI
// needs no separate Sxtw.
armAsm->Sxtw(a64::x10, a64::w9);
armAsm->Str(a64::x10, armCpuRegMem(&cpuRegs.LO.SD[dd]));
armAsm->Asr(a64::x8, a64::x9, 32);
armAsm->Str(a64::x8, armCpuRegMem(&cpuRegs.HI.SD[dd]));
if (_Rd_)
{
// Rd.UD[dd] is the raw result: its low word is what LO keeps and its
// high word is what HI keeps. Through armStoreEERegPtr, NOT a raw Str:
// a raw store bypassed the pin mirror, leaving a pinned Rd's mirror
// stale after PMADDW/PMSUBW (latent under write-through — any
// pin-served read of Rd afterward saw the old value; fatal under
// lazy-dirty, where the seam flush then clobbered the result).
armStoreEERegPtr(a64::x9, &cpuRegs.GPR.r[_Rd_].UD[dd]);
}
}
void recPMADDW()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
recPMADDWLane(0, 0, false);
recPMADDWLane(1, 2, false);
}
void recPMSUBW()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
recPMADDWLane(0, 0, true);
recPMADDWLane(1, 2, true);
}
// PMULTW: 2-lane signed 32x32->64 multiply on even-indexed source words.
// prod[0] = (s64)Rs.SL[0] * (s64)Rt.SL[0]
// prod[1] = (s64)Rs.SL[2] * (s64)Rt.SL[2]
// LO.UD[0..1] = sign-extended low32 of each product
// HI.UD[0..1] = sign-extended high32 of each product
// Rd.SD[0..1] = full 64-bit products
void recPMULTW()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
// Pack even-indexed 32-bit lanes into the low half (SL[0],SL[2] -> S[0],S[1])
armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH.V4S());
armAsm->Uzp1(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), RQSCRATCH2.V4S());
// 2-lane signed 32x32->64 -> { prod0, prod1 } as 2x64
armAsm->Smull(RQSCRATCH3.V2D(), RQSCRATCH.V2S(), RQSCRATCH2.V2S());
// LO = sign-extended low32 of each product
armAsm->Xtn(RQSCRATCH.V2S(), RQSCRATCH3.V2D());
armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
// HI = sign-extended high32 of each product (shift right narrow + sxtl)
armAsm->Shrn(RQSCRATCH.V2S(), RQSCRATCH3.V2D(), 32);
armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ));
if (_Rd_)
mmiStoreReg(_Rd_, RQSCRATCH3);
}
// PMULTUW: 2-lane unsigned 32x32->64 multiply on even-indexed source words.
// prod[0] = (u64)Rs.UL[0] * (u64)Rt.UL[0]
// prod[1] = (u64)Rs.UL[2] * (u64)Rt.UL[2]
// LO.UD[0..1] = sign-extended low32 of each product (interp casts (s32))
// HI.UD[0..1] = sign-extended high32 of each product
// Rd.UD[0..1] = full 64-bit products
void recPMULTUW()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH.V4S());
armAsm->Uzp1(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), RQSCRATCH2.V4S());
armAsm->Umull(RQSCRATCH3.V2D(), RQSCRATCH.V2S(), RQSCRATCH2.V2S());
armAsm->Xtn(RQSCRATCH.V2S(), RQSCRATCH3.V2D());
armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
armAsm->Shrn(RQSCRATCH.V2S(), RQSCRATCH3.V2D(), 32);
armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ));
if (_Rd_)
mmiStoreReg(_Rd_, RQSCRATCH3);
}
// PMADDUW: 2-lane unsigned 32x32+64->64 multiply-accumulate on even-indexed
// source words.
// tempu[k] = (u64)(LO.UL[2k] | (HI.UL[2k] << 32)) + (u64)Rs.UL[2k] * Rt.UL[2k]
// LO.UD[k] = sign-extended low32 of tempu[k]
// HI.UD[k] = sign-extended high32 of tempu[k]
// Rd.UD[k] = tempu[k] (full u64)
//
// Interp has no PS2 multiplication errata for the unsigned variant — plain u64
// arithmetic — so this matches interp bit-for-bit (unlike PMADDW/PMSUBW which
// stay REC_FUNC above).
//
// Bypasses the LO/HI allocator path: EE rec's info-word layout packs EEREC_LO
// and EEREC_HI into the same 5-bit field (the EEREC_LO/EEREC_HI info-word
// macros decode the same bits), so it can't produce two distinct register
// indices. An op that needs both LO and HI live must load them from memory.
void recPMADDUW()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
// LO/HI are never NEON-resident in the EE rec (no opcode passes XMMINFO_*LO/HI),
// so the Ldrs below already see fresh memory — no allocator flush needed.
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
// Pack even-indexed 32-bit lanes into the low half (UL[0],UL[2] -> S[0],S[1])
armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH.V4S());
armAsm->Uzp1(RQSCRATCH2.V4S(), RQSCRATCH2.V4S(), RQSCRATCH2.V4S());
// 2-lane unsigned 32x32->64 product
armAsm->Umull(RQSCRATCH3.V2D(), RQSCRATCH.V2S(), RQSCRATCH2.V2S());
// Compose accumulator: { LO.UL[0] | HI.UL[0]<<32, LO.UL[2] | HI.UL[2]<<32 }
// Trn1.V4S(d, a, b) = { a[0], b[0], a[2], b[2] } -> as V2D, gives LO|HI<<32 per lane.
armAsm->Ldr(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ));
armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH2.V4S());
// sum = composed + product (2x64 unsigned add)
armAsm->Add(RQSCRATCH3.V2D(), RQSCRATCH.V2D(), RQSCRATCH3.V2D());
// LO = sign-extended low32 of each 64-bit lane
armAsm->Xtn(RQSCRATCH.V2S(), RQSCRATCH3.V2D());
armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
// HI = sign-extended high32 of each 64-bit lane
armAsm->Shrn(RQSCRATCH.V2S(), RQSCRATCH3.V2D(), 32);
armAsm->Sxtl(RQSCRATCH.V2D(), RQSCRATCH.V2S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ));
// Rd = full 2x64 unsigned sum
if (_Rd_)
mmiStoreReg(_Rd_, RQSCRATCH3);
}
// PHMADH: 8-lane signed 16x16->32 multiply, pair-sum (n + n+1):
// sum[k] = Rs.SH[2k]*Rt.SH[2k] + Rs.SH[2k+1]*Rt.SH[2k+1] for k = 0..3
// firsttemp[k] = Rs.SH[2k+1]*Rt.SH[2k+1] (the second product of each pair)
// LO = { sum[0], firsttemp[0], sum[2], firsttemp[2] }
// HI = { sum[1], firsttemp[1], sum[3], firsttemp[3] }
// Rd = { sum[0], sum[1], sum[2], sum[3] } (post-update LO/HI even lanes)
void recPHMADH()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
// p_lo = { Rs.SH[i] * Rt.SH[i] } for i = 0..3 (as 4x32)
// p_hi = { Rs.SH[i] * Rt.SH[i] } for i = 4..7 (as 4x32)
armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H());
armAsm->Smull2(RQSCRATCH.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H());
// sums = ADDP(p_lo, p_hi).4S = { p0+p1, p2+p3, p4+p5, p6+p7 }
armAsm->Addp(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S());
// firsts = UZP2(p_lo, p_hi).4S = { p1, p3, p5, p7 }
armAsm->Uzp2(RQSCRATCH3.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S());
// LO = TRN1.4S(sums, firsts) = { sum0, p1, sum2, p5 }
armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
// HI = TRN2.4S(sums, firsts) = { sum1, p3, sum3, p7 }
armAsm->Trn2(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ));
if (_Rd_)
mmiStoreReg(_Rd_, RQSCRATCH2);
}
// PHMSBH: 8-lane signed 16x16->32 multiply, pair-diff (n+1 - n):
// sum[k] = Rs.SH[2k+1]*Rt.SH[2k+1] - Rs.SH[2k]*Rt.SH[2k] (k = 0..3)
// firsttemp[k] = Rs.SH[2k+1]*Rt.SH[2k+1] (the second product per pair)
// LO = { sum[0], ~firsttemp[0], sum[2], ~firsttemp[2] } (note: bitwise NOT)
// HI = { sum[1], ~firsttemp[1], sum[3], ~firsttemp[3] }
// Rd = { sum[0], sum[1], sum[2], sum[3] }
void recPHMSBH()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H());
armAsm->Smull2(RQSCRATCH.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H());
// odds = UZP2(p_lo, p_hi).4S = { p1, p3, p5, p7 } (firsttemps)
// evens = UZP1(p_lo, p_hi).4S = { p0, p2, p4, p6 }
armAsm->Uzp2(RQSCRATCH2.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S());
armAsm->Uzp1(RQSCRATCH3.V4S(), RQSCRATCH3.V4S(), RQSCRATCH.V4S());
// sums = odds - evens = { p1-p0, p3-p2, p5-p4, p7-p6 }
armAsm->Sub(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S());
// nfirsts = ~odds (reuse RQSCRATCH3 — evens are dead after Sub)
armAsm->Mvn(RQSCRATCH3.V16B(), RQSCRATCH2.V16B());
if (_Rd_)
mmiStoreReg(_Rd_, RQSCRATCH);
// LO = TRN1.4S(sums, nfirsts) = { sum0, ~p1, sum2, ~p5 }
armAsm->Trn1(RQSCRATCH2.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S());
armAsm->Str(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ));
// HI = TRN2.4S(sums, nfirsts) = { sum1, ~p3, sum3, ~p7 }
armAsm->Trn2(RQSCRATCH2.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S());
armAsm->Str(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ));
}
// PMULTH: 8-lane signed 16x16->32 multiply.
// r[i] = Rs.SH[i] * Rt.SH[i] for i in 0..7
// LO = { r0, r1, r4, r5 }
// HI = { r2, r3, r6, r7 }
// Rd = { r0, r2, r4, r6 } (even-indexed products)
void recPMULTH()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
// q29 = SMULL Rs.4H, Rt.4H -> { r0,r1,r2,r3 } as 4x32
// q31 = SMULL2 Rs.8H, Rt.8H -> { r4,r5,r6,r7 } as 4x32 (in-place over Rt)
armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H());
armAsm->Smull2(RQSCRATCH2.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H());
// LO = TRN1.2D(prod_lo, prod_hi) = { prod_lo.D[0], prod_hi.D[0] } = { r0,r1,r4,r5 }
armAsm->Trn1(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
// HI = TRN2.2D(prod_lo, prod_hi) = { prod_lo.D[1], prod_hi.D[1] } = { r2,r3,r6,r7 }
armAsm->Trn2(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.HI.UQ));
if (_Rd_)
{
// Rd = UZP1.4S(prod_lo, prod_hi) = { r0, r2, r4, r6 } (even-indexed)
armAsm->Uzp1(RQSCRATCH.V4S(), RQSCRATCH3.V4S(), RQSCRATCH2.V4S());
mmiStoreReg(_Rd_, RQSCRATCH);
}
}
// PMADDH: 8-lane signed 16x16->32 multiply, accumulate into existing LO/HI.
// r[i] = Rs.SH[i] * Rt.SH[i] for i in 0..7
// LO.UL[0..3] += { r0, r1, r4, r5 }
// HI.UL[0..3] += { r2, r3, r6, r7 }
// Rd = { new_LO[0], new_HI[0], new_LO[2], new_HI[2] }
void recPMADDH()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H());
armAsm->Smull2(RQSCRATCH2.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H());
// q30 = LO_increment = { r0,r1,r4,r5 }
armAsm->Trn1(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D());
// q29 = HI_increment = { r2,r3,r6,r7 }
armAsm->Trn2(RQSCRATCH3.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D());
// q31 = old LO; add and store
armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ));
armAsm->Add(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH.V4S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
// q31 = old HI; add and store
armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ));
armAsm->Add(RQSCRATCH3.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S());
armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ));
if (_Rd_)
{
// Rd = TRN1.4S(new_LO, new_HI) = { LO[0], HI[0], LO[2], HI[2] }
armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S());
mmiStoreReg(_Rd_, RQSCRATCH);
}
}
// PMSUBH: 8-lane signed 16x16->32 multiply, subtract from existing LO/HI.
// r[i] = Rs.SH[i] * Rt.SH[i] for i in 0..7
// LO.UL[0..3] -= { r0, r1, r4, r5 }
// HI.UL[0..3] -= { r2, r3, r6, r7 }
// Rd = { new_LO[0], new_HI[0], new_LO[2], new_HI[2] }
void recPMSUBH()
{
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
if (_Rd_)
mmiInvalidateDest(_Rd_);
mmiLoadReg(RQSCRATCH, _Rs_);
mmiLoadReg(RQSCRATCH2, _Rt_);
armAsm->Smull(RQSCRATCH3.V4S(), RQSCRATCH.V4H(), RQSCRATCH2.V4H());
armAsm->Smull2(RQSCRATCH2.V4S(), RQSCRATCH.V8H(), RQSCRATCH2.V8H());
armAsm->Trn1(RQSCRATCH.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D());
armAsm->Trn2(RQSCRATCH3.V2D(), RQSCRATCH3.V2D(), RQSCRATCH2.V2D());
armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ));
armAsm->Sub(RQSCRATCH.V4S(), RQSCRATCH2.V4S(), RQSCRATCH.V4S());
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.LO.UQ));
armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.HI.UQ));
armAsm->Sub(RQSCRATCH3.V4S(), RQSCRATCH2.V4S(), RQSCRATCH3.V4S());
armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ));
if (_Rd_)
{
armAsm->Trn1(RQSCRATCH.V4S(), RQSCRATCH.V4S(), RQSCRATCH3.V4S());
mmiStoreReg(_Rd_, RQSCRATCH);
}
}
// ============================================================================
// QFSRV: Quad Funnel Shift Right Variable
// ============================================================================
// QFSRV: Rd = {Rs, Rt} >> (sa * 8), truncated to 128 bits.
// cpuRegs.sa is in bytes (0-15). Concatenate Rt (low) and Rs (high)
// into a 256-bit value, shift right by sa bytes, take lower 128 bits.
//
// TBL over a two-register table is exactly this operation: the table is 32
// bytes with Rt at 0-15 and Rs at 16-31, and byte i of the result is
// table[sa + i]. The index vector is a {0..15} ramp plus a broadcast sa, so
// the largest index a legal sa can produce is 15 + 15 = 30 and the table
// always covers it.
//
// SSE has no cross-register variable byte shift, so the x86 emitter spills
// both operands to a static buffer and reloads 128 bits at offset sa. This
// did the same until the table form replaced it. The reload was the
// expensive part: a 16-byte load is forwarded from the two stores only when
// it is 8-byte aligned, so fourteen of the sixteen sa values drained the
// store buffer instead.
//
// The other consequence is on the And below. An out-of-range index cannot
// name a host address here — TBL answers zero for one — so that mask now
// keeps the guest semantics (sa is a byte count mod 16) rather than standing
// between a guest-written sa and a host out-of-bounds read. The
// adjacent-source path above still indexes host memory and still needs it for
// the original reason.
void recQFSRV()
{
if (!_Rd_) return;
mmiFlushReg(_Rs_);
mmiFlushReg(_Rt_);
mmiInvalidateDest(_Rd_);
// Adjacent sources (Rs == Rt+1) are contiguous in the GPR array, so a
// 16-byte read at &GPR.r[Rt] + sa funnels them without a TBL. That shortcut
// was removed: it built a host address out of sa, and a pinned source —
// twelve of the adjacent pairs have one — had to be flushed into the window
// first, an 8-byte store under a 16-byte load that drains the store buffer
// at 13.1 cycles instead of forwarding. Unpinned it ran 2.38 against this
// sequence's 2.13 (A78C) and 2.34 against 2.00 (X1C).
// index = sa + {0..15}, built in RQSCRATCH3 before the operands land so
// the broadcast can borrow RQSCRATCH.
armAsm->Ldr(RQSCRATCH3, armCpuRegMem(&_cpuRegistersPack.byteRamp));
armLoadEERegPtr(RWSCRATCH, &cpuRegs.sa);
// SA is 4 bits and MTSA/MTSAB/MTSAH all mask at the write, so this is
// belt-and-braces. A TBL index past the 32-byte table answers zero, so no
// host address depends on it.
armAsm->And(RWSCRATCH, RWSCRATCH, 0xf);
armAsm->Dup(RQSCRATCH.V16B(), RWSCRATCH);
armAsm->Add(RQSCRATCH3.V16B(), RQSCRATCH3.V16B(), RQSCRATCH.V16B());
// The table has to be a consecutive pair, which q30/q31 are. Emitted
// directly rather than through armEmitVTBL, whose assert rejects
// RQSCRATCH/RQSCRATCH2 as sources: it reserves them for the copy it makes
// when the pair is not consecutive, the one case this cannot hit.
mmiLoadReg(RQSCRATCH, _Rt_); // table bytes 0..15
mmiLoadReg(RQSCRATCH2, _Rs_); // table bytes 16..31
armAsm->Tbl(RQSCRATCH3.V16B(), RQSCRATCH.V16B(), RQSCRATCH2.V16B(), RQSCRATCH3.V16B());
armStoreEEGPRQuad(RQSCRATCH3, _Rd_);
}
// ============================================================================
// Other MMI
// ============================================================================
// PLZCW: count leading sign bits (excluding the sign bit itself) for words 0 and 1
void recPLZCW()
{
if (!_Rd_) return;
mmiFlushReg(_Rs_);
mmiInvalidateDest(_Rd_);
// Word 0: ARM64 CLS counts leading sign bits excluding the MSB sign bit itself,
// which matches the PS2 PLZCW definition (CountLeadingSignBits - 1).
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rs_].UL[0]);
armAsm->Cls(a64::w0, a64::w0);
armStoreEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rd_].UL[0]);
// Word 1
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rs_].UL[1]);
armAsm->Cls(a64::w0, a64::w0);
armStoreEERegPtr(a64::w0, &cpuRegs.GPR.r[_Rd_].UL[1]);
}
// PMFHL — read LO/HI 128-bit register pair, dispatch on sa:
// 0x00 LW : Rd = { LO.UL[0], HI.UL[0], LO.UL[2], HI.UL[2] }
// 0x01 UW : Rd = { LO.UL[1], HI.UL[1], LO.UL[3], HI.UL[3] }
// 0x02 SLW: composed s64 (HI.UL[2k]:LO.UL[2k]) signed-saturated to s32
// then sign-extended to s64; written to Rd.UD[k] for k=0,1.
// 0x03 LH : Rd.US lanes from even-indexed LO/HI halfwords, interleaved
// at 32-bit-pair granularity.
// 0x04 SH : per-lane s32→s16 signed saturation of LO/HI words, interleaved
// at 32-bit-pair granularity.
// sa >= 5 — interpreter is a no-op (no default-case in MMI.cpp PMFHL); mirror
// that by early-returning without touching Rd. (x86 rec asserts on this path,
// but the interp doesn't, so the recompiled-vs-interp diff would fail an assert
// rather than catch real divergence — matching the interp is the safer choice.)
void recPMFHL()
{
if (!_Rd_)
return;
if (_Sa_ > 0x04)
return;
// LO/HI loaded directly from memory rather than via the allocator
// (XMMINFO_READLO | XMMINFO_READHI). Reason: the arm64 EE rec's
// info-word layout packs PROCESS_EE_SET_LO and PROCESS_EE_SET_HI
// into the SAME 5-bit field at bits 23..27 (see iCore-arm64.h —
// EEREC_LO == EEREC_HI == EEREC_ACC). PMFHL needs both LO and HI as
// simultaneous live inputs, which that field collision cannot represent.
// Bypass with explicit Ldrs. LO/HI are never NEON-resident in the EE rec
// (no opcode passes XMMINFO_*LO/HI), so the memory image is already current.
int info = eeRecompileCodeXMM(XMMINFO_WRITED);
const a64::VRegister qd = armQRegister(EEREC_D);
(void)info;
// Pre-loaded into reserved scratch quads (outside the allocator pool).
const a64::VRegister qlo = RQSCRATCH;
const a64::VRegister qhi = RQSCRATCH2;
armAsm->Ldr(qlo, armCpuRegMem(&cpuRegs.LO.UQ));
armAsm->Ldr(qhi, armCpuRegMem(&cpuRegs.HI.UQ));
switch (_Sa_)
{
case 0x00: // LW: pick even-indexed words from LO/HI and interleave
// TRN1.V4S → { LO.S[0], HI.S[0], LO.S[2], HI.S[2] }
armAsm->Trn1(qd.V4S(), qlo.V4S(), qhi.V4S());
break;
case 0x01: // UW: pick odd-indexed words from LO/HI and interleave
// TRN2.V4S → { LO.S[1], HI.S[1], LO.S[3], HI.S[3] }
armAsm->Trn2(qd.V4S(), qlo.V4S(), qhi.V4S());
break;
case 0x02: // SLW: compose s64 (HI:LO) per even-word lane, saturate to s32, sign-extend back
// TRN1.V4S → V2D { (HI[0]:LO[0]), (HI[2]:LO[2]) } (LO in low 32 of each 64)
// SQXTN.V2S — signed-saturating narrow 2x64 → 2x32 (matches interp's
// "in-range -> (s64)(s32)LO.UL[2k]; saturate to INT32_MIN/MAX" bounds).
// SXTL.V2D — sign-extend 2x32 → 2x64 (= the recorded Rd.UD shape).
armAsm->Trn1(qd.V4S(), qlo.V4S(), qhi.V4S());
armAsm->Sqxtn(qd.V2S(), qd.V2D());
armAsm->Sxtl(qd.V2D(), qd.V2S());
break;
case 0x03: // LH: even halfwords from LO/HI, interleaved at S-pair granularity
// UZP1.V8H(x, x) gathers x's even halfwords into the low 64 bits of x.
// ZIP1.V4S picks S[0]/S[1] of each input → output S[0..3] =
// { (LO[0]:LO[2]), (HI[0]:HI[2]), (LO[4]:LO[6]), (HI[4]:HI[6]) }
// which as V8H = { LO[0], LO[2], HI[0], HI[2], LO[4], LO[6], HI[4], HI[6] }.
armAsm->Uzp1(qlo.V8H(), qlo.V8H(), qlo.V8H());
armAsm->Uzp1(qhi.V8H(), qhi.V8H(), qhi.V8H());
armAsm->Zip1(qd.V4S(), qlo.V4S(), qhi.V4S());
break;
case 0x04: // SH: signed-saturating narrow 32→16 per word, interleaved at S-pair granularity
// SQXTN.V4H — 4x32 signed-sat narrowed to 4x16 in low 64 of each scratch.
// ZIP1.V4S → output S[0..3] = { sat(LO[0..1]), sat(HI[0..1]), sat(LO[2..3]), sat(HI[2..3]) }
// which as V8H is exactly the interp's PMFHL_CLAMP-per-lane pattern.
armAsm->Sqxtn(qlo.V4H(), qlo.V4S());
armAsm->Sqxtn(qhi.V4H(), qhi.V4S());
armAsm->Zip1(qd.V4S(), qlo.V4S(), qhi.V4S());
break;
}
}
// PMTHL.LW: even-indexed words of LO/HI receive Rs's four words; the
// odd-indexed words (UL[1] and UL[3] of each) are preserved. Matches
// interp at MMI.cpp:217-224 and x86 BLENDPS/SHUFPS sequence at
// iMMI.cpp:234-248. Strategy: load LO/HI as Q regs, INS lanes 1+3 from
// the prior values to preserve them; lane 0 and lane 2 come from Rs's
// word 0/2 for LO, word 1/3 for HI.
void recPMTHL()
{
if (_Sa_ != 0)
return;
mmiFlushReg(_Rs_);
mmiLoadReg(RQSCRATCH, _Rs_);
// LO_new = [Rs.UL[0], LO.UL[1], Rs.UL[2], LO.UL[3]]
armAsm->Ldr(RQSCRATCH2, armCpuRegMem(&cpuRegs.LO.UQ));
armAsm->Mov(RQSCRATCH3.V16B(), RQSCRATCH.V16B());
armAsm->Ins(RQSCRATCH3.V4S(), 1, RQSCRATCH2.V4S(), 1);
armAsm->Ins(RQSCRATCH3.V4S(), 3, RQSCRATCH2.V4S(), 3);
armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.LO.UQ));
// HI_new = [Rs.UL[1], HI.UL[1], Rs.UL[3], HI.UL[3]]
armAsm->Ldr(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ));
armAsm->Ins(RQSCRATCH3.V4S(), 0, RQSCRATCH.V4S(), 1);
armAsm->Ins(RQSCRATCH3.V4S(), 2, RQSCRATCH.V4S(), 3);
armAsm->Str(RQSCRATCH3, armCpuRegMem(&cpuRegs.HI.UQ));
}
} // namespace MMI
} // namespace OpcodeImpl
} // namespace Dynarec
} // namespace R5900