Files
ARMSX2/pcsx2/arm64/iR3000A-arm64.cpp
T
J1coding dac184c894 iOS: fix Legacy-mode SIGBUS on cross-window JIT code patches
Boot crash on iOS 18 under LiveContainer (Legacy W^X mode): CPU thread,
EXC_BAD_ACCESS KERN_PROTECTION_FAILURE in Arm64BaseBlocks::New -- a str of
a branch encoding into an r-x page. In Legacy mode armStartBlock flips
only the current block's 1 MiB window to RW, but New()'s link-repoint
loop, Remove()'s entry stubs and the backedge patch write into EARLIER
blocks, whose windows are execute-protected by then. PatchWord trusted
its callers to hold a Begin/EndCodeWrite scope; no caller on those paths
did. The toggle modes (macOS, Simulator) masked it because the compile
thread's write-protect is off arena-wide during recRecompile.

Give the patch primitive its own scope instead: armPatchCodeWord stores
through the dual-map alias and, when the target page is outside the open
emit window, wraps the store in a page-granular Begin/EndCodeWriteRange.
The page check matters -- the bump allocator routinely puts the previous
block's link site on the same page as the current block's start, and
RX-flipping that page mid-emit would kill the compile in a new way.

The whole-arena Begin/EndCodeWrite scopes that only existed to cover
those patches are gone with it. They were their own crash: Legacy range
windows bypass the refcount, so recClear's paired EndCodeWrite RX-flipped
the open emit window whenever the stale-overlap walk cleared blocks
mid-compile, and recClearIOP paid a whole-arena mprotect pair per covered
IOP store. The fastmem backpatch and the cold-island patches route
through armPatchCodeWord for the same reason, and microVU's code-cache
scope narrows to its own buffer span so an MTVU close can no longer
RX-flip an EE emit window open on the other thread.
2026-07-26 13:48:45 +02:00

1712 lines
51 KiB
C++

