[ARM64] Initial commit for arm64 backend

Based entirely off existing xbyak x86 implementation and available
tests. Still needs a lot of optimization and testing on non-Windows
platforms.

So far passes all tests and boots at least some games on Windows.
This commit is contained in:
Herman S.
2026-03-21 01:17:18 +09:00
parent c6e112bcd8
commit 883c2030d0
44 changed files with 13428 additions and 140 deletions

View File

@@ -12,7 +12,12 @@ xe_target_defaults(xenia-core)
# All subdirectories
add_subdirectory(base)
add_subdirectory(cpu)
add_subdirectory(cpu/backend/x64)
if(XE_TARGET_X86_64)
add_subdirectory(cpu/backend/x64)
endif()
if(XE_TARGET_AARCH64)
add_subdirectory(cpu/backend/a64)
endif()
add_subdirectory(apu)
add_subdirectory(apu/nop)
add_subdirectory(gpu)

View File

@@ -24,17 +24,19 @@ if(WIN32)
${CMAKE_CURRENT_SOURCE_DIR}/main_resources.rc
${PROJECT_SOURCE_DIR}/src/xenia/base/app_win32.manifest
)
# main_init_win.cc needs SSE2 only (not AVX)
set_source_files_properties(
${PROJECT_SOURCE_DIR}/src/xenia/base/main_init_win.cc
PROPERTIES COMPILE_OPTIONS "/arch:SSE2"
)
if(XE_TARGET_X86_64)
# main_init_win.cc needs SSE2 only (not AVX)
set_source_files_properties(
${PROJECT_SOURCE_DIR}/src/xenia/base/main_init_win.cc
PROPERTIES COMPILE_OPTIONS "/arch:SSE2"
)
endif()
else()
target_sources(xenia-app PRIVATE
${PROJECT_SOURCE_DIR}/src/xenia/base/main_init_posix.cc
${PROJECT_SOURCE_DIR}/src/xenia/ui/windowed_app_main_posix.cc
)
if(NOT MSVC)
if(NOT MSVC AND XE_TARGET_X86_64)
set_source_files_properties(
${PROJECT_SOURCE_DIR}/src/xenia/base/main_init_posix.cc
PROPERTIES COMPILE_OPTIONS "-msse2;-mno-avx"
@@ -88,9 +90,11 @@ target_link_libraries(xenia-app PRIVATE
xenia-hid-sdl
)
# x64 backend
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
# Architecture-specific backend
if(XE_TARGET_X86_64)
target_link_libraries(xenia-app PRIVATE xenia-cpu-backend-x64)
elseif(XE_TARGET_AARCH64)
target_link_libraries(xenia-app PRIVATE xenia-cpu-backend-a64)
endif()
# Platform-specific libraries

View File

@@ -29,6 +29,57 @@ constexpr size_t kMaxHandlerCount = 8;
// Executed in order.
std::pair<ExceptionHandler::Handler, void*> handlers_[kMaxHandlerCount];
static void CaptureThreadContext(HostThreadContext& thread_context,
PCONTEXT ctx) {
#if XE_ARCH_AMD64
thread_context.rip = ctx->Rip;
thread_context.eflags = ctx->EFlags;
std::memcpy(thread_context.int_registers, &ctx->Rax,
sizeof(thread_context.int_registers));
std::memcpy(thread_context.xmm_registers, &ctx->Xmm0,
sizeof(thread_context.xmm_registers));
#elif XE_ARCH_ARM64
thread_context.pc = ctx->Pc;
thread_context.pstate = ctx->Cpsr;
thread_context.sp = ctx->Sp;
std::memcpy(thread_context.x, &ctx->X0, sizeof(thread_context.x));
std::memcpy(thread_context.v, &ctx->V[0], sizeof(thread_context.v));
#endif
}
static void RestoreThreadContext(PCONTEXT ctx,
const HostThreadContext& thread_context,
const Exception& ex) {
#if XE_ARCH_AMD64
ctx->Rip = thread_context.rip;
ctx->EFlags = thread_context.eflags;
uint32_t modified_register_index;
uint16_t modified_int_registers_remaining = ex.modified_int_registers();
while (xe::bit_scan_forward(modified_int_registers_remaining,
&modified_register_index)) {
modified_int_registers_remaining &=
~(UINT16_C(1) << modified_register_index);
(&ctx->Rax)[modified_register_index] =
thread_context.int_registers[modified_register_index];
}
uint16_t modified_xmm_registers_remaining = ex.modified_xmm_registers();
while (xe::bit_scan_forward(modified_xmm_registers_remaining,
&modified_register_index)) {
modified_xmm_registers_remaining &=
~(UINT16_C(1) << modified_register_index);
std::memcpy(&ctx->Xmm0 + modified_register_index,
&thread_context.xmm_registers[modified_register_index],
sizeof(vec128_t));
}
#elif XE_ARCH_ARM64
ctx->Pc = thread_context.pc;
ctx->Cpsr = thread_context.pstate;
ctx->Sp = thread_context.sp;
std::memcpy(&ctx->X0, thread_context.x, sizeof(thread_context.x));
std::memcpy(&ctx->V[0], thread_context.v, sizeof(thread_context.v));
#endif
}
LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
// Visual Studio SetThreadName.
if (ex_info->ExceptionRecord->ExceptionCode == 0x406D1388) {
@@ -36,12 +87,7 @@ LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
}
HostThreadContext thread_context;
thread_context.rip = ex_info->ContextRecord->Rip;
thread_context.eflags = ex_info->ContextRecord->EFlags;
std::memcpy(thread_context.int_registers, &ex_info->ContextRecord->Rax,
sizeof(thread_context.int_registers));
std::memcpy(thread_context.xmm_registers, &ex_info->ContextRecord->Xmm0,
sizeof(thread_context.xmm_registers));
CaptureThreadContext(thread_context, ex_info->ContextRecord);
// https://msdn.microsoft.com/en-us/library/ms679331(v=vs.85).aspx
// https://msdn.microsoft.com/en-us/library/aa363082(v=vs.85).aspx
@@ -78,26 +124,7 @@ LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
for (size_t i = 0; i < xe::countof(handlers_) && handlers_[i].first; ++i) {
if (handlers_[i].first(&ex, handlers_[i].second)) {
// Exception handled.
ex_info->ContextRecord->Rip = thread_context.rip;
ex_info->ContextRecord->EFlags = thread_context.eflags;
uint32_t modified_register_index;
uint16_t modified_int_registers_remaining = ex.modified_int_registers();
while (xe::bit_scan_forward(modified_int_registers_remaining,
&modified_register_index)) {
modified_int_registers_remaining &=
~(UINT16_C(1) << modified_register_index);
(&ex_info->ContextRecord->Rax)[modified_register_index] =
thread_context.int_registers[modified_register_index];
}
uint16_t modified_xmm_registers_remaining = ex.modified_xmm_registers();
while (xe::bit_scan_forward(modified_xmm_registers_remaining,
&modified_register_index)) {
modified_xmm_registers_remaining &=
~(UINT16_C(1) << modified_register_index);
std::memcpy(&ex_info->ContextRecord->Xmm0 + modified_register_index,
&thread_context.xmm_registers[modified_register_index],
sizeof(vec128_t));
}
RestoreThreadContext(ex_info->ContextRecord, thread_context, ex);
return EXCEPTION_CONTINUE_EXECUTION;
}
}

View File

@@ -0,0 +1,12 @@
add_library(xenia-cpu-backend-a64 STATIC)
xe_platform_sources(xenia-cpu-backend-a64 ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(xenia-cpu-backend-a64 PRIVATE
CAPSTONE_HAS_ARM64
CAPSTONE_USE_SYS_DYN_MEM
)
target_include_directories(xenia-cpu-backend-a64 PRIVATE
${PROJECT_SOURCE_DIR}/third_party/capstone/include
${PROJECT_SOURCE_DIR}/third_party/xbyak_aarch64/xbyak_aarch64
)
target_link_libraries(xenia-cpu-backend-a64 PUBLIC capstone fmt xenia-base xenia-cpu xbyak_aarch64)
xe_target_defaults(xenia-cpu-backend-a64)

View File

@@ -0,0 +1,143 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_assembler.h"
#include <climits>
#include "third_party/capstone/include/capstone/aarch64.h"
#include "third_party/capstone/include/capstone/capstone.h"
#include "xenia/base/profiling.h"
#include "xenia/base/reset_scope.h"
#include "xenia/base/string.h"
#include "xenia/cpu/backend/a64/a64_backend.h"
#include "xenia/cpu/backend/a64/a64_code_cache.h"
#include "xenia/cpu/backend/a64/a64_emitter.h"
#include "xenia/cpu/backend/a64/a64_function.h"
#include "xenia/cpu/cpu_flags.h"
#include "xenia/cpu/hir/hir_builder.h"
#include "xenia/cpu/hir/label.h"
#include "xenia/cpu/processor.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
using xe::cpu::hir::HIRBuilder;
A64Assembler::A64Assembler(A64Backend* backend)
: Assembler(backend), a64_backend_(backend), capstone_handle_(0) {
if (cs_open(CS_ARCH_AARCH64, CS_MODE_ARM, &capstone_handle_) != CS_ERR_OK) {
assert_always("Failed to initialize capstone for ARM64");
}
cs_option(capstone_handle_, CS_OPT_DETAIL, CS_OPT_OFF);
}
A64Assembler::~A64Assembler() {
emitter_.reset();
if (capstone_handle_) {
cs_close(&capstone_handle_);
}
}
bool A64Assembler::Initialize() {
if (!Assembler::Initialize()) {
return false;
}
emitter_.reset(new A64Emitter(a64_backend_, &allocator_));
return true;
}
void A64Assembler::Reset() {
string_buffer_.Reset();
Assembler::Reset();
}
bool A64Assembler::Assemble(GuestFunction* function, HIRBuilder* builder,
uint32_t debug_info_flags,
std::unique_ptr<FunctionDebugInfo> debug_info) {
SCOPE_profile_cpu_f("cpu");
// Reset when we leave.
xe::make_reset_scope(this);
// Lower HIR -> ARM64.
void* machine_code = nullptr;
size_t code_size = 0;
if (!emitter_->Emit(function, builder, debug_info_flags, debug_info.get(),
&machine_code, &code_size, &function->source_map())) {
return false;
}
// Stash generated machine code.
if (debug_info_flags & DebugInfoFlags::kDebugInfoDisasmMachineCode) {
DumpMachineCode(machine_code, code_size, function->source_map(),
&string_buffer_);
debug_info->set_machine_code_disasm(xe_strdup(string_buffer_.buffer()));
string_buffer_.Reset();
}
function->set_debug_info(std::move(debug_info));
static_cast<A64Function*>(function)->Setup(
reinterpret_cast<uint8_t*>(machine_code), code_size);
// Install into indirection table.
uint64_t host_address = reinterpret_cast<uint64_t>(machine_code);
assert_true((host_address >> 32) == 0);
reinterpret_cast<A64CodeCache*>(backend_->code_cache())
->AddIndirection(function->address(),
static_cast<uint32_t>(host_address));
return true;
}
void A64Assembler::DumpMachineCode(
void* machine_code, size_t code_size,
const std::vector<SourceMapEntry>& source_map, StringBuffer* str) {
if (source_map.empty()) {
return;
}
auto source_map_index = 0;
uint32_t next_code_offset = source_map[0].code_offset;
const uint8_t* code_ptr = reinterpret_cast<uint8_t*>(machine_code);
size_t remaining_code_size = code_size;
uint64_t address = uint64_t(machine_code);
cs_insn insn = {0};
while (remaining_code_size &&
cs_disasm_iter(capstone_handle_, &code_ptr, &remaining_code_size,
&address, &insn)) {
// Look up source offset.
auto code_offset =
uint32_t(code_ptr - reinterpret_cast<uint8_t*>(machine_code));
if (code_offset >= next_code_offset &&
source_map_index < source_map.size()) {
auto& source_map_entry = source_map[source_map_index];
str->AppendFormat("{:08X} ", source_map_entry.guest_address);
++source_map_index;
next_code_offset = source_map_index < source_map.size()
? source_map[source_map_index].code_offset
: UINT_MAX;
} else {
str->Append(" ");
}
str->AppendFormat("{:08X} {:<6} {}\n", uint32_t(insn.address),
insn.mnemonic, insn.op_str);
}
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,60 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_ASSEMBLER_H_
#define XENIA_CPU_BACKEND_A64_A64_ASSEMBLER_H_
#include <memory>
#include <vector>
#include "xenia/base/string_buffer.h"
#include "xenia/cpu/backend/a64/a64_emitter.h"
#include "xenia/cpu/backend/assembler.h"
#include "xenia/cpu/function.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class A64Backend;
class A64Assembler : public Assembler {
public:
explicit A64Assembler(A64Backend* backend);
~A64Assembler() override;
bool Initialize() override;
void Reset() override;
bool Assemble(GuestFunction* function, hir::HIRBuilder* builder,
uint32_t debug_info_flags,
std::unique_ptr<FunctionDebugInfo> debug_info) override;
private:
void DumpMachineCode(void* machine_code, size_t code_size,
const std::vector<SourceMapEntry>& source_map,
StringBuffer* str);
private:
A64Backend* a64_backend_;
std::unique_ptr<A64Emitter> emitter_;
XbyakA64Allocator allocator_;
uintptr_t capstone_handle_ = 0;
StringBuffer string_buffer_;
};
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_ASSEMBLER_H_

View File

@@ -0,0 +1,869 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_backend.h"
#include <cstddef>
#include <cstring>
#include "xenia/base/clock.h"
#include "xenia/base/exception_handler.h"
#include "xenia/base/logging.h"
#include "xenia/base/memory.h"
#include "xenia/base/platform.h"
#if XE_PLATFORM_WIN32
#include "xenia/base/platform_win.h"
#endif
#if XE_ARCH_ARM64 && XE_COMPILER_MSVC
#include <intrin.h>
#endif
#include "xenia/cpu/backend/a64/a64_assembler.h"
#include "xenia/cpu/backend/a64/a64_code_cache.h"
#include "xenia/cpu/backend/a64/a64_emitter.h"
#include "xenia/cpu/backend/a64/a64_function.h"
#include "xenia/cpu/backend/a64/a64_sequences.h"
#include "xenia/cpu/backend/a64/a64_stack_layout.h"
#include "xenia/cpu/breakpoint.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/processor.h"
#include "xenia/cpu/stack_walker.h"
#include "xenia/cpu/thread_state.h"
#include "xenia/cpu/xex_module.h"
DEFINE_int64(a64_max_stackpoints, 65536,
"Max number of host->guest stack mappings we can record.", "a64");
DEFINE_bool(a64_enable_host_guest_stack_synchronization, true,
"Records entries for guest/host stack mappings at function starts "
"and checks for reentry at return sites. Has slight performance "
"impact, but fixes crashes in games that use setjmp/longjmp.",
"a64");
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
// Resolve a guest function at runtime. Called by the resolve thunk when
// a guest address has not yet been compiled.
uint64_t ResolveFunction(void* raw_context, uint64_t target_address);
// ==========================================================================
// A64HelperEmitter — generates thunks using xbyak_aarch64.
// ==========================================================================
class A64HelperEmitter : public A64Emitter {
public:
A64HelperEmitter(A64Backend* backend, XbyakA64Allocator* allocator);
HostToGuestThunk EmitHostToGuestThunk();
GuestToHostThunk EmitGuestToHostThunk();
ResolveFunctionThunk EmitResolveFunctionThunk();
void* EmitGuestAndHostSynchronizeStackHelper();
};
A64HelperEmitter::A64HelperEmitter(A64Backend* backend,
XbyakA64Allocator* allocator)
: A64Emitter(backend, allocator) {}
// --------------------------------------------------------------------------
// HostToGuestThunk
// --------------------------------------------------------------------------
// Called from host C++ code to enter JIT'd guest code.
// x0 = target machine code address
// x1 = PPCContext* (arg0)
// x2 = return address value (arg1)
//
// ARM64 AAPCS64 calling convention:
// Caller-saved: x0-x18, v0-v7, v16-v31
// Callee-saved: x19-x28, x29(FP), x30(LR), d8-d15
//
// We save all callee-saved regs, set up context (x20) and membase (x21),
// then call the target. On return, restore and return to host.
HostToGuestThunk A64HelperEmitter::EmitHostToGuestThunk() {
struct {
size_t prolog;
size_t prolog_stack_alloc;
size_t body;
size_t epilog;
size_t tail;
} code_offsets = {};
code_offsets.prolog = getSize();
// Allocate thunk stack frame.
// Save x29(FP) and x30(LR) first, then callee-saved GPRs and NEON regs.
const size_t thunk_stack = StackLayout::THUNK_STACK_SIZE;
// sub sp, sp, #thunk_stack
sub(sp, sp, static_cast<uint32_t>(thunk_stack));
code_offsets.prolog_stack_alloc = getSize();
// Save callee-saved GPRs: x19-x28, x29, x30
stp(x19, x20, ptr(sp, 0x00));
stp(x21, x22, ptr(sp, 0x10));
stp(x23, x24, ptr(sp, 0x20));
stp(x25, x26, ptr(sp, 0x30));
stp(x27, x28, ptr(sp, 0x40));
stp(x29, x30, ptr(sp, 0x50));
// Save callee-saved NEON regs: d8-d15
stp(d8, d9, ptr(sp, 0x60));
stp(d10, d11, ptr(sp, 0x70));
stp(d12, d13, ptr(sp, 0x80));
stp(d14, d15, ptr(sp, 0x90));
code_offsets.body = getSize();
// Set up guest execution state.
// x20 = context (PPCContext*)
mov(x20, x1);
// x21 = virtual_membase (loaded from context)
ldr(x21, ptr(x20, static_cast<int32_t>(
offsetof(ppc::PPCContext, virtual_membase))));
// x0 still holds target, x2 holds return address.
// The guest function's prolog stores x0 to GUEST_RET_ADDR on its stack
// frame. Move the target to a scratch reg and put the guest return
// address into x0.
mov(x9, x0); // x9 = target (scratch reg)
// Pass guest return address in x0 (convention for guest function entry).
mov(x0, x2); // x0 = guest return address
// Call the guest function.
blr(x9);
code_offsets.epilog = getSize();
// Restore callee-saved NEON regs.
ldp(d14, d15, ptr(sp, 0x90));
ldp(d12, d13, ptr(sp, 0x80));
ldp(d10, d11, ptr(sp, 0x70));
ldp(d8, d9, ptr(sp, 0x60));
// Restore callee-saved GPRs.
ldp(x29, x30, ptr(sp, 0x50));
ldp(x27, x28, ptr(sp, 0x40));
ldp(x25, x26, ptr(sp, 0x30));
ldp(x23, x24, ptr(sp, 0x20));
ldp(x21, x22, ptr(sp, 0x10));
ldp(x19, x20, ptr(sp, 0x00));
// Deallocate stack.
add(sp, sp, static_cast<uint32_t>(thunk_stack));
ret();
code_offsets.tail = getSize();
EmitFunctionInfo func_info = {};
func_info.code_size.total = getSize();
func_info.code_size.prolog = code_offsets.body - code_offsets.prolog;
func_info.code_size.body = code_offsets.epilog - code_offsets.body;
func_info.code_size.epilog = code_offsets.tail - code_offsets.epilog;
func_info.code_size.tail = getSize() - code_offsets.tail;
func_info.prolog_stack_alloc_offset =
code_offsets.prolog_stack_alloc - code_offsets.prolog;
func_info.stack_size = thunk_stack;
void* fn = Emplace(func_info);
return reinterpret_cast<HostToGuestThunk>(fn);
}
// --------------------------------------------------------------------------
// GuestToHostThunk
// --------------------------------------------------------------------------
// Called from guest JIT code to transition into a host (C++) function.
// x0 = target host function
// x1 = arg0
// x2 = arg1
//
// We save volatile guest registers that we need to preserve across the
// host call, then call the host function with context as the first arg.
GuestToHostThunk A64HelperEmitter::EmitGuestToHostThunk() {
struct {
size_t prolog;
size_t prolog_stack_alloc;
size_t body;
size_t epilog;
size_t tail;
} code_offsets = {};
code_offsets.prolog = getSize();
// The guest JIT uses v4-v7 and v16-v31 as allocatable VEC regs.
// These are caller-saved in AAPCS64, so the host C function WILL clobber
// them. We must save all guest-allocated VEC regs (full 128-bit Q regs).
// GPRs x19-x28 are callee-saved in AAPCS64, so the C function preserves them.
//
// Stack layout:
// q4, q5 sp + 0x000 (32 bytes)
// q6, q7 sp + 0x020
// q16, q17 sp + 0x040
// q18, q19 sp + 0x060
// q20, q21 sp + 0x080
// q22, q23 sp + 0x0A0
// q24, q25 sp + 0x0C0
// q26, q27 sp + 0x0E0
// q28, q29 sp + 0x100
// q30, q31 sp + 0x120
// x29, x30 sp + 0x140
// Total: 0x150 = 336 bytes (16-byte aligned)
const size_t g2h_stack = 336;
sub(sp, sp, static_cast<uint32_t>(g2h_stack));
code_offsets.prolog_stack_alloc = getSize();
// Save guest-allocated VEC regs (full Q = 128-bit).
stp(Xbyak_aarch64::QReg(4), Xbyak_aarch64::QReg(5), ptr(sp, 0x000));
stp(Xbyak_aarch64::QReg(6), Xbyak_aarch64::QReg(7), ptr(sp, 0x020));
stp(Xbyak_aarch64::QReg(16), Xbyak_aarch64::QReg(17), ptr(sp, 0x040));
stp(Xbyak_aarch64::QReg(18), Xbyak_aarch64::QReg(19), ptr(sp, 0x060));
stp(Xbyak_aarch64::QReg(20), Xbyak_aarch64::QReg(21), ptr(sp, 0x080));
stp(Xbyak_aarch64::QReg(22), Xbyak_aarch64::QReg(23), ptr(sp, 0x0A0));
stp(Xbyak_aarch64::QReg(24), Xbyak_aarch64::QReg(25), ptr(sp, 0x0C0));
stp(Xbyak_aarch64::QReg(26), Xbyak_aarch64::QReg(27), ptr(sp, 0x0E0));
stp(Xbyak_aarch64::QReg(28), Xbyak_aarch64::QReg(29), ptr(sp, 0x100));
stp(Xbyak_aarch64::QReg(30), Xbyak_aarch64::QReg(31), ptr(sp, 0x120));
// Save x29/x30 (FP/LR).
stp(x29, x30, ptr(sp, 0x140));
code_offsets.body = getSize();
// Call host function.
// AAPCS64: x0=first arg. We set x0=context (from x20).
mov(x9, x0); // x9 = target function (scratch)
mov(x0, x20); // x0 = PPCContext* (our context reg)
// x1, x2, x3 already hold args from the caller.
blr(x9);
code_offsets.epilog = getSize();
// Restore.
ldp(x29, x30, ptr(sp, 0x140));
ldp(Xbyak_aarch64::QReg(30), Xbyak_aarch64::QReg(31), ptr(sp, 0x120));
ldp(Xbyak_aarch64::QReg(28), Xbyak_aarch64::QReg(29), ptr(sp, 0x100));
ldp(Xbyak_aarch64::QReg(26), Xbyak_aarch64::QReg(27), ptr(sp, 0x0E0));
ldp(Xbyak_aarch64::QReg(24), Xbyak_aarch64::QReg(25), ptr(sp, 0x0C0));
ldp(Xbyak_aarch64::QReg(22), Xbyak_aarch64::QReg(23), ptr(sp, 0x0A0));
ldp(Xbyak_aarch64::QReg(20), Xbyak_aarch64::QReg(21), ptr(sp, 0x080));
ldp(Xbyak_aarch64::QReg(18), Xbyak_aarch64::QReg(19), ptr(sp, 0x060));
ldp(Xbyak_aarch64::QReg(16), Xbyak_aarch64::QReg(17), ptr(sp, 0x040));
ldp(Xbyak_aarch64::QReg(6), Xbyak_aarch64::QReg(7), ptr(sp, 0x020));
ldp(Xbyak_aarch64::QReg(4), Xbyak_aarch64::QReg(5), ptr(sp, 0x000));
add(sp, sp, static_cast<uint32_t>(g2h_stack));
ret();
code_offsets.tail = getSize();
EmitFunctionInfo func_info = {};
func_info.code_size.total = getSize();
func_info.code_size.prolog = code_offsets.body - code_offsets.prolog;
func_info.code_size.body = code_offsets.epilog - code_offsets.body;
func_info.code_size.epilog = code_offsets.tail - code_offsets.epilog;
func_info.code_size.tail = getSize() - code_offsets.tail;
func_info.prolog_stack_alloc_offset =
code_offsets.prolog_stack_alloc - code_offsets.prolog;
func_info.stack_size = g2h_stack;
void* fn = Emplace(func_info);
return reinterpret_cast<GuestToHostThunk>(fn);
}
// --------------------------------------------------------------------------
// ResolveFunctionThunk
// --------------------------------------------------------------------------
// Called when guest code calls an unresolved function address.
// The indirection table initially points all entries here.
// We call ResolveFunction to compile/lookup the target, then jump to it.
//
// On entry from the indirection table:
// w16 = guest PPC address (loaded by the call sequence)
// x20 = context
// x30 = return address (from the BLR that got us here)
ResolveFunctionThunk A64HelperEmitter::EmitResolveFunctionThunk() {
struct {
size_t prolog;
size_t prolog_stack_alloc;
size_t body;
size_t epilog;
size_t tail;
} code_offsets = {};
code_offsets.prolog = getSize();
const size_t thunk_stack = StackLayout::THUNK_STACK_SIZE;
sub(sp, sp, static_cast<uint32_t>(thunk_stack));
code_offsets.prolog_stack_alloc = getSize();
// Save x29/x30 and x0 (guest return address, needed by the resolved
// function's prolog). x19 is callee-saved so it survives the C call.
stp(x29, x30, ptr(sp, 0x50));
stp(x0, x19, ptr(sp, 0x00)); // save x0 (guest ret addr) and x19
code_offsets.body = getSize();
// Call ResolveFunction(context, target_address).
mov(x0, x20); // x0 = PPCContext*
mov(x1, x16); // x1 = guest address (32-bit in w16)
// Load address of ResolveFunction.
mov(x9, reinterpret_cast<uint64_t>(&ResolveFunction));
blr(x9);
// x0 now holds the resolved host machine code address.
mov(x9, x0);
code_offsets.epilog = getSize();
// Restore x0 (guest return address) and saved regs.
ldp(x0, x19, ptr(sp, 0x00));
ldp(x29, x30, ptr(sp, 0x50));
add(sp, sp, static_cast<uint32_t>(thunk_stack));
cbz(x9, 8); // skip br x9 if null, fall through to brk
br(x9); // Jump to the resolved function (tail call — preserves LR).
brk(0xF0); // Resolution failed — trap for debugging.
code_offsets.tail = getSize();
EmitFunctionInfo func_info = {};
func_info.code_size.total = getSize();
func_info.code_size.prolog = code_offsets.body - code_offsets.prolog;
func_info.code_size.body = code_offsets.epilog - code_offsets.body;
func_info.code_size.epilog = code_offsets.tail - code_offsets.epilog;
func_info.code_size.tail = getSize() - code_offsets.tail;
func_info.prolog_stack_alloc_offset =
code_offsets.prolog_stack_alloc - code_offsets.prolog;
func_info.stack_size = thunk_stack;
void* fn = Emplace(func_info);
return reinterpret_cast<ResolveFunctionThunk>(fn);
}
// --------------------------------------------------------------------------
// GuestAndHostSynchronizeStackHelper
// --------------------------------------------------------------------------
// Called when longjmp is detected (guest r1 changed after a call returned).
// Walks the stackpoint array backward to find the matching host SP, restores
// it, and jumps back to the caller.
//
// On entry (set by the tail-emitted sync check in the guest function):
// x8 = return address (where to jump after fixup)
// x9 = caller's stack size (to subtract from restored SP)
// x20 = PPCContext*
// The backend context is at (x20 - sizeof(A64BackendContext)).
void* A64HelperEmitter::EmitGuestAndHostSynchronizeStackHelper() {
using namespace Xbyak_aarch64;
struct {
size_t prolog;
size_t prolog_stack_alloc;
size_t body;
size_t epilog;
size_t tail;
} code_offsets = {};
code_offsets.prolog = getSize();
code_offsets.prolog_stack_alloc = getSize();
code_offsets.body = getSize();
// Load backend context pointer: x17 = x20 - sizeof(A64BackendContext)
mov(x17, static_cast<uint64_t>(sizeof(A64BackendContext)));
sub(x17, x20, x17);
// x10 = stackpoints array pointer
ldr(x10, ptr(x17, static_cast<uint32_t>(
offsetof(A64BackendContext, stackpoints))));
// w11 = current_stackpoint_depth
ldr(w11, ptr(x17, static_cast<uint32_t>(offsetof(A64BackendContext,
current_stackpoint_depth))));
// w12 = current guest r1
ldr(w12, ptr(x20, static_cast<int32_t>(offsetof(ppc::PPCContext, r[1]))));
// Search backward through stackpoints for the first entry where
// guest_stack_ >= current r1 (guest stack was unwound past that frame).
// ecx = loop index, starting at depth - 1
sub(w13, w11, 1);
auto& loop = NewCachedLabel();
auto& found = NewCachedLabel();
auto& underflow = NewCachedLabel();
L(loop);
// Bounds check
tbnz(w13, 31, underflow); // if index went negative, bail
// x14 = &stackpoints[w13] = x10 + w13 * sizeof(A64BackendStackpoint)
mov(w14, static_cast<uint32_t>(sizeof(A64BackendStackpoint)));
umull(x14, w13, w14);
add(x14, x10, x14);
// w15 = stackpoints[index].guest_stack_
ldr(w15, ptr(x14, static_cast<uint32_t>(
offsetof(A64BackendStackpoint, guest_stack_))));
// If guest_stack_ >= current r1, we found our target frame.
cmp(w15, w12);
b(GE, found);
// Not found yet, go to previous entry.
sub(w13, w13, 1);
b(loop);
L(found);
// x14 points to the matching stackpoint entry.
// Restore host SP from stackpoints[index].host_stack_
ldr(x16, ptr(x14, static_cast<uint32_t>(
offsetof(A64BackendStackpoint, host_stack_))));
// Adjust for the caller's stack frame: SP = host_stack_ - stack_size
sub(x16, x16, x9);
mov(sp, x16);
// Update current_stackpoint_depth = index + 1
// (the entry we restored to has been consumed)
add(w13, w13, 1);
str(w13, ptr(x17, static_cast<uint32_t>(offsetof(A64BackendContext,
current_stackpoint_depth))));
// Jump back to the caller.
br(x8);
L(underflow);
// Should be impossible — stackpoint array underflowed.
brk(0xF1);
code_offsets.epilog = getSize();
code_offsets.tail = getSize();
EmitFunctionInfo func_info = {};
func_info.code_size.total = getSize();
func_info.code_size.prolog = code_offsets.body - code_offsets.prolog;
func_info.code_size.body = code_offsets.epilog - code_offsets.body;
func_info.code_size.epilog = code_offsets.tail - code_offsets.epilog;
func_info.code_size.tail = getSize() - code_offsets.tail;
func_info.prolog_stack_alloc_offset =
code_offsets.prolog_stack_alloc - code_offsets.prolog;
func_info.stack_size = 0;
return Emplace(func_info);
}
// ==========================================================================
// ResolveFunction — runtime function resolution.
// ==========================================================================
uint64_t ResolveFunction(void* raw_context, uint64_t target_address) {
auto guest_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
auto thread_state = guest_context->thread_state;
assert_not_zero(target_address);
auto fn = thread_state->processor()->ResolveFunction(
static_cast<uint32_t>(target_address));
if (!fn) {
// Unresolvable — return 0 which will fault.
return 0;
}
auto guest_fn = static_cast<GuestFunction*>(fn);
auto code = guest_fn->machine_code();
if (!code) {
return 0;
}
return reinterpret_cast<uint64_t>(code);
}
// ==========================================================================
// A64Backend
// ==========================================================================
// ARM64 guest trampoline template.
// Loads proc, userdata1, userdata2 into x0-x2, then jumps to guest_to_host
// thunk via x9. Each 64-bit immediate uses movz + 3x movk (16 bytes).
// Total: 4 registers × 16 bytes + 4 bytes (br x9) = 68 bytes.
//
// Template layout (offsets where 64-bit immediates are patched):
// +0x00: movz x0, #imm16; movk x0, ..., lsl 16/32/48 -> proc
// +0x10: movz x1, #imm16; movk x1, ..., lsl 16/32/48 -> userdata1
// +0x20: movz x2, #imm16; movk x2, ..., lsl 16/32/48 -> userdata2
// +0x30: movz x9, #imm16; movk x9, ..., lsl 16/32/48 -> g2h thunk
// +0x40: br x9
//
// ARM64 encoding helpers:
// movz xN, #imm16 = 0xD2800000 | (imm16 << 5) | N
// movk xN, #imm16, lsl #S = 0xF2800000 | (hw << 21) | (imm16 << 5) | N
// where hw = S/16 (0,1,2,3)
static void EncodeMovImm64(uint32_t* out, uint32_t reg, uint64_t imm) {
out[0] = 0xD2800000 | (static_cast<uint32_t>(imm & 0xFFFF) << 5) | reg;
out[1] =
0xF2A00000 | (static_cast<uint32_t>((imm >> 16) & 0xFFFF) << 5) | reg;
out[2] =
0xF2C00000 | (static_cast<uint32_t>((imm >> 32) & 0xFFFF) << 5) | reg;
out[3] =
0xF2E00000 | (static_cast<uint32_t>((imm >> 48) & 0xFFFF) << 5) | reg;
}
static constexpr size_t kGuestTrampolineSize = 68; // 17 instructions × 4
static constexpr uint32_t kTrampolineOffsetProc = 0x00;
static constexpr uint32_t kTrampolineOffsetArg1 = 0x10;
static constexpr uint32_t kTrampolineOffsetArg2 = 0x20;
static constexpr uint32_t kTrampolineOffsetThunk = 0x30;
static void BuildGuestTrampoline(uint8_t* buf, void* proc, void* userdata1,
void* userdata2, void* g2h_thunk) {
auto* code = reinterpret_cast<uint32_t*>(buf);
// x0 = proc (target function for guest-to-host thunk)
EncodeMovImm64(&code[0], 0, reinterpret_cast<uint64_t>(proc));
// x1 = userdata1
EncodeMovImm64(&code[4], 1, reinterpret_cast<uint64_t>(userdata1));
// x2 = userdata2
EncodeMovImm64(&code[8], 2, reinterpret_cast<uint64_t>(userdata2));
// x9 = guest_to_host_thunk
EncodeMovImm64(&code[12], 9, reinterpret_cast<uint64_t>(g2h_thunk));
// br x9
code[16] = 0xD61F0120; // br x9
}
A64Backend::A64Backend() {
code_cache_ = A64CodeCache::Create();
// Allocate executable memory for guest trampolines.
uint32_t base_address = 0x10000;
void* buf = nullptr;
while (base_address < 0x80000000) {
buf = memory::AllocFixed(
reinterpret_cast<void*>(static_cast<uintptr_t>(base_address)),
kGuestTrampolineSize * MAX_GUEST_TRAMPOLINES,
xe::memory::AllocationType::kReserveCommit,
xe::memory::PageAccess::kExecuteReadWrite);
if (!buf) {
base_address += 65536;
} else {
break;
}
}
xenia_assert(buf);
guest_trampoline_memory_ = reinterpret_cast<uint8_t*>(buf);
guest_trampoline_address_bitmap_.Resize(MAX_GUEST_TRAMPOLINES);
}
A64Backend::~A64Backend() {
ExceptionHandler::Uninstall(&ExceptionCallbackThunk, this);
if (guest_trampoline_memory_) {
memory::DeallocFixed(guest_trampoline_memory_,
kGuestTrampolineSize * MAX_GUEST_TRAMPOLINES,
memory::DeallocationType::kRelease);
guest_trampoline_memory_ = nullptr;
}
}
bool A64Backend::Initialize(Processor* processor) {
if (!Backend::Initialize(processor)) {
return false;
}
// Initialize the code cache.
if (!code_cache_->Initialize()) {
XELOGE("A64Backend: Failed to initialize code cache");
return false;
}
// Expose the code cache to the base Backend class.
Backend::code_cache_ = code_cache_.get();
// Set up machine info for the register allocator.
machine_info_.supports_extended_load_store = true;
// GPR set: x19, x22-x28 (8 registers, excluding x20=context, x21=membase)
auto& gpr_set = machine_info_.register_sets[0];
gpr_set.id = 0;
std::strcpy(gpr_set.name, "gpr");
gpr_set.types = MachineInfo::RegisterSet::INT_TYPES;
gpr_set.count = A64Emitter::GPR_COUNT;
// VEC set: v4-v7, v16-v31 (20 registers, v0-v3 scratch)
auto& vec_set = machine_info_.register_sets[1];
vec_set.id = 1;
std::strcpy(vec_set.name, "vec");
vec_set.types = MachineInfo::RegisterSet::FLOAT_TYPES |
MachineInfo::RegisterSet::VEC_TYPES;
vec_set.count = A64Emitter::VEC_COUNT;
// Generate thunks using ARM64 assembler.
XbyakA64Allocator allocator;
A64HelperEmitter thunk_emitter(this, &allocator);
host_to_guest_thunk_ = thunk_emitter.EmitHostToGuestThunk();
guest_to_host_thunk_ = thunk_emitter.EmitGuestToHostThunk();
resolve_function_thunk_ = thunk_emitter.EmitResolveFunctionThunk();
if (!host_to_guest_thunk_ || !guest_to_host_thunk_ ||
!resolve_function_thunk_) {
XELOGE("A64Backend: Failed to generate thunks");
return false;
}
if (cvars::a64_enable_host_guest_stack_synchronization) {
synchronize_guest_and_host_stack_helper_ =
thunk_emitter.EmitGuestAndHostSynchronizeStackHelper();
}
// Set the indirection table default to point at the resolve thunk.
code_cache_->set_indirection_default(
uint32_t(reinterpret_cast<uint64_t>(resolve_function_thunk_)));
// Commit the indirection table range used by guest trampolines so that
// CreateGuestTrampoline can call AddIndirection without faulting.
code_cache_->CommitExecutableRange(GUEST_TRAMPOLINE_BASE,
GUEST_TRAMPOLINE_END);
// Commit special indirection ranges (force return address, etc.).
code_cache_->CommitExecutableRange(0x9FFF0000, 0x9FFFFFFF);
// Register exception handler for MMIO access from JIT code.
ExceptionHandler::Install(ExceptionCallbackThunk, this);
return true;
}
void A64Backend::CommitExecutableRange(uint32_t guest_low,
uint32_t guest_high) {
code_cache_->CommitExecutableRange(guest_low, guest_high);
}
std::unique_ptr<Assembler> A64Backend::CreateAssembler() {
return std::make_unique<A64Assembler>(this);
}
std::unique_ptr<GuestFunction> A64Backend::CreateGuestFunction(
Module* module, uint32_t address) {
return std::make_unique<A64Function>(module, address);
}
uint64_t A64Backend::CalculateNextHostInstruction(ThreadDebugInfo* thread_info,
uint64_t current_pc) {
// ARM64 instructions are fixed 4 bytes.
return current_pc + 4;
}
// ARM64 BRK #0 encoding (4 bytes, fixed-width instruction).
static constexpr uint32_t kArm64Brk0 = 0xD4200000;
void A64Backend::InstallBreakpoint(Breakpoint* breakpoint) {
breakpoint->ForEachHostAddress([breakpoint](uint64_t host_address) {
auto ptr = reinterpret_cast<void*>(host_address);
auto original_bytes = xe::load<uint32_t>(ptr);
assert_true(original_bytes != kArm64Brk0);
xe::store<uint32_t>(ptr, kArm64Brk0);
breakpoint->backend_data().emplace_back(host_address, original_bytes);
});
}
void A64Backend::InstallBreakpoint(Breakpoint* breakpoint, Function* fn) {
assert_true(breakpoint->address_type() == Breakpoint::AddressType::kGuest);
assert_true(fn->is_guest());
auto guest_function = reinterpret_cast<cpu::GuestFunction*>(fn);
auto host_address =
guest_function->MapGuestAddressToMachineCode(breakpoint->guest_address());
if (!host_address) {
assert_always();
return;
}
auto ptr = reinterpret_cast<void*>(host_address);
auto original_bytes = xe::load<uint32_t>(ptr);
assert_true(original_bytes != kArm64Brk0);
xe::store<uint32_t>(ptr, kArm64Brk0);
breakpoint->backend_data().emplace_back(host_address, original_bytes);
}
void A64Backend::UninstallBreakpoint(Breakpoint* breakpoint) {
for (auto& pair : breakpoint->backend_data()) {
auto ptr = reinterpret_cast<uint8_t*>(pair.first);
auto instruction_bytes = xe::load<uint32_t>(ptr);
assert_true(instruction_bytes == kArm64Brk0);
xe::store<uint32_t>(ptr, static_cast<uint32_t>(pair.second));
}
breakpoint->backend_data().clear();
}
void A64Backend::InitializeBackendContext(void* ctx) {
auto* a64_ctx = BackendContextForGuestContext(ctx);
std::memset(a64_ctx, 0, sizeof(A64BackendContext));
a64_ctx->reserve_helper_ = &reserve_helper_;
a64_ctx->Ox1000 = 0x1000;
a64_ctx->fpcr_fpu = DEFAULT_FPU_FPCR;
a64_ctx->fpcr_vmx = DEFAULT_VMX_FPCR;
a64_ctx->flags = (1U << kA64BackendNJMOn); // NJM on by default
a64_ctx->guest_tick_count = Clock::GetGuestTickCountPointer();
// Allocate stackpoints for longjmp detection.
if (cvars::a64_enable_host_guest_stack_synchronization) {
uint64_t max_stackpoints = cvars::a64_max_stackpoints;
if (max_stackpoints > 0) {
a64_ctx->stackpoints = new A64BackendStackpoint[max_stackpoints]();
}
}
}
void A64Backend::DeinitializeBackendContext(void* ctx) {
auto* a64_ctx = BackendContextForGuestContext(ctx);
if (a64_ctx->stackpoints) {
delete[] a64_ctx->stackpoints;
a64_ctx->stackpoints = nullptr;
}
}
void A64Backend::PrepareForReentry(void* ctx) {
auto* a64_ctx = BackendContextForGuestContext(ctx);
a64_ctx->current_stackpoint_depth = 0;
}
uint32_t A64Backend::CreateGuestTrampoline(GuestTrampolineProc proc,
void* userdata1, void* userdata2,
bool long_term) {
size_t new_index;
if (long_term) {
new_index = guest_trampoline_address_bitmap_.AcquireFromBack();
} else {
new_index = guest_trampoline_address_bitmap_.Acquire();
}
xenia_assert(new_index != static_cast<size_t>(-1));
uint8_t* write_pos =
&guest_trampoline_memory_[kGuestTrampolineSize * new_index];
BuildGuestTrampoline(write_pos, reinterpret_cast<void*>(proc), userdata1,
userdata2,
reinterpret_cast<void*>(guest_to_host_thunk_));
// Flush instruction cache for the new trampoline code.
#if XE_PLATFORM_WIN32
FlushInstructionCache(GetCurrentProcess(), write_pos, kGuestTrampolineSize);
#else
__builtin___clear_cache(
reinterpret_cast<char*>(write_pos),
reinterpret_cast<char*>(write_pos + kGuestTrampolineSize));
#endif
uint32_t indirection_guest_addr =
GUEST_TRAMPOLINE_BASE +
(static_cast<uint32_t>(new_index) * GUEST_TRAMPOLINE_MIN_LEN);
code_cache()->AddIndirection(
indirection_guest_addr,
static_cast<uint32_t>(reinterpret_cast<uintptr_t>(write_pos)));
return indirection_guest_addr;
}
void A64Backend::FreeGuestTrampoline(uint32_t trampoline_addr) {
xenia_assert(trampoline_addr >= GUEST_TRAMPOLINE_BASE &&
trampoline_addr < GUEST_TRAMPOLINE_END);
size_t index =
(trampoline_addr - GUEST_TRAMPOLINE_BASE) / GUEST_TRAMPOLINE_MIN_LEN;
guest_trampoline_address_bitmap_.Release(index);
}
// PPC rounding mode (3-bit) to ARM64 FPCR value.
// Same table as in a64_sequences.cc SET_ROUNDING_MODE.
static constexpr uint32_t fpcr_table[8] = {
(0b00 << 22), // PPC 0: nearest, IEEE
(0b11 << 22), // PPC 1: toward zero, IEEE
(0b01 << 22), // PPC 2: toward +inf, IEEE
(0b10 << 22), // PPC 3: toward -inf, IEEE
(0b00 << 22) | (1 << 24), // PPC 4: nearest, flush-to-zero
(0b11 << 22) | (1 << 24), // PPC 5: toward zero, flush-to-zero
(0b01 << 22) | (1 << 24), // PPC 6: toward +inf, flush-to-zero
(0b10 << 22) | (1 << 24), // PPC 7: toward -inf, flush-to-zero
};
void A64Backend::SetGuestRoundingMode(void* ctx, unsigned int mode) {
A64BackendContext* bctx = BackendContextForGuestContext(ctx);
uint32_t control = mode & 7;
uint32_t fpcr_val = fpcr_table[control];
#if XE_ARCH_ARM64
#if XE_COMPILER_MSVC
// MSVC ARM64 intrinsic: ARM64_FPCR = register ID 0x5A20.
_WriteStatusReg(0x5A20, static_cast<uint64_t>(fpcr_val));
#else
__asm__ volatile("msr fpcr, %0" : : "r"(static_cast<uint64_t>(fpcr_val)));
#endif
#endif
bctx->fpcr_fpu = fpcr_val;
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(ctx);
ppc_context->fpscr.bits.rn = control;
ppc_context->fpscr.bits.ni = control >> 2;
}
bool A64Backend::PopulatePseudoStacktrace(GuestPseudoStackTrace* st) {
ThreadState* thrd_state = ThreadState::Get();
if (!thrd_state) {
return false;
}
ppc::PPCContext* ctx = thrd_state->context();
A64BackendContext* backend_ctx = BackendContextForGuestContext(ctx);
uint32_t depth = backend_ctx->current_stackpoint_depth - 1;
if (static_cast<int32_t>(depth) < 1) {
return false;
}
uint32_t num_entries_to_populate =
std::min(MAX_GUEST_PSEUDO_STACKTRACE_ENTRIES, depth);
st->count = num_entries_to_populate;
st->truncated_flag = num_entries_to_populate < depth ? 1 : 0;
A64BackendStackpoint* current_stackpoint =
&backend_ctx->stackpoints[backend_ctx->current_stackpoint_depth - 1];
for (uint32_t stp_index = 0; stp_index < num_entries_to_populate;
++stp_index) {
st->return_addrs[stp_index] = current_stackpoint->guest_return_address_;
current_stackpoint--;
}
return true;
}
void A64Backend::RecordMMIOExceptionForGuestInstruction(void* host_address) {
uint64_t host_addr_u64 = reinterpret_cast<uint64_t>(host_address);
auto fnfor = code_cache()->LookupFunction(host_addr_u64);
if (fnfor) {
uint32_t guestaddr = fnfor->MapMachineCodeToGuestAddress(host_addr_u64);
Module* guest_module = fnfor->module();
if (guest_module) {
XexModule* xex_guest_module = dynamic_cast<XexModule*>(guest_module);
if (xex_guest_module) {
cpu::InfoCacheFlags* icf =
xex_guest_module->GetInstructionAddressFlags(guestaddr);
if (icf) {
icf->accessed_mmio = true;
}
}
}
}
}
bool A64Backend::ExceptionCallbackThunk(Exception* ex, void* data) {
auto* backend = reinterpret_cast<A64Backend*>(data);
return backend->ExceptionCallback(ex);
}
bool A64Backend::ExceptionCallback(Exception* ex) {
if (ex->code() != Exception::Code::kIllegalInstruction) {
return false;
}
// Verify it's our BRK #0 instruction.
auto instruction_bytes =
xe::load<uint32_t>(reinterpret_cast<void*>(ex->pc()));
if (instruction_bytes != kArm64Brk0) {
return false;
}
return processor()->OnThreadBreakpointHit(ex);
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,175 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_BACKEND_H_
#define XENIA_CPU_BACKEND_A64_A64_BACKEND_H_
#include <memory>
#include "xenia/base/bit_map.h"
#include "xenia/base/cvar.h"
#include "xenia/cpu/backend/backend.h"
namespace xe {
class Exception;
} // namespace xe
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class A64CodeCache;
typedef void* (*HostToGuestThunk)(void* target, void* arg0, void* arg1);
typedef void* (*GuestToHostThunk)(void* target, void* arg0, void* arg1);
typedef void (*ResolveFunctionThunk)();
// Place guest trampolines in an address range that the HV normally occupies.
static constexpr uint32_t GUEST_TRAMPOLINE_BASE = 0x80000000;
static constexpr uint32_t GUEST_TRAMPOLINE_END = 0x80040000;
static constexpr uint32_t GUEST_TRAMPOLINE_MIN_LEN = 8;
static constexpr uint32_t MAX_GUEST_TRAMPOLINES =
(GUEST_TRAMPOLINE_END - GUEST_TRAMPOLINE_BASE) / GUEST_TRAMPOLINE_MIN_LEN;
#define A64_RESERVE_BLOCK_SHIFT 16
#define A64_RESERVE_NUM_ENTRIES \
((1024ULL * 1024ULL * 1024ULL * 4ULL) >> A64_RESERVE_BLOCK_SHIFT)
struct ReserveHelper {
uint64_t blocks[A64_RESERVE_NUM_ENTRIES / 64];
ReserveHelper() { memset(blocks, 0, sizeof(blocks)); }
};
struct A64BackendStackpoint {
uint64_t host_stack_;
unsigned guest_stack_;
unsigned guest_return_address_;
};
enum : uint32_t {
kA64BackendFPCRModeBit = 0,
kA64BackendHasReserveBit = 1,
kA64BackendNJMOn = 2,
kA64BackendNonIEEEMode = 3,
};
// Located prior to the context register (x20) in memory.
struct A64BackendContext {
// Scratch vectors for helper routines.
// Using uint8_t[16] instead of NEON intrinsic types to avoid including
// arm_neon.h in the header.
alignas(16) uint8_t helper_scratch_v128s[4][16];
union {
uint64_t helper_scratch_u64s[8];
uint32_t helper_scratch_u32s[16];
};
ReserveHelper* reserve_helper_;
uint64_t cached_reserve_value_;
uint64_t* guest_tick_count;
A64BackendStackpoint* stackpoints;
uint64_t cached_reserve_offset;
uint32_t cached_reserve_bit;
unsigned int current_stackpoint_depth;
unsigned int fpcr_fpu;
unsigned int fpcr_vmx;
// bit 0 = 0 if fpcr is fpu, else it is vmx
// bit 1 = got reserve
unsigned int flags;
unsigned int Ox1000; // constant 0x1000
};
// Default FPCR for FPU mode (round to nearest, no flush to zero).
constexpr unsigned int DEFAULT_FPU_FPCR = 0;
// Default FPCR for VMX mode (flush to zero, default NaN).
constexpr unsigned int DEFAULT_VMX_FPCR = (1 << 24) | (1 << 25); // FZ | DN
class A64Backend : public Backend {
public:
static constexpr uint32_t kForceReturnAddress = 0x9FFF0000u;
explicit A64Backend();
~A64Backend() override;
A64CodeCache* code_cache() const { return code_cache_.get(); }
uintptr_t emitter_data() const { return emitter_data_; }
HostToGuestThunk host_to_guest_thunk() const { return host_to_guest_thunk_; }
GuestToHostThunk guest_to_host_thunk() const { return guest_to_host_thunk_; }
ResolveFunctionThunk resolve_function_thunk() const {
return resolve_function_thunk_;
}
void* synchronize_guest_and_host_stack_helper() const {
return synchronize_guest_and_host_stack_helper_;
}
bool Initialize(Processor* processor) override;
void CommitExecutableRange(uint32_t guest_low, uint32_t guest_high) override;
std::unique_ptr<Assembler> CreateAssembler() override;
std::unique_ptr<GuestFunction> CreateGuestFunction(Module* module,
uint32_t address) override;
uint64_t CalculateNextHostInstruction(ThreadDebugInfo* thread_info,
uint64_t current_pc) override;
void InstallBreakpoint(Breakpoint* breakpoint) override;
void InstallBreakpoint(Breakpoint* breakpoint, Function* fn) override;
void UninstallBreakpoint(Breakpoint* breakpoint) override;
void InitializeBackendContext(void* ctx) override;
void DeinitializeBackendContext(void* ctx) override;
void PrepareForReentry(void* ctx) override;
A64BackendContext* BackendContextForGuestContext(void* ctx) {
return reinterpret_cast<A64BackendContext*>(
reinterpret_cast<intptr_t>(ctx) - sizeof(A64BackendContext));
}
uint32_t CreateGuestTrampoline(GuestTrampolineProc proc, void* userdata1,
void* userdata2, bool long_term) override;
void FreeGuestTrampoline(uint32_t trampoline_addr) override;
void SetGuestRoundingMode(void* ctx, unsigned int mode) override;
bool PopulatePseudoStacktrace(GuestPseudoStackTrace* st) override;
void RecordMMIOExceptionForGuestInstruction(void* host_address);
private:
static bool ExceptionCallbackThunk(Exception* ex, void* data);
bool ExceptionCallback(Exception* ex);
uintptr_t capstone_handle_ = 0;
std::unique_ptr<A64CodeCache> code_cache_;
uintptr_t emitter_data_ = 0;
HostToGuestThunk host_to_guest_thunk_ = nullptr;
GuestToHostThunk guest_to_host_thunk_ = nullptr;
ResolveFunctionThunk resolve_function_thunk_ = nullptr;
void* synchronize_guest_and_host_stack_helper_ = nullptr;
public:
void* try_acquire_reservation_helper_ = nullptr;
void* reserved_store_32_helper = nullptr;
void* reserved_store_64_helper = nullptr;
private:
alignas(64) ReserveHelper reserve_helper_;
BitMap guest_trampoline_address_bitmap_;
uint8_t* guest_trampoline_memory_ = nullptr;
};
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_BACKEND_H_

View File

@@ -0,0 +1,48 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_code_cache.h"
#include "xenia/base/platform.h"
#if XE_PLATFORM_WIN32
#include "xenia/base/platform_win.h"
#endif
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
bool A64CodeCache::Initialize() { return CodeCacheBase::Initialize(); }
void A64CodeCache::FillCode(void* write_address, size_t size) {
// Fill with BRK #0 (0xD4200000), 4-byte aligned.
constexpr uint32_t kBrk0 = 0xD4200000;
auto* p = reinterpret_cast<uint32_t*>(write_address);
auto* end =
reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(write_address) + size);
for (; p < end; ++p) {
*p = kBrk0;
}
}
void A64CodeCache::FlushCodeRange(void* address, size_t size) {
#if XE_PLATFORM_WIN32
FlushInstructionCache(GetCurrentProcess(), address, size);
#else
__builtin___clear_cache(
reinterpret_cast<char*>(address),
reinterpret_cast<char*>(static_cast<uint8_t*>(address) + size));
#endif
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,54 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_CODE_CACHE_H_
#define XENIA_CPU_BACKEND_A64_A64_CODE_CACHE_H_
#include <memory>
#include "xenia/cpu/backend/code_cache_base.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class A64CodeCache : public CodeCacheBase<A64CodeCache> {
public:
~A64CodeCache() override = default;
static std::unique_ptr<A64CodeCache> Create();
virtual bool Initialize();
void* LookupUnwindInfo(uint64_t host_pc) override { return nullptr; }
// CRTP hooks for CodeCacheBase.
void FillCode(void* write_address, size_t size);
void FlushCodeRange(void* address, size_t size);
// Virtual for platform-specific overrides (_win.cc / _posix.cc).
virtual UnwindReservation RequestUnwindReservation(uint8_t* entry_address) {
return UnwindReservation();
}
virtual void PlaceCode(uint32_t guest_address, void* machine_code,
const EmitFunctionInfo& func_info,
void* code_execute_address,
UnwindReservation unwind_reservation) {}
protected:
A64CodeCache() = default;
};
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_CODE_CACHE_H_

View File

@@ -0,0 +1,325 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_code_cache.h"
#include <cstring>
#include <vector>
#include "xenia/base/assert.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/cpu/backend/a64/a64_stack_layout.h"
// libgcc/libunwind APIs for registering DWARF .eh_frame unwind info.
extern "C" void __register_frame(void*);
extern "C" void __deregister_frame(void*);
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
// Maximum size of DWARF .eh_frame data per function (CIE + FDE + terminator).
static constexpr uint32_t kMaxUnwindInfoSize = 128;
// DWARF register numbers for AArch64.
static constexpr uint8_t kDwarfRegX19 = 19;
static constexpr uint8_t kDwarfRegX20 = 20;
static constexpr uint8_t kDwarfRegX21 = 21;
static constexpr uint8_t kDwarfRegX22 = 22;
static constexpr uint8_t kDwarfRegX23 = 23;
static constexpr uint8_t kDwarfRegX24 = 24;
static constexpr uint8_t kDwarfRegX25 = 25;
static constexpr uint8_t kDwarfRegX26 = 26;
static constexpr uint8_t kDwarfRegX27 = 27;
static constexpr uint8_t kDwarfRegX28 = 28;
static constexpr uint8_t kDwarfRegFP = 29; // x29 / frame pointer
static constexpr uint8_t kDwarfRegLR = 30; // x30 / link register
static constexpr uint8_t kDwarfRegSP = 31; // stack pointer
static constexpr uint8_t kDwarfRegD8 = 72; // d8-d15 are callee-saved
static constexpr uint8_t kDwarfRegD9 = 73;
static constexpr uint8_t kDwarfRegD10 = 74;
static constexpr uint8_t kDwarfRegD11 = 75;
static constexpr uint8_t kDwarfRegD12 = 76;
static constexpr uint8_t kDwarfRegD13 = 77;
static constexpr uint8_t kDwarfRegD14 = 78;
static constexpr uint8_t kDwarfRegD15 = 79;
// DWARF CFA opcodes.
static constexpr uint8_t kDW_CFA_advance_loc1 = 0x02;
static constexpr uint8_t kDW_CFA_advance_loc2 = 0x03;
static constexpr uint8_t kDW_CFA_def_cfa = 0x0c;
static constexpr uint8_t kDW_CFA_def_cfa_offset = 0x0e;
static constexpr uint8_t kDW_CFA_nop = 0x00;
// DWARF pointer encoding constants.
static constexpr uint8_t kDW_EH_PE_pcrel = 0x10;
static constexpr uint8_t kDW_EH_PE_sdata4 = 0x0b;
static size_t WriteULEB128(uint8_t* p, uint64_t value) {
size_t count = 0;
do {
uint8_t byte = value & 0x7F;
value >>= 7;
if (value) byte |= 0x80;
p[count++] = byte;
} while (value);
return count;
}
static size_t WriteSLEB128(uint8_t* p, int64_t value) {
size_t count = 0;
bool more = true;
while (more) {
uint8_t byte = value & 0x7F;
value >>= 7;
if ((value == 0 && !(byte & 0x40)) || (value == -1 && (byte & 0x40))) {
more = false;
} else {
byte |= 0x80;
}
p[count++] = byte;
}
return count;
}
class PosixA64CodeCache : public A64CodeCache {
public:
PosixA64CodeCache();
~PosixA64CodeCache() override;
bool Initialize() override;
void* LookupUnwindInfo(uint64_t host_pc) override { return nullptr; }
private:
UnwindReservation RequestUnwindReservation(uint8_t* entry_address) override;
void PlaceCode(uint32_t guest_address, void* machine_code,
const EmitFunctionInfo& func_info, void* code_execute_address,
UnwindReservation unwind_reservation) override;
void InitializeUnwindEntry(uint8_t* unwind_entry_address,
void* code_execute_address,
const EmitFunctionInfo& func_info);
std::vector<void*> registered_frames_;
uint32_t unwind_table_count_ = 0;
};
std::unique_ptr<A64CodeCache> A64CodeCache::Create() {
return std::make_unique<PosixA64CodeCache>();
}
PosixA64CodeCache::PosixA64CodeCache() = default;
PosixA64CodeCache::~PosixA64CodeCache() {
for (auto frame : registered_frames_) {
__deregister_frame(frame);
}
}
bool PosixA64CodeCache::Initialize() {
if (!A64CodeCache::Initialize()) {
return false;
}
registered_frames_.reserve(kMaximumFunctionCount);
return true;
}
A64CodeCache::UnwindReservation PosixA64CodeCache::RequestUnwindReservation(
uint8_t* entry_address) {
#if defined(NDEBUG)
if (unwind_table_count_ >= kMaximumFunctionCount) {
xe::FatalError(
"Unwind table count exceeded maximum! Please report this to "
"Xenia developers");
}
#else
assert_false(unwind_table_count_ >= kMaximumFunctionCount);
#endif
UnwindReservation unwind_reservation;
unwind_reservation.data_size = xe::round_up(kMaxUnwindInfoSize, 16);
unwind_reservation.table_slot = unwind_table_count_++;
unwind_reservation.entry_address = entry_address;
return unwind_reservation;
}
void PosixA64CodeCache::PlaceCode(uint32_t guest_address, void* machine_code,
const EmitFunctionInfo& func_info,
void* code_execute_address,
UnwindReservation unwind_reservation) {
InitializeUnwindEntry(unwind_reservation.entry_address, code_execute_address,
func_info);
void* unwind_execute_address = unwind_reservation.entry_address -
generated_code_write_base_ +
generated_code_execute_base_;
__register_frame(unwind_execute_address);
registered_frames_.push_back(unwind_execute_address);
}
void PosixA64CodeCache::InitializeUnwindEntry(
uint8_t* unwind_entry_address, void* code_execute_address,
const EmitFunctionInfo& func_info) {
// Compute execute-side base address of the unwind buffer.
uint8_t* unwind_execute_base = unwind_entry_address -
generated_code_write_base_ +
generated_code_execute_base_;
uint8_t* p = unwind_entry_address;
uint8_t* cie_start = p;
// === CIE (Common Information Entry) ===
uint8_t* cie_length_ptr = p;
p += 4;
uint8_t* cie_content_start = p;
// CIE ID = 0.
*reinterpret_cast<uint32_t*>(p) = 0;
p += 4;
// Version = 1.
*p++ = 1;
// Augmentation string "zR".
*p++ = 'z';
*p++ = 'R';
*p++ = '\0';
// Code alignment factor = 4 (ARM64 instructions are 4 bytes).
p += WriteULEB128(p, 4);
// Data alignment factor = -8.
p += WriteSLEB128(p, -8);
// Return address register = x30 (LR).
p += WriteULEB128(p, kDwarfRegLR);
// Augmentation data length = 1.
p += WriteULEB128(p, 1);
// FDE pointer encoding: pc-relative, signed 32-bit.
*p++ = kDW_EH_PE_pcrel | kDW_EH_PE_sdata4;
// Initial instructions:
// DW_CFA_def_cfa SP, 0 — at function entry, CFA = SP.
*p++ = kDW_CFA_def_cfa;
p += WriteULEB128(p, kDwarfRegSP);
p += WriteULEB128(p, 0);
// Pad CIE to pointer-size (8-byte) alignment.
size_t cie_content_len = static_cast<size_t>(p - cie_content_start);
size_t cie_padded_len = xe::round_up(cie_content_len, sizeof(void*));
while (p < cie_content_start + cie_padded_len) {
*p++ = kDW_CFA_nop;
}
*reinterpret_cast<uint32_t*>(cie_length_ptr) =
static_cast<uint32_t>(p - cie_content_start);
// === FDE (Frame Description Entry) ===
uint8_t* fde_length_ptr = p;
p += 4;
uint8_t* fde_content_start = p;
// CIE pointer.
*reinterpret_cast<uint32_t*>(p) = static_cast<uint32_t>(p - cie_start);
p += 4;
// PC begin (pc-relative).
uint8_t* pc_begin_execute_addr =
unwind_execute_base + (p - unwind_entry_address);
*reinterpret_cast<int32_t*>(p) =
static_cast<int32_t>(reinterpret_cast<intptr_t>(code_execute_address) -
reinterpret_cast<intptr_t>(pc_begin_execute_addr));
p += 4;
// PC range.
*reinterpret_cast<uint32_t*>(p) =
static_cast<uint32_t>(func_info.code_size.total);
p += 4;
// Augmentation data length = 0.
p += WriteULEB128(p, 0);
// FDE instructions.
if (func_info.stack_size > 0) {
// Advance to the instruction after the stack allocation.
size_t alloc_offset = func_info.prolog_stack_alloc_offset;
if (alloc_offset > 0) {
// ARM64 code alignment factor is 4, so divide by 4.
uint32_t factored_offset = static_cast<uint32_t>(alloc_offset / 4);
if (factored_offset < 64) {
*p++ = 0x40 | static_cast<uint8_t>(factored_offset);
} else if (factored_offset < 256) {
*p++ = kDW_CFA_advance_loc1;
*p++ = static_cast<uint8_t>(factored_offset);
} else {
*p++ = kDW_CFA_advance_loc2;
*reinterpret_cast<uint16_t*>(p) =
static_cast<uint16_t>(factored_offset);
p += 2;
}
}
// DW_CFA_def_cfa_offset: CFA = SP + stack_size.
*p++ = kDW_CFA_def_cfa_offset;
p += WriteULEB128(p, func_info.stack_size);
// For thunk functions, encode callee-saved register save locations.
if (func_info.stack_size == StackLayout::THUNK_STACK_SIZE) {
size_t cfa = func_info.stack_size;
// x19 at sp+0x000
*p++ = 0x80 | kDwarfRegX19;
p += WriteULEB128(p, (cfa - 0x000) / 8);
// x20 at sp+0x008
*p++ = 0x80 | kDwarfRegX20;
p += WriteULEB128(p, (cfa - 0x008) / 8);
// x21 at sp+0x010
*p++ = 0x80 | kDwarfRegX21;
p += WriteULEB128(p, (cfa - 0x010) / 8);
// x29 at sp+0x050
*p++ = 0x80 | kDwarfRegFP;
p += WriteULEB128(p, (cfa - 0x050) / 8);
// x30 at sp+0x058
*p++ = 0x80 | kDwarfRegLR;
p += WriteULEB128(p, (cfa - 0x058) / 8);
}
}
// Pad FDE.
size_t fde_content_len = static_cast<size_t>(p - fde_content_start);
size_t fde_padded_len = xe::round_up(fde_content_len, sizeof(void*));
while (p < fde_content_start + fde_padded_len) {
*p++ = kDW_CFA_nop;
}
*reinterpret_cast<uint32_t*>(fde_length_ptr) =
static_cast<uint32_t>(p - fde_content_start);
// === Terminator ===
*reinterpret_cast<uint32_t*>(p) = 0;
p += 4;
assert_true(static_cast<size_t>(p - unwind_entry_address) <=
kMaxUnwindInfoSize);
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,419 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_code_cache.h"
#include <cstdlib>
#include <cstring>
#include "xenia/base/assert.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/base/platform_win.h"
#include "xenia/cpu/backend/a64/a64_stack_layout.h"
#include "xenia/cpu/function.h"
// Function pointer definitions for growable function tables.
using FnRtlAddGrowableFunctionTable = decltype(&RtlAddGrowableFunctionTable);
using FnRtlGrowFunctionTable = decltype(&RtlGrowFunctionTable);
using FnRtlDeleteGrowableFunctionTable =
decltype(&RtlDeleteGrowableFunctionTable);
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
// ARM64 .xdata unwind codes.
// See: https://learn.microsoft.com/en-us/cpp/build/arm64-exception-handling
//
// Codes are stored as a byte array (big-endian for multi-byte codes), listed
// in reverse prolog order (last prolog instruction's code first).
//
// For the thunk prolog:
// sub sp, sp, #0xA0 (alloc_s or alloc_m)
// stp x19,x20, [sp, #0x00] (save_regp)
// stp x21,x22, [sp, #0x10] (save_regp)
// stp x23,x24, [sp, #0x20] (save_regp)
// stp x25,x26, [sp, #0x30] (save_regp)
// stp x27,x28, [sp, #0x40] (save_regp)
// stp x29,x30, [sp, #0x50] (save_fplr)
// stp d8, d9, [sp, #0x60] (save_fregp)
// stp d10,d11, [sp, #0x70] (save_fregp)
// stp d12,d13, [sp, #0x80] (save_fregp)
// stp d14,d15, [sp, #0x90] (save_fregp)
// ARM64 unwind code builders.
// alloc_s: 000XXXXX, allocate X*16 bytes (0..496).
static void EmitAllocS(uint8_t* buf, size_t& off, uint32_t size_bytes) {
assert_true(size_bytes <= 496 && (size_bytes % 16) == 0);
buf[off++] = static_cast<uint8_t>(size_bytes / 16);
}
// alloc_m: 11000XXX XXXXXXXX, allocate X*16 bytes (0..32752).
static void EmitAllocM(uint8_t* buf, size_t& off, uint32_t size_bytes) {
assert_true(size_bytes <= 32752 && (size_bytes % 16) == 0);
uint16_t val = static_cast<uint16_t>(size_bytes / 16);
buf[off++] = static_cast<uint8_t>(0xC0 | ((val >> 8) & 0x07));
buf[off++] = static_cast<uint8_t>(val & 0xFF);
}
// save_regp: 110010XX XXzzzzzz
// Save x(19+X), x(20+X) pair at [sp + Z*8].
// X is the register offset from x19 (not a pair ordinal).
// e.g., x21,x22 -> X=2, x27,x28 -> X=8.
static void EmitSaveRegp(uint8_t* buf, size_t& off, uint32_t reg_offset,
uint32_t sp_offset) {
assert_true(reg_offset <= 10);
assert_true((sp_offset % 8) == 0 && sp_offset / 8 <= 63);
uint32_t z = sp_offset / 8;
buf[off++] = static_cast<uint8_t>(0xC8 | ((reg_offset >> 2) & 0x03));
buf[off++] = static_cast<uint8_t>(((reg_offset & 0x03) << 6) | (z & 0x3F));
}
// save_fplr: 01zzzzzz
// Save <x29, lr> at [sp + Z*8].
static void EmitSaveFplr(uint8_t* buf, size_t& off, uint32_t sp_offset) {
assert_true((sp_offset % 8) == 0 && sp_offset / 8 <= 63);
buf[off++] = static_cast<uint8_t>(0x40 | (sp_offset / 8));
}
// save_fregp: 1101100X XXzzzzzz
// Save d(8+X), d(9+X) pair at [sp + Z*8].
// X is the register offset from d8 (not a pair ordinal).
// e.g., d10,d11 -> X=2, d14,d15 -> X=6.
static void EmitSaveFregp(uint8_t* buf, size_t& off, uint32_t reg_offset,
uint32_t sp_offset) {
assert_true(reg_offset <= 7);
assert_true((sp_offset % 8) == 0 && sp_offset / 8 <= 63);
uint32_t z = sp_offset / 8;
buf[off++] = static_cast<uint8_t>(0xD8 | ((reg_offset >> 2) & 0x01));
buf[off++] = static_cast<uint8_t>(((reg_offset & 0x03) << 6) | (z & 0x3F));
}
// end: 0xE4
static void EmitEnd(uint8_t* buf, size_t& off) { buf[off++] = 0xE4; }
// Build the .xdata unwind codes for a thunk prolog that saves callee-saved
// registers at known offsets (see StackLayout in a64_stack_layout.h).
// Returns the number of bytes written.
static size_t BuildThunkUnwindCodes(uint8_t* buf) {
size_t off = 0;
// Codes listed in reverse prolog order (last prolog instruction first).
// stp d14, d15, [sp, #0x90] — d14 = d(8+6)
EmitSaveFregp(buf, off, 6, 0x90);
// stp d12, d13, [sp, #0x80] — d12 = d(8+4)
EmitSaveFregp(buf, off, 4, 0x80);
// stp d10, d11, [sp, #0x70] — d10 = d(8+2)
EmitSaveFregp(buf, off, 2, 0x70);
// stp d8, d9, [sp, #0x60] — d8 = d(8+0)
EmitSaveFregp(buf, off, 0, 0x60);
// stp x29, x30, [sp, #0x50]
EmitSaveFplr(buf, off, 0x50);
// stp x27, x28, [sp, #0x40] — x27 = x(19+8)
EmitSaveRegp(buf, off, 8, 0x40);
// stp x25, x26, [sp, #0x30] — x25 = x(19+6)
EmitSaveRegp(buf, off, 6, 0x30);
// stp x23, x24, [sp, #0x20] — x23 = x(19+4)
EmitSaveRegp(buf, off, 4, 0x20);
// stp x21, x22, [sp, #0x10] — x21 = x(19+2)
EmitSaveRegp(buf, off, 2, 0x10);
// stp x19, x20, [sp, #0x00] — x19 = x(19+0)
EmitSaveRegp(buf, off, 0, 0x00);
// sub sp, sp, #0xA0 (160 bytes)
EmitAllocS(buf, off, StackLayout::THUNK_STACK_SIZE);
EmitEnd(buf, off);
return off;
}
// Build minimal unwind codes for a guest function prolog.
// Guest functions only do: sub sp, sp, #N; str x30, [sp, #64]
// The callee-saved registers were already saved by the thunk.
static size_t BuildGuestUnwindCodes(uint8_t* buf, uint32_t stack_size) {
size_t off = 0;
// The guest function stores x30 at [sp + HOST_RET_ADDR] via STR, not STP.
// Windows unwinder needs to know where LR is to unwind. We encode this as
// save_lrpair — but there's no single-register LR save opcode on ARM64.
// Instead we describe the stack allocation only. The host return address
// is stored by the JIT but is not a callee-save operation (it's the thunk's
// LR, not the guest function's). The unwinder will walk up to the thunk
// frame which has the full unwind info.
if (stack_size <= 496) {
EmitAllocS(buf, off, stack_size);
} else {
EmitAllocM(buf, off, stack_size);
}
EmitEnd(buf, off);
return off;
}
// Size of .xdata record for a thunk (header + codes + padding).
// Thunk codes: 5x save_regp(2B) + 1x save_fplr(1B) + 4x save_fregp(2B) +
// 1x alloc_s(1B) + end(1B) = 21 bytes -> 24 bytes padded -> 6
// code words. Header is 1 word. Total: 7 words = 28 bytes.
static constexpr uint32_t kThunkXdataSize = 28;
// Size of .xdata record for a guest function (header + codes + padding).
// Guest codes: alloc_s(1B) or alloc_m(2B) + end(1B) = 2-3 bytes -> 4 bytes
// padded -> 1 code word. Header is 1 word. Total: 2 words = 8
// bytes. We reserve the larger case.
static constexpr uint32_t kGuestXdataSize = 12;
// Compute the maximum unwind data size for any function.
static constexpr uint32_t kMaxUnwindSize = kThunkXdataSize;
// Build a complete .xdata record. Returns the total size written.
static size_t BuildXdataRecord(uint8_t* xdata, uint32_t func_length_bytes,
const uint8_t* codes, size_t codes_length) {
// Pad codes to 4-byte boundary.
size_t codes_padded = xe::round_up(codes_length, size_t{4});
uint32_t code_words = static_cast<uint32_t>(codes_padded / 4);
assert_true(code_words <= 31);
assert_true(func_length_bytes % 4 == 0);
uint32_t func_len_div4 = func_length_bytes / 4;
assert_true(func_len_div4 <= 0x3FFFF);
// Header word:
// bits 0-17: Function Length / 4
// bits 18-19: Version = 0
// bit 20: X = 0 (no exception handler)
// bit 21: E = 1 (single epilog, packed in header)
// bits 22-26: Epilog start index (0 = epilog uses same codes from start)
// bits 27-31: Code Words
uint32_t header = (func_len_div4 & 0x3FFFF) | (0u << 18) // Vers = 0
| (0u << 20) // X = 0
| (1u << 21) // E = 1
| (0u << 22) // Epilog start index = 0
| (code_words << 27);
std::memcpy(xdata, &header, 4);
// Write codes, zero-padded to code_words * 4 bytes.
std::memset(xdata + 4, 0, codes_padded);
std::memcpy(xdata + 4, codes, codes_length);
return 4 + codes_padded;
}
class Win32A64CodeCache : public A64CodeCache {
public:
Win32A64CodeCache();
~Win32A64CodeCache() override;
bool Initialize() override;
void* LookupUnwindInfo(uint64_t host_pc) override;
private:
UnwindReservation RequestUnwindReservation(uint8_t* entry_address) override;
void PlaceCode(uint32_t guest_address, void* machine_code,
const EmitFunctionInfo& func_info, void* code_execute_address,
UnwindReservation unwind_reservation) override;
void InitializeUnwindEntry(uint8_t* unwind_entry_address,
size_t unwind_table_slot,
void* code_execute_address,
const EmitFunctionInfo& func_info);
// Growable function table system handle.
void* unwind_table_handle_ = nullptr;
// Actual unwind table entries.
std::vector<RUNTIME_FUNCTION> unwind_table_;
// End addresses for each entry (ARM64 RUNTIME_FUNCTION lacks EndAddress).
std::vector<DWORD> unwind_table_end_address_;
// Current number of entries in the table.
std::atomic<uint32_t> unwind_table_count_ = {0};
// Does this version of Windows support growable function tables?
bool supports_growable_table_ = false;
FnRtlAddGrowableFunctionTable add_growable_table_ = nullptr;
FnRtlDeleteGrowableFunctionTable delete_growable_table_ = nullptr;
FnRtlGrowFunctionTable grow_table_ = nullptr;
};
std::unique_ptr<A64CodeCache> A64CodeCache::Create() {
return std::make_unique<Win32A64CodeCache>();
}
Win32A64CodeCache::Win32A64CodeCache() = default;
Win32A64CodeCache::~Win32A64CodeCache() {
if (supports_growable_table_) {
if (unwind_table_handle_) {
delete_growable_table_(unwind_table_handle_);
}
} else {
if (generated_code_execute_base_) {
RtlDeleteFunctionTable(reinterpret_cast<PRUNTIME_FUNCTION>(
reinterpret_cast<DWORD64>(generated_code_execute_base_) | 0x3));
}
}
}
bool Win32A64CodeCache::Initialize() {
if (!A64CodeCache::Initialize()) {
return false;
}
// Allocate unwind table with maximum entry count.
unwind_table_.resize(kMaximumFunctionCount);
unwind_table_end_address_.resize(kMaximumFunctionCount);
// Check if this version of Windows supports growable function tables.
auto ntdll_handle = GetModuleHandleW(L"ntdll.dll");
if (!ntdll_handle) {
add_growable_table_ = nullptr;
delete_growable_table_ = nullptr;
grow_table_ = nullptr;
} else {
add_growable_table_ = (FnRtlAddGrowableFunctionTable)GetProcAddress(
ntdll_handle, "RtlAddGrowableFunctionTable");
delete_growable_table_ = (FnRtlDeleteGrowableFunctionTable)GetProcAddress(
ntdll_handle, "RtlDeleteGrowableFunctionTable");
grow_table_ = (FnRtlGrowFunctionTable)GetProcAddress(
ntdll_handle, "RtlGrowFunctionTable");
}
supports_growable_table_ =
add_growable_table_ && delete_growable_table_ && grow_table_;
if (supports_growable_table_) {
if (add_growable_table_(
&unwind_table_handle_, unwind_table_.data(), unwind_table_count_,
DWORD(unwind_table_.size()),
reinterpret_cast<ULONG_PTR>(generated_code_execute_base_),
reinterpret_cast<ULONG_PTR>(generated_code_execute_base_ +
kGeneratedCodeSize))) {
XELOGE("Unable to create unwind function table");
return false;
}
} else {
if (!RtlInstallFunctionTableCallback(
reinterpret_cast<DWORD64>(generated_code_execute_base_) | 0x3,
reinterpret_cast<DWORD64>(generated_code_execute_base_),
kGeneratedCodeSize,
[](DWORD64 control_pc, PVOID context) {
auto code_cache = reinterpret_cast<Win32A64CodeCache*>(context);
return reinterpret_cast<PRUNTIME_FUNCTION>(
code_cache->LookupUnwindInfo(control_pc));
},
this, nullptr)) {
XELOGE("Unable to install function table callback");
return false;
}
}
return true;
}
Win32A64CodeCache::UnwindReservation
Win32A64CodeCache::RequestUnwindReservation(uint8_t* entry_address) {
#if defined(NDEBUG)
if (unwind_table_count_ >= kMaximumFunctionCount) {
xe::FatalError(
"Unwind table count exceeded maximum! Please report this to "
"Xenia/Canary developers");
}
#else
assert_false(unwind_table_count_ >= kMaximumFunctionCount);
#endif
UnwindReservation unwind_reservation;
unwind_reservation.data_size = xe::round_up(kMaxUnwindSize, size_t{16});
unwind_reservation.table_slot = unwind_table_count_++;
unwind_reservation.entry_address = entry_address;
return unwind_reservation;
}
void Win32A64CodeCache::PlaceCode(uint32_t guest_address, void* machine_code,
const EmitFunctionInfo& func_info,
void* code_execute_address,
UnwindReservation unwind_reservation) {
// Add unwind info.
InitializeUnwindEntry(unwind_reservation.entry_address,
unwind_reservation.table_slot, code_execute_address,
func_info);
if (supports_growable_table_) {
grow_table_(unwind_table_handle_, unwind_table_count_);
}
FlushInstructionCache(GetCurrentProcess(), code_execute_address,
func_info.code_size.total);
}
void Win32A64CodeCache::InitializeUnwindEntry(
uint8_t* unwind_entry_address, size_t unwind_table_slot,
void* code_execute_address, const EmitFunctionInfo& func_info) {
uint8_t codes[32];
size_t codes_length;
// Guest function prologs are exactly 4 instructions (16 bytes):
// sub sp, sp, #N; str x30, [sp, #64]; str x0, [sp, #48]; str xzr, [sp, #56]
// The HostToGuest thunk has a much larger prolog that saves all
// callee-saved registers. We detect it by stack size.
// Other thunks (GuestToHost, ResolveFunction) have different layouts
// but are only called from within JIT'd code; stack-alloc-only unwind
// info is sufficient for them since the unwinder will walk up to the
// HostToGuest frame which has full unwind info.
bool is_host_to_guest_thunk =
(func_info.stack_size == StackLayout::THUNK_STACK_SIZE &&
func_info.code_size.prolog > 16);
if (is_host_to_guest_thunk) {
codes_length = BuildThunkUnwindCodes(codes);
} else if (func_info.stack_size > 0) {
codes_length = BuildGuestUnwindCodes(
codes, static_cast<uint32_t>(func_info.stack_size));
} else {
// Thunks with no stack allocation (e.g. GuestToHost, ResolveFunction)
// still need minimal unwind info — emit empty unwind codes.
codes_length = 0;
}
size_t xdata_size = BuildXdataRecord(
unwind_entry_address, static_cast<uint32_t>(func_info.code_size.total),
codes, codes_length);
// Add RUNTIME_FUNCTION entry.
// ARM64 RUNTIME_FUNCTION has BeginAddress and UnwindData but no EndAddress
// (the function length is encoded in the .xdata header).
auto& fn_entry = unwind_table_[unwind_table_slot];
fn_entry.BeginAddress =
DWORD(reinterpret_cast<uint8_t*>(code_execute_address) -
generated_code_execute_base_);
fn_entry.UnwindData =
DWORD(unwind_entry_address - generated_code_execute_base_);
// Store end address in parallel array for LookupUnwindInfo.
unwind_table_end_address_[unwind_table_slot] =
DWORD(fn_entry.BeginAddress + func_info.code_size.total);
}
void* Win32A64CodeCache::LookupUnwindInfo(uint64_t host_pc) {
// ARM64 RUNTIME_FUNCTION lacks EndAddress, so we do a manual binary search
// using our parallel end address array.
uint32_t key = static_cast<uint32_t>(host_pc - kGeneratedCodeExecuteBase);
uint32_t count = unwind_table_count_;
uint32_t lo = 0, hi = count;
while (lo < hi) {
uint32_t mid = lo + (hi - lo) / 2;
if (key < unwind_table_[mid].BeginAddress) {
hi = mid;
} else if (key >= unwind_table_end_address_[mid]) {
lo = mid + 1;
} else {
return &unwind_table_[mid];
}
}
return nullptr;
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,646 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_emitter.h"
#include <cstring>
#include "xenia/base/debugging.h"
#include "xenia/base/logging.h"
#include "xenia/base/math.h"
#include "xenia/base/profiling.h"
#include "xenia/cpu/backend/a64/a64_backend.h"
#include "xenia/cpu/backend/a64/a64_code_cache.h"
#include "xenia/cpu/backend/a64/a64_function.h"
#include "xenia/cpu/backend/a64/a64_sequences.h"
#include "xenia/cpu/backend/a64/a64_stack_layout.h"
#include "xenia/cpu/cpu_flags.h"
#include "xenia/cpu/hir/hir_builder.h"
#include "xenia/cpu/hir/label.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/processor.h"
DECLARE_int64(a64_max_stackpoints);
DECLARE_bool(a64_enable_host_guest_stack_synchronization);
namespace {
void TraceFunctionEntry(void* raw_context, uint64_t function_address) {
auto ctx = reinterpret_cast<xe::cpu::ppc::PPCContext*>(raw_context);
XELOGI("a64 call {:08X} t{}", static_cast<uint32_t>(function_address),
ctx->thread_id);
}
} // namespace
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
using namespace Xbyak_aarch64;
// Defined in a64_backend.cc.
extern uint64_t ResolveFunction(void* raw_context, uint64_t target_address);
static uint64_t UndefinedCallExtern(void* raw_context, uint64_t function_ptr) {
auto function = reinterpret_cast<Function*>(function_ptr);
XELOGE("undefined extern call to {:08X} {}", function->address(),
function->name());
return 0;
}
static constexpr size_t kMaxCodeSize = 1_MiB;
// Register maps:
// GPR allocatable registers: x19, x22, x23, x24, x25, x26, x27, x28
// (x20=context, x21=membase are reserved)
const uint32_t A64Emitter::gpr_reg_map_[GPR_COUNT] = {
19, 22, 23, 24, 25, 26, 27, 28,
};
// VEC allocatable registers: v4-v7, v16-v31
// (v0-v3 are scratch)
const uint32_t A64Emitter::vec_reg_map_[VEC_COUNT] = {
4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
};
A64Emitter::A64Emitter(A64Backend* backend, XbyakA64Allocator* allocator)
: CodeGenerator(kMaxCodeSize, Xbyak_aarch64::DontSetProtectRWE, allocator),
processor_(backend->processor()),
backend_(backend),
code_cache_(backend->code_cache()),
allocator_(allocator) {}
A64Emitter::~A64Emitter() = default;
bool A64Emitter::Emit(GuestFunction* function, hir::HIRBuilder* builder,
uint32_t debug_info_flags, FunctionDebugInfo* debug_info,
void** out_code_address, size_t* out_code_size,
std::vector<SourceMapEntry>* out_source_map) {
SCOPE_profile_cpu_f("cpu");
guest_module_ = dynamic_cast<XexModule*>(function->module());
debug_info_ = debug_info;
debug_info_flags_ = debug_info_flags;
trace_data_ = &function->trace_data();
current_guest_function_ = function->address();
// Reset state.
stack_size_ = StackLayout::GUEST_STACK_SIZE;
source_map_arena_.Reset();
tail_code_.clear();
fpcr_mode_ = FPCRMode::Unknown;
// Try to emit.
EmitFunctionInfo func_info = {};
if (!Emit(builder, func_info)) {
return false;
}
// Emplace the code into the code cache.
*out_code_address = Emplace(func_info, function);
*out_code_size = func_info.code_size.total;
// Copy source map.
source_map_arena_.CloneContents(out_source_map);
return *out_code_address != nullptr;
}
bool A64Emitter::Emit(hir::HIRBuilder* builder, EmitFunctionInfo& func_info) {
// Calculate local variable stack offsets.
auto locals = builder->locals();
size_t stack_offset = StackLayout::GUEST_STACK_SIZE;
for (auto it = locals.begin(); it != locals.end(); ++it) {
auto slot = *it;
size_t type_size = hir::GetTypeSize(slot->type);
// Align to natural size (at least 4 bytes for ARM64 alignment).
size_t align_size = xe::round_up(type_size, static_cast<size_t>(4));
stack_offset = xe::align(stack_offset, align_size);
slot->set_constant(static_cast<uint32_t>(stack_offset));
stack_offset += type_size;
}
// Align total stack offset to 16 bytes (ARM64 ABI requirement).
stack_offset -= StackLayout::GUEST_STACK_SIZE;
stack_offset = xe::align(stack_offset, static_cast<size_t>(16));
const size_t stack_size = StackLayout::GUEST_STACK_SIZE + stack_offset;
// ARM64 ABI: SP must always be 16-byte aligned.
assert_true(stack_size % 16 == 0);
func_info.stack_size = stack_size;
stack_size_ = stack_size;
struct {
size_t prolog;
size_t body;
size_t epilog;
size_t tail;
size_t prolog_stack_alloc;
} code_offsets = {};
// ========================================================================
// PROLOG
// ========================================================================
code_offsets.prolog = getSize();
// sub sp, sp, #stack_size
if (stack_size <= 4095) {
sub(sp, sp, static_cast<uint32_t>(stack_size));
} else {
mov(x17, static_cast<uint64_t>(stack_size));
sub(sp, sp, x17, UXTX);
}
code_offsets.prolog_stack_alloc = getSize();
// Store host return address (x30/LR) so the epilog can restore it.
str(x30, ptr(sp, static_cast<uint32_t>(StackLayout::HOST_RET_ADDR)));
// Store guest PPC return address (passed in x0 by convention).
str(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_RET_ADDR)));
// Store zero for call return address (we haven't made a call yet).
str(xzr, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_CALL_RET_ADDR)));
// Record stackpoint for longjmp recovery, then save the resulting depth
// for post-call detection (if depth changes, a longjmp skipped frames).
PushStackpoint();
if (cvars::a64_enable_host_guest_stack_synchronization) {
mov(x17, static_cast<uint64_t>(sizeof(A64BackendContext)));
sub(x17, x20, x17);
ldr(w16, ptr(x17, static_cast<uint32_t>(offsetof(
A64BackendContext, current_stackpoint_depth))));
str(w16, ptr(sp, static_cast<uint32_t>(
StackLayout::GUEST_SAVED_STACKPOINT_DEPTH)));
}
// ========================================================================
// BODY
// ========================================================================
code_offsets.body = getSize();
// Allocate the epilog label.
auto epilog_label_ptr = new Label();
epilog_label_ = epilog_label_ptr;
// Walk HIR blocks and emit ARM64 instructions.
auto block = builder->first_block();
synchronize_stack_on_next_instruction_ = false;
while (block) {
// Reset FPCR tracking on each block entry (we don't know which
// predecessor ran, so mode is unknown).
ForgetFpcrMode();
// Bind all labels targeting this block.
auto label = block->label_head;
while (label) {
L(GetLabel(label->id));
label = label->next;
}
// Process each instruction in the block.
const hir::Instr* instr = block->instr_head;
while (instr) {
// After a guest call, check for longjmp on the next real instruction.
// Skip SOURCE_OFFSET because the return address from the call would
// point past the check, so it would never execute.
if (synchronize_stack_on_next_instruction_) {
if (instr->GetOpcodeNum() != hir::OPCODE_SOURCE_OFFSET) {
synchronize_stack_on_next_instruction_ = false;
EnsureSynchronizedGuestAndHostStack();
}
}
const hir::Instr* new_tail = instr;
if (!SelectSequence(this, instr, &new_tail)) {
// No sequence matched — this is expected in Phase 1 before
// sequences are implemented.
XELOGE("A64: Unable to process HIR opcode {}",
hir::GetOpcodeName(instr->GetOpcodeInfo()));
return false;
}
instr = new_tail;
}
block = block->next;
}
// ========================================================================
// EPILOG
// ========================================================================
L(*epilog_label_);
epilog_label_ = nullptr;
code_offsets.epilog = getSize();
// Pop stackpoint before leaving.
PopStackpoint();
// Restore host return address and deallocate stack.
ldr(x30, ptr(sp, static_cast<uint32_t>(StackLayout::HOST_RET_ADDR)));
if (stack_size <= 4095) {
add(sp, sp, static_cast<uint32_t>(stack_size));
} else {
mov(x17, static_cast<uint64_t>(stack_size));
add(sp, sp, x17, UXTX);
}
ret();
// ========================================================================
// TAIL CODE
// ========================================================================
for (auto& tail_item : tail_code_) {
// ARM64 instructions are always 4-byte aligned, so alignment is mostly
// a no-op unless we want cache-line alignment for hot paths.
L(tail_item.label);
tail_item.func(*this, tail_item.label);
}
code_offsets.tail = getSize();
// Fill in EmitFunctionInfo metrics.
assert_zero(code_offsets.prolog);
func_info.code_size.total = getSize();
func_info.code_size.prolog = code_offsets.body - code_offsets.prolog;
func_info.code_size.body = code_offsets.epilog - code_offsets.body;
func_info.code_size.epilog = code_offsets.tail - code_offsets.epilog;
func_info.code_size.tail = getSize() - code_offsets.tail;
func_info.prolog_stack_alloc_offset =
code_offsets.prolog_stack_alloc - code_offsets.prolog;
return true;
}
void* A64Emitter::Emplace(const EmitFunctionInfo& func_info,
GuestFunction* function) {
assert_true(func_info.code_size.total == getSize());
void* new_execute_address;
void* new_write_address;
if (function) {
code_cache_->PlaceGuestCode(
function->address(),
const_cast<void*>(static_cast<const void*>(getCode())), func_info,
function, new_execute_address, new_write_address);
} else {
code_cache_->PlaceHostCode(
0, const_cast<void*>(static_cast<const void*>(getCode())), func_info,
new_execute_address, new_write_address);
}
// In xbyak_aarch64, labels are resolved at define time (backpatching),
// so all relative offsets are already correct. We just need to reset
// the codegen state for the next function.
reset();
tail_code_.clear();
// Clean up cached labels.
for (auto* cached_label : label_cache_) {
delete cached_label;
}
label_cache_.clear();
// Clean up HIR->xbyak label map.
for (auto& pair : label_map_) {
delete pair.second;
}
label_map_.clear();
return new_execute_address;
}
void A64Emitter::MarkSourceOffset(const hir::Instr* i) {
auto entry = source_map_arena_.Alloc<SourceMapEntry>();
entry->guest_address = static_cast<uint32_t>(i->src1.offset);
entry->hir_offset = uint32_t(i->block->ordinal << 16) | i->ordinal;
entry->code_offset = static_cast<uint32_t>(getSize());
}
void A64Emitter::DebugBreak() { brk(0); }
void A64Emitter::Trap(uint16_t trap_type) { brk(trap_type); }
void A64Emitter::UnimplementedInstr(const hir::Instr* i) {
XELOGE("A64: Unimplemented HIR instruction: {}",
hir::GetOpcodeName(i->GetOpcodeInfo()));
DebugBreak();
}
void A64Emitter::Call(const hir::Instr* instr, GuestFunction* function) {
assert_not_null(function);
ForgetFpcrMode();
auto fn = static_cast<A64Function*>(function);
if (fn->machine_code()) {
// Direct call — function is already compiled.
mov(x9, reinterpret_cast<uint64_t>(fn->machine_code()));
if (!(instr->flags & hir::CALL_TAIL)) {
// Pass the next call's guest return address in x0.
ldr(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_CALL_RET_ADDR)));
blr(x9);
synchronize_stack_on_next_instruction_ = true;
} else {
// Tail call: pass our return address to the callee.
PopStackpoint();
ldr(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_RET_ADDR)));
ldr(x30, ptr(sp, static_cast<uint32_t>(StackLayout::HOST_RET_ADDR)));
if (stack_size() <= 4095) {
add(sp, sp, static_cast<uint32_t>(stack_size()));
} else {
mov(x17, static_cast<uint64_t>(stack_size()));
add(sp, sp, x17, UXTX);
}
br(x9);
}
return;
}
if (code_cache_->has_indirection_table()) {
// Load host code address from indirection table.
mov(w16, function->address());
ldr(w9, ptr(x16, static_cast<uint32_t>(0)));
} else {
// Fallback: resolve at runtime.
mov(x0, x20); // context
mov(x1, static_cast<uint64_t>(function->address()));
mov(x9, reinterpret_cast<uint64_t>(&ResolveFunction));
blr(x9);
mov(x9, x0); // resolved address in x9
}
if (instr->flags & hir::CALL_TAIL) {
PopStackpoint();
ldr(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_RET_ADDR)));
ldr(x30, ptr(sp, static_cast<uint32_t>(StackLayout::HOST_RET_ADDR)));
if (stack_size() <= 4095) {
add(sp, sp, static_cast<uint32_t>(stack_size()));
} else {
mov(x17, static_cast<uint64_t>(stack_size()));
add(sp, sp, x17, UXTX);
}
br(x9);
} else {
ldr(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_CALL_RET_ADDR)));
blr(x9);
synchronize_stack_on_next_instruction_ = true;
}
}
void A64Emitter::CallIndirect(const hir::Instr* instr, int reg_index) {
ForgetFpcrMode();
auto target_w = WReg(reg_index);
// Check if this is a possible return (e.g., PPC blr).
if (instr->flags & hir::CALL_POSSIBLE_RETURN) {
// Compare target guest address with our function's return address.
ldr(w0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_RET_ADDR)));
cmp(target_w, w0);
b(EQ, epilog_label());
}
// Load host code address from indirection table.
if (code_cache_->has_indirection_table()) {
mov(w16, target_w); // w16 = guest address (also used by resolve thunk)
ldr(w9, ptr(x16, static_cast<uint32_t>(
0))); // w9 = host code from indirection table
} else {
// Fallback: resolve at runtime.
mov(w16, target_w);
mov(x0, x20); // context
mov(x1, x16); // guest address
mov(x9, reinterpret_cast<uint64_t>(&ResolveFunction));
blr(x9);
mov(x9, x0); // resolved address
}
if (instr->flags & hir::CALL_TAIL) {
// Tail call: pass our return address to the callee.
PopStackpoint();
ldr(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_RET_ADDR)));
ldr(x30, ptr(sp, static_cast<uint32_t>(StackLayout::HOST_RET_ADDR)));
if (stack_size() <= 4095) {
add(sp, sp, static_cast<uint32_t>(stack_size()));
} else {
mov(x17, static_cast<uint64_t>(stack_size()));
add(sp, sp, x17, UXTX);
}
br(x9);
} else {
// Regular call: pass the next call's return address.
ldr(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_CALL_RET_ADDR)));
blr(x9);
synchronize_stack_on_next_instruction_ = true;
}
}
void A64Emitter::CallExtern(const hir::Instr* instr, const Function* function) {
ForgetFpcrMode();
bool undefined = true;
if (function->behavior() == Function::Behavior::kBuiltin) {
auto builtin_function = static_cast<const BuiltinFunction*>(function);
if (builtin_function->handler()) {
undefined = false;
// GuestToHostThunk: x0=target, x1=arg0, x2=arg1
// Thunk rearranges to: x0=context, x1=arg0, x2=arg1, calls target
mov(x0, reinterpret_cast<uint64_t>(builtin_function->handler()));
mov(x1, reinterpret_cast<uint64_t>(builtin_function->arg0()));
mov(x2, reinterpret_cast<uint64_t>(builtin_function->arg1()));
mov(x9, reinterpret_cast<uint64_t>(backend()->guest_to_host_thunk()));
blr(x9);
}
} else if (function->behavior() == Function::Behavior::kExtern) {
auto extern_function = static_cast<const GuestFunction*>(function);
if (extern_function->extern_handler()) {
undefined = false;
// GuestToHostThunk: x0=target, x1=arg0
mov(x0, reinterpret_cast<uint64_t>(extern_function->extern_handler()));
ldr(x1, ptr(GetContextReg(), static_cast<int32_t>(offsetof(
ppc::PPCContext, kernel_state))));
mov(x9, reinterpret_cast<uint64_t>(backend()->guest_to_host_thunk()));
blr(x9);
}
}
if (undefined) {
// Set arg0 = function pointer, then call UndefinedCallExtern via thunk.
mov(x1, reinterpret_cast<uint64_t>(function));
CallNativeSafe(reinterpret_cast<void*>(&UndefinedCallExtern));
}
}
void A64Emitter::CallNative(void* fn) { CallNativeSafe(fn); }
void A64Emitter::CallNativeSafe(void* fn) {
// GuestToHostThunk: x0=target function, x1/x2=args (set by caller).
// The thunk rearranges: saves x0 in x9, sets x0=context, calls x9.
mov(x0, reinterpret_cast<uint64_t>(fn));
mov(x9, reinterpret_cast<uint64_t>(backend()->guest_to_host_thunk()));
blr(x9);
}
void A64Emitter::SetReturnAddress(uint64_t value) {
mov(x0, value);
str(x0, ptr(sp, static_cast<uint32_t>(StackLayout::GUEST_CALL_RET_ADDR)));
}
void A64Emitter::ReloadMembase() {
// Reload x21 from context->virtual_membase.
ldr(x21, ptr(x20, static_cast<int32_t>(
offsetof(ppc::PPCContext, virtual_membase))));
}
bool A64Emitter::ChangeFpcrMode(FPCRMode new_mode, bool already_set) {
if (fpcr_mode_ == new_mode) {
return false;
}
fpcr_mode_ = new_mode;
if (!already_set) {
// Read current FPCR.
mrs(x0, 3, 3, 4, 4, 0); // mrs x0, FPCR
if (new_mode == FPCRMode::Vmx) {
// VMX mode: set FZ (bit 24) for flush-to-zero.
// Do NOT set DN (bit 25) — PPC preserves NaN payloads.
mov(x17, ~static_cast<uint64_t>(1u << 25));
and_(x0, x0, x17);
orr(x0, x0, static_cast<uint64_t>(1u << 24));
} else {
// FPU mode: clear FZ and DN for IEEE-compliant behavior.
mov(x17, ~static_cast<uint64_t>(3u << 24));
and_(x0, x0, x17);
}
msr(3, 3, 4, 4, 0, x0); // msr FPCR, x0
}
return true;
}
Label& A64Emitter::AddToTail(TailEmitCallback callback, uint32_t alignment) {
TailEmitter tail;
tail.alignment = alignment;
tail.func = std::move(callback);
tail_code_.push_back(std::move(tail));
return tail_code_.back().label;
}
Label& A64Emitter::NewCachedLabel() {
auto* label = new Label();
label_cache_.push_back(label);
return *label;
}
Label& A64Emitter::GetLabel(uint32_t label_id) {
auto it = label_map_.find(label_id);
if (it != label_map_.end()) {
return *it->second;
}
auto* label = new Label();
label_map_[label_id] = label;
return *label;
}
void A64Emitter::HandleStackpointOverflowError(ppc::PPCContext* context) {
if (debugging::IsDebuggerAttached()) {
debugging::Break();
}
xe::FatalError(
"Overflowed stackpoints! Please report this error for this title to "
"Xenia developers.");
}
void A64Emitter::PushStackpoint() {
if (!cvars::a64_enable_host_guest_stack_synchronization) {
return;
}
// Load backend context pointer: x17 = x20 - sizeof(A64BackendContext)
mov(x17, static_cast<uint64_t>(sizeof(A64BackendContext)));
sub(x17, x20, x17);
// x8 = stackpoints array, w9 = current depth
ldr(x8, ptr(x17,
static_cast<uint32_t>(offsetof(A64BackendContext, stackpoints))));
ldr(w9, ptr(x17, static_cast<uint32_t>(
offsetof(A64BackendContext, current_stackpoint_depth))));
// Compute offset into array: x10 = w9 * sizeof(A64BackendStackpoint)
mov(w10, static_cast<uint32_t>(sizeof(A64BackendStackpoint)));
umull(x10, w9, w10);
add(x8, x8, x10);
// Store host SP.
mov(x10, sp);
str(x10, ptr(x8, static_cast<uint32_t>(
offsetof(A64BackendStackpoint, host_stack_))));
// Store guest r1 (32-bit).
ldr(w10, ptr(x20, static_cast<int32_t>(offsetof(ppc::PPCContext, r[1]))));
str(w10, ptr(x8, static_cast<uint32_t>(
offsetof(A64BackendStackpoint, guest_stack_))));
// Store guest LR (32-bit).
ldr(w10, ptr(x20, static_cast<int32_t>(offsetof(ppc::PPCContext, lr))));
str(w10, ptr(x8, static_cast<uint32_t>(
offsetof(A64BackendStackpoint, guest_return_address_))));
// Increment depth.
add(w9, w9, 1);
str(w9, ptr(x17, static_cast<uint32_t>(
offsetof(A64BackendContext, current_stackpoint_depth))));
// Check for overflow.
mov(w10, static_cast<uint32_t>(cvars::a64_max_stackpoints));
cmp(w9, w10);
auto& overflow_label = AddToTail([](A64Emitter& e, Label& lbl) {
e.CallNativeSafe(
reinterpret_cast<void*>(A64Emitter::HandleStackpointOverflowError));
});
b(GE, overflow_label);
}
void A64Emitter::PopStackpoint() {
if (!cvars::a64_enable_host_guest_stack_synchronization) {
return;
}
// Decrement current_stackpoint_depth.
mov(x17, static_cast<uint64_t>(sizeof(A64BackendContext)));
sub(x17, x20, x17);
ldr(w8, ptr(x17, static_cast<uint32_t>(
offsetof(A64BackendContext, current_stackpoint_depth))));
sub(w8, w8, 1);
str(w8, ptr(x17, static_cast<uint32_t>(
offsetof(A64BackendContext, current_stackpoint_depth))));
}
void A64Emitter::EnsureSynchronizedGuestAndHostStack() {
if (!cvars::a64_enable_host_guest_stack_synchronization) {
return;
}
// Compare current stackpoint depth against the value saved after
// PushStackpoint in the prolog. If different, a longjmp occurred and
// some frames' PopStackpoint never ran.
auto& return_from_sync = NewCachedLabel();
mov(x17, static_cast<uint64_t>(sizeof(A64BackendContext)));
sub(x17, x20, x17);
ldr(w17, ptr(x17, static_cast<uint32_t>(offsetof(A64BackendContext,
current_stackpoint_depth))));
ldr(w16, ptr(sp, static_cast<uint32_t>(
StackLayout::GUEST_SAVED_STACKPOINT_DEPTH)));
cmp(w17, w16);
auto& sync_label = AddToTail([&return_from_sync](A64Emitter& e, Label& lbl) {
// Set up arguments for the sync helper:
// x8 = return address (where to resume after fixup)
// x9 = this function's stack size
e.adr(e.x8, return_from_sync);
e.mov(e.x9, static_cast<uint64_t>(e.stack_size()));
e.mov(e.x10, reinterpret_cast<uint64_t>(
e.backend()->synchronize_guest_and_host_stack_helper()));
e.br(e.x10);
});
b(NE, sync_label);
L(return_from_sync);
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,192 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_EMITTER_H_
#define XENIA_CPU_BACKEND_A64_A64_EMITTER_H_
#include <functional>
#include <unordered_map>
#include <vector>
#include "xenia/base/arena.h"
#include "xenia/cpu/backend/code_cache_base.h"
#include "xenia/cpu/function.h"
#include "xenia/cpu/function_trace_data.h"
#include "xenia/cpu/hir/hir_builder.h"
#include "xenia/cpu/hir/instr.h"
#include "xenia/cpu/hir/value.h"
#include "xenia/cpu/xex_module.h"
#include "xenia/memory.h"
#include "xbyak_aarch64.h"
namespace xe {
namespace cpu {
class Processor;
} // namespace cpu
} // namespace xe
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class A64Backend;
class A64CodeCache;
enum class FPCRMode : uint32_t { Unknown, Fpu, Vmx };
// Unfortunately due to the design of xbyak we have to pass this to the ctor.
class XbyakA64Allocator : public Xbyak_aarch64::Allocator {
public:
virtual bool useProtect() const { return false; }
};
class A64Emitter;
using TailEmitCallback =
std::function<void(A64Emitter& e, Xbyak_aarch64::Label& lbl)>;
struct TailEmitter {
Xbyak_aarch64::Label label;
uint32_t alignment;
TailEmitCallback func;
};
class A64Emitter : public Xbyak_aarch64::CodeGenerator {
public:
A64Emitter(A64Backend* backend, XbyakA64Allocator* allocator);
virtual ~A64Emitter();
Processor* processor() const { return processor_; }
A64Backend* backend() const { return backend_; }
bool Emit(GuestFunction* function, hir::HIRBuilder* builder,
uint32_t debug_info_flags, FunctionDebugInfo* debug_info,
void** out_code_address, size_t* out_code_size,
std::vector<SourceMapEntry>* out_source_map);
public:
// Reserved: sp, x20 (context), x21 (membase)
// Scratch: x0-x18 (caller-saved), v0-v3
// Available GPRs for register allocator: x19, x22-x28
static constexpr int GPR_COUNT = 8;
// Available VEC regs: v4-v7, v16-v31
static constexpr int VEC_COUNT = 20;
static constexpr size_t kStashOffset = 32;
static void SetupReg(const hir::Value* v, Xbyak_aarch64::WReg& r) {
auto idx = gpr_reg_map_[v->reg.index];
r = Xbyak_aarch64::WReg(idx);
}
static void SetupReg(const hir::Value* v, Xbyak_aarch64::XReg& r) {
auto idx = gpr_reg_map_[v->reg.index];
r = Xbyak_aarch64::XReg(idx);
}
static void SetupReg(const hir::Value* v, Xbyak_aarch64::SReg& r) {
auto idx = vec_reg_map_[v->reg.index];
r = Xbyak_aarch64::SReg(idx);
}
static void SetupReg(const hir::Value* v, Xbyak_aarch64::DReg& r) {
auto idx = vec_reg_map_[v->reg.index];
r = Xbyak_aarch64::DReg(idx);
}
static void SetupReg(const hir::Value* v, Xbyak_aarch64::QReg& r) {
auto idx = vec_reg_map_[v->reg.index];
r = Xbyak_aarch64::QReg(idx);
}
static void SetupReg(const hir::Value* v, Xbyak_aarch64::VReg& r) {
auto idx = vec_reg_map_[v->reg.index];
r = Xbyak_aarch64::VReg(idx);
}
Xbyak_aarch64::Label& epilog_label() { return *epilog_label_; }
FunctionDebugInfo* debug_info() const { return debug_info_; }
size_t stack_size() const { return stack_size_; }
void MarkSourceOffset(const hir::Instr* i);
void DebugBreak();
void Trap(uint16_t trap_type = 0);
void UnimplementedInstr(const hir::Instr* i);
void Call(const hir::Instr* instr, GuestFunction* function);
void CallIndirect(const hir::Instr* instr, int reg_index);
void CallExtern(const hir::Instr* instr, const Function* function);
void CallNative(void* fn);
void CallNativeSafe(void* fn);
void SetReturnAddress(uint64_t value);
// Context register = x20.
const Xbyak_aarch64::XReg& GetContextReg() const { return x20; }
// Memory base register = x21.
const Xbyak_aarch64::XReg& GetMembaseReg() const { return x21; }
void ReloadMembase();
void PushStackpoint();
void PopStackpoint();
void EnsureSynchronizedGuestAndHostStack();
static void HandleStackpointOverflowError(ppc::PPCContext* context);
void ForgetFpcrMode() { fpcr_mode_ = FPCRMode::Unknown; }
bool ChangeFpcrMode(FPCRMode new_mode, bool already_set = false);
Xbyak_aarch64::Label& AddToTail(TailEmitCallback callback,
uint32_t alignment = 0);
Xbyak_aarch64::Label& NewCachedLabel();
// Get or create a xbyak_aarch64 label for a HIR label ID.
Xbyak_aarch64::Label& GetLabel(uint32_t label_id);
XexModule* GuestModule() { return guest_module_; }
protected:
void* Emplace(const EmitFunctionInfo& func_info,
GuestFunction* function = nullptr);
bool Emit(hir::HIRBuilder* builder, EmitFunctionInfo& func_info);
protected:
Processor* processor_ = nullptr;
A64Backend* backend_ = nullptr;
A64CodeCache* code_cache_ = nullptr;
XbyakA64Allocator* allocator_ = nullptr;
XexModule* guest_module_ = nullptr;
uint32_t current_guest_function_ = 0;
Xbyak_aarch64::Label* epilog_label_ = nullptr;
hir::Instr* current_instr_ = nullptr;
FunctionDebugInfo* debug_info_ = nullptr;
uint32_t debug_info_flags_ = 0;
FunctionTraceData* trace_data_ = nullptr;
Arena source_map_arena_;
size_t stack_size_ = 0;
static const uint32_t gpr_reg_map_[GPR_COUNT];
static const uint32_t vec_reg_map_[VEC_COUNT];
std::vector<TailEmitter> tail_code_;
std::vector<Xbyak_aarch64::Label*> label_cache_;
// Map from HIR label IDs to xbyak_aarch64 Labels.
std::unordered_map<uint32_t, Xbyak_aarch64::Label*> label_map_;
FPCRMode fpcr_mode_ = FPCRMode::Unknown;
bool synchronize_stack_on_next_instruction_ = false;
};
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_EMITTER_H_

View File

@@ -0,0 +1,48 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_function.h"
#include "xenia/cpu/backend/a64/a64_backend.h"
#include "xenia/cpu/processor.h"
#include "xenia/cpu/thread_state.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
A64Function::A64Function(Module* module, uint32_t address)
: GuestFunction(module, address) {}
A64Function::~A64Function() {
// machine_code_ is freed by code cache.
}
void A64Function::Setup(uint8_t* machine_code, size_t machine_code_length) {
machine_code_ = machine_code;
machine_code_length_ = machine_code_length;
}
bool A64Function::CallImpl(ThreadState* thread_state, uint32_t return_address) {
auto backend =
reinterpret_cast<A64Backend*>(thread_state->processor()->backend());
auto thunk = backend->host_to_guest_thunk();
if (!thunk || !machine_code_) {
return false;
}
thunk(machine_code_, thread_state->context(),
reinterpret_cast<void*>(uintptr_t(return_address)));
return true;
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,44 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_FUNCTION_H_
#define XENIA_CPU_BACKEND_A64_A64_FUNCTION_H_
#include "xenia/cpu/function.h"
#include "xenia/cpu/thread_state.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class A64Function : public GuestFunction {
public:
A64Function(Module* module, uint32_t address);
~A64Function() override;
uint8_t* machine_code() const override { return machine_code_; }
size_t machine_code_length() const override { return machine_code_length_; }
void Setup(uint8_t* machine_code, size_t machine_code_length);
protected:
bool CallImpl(ThreadState* thread_state, uint32_t return_address) override;
private:
uint8_t* machine_code_ = nullptr;
size_t machine_code_length_ = 0;
};
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_FUNCTION_H_

View File

@@ -0,0 +1,419 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_OP_H_
#define XENIA_CPU_BACKEND_A64_A64_OP_H_
#include "xenia/cpu/backend/a64/a64_emitter.h"
#include "xenia/cpu/hir/instr.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
using namespace xe::cpu;
using namespace xe::cpu::hir;
using namespace Xbyak_aarch64;
// Selects the right byte/word/etc from a vector. PPC is big-endian so
// we need to flip logical indices (0,1,2,3,4,5,6,7,...) = (3,2,1,0,7,6,5,4,...)
#define VEC128_B(n) ((n) ^ 0x3)
#define VEC128_W(n) ((n) ^ 0x1)
#define VEC128_D(n) (n)
#define VEC128_F(n) (n)
enum KeyType {
KEY_TYPE_X = OPCODE_SIG_TYPE_X,
KEY_TYPE_L = OPCODE_SIG_TYPE_L,
KEY_TYPE_O = OPCODE_SIG_TYPE_O,
KEY_TYPE_S = OPCODE_SIG_TYPE_S,
KEY_TYPE_V_I8 = OPCODE_SIG_TYPE_V + INT8_TYPE,
KEY_TYPE_V_I16 = OPCODE_SIG_TYPE_V + INT16_TYPE,
KEY_TYPE_V_I32 = OPCODE_SIG_TYPE_V + INT32_TYPE,
KEY_TYPE_V_I64 = OPCODE_SIG_TYPE_V + INT64_TYPE,
KEY_TYPE_V_F32 = OPCODE_SIG_TYPE_V + FLOAT32_TYPE,
KEY_TYPE_V_F64 = OPCODE_SIG_TYPE_V + FLOAT64_TYPE,
KEY_TYPE_V_V128 = OPCODE_SIG_TYPE_V + VEC128_TYPE,
};
using InstrKeyValue = uint32_t;
#pragma pack(push, 1)
union InstrKey {
InstrKeyValue value;
struct {
InstrKeyValue opcode : 8;
InstrKeyValue dest : 5;
InstrKeyValue src1 : 5;
InstrKeyValue src2 : 5;
InstrKeyValue src3 : 5;
InstrKeyValue reserved : 4;
};
operator InstrKeyValue() const { return value; }
InstrKey() : value(0) { static_assert_size(*this, sizeof(value)); }
InstrKey(InstrKeyValue v) : value(v) {}
InstrKey(const Instr* i) : value(0) {
const OpcodeInfo* info = i->GetOpcodeInfo();
InstrKeyValue sig = info->signature;
OpcodeSignatureType dest_type, src1_type, src2_type, src3_type;
UnpackOpcodeSig(sig, dest_type, src1_type, src2_type, src3_type);
InstrKeyValue out_desttype = (InstrKeyValue)dest_type;
InstrKeyValue out_src1type = (InstrKeyValue)src1_type;
InstrKeyValue out_src2type = (InstrKeyValue)src2_type;
InstrKeyValue out_src3type = (InstrKeyValue)src3_type;
Value* destv = i->dest;
Value* src1v = i->src1.value;
Value* src2v = i->src2.value;
Value* src3v = i->src3.value;
if (out_src1type == OPCODE_SIG_TYPE_V) {
out_src1type += src1v->type;
}
if (out_src2type == OPCODE_SIG_TYPE_V) {
out_src2type += src2v->type;
}
if (out_src3type == OPCODE_SIG_TYPE_V) {
out_src3type += src3v->type;
}
opcode = info->num;
dest = out_desttype ? OPCODE_SIG_TYPE_V + destv->type : 0;
src1 = out_src1type;
src2 = out_src2type;
src3 = out_src3type;
}
template <Opcode OPCODE, KeyType DEST = KEY_TYPE_X, KeyType SRC1 = KEY_TYPE_X,
KeyType SRC2 = KEY_TYPE_X, KeyType SRC3 = KEY_TYPE_X>
struct Construct {
static const InstrKeyValue value =
(OPCODE) | (DEST << 8) | (SRC1 << 13) | (SRC2 << 18) | (SRC3 << 23);
};
};
#pragma pack(pop)
static_assert(sizeof(InstrKey) <= 4, "Key must be 4 bytes");
// ============================================================================
// Op types — architecture-independent
// ============================================================================
struct OpBase {};
template <typename T, KeyType KEY_TYPE>
struct Op : OpBase {
static constexpr KeyType key_type = KEY_TYPE;
};
struct VoidOp : Op<VoidOp, KEY_TYPE_X> {
protected:
template <hir::Opcode OPCODE, typename... Ts>
friend struct I;
void Load(const Instr::Op& op) {}
};
struct OffsetOp : Op<OffsetOp, KEY_TYPE_O> {
uint64_t value;
protected:
template <hir::Opcode OPCODE, typename... Ts>
friend struct I;
void Load(const Instr::Op& op) { this->value = op.offset; }
};
struct SymbolOp : Op<SymbolOp, KEY_TYPE_S> {
Function* value;
protected:
template <hir::Opcode OPCODE, typename... Ts>
friend struct I;
bool Load(const Instr::Op& op) {
this->value = op.symbol;
return true;
}
};
struct LabelOp : Op<LabelOp, KEY_TYPE_L> {
hir::Label* value;
protected:
template <hir::Opcode OPCODE, typename... Ts>
friend struct I;
void Load(const Instr::Op& op) { this->value = op.label; }
};
// ============================================================================
// ValueOp — ARM64 register type specializations
// ============================================================================
template <typename T, KeyType KEY_TYPE, typename REG_TYPE, typename CONST_TYPE>
struct ValueOp : Op<ValueOp<T, KEY_TYPE, REG_TYPE, CONST_TYPE>, KEY_TYPE> {
typedef REG_TYPE reg_type;
const Value* value;
bool is_constant;
virtual bool ConstantFitsIn32Reg() const { return true; }
const REG_TYPE& reg() const {
assert_true(!is_constant);
return reg_;
}
operator const REG_TYPE&() const { return reg(); }
bool IsEqual(const T& b) const {
if (is_constant && b.is_constant) {
return reinterpret_cast<const T*>(this)->constant() == b.constant();
} else if (!is_constant && !b.is_constant) {
return reg_.getIdx() == b.reg_.getIdx();
}
return false;
}
bool operator==(const T& b) const { return IsEqual(b); }
bool operator!=(const T& b) const { return !IsEqual(b); }
void Load(const Instr::Op& op) {
value = op.value;
is_constant = value->IsConstant();
if (!is_constant) {
A64Emitter::SetupReg(value, reg_);
}
}
protected:
REG_TYPE reg_ = REG_TYPE(0);
};
// ARM64 integer operands use WReg (32-bit) and XReg (64-bit).
// I8 and I16 are handled via WReg with masking/extension as needed.
struct I8Op : ValueOp<I8Op, KEY_TYPE_V_I8, WReg, int8_t> {
typedef ValueOp<I8Op, KEY_TYPE_V_I8, WReg, int8_t> BASE;
int8_t constant() const {
assert_true(BASE::is_constant);
return BASE::value->constant.i8;
}
};
struct I16Op : ValueOp<I16Op, KEY_TYPE_V_I16, WReg, int16_t> {
typedef ValueOp<I16Op, KEY_TYPE_V_I16, WReg, int16_t> BASE;
int16_t constant() const {
assert_true(BASE::is_constant);
return BASE::value->constant.i16;
}
};
struct I32Op : ValueOp<I32Op, KEY_TYPE_V_I32, WReg, int32_t> {
typedef ValueOp<I32Op, KEY_TYPE_V_I32, WReg, int32_t> BASE;
int32_t constant() const {
assert_true(BASE::is_constant);
return BASE::value->constant.i32;
}
};
struct I64Op : ValueOp<I64Op, KEY_TYPE_V_I64, XReg, int64_t> {
typedef ValueOp<I64Op, KEY_TYPE_V_I64, XReg, int64_t> BASE;
int64_t constant() const {
assert_true(BASE::is_constant);
return BASE::value->constant.i64;
}
bool ConstantFitsIn32Reg() const override {
int64_t v = BASE::value->constant.i64;
if ((v & ~0x7FFFFFFF) == 0) {
return true;
} else if ((v & ~0x7FFFFFFFUL) == ~0x7FFFFFFFUL) {
return true;
}
return false;
}
};
// ARM64 float/vector operands use SReg, DReg, QReg.
struct F32Op : ValueOp<F32Op, KEY_TYPE_V_F32, SReg, float> {
typedef ValueOp<F32Op, KEY_TYPE_V_F32, SReg, float> BASE;
float constant() const {
assert_true(BASE::is_constant);
return BASE::value->constant.f32;
}
};
struct F64Op : ValueOp<F64Op, KEY_TYPE_V_F64, DReg, double> {
typedef ValueOp<F64Op, KEY_TYPE_V_F64, DReg, double> BASE;
double constant() const {
assert_true(BASE::is_constant);
return BASE::value->constant.f64;
}
};
struct V128Op : ValueOp<V128Op, KEY_TYPE_V_V128, QReg, vec128_t> {
typedef ValueOp<V128Op, KEY_TYPE_V_V128, QReg, vec128_t> BASE;
const vec128_t& constant() const {
assert_true(BASE::is_constant);
return BASE::value->constant.v128;
}
};
// ============================================================================
// DestField — handles loading the destination operand
// ============================================================================
template <typename DEST, typename... Tf>
struct DestField;
template <typename DEST>
struct DestField<DEST> {
DEST dest;
protected:
bool LoadDest(const Instr* i) {
Instr::Op op;
op.value = i->dest;
dest.Load(op);
return true;
}
};
template <>
struct DestField<VoidOp> {
protected:
bool LoadDest(const Instr* i) { return true; }
};
// ============================================================================
// I<> — instruction pattern with 0-3 source operands
// ============================================================================
template <hir::Opcode OPCODE, typename... Ts>
struct I;
template <hir::Opcode OPCODE, typename DEST>
struct I<OPCODE, DEST> : DestField<DEST> {
typedef DestField<DEST> BASE;
static constexpr hir::Opcode opcode = OPCODE;
static const uint32_t key =
InstrKey::Construct<OPCODE, DEST::key_type>::value;
static const KeyType dest_type = DEST::key_type;
const Instr* instr;
protected:
template <typename SEQ, typename T>
friend struct Sequence;
bool Load(const Instr* i, InstrKeyValue kv) {
if (kv == key && BASE::LoadDest(i)) {
instr = i;
return true;
}
return false;
}
};
template <hir::Opcode OPCODE, typename DEST, typename SRC1>
struct I<OPCODE, DEST, SRC1> : DestField<DEST> {
typedef DestField<DEST> BASE;
static constexpr hir::Opcode opcode = OPCODE;
static const uint32_t key =
InstrKey::Construct<OPCODE, DEST::key_type, SRC1::key_type>::value;
static const KeyType dest_type = DEST::key_type;
static const KeyType src1_type = SRC1::key_type;
const Instr* instr;
SRC1 src1;
protected:
template <typename SEQ, typename T>
friend struct Sequence;
bool Load(const Instr* i, InstrKeyValue kv) {
if (kv == key && BASE::LoadDest(i)) {
instr = i;
src1.Load(i->src1);
return true;
}
return false;
}
};
template <hir::Opcode OPCODE, typename DEST, typename SRC1, typename SRC2>
struct I<OPCODE, DEST, SRC1, SRC2> : DestField<DEST> {
typedef DestField<DEST> BASE;
static constexpr hir::Opcode opcode = OPCODE;
static const uint32_t key =
InstrKey::Construct<OPCODE, DEST::key_type, SRC1::key_type,
SRC2::key_type>::value;
static const KeyType dest_type = DEST::key_type;
static const KeyType src1_type = SRC1::key_type;
static const KeyType src2_type = SRC2::key_type;
const Instr* instr;
SRC1 src1;
SRC2 src2;
protected:
template <typename SEQ, typename T>
friend struct Sequence;
bool Load(const Instr* i, InstrKeyValue kv) {
if (kv == key && BASE::LoadDest(i)) {
instr = i;
src1.Load(i->src1);
src2.Load(i->src2);
return true;
}
return false;
}
};
template <hir::Opcode OPCODE, typename DEST, typename SRC1, typename SRC2,
typename SRC3>
struct I<OPCODE, DEST, SRC1, SRC2, SRC3> : DestField<DEST> {
typedef DestField<DEST> BASE;
static constexpr hir::Opcode opcode = OPCODE;
static const uint32_t key =
InstrKey::Construct<OPCODE, DEST::key_type, SRC1::key_type,
SRC2::key_type, SRC3::key_type>::value;
static const KeyType dest_type = DEST::key_type;
static const KeyType src1_type = SRC1::key_type;
static const KeyType src2_type = SRC2::key_type;
static const KeyType src3_type = SRC3::key_type;
const Instr* instr;
SRC1 src1;
SRC2 src2;
SRC3 src3;
protected:
template <typename SEQ, typename T>
friend struct Sequence;
bool Load(const Instr* i, InstrKeyValue ikey) {
if (ikey == key && BASE::LoadDest(i)) {
instr = i;
src1.Load(i->src1);
src2.Load(i->src2);
src3.Load(i->src3);
return true;
}
return false;
}
};
// ============================================================================
// Sequence<> — base for all ARM64 instruction sequences
// ============================================================================
template <typename SEQ, typename T>
struct Sequence {
typedef T EmitArgType;
static constexpr uint32_t head_key() { return T::key; }
static bool Select(A64Emitter& e, const Instr* i, InstrKeyValue ikey) {
T args;
if (!args.Load(i, ikey)) {
return false;
}
SEQ::Emit(e, args);
return true;
}
};
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_OP_H_

View File

@@ -0,0 +1,504 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_sequences.h"
#include "xenia/cpu/backend/a64/a64_emitter.h"
#include "xenia/cpu/backend/a64/a64_op.h"
#include "xenia/cpu/backend/a64/a64_stack_layout.h"
#include "xenia/cpu/hir/instr.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
volatile int anchor_control = 0;
// ============================================================================
// OPCODE_BRANCH
// ============================================================================
struct BRANCH : Sequence<BRANCH, I<OPCODE_BRANCH, VoidOp, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.b(e.GetLabel(i.src1.value->id));
}
};
EMITTER_OPCODE_TABLE(OPCODE_BRANCH, BRANCH);
// ============================================================================
// OPCODE_BRANCH_TRUE
// ============================================================================
struct BRANCH_TRUE_I8
: Sequence<BRANCH_TRUE_I8, I<OPCODE_BRANCH_TRUE, VoidOp, I8Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_TRUE_I16
: Sequence<BRANCH_TRUE_I16, I<OPCODE_BRANCH_TRUE, VoidOp, I16Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_TRUE_I32
: Sequence<BRANCH_TRUE_I32, I<OPCODE_BRANCH_TRUE, VoidOp, I32Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_TRUE_I64
: Sequence<BRANCH_TRUE_I64, I<OPCODE_BRANCH_TRUE, VoidOp, I64Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_TRUE_F32
: Sequence<BRANCH_TRUE_F32, I<OPCODE_BRANCH_TRUE, VoidOp, F32Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.fmov(e.w0, i.src1);
e.cbnz(e.w0, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_TRUE_F64
: Sequence<BRANCH_TRUE_F64, I<OPCODE_BRANCH_TRUE, VoidOp, F64Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.fmov(e.x0, i.src1);
e.cbnz(e.x0, e.GetLabel(i.src2.value->id));
}
};
EMITTER_OPCODE_TABLE(OPCODE_BRANCH_TRUE, BRANCH_TRUE_I8, BRANCH_TRUE_I16,
BRANCH_TRUE_I32, BRANCH_TRUE_I64, BRANCH_TRUE_F32,
BRANCH_TRUE_F64);
// ============================================================================
// OPCODE_BRANCH_FALSE
// ============================================================================
struct BRANCH_FALSE_I8
: Sequence<BRANCH_FALSE_I8, I<OPCODE_BRANCH_FALSE, VoidOp, I8Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_FALSE_I16
: Sequence<BRANCH_FALSE_I16,
I<OPCODE_BRANCH_FALSE, VoidOp, I16Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_FALSE_I32
: Sequence<BRANCH_FALSE_I32,
I<OPCODE_BRANCH_FALSE, VoidOp, I32Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_FALSE_I64
: Sequence<BRANCH_FALSE_I64,
I<OPCODE_BRANCH_FALSE, VoidOp, I64Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbz(i.src1, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_FALSE_F32
: Sequence<BRANCH_FALSE_F32,
I<OPCODE_BRANCH_FALSE, VoidOp, F32Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.fmov(e.w0, i.src1);
e.cbz(e.w0, e.GetLabel(i.src2.value->id));
}
};
struct BRANCH_FALSE_F64
: Sequence<BRANCH_FALSE_F64,
I<OPCODE_BRANCH_FALSE, VoidOp, F64Op, LabelOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.fmov(e.x0, i.src1);
e.cbz(e.x0, e.GetLabel(i.src2.value->id));
}
};
EMITTER_OPCODE_TABLE(OPCODE_BRANCH_FALSE, BRANCH_FALSE_I8, BRANCH_FALSE_I16,
BRANCH_FALSE_I32, BRANCH_FALSE_I64, BRANCH_FALSE_F32,
BRANCH_FALSE_F64);
// ============================================================================
// OPCODE_RETURN
// ============================================================================
struct RETURN : Sequence<RETURN, I<OPCODE_RETURN, VoidOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
// Jump to epilog (unless this is the last instruction before epilog).
if (i.instr->next || i.instr->block->next) {
e.b(e.epilog_label());
}
}
};
EMITTER_OPCODE_TABLE(OPCODE_RETURN, RETURN);
// ============================================================================
// OPCODE_SET_RETURN_ADDRESS
// ============================================================================
struct SET_RETURN_ADDRESS
: Sequence<SET_RETURN_ADDRESS,
I<OPCODE_SET_RETURN_ADDRESS, VoidOp, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.SetReturnAddress(i.src1.constant());
}
};
EMITTER_OPCODE_TABLE(OPCODE_SET_RETURN_ADDRESS, SET_RETURN_ADDRESS);
// ============================================================================
// OPCODE_DEBUG_BREAK
// ============================================================================
struct DEBUG_BREAK : Sequence<DEBUG_BREAK, I<OPCODE_DEBUG_BREAK, VoidOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) { e.DebugBreak(); }
};
EMITTER_OPCODE_TABLE(OPCODE_DEBUG_BREAK, DEBUG_BREAK);
// ============================================================================
// OPCODE_DEBUG_BREAK_TRUE
// ============================================================================
struct DEBUG_BREAK_TRUE_I8
: Sequence<DEBUG_BREAK_TRUE_I8, I<OPCODE_DEBUG_BREAK_TRUE, VoidOp, I8Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.DebugBreak();
e.L(skip);
}
};
struct DEBUG_BREAK_TRUE_I16
: Sequence<DEBUG_BREAK_TRUE_I16,
I<OPCODE_DEBUG_BREAK_TRUE, VoidOp, I16Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.DebugBreak();
e.L(skip);
}
};
struct DEBUG_BREAK_TRUE_I32
: Sequence<DEBUG_BREAK_TRUE_I32,
I<OPCODE_DEBUG_BREAK_TRUE, VoidOp, I32Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.DebugBreak();
e.L(skip);
}
};
struct DEBUG_BREAK_TRUE_I64
: Sequence<DEBUG_BREAK_TRUE_I64,
I<OPCODE_DEBUG_BREAK_TRUE, VoidOp, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.DebugBreak();
e.L(skip);
}
};
struct DEBUG_BREAK_TRUE_F32
: Sequence<DEBUG_BREAK_TRUE_F32,
I<OPCODE_DEBUG_BREAK_TRUE, VoidOp, F32Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.fmov(e.w0, i.src1);
auto& skip = e.NewCachedLabel();
e.cbz(e.w0, skip);
e.DebugBreak();
e.L(skip);
}
};
struct DEBUG_BREAK_TRUE_F64
: Sequence<DEBUG_BREAK_TRUE_F64,
I<OPCODE_DEBUG_BREAK_TRUE, VoidOp, F64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.fmov(e.x0, i.src1);
auto& skip = e.NewCachedLabel();
e.cbz(e.x0, skip);
e.DebugBreak();
e.L(skip);
}
};
EMITTER_OPCODE_TABLE(OPCODE_DEBUG_BREAK_TRUE, DEBUG_BREAK_TRUE_I8,
DEBUG_BREAK_TRUE_I16, DEBUG_BREAK_TRUE_I32,
DEBUG_BREAK_TRUE_I64, DEBUG_BREAK_TRUE_F32,
DEBUG_BREAK_TRUE_F64);
// ============================================================================
// OPCODE_TRAP
// ============================================================================
struct TRAP : Sequence<TRAP, I<OPCODE_TRAP, VoidOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.Trap(i.instr->flags);
}
};
EMITTER_OPCODE_TABLE(OPCODE_TRAP, TRAP);
// ============================================================================
// OPCODE_TRAP_TRUE
// ============================================================================
struct TRAP_TRUE_I8
: Sequence<TRAP_TRUE_I8, I<OPCODE_TRAP_TRUE, VoidOp, I8Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Trap(i.instr->flags);
e.L(skip);
}
};
struct TRAP_TRUE_I16
: Sequence<TRAP_TRUE_I16, I<OPCODE_TRAP_TRUE, VoidOp, I16Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Trap(i.instr->flags);
e.L(skip);
}
};
struct TRAP_TRUE_I32
: Sequence<TRAP_TRUE_I32, I<OPCODE_TRAP_TRUE, VoidOp, I32Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Trap(i.instr->flags);
e.L(skip);
}
};
struct TRAP_TRUE_I64
: Sequence<TRAP_TRUE_I64, I<OPCODE_TRAP_TRUE, VoidOp, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Trap(i.instr->flags);
e.L(skip);
}
};
EMITTER_OPCODE_TABLE(OPCODE_TRAP_TRUE, TRAP_TRUE_I8, TRAP_TRUE_I16,
TRAP_TRUE_I32, TRAP_TRUE_I64);
// ============================================================================
// OPCODE_RETURN_TRUE
// ============================================================================
struct RETURN_TRUE_I8
: Sequence<RETURN_TRUE_I8, I<OPCODE_RETURN_TRUE, VoidOp, I8Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.epilog_label());
}
};
struct RETURN_TRUE_I16
: Sequence<RETURN_TRUE_I16, I<OPCODE_RETURN_TRUE, VoidOp, I16Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.epilog_label());
}
};
struct RETURN_TRUE_I32
: Sequence<RETURN_TRUE_I32, I<OPCODE_RETURN_TRUE, VoidOp, I32Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.epilog_label());
}
};
struct RETURN_TRUE_I64
: Sequence<RETURN_TRUE_I64, I<OPCODE_RETURN_TRUE, VoidOp, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.cbnz(i.src1, e.epilog_label());
}
};
EMITTER_OPCODE_TABLE(OPCODE_RETURN_TRUE, RETURN_TRUE_I8, RETURN_TRUE_I16,
RETURN_TRUE_I32, RETURN_TRUE_I64);
// ============================================================================
// OPCODE_CALL
// ============================================================================
struct CALL : Sequence<CALL, I<OPCODE_CALL, VoidOp, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_true(i.src1.value->is_guest());
e.Call(i.instr, static_cast<GuestFunction*>(i.src1.value));
}
};
EMITTER_OPCODE_TABLE(OPCODE_CALL, CALL);
// ============================================================================
// OPCODE_CALL_TRUE
// ============================================================================
struct CALL_TRUE_I8
: Sequence<CALL_TRUE_I8, I<OPCODE_CALL_TRUE, VoidOp, I8Op, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_true(i.src2.value->is_guest());
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Call(i.instr, static_cast<GuestFunction*>(i.src2.value));
e.L(skip);
e.ForgetFpcrMode();
}
};
struct CALL_TRUE_I16
: Sequence<CALL_TRUE_I16, I<OPCODE_CALL_TRUE, VoidOp, I16Op, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_true(i.src2.value->is_guest());
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Call(i.instr, static_cast<GuestFunction*>(i.src2.value));
e.L(skip);
e.ForgetFpcrMode();
}
};
struct CALL_TRUE_I32
: Sequence<CALL_TRUE_I32, I<OPCODE_CALL_TRUE, VoidOp, I32Op, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_true(i.src2.value->is_guest());
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Call(i.instr, static_cast<GuestFunction*>(i.src2.value));
e.L(skip);
e.ForgetFpcrMode();
}
};
struct CALL_TRUE_I64
: Sequence<CALL_TRUE_I64, I<OPCODE_CALL_TRUE, VoidOp, I64Op, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_true(i.src2.value->is_guest());
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
e.Call(i.instr, static_cast<GuestFunction*>(i.src2.value));
e.L(skip);
e.ForgetFpcrMode();
}
};
struct CALL_TRUE_F32
: Sequence<CALL_TRUE_F32, I<OPCODE_CALL_TRUE, VoidOp, F32Op, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_always("CALL_TRUE with float condition is not possible");
}
};
struct CALL_TRUE_F64
: Sequence<CALL_TRUE_F64, I<OPCODE_CALL_TRUE, VoidOp, F64Op, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_always("CALL_TRUE with float condition is not possible");
}
};
EMITTER_OPCODE_TABLE(OPCODE_CALL_TRUE, CALL_TRUE_I8, CALL_TRUE_I16,
CALL_TRUE_I32, CALL_TRUE_I64, CALL_TRUE_F32,
CALL_TRUE_F64);
// ============================================================================
// OPCODE_CALL_INDIRECT
// ============================================================================
struct CALL_INDIRECT
: Sequence<CALL_INDIRECT, I<OPCODE_CALL_INDIRECT, VoidOp, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
if (i.src1.is_constant) [[unlikely]] {
if (i.src1.constant() == 0) {
e.nop();
} else {
e.mov(e.w16, static_cast<uint32_t>(i.src1.constant()));
e.CallIndirect(i.instr, 16);
}
} else {
e.CallIndirect(i.instr, i.src1.reg().getIdx());
}
e.ForgetFpcrMode();
}
};
EMITTER_OPCODE_TABLE(OPCODE_CALL_INDIRECT, CALL_INDIRECT);
// ============================================================================
// OPCODE_CALL_INDIRECT_TRUE
// ============================================================================
struct CALL_INDIRECT_TRUE_I8
: Sequence<CALL_INDIRECT_TRUE_I8,
I<OPCODE_CALL_INDIRECT_TRUE, VoidOp, I8Op, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
if (i.src2.is_constant) {
e.mov(e.w16, static_cast<uint32_t>(i.src2.constant()));
e.CallIndirect(i.instr, 16);
} else {
e.CallIndirect(i.instr, i.src2.reg().getIdx());
}
e.L(skip);
}
};
struct CALL_INDIRECT_TRUE_I16
: Sequence<CALL_INDIRECT_TRUE_I16,
I<OPCODE_CALL_INDIRECT_TRUE, VoidOp, I16Op, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
if (i.src2.is_constant) {
e.mov(e.w16, static_cast<uint32_t>(i.src2.constant()));
e.CallIndirect(i.instr, 16);
} else {
e.CallIndirect(i.instr, i.src2.reg().getIdx());
}
e.L(skip);
}
};
struct CALL_INDIRECT_TRUE_I32
: Sequence<CALL_INDIRECT_TRUE_I32,
I<OPCODE_CALL_INDIRECT_TRUE, VoidOp, I32Op, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
if (i.src2.is_constant) {
e.mov(e.w16, static_cast<uint32_t>(i.src2.constant()));
e.CallIndirect(i.instr, 16);
} else {
e.CallIndirect(i.instr, i.src2.reg().getIdx());
}
e.L(skip);
}
};
struct CALL_INDIRECT_TRUE_I64
: Sequence<CALL_INDIRECT_TRUE_I64,
I<OPCODE_CALL_INDIRECT_TRUE, VoidOp, I64Op, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
auto& skip = e.NewCachedLabel();
e.cbz(i.src1, skip);
if (i.src2.is_constant) {
e.mov(e.w16, static_cast<uint32_t>(i.src2.constant()));
e.CallIndirect(i.instr, 16);
} else {
e.CallIndirect(i.instr, i.src2.reg().getIdx());
}
e.L(skip);
}
};
struct CALL_INDIRECT_TRUE_F32
: Sequence<CALL_INDIRECT_TRUE_F32,
I<OPCODE_CALL_INDIRECT_TRUE, VoidOp, F32Op, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_always("CALL_INDIRECT_TRUE with float condition is not possible");
}
};
struct CALL_INDIRECT_TRUE_F64
: Sequence<CALL_INDIRECT_TRUE_F64,
I<OPCODE_CALL_INDIRECT_TRUE, VoidOp, F64Op, I64Op>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
assert_always("CALL_INDIRECT_TRUE with float condition is not possible");
}
};
EMITTER_OPCODE_TABLE(OPCODE_CALL_INDIRECT_TRUE, CALL_INDIRECT_TRUE_I8,
CALL_INDIRECT_TRUE_I16, CALL_INDIRECT_TRUE_I32,
CALL_INDIRECT_TRUE_I64, CALL_INDIRECT_TRUE_F32,
CALL_INDIRECT_TRUE_F64);
// ============================================================================
// OPCODE_CALL_EXTERN
// ============================================================================
struct CALL_EXTERN
: Sequence<CALL_EXTERN, I<OPCODE_CALL_EXTERN, VoidOp, SymbolOp>> {
static void Emit(A64Emitter& e, const EmitArgType& i) {
e.CallExtern(i.instr, i.src1.value);
}
};
EMITTER_OPCODE_TABLE(OPCODE_CALL_EXTERN, CALL_EXTERN);
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,342 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_SEQ_UTIL_H_
#define XENIA_CPU_BACKEND_A64_A64_SEQ_UTIL_H_
#include "xenia/base/memory.h"
#include "xenia/base/vec128.h"
#include "xenia/cpu/backend/a64/a64_backend.h"
#include "xenia/cpu/backend/a64/a64_emitter.h"
#include "xenia/cpu/backend/a64/a64_op.h"
#include "xenia/cpu/backend/a64/a64_stack_layout.h"
#include "xbyak_aarch64.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
using Xbyak_aarch64::QReg;
using Xbyak_aarch64::VReg;
using Xbyak_aarch64::WReg;
using Xbyak_aarch64::XReg;
// Load a compile-time vec128_t constant into a NEON register.
inline void LoadV128Const(A64Emitter& e, int vreg_idx, const vec128_t& val) {
e.mov(e.x0, val.low);
e.mov(e.x1, val.high);
e.stp(e.x0, e.x1,
Xbyak_aarch64::ptr(e.sp,
static_cast<int32_t>(StackLayout::GUEST_SCRATCH)));
e.ldr(QReg(vreg_idx),
Xbyak_aarch64::ptr(e.sp,
static_cast<int32_t>(StackLayout::GUEST_SCRATCH)));
}
// Resolve a V128 operand to a register index, loading constants into
// scratch_idx if needed.
template <typename T>
inline int SrcVReg(A64Emitter& e, const T& op, int scratch_idx) {
if (op.is_constant) {
LoadV128Const(e, scratch_idx, op.constant());
return scratch_idx;
}
return op.reg().getIdx();
}
// Byte-swap index within 32-bit lanes (for PPC big-endian conversion).
inline int bswap_lane_idx(int byte_idx) {
return (byte_idx & ~3) | (3 - (byte_idx & 3));
}
// Compute a guest memory address, returning the XReg for [x21, xN] addressing.
// For constants, loads the address into x0 (scratch).
inline XReg ComputeMemoryAddress(A64Emitter& e, const I64Op& guest) {
using namespace Xbyak_aarch64;
if (guest.is_constant) {
uint32_t address = static_cast<uint32_t>(guest.constant());
if (address >= 0xE0000000 &&
xe::memory::allocation_granularity() > 0x1000) {
address += 0x1000;
}
e.mov(e.x0, static_cast<uint64_t>(address));
return e.x0;
} else {
if (xe::memory::allocation_granularity() > 0x1000) {
auto src = guest.reg();
e.mov(e.w0, WReg(src.getIdx()));
e.mov(e.w17, 0xE0000000u);
e.cmp(e.w0, e.w17);
auto& skip = e.NewCachedLabel();
e.b(LO, skip);
// 0x1000 doesn't fit in a 12-bit immediate; use mov+add.
e.mov(e.w17, 0x1000u);
e.add(e.w0, e.w0, e.w17);
e.L(skip);
return e.x0;
}
return guest.reg();
}
}
// Flush denormal float32 lanes to zero in a NEON register (in-place).
// A float32 is denormal when 0 < abs(val) < 0x00800000.
// vreg must not equal sa or sb.
// This is needed because FPCR.FZ may not flush denormal inputs on all ARM64
// implementations (the ARM spec says input flushing is implementation-defined).
inline void FlushDenormals_V128(A64Emitter& e, int vreg, int sa = 2,
int sb = 3) {
// val<<1 removes the sign bit and doubles the value.
// Denormals become [0x00000002, 0x00FFFFFE]; zeros become 0x00000000.
// (val<<1) - 1: wraps 0→0xFFFFFFFF (excluded),
// denorms→[0x00000001,0x00FFFFFD]. Denormal iff ((val<<1) - 1) < 0x00FFFFFF
// (unsigned).
e.shl(VReg(sa).s4, VReg(vreg).s4, 1);
e.mov(e.w0, static_cast<uint64_t>(1u));
e.dup(VReg(sb).s4, e.w0);
e.sub(VReg(sa).s4, VReg(sa).s4, VReg(sb).s4);
e.mov(e.w0, static_cast<uint64_t>(0x00FFFFFFu));
e.dup(VReg(sb).s4, e.w0);
e.cmhi(VReg(sb).s4, VReg(sb).s4,
VReg(sa).s4); // mask: all-1s for denormal lanes
// Clear only bits 30:0 (preserve sign bit 31) so -denormal → -0, +denormal →
// +0.
e.ushr(VReg(sa).s4, VReg(sb).s4, 1); // sa = mask with bit 31 cleared
e.bic(VReg(vreg).b16, VReg(vreg).b16, VReg(sa).b16);
}
// Fixup for vmaxfp/vminfp when BOTH inputs are NaN.
// ARM64 fmax/fmin with DN=0 correctly propagates NaN when only one input is
// NaN, but when both are NaN it may quiet an SNaN differently than x64.
// x64 uses maxps(a,b)|maxps(b,a) which effectively gives src1|src2 for NaN
// lanes. We replicate that: use src1|src2 only for lanes where BOTH are NaN.
// Expects: v0=flushed src1, v1=flushed src2, v2=hardware fmax/fmin result.
// Modifies v2 in place. Clobbers v0, v1, v3.
inline void FixupVmxMaxMinNan(A64Emitter& e) {
// Compute OR fallback first (before clobbering v0/v1).
e.orr(VReg(3).b16, VReg(0).b16, VReg(1).b16); // v3 = src1 | src2
// Build "at least one not NaN" mask.
e.fcmeq(VReg(0).s4, VReg(0).s4, VReg(0).s4); // v0 = non-NaN mask for src1
e.fcmeq(VReg(1).s4, VReg(1).s4, VReg(1).s4); // v1 = non-NaN mask for src2
e.orr(VReg(0).b16, VReg(0).b16, VReg(1).b16); // v0 = 1 where at least one ok
// BSL: mask=1 → v2 (fmax result), mask=0 → v3 (src1|src2 for both-NaN)
e.bsl(VReg(0).b16, VReg(2).b16, VReg(3).b16);
e.mov(VReg(2).b16, VReg(0).b16);
}
// Prepare two V128 operands for a VMX FP operation: copy to scratch v0/v1
// and flush denormals. Returns the flushed register indices (always 0 and 1).
template <typename T1, typename T2>
inline void PrepareVmxFpSources(A64Emitter& e, const T1& op1, const T2& op2,
int& out_s1, int& out_s2) {
int s1 = SrcVReg(e, op1, 0);
int s2 = SrcVReg(e, op2, 1);
// Copy to scratch v0/v1 so we don't modify live allocated registers.
if (s1 != 0) e.mov(VReg(0).b16, VReg(s1).b16);
if (s2 != 1) e.mov(VReg(1).b16, VReg(s2).b16);
FlushDenormals_V128(e, 0);
FlushDenormals_V128(e, 1);
out_s1 = 0;
out_s2 = 1;
}
// Fix PPC NaN propagation for V128 float32 lanes after a NEON FP operation.
// Expects: v0=flushed src1, v1=flushed src2, v2=hardware FP result.
// Modifies v2 in place. Clobbers v0, v1, v3, w0, w16, w17.
// PPC rule: first NaN by operand position wins; SNaN is quieted (bit 22 set).
// If neither input was NaN but the op generated NaN (e.g., inf-inf),
// use the PPC default NaN (0xFFC00000).
inline void FixupVmxNan_V128(A64Emitter& e) {
using namespace Xbyak_aarch64;
auto& done = e.NewCachedLabel();
// Fast path: if no result lane is NaN, skip entirely.
e.fcmeq(VReg(3).s4, VReg(2).s4, VReg(2).s4); // all-1s for non-NaN
e.uminv(SReg(3), VReg(3).s4); // min across lanes
e.fmov(e.w0, SReg(3));
e.cbnz(e.w0, done); // all non-NaN → skip
// Save s1/s2 to stack for scalar lane extraction.
e.str(QReg(0), ptr(e.sp, static_cast<int32_t>(StackLayout::GUEST_SCRATCH)));
e.str(QReg(1),
ptr(e.sp, static_cast<int32_t>(StackLayout::GUEST_SCRATCH) + 16));
// NaN threshold: (val<<1) > 0xFF000000 means val is NaN.
e.mov(e.w16, 0xFF000000u);
for (int lane = 0; lane < 4; lane++) {
auto& lane_ok = e.NewCachedLabel();
auto& s1_not_nan = e.NewCachedLabel();
auto& use_default = e.NewCachedLabel();
// Check if result[lane] is NaN.
e.umov(e.w0, VReg(2).s4[lane]);
e.lsl(e.w17, e.w0, 1);
e.cmp(e.w17, e.w16);
e.b(LS, lane_ok);
// Result is NaN. Check s1[lane].
e.ldr(e.w0, ptr(e.sp, static_cast<int32_t>(StackLayout::GUEST_SCRATCH) +
lane * 4));
e.lsl(e.w17, e.w0, 1);
e.cmp(e.w17, e.w16);
e.b(LS, s1_not_nan);
// s1 is NaN: quiet it and insert.
e.orr(e.w0, e.w0, static_cast<uint64_t>(1u << 22));
e.ins(VReg(2).s4[lane], e.w0);
e.b(lane_ok);
e.L(s1_not_nan);
// Check s2[lane].
e.ldr(e.w0, ptr(e.sp, static_cast<int32_t>(StackLayout::GUEST_SCRATCH) +
16 + lane * 4));
e.lsl(e.w17, e.w0, 1);
e.cmp(e.w17, e.w16);
e.b(LS, use_default);
// s2 is NaN: quiet it and insert.
e.orr(e.w0, e.w0, static_cast<uint64_t>(1u << 22));
e.ins(VReg(2).s4[lane], e.w0);
e.b(lane_ok);
e.L(use_default);
// Generated NaN (neither input was NaN): use PPC default NaN.
e.mov(e.w0, 0xFFC00000u);
e.ins(VReg(2).s4[lane], e.w0);
e.L(lane_ok);
}
e.L(done);
}
// Fix PPC NaN propagation for V128 FMA result (3 source operands).
// Expects: result in v2, flushed sources saved on stack at:
// GUEST_SCRATCH + 0 = src1 (16 bytes)
// GUEST_SCRATCH + 16 = src2 (16 bytes)
// GUEST_SCRATCH + 32 = src3 (16 bytes)
// PPC rule: first NaN by operand position (src1 > src2 > src3) wins.
// Clobbers v0, v1, v3, w0, w16, w17.
inline void FixupVmxNan_V128_Fma(A64Emitter& e) {
using namespace Xbyak_aarch64;
auto& done = e.NewCachedLabel();
// Fast path: if no result lane is NaN, skip entirely.
e.fcmeq(VReg(3).s4, VReg(2).s4, VReg(2).s4);
e.uminv(SReg(3), VReg(3).s4);
e.fmov(e.w0, SReg(3));
e.cbnz(e.w0, done);
// NaN threshold constant.
e.mov(e.w16, 0xFF000000u);
for (int lane = 0; lane < 4; lane++) {
auto& lane_ok = e.NewCachedLabel();
auto& s1_not_nan = e.NewCachedLabel();
auto& s2_not_nan = e.NewCachedLabel();
auto& use_default = e.NewCachedLabel();
// Check if result[lane] is NaN.
e.umov(e.w0, VReg(2).s4[lane]);
e.lsl(e.w17, e.w0, 1);
e.cmp(e.w17, e.w16);
e.b(LS, lane_ok);
// Result is NaN. Check src1[lane].
e.ldr(e.w0, ptr(e.sp, static_cast<int32_t>(StackLayout::GUEST_SCRATCH) +
lane * 4));
e.lsl(e.w17, e.w0, 1);
e.cmp(e.w17, e.w16);
e.b(LS, s1_not_nan);
e.orr(e.w0, e.w0, static_cast<uint64_t>(1u << 22));
e.ins(VReg(2).s4[lane], e.w0);
e.b(lane_ok);
e.L(s1_not_nan);
// Check src2[lane].
e.ldr(e.w0, ptr(e.sp, static_cast<int32_t>(StackLayout::GUEST_SCRATCH) +
16 + lane * 4));
e.lsl(e.w17, e.w0, 1);
e.cmp(e.w17, e.w16);
e.b(LS, s2_not_nan);
e.orr(e.w0, e.w0, static_cast<uint64_t>(1u << 22));
e.ins(VReg(2).s4[lane], e.w0);
e.b(lane_ok);
e.L(s2_not_nan);
// Check src3[lane].
e.ldr(e.w0, ptr(e.sp, static_cast<int32_t>(StackLayout::GUEST_SCRATCH) +
32 + lane * 4));
e.lsl(e.w17, e.w0, 1);
e.cmp(e.w17, e.w16);
e.b(LS, use_default);
e.orr(e.w0, e.w0, static_cast<uint64_t>(1u << 22));
e.ins(VReg(2).s4[lane], e.w0);
e.b(lane_ok);
e.L(use_default);
e.mov(e.w0, 0xFFC00000u);
e.ins(VReg(2).s4[lane], e.w0);
e.L(lane_ok);
}
e.L(done);
}
// VMX float32x4 binary operations with full PPC semantics.
enum class VmxFpBinOp { Add, Sub, Mul, Div };
// Execute a VMX float32x4 binary operation with denormal flushing and PPC NaN
// propagation. Result goes into dest_idx.
// Clobbers v0-v3, w0, w16, w17.
template <typename T1, typename T2>
inline void EmitVmxFpBinOp_V128(A64Emitter& e, int dest_idx, const T1& src1,
const T2& src2, VmxFpBinOp op) {
e.ChangeFpcrMode(FPCRMode::Vmx);
// Flush input denormals → v0=s1, v1=s2.
int s1, s2;
PrepareVmxFpSources(e, src1, src2, s1, s2);
// Hardware FP op → v2.
switch (op) {
case VmxFpBinOp::Add:
e.fadd(VReg(2).s4, VReg(s1).s4, VReg(s2).s4);
break;
case VmxFpBinOp::Sub:
e.fsub(VReg(2).s4, VReg(s1).s4, VReg(s2).s4);
break;
case VmxFpBinOp::Mul:
e.fmul(VReg(2).s4, VReg(s1).s4, VReg(s2).s4);
break;
case VmxFpBinOp::Div:
e.fdiv(VReg(2).s4, VReg(s1).s4, VReg(s2).s4);
break;
}
// PPC NaN propagation fixup (fast-path skip when no NaN).
FixupVmxNan_V128(e);
// Flush output denormals.
FlushDenormals_V128(e, 2, 0, 1);
// Move to dest.
e.mov(VReg(dest_idx).b16, VReg(2).b16);
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_SEQ_UTIL_H_

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_SEQUENCES_H_
#define XENIA_CPU_BACKEND_A64_A64_SEQUENCES_H_
#include <unordered_map>
#include "xenia/cpu/hir/instr.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class A64Emitter;
typedef bool (*SequenceSelectFn)(A64Emitter&, const hir::Instr*, uint32_t ikey);
extern std::unordered_map<uint32_t, SequenceSelectFn> sequence_table;
template <typename T>
bool Register() {
sequence_table.insert({T::head_key(), T::Select});
return true;
}
template <typename T, typename Tn, typename... Ts>
static bool Register() {
bool b = true;
b = b && Register<T>();
b = b && Register<Tn, Ts...>();
return b;
}
#define EMITTER_OPCODE_TABLE(name, ...) \
const auto A64_INSTR_##name = Register<__VA_ARGS__>();
bool SelectSequence(A64Emitter* e, const hir::Instr* i,
const hir::Instr** new_tail);
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_SEQUENCES_H_

View File

@@ -0,0 +1,85 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_STACK_LAYOUT_H_
#define XENIA_CPU_BACKEND_A64_A64_STACK_LAYOUT_H_
#include "xenia/base/vec128.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class StackLayout {
public:
/**
* ARM64 Thunk Stack Layout
* NOTE: stack must always be 16-byte aligned.
*
* Thunk (HostToGuest/GuestToHost):
* +------------------+
* | x19 | sp + 0x000
* | x20 (context) | sp + 0x008
* | x21 (membase) | sp + 0x010
* | x22 | sp + 0x018
* | x23 | sp + 0x020
* | x24 | sp + 0x028
* | x25 | sp + 0x030
* | x26 | sp + 0x038
* | x27 | sp + 0x040
* | x28 | sp + 0x048
* | x29 (fp) | sp + 0x050
* | x30 (lr) | sp + 0x058
* | d8 | sp + 0x060
* | d9 | sp + 0x068
* | d10 | sp + 0x070
* | d11 | sp + 0x078
* | d12 | sp + 0x080
* | d13 | sp + 0x088
* | d14 | sp + 0x090
* | d15 | sp + 0x098
* +------------------+
* Total: 0xA0 = 160 bytes (already 16-byte aligned)
*/
static constexpr size_t THUNK_STACK_SIZE = 160;
/**
* ARM64 Guest Stack Layout
* +------------------+
* | scratch, 48b | sp + 0x000 (3 x Q for VMX FP scratch)
* | guest ret addr | sp + 0x030 (guest PPC return address)
* | call ret addr | sp + 0x038 (next call's guest PPC return addr)
* | host ret addr | sp + 0x040 (host x30/LR, for ret instruction)
* | guest saved r1 | sp + 0x048 (guest r1 at function entry, for
* | | longjmp detection)
* | ... locals ... |
* +------------------+
*
* Minimum size: 80 bytes (aligned to 16).
*
* Convention: at guest function entry, x0 holds the guest PPC return
* address. The prolog stores it to GUEST_RET_ADDR and saves x30 (host
* LR) to HOST_RET_ADDR.
*/
static constexpr size_t GUEST_STACK_SIZE = 80; // 16-byte aligned
static constexpr size_t GUEST_SCRATCH = 0; // 48 bytes (3 x Q)
static constexpr size_t GUEST_RET_ADDR = 48;
static constexpr size_t GUEST_CALL_RET_ADDR = 56;
static constexpr size_t HOST_RET_ADDR = 64;
// Stackpoint depth after PushStackpoint in prolog, for longjmp detection.
static constexpr size_t GUEST_SAVED_STACKPOINT_DEPTH = 72;
};
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_STACK_LAYOUT_H_

