arm64/EE: write-through pinned read-cache for $sp/$ra in x22/x23

Pin the two hottest guest GPRs into callee-saved host registers:
x22 mirrors GPR.r[29].UD[0] ($sp), x23 mirrors GPR.r[31].UD[0] ($ra).
Memory stays canonical — every guest-visible write still stores to
cpuRegs.GPR and refreshes the mirror at the same emission point — so
there is no new C-call or block-exit contract, no const-prop coupling,
and no allocator involvement. Reads that would load UD[0] from memory
use the mirror instead:

- armLoadEERegPtr substitutes Mov-from-pin for the Ldr (covers the
  allocator MODE_READ load, _eeMoveGPRtoR's memory fallback, and every
  op-file read), deleting the dependent load at the head of scalar ops.
- The scalar ALU/imm/shift memLoad helpers return the pin register
  directly (zero instructions for a pinned source); memStore helpers
  take the value register so pass-through cases (DADDU rd, rs, $zero;
  DSLL rd, rt, 0; ...) store the pin with no intermediate Mov.
- armStoreEERegPtr write-through: Mov for 64-bit stores, Bfi for
  UL[0]/UL[1] halves, reload-from-memory for odd shapes. New
  armStoreEEGPRQuad covers the 128-bit writers (NEON writeback, MMI,
  LQ, QMFC2) with a lane-0 UMOV refresh.
- Pins reload from memory at every JIT entry and after the C calls
  that can write guest GPRs: recEventTest (savestate load),
  recRecompile (ELF hooks), recCall/recBranchCall (interpreter
  fallbacks), eeloadHook/eeloadHook2.
- x22/x23 leave the dynamic allocator pool (ALLOCATABLE_MASK). They
  were the only pin-capable callee-saved regs: x26-x28 are the mVU
  macro-mode VI cache and the vtlb unaligned helpers' 3 simultaneous
  callee-saved temps. COP2 macro mode never touches x22/x23 (flags go
  through the s_cop2DenormStatusFlag memory scratch, not gprF2/F3);
  the mVU micro dispatcher saves/restores x19-x28.

This is the P2 extraction from the neither/LRPS2 RegCache deep-dive
(2026-07-01): the safe subset of static pinning — reads free, writes
keep the str — without neither's cross-block dirty-pin hazard.

SD865 A/B (codegen_ab.sh, 5 runs, --renderer null): SotC insns -0.06% /
cycles -1.36% (IPC 1.826->1.850), UYA insns -0.50% / cycles -1.36%
(IPC 2.390->2.411) — the guest-reg load latency was not OoO-hidden on
the A77, confirming the deep-dive's prediction. M2: SotC cyc -0.67%,
UYA cyc +0.86% (wider OoO window hides the loads; device is the gate).

