arm64: memory placement, VTLB codegen, and TLB-miss handling

SharedMemoryMappingArea::Create() gains a fixed_base_hint so AllocateMemoryMap can
pin the JIT arena at a constant VA (kArenaBase=4GB, 256MB-stride fallback) on arm64;
the VTLB fastmem backpatch thunk (RecStubs), ArmAddressRecorder relocation hooks
(AsmHelpers), and the cpuTlbMiss rec-vs-interp PC split (R5900) round out the arm64
memory layer. Windows arm64 is a no-op stub.

Co-Authored-By: Ryan Walklin <ryan@testtoast.com>
Co-Authored-By: Brian Degenhardt <bmd@bmdhacks.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Brian Degenhardt
2026-06-20 20:27:55 -07:00
co-authored by Ryan Walklin Claude Opus 4.8
parent e0b85251aa
commit 6799ab0d2d
11 changed files with 621 additions and 42 deletions
+6 -1
View File
@@ -139,7 +139,12 @@ namespace PageFaultHandler
class SharedMemoryMappingArea
{
public:
static std::unique_ptr<SharedMemoryMappingArea> Create(size_t size, bool jit = false);
// fixed_base_hint: when non-zero, the area is placed at that VA (256MB-stride
// fallback slots, then kernel placement) so the reserved region — and any JIT
// code mapped inside it — lands at the same address every run, which a caller
// can rely on for address-stable code caching. Zero = kernel-chosen placement
// (the default).
static std::unique_ptr<SharedMemoryMappingArea> Create(size_t size, bool jit = false, uptr fixed_base_hint = 0);
~SharedMemoryMappingArea();
+29 -2
View File
@@ -153,7 +153,7 @@ SharedMemoryMappingArea::~SharedMemoryMappingArea()
}
std::unique_ptr<SharedMemoryMappingArea> SharedMemoryMappingArea::Create(size_t size, bool jit)
std::unique_ptr<SharedMemoryMappingArea> SharedMemoryMappingArea::Create(size_t size, bool jit, uptr fixed_base_hint)
{
pxAssertRel(Common::IsAlignedPow2(size, __pagesize), "Size is page aligned");
@@ -162,7 +162,34 @@ std::unique_ptr<SharedMemoryMappingArea> SharedMemoryMappingArea::Create(size_t
if (jit)
flags |= MAP_JIT;
#endif
void* alloc = mmap(nullptr, size, PROT_NONE, flags, -1, 0);
void* alloc = MAP_FAILED;
// Deterministic VA placement for the on-disk VU program cache: when a
// fixed base hint is given, try it (plus 256MB-stride fallback slots) with a
// hint-and-verify mmap (no MAP_FIXED, so we never clobber an existing
// mapping). If none of the slots land exactly, fall through to kernel
// placement — the program cache records the arena base and simply misses on
// a mismatch, so this trades determinism for booting, never correctness.
if (fixed_base_hint != 0)
{
for (int slot = 0; slot <= 10; slot++)
{
void* const want = reinterpret_cast<void*>(fixed_base_hint + (static_cast<uptr>(slot) << 28));
void* const got = mmap(want, size, PROT_NONE, flags, -1, 0);
if (got == MAP_FAILED)
continue;
if (got == want)
{
alloc = got;
break;
}
munmap(got, size);
}
}
if (alloc == MAP_FAILED)
alloc = mmap(nullptr, size, PROT_NONE, flags, -1, 0);
if (alloc == MAP_FAILED)
return nullptr;
+6 -1
View File
@@ -143,10 +143,15 @@ SharedMemoryMappingArea::PlaceholderMap::iterator SharedMemoryMappingArea::FindP
return m_placeholder_ranges.end();
}
std::unique_ptr<SharedMemoryMappingArea> SharedMemoryMappingArea::Create(size_t size, bool jit)
std::unique_ptr<SharedMemoryMappingArea> SharedMemoryMappingArea::Create(size_t size, bool jit, uptr fixed_base_hint)
{
pxAssertRel(Common::IsAlignedPow2(size, __pagesize), "Size is page aligned");
// Deterministic fixed-base placement is not implemented on Windows; kernel-chosen
// placement is used and the program cache simply incurs a miss if the arena base
// differs across runs.
(void)fixed_base_hint;
void* alloc = VirtualAlloc2(GetCurrentProcess(), nullptr, size, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0);
if (!alloc)
return nullptr;
+35 -2
View File
@@ -95,7 +95,22 @@ bool SysMemory::AllocateMemoryMap()
return false;
}
if (!(s_memory_mapping_area = SharedMemoryMappingArea::Create(HostMemoryMap::MainSize + HostMemoryMap::CodeSize, true)))
// Constant-VA arena for the on-disk VU program cache: on arm64 the
// whole data+code reservation must sit at the same VA every run so cached
// JIT code (which lives in the code half, at BasePointer() + MainSize)
// reloads without repatching its baked addresses. 4GB clears the ASLR brk
// window (non-PIE image at 0x400000 + brk randomization < 2GB) and sits far
// below the mmap_base / PIE-load regions, so the slot-0 candidate succeeds
// deterministically; Create() walks 256MB-stride fallback slots and finally
// kernel placement (program-cache misses, never corruption). Other arches
// pass 0 and take kernel-chosen placement.
#if defined(__aarch64__) || defined(_M_ARM64)
constexpr uptr kArenaBase = 0x100000000ull; // 4GB
#else
constexpr uptr kArenaBase = 0;
#endif
if (!(s_memory_mapping_area = SharedMemoryMappingArea::Create(HostMemoryMap::MainSize + HostMemoryMap::CodeSize, true, kArenaBase)))
{
Host::ReportErrorAsync("Error", "Failed to map main memory.");
ReleaseMemoryMap();
@@ -172,11 +187,19 @@ void SysMemory::ReleaseMemoryMap()
}
}
void SysMemory::ReserveMemory()
{
// Claim the host memory map (and the arm64 constant-VA arena) up front, so
// the fixed-base placement isn't lost to an intervening heap/mmap. Idempotent.
if (!s_data_memory_file_handle)
AllocateMemoryMap();
}
bool SysMemory::Allocate()
{
DevCon.WriteLn(Color_StrongBlue, "Allocating host memory for virtual systems...");
if (!AllocateMemoryMap())
if (!s_data_memory_file_handle && !AllocateMemoryMap())
return false;
memAllocate();
@@ -411,6 +434,16 @@ void memMapPhy()
// High memory, uninstalled on the configuration we emulate
vtlb_MapHandler(null_handler, Ps2MemSize::ExposedRam, 0x10000000 - Ps2MemSize::ExposedRam);
// Physical RAM mirrors used by BIOS InitRDRAM for RDRAM device configuration.
// On real PS2 hardware:
// 0x20000000-0x21FFFFFF = uncached mirror of main RAM
// 0x30000000-0x31FFFFFF = uncached & accelerated mirror of main RAM
// These mirrors must be present in the physical map; without them, BIOS writes
// to RDRAM device registers hit UnmappedPhyHandler (bus error).
// Requires VTLB_PMAP_SZ >= 1GB to cover these addresses.
vtlb_MapBlock(eeMem->Main, 0x20000000, Ps2MemSize::ExposedRam);
vtlb_MapBlock(eeMem->Main, 0x30000000, Ps2MemSize::ExposedRam);
// Various ROMs (all read-only)
vtlb_MapBlock(eeMem->ROM, 0x1fc00000, Ps2MemSize::Rom);
vtlb_MapBlock(eeMem->ROM1, 0x1e000000, Ps2MemSize::Rom1);
+6
View File
@@ -98,6 +98,12 @@ namespace HostMemoryMap
namespace SysMemory
{
/// Reserve the host memory map (and, on arm64, the constant-VA arena) early,
/// before other allocations could squat on the fixed base. Idempotent — a
/// later Allocate() reuses the same reservation. Used by headless runners and
/// SDL frontends that need the deterministic arena claimed before any
/// heap/mmap could squat on the fixed base.
void ReserveMemory();
bool Allocate();
void Reset();
void Release();
+39 -6
View File
@@ -166,8 +166,17 @@ __ri void cpuException(u32 code, u32 bd)
void cpuTlbMiss(u32 addr, u32 bd, u32 excode)
{
// Avoid too much spamming on the interpreter
if (Cpu != &intCpu || IsDebugBuild) {
// Avoid too much spamming. On x86 the recompiler uses CancelInstruction and
// seldom reaches here, so logging the rec path is cheap; on arm64 every rec
// TLB miss is funneled through this function (see s_recTlbMissOccurred below),
// so logging the rec path would spam Release on every miss. Gate the arm64
// rec path on debug builds, matching the original "don't spam on interp" intent.
#ifdef __aarch64__
const bool log_tlb_miss = IsDebugBuild;
#else
const bool log_tlb_miss = (Cpu != &intCpu) || IsDebugBuild;
#endif
if (log_tlb_miss) {
Console.Error("cpuTlbMiss pc:%x, cycl:%llx, addr: %x, status=%x, code=%x",
cpuRegs.pc, cpuRegs.cycle, addr, cpuRegs.CP0.n.Status.val, excode);
}
@@ -177,8 +186,30 @@ void cpuTlbMiss(u32 addr, u32 bd, u32 excode)
cpuRegs.CP0.n.Context |= (addr >> 9) & 0x007FFFF0;
cpuRegs.CP0.n.EntryHi = (addr & 0xFFFFE000) | (cpuRegs.CP0.n.EntryHi & 0x1FFF);
cpuRegs.pc -= 4;
// The interpreter advances cpuRegs.pc past the current instruction
// before executing it, so pc -= 4 gets back to the faulting instruction.
// The recompiler's FLUSH_PC writes the current instruction's PC
// (not advanced), so we must NOT subtract 4 in that case.
const bool isRec = (Cpu != &intCpu);
if (!isRec)
cpuRegs.pc -= 4;
cpuException(excode, bd);
// For the ARM64 recompiler: set a flag so the JIT block can detect the
// exception after the interpreter call returns and dispatch to the
// exception vector. We don't use CancelInstruction (longjmp) because
// that disrupts cycle counting and timing. The x86 recompiler uses
// CancelInstruction instead.
#ifdef __aarch64__
if (isRec)
{
// Defined in the arm64 EE recompiler TU; the dispatcher reads it after
// this call to route the block to the exception vector.
extern u32 s_recTlbMissOccurred;
s_recTlbMissOccurred = 1;
}
#endif
}
void cpuTlbMissR(u32 addr, u32 bd) {
@@ -394,7 +425,6 @@ __fi void _cpuEventTest_Shared()
// Console.WriteLn( " IOP ahead by: %d cycles", -EEsCycle );
EEsCycle = psxCpu->ExecuteBlock(EEsCycle);
iopEventAction = false;
}
@@ -435,7 +465,10 @@ __fi void _cpuEventTest_Shared()
// ---- Schedule Next Event Test --------------
const float mutiplier = static_cast<float>(PS2CLK) / static_cast<float>(PSXCLK);
const int nextIopEventDeta = ((psxRegs.iopNextEventCycle - psxRegs.cycle) * mutiplier);
// See R3000A.cpp:PSX_INT for the host-divergence rationale: cast the u32
// cycle delta to s32 *before* the float multiply.
const s32 iopCyclesUntilEvent = static_cast<s32>(psxRegs.iopNextEventCycle - psxRegs.cycle);
const int nextIopEventDeta = static_cast<s32>(iopCyclesUntilEvent * mutiplier);
// 8 or more cycles behind and there's an event scheduled
if (EEsCycle >= nextIopEventDeta)
{
@@ -448,7 +481,7 @@ __fi void _cpuEventTest_Shared()
else
{
// Otherwise IOP is caught up/not doing anything so we can wait for the next event.
cpuSetNextEventDelta(((psxRegs.iopNextEventCycle - psxRegs.cycle) * mutiplier) - EEsCycle);
cpuSetNextEventDelta(nextIopEventDeta - EEsCycle);
}
// Apply vsync and other counter nextCycles
+67 -6
View File
@@ -59,6 +59,9 @@ const vixl::aarch64::VRegister& armQRegister(int n)
}
// Opt-in only (matches origin/master): uncomment to compile vixl's
// PrintDisassembler/Decoder for armDisassembleAndDumpCode. Off by default so
// the disassembler TUs and statics don't ship in normal builds.
//#define INCLUDE_DISASSEMBLER
#ifdef INCLUDE_DISASSEMBLER
@@ -71,6 +74,7 @@ thread_local a64::MacroAssembler* armAsm;
thread_local u8* armAsmPtr;
thread_local size_t armAsmCapacity;
thread_local ArmConstantPool* armConstantPool;
thread_local ArmAddressRecorder* armAddressRecorder;
#ifdef INCLUDE_DISASSEMBLER
static std::mutex armDisasmMutex;
@@ -136,12 +140,15 @@ void armDisassembleAndDumpCode(const void* ptr, size_t size)
std::unique_lock lock(armDisasmMutex);
if (!armDisasm)
{
armDisasm = std::make_unique<a64::PrintDisassembler>(stderr);
std::FILE* logFile = Log::GetFileLogHandle();
armDisasm = std::make_unique<a64::PrintDisassembler>(logFile ? logFile : stderr);
armDisasmDecoder = std::make_unique<a64::Decoder>();
armDisasmDecoder->AppendVisitor(armDisasm.get());
}
armDisasmDecoder->Decode(static_cast<const vixl::aarch64::Instruction*>(ptr), static_cast<const vixl::aarch64::Instruction*>(ptr) + size);
const auto* start = reinterpret_cast<const vixl::aarch64::Instruction*>(ptr);
const auto* end = reinterpret_cast<const vixl::aarch64::Instruction*>(static_cast<const u8*>(ptr) + size);
armDisasmDecoder->Decode(start, end);
#else
Console.Error("Not compiled with INCLUDE_DISASSEMBLER");
#endif
@@ -162,13 +169,21 @@ void armEmitJmp(const void* ptr, bool force_inline)
if (use_blr)
{
if (armAddressRecorder)
armAddressRecorder->OnAbsoluteTarget(ptr);
armAsm->Mov(RXVIXLSCRATCH, reinterpret_cast<uintptr_t>(ptr));
armAsm->Br(RXVIXLSCRATCH);
}
else
{
a64::SingleEmissionCheckScope guard(armAsm);
armAsm->b(displacement);
{
a64::SingleEmissionCheckScope guard(armAsm);
armAsm->b(displacement);
}
// Record after emission: the scope entry may flush a pending vixl
// literal pool, so the insn address is only known once it's out.
if (armAddressRecorder)
armAddressRecorder->OnDirectBranch(armGetCurrentCodePointer() - 4, ptr, false);
}
}
@@ -187,13 +202,19 @@ void armEmitCall(const void* ptr, bool force_inline)
if (use_blr)
{
if (armAddressRecorder)
armAddressRecorder->OnAbsoluteTarget(ptr);
armAsm->Mov(RXVIXLSCRATCH, reinterpret_cast<uintptr_t>(ptr));
armAsm->Blr(RXVIXLSCRATCH);
}
else
{
a64::SingleEmissionCheckScope guard(armAsm);
armAsm->bl(displacement);
{
a64::SingleEmissionCheckScope guard(armAsm);
armAsm->bl(displacement);
}
if (armAddressRecorder)
armAddressRecorder->OnDirectBranch(armGetCurrentCodePointer() - 4, ptr, true);
}
}
@@ -226,6 +247,23 @@ void armEmitCondBranch(a64::Condition cond, const void* ptr)
static_cast<s64>(reinterpret_cast<intptr_t>(ptr) - reinterpret_cast<intptr_t>(armGetCurrentCodePointer()));
//pxAssert(Common::IsAligned(jump_distance, 4));
// A recorder patching this branch on relocation needs the imm26 reach of a
// plain B — B.cond's ±1MB imm19 may not survive the move. Force the long
// form for targets the recorder marks relocatable and record the B.
if (armAddressRecorder && armAddressRecorder->WantsLongCondBranch(ptr))
{
a64::MacroEmissionCheckScope guard(armAsm);
a64::Label branch_not_taken;
armAsm->b(&branch_not_taken, a64::InvertCondition(cond));
const s64 new_jump_distance =
static_cast<s64>(reinterpret_cast<intptr_t>(ptr) - reinterpret_cast<intptr_t>(armGetCurrentCodePointer()));
armAsm->b(new_jump_distance >> 2);
armAddressRecorder->OnDirectBranch(armGetCurrentCodePointer() - 4, ptr, false);
armAsm->bind(&branch_not_taken);
return;
}
if (a64::Instruction::IsValidImmPCOffset(a64::CondBranchType, jump_distance >> 2))
{
a64::SingleEmissionCheckScope guard(armAsm);
@@ -249,6 +287,23 @@ void armMoveAddressToReg(const vixl::aarch64::Register& reg, const void* addr)
// psxAsm->Mov(reg, static_cast<u64>(reinterpret_cast<uintptr_t>(addr)));
pxAssert(reg.IsX());
if (armAddressRecorder &&
armAddressRecorder->ClassifyMove(addr) == ArmAddressRecorder::MoveForm::CanonicalAbs)
{
// Fixed-width 16-byte form: every operand bit lives in a movz/movk
// imm16 field a relocation patcher can rewrite in place.
const u64 v = reinterpret_cast<uintptr_t>(addr);
{
vixl::ExactAssemblyScope guard(armAsm, 16);
armAsm->movz(reg, v & 0xFFFF, 0);
armAsm->movk(reg, (v >> 16) & 0xFFFF, 16);
armAsm->movk(reg, (v >> 32) & 0xFFFF, 32);
armAsm->movk(reg, (v >> 48) & 0xFFFF, 48);
}
armAddressRecorder->OnCanonicalAbsMove(armGetCurrentCodePointer() - 16, addr);
return;
}
const void* current_code_ptr_page = reinterpret_cast<const void*>(
reinterpret_cast<uintptr_t>(armGetCurrentCodePointer()) & ~static_cast<uintptr_t>(0xFFF));
const void* ptr_page =
@@ -261,6 +316,8 @@ void armMoveAddressToReg(const vixl::aarch64::Register& reg, const void* addr)
a64::SingleEmissionCheckScope guard(armAsm);
armAsm->adrp(reg, page_displacement);
}
if (armAddressRecorder)
armAddressRecorder->OnAdrp(armGetCurrentCodePointer() - 4, addr);
armAsm->Add(reg, reg, page_offset);
}
else if (vixl::IsInt21(page_displacement) && a64::Assembler::IsImmLogical(page_offset, 64))
@@ -269,10 +326,14 @@ void armMoveAddressToReg(const vixl::aarch64::Register& reg, const void* addr)
a64::SingleEmissionCheckScope guard(armAsm);
armAsm->adrp(reg, page_displacement);
}
if (armAddressRecorder)
armAddressRecorder->OnAdrp(armGetCurrentCodePointer() - 4, addr);
armAsm->Orr(reg, reg, page_offset);
}
else
{
if (armAddressRecorder)
armAddressRecorder->OnAbsoluteTarget(addr);
armAsm->Mov(reg, reinterpret_cast<uintptr_t>(addr));
}
}
+78 -3
View File
@@ -24,9 +24,14 @@
#define RXARG3 vixl::aarch64::x2
#define RXARG4 vixl::aarch64::x3
#define RXVIXLSCRATCH vixl::aarch64::x16
#define RWVIXLSCRATCH vixl::aarch64::w16
#define RSCRATCHADDR vixl::aarch64::x17
#define RXVIXLSCRATCH vixl::aarch64::x16 // Reserved for VIXL internal use — do NOT use in rec code
#define RWVIXLSCRATCH vixl::aarch64::w16 // Reserved for VIXL internal use — do NOT use in rec code
#define RSCRATCHADDR vixl::aarch64::x17 // Address scratch — removed from VIXL pool in armStartBlock
// General-purpose value scratch registers for recompiler use.
// These are caller-saved and NOT in VIXL's scratch pool.
#define RXSCRATCH vixl::aarch64::x8
#define RWSCRATCH vixl::aarch64::w8
#define RQSCRATCH vixl::aarch64::q30
#define RDSCRATCH vixl::aarch64::d30
@@ -59,12 +64,54 @@ const vixl::aarch64::VRegister& armQRegister(int n);
class ArmConstantPool;
// Address-emission observer for the on-disk VU program cache. While a
// recorder is attached (mVU code-cache episodes only — see mVUopenCodeCache),
// the emit helpers below report every host-address-bearing emission so the
// recorder can build a relocation fixup table, and let it force canonical
// fixed-width forms where the default encoding couldn't be patched after the
// code block moves:
// - armMoveAddressToReg of a volatile (heap) target → movz+movk×3 (16 bytes,
// patchable) instead of the shortest mov/adrp form.
// - armEmitCondBranch to a relocatable target → inverted-cond skip + B imm26
// (B.cond's ±1MB imm19 can't survive arbitrary replacement).
// `at` arguments are the address of the first emitted instruction of the
// reported shape. All hooks are no-ops when no recorder is attached.
class ArmAddressRecorder
{
public:
enum class MoveForm
{
Default, // emit in shortest form; recorder may still log it
CanonicalAbs, // emit fixed-width movz+movk×3 so the operand is patchable
};
virtual ~ArmAddressRecorder() = default;
// armMoveAddressToReg: pick the emission form for `addr`.
virtual MoveForm ClassifyMove(const void* addr) = 0;
// armMoveAddressToReg emitted the canonical 16-byte movz+movk×3 at `at`.
virtual void OnCanonicalAbsMove(u8* at, const void* addr) = 0;
// armMoveAddressToReg emitted ADRP (+Add/Orr) at `at`; the page offset is
// PC-relative and must be re-paged if this code moves.
virtual void OnAdrp(u8* at, const void* addr) = 0;
// armEmitJmp/armEmitCall/armEmitCondBranch emitted a direct B/BL imm26 at
// `at` targeting `target`.
virtual void OnDirectBranch(u8* at, const void* target, bool is_call) = 0;
// armEmitCondBranch: return true to force the long (cond-skip + B) form.
virtual bool WantsLongCondBranch(const void* target) = 0;
// An absolute (movz/movk-materialized) target with no patch site — emitted
// by the out-of-range paths of armEmitJmp/armEmitCall/armMoveAddressToReg.
// Recorder uses this to verify the target is run-invariant.
virtual void OnAbsoluteTarget(const void* target) = 0;
};
static const u32 SP_SCRATCH_OFFSET = 0;
extern thread_local vixl::aarch64::MacroAssembler* armAsm;
extern thread_local u8* armAsmPtr;
extern thread_local size_t armAsmCapacity;
extern thread_local ArmConstantPool* armConstantPool;
extern thread_local ArmAddressRecorder* armAddressRecorder;
static __fi bool armHasBlock()
{
@@ -104,6 +151,34 @@ void armGetMemOperandInRegister(const vixl::aarch64::Register& addr_reg,
void armLoadConstant128(const vixl::aarch64::VRegister& reg, const void* ptr);
// Pack 4 per-lane bool lanes (each lane is all-1s or 0 — the natural output of
// a NEON CMxx / FCMxx against zero) into a 4-bit GPR using the canonical
// AArch64 movemask idiom: AND with a per-lane weight vector, ADDV-sum across
// lanes, then UMOV to GPR.
//
// `data` is clobbered (AND in-place, ADDV writes the low S lane in-place).
// `tmp` is loaded with the weight vector via the vixl literal pool; must
// differ from `data`. Both must be Q-form (128-bit).
//
// PS2 MAC flag bit order is bit0=W, bit3=X (reverse of NEON lane order). Pass
// reverse=true to get that mapping; reverse=false yields lane[i]→bit[i].
//
// Emits 4 insns: ldr q (literal pool) + and.16b + addv s + umov w.
__fi static void armEmitPackLaneBits(const vixl::aarch64::Register& dst,
const vixl::aarch64::VRegister& data, const vixl::aarch64::VRegister& tmp,
bool reverse)
{
// Weight vector as u32 lanes [0..3]. low64 packs lanes 0+1, high64 packs 2+3.
// forward {1,2,4,8}: low = (2<<32)|1, high = (8<<32)|4
// reverse {8,4,2,1}: low = (4<<32)|8, high = (1<<32)|2
const u64 low64 = reverse ? 0x0000000400000008ULL : 0x0000000200000001ULL;
const u64 high64 = reverse ? 0x0000000100000002ULL : 0x0000000800000004ULL;
armAsm->Ldr(tmp, high64, low64);
armAsm->And(data.V16B(), data.V16B(), tmp.V16B());
armAsm->Addv(vixl::aarch64::VRegister(data.GetCode(), 32), data.V4S());
armAsm->Umov(dst, data.V4S(), 0);
}
// may clobber RSCRATCH/RSCRATCH2. they shouldn't be inputs.
void armEmitVTBL(const vixl::aarch64::VRegister& dst, const vixl::aarch64::VRegister& src1,
const vixl::aarch64::VRegister& src2, const vixl::aarch64::VRegister& tbl);
+334 -17
View File
@@ -1,30 +1,347 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0
#include "arm64/AsmHelpers.h"
#include "arm64/iR5900-arm64.h"
#include "common/Console.h"
#include "common/HostSys.h"
#include "Memory.h"
#include "MTVU.h"
#include "SaveState.h"
#include "vtlb.h"
#include "common/Assertions.h"
void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc, u32 guest_addr, u32 gpr_bitmask, u32 fpr_bitmask, u8 address_register, u8 data_register, u8 size_in_bits, bool is_signed, bool is_load, bool is_fpr)
namespace a64 = vixl::aarch64;
using namespace vtlb_private;
void vtlb_DynBackpatchLoadStore(uptr code_address, u32 code_size, u32 guest_pc, u32 guest_addr,
u32 gpr_bitmask, u32 fpr_bitmask, u8 address_register, u8 data_register,
u8 size_in_bits, bool is_signed, bool is_load, bool is_fpr)
{
pxFailRel("Not implemented.");
DevCon.WriteLn("Backpatching %s at %p[%u] (pc %08X vaddr %08X): GPR %08X FPR %08X Addr %u Data %u Size %u Flags %02X %02X",
is_load ? "load" : "store", (void*)code_address, code_size, guest_pc, guest_addr,
gpr_bitmask, fpr_bitmask, address_register, data_register, size_in_bits, is_signed, is_load);
u8* thunk = recBeginThunk();
// Collect caller-saved GPRs that need saving.
// Callee-saved (x19+) are preserved by the C call and don't need saving.
// For loads into a GPR, skip the data register (result goes there).
static constexpr u32 MAX_SAVE_GPRS = 16;
u8 gprs_to_save[MAX_SAVE_GPRS];
u32 num_gprs = 0;
for (u32 i = 0; i < 19; i++)
{
if (!(gpr_bitmask & (1u << i)))
continue;
// Skip scratch/reserved: x8 (RWSCRATCH), x16 (VIXL), x17 (RSCRATCHADDR), x18 (platform)
if (i == 8 || i >= 16)
continue;
// For loads into GPR, skip the data register
if (is_load && !is_fpr && i == data_register)
continue;
pxAssert(num_gprs < MAX_SAVE_GPRS);
gprs_to_save[num_gprs++] = static_cast<u8>(i);
}
// Collect NEON regs that need saving.
// q8-q15 lower 64 bits are callee-saved, but the JIT uses full 128-bit, so save all live ones.
static constexpr u32 MAX_SAVE_FPRS = 32;
u8 fprs_to_save[MAX_SAVE_FPRS];
u32 num_fprs = 0;
for (u32 i = 0; i < 32; i++)
{
if (!(fpr_bitmask & (1u << i)))
continue;
// For loads into FPR, skip the data register
if (is_load && is_fpr && i == data_register)
continue;
pxAssert(num_fprs < MAX_SAVE_FPRS);
fprs_to_save[num_fprs++] = static_cast<u8>(i);
}
// Calculate stack size (must be 16-byte aligned)
const u32 gpr_save_bytes = num_gprs * 8;
const u32 fpr_save_bytes = num_fprs * 16;
const u32 stack_size = (gpr_save_bytes + fpr_save_bytes + 15u) & ~15u;
if (stack_size > 0)
armAsm->Sub(a64::sp, a64::sp, stack_size);
// Save GPRs to stack
u32 offset = 0;
for (u32 i = 0; i < num_gprs; i++)
{
armAsm->Str(a64::XRegister(gprs_to_save[i]), a64::MemOperand(a64::sp, offset));
offset += 8;
}
// Save NEON regs to stack
for (u32 i = 0; i < num_fprs; i++)
{
armAsm->Str(a64::QRegister(fprs_to_save[i]), a64::MemOperand(a64::sp, offset));
offset += 16;
}
// At this point, all host registers still have their original JIT values
// (STR only reads, doesn't modify the source register).
// Flush cpuRegs.pc and cpuRegs.code for exception handling.
// The fastmem path skips iFlushCall, so these may be stale.
// If the vtlb handler triggers a TLB miss or other exception,
// cpuTlbMiss reads cpuRegs.pc to set EPC.
armAsm->Mov(RWSCRATCH, guest_pc);
armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.pc));
armAsm->Mov(RWSCRATCH, *(u32*)PSM(guest_pc));
armAsm->Str(RWSCRATCH, armCpuRegMem(&cpuRegs.code));
// Set up arguments for the vtlb handler call.
// 128-bit fastmem path. Always uses q0 (data_register == 0); the JIT
// callers in recVTLB-arm64.cpp materialize the load result / store
// value into q0 and never allocate q0 to a guest register at the
// fastmem emit point. vtlb_memRead128 returns r128 in q0 per AAPCS64;
// vtlb_memWrite128 takes value in q0.
if (size_in_bits == 128)
{
pxAssertRel(is_fpr && data_register == 0,
"128-bit fastmem backpatch must target q0");
if (address_register != 9)
armAsm->Mov(a64::w9, armWRegister(address_register));
armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS);
armMoveAddressToReg(RSCRATCHADDR, vtlb_private::vtlbdata.vmap);
armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3));
armAsm->Add(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW));
a64::Label slow_path, done;
armAsm->Tbnz(a64::x0, 63, &slow_path);
if (is_load)
armAsm->Ldr(a64::q0, a64::MemOperand(a64::x0));
else
armAsm->Str(a64::q0, a64::MemOperand(a64::x0));
armAsm->B(&done);
armAsm->Bind(&slow_path);
armAsm->Mov(a64::w0, a64::w9);
// Spill/reload RECCYCLE around the vtlb handler call — the slow path
// dispatches to MMIO handlers (hwRead*/hwWrite*) which read/write
// cpuRegs.cycle (timer regs, IntCHackCheck, etc.). Without this,
// the handler sees a stale cycle value, which can mis-schedule
// events and cause cascading mid-block timing bugs. Matches the
// pattern at recVTLB-arm64.cpp:112+120.
armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle));
if (is_load)
armEmitCall((void*)vtlb_memRead128);
else
armEmitCall((void*)vtlb_memWrite128);
armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle));
armAsm->Bind(&done);
}
else if (is_load)
{
// Load backpatch: emit inline VTLB read code (same as vtlbSoftmemRead).
if (address_register != 9)
armAsm->Mov(a64::w9, armWRegister(address_register));
// Inline VTLB lookup
armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS);
armMoveAddressToReg(RSCRATCHADDR, vtlb_private::vtlbdata.vmap);
armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3));
armAsm->Add(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW));
a64::Label slow_path, done;
armAsm->Tbnz(a64::x0, 63, &slow_path);
// Fast path: direct memory read via resolved host pointer
switch (size_in_bits)
{
case 8:
if (is_signed)
armAsm->Ldrsb(a64::x0, a64::MemOperand(a64::x0));
else
armAsm->Ldrb(a64::w0, a64::MemOperand(a64::x0));
break;
case 16:
if (is_signed)
armAsm->Ldrsh(a64::x0, a64::MemOperand(a64::x0));
else
armAsm->Ldrh(a64::w0, a64::MemOperand(a64::x0));
break;
case 32:
if (is_signed)
armAsm->Ldrsw(a64::x0, a64::MemOperand(a64::x0));
else
armAsm->Ldr(a64::w0, a64::MemOperand(a64::x0));
break;
case 64:
armAsm->Ldr(a64::x0, a64::MemOperand(a64::x0));
break;
default: pxFailRel("Unsupported load size in backpatch"); break;
}
armAsm->B(&done);
// Slow path: call vtlb_memRead handler
armAsm->Bind(&slow_path);
armAsm->Mov(a64::w0, a64::w9);
// Spill/reload RECCYCLE — see 128-bit slow_path above for rationale.
armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle));
switch (size_in_bits)
{
case 8: armEmitCall((void*)vtlb_memRead<mem8_t>); break;
case 16: armEmitCall((void*)vtlb_memRead<mem16_t>); break;
case 32: armEmitCall((void*)vtlb_memRead<mem32_t>); break;
case 64: armEmitCall((void*)vtlb_memRead<mem64_t>); break;
default: break;
}
armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle));
// Extend the handler return into x0 for the 64-bit cpuRegs.GPR store.
// AAPCS64 leaves the upper bits of x0 unspecified for sub-word returns,
// so UNSIGNED sub-64-bit loads must Uxtw too — otherwise the garbage
// upper 32 bits leak into the 64-bit EE GPR (LWU/LBU/LHU faulting to an
// MMIO/handler page). The fast inline path zero-extends via Ldrb/Ldrh/
// Ldr w0; this mirrors that, and the const-paddr shortcut in
// recVTLB-arm64.cpp which handles the identical hazard.
if (size_in_bits < 64)
{
if (is_signed)
{
if (size_in_bits == 8)
armAsm->Sxtb(a64::x0, a64::w0);
else if (size_in_bits == 16)
armAsm->Sxth(a64::x0, a64::w0);
else if (size_in_bits == 32)
armAsm->Sxtw(a64::x0, a64::w0);
}
else
{
armAsm->Uxtw(a64::x0, a64::w0);
}
}
armAsm->Bind(&done);
// Move result to data register
if (!is_fpr)
{
if (data_register != 0)
armAsm->Mov(armXRegister(data_register), a64::x0);
}
else
{
armAsm->Fmov(a64::SRegister(data_register), a64::w0);
}
}
else
{
// Store backpatch: emit inline VTLB write code (same as vtlbSoftmemWrite),
// emitting the inline VTLB lookup + store rather than calling vtlb_memWrite.
// Move address to w9, value to w10 (standard scratch for inline VTLB).
if (address_register != 9)
armAsm->Mov(a64::w9, armWRegister(address_register));
if (data_register != 10)
{
if (size_in_bits <= 32)
armAsm->Mov(a64::w10, armWRegister(data_register));
else
armAsm->Mov(a64::x10, armXRegister(data_register));
}
// Inline VTLB lookup: vmap[addr >> PAGE_BITS] → ppf
armAsm->Lsr(a64::w8, a64::w9, VTLB_PAGE_BITS);
armMoveAddressToReg(RSCRATCHADDR, vtlb_private::vtlbdata.vmap);
armAsm->Ldr(a64::x8, a64::MemOperand(RSCRATCHADDR, a64::x8, a64::LSL, 3));
armAsm->Add(a64::x0, a64::x8, a64::Operand(a64::w9, a64::UXTW));
a64::Label slow_path, done;
armAsm->Tbnz(a64::x0, 63, &slow_path);
// Fast path: direct memory write via resolved host pointer
switch (size_in_bits)
{
case 8: armAsm->Strb(a64::w10, a64::MemOperand(a64::x0)); break;
case 16: armAsm->Strh(a64::w10, a64::MemOperand(a64::x0)); break;
case 32: armAsm->Str(a64::w10, a64::MemOperand(a64::x0)); break;
case 64: armAsm->Str(a64::x10, a64::MemOperand(a64::x0)); break;
default: pxFailRel("Unsupported store size in backpatch"); break;
}
armAsm->B(&done);
// Slow path: call vtlb_memWrite handler
armAsm->Bind(&slow_path);
armAsm->Mov(a64::w0, a64::w9);
if (size_in_bits <= 32)
armAsm->Mov(a64::w1, a64::w10);
else
armAsm->Mov(a64::x1, a64::x10);
// Spill/reload RECCYCLE — see 128-bit slow_path above for rationale.
armAsm->Str(RECCYCLE, armCpuRegMem(&cpuRegs.cycle));
switch (size_in_bits)
{
case 8: armEmitCall((void*)vtlb_memWrite<mem8_t>); break;
case 16: armEmitCall((void*)vtlb_memWrite<mem16_t>); break;
case 32: armEmitCall((void*)vtlb_memWrite<mem32_t>); break;
case 64: armEmitCall((void*)vtlb_memWrite<mem64_t>); break;
default: pxFailRel("Unsupported store size in backpatch"); break;
}
armAsm->Ldr(RECCYCLE, armCpuRegMem(&cpuRegs.cycle));
armAsm->Bind(&done);
}
// Restore GPRs from stack
offset = 0;
for (u32 i = 0; i < num_gprs; i++)
{
armAsm->Ldr(a64::XRegister(gprs_to_save[i]), a64::MemOperand(a64::sp, offset));
offset += 8;
}
// Restore NEON regs from stack
for (u32 i = 0; i < num_fprs; i++)
{
armAsm->Ldr(a64::QRegister(fprs_to_save[i]), a64::MemOperand(a64::sp, offset));
offset += 16;
}
if (stack_size > 0)
armAsm->Add(a64::sp, a64::sp, stack_size);
// Branch back to the instruction after the faulting load/store
armEmitJmp((void*)(code_address + code_size));
u8* thunk_end = recEndThunk();
// Flush instruction cache for the ENTIRE thunk.
// ARM64 icache is not coherent with dcache — without this, the CPU may
// execute stale instructions from previously compiled code at the thunk's
// address, causing SIGILL or corruption.
HostSys::FlushInstructionCache(thunk, static_cast<u32>(thunk_end - thunk));
// Patch the faulting instruction with a B (branch) to the thunk.
// ARM64 B instruction: 0x14000000 | imm26, where imm26 = byte_offset / 4
const s64 branch_offset = static_cast<s64>(thunk - reinterpret_cast<u8*>(code_address));
pxAssert((branch_offset & 3) == 0);
const s64 branch_imm26 = branch_offset >> 2;
pxAssertRel(branch_imm26 >= -0x2000000 && branch_imm26 <= 0x1FFFFFF,
"Backpatch thunk too far from faulting instruction for B instruction");
HostSys::BeginCodeWrite();
u32* patch_ptr = reinterpret_cast<u32*>(code_address);
*patch_ptr = 0x14000000u | (static_cast<u32>(branch_imm26) & 0x03FFFFFFu);
HostSys::EndCodeWrite();
// Flush icache at the patch point too.
HostSys::FlushInstructionCache(reinterpret_cast<void*>(code_address), 4);
}
bool SaveStateBase::vuJITFreeze()
{
if(IsSaving())
vu1Thread.WaitVU();
Console.Warning("recompiler state is stubbed in arm64!");
// HACK!!
// size of microRegInfo structure
std::array<u8,96> empty_data{};
Freeze(empty_data);
Freeze(empty_data);
return true;
}
// vuJITFreeze() is defined in microVU-arm64.cpp
+16 -2
View File
@@ -516,7 +516,8 @@ static __ri void vtlb_Miss(u32 addr, u32 mode)
if (EmuConfig.Gamefixes.GoemonTlbHack)
GoemonTlbMissDebug();
// Hack to handle expected tlb miss by some games.
// Interpreter: raise the exception, then CancelInstruction stops the current
// instruction so the exception vector is dispatched immediately.
if (Cpu == &intCpu)
{
if (mode)
@@ -524,7 +525,6 @@ static __ri void vtlb_Miss(u32 addr, u32 mode)
else
cpuTlbMissR(addr, cpuRegs.branch);
// Exception handled. Current instruction need to be stopped
Cpu->CancelInstruction();
return;
}
@@ -539,9 +539,23 @@ static __ri void vtlb_Miss(u32 addr, u32 mode)
return;
}
#ifdef __aarch64__
// arm64 recompiler: raise the TLB-miss exception here. cpuTlbMissR/W sets
// cpuRegs.pc to the exception vector, which the rec picks up at the next
// dispatch — no CancelInstruction longjmp (which is interpreter-only; the
// arm64 rec returns and lets the exception state take effect at block end).
if (mode)
cpuTlbMissW(addr, cpuRegs.branch);
else
cpuTlbMissR(addr, cpuRegs.branch);
#else
// x86 recompiler: upstream behavior — log and continue without raising
// (x86 recCancelInstruction is a stub, so the arm64 exception path above
// must not run here).
static int spamStop = 0;
if (spamStop++ < 50 || IsDevBuild)
Console.Error(message);
#endif
}
// BusError exception: more serious than a TLB miss. If properly emulated the PS2 kernel
+5 -2
View File
@@ -121,7 +121,10 @@ namespace vtlb_private
static const uint VTLB_PAGE_MASK = 4095;
static const uint VTLB_PAGE_SIZE = 4096;
static const uint VTLB_PMAP_SZ = _1mb * 512;
// Physical map covers 1GB to include RAM mirrors at 0x20000000 (uncached)
// and 0x30000000 (uncached & accelerated) used by BIOS InitRDRAM.
// 1GB is sufficient for all known PS2 mappings.
static const uint VTLB_PMAP_SZ = _1mb * 1024;
static const uint VTLB_PMAP_ITEMS = VTLB_PMAP_SZ / VTLB_PAGE_SIZE;
static const uint VTLB_VMAP_ITEMS = _4gb / VTLB_PAGE_SIZE;
@@ -189,7 +192,7 @@ namespace vtlb_private
// third indexer -- 128 possible handlers!
void* RWFT[5][2][VTLB_HANDLER_ITEMS];
VTLBPhysical pmap[VTLB_PMAP_ITEMS]; //512KB // PS2 physical to x86 physical
VTLBPhysical pmap[VTLB_PMAP_ITEMS]; //2MB (VTLB_PMAP_ITEMS * sizeof(VTLBPhysical)) // PS2 physical to host physical
VTLBVirtual* vmap; //4MB (allocated by vtlb_init) // PS2 virtual to x86 physical