View File

@@ -0,0 +1,231 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "xenia/cpu/backend/a64/a64_tracers.h"
#include <cstring>
#include "xenia/base/logging.h"
#include "xenia/base/vec128.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/thread_state.h"
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
#define ITRACE 0
#define DTRACE 0
#define TARGET_THREAD 0
bool trace_enabled = true;
#define THREAD_MATCH (!TARGET_THREAD || ppc_context->thread_id == TARGET_THREAD)
#define IFLUSH()
#define IPRINT(s) \
if (trace_enabled && THREAD_MATCH) \
xe::logging::AppendLogLine(xe::LogLevel::Debug, 't', s, xe::LogSrc::Cpu)
#define DFLUSH()
#define DPRINT(...) \
if (trace_enabled && THREAD_MATCH) \
xe::logging::AppendLogLineFormat(xe::LogSrc::Cpu, xe::LogLevel::Debug, 't', \
__VA_ARGS__)
// Helper to read float/int lanes from a V128 passed as const uint8_t*.
static inline float v128_f32(const uint8_t* v, int lane) {
float f;
std::memcpy(&f, v + lane * 4, 4);
return f;
}
static inline uint32_t v128_i32(const uint8_t* v, int lane) {
uint32_t i;
std::memcpy(&i, v + lane * 4, 4);
return i;
}
uint32_t GetTracingMode() {
uint32_t mode = 0;
#if ITRACE
mode |= TRACING_INSTR;
#endif
#if DTRACE
mode |= TRACING_DATA;
#endif
return mode;
}
void TraceString(void* raw_context, const char* str) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
IPRINT(str);
IFLUSH();
}
void TraceContextLoadI8(void* raw_context, uint64_t offset, uint8_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = ctx i8 +{}\n", (int8_t)value, value, offset);
}
void TraceContextLoadI16(void* raw_context, uint64_t offset, uint16_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = ctx i16 +{}\n", (int16_t)value, value, offset);
}
void TraceContextLoadI32(void* raw_context, uint64_t offset, uint32_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = ctx i32 +{}\n", (int32_t)value, value, offset);
}
void TraceContextLoadI64(void* raw_context, uint64_t offset, uint64_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = ctx i64 +{}\n", (int64_t)value, value, offset);
}
void TraceContextLoadF32(void* raw_context, uint64_t offset, float value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint32_t iv;
std::memcpy(&iv, &value, 4);
DPRINT("{} ({:X}) = ctx f32 +{}\n", value, iv, offset);
}
void TraceContextLoadF64(void* raw_context, uint64_t offset, double value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint64_t iv;
std::memcpy(&iv, &value, 8);
DPRINT("{} ({:X}) = ctx f64 +{}\n", value, iv, offset);
}
void TraceContextLoadV128(void* raw_context, uint64_t offset,
const uint8_t* value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("[{}, {}, {}, {}] [{:08X}, {:08X}, {:08X}, {:08X}] = ctx v128 +{}\n",
v128_f32(value, 0), v128_f32(value, 1), v128_f32(value, 2),
v128_f32(value, 3), v128_i32(value, 0), v128_i32(value, 1),
v128_i32(value, 2), v128_i32(value, 3), offset);
}
void TraceContextStoreI8(void* raw_context, uint64_t offset, uint8_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("ctx i8 +{} = {} ({:X})\n", offset, (int8_t)value, value);
}
void TraceContextStoreI16(void* raw_context, uint64_t offset, uint16_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("ctx i16 +{} = {} ({:X})\n", offset, (int16_t)value, value);
}
void TraceContextStoreI32(void* raw_context, uint64_t offset, uint32_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("ctx i32 +{} = {} ({:X})\n", offset, (int32_t)value, value);
}
void TraceContextStoreI64(void* raw_context, uint64_t offset, uint64_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("ctx i64 +{} = {} ({:X})\n", offset, (int64_t)value, value);
}
void TraceContextStoreF32(void* raw_context, uint64_t offset, float value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint32_t iv;
std::memcpy(&iv, &value, 4);
DPRINT("ctx f32 +{} = {} ({:X})\n", offset, value, iv);
}
void TraceContextStoreF64(void* raw_context, uint64_t offset, double value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint64_t iv;
std::memcpy(&iv, &value, 8);
DPRINT("ctx f64 +{} = {} ({:X})\n", offset, value, iv);
}
void TraceContextStoreV128(void* raw_context, uint64_t offset,
const uint8_t* value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("ctx v128 +{} = [{}, {}, {}, {}] [{:08X}, {:08X}, {:08X}, {:08X}]\n",
offset, v128_f32(value, 0), v128_f32(value, 1), v128_f32(value, 2),
v128_f32(value, 3), v128_i32(value, 0), v128_i32(value, 1),
v128_i32(value, 2), v128_i32(value, 3));
}
void TraceMemoryLoadI8(void* raw_context, uint32_t address, uint8_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = load.i8 {:08X}\n", (int8_t)value, value, address);
}
void TraceMemoryLoadI16(void* raw_context, uint32_t address, uint16_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = load.i16 {:08X}\n", (int16_t)value, value, address);
}
void TraceMemoryLoadI32(void* raw_context, uint32_t address, uint32_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = load.i32 {:08X}\n", (int32_t)value, value, address);
}
void TraceMemoryLoadI64(void* raw_context, uint32_t address, uint64_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("{} ({:X}) = load.i64 {:08X}\n", (int64_t)value, value, address);
}
void TraceMemoryLoadF32(void* raw_context, uint32_t address, float value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint32_t iv;
std::memcpy(&iv, &value, 4);
DPRINT("{} ({:X}) = load.f32 {:08X}\n", value, iv, address);
}
void TraceMemoryLoadF64(void* raw_context, uint32_t address, double value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint64_t iv;
std::memcpy(&iv, &value, 8);
DPRINT("{} ({:X}) = load.f64 {:08X}\n", value, iv, address);
}
void TraceMemoryLoadV128(void* raw_context, uint32_t address,
const uint8_t* value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT(
"[{}, {}, {}, {}] [{:08X}, {:08X}, {:08X}, {:08X}] = load.v128 {:08X}\n",
v128_f32(value, 0), v128_f32(value, 1), v128_f32(value, 2),
v128_f32(value, 3), v128_i32(value, 0), v128_i32(value, 1),
v128_i32(value, 2), v128_i32(value, 3), address);
}
void TraceMemoryStoreI8(void* raw_context, uint32_t address, uint8_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("store.i8 {:08X} = {} ({:X})\n", address, (int8_t)value, value);
}
void TraceMemoryStoreI16(void* raw_context, uint32_t address, uint16_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("store.i16 {:08X} = {} ({:X})\n", address, (int16_t)value, value);
}
void TraceMemoryStoreI32(void* raw_context, uint32_t address, uint32_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("store.i32 {:08X} = {} ({:X})\n", address, (int32_t)value, value);
}
void TraceMemoryStoreI64(void* raw_context, uint32_t address, uint64_t value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("store.i64 {:08X} = {} ({:X})\n", address, (int64_t)value, value);
}
void TraceMemoryStoreF32(void* raw_context, uint32_t address, float value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint32_t iv;
std::memcpy(&iv, &value, 4);
DPRINT("store.f32 {:08X} = {} ({:X})\n", address, value, iv);
}
void TraceMemoryStoreF64(void* raw_context, uint32_t address, double value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
uint64_t iv;
std::memcpy(&iv, &value, 8);
DPRINT("store.f64 {:08X} = {} ({:X})\n", address, value, iv);
}
void TraceMemoryStoreV128(void* raw_context, uint32_t address,
const uint8_t* value) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT(
"store.v128 {:08X} = [{}, {}, {}, {}] [{:08X}, {:08X}, {:08X}, {:08X}]\n",
address, v128_f32(value, 0), v128_f32(value, 1), v128_f32(value, 2),
v128_f32(value, 3), v128_i32(value, 0), v128_i32(value, 1),
v128_i32(value, 2), v128_i32(value, 3));
}
void TraceMemset(void* raw_context, uint32_t address, uint8_t value,
uint32_t length) {
auto ppc_context = reinterpret_cast<ppc::PPCContext*>(raw_context);
DPRINT("memset {:08X}-{:08X} ({}) = {:02X}", address, address + length,
length, value);
}
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe

View File

@@ -0,0 +1,77 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#ifndef XENIA_CPU_BACKEND_A64_A64_TRACERS_H_
#define XENIA_CPU_BACKEND_A64_A64_TRACERS_H_
#include <cstdint>
namespace xe {
namespace cpu {
namespace backend {
namespace a64 {
class A64Emitter;
enum TracingMode {
TRACING_INSTR = (1 << 1),
TRACING_DATA = (1 << 2),
};
uint32_t GetTracingMode();
inline bool IsTracingInstr() { return (GetTracingMode() & TRACING_INSTR) != 0; }
inline bool IsTracingData() { return (GetTracingMode() & TRACING_DATA) != 0; }
void TraceString(void* raw_context, const char* str);
void TraceContextLoadI8(void* raw_context, uint64_t offset, uint8_t value);
void TraceContextLoadI16(void* raw_context, uint64_t offset, uint16_t value);
void TraceContextLoadI32(void* raw_context, uint64_t offset, uint32_t value);
void TraceContextLoadI64(void* raw_context, uint64_t offset, uint64_t value);
void TraceContextLoadF32(void* raw_context, uint64_t offset, float value);
void TraceContextLoadF64(void* raw_context, uint64_t offset, double value);
void TraceContextLoadV128(void* raw_context, uint64_t offset,
const uint8_t* value);
void TraceContextStoreI8(void* raw_context, uint64_t offset, uint8_t value);
void TraceContextStoreI16(void* raw_context, uint64_t offset, uint16_t value);
void TraceContextStoreI32(void* raw_context, uint64_t offset, uint32_t value);
void TraceContextStoreI64(void* raw_context, uint64_t offset, uint64_t value);
void TraceContextStoreF32(void* raw_context, uint64_t offset, float value);
void TraceContextStoreF64(void* raw_context, uint64_t offset, double value);
void TraceContextStoreV128(void* raw_context, uint64_t offset,
const uint8_t* value);
void TraceMemoryLoadI8(void* raw_context, uint32_t address, uint8_t value);
void TraceMemoryLoadI16(void* raw_context, uint32_t address, uint16_t value);
void TraceMemoryLoadI32(void* raw_context, uint32_t address, uint32_t value);
void TraceMemoryLoadI64(void* raw_context, uint32_t address, uint64_t value);
void TraceMemoryLoadF32(void* raw_context, uint32_t address, float value);
void TraceMemoryLoadF64(void* raw_context, uint32_t address, double value);
void TraceMemoryLoadV128(void* raw_context, uint32_t address,
const uint8_t* value);
void TraceMemoryStoreI8(void* raw_context, uint32_t address, uint8_t value);
void TraceMemoryStoreI16(void* raw_context, uint32_t address, uint16_t value);
void TraceMemoryStoreI32(void* raw_context, uint32_t address, uint32_t value);
void TraceMemoryStoreI64(void* raw_context, uint32_t address, uint64_t value);
void TraceMemoryStoreF32(void* raw_context, uint32_t address, float value);
void TraceMemoryStoreF64(void* raw_context, uint32_t address, double value);
void TraceMemoryStoreV128(void* raw_context, uint32_t address,
const uint8_t* value);
void TraceMemset(void* raw_context, uint32_t address, uint8_t value,
uint32_t length);
} // namespace a64
} // namespace backend
} // namespace cpu
} // namespace xe
#endif // XENIA_CPU_BACKEND_A64_A64_TRACERS_H_