Tests: new ee_rec_pinned_gpr_tests (5 cases: scalar reads, scalar
write-through + read-back, 128-bit MMI write-through, vtlb-load
write-through, CFC2 half-word Bfi path), each red under a deliberate
pin-update mutation and green with it restored. Full recompiler_tests
1052/1052.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-07-01 20:29:06 -07:00
co-authored by Claude Fable 5
parent 02cc7c7f89
commit e575485963
13 changed files with 733 additions and 268 deletions
+4 -4
View File
@@ -710,7 +710,7 @@ void recCOP2_QMFC2()
// 128-bit copy: VU0.VF[fs] → cpuRegs.GPR.r[rt]
armAsm->Ldr(RQSCRATCH, armVU0Mem(&VU0.VF[_Rd_]));
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_]));
armStoreEEGPRQuad(RQSCRATCH, _Rt_);
}
// QMTC2: VU0.VF[fs] = cpuRegs.GPR[rt] (128-bit copy, EE GPR → VF)
@@ -748,16 +748,16 @@ void recCOP2_CFC2()
// REG_R: mask to 23 bits, write only UL[0]
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[REG_R]));
armAsm->And(RWSCRATCH, RWSCRATCH, 0x7FFFFF);
armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UL[0]));
armStoreEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]);
}
else
{
// General VI: load 32-bit, sign-extend to UL[0]+UL[1]
armAsm->Ldr(RWSCRATCH, armVU0Mem(&VU0.VI[_Rd_]));
armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UL[0]));
armStoreEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]);
// Sign-extend: UL[1] = (UL[0] & 0x80000000) ? 0xFFFFFFFF : 0
armAsm->Asr(RWSCRATCH, RWSCRATCH, 31);
armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UL[1]));
armStoreEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[1]);
}
}
+7 -4
View File
@@ -62,7 +62,8 @@ _arm64neonregs arm64neon[NUM_ARM_NEON_REGS], s_saveArm64NEONregs[NUM_ARM_NEON_RE
// x19: RFASTMEMBASE — NOT allocatable (reserved for fastmem base)
// x20: RSTATE — NOT allocatable (reserved for cpuRegs pointer)
// x21: RPSXSTATE — NOT allocatable (reserved for psxRegs pointer in IOP JIT)
// x22-x23: callee-saved (allocatable)
// x22: REEPIN_SP — NOT allocatable (pinned mirror of GPR.r[29].UD[0], $sp)
// x23: REEPIN_RA — NOT allocatable (pinned mirror of GPR.r[31].UD[0], $ra)
// x24: RVU0 — NOT allocatable (reserved for &VU0 pointer in EE COP2 JIT)
// x25: RECCYCLE — NOT allocatable (pinned cycle delta: cycle - nextEventCycle)
// x26-x28: callee-saved (allocatable)
@@ -77,6 +78,7 @@ _arm64neonregs arm64neon[NUM_ARM_NEON_REGS], s_saveArm64NEONregs[NUM_ARM_NEON_RE
// bit 19 — x19 : RFASTMEMBASE
// bit 20 — x20 : RSTATE (cpuRegs base pointer)
// bit 21 — x21 : RPSXSTATE (psxRegs base; shared alloc table with EE)
// bits 22-23 — x22/x23 : REEPIN_SP/REEPIN_RA (pinned $sp/$ra mirrors)
// bit 24 — x24 : RVU0 (pinned &VU0 for iCOP2)
// bit 25 — x25 : RECCYCLE (pinned cycle delta)
// bits 29-30 — x29/x30 : FP, LR — never allocatable
@@ -87,6 +89,7 @@ static constexpr uint32_t ALLOCATABLE_MASK = ~((1u << 8)
| (1u << 9) | (1u << 10)
| (7u << 16)
| (1u << 19) | (1u << 20) | (1u << 21)
| (3u << 22)
| (1u << 24) | (1u << 25)
| (3u << 29));
@@ -437,7 +440,7 @@ void _flushConstReg(int reg)
if (GPR_IS_CONST1(reg) && !(g_cpuFlushedConstReg & (1 << reg)))
{
armAsm->Mov(RXSCRATCH, static_cast<s64>(g_cpuConstRegs[reg].SD[0]));
armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[reg].UD[0]));
armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[reg].UD[0]);
g_cpuFlushedConstReg |= (1 << reg);
if (reg == 0)
DevCon.Warning("Flushing r0!");
@@ -452,7 +455,7 @@ void _flushConstRegs(bool delete_const)
continue;
armAsm->Mov(RXSCRATCH, static_cast<u64>(g_cpuConstRegs[i].UD[0]));
armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[i].UD[0]));
armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[i].UD[0]);
g_cpuFlushedConstReg |= 1u << i;
}
@@ -922,7 +925,7 @@ void _writebackNEONreg(int neonreg)
else if (reg == NEONGPR_HI)
armStorePtr(armQRegister(neonreg), &cpuRegs.HI.UQ);
else
armStorePtr(armQRegister(neonreg), &cpuRegs.GPR.r[reg].UQ);
armStoreEEGPRQuad(armQRegister(neonreg), reg);
}
break;
+3 -3
View File
@@ -64,7 +64,7 @@ static void mmiLoadReg(const a64::VRegister& qreg, int gpr)
static void mmiStoreReg(int gpr, const a64::VRegister& qreg)
{
pxAssert(gpr != 0);
armAsm->Str(qreg, armCpuRegMem(&cpuRegs.GPR.r[gpr].UQ));
armStoreEEGPRQuad(qreg, gpr);
}
// Standard 3-operand MMI: rd = rs OP rt (128-bit).
@@ -1269,7 +1269,7 @@ void recQFSRV()
armMoveAddressToReg(RSCRATCHADDR, &cpuRegs.GPR.r[_Rt_]);
armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, RXSCRATCH);
armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR));
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rd_]));
armStoreEEGPRQuad(RQSCRATCH, _Rd_);
return;
}
@@ -1291,7 +1291,7 @@ void recQFSRV()
armAsm->Ldr(RQSCRATCH, a64::MemOperand(RSCRATCHADDR));
// Store result to Rd
armAsm->Str(RQSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[_Rd_]));
armStoreEEGPRQuad(RQSCRATCH, _Rd_);
}
// ============================================================================
+21 -6
View File
@@ -422,6 +422,9 @@ static const void* _DynGen_JITCompile()
armEmitCall((void*)recRecompile);
armReloadCycleDelta();
// Compile-time hooks (EntryPointCompilingOnCPUThread → game starting /
// ELF load) can write guest GPRs — refresh the pin mirrors.
armReloadEEGPRPins();
armEmitJmp(DispatcherReg);
@@ -438,6 +441,9 @@ static const void* _DynGen_DispatcherEvent()
armFlushCycleDelta();
armEmitCall((void*)recEventTest);
armReloadCycleDelta();
// Event processing can rewrite every guest GPR (savestate load on the EE
// thread, debugger pokes at a pause point) — refresh the pin mirrors.
armReloadEEGPRPins();
return retval; // falls through to DispatcherReg
}
@@ -480,6 +486,10 @@ static const void* _DynGen_EnterRecompiledCode()
// calls that touch cycle/nextEventCycle.
armReloadCycleDelta();
// Load the $sp/$ra write-through pin mirrors (see REEPIN_* doc in
// iR5900-arm64.h).
armReloadEEGPRPins();
// Load &VU0 into RVU0. Same idea as RSTATE: VU0 is a static reference
// (constant address), so iCOP2 codegen can reach every VURegs field via
// [RVU0, #imm12]. Survives both armEmitCall and mVU dispatcher runs.
@@ -674,6 +684,9 @@ void recCall(void (*func)())
// rescheduled nextEventCycle.
armReloadCycleDelta();
// The interpreter writes guest GPRs in memory — refresh the pin mirrors.
armReloadEEGPRPins();
// After interpreter calls, dispatch a pending TLB-miss exception.
recEmitInterpTlbMissCheck();
}
@@ -696,6 +709,7 @@ void recBranchCall(void (*func)())
g_branch = 2;
armReloadCycleDelta();
armReloadEEGPRPins();
}
// s_nBlockCycles is 3-bit fixed point. Divide by 8 when done!
@@ -1887,8 +1901,8 @@ static bool skipMPEG_By_Pattern(u32 sPC)
// v0 = 1 (low) / 0 (high); pc = ra.
armAsm->Mov(RWSCRATCH, 1);
armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.GPR.n.v0.UL[0]));
armAsm->Str(a64::wzr, armCpuRegMem(&cpuRegs.GPR.n.v0.UL[1]));
armStoreEERegPtr(RWSCRATCH, &cpuRegs.GPR.n.v0.UL[0]);
armStoreEERegPtr(a64::wzr, &cpuRegs.GPR.n.v0.UL[1]);
armLoadEERegPtr(a64::w0, &cpuRegs.GPR.n.ra.UL[0]);
armAsm->Str(a64::w0, armCpuRegMem(&cpuRegs.pc));
@@ -1950,12 +1964,11 @@ static bool recSkipTimeoutLoop(s32 reg, bool is_timeout_loop)
// event check uses RECCYCLE directly).
armAsm->Mov(RECCYCLE, a64::x5);
// reg -= iterations consumed
// reg -= iterations consumed; sign-extend into the 64-bit guest reg
// (the full UD[0] store covers the UL[0] half).
armAsm->Sub(a64::w4, a64::w4, a64::w6);
armAsm->Str(a64::w4, armCpuRegMem(&cpuRegs.GPR.r[reg].UL[0]));
// Also sign-extend to upper 32 bits (EE GPRs are 64-bit for lower half)
armAsm->Sxtw(a64::x4, a64::w4);
armAsm->Str(a64::x4, armCpuRegMem(&cpuRegs.GPR.r[reg].UD[0]));
armStoreEERegPtr(a64::x4, &cpuRegs.GPR.r[reg].UD[0]);
// if reg != 0, event interrupted the loop — go to dispatcher
armEmitCbnz(a64::w4, DispatcherEvent);
@@ -2076,6 +2089,7 @@ static void recRecompile(const u32 startpc)
armFlushCycleDelta();
armEmitCall((void*)eeloadHook);
armReloadCycleDelta();
armReloadEEGPRPins(); // ELF load / arg injection writes guest GPRs
if (VMManager::Internal::IsFastBootInProgress())
{
// Four known EELOAD versions, identified by the location of the 'jal' to
@@ -2100,6 +2114,7 @@ static void recRecompile(const u32 startpc)
armFlushCycleDelta();
armEmitCall((void*)eeloadHook2);
armReloadCycleDelta();
armReloadEEGPRPins(); // eeloadHook2 injects launch arguments into GPRs
}
// Goemon TLB-cache preload/unload intercept (mirrors x86 iR5900.cpp:2241-2255,
+124
View File
@@ -53,6 +53,28 @@
// scheduled event — worst case ~one hblank later — instead of the very
// next block tail. recEventTest still observes eeRecExitRequested.)
#define RECCYCLE vixl::aarch64::x25
// x22/x23: Write-through pinned read-cache for the two hottest guest GPRs:
// x22 = cpuRegs.GPR.r[29].UD[0] ($sp)
// x23 = cpuRegs.GPR.r[31].UD[0] ($ra)
// MEMORY STAYS CANONICAL. Every guest-visible write still stores to
// cpuRegs.GPR; the pin mirror is refreshed at the same emission point
// (armStoreEERegPtr write-through, armStoreEEGPRQuad for 128-bit stores).
// Reads that would load UD[0] from memory use the pin register instead
// (armLoadEERegPtr substitution / armEEPinForGPR in the scalar templates).
// Because the pin never holds a value memory doesn't, there is no new
// C-call or block-exit contract: C code reads and writes memory exactly as
// before, and the pins are re-read from memory (armReloadEEGPRPins) after
// the C calls that can write guest GPRs — interpreter fallbacks
// (recCall/recBranchCall), recEventTest (savestate load), recRecompile
// (ELF entry hooks), and eeloadHook/eeloadHook2. The upper 64 bits of the
// 128-bit guest reg are NOT mirrored; only UD[0] accesses match.
// Both host regs are callee-saved, carved out of the dynamic allocator
// pool (ALLOCATABLE_MASK in iCore-arm64.cpp), and are not used by any
// emission context reachable from inside an EE block: COP2 macro-mode flag
// code uses the s_cop2DenormStatusFlag memory scratch (not gprF2/F3), and
// the mVU micro dispatcher saves/restores x19-x28 around VU execution.
#define REEPIN_SP vixl::aarch64::x22
#define REEPIN_RA vixl::aarch64::x23
// Build a MemOperand addressing a cpuRegs field via RSTATE.
// Replaces the 3-instruction `armMoveAddressToReg(RSCRATCHADDR, &cpuRegs.X);
@@ -98,8 +120,72 @@ static __fi bool armIsCpuRegPtr(const void* field)
const u8* p = reinterpret_cast<const u8*>(field);
return p >= base && p < base + sizeof(cpuRegistersPack);
}
// Pin lookup by guest GPR index. Returns the pinned host register mirroring
// GPR.r[gpr].UD[0], or nullptr when gpr is not pinned.
static __fi const vixl::aarch64::Register* armEEPinForGPR(int gpr)
{
if (gpr == 29)
return &REEPIN_SP;
if (gpr == 31)
return &REEPIN_RA;
return nullptr;
}
// Pin lookup by target pointer: matches any byte within the LOWER 64 bits of
// a pinned guest GPR slot. *offset_in_dword receives the byte offset (0..7)
// of `field` within UD[0]. UD[1]/UL[2]/UL[3] accesses do not match (the
// upper half is not mirrored).
static __fi const vixl::aarch64::Register* armEEPinForPtr(const void* field, int* offset_in_dword)
{
const u8* p = reinterpret_cast<const u8*>(field);
const ptrdiff_t off_sp = p - reinterpret_cast<const u8*>(&cpuRegs.GPR.r[29]);
if (off_sp >= 0 && off_sp < 8)
{
*offset_in_dword = static_cast<int>(off_sp);
return &REEPIN_SP;
}
const ptrdiff_t off_ra = p - reinterpret_cast<const u8*>(&cpuRegs.GPR.r[31]);
if (off_ra >= 0 && off_ra < 8)
{
*offset_in_dword = static_cast<int>(off_ra);
return &REEPIN_RA;
}
return nullptr;
}
// Re-read the pin mirrors from canonical memory. Needed after any C call
// that can write guest GPRs, and at every JIT entry (see the REEPIN_* doc).
static __fi void armReloadEEGPRPins()
{
armAsm->Ldr(REEPIN_SP, armCpuRegMem(&cpuRegs.GPR.r[29].UD[0]));
armAsm->Ldr(REEPIN_RA, armCpuRegMem(&cpuRegs.GPR.r[31].UD[0]));
}
static __fi void armLoadEERegPtr(const vixl::aarch64::CPURegister& reg, const void* field)
{
// Pinned guest GPR: serve the read from the mirror register. The mirror
// always equals memory, so this is exactly the load it replaces.
int off;
if (const vixl::aarch64::Register* pin = armEEPinForPtr(field, &off); pin && reg.IsRegister())
{
const vixl::aarch64::Register dst(reg);
if (off == 0 && reg.Is64Bits())
{
armAsm->Mov(dst, *pin);
return;
}
if (off == 0 && reg.Is32Bits())
{
armAsm->Mov(dst, pin->W());
return;
}
if (off == 4 && reg.Is32Bits())
{
armAsm->Lsr(dst.X(), *pin, 32);
return;
}
// Unusual shapes fall through to the (identical) canonical memory load.
}
if (armIsCpuRegPtr(field))
armAsm->Ldr(reg, armCpuRegMem(field));
else
@@ -111,6 +197,44 @@ static __fi void armStoreEERegPtr(const vixl::aarch64::CPURegister& reg, const v
armAsm->Str(reg, armCpuRegMem(field));
else
armStorePtr(reg, field);
// Write-through: keep the pin mirror equal to the memory just written.
int off;
if (const vixl::aarch64::Register* pin = armEEPinForPtr(field, &off))
{
if (reg.IsRegister())
{
const vixl::aarch64::Register src(reg);
if (off == 0 && reg.Is64Bits())
{
if (!src.Is(*pin))
armAsm->Mov(*pin, src);
return;
}
if (off == 0 && reg.Is32Bits())
{
armAsm->Bfi(*pin, src.X(), 0, 32);
return;
}
if (off == 4 && reg.Is32Bits())
{
armAsm->Bfi(*pin, src.X(), 32, 32);
return;
}
}
// Odd store shape (vector reg / sub-word): reload the mirror from the
// just-written canonical memory.
armAsm->Ldr(*pin, armCpuRegMem(pin == &REEPIN_SP ? static_cast<const void*>(&cpuRegs.GPR.r[29].UD[0]) : static_cast<const void*>(&cpuRegs.GPR.r[31].UD[0])));
}
}
// 128-bit guest-GPR store (MMI/NEON writeback, LQ, QMFC2): store the full
// quad, then refresh the pin mirror from lane 0 when gpr is pinned.
static __fi void armStoreEEGPRQuad(const vixl::aarch64::VRegister& q, int gpr)
{
armAsm->Str(q, armCpuRegMem(&cpuRegs.GPR.r[gpr].UQ));
if (const vixl::aarch64::Register* pin = armEEPinForGPR(gpr))
armAsm->Mov(*pin, q.V2D(), 0);
}
// Build a MemOperand addressing a VU0 field via RVU0. VURegs is < 2 KB, so
File diff suppressed because it is too large Load Diff
+68 -28
View File
@@ -26,10 +26,26 @@ REC_FUNC(SLTI);
REC_FUNC(SLTIU);
#else
// Memory load/store helpers
static void memLoadS32() { armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rs_].UL[0]); }
static void memLoadS64() { armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]); }
static void memStoreT() { armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]); }
// Memory load/store helpers. Loads return the register holding the operand:
// the write-through pin mirror when the guest reg is pinned ($sp/$ra — no
// load emitted), the scratch otherwise. Callers must treat the returned
// register as READ-ONLY and put results in scratch (or pass them straight
// to the store helper).
static a64::Register memLoadS32()
{
if (const a64::Register* pin = armEEPinForGPR(_Rs_))
return pin->W();
armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rs_].UL[0]);
return RWSCRATCH;
}
static a64::Register memLoadS64()
{
if (const a64::Register* pin = armEEPinForGPR(_Rs_))
return *pin;
armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rs_].UD[0]);
return RXSCRATCH;
}
static void memStoreT(const a64::Register& src) { armStoreEERegPtr(src, &cpuRegs.GPR.r[_Rt_].UD[0]); }
//// ADDI / ADDIU — rt = sign_extend(rs + imm)
static void recADDI_const()
@@ -39,11 +55,17 @@ static void recADDI_const()
static void recADDI_(int info)
{
memLoadS32();
const a64::Register rs = memLoadS32();
if (_Imm_ != 0)
armAsm->Add(RWSCRATCH, RWSCRATCH, _Imm_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreT();
{
armAsm->Add(RWSCRATCH, rs, _Imm_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
}
else
{
armAsm->Sxtw(RXSCRATCH, rs);
}
memStoreT(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, ADDI, XMMINFO_WRITET | XMMINFO_READS);
@@ -58,14 +80,18 @@ static void recDADDI_const()
static void recDADDI_(int info)
{
memLoadS64();
const a64::Register rs = memLoadS64();
if (_Imm_ != 0)
{
// vixl's Add(int64_t) picks the right ADD/SUB-imm encoding and
// materializes via x16 when the immediate is unencodable.
armAsm->Add(RXSCRATCH, RXSCRATCH, static_cast<int64_t>(static_cast<s32>(_Imm_)));
armAsm->Add(RXSCRATCH, rs, static_cast<int64_t>(static_cast<s32>(_Imm_)));
memStoreT(RXSCRATCH);
}
else
{
memStoreT(rs);
}
memStoreT();
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, DADDI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP);
@@ -80,12 +106,14 @@ static void recANDI_const()
static void recANDI_(int info)
{
memLoadS64();
if (_ImmU_ == 0)
armAsm->Mov(RXSCRATCH, 0);
else
armAsm->And(RXSCRATCH, RXSCRATCH, static_cast<uint64_t>(static_cast<u16>(_ImmU_)));
memStoreT();
{
memStoreT(a64::xzr);
return;
}
const a64::Register rs = memLoadS64();
armAsm->And(RXSCRATCH, rs, static_cast<uint64_t>(static_cast<u16>(_ImmU_)));
memStoreT(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, ANDI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP);
@@ -98,10 +126,16 @@ static void recORI_const()
static void recORI_(int info)
{
memLoadS64();
const a64::Register rs = memLoadS64();
if (_ImmU_ != 0)
armAsm->Orr(RXSCRATCH, RXSCRATCH, static_cast<uint64_t>(static_cast<u16>(_ImmU_)));
memStoreT();
{
armAsm->Orr(RXSCRATCH, rs, static_cast<uint64_t>(static_cast<u16>(_ImmU_)));
memStoreT(RXSCRATCH);
}
else
{
memStoreT(rs);
}
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, ORI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP);
@@ -114,10 +148,16 @@ static void recXORI_const()
static void recXORI_(int info)
{
memLoadS64();
const a64::Register rs = memLoadS64();
if (_ImmU_ != 0)
armAsm->Eor(RXSCRATCH, RXSCRATCH, static_cast<uint64_t>(static_cast<u16>(_ImmU_)));
memStoreT();
{
armAsm->Eor(RXSCRATCH, rs, static_cast<uint64_t>(static_cast<u16>(_ImmU_)));
memStoreT(RXSCRATCH);
}
else
{
memStoreT(rs);
}
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, XORI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP);
@@ -130,10 +170,10 @@ static void recSLTI_const()
static void recSLTI_(int info)
{
memLoadS64();
armAsm->Cmp(RXSCRATCH, static_cast<int64_t>(static_cast<s32>(_Imm_)));
const a64::Register rs = memLoadS64();
armAsm->Cmp(rs, static_cast<int64_t>(static_cast<s32>(_Imm_)));
armAsm->Cset(RXSCRATCH, a64::lt);
memStoreT();
memStoreT(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, SLTI, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP);
@@ -146,12 +186,12 @@ static void recSLTIU_const()
static void recSLTIU_(int info)
{
memLoadS64();
const a64::Register rs = memLoadS64();
// Sign-extended imm — Cmp condition flags are signedness-agnostic; only
// the Cset (lo = unsigned-less-than) differs from SLTI.
armAsm->Cmp(RXSCRATCH, static_cast<int64_t>(static_cast<s32>(_Imm_)));
armAsm->Cmp(rs, static_cast<int64_t>(static_cast<s32>(_Imm_)));
armAsm->Cset(RXSCRATCH, a64::lo);
memStoreT();
memStoreT(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC1_MEM, SLTIU, XMMINFO_WRITET | XMMINFO_READS | XMMINFO_64BITOP);
+2 -2
View File
@@ -451,7 +451,7 @@ static void recBranchLink(bool ltz)
_deleteEEreg(31, 0);
// Store return address directly to memory
armAsm->Mov(RXSCRATCH, (u64)(pc + 4));
armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.n.ra.UD[0]));
armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.n.ra.UD[0]);
if (GPR_IS_CONST1(_Rs_))
{
@@ -492,7 +492,7 @@ static void recBranchLinkLikely(bool ltz)
_deleteEEreg(31, 0);
armAsm->Mov(RXSCRATCH, (u64)(pc + 4));
armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.n.ra.UD[0]));
armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.n.ra.UD[0]);
if (GPR_IS_CONST1(_Rs_))
{
+1 -1
View File
@@ -53,7 +53,7 @@ void recJAL()
else
{
armAsm->Mov(RXSCRATCH, (u64)(pc + 4));
armAsm->Str(RXSCRATCH, armCpuRegMem(&cpuRegs.GPR.r[31].UD[0]));
armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[31].UD[0]);
}
recompileNextInstruction(true, false);
+188 -100
View File
@@ -33,30 +33,46 @@ REC_FUNC(DSRLV);
REC_FUNC(DSRAV);
#else
// Memory load/store helpers — always use cpuRegs memory
static void memLoadS32()
// Memory load/store helpers. Loads return the register holding the operand:
// the write-through pin mirror when the guest reg is pinned ($sp/$ra — no
// load emitted), the scratch otherwise. Callers must treat the returned
// register as READ-ONLY and put results in scratch (or pass them straight
// to the store helper).
static a64::Register memLoadS32()
{
if (const a64::Register* pin = armEEPinForGPR(_Rs_))
return pin->W();
armLoadEERegPtr(RWARG1, &cpuRegs.GPR.r[_Rs_].UL[0]);
return RWARG1;
}
static void memLoadS64()
static a64::Register memLoadS64()
{
if (const a64::Register* pin = armEEPinForGPR(_Rs_))
return *pin;
armLoadEERegPtr(RXARG1, &cpuRegs.GPR.r[_Rs_].UD[0]);
return RXARG1;
}
static void memLoadT32()
static a64::Register memLoadT32()
{
if (const a64::Register* pin = armEEPinForGPR(_Rt_))
return pin->W();
armLoadEERegPtr(RWSCRATCH, &cpuRegs.GPR.r[_Rt_].UL[0]);
return RWSCRATCH;
}
static void memLoadT64()
static a64::Register memLoadT64()
{
if (const a64::Register* pin = armEEPinForGPR(_Rt_))
return *pin;
armLoadEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rt_].UD[0]);
return RXSCRATCH;
}
static void memStoreD()
static void memStoreD(const a64::Register& src)
{
armStoreEERegPtr(RXSCRATCH, &cpuRegs.GPR.r[_Rd_].UD[0]);
armStoreEERegPtr(src, &cpuRegs.GPR.r[_Rd_].UD[0]);
}
/*********************************************************
@@ -72,11 +88,17 @@ static void recSLL_const()
static void recSLL_(int info)
{
memLoadT32();
const a64::Register rt = memLoadT32();
if (_Sa_ != 0)
armAsm->Lsl(RWSCRATCH, RWSCRATCH, _Sa_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
{
armAsm->Lsl(RWSCRATCH, rt, _Sa_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
}
else
{
armAsm->Sxtw(RXSCRATCH, rt);
}
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, SLL, XMMINFO_WRITED | XMMINFO_READT);
@@ -89,11 +111,17 @@ static void recSRL_const()
static void recSRL_(int info)
{
memLoadT32();
const a64::Register rt = memLoadT32();
if (_Sa_ != 0)
armAsm->Lsr(RWSCRATCH, RWSCRATCH, _Sa_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
{
armAsm->Lsr(RWSCRATCH, rt, _Sa_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
}
else
{
armAsm->Sxtw(RXSCRATCH, rt);
}
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, SRL, XMMINFO_WRITED | XMMINFO_READT);
@@ -106,11 +134,17 @@ static void recSRA_const()
static void recSRA_(int info)
{
memLoadT32();
const a64::Register rt = memLoadT32();
if (_Sa_ != 0)
armAsm->Asr(RWSCRATCH, RWSCRATCH, _Sa_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
{
armAsm->Asr(RWSCRATCH, rt, _Sa_);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
}
else
{
armAsm->Sxtw(RXSCRATCH, rt);
}
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, SRA, XMMINFO_WRITED | XMMINFO_READT);
@@ -123,10 +157,16 @@ static void recDSLL_const()
static void recDSLL_(int info)
{
memLoadT64();
const a64::Register rt = memLoadT64();
if (_Sa_ != 0)
armAsm->Lsl(RXSCRATCH, RXSCRATCH, _Sa_);
memStoreD();
{
armAsm->Lsl(RXSCRATCH, rt, _Sa_);
memStoreD(RXSCRATCH);
}
else
{
memStoreD(rt);
}
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSLL, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP);
@@ -139,10 +179,16 @@ static void recDSRL_const()
static void recDSRL_(int info)
{
memLoadT64();
const a64::Register rt = memLoadT64();
if (_Sa_ != 0)
armAsm->Lsr(RXSCRATCH, RXSCRATCH, _Sa_);
memStoreD();
{
armAsm->Lsr(RXSCRATCH, rt, _Sa_);
memStoreD(RXSCRATCH);
}
else
{
memStoreD(rt);
}
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRL, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP);
@@ -155,10 +201,16 @@ static void recDSRA_const()
static void recDSRA_(int info)
{
memLoadT64();
const a64::Register rt = memLoadT64();
if (_Sa_ != 0)
armAsm->Asr(RXSCRATCH, RXSCRATCH, _Sa_);
memStoreD();
{
armAsm->Asr(RXSCRATCH, rt, _Sa_);
memStoreD(RXSCRATCH);
}
else
{
memStoreD(rt);
}
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRA, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP);
@@ -171,9 +223,9 @@ static void recDSLL32_const()
static void recDSLL32_(int info)
{
memLoadT64();
armAsm->Lsl(RXSCRATCH, RXSCRATCH, _Sa_ + 32);
memStoreD();
const a64::Register rt = memLoadT64();
armAsm->Lsl(RXSCRATCH, rt, _Sa_ + 32);
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSLL32, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP);
@@ -186,9 +238,9 @@ static void recDSRL32_const()
static void recDSRL32_(int info)
{
memLoadT64();
armAsm->Lsr(RXSCRATCH, RXSCRATCH, _Sa_ + 32);
memStoreD();
const a64::Register rt = memLoadT64();
armAsm->Lsr(RXSCRATCH, rt, _Sa_ + 32);
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRL32, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP);
@@ -201,9 +253,9 @@ static void recDSRA32_const()
static void recDSRA32_(int info)
{
memLoadT64();
armAsm->Asr(RXSCRATCH, RXSCRATCH, _Sa_ + 32);
memStoreD();
const a64::Register rt = memLoadT64();
armAsm->Asr(RXSCRATCH, rt, _Sa_ + 32);
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODEX_MEM(eeRecompileCodeRC2_MEM, DSRA32, XMMINFO_WRITED | XMMINFO_READT | XMMINFO_64BITOP);
@@ -221,30 +273,36 @@ static void recSLLV_const()
static void recSLLV_consts(int info)
{
memLoadT32();
const a64::Register rt = memLoadT32();
u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x1f;
if (sa != 0)
armAsm->Lsl(RWSCRATCH, RWSCRATCH, sa);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
{
armAsm->Lsl(RWSCRATCH, rt, sa);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
}
else
{
armAsm->Sxtw(RXSCRATCH, rt);
}
memStoreD(RXSCRATCH);
}
static void recSLLV_constt(int info)
{
memLoadS32();
const a64::Register rs = memLoadS32();
armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]);
armAsm->Lsl(RWSCRATCH, RWSCRATCH, RWARG1);
armAsm->Lsl(RWSCRATCH, RWSCRATCH, rs);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
memStoreD(RXSCRATCH);
}
static void recSLLV_(int info)
{
memLoadS32();
memLoadT32();
armAsm->Lsl(RWSCRATCH, RWSCRATCH, RWARG1);
const a64::Register rs = memLoadS32();
const a64::Register rt = memLoadT32();
armAsm->Lsl(RWSCRATCH, rt, rs);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODERC0_MEM(SLLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
@@ -257,30 +315,36 @@ static void recSRLV_const()
static void recSRLV_consts(int info)
{
memLoadT32();
const a64::Register rt = memLoadT32();
u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x1f;
if (sa != 0)
armAsm->Lsr(RWSCRATCH, RWSCRATCH, sa);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
{
armAsm->Lsr(RWSCRATCH, rt, sa);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
}
else
{
armAsm->Sxtw(RXSCRATCH, rt);
}
memStoreD(RXSCRATCH);
}
static void recSRLV_constt(int info)
{
memLoadS32();
const a64::Register rs = memLoadS32();
armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].UL[0]);
armAsm->Lsr(RWSCRATCH, RWSCRATCH, RWARG1);
armAsm->Lsr(RWSCRATCH, RWSCRATCH, rs);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
memStoreD(RXSCRATCH);
}
static void recSRLV_(int info)
{
memLoadS32();
memLoadT32();
armAsm->Lsr(RWSCRATCH, RWSCRATCH, RWARG1);
const a64::Register rs = memLoadS32();
const a64::Register rt = memLoadT32();
armAsm->Lsr(RWSCRATCH, rt, rs);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODERC0_MEM(SRLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
@@ -293,30 +357,36 @@ static void recSRAV_const()
static void recSRAV_consts(int info)
{
memLoadT32();
const a64::Register rt = memLoadT32();
u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x1f;
if (sa != 0)
armAsm->Asr(RWSCRATCH, RWSCRATCH, sa);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
{
armAsm->Asr(RWSCRATCH, rt, sa);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
}
else
{
armAsm->Sxtw(RXSCRATCH, rt);
}
memStoreD(RXSCRATCH);
}
static void recSRAV_constt(int info)
{
memLoadS32();
const a64::Register rs = memLoadS32();
armAsm->Mov(RWSCRATCH, g_cpuConstRegs[_Rt_].SL[0]);
armAsm->Asr(RWSCRATCH, RWSCRATCH, RWARG1);
armAsm->Asr(RWSCRATCH, RWSCRATCH, rs);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
memStoreD(RXSCRATCH);
}
static void recSRAV_(int info)
{
memLoadS32();
memLoadT32();
armAsm->Asr(RWSCRATCH, RWSCRATCH, RWARG1);
const a64::Register rs = memLoadS32();
const a64::Register rt = memLoadT32();
armAsm->Asr(RWSCRATCH, rt, rs);
armAsm->Sxtw(RXSCRATCH, RWSCRATCH);
memStoreD();
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODERC0_MEM(SRAV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT);
@@ -329,27 +399,33 @@ static void recDSLLV_const()
static void recDSLLV_consts(int info)
{
memLoadT64();
const a64::Register rt = memLoadT64();
u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x3f;
if (sa != 0)
armAsm->Lsl(RXSCRATCH, RXSCRATCH, sa);
memStoreD();
{
armAsm->Lsl(RXSCRATCH, rt, sa);
memStoreD(RXSCRATCH);
}
else
{
memStoreD(rt);
}
}
static void recDSLLV_constt(int info)
{
memLoadS64();
const a64::Register rs = memLoadS64();
armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].UD[0]);
armAsm->Lsl(RXSCRATCH, RXSCRATCH, RXARG1);
memStoreD();
armAsm->Lsl(RXSCRATCH, RXSCRATCH, rs);
memStoreD(RXSCRATCH);
}
static void recDSLLV_(int info)
{
memLoadS64();
memLoadT64();
armAsm->Lsl(RXSCRATCH, RXSCRATCH, RXARG1);
memStoreD();
const a64::Register rs = memLoadS64();
const a64::Register rt = memLoadT64();
armAsm->Lsl(RXSCRATCH, rt, rs);
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODERC0_MEM(DSLLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP);
@@ -362,27 +438,33 @@ static void recDSRLV_const()
static void recDSRLV_consts(int info)
{
memLoadT64();
const a64::Register rt = memLoadT64();
u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x3f;
if (sa != 0)
armAsm->Lsr(RXSCRATCH, RXSCRATCH, sa);
memStoreD();
{
armAsm->Lsr(RXSCRATCH, rt, sa);
memStoreD(RXSCRATCH);
}
else
{
memStoreD(rt);
}
}
static void recDSRLV_constt(int info)
{
memLoadS64();
const a64::Register rs = memLoadS64();
armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].UD[0]);
armAsm->Lsr(RXSCRATCH, RXSCRATCH, RXARG1);
memStoreD();
armAsm->Lsr(RXSCRATCH, RXSCRATCH, rs);
memStoreD(RXSCRATCH);
}
static void recDSRLV_(int info)
{
memLoadS64();
memLoadT64();
armAsm->Lsr(RXSCRATCH, RXSCRATCH, RXARG1);
memStoreD();
const a64::Register rs = memLoadS64();
const a64::Register rt = memLoadT64();
armAsm->Lsr(RXSCRATCH, rt, rs);
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODERC0_MEM(DSRLV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP);
@@ -395,27 +477,33 @@ static void recDSRAV_const()
static void recDSRAV_consts(int info)
{
memLoadT64();
const a64::Register rt = memLoadT64();
u32 sa = g_cpuConstRegs[_Rs_].UL[0] & 0x3f;
if (sa != 0)
armAsm->Asr(RXSCRATCH, RXSCRATCH, sa);
memStoreD();
{
armAsm->Asr(RXSCRATCH, rt, sa);
memStoreD(RXSCRATCH);
}
else
{
memStoreD(rt);
}
}
static void recDSRAV_constt(int info)
{
memLoadS64();
const a64::Register rs = memLoadS64();
armAsm->Mov(RXSCRATCH, g_cpuConstRegs[_Rt_].SD[0]);
armAsm->Asr(RXSCRATCH, RXSCRATCH, RXARG1);
memStoreD();
armAsm->Asr(RXSCRATCH, RXSCRATCH, rs);
memStoreD(RXSCRATCH);
}
static void recDSRAV_(int info)
{
memLoadS64();
memLoadT64();
armAsm->Asr(RXSCRATCH, RXSCRATCH, RXARG1);
memStoreD();
const a64::Register rs = memLoadS64();
const a64::Register rt = memLoadT64();
armAsm->Asr(RXSCRATCH, rt, rs);
memStoreD(RXSCRATCH);
}
EERECOMPILE_CODERC0_MEM(DSRAV, XMMINFO_WRITED | XMMINFO_READS | XMMINFO_READT | XMMINFO_64BITOP);
+1 -1
View File
@@ -705,7 +705,7 @@ void recLQ()
{
_deleteEEreg(_Rt_, 0);
GPR_DEL_CONST(_Rt_);
armAsm->Str(a64::q0, armCpuRegMem(&cpuRegs.GPR.r[_Rt_].UD[0]));
armStoreEEGPRQuad(a64::q0, _Rt_);
}
}
@@ -50,6 +50,7 @@ add_pcsx2_test(recompiler_tests
ee_rec_mmi_simd_tests.cpp
ee_rec_mmi_tests.cpp
ee_rec_move_tests.cpp
ee_rec_pinned_gpr_tests.cpp
ee_rec_muldiv_tests.cpp
ee_rec_multiblock_tests.cpp
ee_rec_shift_tests.cpp
@@ -0,0 +1,129 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// Write-through pinned read-cache coverage ($sp → x22, $ra → x23; see
// REEPIN_* in arm64/iR5900-arm64.h). The pins mirror GPR.r[29/31].UD[0]
// while memory stays canonical, so a broken write-through path does NOT
// corrupt memory — it strands the mirror. These tests make that observable
// by writing a pinned reg through each write path and then READING IT BACK
// inside the same block: a stale mirror feeds the read-back a wrong value
// and the JIT-vs-interp diff goes red.
//
// The harness parks the machine via a trailing JR $ra (SeedEntryState sets
// $ra = kParkingPc), so every block that scribbles $ra saves it to a temp
// first and restores it last — the save/restore pair itself exercises
// pinned reads and writes.
#include "harness/EeRecTestHarness.h"
#include <gtest/gtest.h>
using namespace recompiler_tests;
using namespace mips;
using namespace mips::ee;
namespace {
constexpr u32 kScratch = RecompilerTestEnvironment::kScratchAddr;
}
// Pinned $sp/$ra as scalar sources: imm-ALU, 3-op ALU, shift, set-on-lt.
TEST(EeRecPinnedGpr, ScalarReadsOfPinnedRegs)
{
EeRecTestHarness h;
h.SetGpr64(reg::sp, 0x0000000001F00010ull);
h.SetGpr64(reg::t5, 0xFFFFFFFF80001234ull);
h.LoadProgram({
OR(reg::t6, reg::ra, reg::zero), // save parking $ra (pinned read)
OR(reg::ra, reg::t5, reg::zero), // write pinned $ra
ADDIU(reg::t0, reg::sp, -16),
DADDU(reg::t1, reg::sp, reg::ra),
SLTI(reg::t2, reg::ra, 0),
DSLL(reg::t3, reg::ra, 4),
OR(reg::t4, reg::sp, reg::ra),
OR(reg::ra, reg::t6, reg::zero), // restore parking $ra
});
h.Run();
EXPECT_EQ(h.GetGpr64Interp(reg::t0), 0x0000000001F00000ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t1), 0xFFFFFFFF81F01244ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t2), 1ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t3), 0xFFFFFFF800012340ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t4), 0xFFFFFFFF81F01234ull);
}
// Scalar write-through: writes to $sp/$ra go through the memStore helpers'
// armStoreEERegPtr, which must refresh the mirror the read-backs consume.
TEST(EeRecPinnedGpr, ScalarWriteThroughThenReadBack)
{
EeRecTestHarness h;
h.SetGpr64(reg::t0, 0x0000000000010000ull);
h.SetGpr64(reg::t1, 0x0000000000000230ull);
h.LoadProgram({
OR(reg::t6, reg::ra, reg::zero), // save parking $ra
DADDU(reg::sp, reg::t0, reg::t1), // write pinned $sp
ADDIU(reg::t2, reg::sp, 8), // read $sp back via the pin
DADDU(reg::ra, reg::t2, reg::t0), // write pinned $ra
DADDU(reg::t3, reg::ra, reg::t1), // read $ra back via the pin
ADDIU(reg::sp, reg::sp, -32), // pinned RMW: read + write $sp
OR(reg::t4, reg::sp, reg::zero), // read the RMW result back
OR(reg::ra, reg::t6, reg::zero), // restore parking $ra
});
h.Run();
EXPECT_EQ(h.GetGpr64Interp(reg::t2), 0x0000000000010238ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t3), 0x0000000000020468ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t4), 0x0000000000010210ull);
}
// 128-bit write-through: PADDW allocates $sp as a NEON dest; the scalar
// read-back forces the NEON→memory writeback (armStoreEEGPRQuad), whose
// lane-0 UMOV must refresh the mirror.
TEST(EeRecPinnedGpr, MmiQuadWriteThroughThenReadBack)
{
EeRecTestHarness h;
h.SetGpr64(reg::t0, 0x1111111122222222ull);
h.SetGpr64(reg::t1, 0x0000000300000004ull);
h.LoadProgram({
PADDW(reg::sp, reg::t0, reg::t1), // 128-bit write of pinned $sp
DADDU(reg::t2, reg::sp, reg::zero), // scalar read-back via the pin
});
h.Run();
EXPECT_EQ(h.GetGpr64Interp(reg::t2), 0x1111111422222226ull);
}
// vtlb-load write-through: LW/LD land in the guest reg via
// recStoreLoadResult's armStoreEERegPtr; the read-back consumes the mirror.
// (LD $ra, off($sp) is the ubiquitous epilogue stack-restore idiom.)
TEST(EeRecPinnedGpr, LoadIntoPinnedThenReadBack)
{
EeRecTestHarness h;
h.WriteU64(kScratch, 0xFFFFFFFF80332211ull);
h.WriteU32(kScratch + 8, 0x00445566u);
h.SetGpr64(reg::sp, kScratch);
h.LoadProgram({
OR(reg::t6, reg::ra, reg::zero), // save parking $ra
LD(reg::ra, 0, reg::sp), // pinned base, pinned dest
DADDU(reg::t0, reg::ra, reg::zero),
LW(reg::sp, 8, reg::sp), // pinned base and dest, 32-bit
DADDU(reg::t1, reg::sp, reg::zero),
OR(reg::ra, reg::t6, reg::zero), // restore parking $ra
});
h.Run();
EXPECT_EQ(h.GetGpr64Interp(reg::t0), 0xFFFFFFFF80332211ull);
EXPECT_EQ(h.GetGpr64Interp(reg::t1), 0x0000000000445566ull);
}
// 32-bit-half write-through (Bfi path): CFC2 writes UL[0] and UL[1] of the
// pinned reg separately; the read-back consumes the mirror.
TEST(EeRecPinnedGpr, HalfWordWriteThroughViaCfc2)
{
EeRecTestHarness h;
h.EnableVu0Capture();
h.SeedVu0Vi(1, 0x8123);
h.LoadProgram({
OR(reg::t6, reg::ra, reg::zero), // save parking $ra
CFC2(reg::ra, 1), // UL[0]+UL[1] stores into pinned $ra
DADDU(reg::t0, reg::ra, reg::zero), // read $ra back via the pin
OR(reg::ra, reg::t6, reg::zero), // restore parking $ra
});
h.Run();
EXPECT_EQ(h.GetGpr64Interp(reg::t0), 0x0000000000008123ull);
}