Files
ARMSX2/pcsx2/arm64/iR3000Atables-arm64.cpp
Brian Degenhardtandpstef fd1fa89e10 IOP: route JIT RAM stores through out-of-line fast-path stubs
After the removal of the per-store block search, the remaining per-store cost
was the C call chain itself: iopMemWrite* region dispatch + WLUT load, the
indirect psxCpu->Clear call, and the recClearIOP/psxRecClearMem frames --
~2.9% of battle EE-thread samples in pstef's profile (iopMemWrite32 1.13%,
iopMemWrite16 0.28%, recClearIOP 1.44%).

Emit three per-width stubs (_DynGen_StoreStub) alongside the dispatchers
instead: gate, IsC check, direct store into Main[addr & (ExposedIopRam-1)],
and an inline g_iopCodeCov granule probe that only calls into the clear
machinery (iopStoreClearHit -> recClearIOP) when a live block overlaps the
store. Store sites emit a single BL, exactly the size of the old C call.
Hw/unmapped targets tail-jump from the stub to iopMemWrite*, which returns
straight to the store site; compile-time-known hw addresses keep calling C
directly.

The condition is (addr & 0x1f800000) == 0, not a bit-28 test: iopMemWrite*
masks addresses to 0x1fffffff, and within that space pages 0x00-0x7f are the
only WLUT-mapped write target reachable this way (plain RAM; the parallel port
at 0x1f00 is the other mapped page and the mask excludes it). Everything else
-- 0x1f80/0x1f40 hw, SIF, DEV9, SPU2, ROM, and the unmapped
0x00800000-0x0fffffff range that bit 28 alone misclassifies as RAM -- must
take the C path. Bits 23-28 survive the phys mask, so the test is exact for
every KUSEG/KSEG0/KSEG1 mirror, and matches iopMemReset's `for (i < 0x0080)`
mapping loop. The RAM mask equals the recLUT_SetPage mirror collapse, so the
masked offset doubles as the coverage probe index. IsC (Status bit 16)
swallows the store with no clear, matching the C RAM branch's
p != NULL && !IsC guard. Stubs are re-emitted on every recResetIOP, so the
baked mask and RAM base track extra-memory-mode flips (which discard all
blocks). RPSXSTATE is x21, callee-saved, so it survives both the BL and the
slow-path tail jump.

Outlining is deliberate: pstef measured a fully inline fast path (~15
insns/site) first, and its per-site code growth cost +10% L1I misses/frame,
eating the win (3D net zero, FMV only -1.08% cycles). The shared stubs keep
sites baseline-sized and also displace the C functions' own icache footprint.

Documented at the stub, not fixed here: the inline probe is mirror-collapsed
(addr & (ExposedIopRam-1)) while iopCovAdjust and psxRecClearMem key the same
array by HWADDR, which strips the KSEG base but not the RAM mirrors. Under a
2MB configuration a block executed from a mirror page registers coverage at a
different granule than a store to its physical alias probes. That is exactly
the pre-existing blindness of keying recBlocks by HWADDR -- not a regression
-- but the two domains are a trap for anyone making either side finer-grained.

Six new IopSmc cases: KSEG1-mirror, const-address, SB, and SH stores over a
pre-compiled victim block (compiled first, then stored into via RunResume, so
a stale-cache leak is actually observable), unmapped-low-region drop, and
IsC-swallow. Verified not inert: a bit-28-only gate turns
JitStoreToUnmappedLowRegionIsDropped red, and defeating the IsC check turns
six cases red including JitStoreWithCacheIsolatedIsSwallowed. Full suite
1432/1432.

