[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

2
.gitignore vendored
View File

@@ -92,6 +92,8 @@ node_modules/.bin/
/scratch/ /scratch/
/build/ /build/
/build-arm64/
/build-x64/
# ============================================================================== # ==============================================================================
# Local-only paths # Local-only paths

View File

@@ -6,6 +6,27 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_C_STANDARD 17) set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON) 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 # Options
option(XENIA_BUILD_TESTS "Build test suites" OFF) option(XENIA_BUILD_TESTS "Build test suites" OFF)
option(XENIA_BUILD_MISC "Build misc subprojects (trace viewers, shader compiler, vfs-dump, demos)" 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") set(XE_PLATFORM_NAME "Linux")
endif() endif()
# Output directories — use CMAKE_BINARY_DIR so each build tree (build/, build-arm64/, # Output directories — strip any vs-* nesting from CMAKE_BINARY_DIR so that
# build/vs-arm64/, etc.) gets its own output paths. # VS and Ninja builds for the same architecture share an output directory,
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${XE_PLATFORM_NAME}") # while different architectures (build/ vs build-arm64/) stay separate.
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${XE_PLATFORM_NAME}") 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}") set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/obj/${XE_PLATFORM_NAME}")
# Create scratch/ if needed # Create scratch/ if needed
@@ -78,9 +101,11 @@ if(MSVC)
add_compile_options( add_compile_options(
/utf-8 # Build correctly on systems with non-Latin codepages /utf-8 # Build correctly on systems with non-Latin codepages
/wd4201 # Nameless struct/unions are ok /wd4201 # Nameless struct/unions are ok
/arch:AVX # AVX vector extensions
/MP # Multi-processor compilation /MP # Multi-processor compilation
) )
if(XE_TARGET_X86_64)
add_compile_options(/arch:AVX)
endif()
add_compile_definitions( add_compile_definitions(
_CRT_NONSTDC_NO_DEPRECATE _CRT_NONSTDC_NO_DEPRECATE
_CRT_SECURE_NO_WARNINGS _CRT_SECURE_NO_WARNINGS
@@ -139,7 +164,9 @@ if(MSVC)
else() else()
# GCC/Clang (Linux) # GCC/Clang (Linux)
if(XE_TARGET_X86_64)
add_compile_options(-mavx) add_compile_options(-mavx)
endif()
# Disable specific warnings for C++ # Disable specific warnings for C++
add_compile_options( add_compile_options(

View File

@@ -23,6 +23,22 @@
}, },
"architecture": "x64", "architecture": "x64",
"binaryDir": "${sourceDir}/build" "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": [ "buildPresets": [
@@ -58,6 +74,21 @@
"name": "vs-checked", "name": "vs-checked",
"configurePreset": "vs", "configurePreset": "vs",
"configuration": "Checked" "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"
} }
] ]
} }

View File

@@ -5,7 +5,7 @@ include(CMakeParseArguments)
# Platform suffix lists for file filtering # Platform suffix lists for file filtering
set(XE_PLATFORM_SUFFIXES 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]) # xe_platform_sources(target base_path [RECURSIVE])
@@ -78,6 +78,17 @@ function(xe_platform_sources target base_path)
endif() endif()
list(APPEND _sources ${_plat_sources}) 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}) target_sources(${target} PRIVATE ${_sources})
endfunction() endfunction()

View File

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

View File

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

View File

@@ -29,6 +29,57 @@ constexpr size_t kMaxHandlerCount = 8;
// Executed in order. // Executed in order.
std::pair<ExceptionHandler::Handler, void*> handlers_[kMaxHandlerCount]; 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) { LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
// Visual Studio SetThreadName. // Visual Studio SetThreadName.
if (ex_info->ExceptionRecord->ExceptionCode == 0x406D1388) { if (ex_info->ExceptionRecord->ExceptionCode == 0x406D1388) {
@@ -36,12 +87,7 @@ LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
} }
HostThreadContext thread_context; HostThreadContext thread_context;
thread_context.rip = ex_info->ContextRecord->Rip; CaptureThreadContext(thread_context, ex_info->ContextRecord);
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));
// https://msdn.microsoft.com/en-us/library/ms679331(v=vs.85).aspx // https://msdn.microsoft.com/en-us/library/ms679331(v=vs.85).aspx
// https://msdn.microsoft.com/en-us/library/aa363082(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) { for (size_t i = 0; i < xe::countof(handlers_) && handlers_[i].first; ++i) {
if (handlers_[i].first(&ex, handlers_[i].second)) { if (handlers_[i].first(&ex, handlers_[i].second)) {
// Exception handled. // Exception handled.
ex_info->ContextRecord->Rip = thread_context.rip; RestoreThreadContext(ex_info->ContextRecord, thread_context, ex);
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));
}
return EXCEPTION_CONTINUE_EXECUTION; 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 capstone fmt imgui mspack
) )
# x64 backend # Architecture-specific backend
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") if(XE_TARGET_X86_64)
target_link_libraries(xenia-cpu-ppc-tests PRIVATE xenia-cpu-backend-x64) 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() endif()
xe_target_defaults(xenia-cpu-ppc-tests) xe_target_defaults(xenia-cpu-ppc-tests)

View File