// SPDX-FileCopyrightText: 2026 yaps2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "arm64/iR3000A-arm64.h"
#include "arm64/AsmHelpers.h"
#include "common/HostSys.h"
#include "Host.h"
#include "R3000A.h"
#include "arm64/BaseblockEx-arm64.h"
#include "R5900OpcodeTables.h"
#include "IopBios.h"
#include "IopHw.h"
#include "DebugTools/SymbolGuardian.h"
#include "Common.h"
#include "VMManager.h"
#include "Config.h"
#include "common/Assertions.h"
#include "common/AlignedMalloc.h"
#include "common/Console.h"
#include "common/FastJmp.h"
#include "common/HeapArray.h"
#include "common/Perf.h"
namespace a64 = vixl::aarch64;
using namespace R3000A;
extern void psxBREAK();
u32 g_psxMaxRecMem = 0;
uptr psxRecLUT[0x10000];
u32 psxhwLUT[0x10000];
static __fi u32 HWADDR(u32 mem) { return psxhwLUT[mem >> 16] + mem; }
static BASEBLOCK* recRAM = nullptr;
static BASEBLOCK* recROM = nullptr;
static BASEBLOCK* recROM1 = nullptr;
static BASEBLOCK* recROM2 = nullptr;
static Arm64BaseBlocks recBlocks;
static u8* recPtr = nullptr;
static u8* recPtrEnd = nullptr;
u32 psxpc;
int psxbranch;
u32 g_iopCyclePenalty;
static EEINST* s_pInstCache = nullptr;
static u32 s_nInstCacheSize = 0;
static BASEBLOCK* s_pCurBlock = nullptr;
static BASEBLOCKEX* s_pCurBlockEx = nullptr;
static u32 s_nEndBlock = 0;
static u32 s_branchTo;
static bool s_nBlockFF;
// Longest guest extent, in bytes, of any block compiled since the last reset.
// Bounds how far back the straddler walk in psxRecClearMem must scan.
static u32 s_maxBlockBytes = 0;
static u32 s_saveConstRegs[32];
static u32 s_saveHasConstReg = 0, s_saveFlushedConstReg = 0;
static EEINST* s_psaveInstInfo = nullptr;
u32 s_psxBlockCycles = 0;
static u32 s_savenBlockCycles = 0;
static bool s_recompilingDelaySlot = false;
static void iPsxBranchTest(u32 newpc, u32 cpuBranch);
void psxRecompileNextInstruction(bool delayslot, bool swapped_delayslot);
extern void (*rpsxBSC[64])();
void rpsxpropBSC(EEINST* prev, EEINST* pinst);
static void iopClearRecLUT(BASEBLOCK* base, int count);
static void iopRecError(int err);
#define PSX_GETBLOCK(x) PC_GETBLOCK_(x, psxRecLUT)
#define PSXREC_CLEARM(mem) \
(((mem) < g_psxMaxRecMem && (psxRecLUT[(mem) >> 16] + (mem))) ? \
psxRecClearMem(mem) : \
4)
// LUT page management — same as x86 (architecture-independent)
static DynamicHeapArray<BASEBLOCK, 4096> recLutReserve;
static DynamicHeapArray<BASEBLOCK, 4096> recLutUnmapped;
static size_t recLutEntries = 0;
static bool extraRam = false;
// Constant pool for the IOP recompiler
static ArmConstantPool s_iopConstantPool;
// =====================================================================================================
// Dynamically Compiled Dispatchers - R3000A ARM64
// =====================================================================================================
static void iopRecRecompile(u32 startpc);
static const void* iopDispatcherEvent = nullptr;
static const void* iopDispatcherReg = nullptr;
static const void* iopJITCompile = nullptr;
static const void* iopEnterRecompiledCode = nullptr;
static const void* iopExitRecompiledCode = nullptr;
static const void* iopUnmappedRecLUTPage = nullptr;
static void recEventTest()
{
_cpuEventTest_Shared();
}
// ARM64 dispatcher: Load PC → two-level LUT lookup → jump to block
//
// psxRecLUT[pc >> 16] gives a base pointer to the BASEBLOCK array for that 64K page.
// Each BASEBLOCK is 8 bytes (one uptr function pointer).
// Index within page: (pc & 0xFFFF) >> 2 (since instructions are 4-byte aligned).
// So: base + ((pc & 0xFFFF) >> 2) * 8 = base + (pc & 0xFFFF) * 2
//
// ARM64 codegen:
// ldr w0, [addr of psxRegs.pc] // load PC
// lsr w1, w0, #16 // page index
// adr x2, psxRecLUT
// ldr x2, [x2, x1, lsl #3] // page base = psxRecLUT[pc >> 16]
// and w0, w0, #0xFFFF // low 16 bits
// add x2, x2, w0, uxtw #1 // base + (pc & 0xFFFF) * 2
// ldr x2, [x2] // block fnptr
// br x2 // jump to compiled code
static const void* _DynGen_DispatcherReg()
{
u8* retval = armGetCurrentCodePointer();
// Load psxRegs.pc
armAsm->Ldr(a64::w0, armPsxRegMem(&psxRegs.pc));
// Two-level LUT lookup
armAsm->Lsr(a64::w1, a64::w0, 16);
armMoveAddressToReg(RSCRATCHADDR, psxRecLUT);
armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RSCRATCHADDR, a64::x1, a64::LSL, 3));
// Index with full PC: base + pc * sizeof(BASEBLOCK)/4 = base + pc * 2
// recLUT_SetPage adjusts base to account for upper bits, so use full pc.
armAsm->Add(RSCRATCHADDR, RSCRATCHADDR, a64::Operand(a64::x0, a64::LSL, 1));
// Load block function pointer and jump
armAsm->Ldr(RSCRATCHADDR, a64::MemOperand(RSCRATCHADDR));
armAsm->Br(RSCRATCHADDR);
return retval;
}
// Called when a block hasn't been compiled yet — compile it, then dispatch
static const void* _DynGen_JITCompile()
{
u8* retval = armGetCurrentCodePointer();
// Call iopRecRecompile(psxRegs.pc)
armAsm->Ldr(RWARG1, armPsxRegMem(&psxRegs.pc));
armEmitCall((void*)iopRecRecompile);
// Now dispatch to the newly compiled block
armEmitJmp(iopDispatcherReg);
return retval;
}
// Entry point called from C code — sets up stack frame, enters dispatcher loop
static const void* _DynGen_EnterRecompiledCode()
{
u8* retval = armGetCurrentCodePointer();
// Save callee-saved registers and set up stack frame
armBeginStackFrame(false);
// Pin &psxRegs into RPSXSTATE for the duration of IOP JIT execution. Like
// EE's RSTATE, this turns "armMoveAddressToReg(scratch, &psxRegs.X); ldr"
// into a single ldr [RPSXSTATE, #off]. Callee-saved across all C calls.
armMoveAddressToReg(RPSXSTATE, &psxRegs);
// Jump into the dispatcher
armEmitJmp(iopDispatcherReg);
// Exit point — restore callee-saved registers and return
iopExitRecompiledCode = armGetCurrentCodePointer();
armEndStackFrame(false);
armAsm->Ret();
return retval;
}
// Out-of-line RAM-store fast path, one stub per store width (see
// rpsxStoreGeneric, which emits a single BL here per store site — keeping
// sites baseline-sized so the fast path costs no extra icache footprint).
//
// Calling convention: w0 = guest address, w1 = value, called after a full
// register flush (all volatiles dead, x21 = RPSXSTATE pinned and callee-saved
// across both the BL and the slow-path tail jump). iopMemWrite* masks
// addresses to 0x1fffffff; within that space the only WLUT-mapped write
// targets are main RAM on pages 0x00-0x7f and the parallel port at 0x1f00.
// Every hw region (0x1f80, 0x1f40, SIF 0x1d00, DEV9 0x1000, SPU2 0x1f90,
// parallel port 0x1f00, ROM 0x1fc0) and the unmapped rest have at least one
// of bits 23-28 set, and those bits survive the phys mask — so
// (addr & 0x1f800000) == 0 exactly selects "iopMemWrite* would take the WLUT
// RAM branch" for every KUSEG/KSEG0/KSEG1 mirror, matching iopMemReset's
// `for (i < 0x0080)` mapping loop. RAM stores hit
// Main[addr & (ExposedIopRam-1)] — the same bytes the WLUT path writes, since
// the C path's ((page & mask) << 16) | (mem & 0xffff) reduces to exactly that
// mask for both the 2MB and 8MB configurations — are swallowed when CP0
// Status IsC is set (cache isolated: no store, no SMC clear, matching the C
// RAM branch's p != NULL && !IsC guard), and only take the SMC clear call
// when the granule counter is nonzero, i.e. when psxRecClearMem would not
// have early-outed anyway.
//
// ⚠ The coverage probe here is mirror-collapsed (addr & (ExposedIopRam-1)),
// while iopCovAdjust and psxRecClearMem key the same array by HWADDR, which
// strips the KSEG base but does NOT collapse the RAM mirrors — recLUT_SetPage
// only sets psxhwLUT[page] = -(pagebase << 16). Under a 2MB configuration a
// block executed from a mirror page (0x20-0x7f) therefore registers coverage
// at a different granule than a store to its physical alias probes. That
// degrades to exactly the pre-existing blindness of keying recBlocks by
// HWADDR, so it is not a regression, but the two domains are a trap: keep
// them in mind before making either side finer-grained.
//
// Anything else tail-jumps to the C handler, which returns to the store site.
//
// Regenerated on every recResetIOP, so the baked ExposedIopRam mask and RAM
// base track extra-memory-mode flips (which discard all blocks).
const void* g_iopStoreStub[3] = {};
static const void* _DynGen_StoreStub(int size)
{
u8* retval = armGetCurrentCodePointer();
const u32 ram_mask = Ps2MemSize::ExposedIopRam - 1;
a64::Label slowPath, swallowed, clearHit;
armAsm->Tst(a64::w0, 0x1f800000);
armAsm->B(&slowPath, a64::ne);
armAsm->Ldr(a64::w3, armPsxRegMem(&psxRegs.CP0.n.Status));
armAsm->Tbnz(a64::w3, 16, &swallowed);
armMoveAddressToReg(a64::x4, iopMem->Main);
armAsm->And(a64::w2, a64::w0, ram_mask);
switch (size)
{
case 8: armAsm->Strb(a64::w1, a64::MemOperand(a64::x4, a64::x2)); break;
case 16: armAsm->Strh(a64::w1, a64::MemOperand(a64::x4, a64::x2)); break;
case 32: armAsm->Str(a64::w1, a64::MemOperand(a64::x4, a64::x2)); break;
}
armMoveAddressToReg(a64::x5, g_iopCodeCov);
armAsm->Lsr(a64::w6, a64::w2, kIopCovShift);
armAsm->Ldrh(a64::w6, a64::MemOperand(a64::x5, a64::x6, a64::LSL, 1));
armAsm->Cbnz(a64::w6, &clearHit);
armAsm->Bind(&swallowed);
armAsm->Ret();
// Rare: the store landed in a granule with live block coverage — run the
// full SMC clear (w0 still holds the original address).
armAsm->Bind(&clearHit);
armAsm->Sub(a64::sp, a64::sp, 16);
armAsm->Stp(a64::x29, a64::lr, a64::MemOperand(a64::sp));
armEmitCall((void*)iopStoreClearHit);
armAsm->Ldp(a64::x29, a64::lr, a64::MemOperand(a64::sp));
armAsm->Add(a64::sp, a64::sp, 16);
armAsm->Ret();
// Hw/unmapped target: tail-jump to C, which returns to the store site.
armAsm->Bind(&slowPath);
switch (size)
{
case 8: armEmitJmp((void*)iopMemWrite8); break;
case 16: armEmitJmp((void*)iopMemWrite16); break;
case 32: armEmitJmp((void*)iopMemWrite32); break;
}
return retval;
}
// Error handler for jumps to unmapped memory
static const void* _DynGen_UnmappedRecLUTPage()
{
u8* retval = armGetCurrentCodePointer();
armAsm->Mov(RWARG1, 0);
armEmitCall((void*)iopRecError);
armEmitJmp(iopExitRecompiledCode);
return retval;
}
// Generate all dispatcher stubs during reset
static void _DynGen_Dispatchers()
{
const u8* start = armGetCurrentCodePointer();
// Event test: call recEventTest, then fall through to dispatcher
iopDispatcherEvent = armGetCurrentCodePointer();
armEmitCall((void*)recEventTest);
iopDispatcherReg = _DynGen_DispatcherReg();
iopJITCompile = _DynGen_JITCompile();
iopEnterRecompiledCode = _DynGen_EnterRecompiledCode();
iopUnmappedRecLUTPage = _DynGen_UnmappedRecLUTPage();
g_iopStoreStub[0] = _DynGen_StoreStub(8);
g_iopStoreStub[1] = _DynGen_StoreStub(16);
g_iopStoreStub[2] = _DynGen_StoreStub(32);
// Block linker: stale / not-yet-compiled link sites divert to
// iopDispatcherReg — re-dispatch from the pc the site's tail already
// stored (psxSetBranchImm and the fallthrough tail both Str psxRegs.pc
// before the link site), never a direct re-compile. See the
// stale-dispatch policy in BaseblockEx-arm64.h; mirrors the EE rec.
recBlocks.SetJITCompile(iopJITCompile);
recBlocks.SetDispatcher(iopDispatcherReg);
Perf::any.Register(start, static_cast<u32>(armGetCurrentCodePointer() - start), "IOP Dispatcher");
}
static void iopRecError(int err)
{
switch (err)
{
case 0:
Host::ReportErrorAsync("R3000A Exception",
fmt::format("Unrecognized opcode (PC: 0x{:08x})", psxRegs.pc));
break;
case 1:
Host::ReportErrorAsync("R3000A Exception",
fmt::format("Jump to unaligned address (PC: 0x{:08x})", psxRegs.pc));
break;
}
VMManager::SetPaused(true);
Cpu->ExitExecution();
}
// =====================================================================================================
// Register allocation and code generation helpers
// =====================================================================================================
void _psxFlushConstReg(int reg)
{
if (PSX_IS_CONST1(reg) && !(g_psxFlushedConstReg & (1 << reg)))
{
armAsm->Mov(RWSCRATCH, g_psxConstRegs[reg]);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.r[reg]));
g_psxFlushedConstReg |= (1 << reg);
}
}
void _psxFlushConstRegs()
{
for (int i = 1; i < 32; ++i)
{
if (g_psxHasConstReg & (1 << i))
{
if (!(g_psxFlushedConstReg & (1 << i)))
{
armAsm->Mov(RWSCRATCH, g_psxConstRegs[i]);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.GPR.r[i]));
g_psxFlushedConstReg |= 1 << i;
}
if (g_psxHasConstReg == g_psxFlushedConstReg)
break;
}
}
}
void _psxDeleteReg(int reg, int flush)
{
if (!reg)
return;
if (flush && PSX_IS_CONST1(reg))
_psxFlushConstReg(reg);
PSX_DEL_CONST(reg);
_deletePSXtoArm64GPR(reg, flush ? DELETE_REG_FREE : DELETE_REG_FREE_NO_WRITEBACK);
}
void _psxMoveGPRtoR(const a64::Register& to, int fromgpr)
{
if (PSX_IS_CONST1(fromgpr))
{
armAsm->Mov(to.IsX() ? to : a64::Register(to.GetCode(), a64::kWRegSize), g_psxConstRegs[fromgpr]);
}
else
{
const int reg = EEINST_USEDTEST(fromgpr)
? _allocArm64GPR(ARM64TYPE_PSX, fromgpr, MODE_READ)
: _checkArm64GPR(ARM64TYPE_PSX, fromgpr, MODE_READ);
if (reg >= 0)
armAsm->Mov(to.IsX() ? to : a64::Register(to.GetCode(), a64::kWRegSize), armWRegister(reg));
else
armLoadPsxRegPtr(to.IsX() ? to : a64::Register(to.GetCode(), a64::kWRegSize), &psxRegs.GPR.r[fromgpr]);
}
}
void _psxFlushCall(int flushtype)
{
// Free caller-saved registers (and optionally others per flushtype)
for (int i = 0; i < NUM_ARM_GPR_REGS; i++)
{
if (!arm64gprs[i].inuse)
continue;
if (!armIsCalleeSavedRegister(i) ||
((flushtype & FLUSH_FREE_NONTEMP_X86) && arm64gprs[i].type != ARM64TYPE_TEMP) ||
((flushtype & FLUSH_FREE_TEMP_X86) && arm64gprs[i].type == ARM64TYPE_TEMP))
{
_freeArm64GPR(i);
}
}
if (flushtype & FLUSH_ALL_X86)
_flushArm64GPRregs();
if (flushtype & FLUSH_CONSTANT_REGS)
_psxFlushConstRegs();
if (flushtype & FLUSH_PC)
{
armAsm->Mov(RWSCRATCH, psxpc);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
}
}
void _psxFlushAllDirty()
{
for (u32 i = 0; i < 32; ++i)
{
if (PSX_IS_CONST1(i))
_psxFlushConstReg(i);
}
_flushArm64GPRregs();
}
void _psxOnWriteReg(int reg)
{
PSX_DEL_CONST(reg);
}
void psxSaveBranchState()
{
s_savenBlockCycles = s_psxBlockCycles;
memcpy(s_saveConstRegs, g_psxConstRegs, sizeof(g_psxConstRegs));
s_saveHasConstReg = g_psxHasConstReg;
s_saveFlushedConstReg = g_psxFlushedConstReg;
s_psaveInstInfo = g_pCurInstInfo;
memcpy(s_saveArm64GPRregs, arm64gprs, sizeof(arm64gprs));
}
void psxLoadBranchState()
{
s_psxBlockCycles = s_savenBlockCycles;
memcpy(g_psxConstRegs, s_saveConstRegs, sizeof(g_psxConstRegs));
g_psxHasConstReg = s_saveHasConstReg;
g_psxFlushedConstReg = s_saveFlushedConstReg;
g_pCurInstInfo = s_psaveInstInfo;
memcpy(arm64gprs, s_saveArm64GPRregs, sizeof(arm64gprs));
}
// =====================================================================================================
// Constant Propagation Code Templates
// =====================================================================================================
// rd = rs op rt — dispatch based on which operands are constant
// rd = rs op rt — matching x86 ordering: const check before _addNeeded
void psxRecompileCodeConst0(R3000AFNPTR constcode, R3000AFNPTR_INFO constscode, R3000AFNPTR_INFO consttcode, R3000AFNPTR_INFO noconstcode, int xmminfo)
{
if (!_Rd_)
return;
if (PSX_IS_CONST2(_Rs_, _Rt_))
{
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
constcode();
return;
}
int info = 0;
if (PSX_IS_CONST1(_Rs_))
info |= PROCESS_CONSTS;
if (PSX_IS_CONST1(_Rt_))
info |= PROCESS_CONSTT;
_addNeededPSXtoArm64GPR(_Rs_);
_addNeededPSXtoArm64GPR(_Rt_);
_addNeededPSXtoArm64GPR(_Rd_);
if (xmminfo & XMMINFO_READS)
{
if (!(info & PROCESS_CONSTS))
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
if (reg >= 0)
info |= PROCESS_EE_SET_S(reg);
}
}
if (xmminfo & XMMINFO_READT)
{
if (!(info & PROCESS_CONSTT))
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
if (reg >= 0)
info |= PROCESS_EE_SET_T(reg);
}
}
if (xmminfo & XMMINFO_WRITED)
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
if (reg >= 0)
info |= PROCESS_EE_SET_D(reg);
}
PSX_DEL_CONST(_Rd_);
if (info & PROCESS_CONSTS)
constscode(info);
else if (info & PROCESS_CONSTT)
consttcode(info);
else
noconstcode(info);
_clearNeededArm64GPRregs();
}
// IOP v1.0 IRX-import HLE backdoor — mirrors x86 psxRecompileIrxImport
// (pcsx2/x86/iR3000A.cpp). The IOP loader leaves module-import trampolines as
// an `ADDIU $0,$0,<index>` whose opcode top half is 0x2400. If the libname at
// the import table for this PC resolves a registered HLE handler, call it
// directly. A non-zero return means the handler advanced psxRegs.pc, so we must
// re-dispatch there instead of tearing the recompiled stint down (commit
// 89c7eb3a2). Restores HostFS (ioman/iomanx) redirection + IOP module symbol
// recovery under the JIT (DT-01). The devbuild-only trace/irxImportDebug path
// is intentionally omitted (diagnostic logging, no guest-visible effect).
void psxRecompileIrxImport()
{
const u32 import_table = irxImportTableAddr(psxpc - 4);
if (!import_table)
return;
const std::string libname = iopMemReadString(import_table + 12, 8);
const irxHLE hle = irxImportHLE(libname, static_cast<u16>(psxRegs.code & 0xffff));
if (!hle)
return;
armAsm->Mov(RWSCRATCH, psxRegs.code);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code));
armAsm->Mov(RWSCRATCH, psxpc);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
_psxFlushCall(FLUSH_NODESTROY);
armEmitCall(reinterpret_cast<const void*>(hle));
// HLE handled it (returned non-zero) → it set psxRegs.pc; re-dispatch.
armEmitCbnz(a64::w0, iopDispatcherReg);
}
// rt = rs op imm16
// Matches x86 pattern: const check before _addNeeded, PSX_DEL_CONST before noconstcode
void psxRecompileCodeConst1(R3000AFNPTR constcode, R3000AFNPTR_INFO noconstcode, int xmminfo)
{
if (!_Rt_)
return;
// x86 checks const BEFORE _addNeeded
if (PSX_IS_CONST1(_Rs_))
{
_psxDeleteReg(_Rt_, 0);
PSX_SET_CONST(_Rt_);
constcode();
return;
}
_addNeededPSXtoArm64GPR(_Rs_);
_addNeededPSXtoArm64GPR(_Rt_);
int info = 0;
if (xmminfo & XMMINFO_READS)
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
if (reg >= 0)
info |= PROCESS_EE_SET_S(reg);
}
if (xmminfo & XMMINFO_WRITET)
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_WRITE);
if (reg >= 0)
info |= PROCESS_EE_SET_T(reg);
}
// x86 deletes const BEFORE noconstcode
PSX_DEL_CONST(_Rt_);
noconstcode(info);
_clearNeededArm64GPRregs();
}
// rd = rt op sa
void psxRecompileCodeConst2(R3000AFNPTR constcode, R3000AFNPTR_INFO noconstcode, int xmminfo)
{
if (!_Rd_)
return;
_addNeededPSXtoArm64GPR(_Rt_);
_addNeededPSXtoArm64GPR(_Rd_);
int info = 0;
if (PSX_IS_CONST1(_Rt_))
{
_psxDeleteReg(_Rd_, 0);
PSX_SET_CONST(_Rd_);
constcode();
_clearNeededArm64GPRregs();
return;
}
if (xmminfo & XMMINFO_READT)
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
if (reg >= 0)
info |= PROCESS_EE_SET_T(reg);
}
if (xmminfo & XMMINFO_WRITED)
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rd_, MODE_WRITE);
if (reg >= 0)
info |= PROCESS_EE_SET_D(reg);
}
noconstcode(info);
_clearNeededArm64GPRregs();
PSX_DEL_CONST(_Rd_);
}
// [lo,hi] = rt op rs
void psxRecompileCodeConst3(R3000AFNPTR constcode, R3000AFNPTR_INFO constscode, R3000AFNPTR_INFO consttcode, R3000AFNPTR_INFO noconstcode, int LOHI)
{
_addNeededPSXtoArm64GPR(_Rs_);
_addNeededPSXtoArm64GPR(_Rt_);
int info = 0;
if (PSX_IS_CONST2(_Rs_, _Rt_))
{
constcode();
_clearNeededArm64GPRregs();
return;
}
if (PSX_IS_CONST1(_Rs_))
info |= PROCESS_CONSTS;
if (PSX_IS_CONST1(_Rt_))
info |= PROCESS_CONSTT;
if (!(info & PROCESS_CONSTS))
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rs_, MODE_READ);
if (reg >= 0)
info |= PROCESS_EE_SET_S(reg);
}
if (!(info & PROCESS_CONSTT))
{
int reg = _allocArm64GPR(ARM64TYPE_PSX, _Rt_, MODE_READ);
if (reg >= 0)
info |= PROCESS_EE_SET_T(reg);
}
if (info & PROCESS_CONSTS)
constscode(info);
else if (info & PROCESS_CONSTT)
consttcode(info);
else
noconstcode(info);
_clearNeededArm64GPRregs();
}
// =====================================================================================================
// Branch handling
// =====================================================================================================
void psxSetBranchReg()
{
psxbranch = 1;
// Flush all register allocations first, then load branch target from pcWriteback.
// This matches the EE SetBranchReg pattern — ensures delay slot results are
// written back before the branch target overwrites w0.
_psxFlushCall(FLUSH_EVERYTHING);
// Load branch target from pcWriteback
armLoadPsxRegPtr(a64::w0, &psxRegs.pcWriteback);
// Store to psxRegs.pc
armAsm->Str(a64::w0, armPsxRegMem(&psxRegs.pc));
// Check alignment
a64::Label unaligned;
armAsm->Tst(a64::w0, 3);
armAsm->B(&unaligned, a64::ne);
iPsxBranchTest(0xffffffff, 1);
armEmitJmp(iopDispatcherReg);
armAsm->Bind(&unaligned);
armAsm->Mov(RWARG1, 1);
armEmitCall((void*)iopRecError);
armEmitJmp(iopExitRecompiledCode);
}
void psxSetBranchImm(u32 imm)
{
psxbranch = 1;
pxAssert(imm);
armAsm->Mov(RWSCRATCH, imm);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
_psxFlushCall(FLUSH_EVERYTHING);
iPsxBranchTest(imm, imm <= psxpc);
// Block linking: emit a single B as the patch site. Initially routed
// through iopJITCompile via recBlocks.Link(); once the target block is
// compiled, recBlocks.New() rewrites this B's imm26 to branch to the
// target's fnptr directly, bypassing the dispatcher. Mirrors EE rec
// at iR5900-arm64.cpp:1064.
{
a64::SingleEmissionCheckScope guard(armAsm);
u8* patch_site = armGetCurrentCodePointer();
armAsm->b(int64_t{0}); // placeholder; recBlocks.Link will overwrite
recBlocks.Link(HWADDR(imm), patch_site);
}
}
static __fi u32 psxScaleBlockCycles()
{
return s_psxBlockCycles;
}
// Contract: leaves the new iopCycleEE value in RWSCRATCH (callers test it
// against 0 for the timeslice exit) and must not clobber x2 (the caller's
// freshly-stored psxRegs.cycle, still live for the event check).
static void iPsxAddEECycles(u32 blockCycles)
{
if (!(psxHu32(HW_ICFG) & (1 << 3))) [[likely]]
{
// PS2 mode: flat 1:8 IOP:EE ratio. Subtract cycles * 8 from iopCycleEE.
armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE));
if (blockCycles != 0xFFFFFFFF)
{
if (blockCycles * 8 < 4096)
armAsm->Sub(RWSCRATCH, RWSCRATCH, blockCycles * 8);
else
{
armAsm->Mov(a64::w1, blockCycles * 8);
armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w1);
}
}
else
{
// blockCycles in w0 (from wait loop optimization)
armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w0);
}
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE));
return;
}
// PS1 backwards-compat mode (HW_ICFG bit 3, set by the SBUS handshake on
// PS1-disc boot): exact-ratio conversion with a carried remainder, matching
// x86 iR3000A.cpp iPsxAddEECycles and the interpreter's iopBranchTest.
// Compile-time check is safe: entering PS1 mode goes through psxReset,
// which flushes the IOP rec. (AX-09)
// F = gcd(PS2CLK, PSXCLK) = 230400
const u32 cnum = 1280; // PS2CLK / F
const u32 cdenom = 147; // PSXCLK / F
if (blockCycles != 0xFFFFFFFF)
armAsm->Mov(a64::w0, blockCycles * cnum);
// else: w0 holds the runtime cycle count from the wait-loop path. x86
// does NOT scale that path by cnum — quirk kept for bit-parity.
armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEECarry));
armAsm->Add(a64::w0, a64::w0, RWSCRATCH);
armAsm->Mov(a64::w1, cdenom);
armAsm->Udiv(a64::w3, a64::w0, a64::w1); // w3 = (in+carry) / cdenom
armAsm->Msub(a64::w1, a64::w3, a64::w1, a64::w0); // w1 = remainder
armAsm->Str(a64::w1, armPsxRegMem(&psxRegs.iopCycleEECarry));
armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE));
armAsm->Sub(RWSCRATCH, RWSCRATCH, a64::w3);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE));
}
static void iPsxBranchTest(u32 newpc, u32 cpuBranch)
{
u32 blockCycles = psxScaleBlockCycles();
if (EmuConfig.Speedhacks.WaitLoop && s_nBlockFF && newpc == s_branchTo)
{
// WaitLoop fast-forward: tight busy-wait loop detected.
// Advance cycle counter to consume the remaining IOP timeslice,
// clamped to iopNextEventCycle. Matches x86 iR3000A.cpp:1179.
// new_cycle = old_cycle + (iopCycleEE + 7) / 8
// new_cycle = min(new_cycle, iopNextEventCycle)
armAsm->Ldr(a64::x2, armPsxRegMem(&psxRegs.cycle)); // x2 = old cycle
armAsm->Mov(a64::x4, a64::x2); // x4 = old cycle (saved)
// iopCycleEE is the ONLY signed quantity in this block — it can go
// negative, so the timeslice divide uses Asr (arithmetic shift). The
// cycle/iopNextEventCycle clamp below is unsigned (Csel hi); don't
// swap predicates between the two.
armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.iopCycleEE)); // w8 = iopCycleEE (s32)
armAsm->Add(RWSCRATCH, RWSCRATCH, 7);
armAsm->Asr(RWSCRATCH, RWSCRATCH, 3); // w8 = (iopCycleEE + 7) >> 3 (signed)
armAsm->Add(a64::x2, a64::x2, a64::x8); // x2 = cycle + advance
// Clamp to iopNextEventCycle
armAsm->Ldr(a64::x3, armPsxRegMem(&psxRegs.iopNextEventCycle)); // x3 = iopNextEventCycle
armAsm->Cmp(a64::x2, a64::x3);
// Both cycle counters are u32; use unsigned predicate so the clamp
// stays correct after either operand crosses 2^31. x86 CMOVA / JL
// (the upstream reference) are likewise unsigned.
armAsm->Csel(a64::x2, a64::x3, a64::x2, a64::hi); // x2 = min(x2, x3) unsigned
// Store new cycle
armAsm->Str(a64::x2, armPsxRegMem(&psxRegs.cycle));
// consumed = (new_cycle - old_cycle) << 3, in w0 for iPsxAddEECycles
armAsm->Sub(a64::x0, a64::x2, a64::x4);
armAsm->Lsl(a64::w0, a64::w0, 3);
// Subtract consumed cycles from iopCycleEE
iPsxAddEECycles(0xFFFFFFFF); // uses w0 as the cycle count
armAsm->Cmp(RWSCRATCH, 0);
armEmitCondBranch(a64::le, iopExitRecompiledCode);
// Call event test
armEmitCall((void*)iopEventTest);
if (newpc != 0xffffffff)
{
armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
armAsm->Cmp(RWSCRATCH, newpc);
armEmitCondBranch(a64::ne, iopDispatcherReg);
}
}
else
{
// Normal path: add block cycles and check events
armAsm->Ldr(a64::x2, armPsxRegMem(&psxRegs.cycle));
if (blockCycles < 4096)
armAsm->Add(a64::x2, a64::x2, blockCycles);
else
{
armAsm->Mov(a64::x3, static_cast<u64>(blockCycles));
armAsm->Add(a64::x2, a64::x2, a64::x3);
}
armAsm->Str(a64::x2, armPsxRegMem(&psxRegs.cycle));
// Subtract from iopCycleEE — exit if <= 0
iPsxAddEECycles(blockCycles);
armAsm->Cmp(RWSCRATCH, 0);
armEmitCondBranch(a64::le, iopExitRecompiledCode);
// Check if event is pending: cycle >= iopNextEventCycle
armAsm->Ldr(a64::x3, armPsxRegMem(&psxRegs.iopNextEventCycle));
armAsm->Cmp(a64::x2, a64::x3);
a64::Label noEvent;
// Unsigned predicate — see clamp note above. Both operands are u32;
// signed lt would mis-fire after either crosses 2^31.
armAsm->B(&noEvent, a64::lo);
// Event pending — call event test
armEmitCall((void*)iopEventTest);
if (newpc != 0xffffffff)
{
armAsm->Ldr(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
armAsm->Cmp(RWSCRATCH, newpc);
armEmitCondBranch(a64::ne, iopDispatcherReg);
}
armAsm->Bind(&noEvent);
}
}
// =====================================================================================================
// Delay slot swap optimization
// =====================================================================================================
// Hoist the branch's delay-slot instruction ahead of the branch test when it
// provably doesn't read/write any of the branch's source/dest registers.
// Skips the psxSaveBranchState/recompile/psxLoadBranchState/recompile-again
// ceremony that the branch handler otherwise pays. Mirrors x86 iR3000A.cpp:439
// opcode-for-opcode.
bool psxTrySwapDelaySlot(u32 rs, u32 rt, u32 rd)
{
if (s_recompilingDelaySlot)
return false;
const u32 opcode_encoded = iopMemRead32(psxpc);
if (opcode_encoded == 0)
{
psxRecompileNextInstruction(true, true);
return true;
}
const u32 opcode_rs = ((opcode_encoded >> 21) & 0x1F);
const u32 opcode_rt = ((opcode_encoded >> 16) & 0x1F);
const u32 opcode_rd = ((opcode_encoded >> 11) & 0x1F);
switch (opcode_encoded >> 26)
{
case 8: // ADDI
case 9: // ADDIU
case 10: // SLTI
case 11: // SLTIU
case 12: // ANDI
case 13: // ORI
case 14: // XORI
case 15: // LUI
case 32: // LB
case 33: // LH
case 34: // LWL
case 35: // LW
case 36: // LBU
case 37: // LHU
case 38: // LWR
case 39: // LWU
case 40: // SB
case 41: // SH
case 42: // SWL
case 43: // SW
case 46: // SWR
{
if ((rs != 0 && rs == opcode_rt) || (rt != 0 && rt == opcode_rt) || (rd != 0 && (rd == opcode_rs || rd == opcode_rt)))
return false;
}
break;
case 50: // LWC2
case 58: // SWC2
break;
case 0: // SPECIAL
{
switch (opcode_encoded & 0x3F)
{
case 0: // SLL
case 2: // SRL
case 3: // SRA
case 4: // SLLV
case 6: // SRLV
case 7: // SRAV
case 32: // ADD
case 33: // ADDU
case 34: // SUB
case 35: // SUBU
case 36: // AND
case 37: // OR
case 38: // XOR
case 39: // NOR
case 42: // SLT
case 43: // SLTU
{
if ((rs != 0 && rs == opcode_rd) || (rt != 0 && rt == opcode_rd) || (rd != 0 && (rd == opcode_rs || rd == opcode_rt)))
return false;
}
break;
case 15: // SYNC
case 24: // MULT
case 25: // MULTU
case 26: // DIV
case 27: // DIVU
break;
default:
return false;
}
}
break;
case 16: // COP0
case 17: // COP1
case 18: // COP2
case 19: // COP3
{
switch ((opcode_encoded >> 21) & 0x1F)
{
case 0: // MFC
case 2: // CFC
{
if ((rs != 0 && rs == opcode_rt) || (rt != 0 && rt == opcode_rt) || (rd != 0 && rd == opcode_rt))
return false;
}
break;
case 4: // MTC
case 6: // CTC
break;
default:
// GTE (COP2) ops are safe.
if ((opcode_encoded >> 26) != 18)
return false;
break;
}
}
break;
default:
return false;
}
psxRecompileNextInstruction(true, true);
return true;
}
// =====================================================================================================
// Instruction recompilation
// =====================================================================================================
// delayslot = true when called from a branch handler to compile its delay
// slot. swapped_delayslot = true when called via psxTrySwapDelaySlot —
// the delay slot is being hoisted ahead of the branch test, so we must
// snapshot psxRegs.code + g_pCurInstInfo so the enclosing branch handler's
// codegen continues against its own instruction, and suppress the trailing
// _clearNeededArm64GPRregs because the branch hasn't emitted its flush yet.
void psxRecompileNextInstruction(bool delayslot, bool swapped_delayslot)
{
// IOP recompiler breakpoint support is not implemented on this target.
const u32 old_code = psxRegs.code;
EEINST* const old_inst_info = g_pCurInstInfo;
s_recompilingDelaySlot = delayslot;
// Read the instruction
psxRegs.code = iopMemRead32(psxpc);
s_psxBlockCycles++;
psxpc += 4;
g_pCurInstInfo++;
g_iopCyclePenalty = 0;
// Dispatch to the appropriate recompiler function
rpsxBSC[psxRegs.code >> 26]();
s_psxBlockCycles += g_iopCyclePenalty;
if (!swapped_delayslot)
{
_clearNeededArm64GPRregs();
}
else
{
psxRegs.code = old_code;
g_pCurInstInfo = old_inst_info;
}
s_recompilingDelaySlot = false;
}
// SYSCALL and BREAK
void rpsxSYSCALL()
{
armAsm->Mov(RWSCRATCH, psxRegs.code);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code));
armAsm->Mov(RWSCRATCH, psxpc - 4);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
_psxFlushCall(FLUSH_NODESTROY);
armAsm->Mov(RWARG1, 0x20);
armAsm->Mov(RWARG2, psxbranch == 1 ? 1 : 0);
armEmitCall((void*)psxException);
// psxException unconditionally rewrites pc to the exception vector
// (R3000A.cpp: BEV ? 0xbfc00180 : 0x80000080), so always update cycles
// and re-dispatch. (x86 carries a "did pc change?" fall-through check
// here; it can only trigger for a SYSCALL sitting AT the vector, and
// falling through into the block tail would be wrong even then.)
armAsm->Ldr(a64::x0, armPsxRegMem(&psxRegs.cycle));
if (psxScaleBlockCycles() < 4096)
armAsm->Add(a64::x0, a64::x0, psxScaleBlockCycles());
else
{
armAsm->Mov(a64::x1, static_cast<u64>(psxScaleBlockCycles()));
armAsm->Add(a64::x0, a64::x0, a64::x1);
}
armAsm->Str(a64::x0, armPsxRegMem(&psxRegs.cycle));
iPsxAddEECycles(psxScaleBlockCycles());
armEmitJmp(iopDispatcherReg);
}
void rpsxBREAK()
{
armAsm->Mov(RWSCRATCH, psxRegs.code);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.code));
armAsm->Mov(RWSCRATCH, psxpc - 4);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
_psxFlushCall(FLUSH_NODESTROY);
armAsm->Mov(RWARG1, 0x24);
armAsm->Mov(RWARG2, psxbranch == 1 ? 1 : 0);
armEmitCall((void*)psxException);
// See rpsxSYSCALL — pc always changed, dispatch unconditionally.
armAsm->Ldr(a64::x0, armPsxRegMem(&psxRegs.cycle));
if (psxScaleBlockCycles() < 4096)
armAsm->Add(a64::x0, a64::x0, psxScaleBlockCycles());
else
{
armAsm->Mov(a64::x1, static_cast<u64>(psxScaleBlockCycles()));
armAsm->Add(a64::x0, a64::x0, a64::x1);
}
armAsm->Str(a64::x0, armPsxRegMem(&psxRegs.cycle));
iPsxAddEECycles(psxScaleBlockCycles());
armEmitJmp(iopDispatcherReg);
}
// =====================================================================================================
// Memory management and block clearing
// =====================================================================================================
// recLUT_SetPage is defined in BaseblockEx.h (architecture-independent)
// Block-coverage counters for the recClearIOP fast path. g_iopCodeCov[g]
// counts live recBlocks entries whose [startpc, startpc+size*4) span overlaps
// 256-byte granule g of the HWADDR window. Every qualifying IOP RAM store
// funnels through psxRecClearMem (SMC detection), and in practice ~100% of
// them hit addresses no compiled block covers — a zero counter proves that in
// O(1) and skips the binary search over recBlocks entirely. Maintained at the
// three block-lifecycle points in this file (size-finalize, recompile reuse
// of an existing entry, psxRecClearMem removal) and zeroed on recResetIOP.
// rpsxStoreGeneric additionally probes it from the JIT RAM-store fast-path
// stubs (declared in iR3000A-arm64.h alongside kIopCovShift/kIopCovSpan).
//
// Counting the whole span rather than just block heads is what keeps
// mid-block stores correct, and counts (not bits) stay exact when overlapping
// blocks are removed independently.
//
// ROM-resident blocks (startpc >= kIopCovSpan, e.g. BIOS at 0x1fcxxxxx) are
// never counted: stores can't target ROM, so no clear ever queries them.
u16 g_iopCodeCov[kIopCovGranules];
static __fi void iopCovAdjust(u32 startpc_hw, u32 bytes, int delta)
{
if (startpc_hw >= kIopCovSpan || bytes == 0)
return;
const u32 gfirst = startpc_hw >> kIopCovShift;
// A block whose span runs off the end of the window (top-of-RAM start,
// long block) would otherwise index past the array. Those tail granules
// are unreachable by any store — the probe index is masked into the
// window — so clamping only drops coverage nothing can query.
const u32 glast = std::min((startpc_hw + bytes - 1) >> kIopCovShift, kIopCovGranules - 1);
for (u32 g = gfirst; g <= glast; g++)
{
pxAssert(delta > 0 ? g_iopCodeCov[g] != 0xFFFF : g_iopCodeCov[g] != 0);
g_iopCodeCov[g] = static_cast<u16>(static_cast<int>(g_iopCodeCov[g]) + delta);
}
}
static __fi u32 psxRecClearMem(u32 pc)
{
// O(1) reject: if no live block's span overlaps this granule, nothing can
// need clearing — skip ahead to the next granule boundary. This is the
// overwhelmingly common case (IOP stores to data addresses).
const u32 hw = HWADDR(pc);
if (hw < kIopCovSpan && g_iopCodeCov[hw >> kIopCovShift] == 0)
return (((hw >> kIopCovShift) + 1) << kIopCovShift) - hw;
// Look up the containing block via recBlocks instead of testing the
// per-instruction LUT fnptr. The LUT only patches block-head slots away
// from iopJITCompile; mid-block words still hold the trampoline, so a
// fnptr-based early-exit would leak mid-block writes through to stale
// compiled code.
int blockidx = recBlocks.Index(HWADDR(pc));
if (blockidx == -1)
return 4;
u32 lowerextent = pc, upperextent = pc + 4;
// Walk down for straddlers: blocks starting below the merged range whose
// bodies reach into it. `scanidx` is the cursor; `blockidx` only follows it
// down onto blocks we actually absorb, so a skipped survivor never becomes
// the start of the removal range below.
int scanidx = blockidx - 1;
while (BASEBLOCKEX* pexblock = recBlocks[scanidx])
{
if (pexblock->startpc + pexblock->size * 4 <= HWADDR(lowerextent))
{
// Not overlapping — but recBlocks is ordered by startpc, NOT by end
// address, so a block further down can still be long enough to reach
// the merged range; this one is no proof the rest miss too. Skip it
// and carry on until even the longest block compiled since the last
// reset could not span the gap. (Upstream x86 breaks here —
// iR3000A.cpp — and silently leaves the straddler below compiled.)
if (pexblock->startpc + s_maxBlockBytes <= HWADDR(lowerextent))
break;
scanidx--;
continue;
}
lowerextent = std::min(lowerextent, pexblock->startpc);
blockidx = scanidx;
scanidx--;
}
while (BASEBLOCKEX* pexblock = recBlocks[blockidx])
{
if (pexblock->startpc >= HWADDR(upperextent))
break;
lowerextent = std::min(lowerextent, pexblock->startpc);
upperextent = std::max(upperextent, pexblock->startpc + pexblock->size * 4);
iopCovAdjust(pexblock->startpc, pexblock->size * 4, -1);
recBlocks.Remove(blockidx, blockidx);
}
// Clear all blocks in the range
for (u32 addr = lowerextent; addr < upperextent; addr += 4)
{
BASEBLOCK* p = PSX_GETBLOCK(addr);
p->SetFnptr((uptr)iopJITCompile);
}
return upperextent - pc;
}
static void recClearIOP(u32 Addr, u32 Size)
{
// IOP SMC detection: every qualifying store funnels here via psxCpu->Clear
// (IopMem.cpp, DMA/SIF/BIOS-HLE, and the out-of-line store stubs'
// iopStoreClearHit). The only code writes underneath are
// Arm64BaseBlocks::Remove()'s entry stubs, and those open their own
// per-site W^X scope (armPatchCodeWord) — SetFnptr targets are heap data.
// The arena-wide Begin/EndCodeWrite this used to hold was both wasteful
// (a whole-arena mprotect pair per covered store in iOS Legacy mode) and
// hazardous: Legacy range windows bypass the refcount, so the paired
// EndCodeWrite RX-flipped any emit window open on another thread.
u32 end = Addr + Size * 4;
for (u32 i = Addr; i < end; i += PSXREC_CLEARM(i))
;
}
// Called from the JIT RAM-store fast-path stubs when the stored-to granule
// has live block coverage. Mirrors what iopMemWrite* would have done: mask to
// 0x1fffffff at entry (so the g_psxMaxRecMem filter in PSXREC_CLEARM sees the
// masked value), then the psxCpu->Clear(mem & ~3, 1) single-word clear, which
// routes through recClearIOP for the BeginCodeWrite scope around the
// entry-point patching.
void iopStoreClearHit(u32 addr)
{
recClearIOP((addr & 0x1fffffff) & ~3u, 1);
}
static void iopClearRecLUT(BASEBLOCK* base, int count)
{
for (int i = 0; i < count / 4; i++)
base[i].SetFnptr((uptr)iopJITCompile);
}
// =====================================================================================================
// Reserve / Reset / Shutdown / Execute
// =====================================================================================================
static void recReserveRAM()
{
recLutEntries =
((Ps2MemSize::ExposedIopRam + Ps2MemSize::Rom + Ps2MemSize::Rom1 + Ps2MemSize::Rom2) / 4);
if (recLutReserve.size() != recLutEntries)
recLutReserve.resize(recLutEntries);
recLutUnmapped.resize(_64kb / 4);
BASEBLOCK* curpos = recLutReserve.data();
recRAM = curpos;
curpos += (Ps2MemSize::ExposedIopRam / 4);
recROM = curpos;
curpos += (Ps2MemSize::Rom / 4);
recROM1 = curpos;
curpos += (Ps2MemSize::Rom1 / 4);
recROM2 = curpos;
curpos += (Ps2MemSize::Rom2 / 4);
}
static void recReserve()
{
Console.WriteLn(Color_Green, "IOP: ARM64 Recompiler reserved.");
// Constant pool at the region tail; code region ends below it minus the
// 64KB single-compile overhang slop (same layout hazard as the EE rec —
// see recReserve in iR5900-arm64.cpp).
const u32 poolSize = 65536;
u8* poolBase = SysMemory::GetIOPRecEnd() - poolSize;
s_iopConstantPool.Init(poolBase, poolSize);
recPtr = SysMemory::GetIOPRec();
recPtrEnd = poolBase - _64kb;
pxAssertRel(recPtrEnd > recPtr && recPtrEnd + _64kb <= poolBase,
"IOP rec code region must not reach the constant pool");
recReserveRAM();
pxAssertRel(!s_pInstCache, "InstCache not allocated");
s_nInstCacheSize = 128;
s_pInstCache = (EEINST*)malloc(sizeof(EEINST) * s_nInstCacheSize);
if (!s_pInstCache)
pxFailRel("Failed to allocate R3000A InstCache array.");
}
void recResetIOP()
{
Console.WriteLn(Color_Green, "iR3000A-ARM64 Recompiler reset.");
if (CHECK_EXTRAMEM != extraRam)
{
recReserveRAM();
extraRam = !extraRam;
}
armSetAsmPtr(SysMemory::GetIOPRec(), SysMemory::GetIOPRecEnd() - SysMemory::GetIOPRec(), &s_iopConstantPool);
armStartBlock();
_DynGen_Dispatchers();
recPtr = armEndBlock();
iopClearRecLUT(reinterpret_cast<BASEBLOCK*>(recLutReserve.data()),
Ps2MemSize::ExposedIopRam + Ps2MemSize::Rom + Ps2MemSize::Rom1 + Ps2MemSize::Rom2);
BASEBLOCK* unmapped = recLutUnmapped.data();
for (int i = 0; i < 0x10000; i++)
recLUT_SetPage(psxRecLUT, psxhwLUT, unmapped, i, 0, 0);
for (int i = 0; i < _64kb / 4; i++)
unmapped[i].SetFnptr((uptr)iopUnmappedRecLUTPage);
// Map IOP RAM (with mirrors)
for (int i = 0; i < 0x80; i++)
{
u32 mask = (Ps2MemSize::ExposedIopRam / _64kb) - 1;
recLUT_SetPage(psxRecLUT, psxhwLUT, recRAM, 0x0000, i, i & mask);
recLUT_SetPage(psxRecLUT, psxhwLUT, recRAM, 0x8000, i, i & mask);
recLUT_SetPage(psxRecLUT, psxhwLUT, recRAM, 0xa000, i, i & mask);
}
// Map ROM
for (int i = 0x1fc0; i < 0x2000; i++)
{
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM, 0x0000, i, i - 0x1fc0);
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM, 0x8000, i, i - 0x1fc0);
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM, 0xa000, i, i - 0x1fc0);
}
for (int i = 0x1e00; i < 0x1e40; i++)
{
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM1, 0x0000, i, i - 0x1e00);
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM1, 0x8000, i, i - 0x1e00);
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM1, 0xa000, i, i - 0x1e00);
}
for (int i = 0x1e40; i < 0x1e48; i++)
{
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM2, 0x0000, i, i - 0x1e40);
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM2, 0x8000, i, i - 0x1e40);
recLUT_SetPage(psxRecLUT, psxhwLUT, recROM2, 0xa000, i, i - 0x1e40);
}
if (s_pInstCache)
memset(s_pInstCache, 0, sizeof(EEINST) * s_nInstCacheSize);
recBlocks.Reset();
memset(g_iopCodeCov, 0, sizeof(g_iopCodeCov));
g_psxMaxRecMem = 0;
s_maxBlockBytes = 0;
psxbranch = 0;
}
static void recShutdown()
{
s_iopConstantPool.Destroy();
recLutReserve.deallocate();
safe_free(s_pInstCache);
s_nInstCacheSize = 0;
recPtr = nullptr;
recPtrEnd = nullptr;
}
static __noinline s32 recExecuteBlock(s32 eeCycles)
{
psxRegs.iopBreak = 0;
psxRegs.iopCycleEE = eeCycles;
((void (*)())iopEnterRecompiledCode)();
return psxRegs.iopBreak + psxRegs.iopCycleEE;
}
// =====================================================================================================
// Main Recompilation Loop
// =====================================================================================================
static void iopRecRecompile(const u32 startpc)
{
u32 i;
u32 link_next_block = 0;
// BIOS hacks
if (startpc == 0x890)
{
DevCon.WriteLn(Color_Gray, "R3000 Debugger: Branch to 0x890 (SYSMEM). Clearing modules.");
R3000SymbolGuardian.ClearIrxModules();
}
if (startpc == 0x1630 && EmuConfig.CurrentIRX.length() > 3)
{
if (iopMemRead32(0x20018) == 0x1F)
iopMemWrite32(0x20094, 0xbffc0000);
}
if (startpc == 0xbfc4a000)
psxRegs.GPR.n.a0 = Ps2MemSize::ExposedIopRam >> 20;
// PS1 mode can drive the IOP into garbage paths (e.g. JR through a
// register that ended up 0). Real hardware raises an Address Error on
// the instruction fetch at PC=0 and the BIOS handler at 0x80000080
// takes over; mirror that instead of hard-asserting (Devel/Debug keep
// pxAssert live, turning this into an abort). psxException moves
// psxRegs.pc to the vector; returning without compiling lets the
// JITCompile stub's dispatcher re-read pc and continue there. AdEL =
// ExcCode 4 => code 0x10 in psxException's Cause<<2 convention.
// Mirrors ARMSX2 3c8332bfe. (AX-11)
if (startpc == 0)
{
Console.Warning("IOP: fetch from PC=0 — raising AdEL instead of compiling");
psxException(0x10, 0);
return;
}
pxAssert(startpc);
// Reset code buffer if full
if (recPtr >= recPtrEnd)
recResetIOP();
armSetAsmPtr(recPtr, recPtrEnd - recPtr + _64kb, &s_iopConstantPool);
armStartBlock();
s_pCurBlock = PSX_GETBLOCK(startpc);
pxAssert(s_pCurBlock->GetFnptr() == (uptr)iopJITCompile);
// armStartBlock() aligned armAsmPtr to 16 bytes, so the actual block
// code starts at armGetCurrentCodePointer(), not at recPtr. Block
// linking branches to BASEBLOCKEX::fnptr, so it must be the aligned
// address — using recPtr instead lands the branch on padding bytes
// and triggers SIGILL. Same fix as EE rec at iR5900-arm64.cpp:1751.
const uptr block_fnptr = (uptr)armGetCurrentCodePointer();
// New() re-binds an existing entry in place rather than replacing it, and
// the entry keeps its old size until the size-finalize at the end of this
// function overwrites it. Retire that stale span's coverage first — the
// old and new spans can differ. (A never-finalized entry has size 0 from
// insert()'s memset, which iopCovAdjust ignores.)
if (BASEBLOCKEX* prev = recBlocks.Get(HWADDR(startpc)); prev && prev->startpc == HWADDR(startpc))
iopCovAdjust(prev->startpc, prev->size * 4, -1);
// See the EE rec's equivalent: New() creates or re-binds, and publishes
// the owner for the link sites this block is about to register.
s_pCurBlockEx = recBlocks.New(HWADDR(startpc), block_fnptr);
psxbranch = 0;
s_pCurBlock->SetFnptr(block_fnptr);
s_psxBlockCycles = 0;
// Reset recomp state
psxpc = startpc;
g_psxHasConstReg = g_psxFlushedConstReg = 1;
_initArm64GPRregs();
// BIOS call interception
if ((psxHu32(HW_ICFG) & 8) && (HWADDR(startpc) == 0xa0 || HWADDR(startpc) == 0xb0 || HWADDR(startpc) == 0xc0))
{
armEmitCall((void*)psxBiosCall);
// If psxBiosCall returns non-zero, skip to dispatcher. CBNZ folds
// the Tst + B.ne pair (AX-12, after ARMSX2 1c1d0b880).
armEmitCbnz(RWRET, iopDispatcherReg);
}
// Scan for block boundary
i = startpc;
s_nEndBlock = 0xffffffff;
s_branchTo = -1;
while (1)
{
BASEBLOCK* pblock = PSX_GETBLOCK(i);
if (i != startpc && pblock->GetFnptr() != (uptr)iopJITCompile)
{
link_next_block = 1;
s_nEndBlock = i;
break;
}
psxRegs.code = iopMemRead32(i);
switch (psxRegs.code >> 26)
{
case 0: // special
if (_Funct_ == 8 || _Funct_ == 9) // JR, JALR
{
s_nEndBlock = i + 8;
goto StartRecomp;
}
break;
case 1: // regimm
if (_Rt_ == 0 || _Rt_ == 1 || _Rt_ == 16 || _Rt_ == 17)
{
s_branchTo = _Imm_ * 4 + i + 4;
if (s_branchTo > startpc && s_branchTo < i)
s_nEndBlock = s_branchTo;
else
s_nEndBlock = i + 8;
goto StartRecomp;
}
break;
case 2: // J
case 3: // JAL
s_branchTo = (_InstrucTarget_ << 2) | ((i + 4) & 0xf0000000);
s_nEndBlock = i + 8;
goto StartRecomp;
case 4: case 5: case 6: case 7: // BEQ, BNE, BLEZ, BGTZ
s_branchTo = _Imm_ * 4 + i + 4;
if (s_branchTo > startpc && s_branchTo < i)
s_nEndBlock = s_branchTo;
else
s_nEndBlock = i + 8;
goto StartRecomp;
}
i += 4;
}
StartRecomp:
// Detect infinite loops (NOP loops)
s_nBlockFF = false;
if (s_branchTo == startpc)
{
s_nBlockFF = true;
for (i = startpc; i < s_nEndBlock; i += 4)
{
if (i != s_nEndBlock - 8)
{
if (iopMemRead32(i) != 0)
{
s_nBlockFF = false;
break;
}
}
}
}
// Instruction liveness analysis (backward pass)
{
EEINST* pcur;
if (s_nInstCacheSize < (s_nEndBlock - startpc) / 4 + 1)
{
free(s_pInstCache);
s_nInstCacheSize = (s_nEndBlock - startpc) / 4 + 10;
s_pInstCache = (EEINST*)malloc(sizeof(EEINST) * s_nInstCacheSize);
pxAssert(s_pInstCache != NULL);
}
pcur = s_pInstCache + (s_nEndBlock - startpc) / 4;
_recClearInst(pcur);
pcur->info = 0;
for (i = s_nEndBlock; i > startpc; i -= 4)
{
psxRegs.code = iopMemRead32(i - 4);
pcur[-1] = pcur[0];
rpsxpropBSC(pcur - 1, pcur);
pcur--;
}
}
// Compile instructions (forward pass)
g_pCurInstInfo = s_pInstCache;
while (!psxbranch && psxpc < s_nEndBlock)
{
psxRecompileNextInstruction(false, false);
}
pxAssert((psxpc - startpc) >> 2 <= 0xffff);
s_pCurBlockEx->size = (psxpc - startpc) >> 2;
iopCovAdjust(s_pCurBlockEx->startpc, s_pCurBlockEx->size * 4, +1);
// High-water mark of any compiled block's guest extent, for the straddler
// walk's scan-back bound in psxRecClearMem. Reset with the block array.
s_maxBlockBytes = std::max(s_maxBlockBytes, psxpc - startpc);
if (!(psxpc & 0x10000000))
g_psxMaxRecMem = std::max((psxpc & ~0xa0000000), g_psxMaxRecMem);
if (psxbranch == 2)
{
_psxFlushCall(FLUSH_EVERYTHING);
iPsxBranchTest(0xffffffff, 1);
armEmitJmp(iopDispatcherReg);
}
else
{
if (psxbranch)
pxAssert(!link_next_block);
else
{
// Non-branch block end: add cycles.
//
// The flush MUST precede the cycle accounting below: the accounting
// uses x0/x1 as scratch, but the register allocator can still hold
// an unflushed write-back in x0 from the block's last instruction
// (e.g. SRA writing rd via _allocArm64GPR MODE_WRITE). Without this
// hoist, the deferred flush at the link-next-block site stores the
// cycle counter's low bits into the dirty guest register's psxRegs
// slot.
_psxFlushCall(FLUSH_EVERYTHING);
armAsm->Ldr(a64::x0, armPsxRegMem(&psxRegs.cycle));
u32 scaledCycles = psxScaleBlockCycles();
if (scaledCycles < 4096)
armAsm->Add(a64::x0, a64::x0, scaledCycles);
else
{
armAsm->Mov(a64::x1, static_cast<u64>(scaledCycles));
armAsm->Add(a64::x0, a64::x0, a64::x1);
}
armAsm->Str(a64::x0, armPsxRegMem(&psxRegs.cycle));
iPsxAddEECycles(psxScaleBlockCycles());
}
if (link_next_block || !psxbranch)
{
pxAssert(psxpc == s_nEndBlock);
_psxFlushCall(FLUSH_EVERYTHING);
armAsm->Mov(RWSCRATCH, psxpc);
armAsm->Str(RWSCRATCH, armPsxRegMem(&psxRegs.pc));
// Block linking — see psxSetBranchImm.
{
a64::SingleEmissionCheckScope guard(armAsm);
u8* patch_site = armGetCurrentCodePointer();
armAsm->b(int64_t{0}); // placeholder; recBlocks.Link will overwrite
recBlocks.Link(HWADDR(psxpc), patch_site);
}
psxbranch = 3;
}
}
pxAssert(armGetCurrentCodePointer() < SysMemory::GetIOPRecEnd());
s_pCurBlockEx->x86size = static_cast<u32>(armGetCurrentCodePointer() - recPtr);
Perf::iop.RegisterPC((void*)s_pCurBlockEx->fnptr, s_pCurBlockEx->x86size, s_pCurBlockEx->startpc);
recPtr = armEndBlock();
pxAssert((g_psxHasConstReg & g_psxFlushedConstReg) == g_psxHasConstReg);
s_pCurBlock = NULL;
s_pCurBlockEx = NULL;
}
// =====================================================================================================
// R3000Acpu struct — the public interface
// =====================================================================================================
R3000Acpu psxRec = {
recReserve,
recResetIOP,
recExecuteBlock,
recClearIOP,
recShutdown,
};