View File

@@ -29,9 +29,11 @@ target_link_libraries(xenia-cpu-ppc-tests PRIVATE
capstone fmt imgui mspack
)
# x64 backend
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
# Architecture-specific backend
if(XE_TARGET_X86_64)
target_link_libraries(xenia-cpu-ppc-tests PRIVATE xenia-cpu-backend-x64)
elseif(XE_TARGET_AARCH64)
target_link_libraries(xenia-cpu-ppc-tests PRIVATE xenia-cpu-backend-a64)
endif()
xe_target_defaults(xenia-cpu-ppc-tests)

View File

@@ -29,6 +29,8 @@
#if XE_ARCH_AMD64
#include "xenia/cpu/backend/x64/x64_backend.h"
#elif XE_ARCH_ARM64
#include "xenia/cpu/backend/a64/a64_backend.h"
#endif // XE_ARCH
#if XE_COMPILER_MSVC
@@ -248,11 +250,17 @@ class TestRunner {
if (cvars::cpu == "x64") {
backend.reset(new xe::cpu::backend::x64::X64Backend());
}
#elif XE_ARCH_ARM64
if (cvars::cpu == "a64") {
backend.reset(new xe::cpu::backend::a64::A64Backend());
}
#endif // XE_ARCH
if (cvars::cpu == "any") {
if (!backend) {
#if XE_ARCH_AMD64
backend.reset(new xe::cpu::backend::x64::X64Backend());
#elif XE_ARCH_ARM64
backend.reset(new xe::cpu::backend::a64::A64Backend());
#endif // XE_ARCH
}
}
@@ -310,6 +318,21 @@ class TestRunner {
x64_backend->BackendContextForGuestContext(thread_state_->context());
bctx->flags &= ~(1U << xe::cpu::backend::x64::kX64BackendMXCSRModeBit);
}
#elif XE_ARCH_ARM64
// Reset FPCR and backend flags to default FPU state before each test.
{
auto* a64_backend = static_cast<xe::cpu::backend::a64::A64Backend*>(
processor_->backend());
auto* bctx =
a64_backend->BackendContextForGuestContext(thread_state_->context());
bctx->flags &= ~(1U << xe::cpu::backend::a64::kA64BackendFPCRModeBit);
// Explicitly reset the hardware FPCR to default FPU mode (0 = round
// nearest, no flush-to-zero, no default-NaN). Without this, a previous
// test that set VMX mode (FZ|DN) leaves the hardware FPCR dirty, and
// subsequent scalar FP tests produce wrong NaN results because DN=1
// causes ARM64 to return the default NaN instead of propagating inputs.
a64_backend->SetGuestRoundingMode(thread_state_->context(), 0);
}
#endif
// Execute test.

View File

@@ -34,7 +34,11 @@
#include "xenia/cpu/xex_module.h"
// TODO(benvanik): based on compiler support
#if XE_ARCH_AMD64
#include "xenia/cpu/backend/x64/x64_backend.h"
#elif XE_ARCH_ARM64
#include "xenia/cpu/backend/a64/a64_backend.h"
#endif
#if 0 && DEBUG
#define DEFAULT_DEBUG_FLAG true

View File

@@ -173,40 +173,71 @@ class Win32StackWalker : public StackWalker {
} else {
// Copy thread context local. We will be modifying it during stack
// walking, so we don't want to mess with the incoming copy.
#if XE_ARCH_AMD64
thread_context.Rip = in_host_context->rip;
thread_context.EFlags = in_host_context->eflags;
std::memcpy(&thread_context.Rax, in_host_context->int_registers,
sizeof(in_host_context->int_registers));
std::memcpy(&thread_context.Xmm0, in_host_context->xmm_registers,
sizeof(in_host_context->xmm_registers));
#elif XE_ARCH_ARM64
thread_context.Pc = in_host_context->pc;
thread_context.Cpsr = in_host_context->pstate;
thread_context.Sp = in_host_context->sp;
std::memcpy(&thread_context.X0, in_host_context->x,
sizeof(in_host_context->x));
std::memcpy(&thread_context.V[0], in_host_context->v,
sizeof(in_host_context->v));
#endif
}
if (out_host_context) {
// Write out the captured thread context if the caller asked for it.
#if XE_ARCH_AMD64
out_host_context->rip = thread_context.Rip;
out_host_context->eflags = thread_context.EFlags;
std::memcpy(out_host_context->int_registers, &thread_context.Rax,
sizeof(out_host_context->int_registers));
std::memcpy(out_host_context->xmm_registers, &thread_context.Xmm0,
sizeof(out_host_context->xmm_registers));
#elif XE_ARCH_ARM64
out_host_context->pc = thread_context.Pc;
out_host_context->pstate = thread_context.Cpsr;
out_host_context->sp = thread_context.Sp;
std::memcpy(out_host_context->x, &thread_context.X0,
sizeof(out_host_context->x));
std::memcpy(out_host_context->v, &thread_context.V[0],
sizeof(out_host_context->v));
#endif
}
// Setup the frame for walking.
STACKFRAME64 stack_frame = {};
stack_frame.AddrPC.Mode = AddrModeFlat;
stack_frame.AddrPC.Offset = thread_context.Rip;
stack_frame.AddrFrame.Mode = AddrModeFlat;
stack_frame.AddrFrame.Offset = thread_context.Rbp;
stack_frame.AddrStack.Mode = AddrModeFlat;
#if XE_ARCH_AMD64
stack_frame.AddrPC.Offset = thread_context.Rip;
stack_frame.AddrFrame.Offset = thread_context.Rbp;
stack_frame.AddrStack.Offset = thread_context.Rsp;
DWORD machine_type = IMAGE_FILE_MACHINE_AMD64;
#elif XE_ARCH_ARM64
stack_frame.AddrPC.Offset = thread_context.Pc;
stack_frame.AddrFrame.Offset = thread_context.Fp;
stack_frame.AddrStack.Offset = thread_context.Sp;
DWORD machine_type = IMAGE_FILE_MACHINE_ARM64;
#endif
// Walk the stack.
// Note that StackWalk64 is thread safe, though other dbghelp functions are
// not.
// TODO(has207): verify StackWalk64 + dbghelp callbacks work for JIT'd
// ARM64 code with dynamically registered unwind info
// (RtlAddGrowableFunctionTable).
size_t frame_index = 0;
while (frame_index < frame_count &&
stack_walk_64_(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(),
thread_handle, &stack_frame, &thread_context, nullptr,
stack_walk_64_(machine_type, GetCurrentProcess(), thread_handle,
&stack_frame, &thread_context, nullptr,
XSymFunctionTableAccess64, XSymGetModuleBase64,
nullptr) == TRUE) {
if (frame_index >= frame_offset) {

View File

@@ -3,5 +3,6 @@ xe_test_suite(xenia-cpu-tests ${CMAKE_CURRENT_SOURCE_DIR}
capstone fmt imgui
xenia-base xenia-core xenia-cpu xenia-gpu
xenia-kernel xenia-ui xenia-patcher
$<$<OR:$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},x86_64>,$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},AMD64>>:xenia-cpu-backend-x64>
$<$<BOOL:${XE_TARGET_X86_64}>:xenia-cpu-backend-x64>
$<$<BOOL:${XE_TARGET_AARCH64}>:xenia-cpu-backend-a64>
)

View File

@@ -13,7 +13,11 @@
#include <vector>
#include "xenia/base/platform.h"
#if XE_ARCH_AMD64
#include "xenia/cpu/backend/x64/x64_backend.h"
#elif XE_ARCH_ARM64
#include "xenia/cpu/backend/a64/a64_backend.h"
#endif // XE_ARCH
#include "xenia/cpu/hir/hir_builder.h"
#include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/ppc/ppc_frontend.h"
@@ -38,6 +42,8 @@ class TestFunction {
std::unique_ptr<xe::cpu::backend::Backend> backend;
#if XE_ARCH_AMD64
backend.reset(new xe::cpu::backend::x64::X64Backend());
#elif XE_ARCH_ARM64
backend.reset(new xe::cpu::backend::a64::A64Backend());
#endif // XE_ARCH
if (backend) {
auto processor = std::make_unique<Processor>(memory.get(), nullptr);

View File

@@ -970,6 +970,7 @@ void DebugWindow::DrawRegistersPane() {
} break;
case RegisterGroup::kHostGeneral: {
ImGui::BeginChild("##host_general");
#if XE_ARCH_AMD64
for (int i = 0; i < 18; ++i) {
auto reg = static_cast<X64Register>(i);
ImGui::BeginGroup();
@@ -990,10 +991,37 @@ void DebugWindow::DrawRegistersPane() {
}
ImGui::EndGroup();
}
#elif XE_ARCH_ARM64
// x0-x30, sp, pc, pstate
for (int i = 0; i < 34; ++i) {
auto reg = static_cast<Arm64Register>(i);
ImGui::BeginGroup();
ImGui::AlignTextToFramePadding();
ImGui::Text("%5s", HostThreadContext::GetRegisterName(reg));
ImGui::SameLine();
ImGui::Dummy(ImVec2(4, 0));
ImGui::SameLine();
if (i < 31) {
dirty_host_context |=
DrawRegisterTextBox(i, &thread_info->host_context.x[i]);
} else if (i == 31) {
dirty_host_context |=
DrawRegisterTextBox(i, &thread_info->host_context.sp);
} else if (i == 32) {
dirty_host_context |=
DrawRegisterTextBox(i, &thread_info->host_context.pc);
} else {
dirty_host_context |=
DrawRegisterTextBox(i, &thread_info->host_context.pstate);
}
ImGui::EndGroup();
}
#endif
ImGui::EndChild();
} break;
case RegisterGroup::kHostVector: {
ImGui::BeginChild("##host_vector");
#if XE_ARCH_AMD64
for (int i = 0; i < 16; ++i) {
auto reg =
static_cast<X64Register>(static_cast<int>(X64Register::kXmm0) + i);
@@ -1007,6 +1035,21 @@ void DebugWindow::DrawRegistersPane() {
i, thread_info->host_context.xmm_registers[i].f32);
ImGui::EndGroup();
}
#elif XE_ARCH_ARM64
for (int i = 0; i < 32; ++i) {
auto reg = static_cast<Arm64Register>(
static_cast<int>(Arm64Register::kV0) + i);
ImGui::BeginGroup();
ImGui::AlignTextToFramePadding();
ImGui::Text("%5s", HostThreadContext::GetRegisterName(reg));
ImGui::SameLine();
ImGui::Dummy(ImVec2(4, 0));
ImGui::SameLine();
dirty_host_context |=
DrawRegisterTextBoxes(i, thread_info->host_context.v[i].f32);
ImGui::EndGroup();
}
#endif
ImGui::EndChild();
}
}

View File

@@ -63,6 +63,8 @@
#if XE_ARCH_AMD64
#include "xenia/cpu/backend/x64/x64_backend.h"
#elif XE_ARCH_ARM64
#include "xenia/cpu/backend/a64/a64_backend.h"
#endif // XE_ARCH
DEFINE_double(time_scalar, 1.0,
@@ -232,11 +234,17 @@ X_STATUS Emulator::Setup(
if (cvars::cpu == "x64") {
backend.reset(new xe::cpu::backend::x64::X64Backend());
}
#elif XE_ARCH_ARM64
if (cvars::cpu == "a64") {
backend.reset(new xe::cpu::backend::a64::A64Backend());
}
#endif // XE_ARCH
if (cvars::cpu == "any") {
if (!backend) {
#if XE_ARCH_AMD64
backend.reset(new xe::cpu::backend::x64::X64Backend());
#elif XE_ARCH_ARM64
backend.reset(new xe::cpu::backend::a64::A64Backend());
#endif // XE_ARCH
}
}

View File

@@ -18,7 +18,7 @@ if(XENIA_BUILD_MISC)
xenia-kernel xenia-patcher xenia-ui xenia-ui-d3d12 xenia-vfs
aes_128 capstone dxbc fmt imgui libavcodec libavutil mspack snappy xxhash
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
if(XE_TARGET_X86_64)
target_link_libraries(xenia-gpu-d3d12-trace-viewer PRIVATE xenia-cpu-backend-x64)
endif()
xe_target_defaults(xenia-gpu-d3d12-trace-viewer)
@@ -34,7 +34,7 @@ if(XENIA_BUILD_MISC)
xenia-kernel xenia-patcher xenia-ui xenia-ui-d3d12 xenia-vfs
aes_128 capstone dxbc fmt imgui libavcodec libavutil mspack snappy xxhash
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
if(XE_TARGET_X86_64)
target_link_libraries(xenia-gpu-d3d12-trace-dump PRIVATE xenia-cpu-backend-x64)
endif()
xe_target_defaults(xenia-gpu-d3d12-trace-dump)

View File

@@ -32,7 +32,7 @@ if(XENIA_BUILD_MISC)
xenia-kernel xenia-patcher xenia-ui xenia-ui-vulkan xenia-vfs
aes_128 capstone fmt glslang-spirv imgui libavcodec libavutil mspack snappy xxhash
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
if(XE_TARGET_X86_64)
target_link_libraries(xenia-gpu-vulkan-trace-viewer PRIVATE xenia-cpu-backend-x64)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
@@ -61,7 +61,7 @@ if(XENIA_BUILD_MISC)
xenia-kernel xenia-patcher xenia-ui xenia-ui-vulkan xenia-vfs
aes_128 capstone fmt glslang-spirv imgui libavcodec libavutil mspack snappy xxhash
)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
if(XE_TARGET_X86_64)
target_link_libraries(xenia-gpu-vulkan-trace-dump PRIVATE xenia-cpu-backend-x64)
endif()
xe_target_defaults(xenia-gpu-vulkan-trace-dump)

View File

@@ -75,7 +75,13 @@ struct HostExceptionReport {
: ExceptionInfo(_ExceptionInfo),
Report_Scratchpos(0u),
last_win32_error(GetLastError()),
#if XE_ARCH_AMD64
last_ntstatus(__readgsdword(0x1250)),
#elif XE_ARCH_ARM64
// TEB.LastStatusValue at offset 0x1250 (same as x64).
// ARM64 uses x18 register for TEB base instead of GS segment.
last_ntstatus(__readx18dword(0x1250)),
#endif
errno_value(errno),
address_format_ring_index(0)
@@ -168,9 +174,13 @@ static bool exception_pointers_handler(HostExceptionReport* report) {
PVOID exception_addr =
report->ExceptionInfo->ExceptionRecord->ExceptionAddress;
#if XE_ARCH_AMD64
DWORD64 last_stackpointer = report->ExceptionInfo->ContextRecord->Rsp;
DWORD64 last_rip = report->ExceptionInfo->ContextRecord->Rip;
#elif XE_ARCH_ARM64
DWORD64 last_stackpointer = report->ExceptionInfo->ContextRecord->Sp;
DWORD64 last_rip = report->ExceptionInfo->ContextRecord->Pc;
#endif
DWORD except_code = report->ExceptionInfo->ExceptionRecord->ExceptionCode;
std::string build = (