[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-22 15:57:37 +09:00
parent c6e112bcd8
commit 883c2030d0
44 changed files with 13428 additions and 140 deletions
+2
View File
@@ -92,6 +92,8 @@ node_modules/.bin/
/scratch/
/build/
/build-arm64/
/build-x64/
# ==============================================================================
# Local-only paths
+33 -6
View File
@@ -6,6 +6,27 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Detect target architecture.
# - VS generator: CMAKE_GENERATOR_PLATFORM (from -A flag) takes priority.
# - Ninja/Makefiles: CMAKE_SYSTEM_PROCESSOR (set via -DCMAKE_SYSTEM_NAME
# + -DCMAKE_SYSTEM_PROCESSOR for cross-compile, or auto-detected natively).
if(CMAKE_GENERATOR_PLATFORM)
if(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64")
set(XE_TARGET_AARCH64 TRUE)
set(XE_TARGET_X86_64 FALSE)
else()
set(XE_TARGET_AARCH64 FALSE)
set(XE_TARGET_X86_64 TRUE)
endif()
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64")
set(XE_TARGET_AARCH64 TRUE)
set(XE_TARGET_X86_64 FALSE)
else()
set(XE_TARGET_AARCH64 FALSE)
set(XE_TARGET_X86_64 TRUE)
endif()
message(STATUS "Target architecture: XE_TARGET_AARCH64=${XE_TARGET_AARCH64} XE_TARGET_X86_64=${XE_TARGET_X86_64} (CMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR})")
# Options
option(XENIA_BUILD_TESTS "Build test suites" OFF)
option(XENIA_BUILD_MISC "Build misc subprojects (trace viewers, shader compiler, vfs-dump, demos)" OFF)
@@ -35,10 +56,12 @@ else()
set(XE_PLATFORM_NAME "Linux")
endif()
# Output directories — use CMAKE_BINARY_DIR so each build tree (build/, build-arm64/,
# build/vs-arm64/, etc.) gets its own output paths.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${XE_PLATFORM_NAME}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${XE_PLATFORM_NAME}")
# Output directories — strip any vs-* nesting from CMAKE_BINARY_DIR so that
# VS and Ninja builds for the same architecture share an output directory,
# while different architectures (build/ vs build-arm64/) stay separate.
string(REGEX REPLACE "/vs-[^/]+$" "" XE_OUTPUT_ROOT "${CMAKE_BINARY_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${XE_OUTPUT_ROOT}/bin/${XE_PLATFORM_NAME}")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${XE_OUTPUT_ROOT}/bin/${XE_PLATFORM_NAME}")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/obj/${XE_PLATFORM_NAME}")
# Create scratch/ if needed
@@ -78,9 +101,11 @@ if(MSVC)
add_compile_options(
/utf-8 # Build correctly on systems with non-Latin codepages
/wd4201 # Nameless struct/unions are ok
/arch:AVX # AVX vector extensions
/MP # Multi-processor compilation
)
if(XE_TARGET_X86_64)
add_compile_options(/arch:AVX)
endif()
add_compile_definitions(
_CRT_NONSTDC_NO_DEPRECATE
_CRT_SECURE_NO_WARNINGS
@@ -139,7 +164,9 @@ if(MSVC)
else()
# GCC/Clang (Linux)
add_compile_options(-mavx)
if(XE_TARGET_X86_64)
add_compile_options(-mavx)
endif()
# Disable specific warnings for C++
add_compile_options(
+31
View File
@@ -23,6 +23,22 @@
},
"architecture": "x64",
"binaryDir": "${sourceDir}/build"
},
{
"name": "vs-arm64",
"displayName": "Visual Studio 2022 (ARM64)",
"generator": "Visual Studio 17 2022",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"architecture": "ARM64",
"toolset": "host=x64",
"binaryDir": "${sourceDir}/build/vs-arm64",
"cacheVariables": {
"CMAKE_SYSTEM_PROCESSOR": "ARM64"
}
}
],
"buildPresets": [
@@ -58,6 +74,21 @@
"name": "vs-checked",
"configurePreset": "vs",
"configuration": "Checked"
},
{
"name": "vs-arm64-debug",
"configurePreset": "vs-arm64",
"configuration": "Debug"
},
{
"name": "vs-arm64-release",
"configurePreset": "vs-arm64",
"configuration": "Release"
},
{
"name": "vs-arm64-checked",
"configurePreset": "vs-arm64",
"configuration": "Checked"
}
]
}
+12 -1
View File
@@ -5,7 +5,7 @@ include(CMakeParseArguments)
# Platform suffix lists for file filtering
set(XE_PLATFORM_SUFFIXES
_win _linux _posix _gnulinux _x11 _gtk _android _mac
_win _linux _posix _gnulinux _x11 _gtk _android _mac _amd64 _arm64
)
# xe_platform_sources(target base_path [RECURSIVE])
@@ -78,6 +78,17 @@ function(xe_platform_sources target base_path)
endif()
list(APPEND _sources ${_plat_sources})
# Add back architecture-specific files
if(XE_TARGET_X86_64)
file(${glob_mode} _arch_sources "${base_path}/*_amd64.h" "${base_path}/*_amd64.cc")
elseif(XE_TARGET_AARCH64)
file(${glob_mode} _arch_sources "${base_path}/*_arm64.h" "${base_path}/*_arm64.cc")
endif()
if(_arch_sources)
list(APPEND _sources ${_arch_sources})
endif()
target_sources(${target} PRIVATE ${_sources})
endfunction()
+6 -1
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)
+12 -8
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
+53 -26
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;
}
}
+12
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)
+143
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
+60
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_
File diff suppressed because it is too large Load Diff
+175
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_
@@ -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
@@ -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_
@@ -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
@@ -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
File diff suppressed because it is too large Load Diff
+192
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_
+48
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
+44
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_

Some files were not shown because too many files have changed in this diff Show More