@@ -29,6 +29,8 @@
#if XE_ARCH_AMD64 #if XE_ARCH_AMD64
#include "xenia/cpu/backend/x64/x64_backend.h" #include "xenia/cpu/backend/x64/x64_backend.h"
#elif XE_ARCH_ARM64
#include "xenia/cpu/backend/a64/a64_backend.h"
#endif // XE_ARCH #endif // XE_ARCH
#if XE_COMPILER_MSVC #if XE_COMPILER_MSVC
@@ -248,11 +250,17 @@ class TestRunner {
if (cvars::cpu == "x64") { if (cvars::cpu == "x64") {
backend.reset(new xe::cpu::backend::x64::X64Backend()); 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 #endif // XE_ARCH
if (cvars::cpu == "any") { if (cvars::cpu == "any") {
if (!backend) { if (!backend) {
#if XE_ARCH_AMD64 #if XE_ARCH_AMD64
backend.reset(new xe::cpu::backend::x64::X64Backend()); backend.reset(new xe::cpu::backend::x64::X64Backend());
#elif XE_ARCH_ARM64
backend.reset(new xe::cpu::backend::a64::A64Backend());
#endif // XE_ARCH #endif // XE_ARCH
} }
} }
@@ -310,6 +318,21 @@ class TestRunner {
x64_backend->BackendContextForGuestContext(thread_state_->context()); x64_backend->BackendContextForGuestContext(thread_state_->context());
bctx->flags &= ~(1U << xe::cpu::backend::x64::kX64BackendMXCSRModeBit); 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 #endif
// Execute test. // Execute test.

View File

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

View File

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

View File

@@ -3,5 +3,6 @@ xe_test_suite(xenia-cpu-tests ${CMAKE_CURRENT_SOURCE_DIR}
capstone fmt imgui capstone fmt imgui
xenia-base xenia-core xenia-cpu xenia-gpu xenia-base xenia-core xenia-cpu xenia-gpu
xenia-kernel xenia-ui xenia-patcher 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 <vector>
#include "xenia/base/platform.h" #include "xenia/base/platform.h"
#if XE_ARCH_AMD64
#include "xenia/cpu/backend/x64/x64_backend.h" #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/hir/hir_builder.h"
#include "xenia/cpu/ppc/ppc_context.h" #include "xenia/cpu/ppc/ppc_context.h"
#include "xenia/cpu/ppc/ppc_frontend.h" #include "xenia/cpu/ppc/ppc_frontend.h"
@@ -38,6 +42,8 @@ class TestFunction {
std::unique_ptr<xe::cpu::backend::Backend> backend; std::unique_ptr<xe::cpu::backend::Backend> backend;
#if XE_ARCH_AMD64 #if XE_ARCH_AMD64
backend.reset(new xe::cpu::backend::x64::X64Backend()); backend.reset(new xe::cpu::backend::x64::X64Backend());
#elif XE_ARCH_ARM64
backend.reset(new xe::cpu::backend::a64::A64Backend());
#endif // XE_ARCH #endif // XE_ARCH
if (backend) { if (backend) {
auto processor = std::make_unique<Processor>(memory.get(), nullptr); auto processor = std::make_unique<Processor>(memory.get(), nullptr);

View File

@@ -970,6 +970,7 @@ void DebugWindow::DrawRegistersPane() {
} break; } break;
case RegisterGroup::kHostGeneral: { case RegisterGroup::kHostGeneral: {
ImGui::BeginChild("##host_general"); ImGui::BeginChild("##host_general");
#if XE_ARCH_AMD64
for (int i = 0; i < 18; ++i) { for (int i = 0; i < 18; ++i) {
auto reg = static_cast<X64Register>(i); auto reg = static_cast<X64Register>(i);
ImGui::BeginGroup(); ImGui::BeginGroup();
@@ -990,10 +991,37 @@ void DebugWindow::DrawRegistersPane() {
} }
ImGui::EndGroup(); 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(); ImGui::EndChild();
} break; } break;
case RegisterGroup::kHostVector: { case RegisterGroup::kHostVector: {
ImGui::BeginChild("##host_vector"); ImGui::BeginChild("##host_vector");
#if XE_ARCH_AMD64
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
auto reg = auto reg =
static_cast<X64Register>(static_cast<int>(X64Register::kXmm0) + i); 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); i, thread_info->host_context.xmm_registers[i].f32);
ImGui::EndGroup(); 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(); ImGui::EndChild();
} }
} }

View File

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

View File

@@ -18,7 +18,7 @@ if(XENIA_BUILD_MISC)
xenia-kernel xenia-patcher xenia-ui xenia-ui-d3d12 xenia-vfs xenia-kernel xenia-patcher xenia-ui xenia-ui-d3d12 xenia-vfs
aes_128 capstone dxbc fmt imgui libavcodec libavutil mspack snappy xxhash 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) target_link_libraries(xenia-gpu-d3d12-trace-viewer PRIVATE xenia-cpu-backend-x64)
endif() endif()
xe_target_defaults(xenia-gpu-d3d12-trace-viewer) 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 xenia-kernel xenia-patcher xenia-ui xenia-ui-d3d12 xenia-vfs
aes_128 capstone dxbc fmt imgui libavcodec libavutil mspack snappy xxhash 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) target_link_libraries(xenia-gpu-d3d12-trace-dump PRIVATE xenia-cpu-backend-x64)
endif() endif()
xe_target_defaults(xenia-gpu-d3d12-trace-dump) 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 xenia-kernel xenia-patcher xenia-ui xenia-ui-vulkan xenia-vfs
aes_128 capstone fmt glslang-spirv imgui libavcodec libavutil mspack snappy xxhash 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) target_link_libraries(xenia-gpu-vulkan-trace-viewer PRIVATE xenia-cpu-backend-x64)
endif() endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux") 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 xenia-kernel xenia-patcher xenia-ui xenia-ui-vulkan xenia-vfs
aes_128 capstone fmt glslang-spirv imgui libavcodec libavutil mspack snappy xxhash 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) target_link_libraries(xenia-gpu-vulkan-trace-dump PRIVATE xenia-cpu-backend-x64)
endif() endif()
xe_target_defaults(xenia-gpu-vulkan-trace-dump) xe_target_defaults(xenia-gpu-vulkan-trace-dump)

View File