Taken from https://github.com/yaps2/yaps2/pull/11 by pstef
(https://github.com/pstef).

Co-authored-by: pstef <3462925+pstef@users.noreply.github.com>
2026-07-24 21:02:50 -07:00

1482 lines
47 KiB
C++

// SPDX-FileCopyrightText: 2026 yaps2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// ARM64 IOP Opcode Implementations
// ALU/shift/mult-div/move/load-store/LWL-LWR/branch/COP0 have native codegen;
// only GTE (COP2) ops fall back to the interpreter via REC_GTE_FUNC.
#include "arm64/iR3000A-arm64.h"
#include "arm64/AsmHelpers.h"
#include "IopMem.h"
#include "IopDma.h" // also declares iopTestIntc()
#include "IopGte.h"
#include "common/Assertions.h"
#include "common/Console.h"
namespace a64 = vixl::aarch64;
extern int g_psxWriteOk;
extern u32 g_psxMaxRecMem;
// IOP interpreter function declarations (defined in R3000AOpcodeTables.cpp)
extern void psxADDI(); extern void psxADDIU(); extern void psxSLTI(); extern void psxSLTIU();
extern void psxANDI(); extern void psxORI(); extern void psxXORI(); extern void psxLUI();
extern void psxADD(); extern void psxADDU(); extern void psxSUB(); extern void psxSUBU();
extern void psxAND(); extern void psxOR(); extern void psxXOR(); extern void psxNOR();
extern void psxSLT(); extern void psxSLTU();
extern void psxSLL(); extern void psxSRL(); extern void psxSRA();
extern void psxSLLV(); extern void psxSRLV(); extern void psxSRAV();
extern void psxMULT(); extern void psxMULTU(); extern void psxDIV(); extern void psxDIVU();
extern void psxMFHI(); extern void psxMTHI(); extern void psxMFLO(); extern void psxMTLO();
extern void psxLB(); extern void psxLH(); extern void psxLW();
extern void psxLBU(); extern void psxLHU(); extern void psxLWL(); extern void psxLWR();
extern void psxSB(); extern void psxSH(); extern void psxSW();
extern void psxSWL(); extern void psxSWR();
extern void psxMFC0(); extern void psxMTC0(); extern void psxCFC0(); extern void psxCTC0();
extern void psxRFE();
// GTE functions (defined in IopGte.cpp)
extern void gteMFC2(); extern void gteMTC2(); extern void gteCFC2(); extern void gteCTC2();
extern void gteLWC2(); extern void gteSWC2();
extern void gteRTPS(); extern void gteNCLIP(); extern void gteOP();
extern void gteDPCS(); extern void gteINTPL(); extern void gteMVMVA(); extern void gteNCDS();
extern void gteCDP(); extern void gteNCDT(); extern void gteNCCS(); extern void gteCC();
extern void gteNCS(); extern void gteNCT(); extern void gteSQR(); extern void gteDCPL();
extern void gteDPCT(); extern void gteAVSZ3(); extern void gteAVSZ4(); extern void gteRTPT();
extern void gteGPF(); extern void gteGPL(); extern void gteNCCT();
////////////////////////////////////////////////////////////////////
// Interpreter Fallback Macro
#define REC_FUNC(f) \
static void rpsx##f() \
{ \
armAsm->Mov(RWSCRATCH, (u32)psxRegs.code); \
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code)); \
_psxFlushCall(FLUSH_EVERYTHING); \
armEmitCall((void*)(uptr)psx##f); \
PSX_DEL_CONST(_Rt_); \
}
#define REC_GTE_FUNC(f) \
static void rgte##f() \
{ \
armAsm->Mov(RWSCRATCH, (u32)psxRegs.code); \
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code)); \
_psxFlushCall(FLUSH_EVERYTHING); \
armEmitCall((void*)(uptr)gte##f); \
}
////////////////////////////////////////////////////////////////////
// ALU Immediate Instructions — rt = rs op imm16
////////////////////////////////////////////////////////////////////
// ALU Instructions — allocator-aware native codegen with const propagation
//
// These keep results live in allocated host regs across IOP instruction
// boundaries, flushing only at block end / before C calls.
// This mirrors the proven PSX_REG_OP path below.
// Allocator-aware 2-op IOP immediate macro: rt = rs OP imm.
// Const-folds when Rs is const; otherwise allocates Rs (MODE_READ) and Rt
// (MODE_WRITE) in host GPRs and emits codeGen, which sees host-reg indices
// rs (Rs) and rt (Rt). The result is left live in the allocator.
#define PSX_IMM_OP(name, constExpr, codeGen) \
static void rpsx##name() \
{ \
if (!_Rt_) \
{ \
/* IRX module-import HLE trampoline marker: ADDIU $0,$0,idx encodes as */ \
/* opcode 0x2400xxxx — the only I-type imm op whose top half is 0x2400. */ \
if ((psxRegs.code >> 16) == 0x2400) \
psxRecompileIrxImport(); \
return; \
} \
if (PSX_IS_CONST1(_Rs_)) \
{ \
const u32 result = (constExpr); \
_psxDeleteReg(_Rt_, 0); \
PSX_SET_CONST(_Rt_); \
g_psxConstRegs[_Rt_] = result; \
return; \
} \
_addNeededPSXtoArm64GPR(_Rs_); \
_addNeededPSXtoArm64GPR(_Rt_); \
const int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); \
const int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_WRITE); \
codeGen; \
_clearNeededArm64GPRregs(); \
PSX_DEL_CONST(_Rt_); \
}
// PS2 IOP doesn't trap signed overflow, so ADDI is an exact alias of ADDIU.
PSX_IMM_OP(ADDIU, g_psxConstRegs[_Rs_] + _Imm_,
{ armAsm->Add(armWRegister(rt), armWRegister(rs), static_cast<s64>(static_cast<s32>(_Imm_))); })
static void rpsxADDI() { rpsxADDIU(); }
PSX_IMM_OP(ANDI, g_psxConstRegs[_Rs_] & _ImmU_,
{ armAsm->And(armWRegister(rt), armWRegister(rs), static_cast<u64>(_ImmU_)); })
PSX_IMM_OP(ORI, g_psxConstRegs[_Rs_] | _ImmU_,
{ armAsm->Orr(armWRegister(rt), armWRegister(rs), static_cast<u64>(_ImmU_)); })
PSX_IMM_OP(XORI, g_psxConstRegs[_Rs_] ^ _ImmU_,
{ armAsm->Eor(armWRegister(rt), armWRegister(rs), static_cast<u64>(_ImmU_)); })
PSX_IMM_OP(SLTI, ((s32)g_psxConstRegs[_Rs_] < (s32)_Imm_) ? 1u : 0u,
{ armAsm->Cmp(armWRegister(rs), static_cast<s64>(static_cast<s32>(_Imm_))); armAsm->Cset(armWRegister(rt), a64::lt); })
PSX_IMM_OP(SLTIU, (g_psxConstRegs[_Rs_] < (u32)(s32)_Imm_) ? 1u : 0u,
{ armAsm->Cmp(armWRegister(rs), static_cast<s64>(static_cast<s32>(_Imm_))); armAsm->Cset(armWRegister(rt), a64::lo); })
static void rpsxLUI()
{
if (!_Rt_) return;
_psxDeleteReg(_Rt_, 0);
PSX_SET_CONST(_Rt_);
g_psxConstRegs[_Rt_] = psxRegs.code << 16;
}
// Allocator-aware 3-op IOP macro: rd = rs OP rt.
//
// Const-folds when both Rs and Rt are const (matches x86
// PSXRECOMPILE_CONSTCODE0); otherwise allocates Rd in a host GPR and uses
// `codeGen` to emit one of three branches:
// rs_const → emit `op(rd, rt, const_Rs)` (or per-op flipped form)
// rt_const → emit `op(rd, rs, const_Rt)`
// else → emit `op(rd, rs, rt)`
// The codeGen block sees five locals: bool rs_const, rt_const, int rs, rt, rd
// (rs/rt are -1 when their const fast-path is taken).
#define PSX_REG_OP(name, foldExpr, codeGen) \
static void rpsx##name() \
{ \
if (!_Rd_) return; \
if (PSX_IS_CONST2(_Rs_, _Rt_)) \
{ \
_psxDeleteReg(_Rd_, 0); \
PSX_SET_CONST(_Rd_); \
g_psxConstRegs[_Rd_] = (foldExpr); \
return; \
} \
_addNeededPSXtoArm64GPR(_Rs_); \
_addNeededPSXtoArm64GPR(_Rt_); \
_addNeededPSXtoArm64GPR(_Rd_); \
const bool rs_const = PSX_IS_CONST1(_Rs_); \
const bool rt_const = PSX_IS_CONST1(_Rt_); \
const int rs = rs_const ? -1 : _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ); \
const int rt = rt_const ? -1 : _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ); \
const int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE); \
(void)rs; (void)rt; \
codeGen; \
_clearNeededArm64GPRregs(); \
PSX_DEL_CONST(_Rd_); \
}
PSX_REG_OP(ADD,
g_psxConstRegs[_Rs_] + g_psxConstRegs[_Rt_],
{
if (rs_const)
armAsm->Add(armWRegister(rd), armWRegister(rt), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rs_])));
else if (rt_const)
armAsm->Add(armWRegister(rd), armWRegister(rs), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rt_])));
else
armAsm->Add(armWRegister(rd), armWRegister(rs), armWRegister(rt));
})
PSX_REG_OP(ADDU,
g_psxConstRegs[_Rs_] + g_psxConstRegs[_Rt_],
{
if (rs_const)
armAsm->Add(armWRegister(rd), armWRegister(rt), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rs_])));
else if (rt_const)
armAsm->Add(armWRegister(rd), armWRegister(rs), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rt_])));
else
armAsm->Add(armWRegister(rd), armWRegister(rs), armWRegister(rt));
})
PSX_REG_OP(SUB,
g_psxConstRegs[_Rs_] - g_psxConstRegs[_Rt_],
{
if (_Rs_ == _Rt_) // Rs - Rs == 0
armAsm->Mov(armWRegister(rd), 0);
else if (rs_const)
{
const u32 cv = g_psxConstRegs[_Rs_];
if (cv == 0)
armAsm->Neg(armWRegister(rd), armWRegister(rt));
else
{
// rd may alias rt (Rd == Rt); materialize the const minuend in a
// non-allocatable scratch so the Mov can't clobber the subtrahend.
armAsm->Mov(RWSCRATCH, cv);
armAsm->Sub(armWRegister(rd), RWSCRATCH, armWRegister(rt));
}
}
else if (rt_const)
armAsm->Sub(armWRegister(rd), armWRegister(rs), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rt_])));
else
armAsm->Sub(armWRegister(rd), armWRegister(rs), armWRegister(rt));
})
PSX_REG_OP(SUBU,
g_psxConstRegs[_Rs_] - g_psxConstRegs[_Rt_],
{
if (_Rs_ == _Rt_) // Rs - Rs == 0
armAsm->Mov(armWRegister(rd), 0);
else if (rs_const)
{
const u32 cv = g_psxConstRegs[_Rs_];
if (cv == 0)
armAsm->Neg(armWRegister(rd), armWRegister(rt));
else
{
// rd may alias rt (Rd == Rt); materialize the const minuend in a
// non-allocatable scratch so the Mov can't clobber the subtrahend.
armAsm->Mov(RWSCRATCH, cv);
armAsm->Sub(armWRegister(rd), RWSCRATCH, armWRegister(rt));
}
}
else if (rt_const)
armAsm->Sub(armWRegister(rd), armWRegister(rs), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rt_])));
else
armAsm->Sub(armWRegister(rd), armWRegister(rs), armWRegister(rt));
})
PSX_REG_OP(AND,
g_psxConstRegs[_Rs_] & g_psxConstRegs[_Rt_],
{
if (rs_const)
armAsm->And(armWRegister(rd), armWRegister(rt), static_cast<u64>(g_psxConstRegs[_Rs_]));
else if (rt_const)
armAsm->And(armWRegister(rd), armWRegister(rs), static_cast<u64>(g_psxConstRegs[_Rt_]));
else
armAsm->And(armWRegister(rd), armWRegister(rs), armWRegister(rt));
})
PSX_REG_OP(OR,
g_psxConstRegs[_Rs_] | g_psxConstRegs[_Rt_],
{
if (rs_const)
armAsm->Orr(armWRegister(rd), armWRegister(rt), static_cast<u64>(g_psxConstRegs[_Rs_]));
else if (rt_const)
armAsm->Orr(armWRegister(rd), armWRegister(rs), static_cast<u64>(g_psxConstRegs[_Rt_]));
else
armAsm->Orr(armWRegister(rd), armWRegister(rs), armWRegister(rt));
})
PSX_REG_OP(XOR,
g_psxConstRegs[_Rs_] ^ g_psxConstRegs[_Rt_],
{
if (rs_const)
armAsm->Eor(armWRegister(rd), armWRegister(rt), static_cast<u64>(g_psxConstRegs[_Rs_]));
else if (rt_const)
armAsm->Eor(armWRegister(rd), armWRegister(rs), static_cast<u64>(g_psxConstRegs[_Rt_]));
else
armAsm->Eor(armWRegister(rd), armWRegister(rs), armWRegister(rt));
})
PSX_REG_OP(NOR,
~(g_psxConstRegs[_Rs_] | g_psxConstRegs[_Rt_]),
{
if (rs_const)
armAsm->Orr(armWRegister(rd), armWRegister(rt), static_cast<u64>(g_psxConstRegs[_Rs_]));
else if (rt_const)
armAsm->Orr(armWRegister(rd), armWRegister(rs), static_cast<u64>(g_psxConstRegs[_Rt_]));
else
armAsm->Orr(armWRegister(rd), armWRegister(rs), armWRegister(rt));
armAsm->Mvn(armWRegister(rd), armWRegister(rd));
})
PSX_REG_OP(SLT,
((s32)g_psxConstRegs[_Rs_] < (s32)g_psxConstRegs[_Rt_]) ? 1u : 0u,
{
// Rs < Rt (signed). When Rs is const k, equivalent to Rt > k → cset gt.
if (rs_const)
{
armAsm->Cmp(armWRegister(rt), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rs_])));
armAsm->Cset(armWRegister(rd), a64::gt);
}
else if (rt_const)
{
armAsm->Cmp(armWRegister(rs), static_cast<s64>(static_cast<s32>(g_psxConstRegs[_Rt_])));
armAsm->Cset(armWRegister(rd), a64::lt);
}
else
{
armAsm->Cmp(armWRegister(rs), armWRegister(rt));
armAsm->Cset(armWRegister(rd), a64::lt);
}
})
PSX_REG_OP(SLTU,
(g_psxConstRegs[_Rs_] < g_psxConstRegs[_Rt_]) ? 1u : 0u,
{
// Rs <u Rt. When Rs is const k, equivalent to Rt >u k → cset hi.
if (rs_const)
{
armAsm->Cmp(armWRegister(rt), static_cast<u64>(g_psxConstRegs[_Rs_]));
armAsm->Cset(armWRegister(rd), a64::hi);
}
else if (rt_const)
{
armAsm->Cmp(armWRegister(rs), static_cast<u64>(g_psxConstRegs[_Rt_]));
armAsm->Cset(armWRegister(rd), a64::lo);
}
else
{
armAsm->Cmp(armWRegister(rs), armWRegister(rt));
armAsm->Cset(armWRegister(rd), a64::lo);
}
})
////////////////////////////////////////////////////////////////////
// Shift Instructions — rd = rt << sa (or variable shift by rs)
static void rpsxSLL()
{
if (!_Rd_) return;
if (PSX_IS_CONST1(_Rt_)) {
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] << _Sa_;
return;
}
_addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_);
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
if (_Sa_)
armAsm->Lsl(armWRegister(rd), armWRegister(rt), _Sa_);
else
armAsm->Mov(armWRegister(rd), armWRegister(rt));
_clearNeededArm64GPRregs();
PSX_DEL_CONST(_Rd_);
}
static void rpsxSRL()
{
if (!_Rd_) return;
if (PSX_IS_CONST1(_Rt_)) {
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] >> _Sa_;
return;
}
_addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_);
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
if (_Sa_)
armAsm->Lsr(armWRegister(rd), armWRegister(rt), _Sa_);
else
armAsm->Mov(armWRegister(rd), armWRegister(rt));
_clearNeededArm64GPRregs();
PSX_DEL_CONST(_Rd_);
}
static void rpsxSRA()
{
if (!_Rd_) return;
if (PSX_IS_CONST1(_Rt_)) {
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
g_psxConstRegs[_Rd_] = (s32)g_psxConstRegs[_Rt_] >> _Sa_;
return;
}
_addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_);
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
if (_Sa_)
armAsm->Asr(armWRegister(rd), armWRegister(rt), _Sa_);
else
armAsm->Mov(armWRegister(rd), armWRegister(rt));
_clearNeededArm64GPRregs();
PSX_DEL_CONST(_Rd_);
}
// Variable shifts: rd = rt <op> (rs & 0x1F). When Rs is const, fold to a
// fixed-immediate shift — same emit as rpsxSLL/SRL/SRA in the imm form.
static void rpsxSLLV()
{
if (!_Rd_) return;
if (PSX_IS_CONST2(_Rs_, _Rt_)) {
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] << (g_psxConstRegs[_Rs_] & 0x1F);
return;
}
_addNeededPSXtoArm64GPR(_Rs_); _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_);
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
if (PSX_IS_CONST1(_Rs_)) {
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
const u32 sa = g_psxConstRegs[_Rs_] & 0x1F;
if (sa)
armAsm->Lsl(armWRegister(rd), armWRegister(rt), sa);
else
armAsm->Mov(armWRegister(rd), armWRegister(rt));
}
else {
// Alloc Rs BEFORE Rd: when Rd == Rs, allocating Rd MODE_WRITE first
// would grab a fresh write-only slot (no load), and the subsequent
// Rs MODE_READ alloc would reuse that empty slot without loading.
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
armAsm->Lsl(armWRegister(rd), armWRegister(rt), armWRegister(rs));
}
_clearNeededArm64GPRregs();
PSX_DEL_CONST(_Rd_);
}
static void rpsxSRLV()
{
if (!_Rd_) return;
if (PSX_IS_CONST2(_Rs_, _Rt_)) {
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
g_psxConstRegs[_Rd_] = g_psxConstRegs[_Rt_] >> (g_psxConstRegs[_Rs_] & 0x1F);
return;
}
_addNeededPSXtoArm64GPR(_Rs_); _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_);
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
if (PSX_IS_CONST1(_Rs_)) {
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
const u32 sa = g_psxConstRegs[_Rs_] & 0x1F;
if (sa)
armAsm->Lsr(armWRegister(rd), armWRegister(rt), sa);
else
armAsm->Mov(armWRegister(rd), armWRegister(rt));
}
else {
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
armAsm->Lsr(armWRegister(rd), armWRegister(rt), armWRegister(rs));
}
_clearNeededArm64GPRregs();
PSX_DEL_CONST(_Rd_);
}
static void rpsxSRAV()
{
if (!_Rd_) return;
if (PSX_IS_CONST2(_Rs_, _Rt_)) {
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
g_psxConstRegs[_Rd_] = (s32)g_psxConstRegs[_Rt_] >> (g_psxConstRegs[_Rs_] & 0x1F);
return;
}
_addNeededPSXtoArm64GPR(_Rs_); _addNeededPSXtoArm64GPR(_Rt_); _addNeededPSXtoArm64GPR(_Rd_);
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
if (PSX_IS_CONST1(_Rs_)) {
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
const u32 sa = g_psxConstRegs[_Rs_] & 0x1F;
if (sa)
armAsm->Asr(armWRegister(rd), armWRegister(rt), sa);
else
armAsm->Mov(armWRegister(rd), armWRegister(rt));
}
else {
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
armAsm->Asr(armWRegister(rd), armWRegister(rt), armWRegister(rs));
}
_clearNeededArm64GPRregs();
PSX_DEL_CONST(_Rd_);
}
////////////////////////////////////////////////////////////////////
// Multiply/Divide — write to HI:LO
// MULT/MULTU/DIV/DIVU compute entirely in non-allocatable scratch
// (w8=RWSCRATCH, w9, w10): _psxMoveGPRtoR fetches each operand from wherever
// it currently lives (const, allocated host reg, or memory) without
// disturbing the register cache, and HI/LO are memory-only on this backend,
// so no flush is needed and cached guest registers stay live across the op.
// The results must NOT land in allocator-pool registers (x0-x7): an earlier
// version wrote the product/quotient to w0-w3, which silently corrupted any
// live "$Rt lives in w0" mapping — e.g. the GCC divide-by-zero check
// `bne $Rt, $zero, +2 / break 7` then read the quotient (often 0) and fired
// BREAK even though $Rt was nonzero. (The x86 rec FLUSH_EVERYTHINGs here
// because its MUL/DIV results land in fixed pool regs edx:eax; ours don't,
// so the flush would be pure waste.)
static void rpsxMULT()
{
_psxMoveGPRtoR(a64::w9, _Rs_);
_psxMoveGPRtoR(a64::w10, _Rt_);
armAsm->Smull(a64::x8, a64::w9, a64::w10);
// IOP HI:LO are 32-bit registers
armAsm->Str(a64::w8, armPsxRegMem(&psxRegs.GPR.n.lo));
armAsm->Lsr(a64::x8, a64::x8, 32);
armAsm->Str(a64::w8, armPsxRegMem(&psxRegs.GPR.n.hi));
g_iopCyclePenalty = psxInstCycles_Mult;
}
static void rpsxMULTU()
{
_psxMoveGPRtoR(a64::w9, _Rs_);
_psxMoveGPRtoR(a64::w10, _Rt_);
armAsm->Umull(a64::x8, a64::w9, a64::w10);
armAsm->Str(a64::w8, armPsxRegMem(&psxRegs.GPR.n.lo));
armAsm->Lsr(a64::x8, a64::x8, 32);
armAsm->Str(a64::w8, armPsxRegMem(&psxRegs.GPR.n.hi));
g_iopCyclePenalty = psxInstCycles_Mult;
}
static void rpsxDIV()
{
_psxMoveGPRtoR(a64::w9, _Rs_);
_psxMoveGPRtoR(a64::w10, _Rt_);
a64::Label zero_case, done;
armAsm->Cbz(a64::w10, &zero_case);
// Normal path: SDIV is defined on aarch64 for the (INT_MIN / -1) overflow
// case (returns INT_MIN, remainder 0) which matches psxDIV()'s explicit
// overflow branch in R3000AOpcodeTables.cpp:69, so only the divide-by-zero
// case needs fixing here. Remainder overwrites w9 (Rs is dead after Msub).
armAsm->Sdiv(a64::w8, a64::w9, a64::w10);
armAsm->Msub(a64::w9, a64::w8, a64::w10, a64::w9);
armAsm->B(&done);
armAsm->Bind(&zero_case);
// LO = sign(Rs) ? 1 : 0xFFFFFFFF; HI = Rs (already in w9).
// Matches psxDIV(_rRt_==0).
armAsm->Mov(a64::w8, -1);
armAsm->Cmp(a64::w9, 0);
armAsm->Cneg(a64::w8, a64::w8, a64::mi);
armAsm->Bind(&done);
armAsm->Str(a64::w8, armPsxRegMem(&psxRegs.GPR.n.lo));
armAsm->Str(a64::w9, armPsxRegMem(&psxRegs.GPR.n.hi));
g_iopCyclePenalty = psxInstCycles_Div;
}
static void rpsxDIVU()
{
_psxMoveGPRtoR(a64::w9, _Rs_);
_psxMoveGPRtoR(a64::w10, _Rt_);
a64::Label zero_case, done;
armAsm->Cbz(a64::w10, &zero_case);
armAsm->Udiv(a64::w8, a64::w9, a64::w10);
armAsm->Msub(a64::w9, a64::w8, a64::w10, a64::w9);
armAsm->B(&done);
armAsm->Bind(&zero_case);
// LO = 0xFFFFFFFF; HI = Rs (already in w9). Matches psxDIVU(_rRt_==0).
armAsm->Mov(a64::w8, -1);
armAsm->Bind(&done);
armAsm->Str(a64::w8, armPsxRegMem(&psxRegs.GPR.n.lo));
armAsm->Str(a64::w9, armPsxRegMem(&psxRegs.GPR.n.hi));
g_iopCyclePenalty = psxInstCycles_Div;
}
////////////////////////////////////////////////////////////////////
// Move from/to HI/LO
static void rpsxMFHI()
{
if (!_Rd_) return;
_psxDeleteReg(_Rd_, 0);
PSX_DEL_CONST(_Rd_);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
armLoadPsxRegPtr(armWRegister(rd), &psxRegs.GPR.n.hi);
_clearNeededArm64GPRregs();
}
static void rpsxMTHI()
{
// const Rs: store immediate to hi directly.
if (PSX_IS_CONST1(_Rs_))
{
armAsm->Mov(RWSCRATCH, g_psxConstRegs[_Rs_]);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.n.hi));
return;
}
// Otherwise read Rs via the allocator — no FLUSH_EVERYTHING needed here.
_addNeededPSXtoArm64GPR(_Rs_);
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
armAsm->Str(armWRegister(rs), armPsxRegMem(&psxRegs.GPR.n.hi));
_clearNeededArm64GPRregs();
}
static void rpsxMFLO()
{
if (!_Rd_) return;
_psxDeleteReg(_Rd_, 0);
PSX_DEL_CONST(_Rd_);
int rd = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
armLoadPsxRegPtr(armWRegister(rd), &psxRegs.GPR.n.lo);
_clearNeededArm64GPRregs();
}
static void rpsxMTLO()
{
if (PSX_IS_CONST1(_Rs_))
{
armAsm->Mov(RWSCRATCH, g_psxConstRegs[_Rs_]);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.n.lo));
return;
}
_addNeededPSXtoArm64GPR(_Rs_);
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
armAsm->Str(armWRegister(rs), armPsxRegMem(&psxRegs.GPR.n.lo));
_clearNeededArm64GPRregs();
}
////////////////////////////////////////////////////////////////////
// Load/Store
//
// Loads: compute address in w0, flush, call iopMemReadN, sign/zero extend, store result
// Stores: compute address in w0, value in w1, flush, call iopMemWriteN
static void rpsxLoadGeneric(int size, bool sign)
{
// Read Rs const value FIRST — before deleting Rt (critical when Rs==Rt,
// since _psxDeleteReg clears const state and frees the host register).
const bool rs_const = PSX_IS_CONST1(_Rs_);
const u32 rs_val = rs_const ? g_psxConstRegs[_Rs_] : 0;
// Delete destination register (flush=1 to write back, in case Rs==Rt
// and Rs is in a host register — need the value in memory).
if (_Rt_)
_psxDeleteReg(_Rt_, 1);
_psxFlushCall(FLUSH_EVERYTHING);
// Compute address: base + imm16 (after flush, safe to use w0)
if (rs_const)
{
armAsm->Mov(RWARG1, rs_val + _Imm_);
}
else
{
armLoadPsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rs_]);
if (_Imm_ != 0)
armAsm->Add(RWARG1, RWARG1, static_cast<s64>(static_cast<s32>(_Imm_)));
}
// Call iopMemRead — address in w0, result returned in w0
switch (size)
{
case 8: armEmitCall((void*)iopMemRead8); break;
case 16: armEmitCall((void*)iopMemRead16); break;
case 32: armEmitCall((void*)iopMemRead32); break;
}
if (!_Rt_)
return; // dummy read
// Sign/zero extend result (w0)
switch (size)
{
case 8:
if (sign)
armAsm->Sxtb(RWARG1, RWARG1);
else
armAsm->Uxtb(RWARG1, RWARG1);
break;
case 16:
if (sign)
armAsm->Sxth(RWARG1, RWARG1);
else
armAsm->Uxth(RWARG1, RWARG1);
break;
case 32:
break; // no extension needed
}
// Store result to destination register
armStorePsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rt_]);
}
// Emit the C fallback for a store — address in w0, value in w1.
static void rpsxEmitStoreCall(int size)
{
switch (size)
{
case 8: armEmitCall((void*)iopMemWrite8); break;
case 16: armEmitCall((void*)iopMemWrite16); break;
case 32: armEmitCall((void*)iopMemWrite32); break;
}
}
static void rpsxStoreGeneric(int size)
{
// Read const values before flush
const bool rs_const = PSX_IS_CONST1(_Rs_);
const u32 rs_val = rs_const ? g_psxConstRegs[_Rs_] : 0;
const bool rt_const = PSX_IS_CONST1(_Rt_);
const u32 rt_val = rt_const ? g_psxConstRegs[_Rt_] : 0;
// Flush all registers BEFORE computing operands
_psxFlushCall(FLUSH_EVERYTHING);
// Compute address: base + imm16
if (rs_const)
{
armAsm->Mov(RWARG1, rs_val + _Imm_);
}
else
{
armLoadPsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rs_]);
if (_Imm_ != 0)
armAsm->Add(RWARG1, RWARG1, static_cast<s64>(static_cast<s32>(_Imm_)));
}
// Load store value into w1
if (rt_const)
armAsm->Mov(RWARG2, rt_val);
else
armLoadPsxRegPtr(RWARG2, &psxRegs.GPR.r[_Rt_]);
// RAM-store fast path, out-of-line: one BL into the shared per-width stub
// (g_iopStoreStub, emitted with the dispatchers — see _DynGen_StoreStub
// in iR3000A-arm64.cpp for the routing derivation). The site stays the
// same size as the old C call, so the fast path costs no per-site icache
// footprint; the stub tail-jumps to iopMemWrite* for hw/unmapped targets,
// which then returns here directly.
//
// Compile-time-known hw/unmapped targets skip the stub and call C
// straight away — identical to the old code.
if (rs_const && ((rs_val + _Imm_) & 0x1f800000) != 0)
{
rpsxEmitStoreCall(size);
return;
}
armEmitCall(g_iopStoreStub[size == 8 ? 0 : (size == 16 ? 1 : 2)]);
}
static void rpsxLB() { rpsxLoadGeneric(8, true); }
static void rpsxLBU() { rpsxLoadGeneric(8, false); }
static void rpsxLH() { rpsxLoadGeneric(16, true); }
static void rpsxLHU() { rpsxLoadGeneric(16, false); }
static void rpsxLW() { rpsxLoadGeneric(32, false); }
static void rpsxSB() { rpsxStoreGeneric(8); }
static void rpsxSH() { rpsxStoreGeneric(16); }
static void rpsxSW() { rpsxStoreGeneric(32); }
// =====================================================================================================
// Unaligned word load/store: LWL / LWR / SWL / SWR
// ----------------------------------------------------------------------------------------------------
// These are partial-word merges keyed on the low two bits of the byte
// address — *not* generic unaligned loads. Compiler-emitted LWL+LWR or
// SWL+SWR pairs construct an unaligned 32-bit access; in isolation each
// op merges memory bytes with the existing register/memory contents per
// the formulae in pcsx2/R3000AOpcodeTables.cpp:psxLWL/LWR/SWL/SWR.
//
// byte_addr = rs + imm; (addr & 3) is saved to the stack across the
// iopMemRead32/Write32 C call; then the mask + shift + or merge is done
// inline, replacing the REC_FUNC interp fallback.
// =====================================================================================================
// Compute byte address (rs + imm) into RWARG1, leaving (byte_addr & 3) in
// RWSCRATCH for the caller's later use *before* the C call clobbers w0.
// On return, RWARG1 holds the aligned address (byte_addr & ~3) ready for
// iopMemRead32/iopMemWrite32.
static void rpsxComputeUnalignedAddr()
{
const bool rs_const = PSX_IS_CONST1(_Rs_);
const u32 rs_val = rs_const ? g_psxConstRegs[_Rs_] : 0;
if (rs_const)
{
const u32 byte_addr = rs_val + _Imm_;
armAsm->Mov(RWARG1, byte_addr & ~3u); // aligned address for memRead/Write32
armAsm->Mov(RWSCRATCH, byte_addr & 3u); // shift_input
}
else
{
armLoadPsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rs_]); // w0 = rs
if (_Imm_ != 0)
armAsm->Add(RWARG1, RWARG1, static_cast<s64>(static_cast<s32>(_Imm_))); // w0 = rs + imm (byte_addr)
armAsm->And(RWSCRATCH, RWARG1, 3); // RWSCRATCH = byte_addr & 3
armAsm->Bic(RWARG1, RWARG1, 3); // w0 = byte_addr & ~3
}
}
static void rpsxLWL()
{
if (_Rt_)
_psxDeleteReg(_Rt_, 1);
_psxFlushCall(FLUSH_EVERYTHING);
rpsxComputeUnalignedAddr();
// Save shift_input across the C call (callee-saved would also work,
// but the IOP rec doesn't reserve any of x19-x28 for the emitter).
armAsm->Sub(a64::sp, a64::sp, 16);
armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp));
armEmitCall((void*)iopMemRead32); // w0 = mem (aligned word)
armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp));
armAsm->Add(a64::sp, a64::sp, 16);
if (!_Rt_)
return; // dummy read — preserve memory side effects only
// shift = (byte_addr & 3) * 8.
armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift
// mask = 0x00ffffff >> shift, mem_shift = 24 - shift.
armAsm->Mov(a64::w2, 0x00ffffffu);
armAsm->Lsr(a64::w2, a64::w2, a64::w1); // w2 = mask
armAsm->Mov(RWSCRATCH, 24);
armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift
armAsm->Lsl(RWARG1, RWARG1, RWSCRATCH); // w0 = mem << (24 - shift)
// Merge: rt = (rt & mask) | (mem << (24 - shift)).
armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]);
armAsm->And(a64::w3, a64::w3, a64::w2);
armAsm->Orr(RWARG1, RWARG1, a64::w3);
armStorePsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rt_]);
}
static void rpsxLWR()
{
if (_Rt_)
_psxDeleteReg(_Rt_, 1);
_psxFlushCall(FLUSH_EVERYTHING);
rpsxComputeUnalignedAddr();
armAsm->Sub(a64::sp, a64::sp, 16);
armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp));
armEmitCall((void*)iopMemRead32); // w0 = mem
armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp));
armAsm->Add(a64::sp, a64::sp, 16);
if (!_Rt_)
return;
// shift = (byte_addr & 3) * 8.
armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift
// mask = 0xffffff00 << (24 - shift); mem_shift = shift.
armAsm->Mov(a64::w2, 0xffffff00u);
armAsm->Mov(RWSCRATCH, 24);
armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift
armAsm->Lsl(a64::w2, a64::w2, RWSCRATCH); // w2 = mask
armAsm->Lsr(RWARG1, RWARG1, a64::w1); // w0 = mem >> shift
armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]);
armAsm->And(a64::w3, a64::w3, a64::w2);
armAsm->Orr(RWARG1, RWARG1, a64::w3);
armStorePsxRegPtr(RWARG1, &psxRegs.GPR.r[_Rt_]);
}
static void rpsxSWL()
{
const bool rt_const = PSX_IS_CONST1(_Rt_);
const u32 rt_val = rt_const ? g_psxConstRegs[_Rt_] : 0;
_psxFlushCall(FLUSH_EVERYTHING);
rpsxComputeUnalignedAddr();
// Save aligned addr (RWARG1) and shift_input (RWSCRATCH) across the
// memRead call. Both needed for the subsequent memWrite + merge.
armAsm->Sub(a64::sp, a64::sp, 16);
armAsm->Str(RWARG1, a64::MemOperand(a64::sp, 0));
armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp, 4));
armEmitCall((void*)iopMemRead32); // w0 = mem
// Reload shift_input and aligned addr; mem stays in w0.
armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp, 4)); // w1 = shift_input
// shift = (byte_addr & 3) * 8.
armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift
// rt_shifted = rt >> (24 - shift)
if (rt_const)
armAsm->Mov(a64::w3, rt_val);
else
armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]);
armAsm->Mov(RWSCRATCH, 24);
armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift
armAsm->Lsr(a64::w3, a64::w3, RWSCRATCH); // w3 = rt >> (24 - shift)
// mem_masked = mem & (0xffffff00 << shift)
armAsm->Mov(a64::w2, 0xffffff00u);
armAsm->Lsl(a64::w2, a64::w2, a64::w1); // w2 = mask
armAsm->And(a64::w0, a64::w0, a64::w2); // w0 = mem & mask
armAsm->Orr(a64::w0, a64::w0, a64::w3); // merged value
// Now write back. iopMemWrite32(addr, value): w0 = addr, w1 = value.
armAsm->Mov(RWARG2, a64::w0); // w1 = value
armAsm->Ldr(RWARG1, a64::MemOperand(a64::sp, 0)); // w0 = aligned addr
armAsm->Add(a64::sp, a64::sp, 16);
armEmitCall((void*)iopMemWrite32);
}
static void rpsxSWR()
{
const bool rt_const = PSX_IS_CONST1(_Rt_);
const u32 rt_val = rt_const ? g_psxConstRegs[_Rt_] : 0;
_psxFlushCall(FLUSH_EVERYTHING);
rpsxComputeUnalignedAddr();
armAsm->Sub(a64::sp, a64::sp, 16);
armAsm->Str(RWARG1, a64::MemOperand(a64::sp, 0));
armAsm->Str(RWSCRATCH, a64::MemOperand(a64::sp, 4));
armEmitCall((void*)iopMemRead32); // w0 = mem
armAsm->Ldr(a64::w1, a64::MemOperand(a64::sp, 4));
armAsm->Lsl(a64::w1, a64::w1, 3); // w1 = shift
// rt_shifted = rt << shift
if (rt_const)
armAsm->Mov(a64::w3, rt_val);
else
armLoadPsxRegPtr(a64::w3, &psxRegs.GPR.r[_Rt_]);
armAsm->Lsl(a64::w3, a64::w3, a64::w1); // w3 = rt << shift
// mem_masked = mem & (0x00ffffff >> (24 - shift))
armAsm->Mov(a64::w2, 0x00ffffffu);
armAsm->Mov(RWSCRATCH, 24);
armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1); // RWSCRATCH = 24 - shift
armAsm->Lsr(a64::w2, a64::w2, RWSCRATCH); // w2 = mask
armAsm->And(a64::w0, a64::w0, a64::w2); // w0 = mem & mask
armAsm->Orr(a64::w0, a64::w0, a64::w3); // merged value
armAsm->Mov(RWARG2, a64::w0);
armAsm->Ldr(RWARG1, a64::MemOperand(a64::sp, 0));
armAsm->Add(a64::sp, a64::sp, 16);
armEmitCall((void*)iopMemWrite32);
}
////////////////////////////////////////////////////////////////////
// Branch/Jump Instructions
static void rpsxJ()
{
u32 newpc = _InstrucTarget_ * 4 + (psxpc & 0xf0000000);
psxRecompileNextInstruction(true, false);
psxSetBranchImm(newpc);
}
static void rpsxJAL()
{
u32 newpc = _InstrucTarget_ * 4 + (psxpc & 0xf0000000);
_psxDeleteReg(31, 0);
PSX_SET_CONST(31);
g_psxConstRegs[31] = psxpc + 4;
psxRecompileNextInstruction(true, false);
psxSetBranchImm(newpc);
}
static void rpsxJR()
{
// Save branch target to pcWriteback before delay slot — the delay slot's
// recCall will clobber w0 via _psxFlushCall.
_psxMoveGPRtoR(RWSCRATCH, _Rs_);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pcWriteback));
_psxFlushCall(FLUSH_EVERYTHING);
const bool swap = psxTrySwapDelaySlot(_Rs_, 0, 0);
if (!swap)
psxRecompileNextInstruction(true, false);
psxSetBranchReg();
}
static void rpsxJALR()
{
// Save branch target to pcWriteback before delay slot
_psxMoveGPRtoR(RWSCRATCH, _Rs_);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pcWriteback));
// Capture link before swap advances psxpc past the delay slot.
const u32 newpc = psxpc + 4;
// Rd == Rs disables swap — the delay slot reading Rs would observe the
// post-link value instead of the pre-link one.
const bool swap = (_Rd_ == _Rs_) ? false : psxTrySwapDelaySlot(_Rs_, 0, _Rd_);
// Save return address
if (_Rd_)
{
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
g_psxConstRegs[_Rd_] = newpc;
}
_psxFlushCall(FLUSH_EVERYTHING);
if (!swap)
psxRecompileNextInstruction(true, false);
psxSetBranchReg();
}
// Helper for conditional branches: compare Rs and Rt, branch if condition met
static void rpsxBranchCompare(a64::Condition cond)
{
u32 branchTo = ((s32)(s16)_Imm_ * 4) + psxpc;
// Compare Rs and Rt
if (PSX_IS_CONST2(_Rs_, _Rt_))
{
// Both constant — evaluate at compile time
bool taken = false;
if (cond == a64::eq) taken = (g_psxConstRegs[_Rs_] == g_psxConstRegs[_Rt_]);
else if (cond == a64::ne) taken = (g_psxConstRegs[_Rs_] != g_psxConstRegs[_Rt_]);
_psxFlushAllDirty();
psxRecompileNextInstruction(true, false);
psxSetBranchImm(taken ? branchTo : psxpc);
return;
}
// Hoist delay slot ahead of compare when it doesn't reference Rs/Rt.
// Flush AFTER swap so any cache dirties left by the swapped delay-slot
// instruction commit to memory before the compare/branch — the runtime
// taken path otherwise won't emit those flushes.
const bool swap = psxTrySwapDelaySlot(_Rs_, _Rt_, 0);
_psxFlushAllDirty();
// Runtime comparison
if (PSX_IS_CONST1(_Rs_))
{
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
armAsm->Cmp(armWRegister(rt), g_psxConstRegs[_Rs_]);
// Condition is symmetric for eq/ne so operand reversal is safe
}
else if (PSX_IS_CONST1(_Rt_))
{
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
armAsm->Cmp(armWRegister(rs), g_psxConstRegs[_Rt_]);
}
else
{
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
armAsm->Cmp(armWRegister(rs), armWRegister(rt));
}
_clearNeededArm64GPRregs();
a64::Label taken;
armAsm->B(&taken, cond);
// Not taken path
if (!swap)
{
psxSaveBranchState();
psxRecompileNextInstruction(true, false);
}
psxSetBranchImm(psxpc);
// Taken path — recompile delay slot from the correct PC
armAsm->Bind(&taken);
if (!swap)
{
psxpc -= 4;
psxLoadBranchState();
psxRecompileNextInstruction(true, false);
}
psxSetBranchImm(branchTo);
}
static void rpsxBEQ() { rpsxBranchCompare(a64::eq); }
static void rpsxBNE() { rpsxBranchCompare(a64::ne); }
// BLEZ / BGTZ / BLTZ / BGEZ — compare Rs against zero
static void rpsxBranchZero(a64::Condition cond)
{
u32 branchTo = ((s32)(s16)_Imm_ * 4) + psxpc;
if (PSX_IS_CONST1(_Rs_))
{
bool taken = false;
s32 val = (s32)g_psxConstRegs[_Rs_];
if (cond == a64::le) taken = (val <= 0);
else if (cond == a64::gt) taken = (val > 0);
else if (cond == a64::lt) taken = (val < 0);
else if (cond == a64::ge) taken = (val >= 0);
// No _psxFlushAllDirty() here: the branch is resolved statically (single
// successor, no compare needing clean regs and no Save/LoadBranchState
// snapshot), so the explicit flush is redundant — the delay-slot recompile
// manages its own dirties and block-end flushes the rest.
psxRecompileNextInstruction(true, false);
psxSetBranchImm(taken ? branchTo : psxpc);
return;
}
// Hoist delay slot ahead of compare when it doesn't reference Rs.
// Flush AFTER swap so any cache dirties left by the swapped delay-slot
// instruction commit to memory before the compare/branch.
const bool swap = psxTrySwapDelaySlot(_Rs_, 0, 0);
_psxFlushAllDirty();
int rs = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
armAsm->Cmp(armWRegister(rs), 0);
_clearNeededArm64GPRregs();
a64::Label taken;
armAsm->B(&taken, cond);
if (!swap)
{
psxSaveBranchState();
psxRecompileNextInstruction(true, false);
}
psxSetBranchImm(psxpc);
armAsm->Bind(&taken);
if (!swap)
{
psxpc -= 4;
psxLoadBranchState();
psxRecompileNextInstruction(true, false);
}
psxSetBranchImm(branchTo);
}
static void rpsxBLEZ() { rpsxBranchZero(a64::le); }
static void rpsxBGTZ() { rpsxBranchZero(a64::gt); }
static void rpsxBLTZ() { rpsxBranchZero(a64::lt); }
static void rpsxBGEZ() { rpsxBranchZero(a64::ge); }
static void rpsxBLTZAL()
{
_psxDeleteReg(31, 0);
PSX_SET_CONST(31);
g_psxConstRegs[31] = psxpc + 4;
rpsxBranchZero(a64::lt);
}
static void rpsxBGEZAL()
{
_psxDeleteReg(31, 0);
PSX_SET_CONST(31);
g_psxConstRegs[31] = psxpc + 4;
rpsxBranchZero(a64::ge);
}
////////////////////////////////////////////////////////////////////
// COP0
// MFC0/CFC0: Rt = CP0[Rd]
static void rpsxMFC0()
{
if (!_Rt_)
return;
// Mirrors x86 rpsxMFC0: allocate Rt as a write target and load CP0[Rd] into
// it. CP0 is never register-allocated, so its memory copy is always current;
// and nothing here calls a C function, so no flush is needed.
const int rt = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_WRITE);
armLoadPsxRegPtr(armWRegister(rt), &psxRegs.CP0.r[_Rd_]);
}
static void rpsxCFC0() { rpsxMFC0(); }
// MTC0/CTC0: CP0[Rd] = Rt
static void rpsxMTC0()
{
// Mirrors x86 rpsxMTC0: read Rt allocator-aware (const / dirty host reg /
// memory, via _psxMoveGPRtoR) and store to CP0[Rd]. No flush — no C call
// follows, and CP0 is not register-allocated so the memory store stands.
_psxMoveGPRtoR(RWSCRATCH, _Rt_);
armStorePsxRegPtr(RWSCRATCH, &psxRegs.CP0.r[_Rd_]);
}
static void rpsxCTC0() { rpsxMTC0(); }
// RFE: Status = (Status & 0xFFFFFFF0) | ((Status & 0x3C) >> 2)
// Then test IOP INTC to raise any pending interrupts.
static void rpsxRFE()
{
_psxFlushCall(FLUSH_EVERYTHING);
armLoadPsxRegPtr(RWSCRATCH, &psxRegs.CP0.n.Status);
armAsm->Ubfx(RWARG1, RWSCRATCH, 2, 4); // (Status >> 2) & 0xF == (Status & 0x3C) >> 2
armAsm->Bfi(RWSCRATCH, RWARG1, 0, 4); // replace low 4 bits of Status
armStorePsxRegPtr(RWSCRATCH, &psxRegs.CP0.n.Status);
armEmitCall((void*)iopTestIntc);
}
////////////////////////////////////////////////////////////////////
// GTE (COP2)
REC_GTE_FUNC(MFC2);
REC_GTE_FUNC(MTC2);
REC_GTE_FUNC(CFC2);
REC_GTE_FUNC(CTC2);
REC_GTE_FUNC(LWC2);
REC_GTE_FUNC(SWC2);
REC_GTE_FUNC(RTPS);
REC_GTE_FUNC(NCLIP);
REC_GTE_FUNC(OP);
REC_GTE_FUNC(DPCS);
REC_GTE_FUNC(INTPL);
REC_GTE_FUNC(MVMVA);
REC_GTE_FUNC(NCDS);
REC_GTE_FUNC(CDP);
REC_GTE_FUNC(NCDT);
REC_GTE_FUNC(NCCS);
REC_GTE_FUNC(CC);
REC_GTE_FUNC(NCS);
REC_GTE_FUNC(NCT);
REC_GTE_FUNC(SQR);
REC_GTE_FUNC(DCPL);
REC_GTE_FUNC(DPCT);
REC_GTE_FUNC(AVSZ3);
REC_GTE_FUNC(AVSZ4);
REC_GTE_FUNC(RTPT);
REC_GTE_FUNC(GPF);
REC_GTE_FUNC(GPL);
REC_GTE_FUNC(NCCT);
////////////////////////////////////////////////////////////////////
// rpsxSYSCALL and rpsxBREAK are defined in iR3000A-arm64.cpp
////////////////////////////////////////////////////////////////////
// Dispatch Tables
extern void (*rpsxBSC[64])();
extern void (*rpsxSPC[64])();
extern void (*rpsxREG[32])();
extern void (*rpsxCP0[32])();
extern void (*rpsxCP2[64])();
extern void (*rpsxCP2BSC[32])();
// Defined in iR3000A-arm64.cpp
extern void rpsxSYSCALL();
extern void rpsxBREAK();
static void rpsxSPECIAL() { rpsxSPC[_Funct_](); }
static void rpsxREGIMM() { rpsxREG[_Rt_](); }
static void rpsxCOP0() { rpsxCP0[_Rs_](); }
static void rpsxCOP2() { rpsxCP2[_Funct_](); }
static void rpsxBASIC() { rpsxCP2BSC[_Rs_](); }
static void rpsxNULL()
{
Console.WriteLn("psxUNK: %8.8x", psxRegs.code);
}
// clang-format off
void (*rpsxBSC[64])() = {
rpsxSPECIAL, rpsxREGIMM, rpsxJ , rpsxJAL , rpsxBEQ , rpsxBNE , rpsxBLEZ, rpsxBGTZ,
rpsxADDI , rpsxADDIU , rpsxSLTI, rpsxSLTIU, rpsxANDI, rpsxORI , rpsxXORI, rpsxLUI ,
rpsxCOP0 , rpsxNULL , rpsxCOP2, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxLB , rpsxLH , rpsxLWL , rpsxLW , rpsxLBU , rpsxLHU , rpsxLWR , rpsxNULL,
rpsxSB , rpsxSH , rpsxSWL , rpsxSW , rpsxNULL, rpsxNULL, rpsxSWR , rpsxNULL,
rpsxNULL , rpsxNULL , rgteLWC2, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxNULL , rpsxNULL , rgteSWC2, rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
};
void (*rpsxSPC[64])() = {
rpsxSLL , rpsxNULL, rpsxSRL , rpsxSRA , rpsxSLLV , rpsxNULL , rpsxSRLV, rpsxSRAV,
rpsxJR , rpsxJALR, rpsxNULL, rpsxNULL, rpsxSYSCALL, rpsxBREAK, rpsxNULL, rpsxNULL,
rpsxMFHI, rpsxMTHI, rpsxMFLO, rpsxMTLO, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL,
rpsxMULT, rpsxMULTU, rpsxDIV, rpsxDIVU, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL,
rpsxADD , rpsxADDU, rpsxSUB , rpsxSUBU, rpsxAND , rpsxOR , rpsxXOR , rpsxNOR ,
rpsxNULL, rpsxNULL, rpsxSLT , rpsxSLTU, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL,
rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL,
rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL,
};
void (*rpsxREG[32])() = {
rpsxBLTZ , rpsxBGEZ , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxBLTZAL, rpsxBGEZAL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
};
void (*rpsxCP0[32])() = {
rpsxMFC0, rpsxNULL, rpsxCFC0, rpsxNULL, rpsxMTC0, rpsxNULL, rpsxCTC0, rpsxNULL,
rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxRFE , rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
};
void (*rpsxCP2[64])() = {
rpsxBASIC, rgteRTPS , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL , rgteNCLIP, rpsxNULL, // 00
rpsxNULL , rpsxNULL , rpsxNULL , rpsxNULL, rgteOP , rpsxNULL , rpsxNULL , rpsxNULL, // 08
rgteDPCS , rgteINTPL, rgteMVMVA, rgteNCDS, rgteCDP , rpsxNULL , rgteNCDT , rpsxNULL, // 10
rpsxNULL , rpsxNULL , rpsxNULL , rgteNCCS, rgteCC , rpsxNULL , rgteNCS , rpsxNULL, // 18
rgteNCT , rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, // 20
rgteSQR , rgteDCPL , rgteDPCT , rpsxNULL, rpsxNULL, rgteAVSZ3, rgteAVSZ4, rpsxNULL, // 28
rgteRTPT , rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rpsxNULL , rpsxNULL , rpsxNULL, // 30
rpsxNULL , rpsxNULL , rpsxNULL , rpsxNULL, rpsxNULL, rgteGPF , rgteGPL , rgteNCCT, // 38
};
void (*rpsxCP2BSC[32])() = {
rgteMFC2, rpsxNULL, rgteCFC2, rpsxNULL, rgteMTC2, rpsxNULL, rgteCTC2, rpsxNULL,
rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL, rpsxNULL,
};
// clang-format on
////////////////////////////////////////////////////////////////////
// Back-Propagation Analysis Tables (architecture-independent)
////////////////////////////////////////////////////////////////////
#define rpsxpropSetRead(reg) \
{ \
if (!(pinst->regs[reg] & EEINST_USED)) \
pinst->regs[reg] |= EEINST_LASTUSE; \
prev->regs[reg] |= EEINST_LIVE | EEINST_USED; \
pinst->regs[reg] |= EEINST_USED; \
_recFillRegister(*pinst, NEONTYPE_GPRREG, reg, 0); \
}
#define rpsxpropSetWrite(reg) \
{ \
prev->regs[reg] &= ~(EEINST_LIVE | EEINST_USED); \
if (!(pinst->regs[reg] & EEINST_USED)) \
pinst->regs[reg] |= EEINST_LASTUSE; \
pinst->regs[reg] |= EEINST_USED; \
_recFillRegister(*pinst, NEONTYPE_GPRREG, reg, 1); \
}
void rpsxpropBSC(EEINST* prev, EEINST* pinst);
void rpsxpropSPECIAL(EEINST* prev, EEINST* pinst);
void rpsxpropREGIMM(EEINST* prev, EEINST* pinst);
void rpsxpropCP0(EEINST* prev, EEINST* pinst);
void rpsxpropCP2(EEINST* prev, EEINST* pinst);
void rpsxpropBSC(EEINST* prev, EEINST* pinst)
{
switch (psxRegs.code >> 26)
{
case 0: rpsxpropSPECIAL(prev, pinst); break;
case 1: rpsxpropREGIMM(prev, pinst); break;
case 2: break; // J
case 3: rpsxpropSetWrite(31); break; // JAL
case 4: case 5: // BEQ, BNE
rpsxpropSetRead(_Rs_);
rpsxpropSetRead(_Rt_);
break;
case 6: case 7: // BLEZ, BGTZ
rpsxpropSetRead(_Rs_);
break;
case 15: // LUI
rpsxpropSetWrite(_Rt_);
break;
case 16: rpsxpropCP0(prev, pinst); break;
case 18: rpsxpropCP2(prev, pinst); break;
case 40: case 41: case 42: case 43: case 46: // stores
rpsxpropSetRead(_Rt_);
rpsxpropSetRead(_Rs_);
break;
case 50: case 58: break; // LWC2, SWC2
default:
rpsxpropSetWrite(_Rt_);
rpsxpropSetRead(_Rs_);
break;
}
}
void rpsxpropSPECIAL(EEINST* prev, EEINST* pinst)
{
switch (_Funct_)
{
case 0: case 2: case 3: // SLL, SRL, SRA
rpsxpropSetWrite(_Rd_);
rpsxpropSetRead(_Rt_);
break;
case 8: // JR
rpsxpropSetRead(_Rs_);
break;
case 9: // JALR
rpsxpropSetWrite(_Rd_);
rpsxpropSetRead(_Rs_);
break;
case 12: case 13: // SYSCALL, BREAK
_recClearInst(prev);
prev->info = 0;
break;
case 15: break; // SYNC
case 16: // MFHI
rpsxpropSetWrite(_Rd_);
rpsxpropSetRead(PSX_HI);
break;
case 17: // MTHI
rpsxpropSetWrite(PSX_HI);
rpsxpropSetRead(_Rs_);
break;
case 18: // MFLO
rpsxpropSetWrite(_Rd_);
rpsxpropSetRead(PSX_LO);
break;
case 19: // MTLO
rpsxpropSetWrite(PSX_LO);
rpsxpropSetRead(_Rs_);
break;
case 24: case 25: case 26: case 27: // MULT, MULTU, DIV, DIVU
rpsxpropSetWrite(PSX_LO);
rpsxpropSetWrite(PSX_HI);
rpsxpropSetRead(_Rs_);
rpsxpropSetRead(_Rt_);
break;
case 32: case 33: case 34: case 35: // ADD, ADDU, SUB, SUBU
rpsxpropSetWrite(_Rd_);
if (_Rs_) rpsxpropSetRead(_Rs_);
if (_Rt_) rpsxpropSetRead(_Rt_);
break;
default:
rpsxpropSetWrite(_Rd_);
rpsxpropSetRead(_Rs_);
rpsxpropSetRead(_Rt_);
break;
}
}
void rpsxpropREGIMM(EEINST* prev, EEINST* pinst)
{
switch (_Rt_)
{
case 0: case 1: // BLTZ, BGEZ
rpsxpropSetRead(_Rs_);
break;
case 16: case 17: // BLTZAL, BGEZAL
rpsxpropSetRead(_Rs_);
break;
default:
break;
}
}
void rpsxpropCP0(EEINST* prev, EEINST* pinst)
{
switch (_Rs_)
{
case 0: case 2: // MFC0, CFC0
rpsxpropSetWrite(_Rt_);
break;
case 4: case 6: // MTC0, CTC0
rpsxpropSetRead(_Rt_);
break;
case 16: break; // RFE
default: break;
}
}
static void rpsxpropCP2_basic(EEINST* prev, EEINST* pinst)
{
switch (_Rs_)
{
case 0: case 2: // MFC2, CFC2
rpsxpropSetWrite(_Rt_);
break;
case 4: case 6: // MTC2, CTC2
rpsxpropSetRead(_Rt_);
break;
default: break;
}
}
void rpsxpropCP2(EEINST* prev, EEINST* pinst)
{
switch (_Funct_)
{
case 0: rpsxpropCP2_basic(prev, pinst); break;
default: break;
}
}