@@ -75,7 +75,13 @@ struct HostExceptionReport {
: ExceptionInfo(_ExceptionInfo), : ExceptionInfo(_ExceptionInfo),
Report_Scratchpos(0u), Report_Scratchpos(0u),
last_win32_error(GetLastError()), last_win32_error(GetLastError()),
#if XE_ARCH_AMD64
last_ntstatus(__readgsdword(0x1250)), 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), errno_value(errno),
address_format_ring_index(0) address_format_ring_index(0)
@@ -168,9 +174,13 @@ static bool exception_pointers_handler(HostExceptionReport* report) {
PVOID exception_addr = PVOID exception_addr =
report->ExceptionInfo->ExceptionRecord->ExceptionAddress; report->ExceptionInfo->ExceptionRecord->ExceptionAddress;
#if XE_ARCH_AMD64
DWORD64 last_stackpointer = report->ExceptionInfo->ContextRecord->Rsp; DWORD64 last_stackpointer = report->ExceptionInfo->ContextRecord->Rsp;
DWORD64 last_rip = report->ExceptionInfo->ContextRecord->Rip; 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; DWORD except_code = report->ExceptionInfo->ExceptionRecord->ExceptionCode;
std::string build = ( std::string build = (

View File

@@ -7,6 +7,27 @@ else()
add_compile_options(-w) add_compile_options(-w)
endif() endif()
# ==============================================================================
# xbyak_aarch64 (ARM64 JIT assembler)
# ==============================================================================
if(XE_TARGET_AARCH64)
add_library(xbyak_aarch64 STATIC
xbyak_aarch64/src/xbyak_aarch64_impl.cpp
xbyak_aarch64/src/util_impl.cpp
)
target_include_directories(xbyak_aarch64 PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/xbyak_aarch64
${CMAKE_CURRENT_SOURCE_DIR}/xbyak_aarch64/src
${CMAKE_CURRENT_SOURCE_DIR}/xbyak_aarch64/xbyak_aarch64
)
if(MSVC)
target_compile_definitions(xbyak_aarch64 PRIVATE NOMINMAX)
target_compile_options(xbyak_aarch64 PRIVATE /w)
else()
target_compile_options(xbyak_aarch64 PRIVATE -w)
endif()
endif()
# ============================================================================== # ==============================================================================
# aes_128 # aes_128
# ============================================================================== # ==============================================================================
@@ -21,17 +42,28 @@ target_include_directories(aes_128 PUBLIC aes_128)
add_library(capstone STATIC add_library(capstone STATIC
capstone/cs.c capstone/cs.c
capstone/MCInst.c capstone/MCInst.c
capstone/MCInstPrinter.c
capstone/MCInstrDesc.c capstone/MCInstrDesc.c
capstone/MCRegisterInfo.c capstone/MCRegisterInfo.c
capstone/SStream.c capstone/SStream.c
capstone/utils.c capstone/utils.c
capstone/Mapping.c capstone/Mapping.c
) )
# Glob X86 arch files # Architecture-specific capstone files
if(XE_TARGET_AARCH64)
file(GLOB _capstone_arch_c "capstone/arch/AArch64/*.c")
file(GLOB _capstone_arch_h "capstone/arch/AArch64/*.h")
file(GLOB _capstone_arch_inc "capstone/arch/AArch64/*.inc")
target_sources(capstone PRIVATE ${_capstone_arch_c} ${_capstone_arch_h} ${_capstone_arch_inc})
target_compile_definitions(capstone PRIVATE
CAPSTONE_HAS_ARM64
CAPSTONE_HAS_AARCH64
CAPSTONE_USE_SYS_DYN_MEM
)
else()
file(GLOB _capstone_x86_c "capstone/arch/X86/*.c") file(GLOB _capstone_x86_c "capstone/arch/X86/*.c")
file(GLOB _capstone_x86_h "capstone/arch/X86/*.h") file(GLOB _capstone_x86_h "capstone/arch/X86/*.h")
file(GLOB _capstone_x86_inc "capstone/arch/X86/*.inc") file(GLOB _capstone_x86_inc "capstone/arch/X86/*.inc")
# Remove excluded files
list(FILTER _capstone_x86_c EXCLUDE REGEX "X86ATTInstPrinter\\.c$") list(FILTER _capstone_x86_c EXCLUDE REGEX "X86ATTInstPrinter\\.c$")
list(FILTER _capstone_x86_inc EXCLUDE REGEX "reduce\\.inc$") list(FILTER _capstone_x86_inc EXCLUDE REGEX "reduce\\.inc$")
target_sources(capstone PRIVATE ${_capstone_x86_c} ${_capstone_x86_h} ${_capstone_x86_inc}) target_sources(capstone PRIVATE ${_capstone_x86_c} ${_capstone_x86_h} ${_capstone_x86_inc})
@@ -40,6 +72,7 @@ target_compile_definitions(capstone PRIVATE
CAPSTONE_HAS_X86 CAPSTONE_HAS_X86
CAPSTONE_USE_SYS_DYN_MEM CAPSTONE_USE_SYS_DYN_MEM
) )
endif()
target_include_directories(capstone PUBLIC capstone/include PRIVATE capstone) target_include_directories(capstone PUBLIC capstone/include PRIVATE capstone)
# Force all capstone sources to compile as C # Force all capstone sources to compile as C
file(GLOB_RECURSE _capstone_all_c "capstone/*.c") file(GLOB_RECURSE _capstone_all_c "capstone/*.c")
@@ -77,9 +110,9 @@ add_library(discord-rpc STATIC
discord-rpc/src/serialization.cpp discord-rpc/src/serialization.cpp
discord-rpc/src/serialization.h discord-rpc/src/serialization.h
) )
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") if(XE_TARGET_X86_64)
target_compile_definitions(discord-rpc PRIVATE RAPIDJSON_SSE42) target_compile_definitions(discord-rpc PRIVATE RAPIDJSON_SSE42)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") elseif(XE_TARGET_ARM64)
target_compile_definitions(discord-rpc PRIVATE RAPIDJSON_NEON) target_compile_definitions(discord-rpc PRIVATE RAPIDJSON_NEON)
endif() endif()
target_include_directories(discord-rpc PUBLIC discord-rpc/include PRIVATE rapidjson/include) target_include_directories(discord-rpc PUBLIC discord-rpc/include PRIVATE rapidjson/include)
@@ -156,11 +189,16 @@ target_include_directories(pugixml PUBLIC pugixml/src)
# ============================================================================== # ==============================================================================
# Generate snappy-stubs-public.h if missing # Generate snappy-stubs-public.h if missing
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/snappy/snappy-stubs-public.h") if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/snappy/snappy-stubs-public.h")
if(XE_TARGET_X86_64)
set(_snappy_avx_flag "-DSNAPPY_REQUIRE_AVX=ON")
else()
set(_snappy_avx_flag "-DSNAPPY_REQUIRE_AVX=OFF")
endif()
execute_process( execute_process(
COMMAND ${CMAKE_COMMAND} COMMAND ${CMAKE_COMMAND}
-DSNAPPY_BUILD_TESTS=OFF -DSNAPPY_BUILD_TESTS=OFF
-DSNAPPY_BUILD_BENCHMARKS=OFF -DSNAPPY_BUILD_BENCHMARKS=OFF
-DSNAPPY_REQUIRE_AVX=ON ${_snappy_avx_flag}
${CMAKE_CURRENT_SOURCE_DIR}/snappy ${CMAKE_CURRENT_SOURCE_DIR}/snappy
-B${CMAKE_CURRENT_SOURCE_DIR}/snappy -B${CMAKE_CURRENT_SOURCE_DIR}/snappy
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
@@ -244,8 +282,19 @@ if(_zlibng_needs_configure)
) )
endif() endif()
file(GLOB _zlibng_c "zlib-ng/*.c") file(GLOB _zlibng_c "zlib-ng/*.c")
file(GLOB _zlibng_x86 "zlib-ng/arch/x86/*.c")
file(GLOB _zlibng_generic "zlib-ng/arch/generic/*.c") file(GLOB _zlibng_generic "zlib-ng/arch/generic/*.c")
if(XE_TARGET_AARCH64)
file(GLOB _zlibng_arch "zlib-ng/arch/arm/*.c")
add_library(zlib-ng STATIC
${_zlibng_c}
${_zlibng_arch}
${_zlibng_generic}
)
target_compile_definitions(zlib-ng PRIVATE
WITH_GZFILEOP
)
else()
file(GLOB _zlibng_x86 "zlib-ng/arch/x86/*.c")
add_library(zlib-ng STATIC add_library(zlib-ng STATIC
${_zlibng_c} ${_zlibng_c}
${_zlibng_x86} ${_zlibng_x86}
@@ -283,6 +332,7 @@ else()
set_target_properties(zlib-ng PROPERTIES SOURCES "") set_target_properties(zlib-ng PROPERTIES SOURCES "")
target_sources(zlib-ng PRIVATE ${_zlibng_c} ${_zlibng_x86} ${_zlibng_generic}) target_sources(zlib-ng PRIVATE ${_zlibng_c} ${_zlibng_x86} ${_zlibng_generic})
endif() endif()
endif()
target_include_directories(zlib-ng PUBLIC zlib-ng) target_include_directories(zlib-ng PUBLIC zlib-ng)
# ============================================================================== # ==============================================================================
@@ -389,6 +439,66 @@ endif()
# ============================================================================== # ==============================================================================
# FFmpeg (source in FFmpeg submodule, config overlay in ffmpeg-xenia) # FFmpeg (source in FFmpeg submodule, config overlay in ffmpeg-xenia)
# ============================================================================== # ==============================================================================
set(XE_HAS_GAS_ASM FALSE)
if(XE_TARGET_AARCH64)
if(MSVC)
# MSVC can't assemble GAS-syntax .S files. Use clang as the ASM compiler
# if available — the resulting .obj files link fine with MSVC-compiled code.
# Pick clang from the host architecture directory so it can actually
# run on this machine (clang is a cross-compiler — it handles the
# target arch via --target regardless of host).
if(CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
set(_clang_host_dir "ARM64")
else()
set(_clang_host_dir "x64")
endif()
find_program(CLANG_EXE clang HINTS
"$ENV{VCINSTALLDIR}/Tools/Llvm/${_clang_host_dir}/bin"
"$ENV{VCINSTALLDIR}/Tools/Llvm/bin"
"C:/Program Files/LLVM/bin"
)
if(CLANG_EXE)
set(XE_HAS_GAS_ASM TRUE)
set(XE_CLANG_ASM "${CLANG_EXE}")
message(STATUS "Using clang for ARM64 assembly: ${CLANG_EXE}")
else()
message(WARNING "clang not found — FFmpeg NEON assembly disabled on MSVC ARM64. "
"Install LLVM/clang to enable optimized audio decoding.")
endif()
else()
enable_language(ASM)
set(XE_HAS_GAS_ASM TRUE)
endif()
endif()
# Helper: compile GAS-syntax .S files with clang on MSVC, or natively on
# GCC/Clang. Adds the resulting objects to the target.
function(xe_add_gas_sources target)
if(NOT XE_HAS_GAS_ASM)
return()
endif()
foreach(src ${ARGN})
if(XE_CLANG_ASM)
# MSVC: use clang to assemble .S -> .obj
get_filename_component(src_name "${src}" NAME_WE)
get_filename_component(src_abs "${src}" ABSOLUTE)
set(obj "${CMAKE_CURRENT_BINARY_DIR}/${src_name}.obj")
add_custom_command(
OUTPUT "${obj}"
COMMAND "${XE_CLANG_ASM}" --target=aarch64-pc-windows-msvc
-I "${CMAKE_CURRENT_SOURCE_DIR}/FFmpeg"
-I "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg-xenia"
-c "${src_abs}" -o "${obj}"
DEPENDS "${src_abs}"
COMMENT "Assembling ${src} with clang"
)
target_sources(${target} PRIVATE "${obj}")
else()
# GCC/Clang: native ASM support
target_sources(${target} PRIVATE "${src}")
endif()
endforeach()
endfunction()
# Common function to apply shared FFmpeg settings to a target. # Common function to apply shared FFmpeg settings to a target.
function(ffmpeg_common target) function(ffmpeg_common target)
@@ -515,7 +625,7 @@ add_library(libavutil STATIC
FFmpeg/libavutil/timestamp.c FFmpeg/libavutil/timestamp.c
) )
# x86 platform files # x86 platform files
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") if(XE_TARGET_X86_64)
target_sources(libavutil PRIVATE target_sources(libavutil PRIVATE
FFmpeg/libavutil/x86/cpu.c FFmpeg/libavutil/x86/cpu.c
FFmpeg/libavutil/x86/fixed_dsp_init.c FFmpeg/libavutil/x86/fixed_dsp_init.c
@@ -525,6 +635,24 @@ if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
FFmpeg/libavutil/x86/tx_float_init.c FFmpeg/libavutil/x86/tx_float_init.c
) )
endif() endif()
# aarch64 platform files
if(XE_TARGET_AARCH64)
target_sources(libavutil PRIVATE
FFmpeg/libavutil/aarch64/cpu.c
)
if(XE_HAS_GAS_ASM)
# NEON init files reference symbols from the .S files, so both must
# be included together. Requires a GAS-compatible assembler (gcc/clang).
target_sources(libavutil PRIVATE
FFmpeg/libavutil/aarch64/float_dsp_init.c
FFmpeg/libavutil/aarch64/tx_float_init.c
)
xe_add_gas_sources(libavutil
FFmpeg/libavutil/aarch64/float_dsp_neon.S
FFmpeg/libavutil/aarch64/tx_float_neon.S
)
endif()
endif()
ffmpeg_common(libavutil) ffmpeg_common(libavutil)
# --- libavcodec --- # --- libavcodec ---
@@ -602,7 +730,7 @@ if(WIN32)
target_sources(libavcodec PRIVATE FFmpeg/libavcodec/file_open.c) target_sources(libavcodec PRIVATE FFmpeg/libavcodec/file_open.c)
endif() endif()
# x86 platform files # x86 platform files
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") if(XE_TARGET_X86_64)
target_sources(libavcodec PRIVATE target_sources(libavcodec PRIVATE
FFmpeg/libavcodec/x86/constants.c FFmpeg/libavcodec/x86/constants.c
FFmpeg/libavcodec/x86/fdctdsp_init.c FFmpeg/libavcodec/x86/fdctdsp_init.c
@@ -611,6 +739,21 @@ if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
FFmpeg/libavcodec/x86/fdct.c FFmpeg/libavcodec/x86/fdct.c
) )
endif() endif()
# aarch64 platform files
if(XE_TARGET_AARCH64)
if(XE_HAS_GAS_ASM)
target_sources(libavcodec PRIVATE
FFmpeg/libavcodec/aarch64/idctdsp_init_aarch64.c
FFmpeg/libavcodec/aarch64/mpegaudiodsp_init.c
)
xe_add_gas_sources(libavcodec
FFmpeg/libavcodec/aarch64/idctdsp_neon.S
FFmpeg/libavcodec/aarch64/mpegaudiodsp_neon.S
FFmpeg/libavcodec/aarch64/neon.S
FFmpeg/libavcodec/aarch64/simple_idct_neon.S
)
endif()
endif()
target_include_directories(libavcodec PRIVATE target_include_directories(libavcodec PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/FFmpeg/libavcodec ${CMAKE_CURRENT_SOURCE_DIR}/FFmpeg/libavcodec
) )

View File

@@ -32,9 +32,13 @@
#define ARCH_X86 0 #define ARCH_X86 0
#define ARCH_X86_64 0 #define ARCH_X86_64 0
#if defined(__aarch64__) #if defined(__aarch64__) || defined(_M_ARM64)
/* ARM64 (Android) */ /* ARM64 */
#if defined(_WIN32)
#define SLIBSUF ".dll"
#else
#define SLIBSUF ".so" #define SLIBSUF ".so"
#endif
#undef ARCH_AARCH64 #undef ARCH_AARCH64
#define ARCH_AARCH64 1 #define ARCH_AARCH64 1
#define HAVE_ARMV8 1 #define HAVE_ARMV8 1
@@ -43,9 +47,15 @@
#define HAVE_ARMV8_EXTERNAL 1 #define HAVE_ARMV8_EXTERNAL 1
#define HAVE_NEON_EXTERNAL 1 #define HAVE_NEON_EXTERNAL 1
#define HAVE_VFP_EXTERNAL 1 #define HAVE_VFP_EXTERNAL 1
#if defined(_MSC_VER)
#define HAVE_ARMV8_INLINE 0
#define HAVE_NEON_INLINE 0
#define HAVE_VFP_INLINE 0
#else
#define HAVE_ARMV8_INLINE 1 #define HAVE_ARMV8_INLINE 1
#define HAVE_NEON_INLINE 1 #define HAVE_NEON_INLINE 1
#define HAVE_VFP_INLINE 1 #define HAVE_VFP_INLINE 1
#endif
#define HAVE_INTRINSICS_NEON 1 #define HAVE_INTRINSICS_NEON 1
#define HAVE_AS_FUNC 0 #define HAVE_AS_FUNC 0
#define HAVE_AS_ARCH_DIRECTIVE 0 #define HAVE_AS_ARCH_DIRECTIVE 0

View File

@@ -9,11 +9,12 @@ Run with --help or no arguments for possible commands.
from datetime import datetime from datetime import datetime
from multiprocessing import Pool from multiprocessing import Pool
from functools import partial from functools import partial
from argparse import ArgumentParser from argparse import ArgumentParser, ArgumentTypeError
from glob import glob from glob import glob
from json import loads as jsonloads from json import loads as jsonloads
import os import os
from re import findall as re_findall from re import findall as re_findall
import platform
from shutil import rmtree from shutil import rmtree
import subprocess import subprocess
import sys import sys
@@ -444,10 +445,11 @@ def shell_call(command, throw_on_error=True, stdout_path=None, stderr_path=None,
return result return result
def generate_version_h(): def generate_version_h(build_dir="build"):
"""Generates a build/version.h file that contains current git info. """Generates version.h in the given build directory with current git info.
""" """
header_file = "build/version.h" os.makedirs(build_dir, exist_ok=True)
header_file = os.path.join(build_dir, "version.h")
pr_number = None pr_number = None
if git_is_repository(): if git_is_repository():
@@ -686,8 +688,32 @@ def get_clang_format_binary():
sys.exit(1) sys.exit(1)
def normalize_target_arch(value):
"""Normalizes --target-arch values to canonical names (arm64, x64, or None)."""
v = value.lower()
if v in ("arm64", "aarch64", "a64"):
return "arm64"
if v in ("x64", "x86_64", "amd64", "x86"):
return "x64"
raise ArgumentTypeError(
f"unknown architecture '{value}' (expected: arm64, aarch64, a64, x64, amd64, x86_64, x86)")
def get_build_dir(target_arch=None):
"""Returns the Ninja build directory for the given target architecture.
Uses a separate directory when cross-compiling to avoid cache conflicts.
"""
is_native_arm64 = platform.machine() in ("ARM64", "aarch64")
if target_arch == "arm64" and not is_native_arm64:
return "build-arm64"
if target_arch == "x64" and is_native_arm64:
return "build-x64"
return "build"
def run_cmake_configure(build_type="Release", cc=None, build_tests=False, def run_cmake_configure(build_type="Release", cc=None, build_tests=False,
extra_args=None): extra_args=None, target_arch=None):
"""Runs cmake configure on the project. """Runs cmake configure on the project.
Args: Args:
@@ -695,14 +721,29 @@ def run_cmake_configure(build_type="Release", cc=None, build_tests=False,
cc: C compiler to use (e.g. 'clang', 'gcc'). cc: C compiler to use (e.g. 'clang', 'gcc').
build_tests: If True, enables building test suites. build_tests: If True, enables building test suites.
extra_args: Additional arguments to pass to cmake (e.g. -D flags). extra_args: Additional arguments to pass to cmake (e.g. -D flags).
target_arch: Target architecture override (e.g. 'arm64' for cross-compile).
Returns: Returns:
Return code from cmake. Return code from cmake.
""" """
# Cross-compilation via --target-arch is only supported on Windows where
# we can locate the MSVC cross-compiler automatically. On Linux it would
# silently produce a native build in a differently-named directory.
if target_arch is not None and sys.platform != "win32":
is_native_arm64 = platform.machine() in ("ARM64", "aarch64")
native_arch = "arm64" if is_native_arm64 else "x64"
if target_arch != native_arch:
print_error(
f"Cross-compilation (--target-arch {target_arch}) is only "
f"supported on Windows.\n"
f" The current host architecture is {native_arch}.")
return 1
build_dir = get_build_dir(target_arch)
args = [ args = [
"cmake", "cmake",
"-S", ".", "-S", ".",
"-B", "build", "-B", build_dir,
"-G", "Ninja Multi-Config", "-G", "Ninja Multi-Config",
] ]
if sys.platform != "win32": if sys.platform != "win32":
@@ -714,6 +755,49 @@ def run_cmake_configure(build_type="Release", cc=None, build_tests=False,
f"-DCMAKE_C_COMPILER={c_compiler}", f"-DCMAKE_C_COMPILER={c_compiler}",
f"-DCMAKE_CXX_COMPILER={cxx_compiler}", f"-DCMAKE_CXX_COMPILER={cxx_compiler}",
] ]
elif platform.machine() in ("ARM64", "aarch64") or target_arch == "arm64":
# Determine the effective target and the appropriate compiler/environment.
is_native_arm64 = platform.machine() in ("ARM64", "aarch64")
if target_arch == "x64" and is_native_arm64:
# Cross-compiling from ARM64 to x64
target = "x64"
vcvars_arg = "arm64_amd64"
processor = "AMD64"
cl_glob = r"C:\Program Files\Microsoft Visual Studio\*\*\VC\Tools\MSVC\*\bin\HostARM64\x64\cl.exe"
else:
# Targeting ARM64 (native or cross-compile from x64)
target = "arm64"
vcvars_arg = "x64_arm64"
processor = "ARM64"
cl_glob = r"C:\Program Files\Microsoft Visual Studio\*\*\VC\Tools\MSVC\*\bin\Hostx64\arm64\cl.exe"
cl_paths = sorted(glob(cl_glob))
if cl_paths:
cl_exe = cl_paths[-1]
# Derive the VS install root from the compiler path:
# .../VC/Tools/MSVC/<ver>/bin/Host<x>/target<y>/cl.exe -> .../VC
vc_root = cl_exe
for _ in range(7): # walk up 7 levels to VC/
vc_root = os.path.dirname(vc_root)
vcvarsall = os.path.join(vc_root, "Auxiliary", "Build", "vcvarsall.bat")
if os.path.exists(vcvarsall):
print(f" Setting up {target.upper()} build environment via: {vcvarsall}")
cmd = f'"{vcvarsall}" {vcvars_arg} >nul 2>&1 && set'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
for line in result.stdout.splitlines():
if "=" in line:
key, _, value = line.partition("=")
os.environ[key] = value
args += [
"-DCMAKE_SYSTEM_NAME=Windows",
f"-DCMAKE_SYSTEM_PROCESSOR={processor}",
f"-DCMAKE_C_COMPILER={cl_exe.replace(os.sep, '/')}",
f"-DCMAKE_CXX_COMPILER={cl_exe.replace(os.sep, '/')}",
]
else:
print(f" WARNING: {target.upper()} cross-compiler not found. Install "
f"'MSVC {target.upper()} build tools' in Visual Studio.")
if build_tests: if build_tests:
args += ["-DXENIA_BUILD_TESTS=ON"] args += ["-DXENIA_BUILD_TESTS=ON"]
if extra_args: if extra_args:
@@ -722,7 +806,7 @@ def run_cmake_configure(build_type="Release", cc=None, build_tests=False,
ret = subprocess.call(args) ret = subprocess.call(args)
if ret == 0: if ret == 0:
generate_version_h() generate_version_h(build_dir)
return ret return ret
@@ -739,8 +823,9 @@ def get_build_bin_path(args):
""" """
config = args["config"].title() config = args["config"].title()
platform = "Windows" if sys.platform == "win32" else "Linux" platform = "Windows" if sys.platform == "win32" else "Linux"
# Multi-config: build/bin/<Platform>/<Config> build_dir = get_build_dir(args.get("target_arch"))
return os.path.join(self_path, "build", "bin", platform, config) # Multi-config: <build_dir>/bin/<Platform>/<Config>
return os.path.join(self_path, build_dir, "bin", platform, config)
def create_clion_workspace(): def create_clion_workspace():
@@ -919,6 +1004,9 @@ class SetupCommand(Command):
name="setup", name="setup",
help_short="Setup the build environment.", help_short="Setup the build environment.",
*args, **kwargs) *args, **kwargs)
self.parser.add_argument(
"--target-arch", type=normalize_target_arch, default=None,
help="Target architecture (arm64/aarch64, x64/amd64/x86_64/x86).")
def execute(self, args, pass_args, cwd): def execute(self, args, pass_args, cwd):
print("Setting up the build environment...\n") print("Setting up the build environment...\n")
@@ -931,7 +1019,7 @@ class SetupCommand(Command):
print_warning("Git not available or not a repository. Dependencies may be missing.") print_warning("Git not available or not a repository. Dependencies may be missing.")
print("\n- running cmake configure...") print("\n- running cmake configure...")
ret = run_cmake_configure() ret = run_cmake_configure(target_arch=args["target_arch"])
print_status(ResultStatus.SUCCESS if not ret else ResultStatus.FAILURE) print_status(ResultStatus.SUCCESS if not ret else ResultStatus.FAILURE)
return ret return ret
@@ -998,11 +1086,15 @@ class PremakeCommand(Command):
self.parser.add_argument( self.parser.add_argument(
"--build-tests", action="store_true", default=False, "--build-tests", action="store_true", default=False,
help="Enables building test suites.") help="Enables building test suites.")
self.parser.add_argument(
"--target-arch", type=normalize_target_arch, default=None,
help="Target architecture (arm64/aarch64, x64/amd64/x86_64/x86).")
def execute(self, args, pass_args, cwd): def execute(self, args, pass_args, cwd):
print("Running cmake configure...\n") print("Running cmake configure...\n")
ret = run_cmake_configure(cc=args["cc"], ret = run_cmake_configure(cc=args["cc"],
build_tests=args["build_tests"]) build_tests=args["build_tests"],
target_arch=args["target_arch"])
print_status(ResultStatus.SUCCESS if not ret else ResultStatus.FAILURE) print_status(ResultStatus.SUCCESS if not ret else ResultStatus.FAILURE)
return ret return ret
@@ -1037,6 +1129,9 @@ class BaseBuildCommand(Command):
"--cmake-define", dest="cmake_defines", action="append", "--cmake-define", dest="cmake_defines", action="append",
default=[], metavar="KEY=VALUE", default=[], metavar="KEY=VALUE",
help="Pass a CMake define (e.g. --cmake-define CMAKE_CXX_FLAGS=/DUSE_BCRYPT_RSA).") help="Pass a CMake define (e.g. --cmake-define CMAKE_CXX_FLAGS=/DUSE_BCRYPT_RSA).")
self.parser.add_argument(
"--target-arch", type=normalize_target_arch, default=None,
help="Target architecture (arm64/aarch64, x64/amd64/x86_64/x86).")
def execute(self, args, pass_args, cwd): def execute(self, args, pass_args, cwd):
config = args["config"].title() config = args["config"].title()
@@ -1045,17 +1140,21 @@ class BaseBuildCommand(Command):
if not args["no_premake"]: if not args["no_premake"]:
print("- running cmake configure...") print("- running cmake configure...")
run_cmake_configure(build_type=config, cc=args["cc"], ret = run_cmake_configure(build_type=config, cc=args["cc"],
build_tests=args["build_tests"], build_tests=args["build_tests"],
target_arch=args["target_arch"],
extra_args=extra_args) extra_args=extra_args)
if ret:
return ret
print("") print("")
build_dir = get_build_dir(args.get("target_arch"))
print("- building (%s):%s..." % ( print("- building (%s):%s..." % (
"all" if not len(args["target"]) else ", ".join(args["target"]), "all" if not len(args["target"]) else ", ".join(args["target"]),
args["config"])) args["config"]))
build_args = [ build_args = [
"cmake", "cmake",
"--build", "build", "--build", build_dir,
"--config", config, "--config", config,
] ]
if args["target"]: if args["target"]:
@@ -1511,9 +1610,10 @@ class CleanCommand(Command):
def execute(self, args, pass_args, cwd): def execute(self, args, pass_args, cwd):
print("Cleaning build artifacts...") print("Cleaning build artifacts...")
# Clean all build directories # Clean all build directories
if os.path.isdir("build"): for build_dir in ["build", "build-arm64"]:
print("- cleaning build...") if os.path.isdir(build_dir):
subprocess.call(["cmake", "--build", "build", "--target", "clean"]) print(f"- cleaning {build_dir}...")
subprocess.call(["cmake", "--build", build_dir, "--target", "clean"])
# Also clean generated files # Also clean generated files
clean_generated_files() clean_generated_files()
@@ -1921,6 +2021,9 @@ class DevenvCommand(Command):
name="devenv", name="devenv",
help_short="Launches the development environment.", help_short="Launches the development environment.",
*args, **kwargs) *args, **kwargs)
self.parser.add_argument(
"--target-arch", type=normalize_target_arch, default=None,
help="Target architecture (arm64/aarch64, x64/amd64/x86_64/x86).")
def execute(self, args, pass_args, cwd): def execute(self, args, pass_args, cwd):
if sys.platform == "win32": if sys.platform == "win32":
@@ -1934,27 +2037,106 @@ class DevenvCommand(Command):
else: else:
print("IDE not detected. CMakeLists.txt is in the project root.") print("IDE not detected. CMakeLists.txt is in the project root.")
target_arch = args.get("target_arch", None)
print("\n- running cmake configure...") print("\n- running cmake configure...")
run_cmake_configure() run_cmake_configure(target_arch=target_arch)
print("\n- launching devenv...") print("\n- launching devenv...")
if sys.platform == "win32": if sys.platform == "win32":
# Generate a VS .sln for IDE use (normal builds still use Ninja) # Generate a VS .sln for IDE use (normal builds still use Ninja)
vs_build_dir = os.path.join("build", "vs") is_native_arm64 = platform.machine() in ("ARM64", "aarch64")
subprocess.call([ # Determine the effective target architecture
if target_arch == "arm64":
vs_arch = "ARM64"
elif target_arch == "x64":
vs_arch = "x64"
elif is_native_arm64:
vs_arch = "ARM64"
else:
vs_arch = "x64"
is_cross = (vs_arch == "ARM64" and not is_native_arm64)
vs_build_dir = os.path.join(get_build_dir(target_arch), "vs-" + vs_arch.lower())
cmake_args = [
"cmake", "cmake",
"-S", ".", "-S", ".",
"-B", vs_build_dir, "-B", vs_build_dir,
"-A", "x64", "-A", vs_arch,
"-DXENIA_BUILD_TESTS=ON", "-DXENIA_BUILD_TESTS=ON",
]) ]
# Since VS 2026 default solution extension is slnx.
slnx_path = os.path.join(vs_build_dir, "xenia.slnx")
if not os.path.exists(slnx_path):
slnx_path = os.path.join(vs_build_dir, "xenia.sln")
print(f"Opening {slnx_path} in Visual Studio...") if is_cross:
shell_call(["devenv", slnx_path]) # Cross-compiling from x64 to ARM64.
# Use vswhere to find a VS installation with ARM64 C++ tools
# and force the correct generator/instance since CMake might
# otherwise pick a VS without ARM64 support.
vs_generator_map = {
2019: "Visual Studio 16 2019",
2022: "Visual Studio 17 2022",
}
try:
vswhere_out = subprocess.check_output(
"tools/vswhere/vswhere.exe"
' -version "[17,)" -latest -prerelease'
" -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64"
" -format json -utf8"
" -products"
" Microsoft.VisualStudio.Product.Enterprise"
" Microsoft.VisualStudio.Product.Professional"
" Microsoft.VisualStudio.Product.Community"
" Microsoft.VisualStudio.Product.BuildTools",
encoding="utf-8",
)
arm64_vs_list = jsonloads(vswhere_out) if vswhere_out else []
except Exception:
arm64_vs_list = []
if not arm64_vs_list:
print_error(
"No Visual Studio installation with ARM64 C++ build tools found.\n"
" Install the 'MSVC ARM64/ARM64EC build tools' component\n"
" via the Visual Studio Installer.")
return 1
arm64_vs = arm64_vs_list[0]
arm64_vs_path = arm64_vs.get("installationPath", "")
arm64_vs_plv = int(arm64_vs.get("catalog", {}).get(
"productLineVersion", VSVERSION_MINIMUM))
vs_generator = vs_generator_map.get(arm64_vs_plv)
toolset_parts = ["host=x64"]
if not vs_generator:
latest_known = max(vs_generator_map.keys())
vs_generator = vs_generator_map[latest_known]
print(f" Note: VS {arm64_vs_plv} detected with ARM64 tools.")
print(f" Using \"{vs_generator}\" generator with that instance.")
vc_dir = os.path.join(arm64_vs_path, "MSBuild", "Microsoft", "VC")
if os.path.isdir(vc_dir):
toolsets = sorted(d for d in os.listdir(vc_dir) if d.startswith("v"))
if toolsets:
toolset_parts.insert(0, toolsets[-1])
cmake_args += [
"-G", vs_generator,
"-T", ",".join(toolset_parts),
"-DCMAKE_SYSTEM_PROCESSOR=ARM64",
f"-DCMAKE_GENERATOR_INSTANCE={arm64_vs_path}",
]
ret = subprocess.call(cmake_args)
if ret == 0:
generate_version_h(vs_build_dir)
# VS 2026+ generates .slnx, older versions generate .sln
sln_path = os.path.join(vs_build_dir, "xenia.slnx")
if not os.path.exists(sln_path):
sln_path = os.path.join(vs_build_dir, "xenia.sln")
if ret != 0 or not os.path.exists(sln_path):
print_error(f"Failed to generate VS solution. Check cmake output above.")
return 1
print(f"Opening {sln_path} in Visual Studio...")
shell_call(["devenv", sln_path])
elif has_bin("clion"): elif has_bin("clion"):
shell_call(["clion", "."]) shell_call(["clion", "."])
elif has_bin("clion.sh"): elif has_bin("clion.sh"):