[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:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -92,6 +92,8 @@ node_modules/.bin/
|
||||
/scratch/
|
||||
|
||||
/build/
|
||||
/build-arm64/
|
||||
/build-x64/
|
||||
|
||||
# ==============================================================================
|
||||
# Local-only paths
|
||||
|
||||
@@ -6,6 +6,27 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_C_STANDARD 17)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# Detect target architecture.
|
||||
# - VS generator: CMAKE_GENERATOR_PLATFORM (from -A flag) takes priority.
|
||||
# - Ninja/Makefiles: CMAKE_SYSTEM_PROCESSOR (set via -DCMAKE_SYSTEM_NAME
|
||||
# + -DCMAKE_SYSTEM_PROCESSOR for cross-compile, or auto-detected natively).
|
||||
if(CMAKE_GENERATOR_PLATFORM)
|
||||
if(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64")
|
||||
set(XE_TARGET_AARCH64 TRUE)
|
||||
set(XE_TARGET_X86_64 FALSE)
|
||||
else()
|
||||
set(XE_TARGET_AARCH64 FALSE)
|
||||
set(XE_TARGET_X86_64 TRUE)
|
||||
endif()
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|ARM64")
|
||||
set(XE_TARGET_AARCH64 TRUE)
|
||||
set(XE_TARGET_X86_64 FALSE)
|
||||
else()
|
||||
set(XE_TARGET_AARCH64 FALSE)
|
||||
set(XE_TARGET_X86_64 TRUE)
|
||||
endif()
|
||||
message(STATUS "Target architecture: XE_TARGET_AARCH64=${XE_TARGET_AARCH64} XE_TARGET_X86_64=${XE_TARGET_X86_64} (CMAKE_SYSTEM_PROCESSOR=${CMAKE_SYSTEM_PROCESSOR})")
|
||||
|
||||
# Options
|
||||
option(XENIA_BUILD_TESTS "Build test suites" OFF)
|
||||
option(XENIA_BUILD_MISC "Build misc subprojects (trace viewers, shader compiler, vfs-dump, demos)" OFF)
|
||||
@@ -35,10 +56,12 @@ else()
|
||||
set(XE_PLATFORM_NAME "Linux")
|
||||
endif()
|
||||
|
||||
# Output directories — use CMAKE_BINARY_DIR so each build tree (build/, build-arm64/,
|
||||
# build/vs-arm64/, etc.) gets its own output paths.
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${XE_PLATFORM_NAME}")
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/${XE_PLATFORM_NAME}")
|
||||
# Output directories — strip any vs-* nesting from CMAKE_BINARY_DIR so that
|
||||
# VS and Ninja builds for the same architecture share an output directory,
|
||||
# while different architectures (build/ vs build-arm64/) stay separate.
|
||||
string(REGEX REPLACE "/vs-[^/]+$" "" XE_OUTPUT_ROOT "${CMAKE_BINARY_DIR}")
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${XE_OUTPUT_ROOT}/bin/${XE_PLATFORM_NAME}")
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${XE_OUTPUT_ROOT}/bin/${XE_PLATFORM_NAME}")
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/obj/${XE_PLATFORM_NAME}")
|
||||
|
||||
# Create scratch/ if needed
|
||||
@@ -78,9 +101,11 @@ if(MSVC)
|
||||
add_compile_options(
|
||||
/utf-8 # Build correctly on systems with non-Latin codepages
|
||||
/wd4201 # Nameless struct/unions are ok
|
||||
/arch:AVX # AVX vector extensions
|
||||
/MP # Multi-processor compilation
|
||||
)
|
||||
if(XE_TARGET_X86_64)
|
||||
add_compile_options(/arch:AVX)
|
||||
endif()
|
||||
add_compile_definitions(
|
||||
_CRT_NONSTDC_NO_DEPRECATE
|
||||
_CRT_SECURE_NO_WARNINGS
|
||||
@@ -139,7 +164,9 @@ if(MSVC)
|
||||
|
||||
else()
|
||||
# GCC/Clang (Linux)
|
||||
if(XE_TARGET_X86_64)
|
||||
add_compile_options(-mavx)
|
||||
endif()
|
||||
|
||||
# Disable specific warnings for C++
|
||||
add_compile_options(
|
||||
|
||||
@@ -23,6 +23,22 @@
|
||||
},
|
||||
"architecture": "x64",
|
||||
"binaryDir": "${sourceDir}/build"
|
||||
},
|
||||
{
|
||||
"name": "vs-arm64",
|
||||
"displayName": "Visual Studio 2022 (ARM64)",
|
||||
"generator": "Visual Studio 17 2022",
|
||||
"condition": {
|
||||
"type": "equals",
|
||||
"lhs": "${hostSystemName}",
|
||||
"rhs": "Windows"
|
||||
},
|
||||
"architecture": "ARM64",
|
||||
"toolset": "host=x64",
|
||||
"binaryDir": "${sourceDir}/build/vs-arm64",
|
||||
"cacheVariables": {
|
||||
"CMAKE_SYSTEM_PROCESSOR": "ARM64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
@@ -58,6 +74,21 @@
|
||||
"name": "vs-checked",
|
||||
"configurePreset": "vs",
|
||||
"configuration": "Checked"
|
||||
},
|
||||
{
|
||||
"name": "vs-arm64-debug",
|
||||
"configurePreset": "vs-arm64",
|
||||
"configuration": "Debug"
|
||||
},
|
||||
{
|
||||
"name": "vs-arm64-release",
|
||||
"configurePreset": "vs-arm64",
|
||||
"configuration": "Release"
|
||||
},
|
||||
{
|
||||
"name": "vs-arm64-checked",
|
||||
"configurePreset": "vs-arm64",
|
||||
"configuration": "Checked"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ include(CMakeParseArguments)
|
||||
|
||||
# Platform suffix lists for file filtering
|
||||
set(XE_PLATFORM_SUFFIXES
|
||||
_win _linux _posix _gnulinux _x11 _gtk _android _mac
|
||||
_win _linux _posix _gnulinux _x11 _gtk _android _mac _amd64 _arm64
|
||||
)
|
||||
|
||||
# xe_platform_sources(target base_path [RECURSIVE])
|
||||
@@ -78,6 +78,17 @@ function(xe_platform_sources target base_path)
|
||||
endif()
|
||||
|
||||
list(APPEND _sources ${_plat_sources})
|
||||
|
||||
# Add back architecture-specific files
|
||||
if(XE_TARGET_X86_64)
|
||||
file(${glob_mode} _arch_sources "${base_path}/*_amd64.h" "${base_path}/*_amd64.cc")
|
||||
elseif(XE_TARGET_AARCH64)
|
||||
file(${glob_mode} _arch_sources "${base_path}/*_arm64.h" "${base_path}/*_arm64.cc")
|
||||
endif()
|
||||
if(_arch_sources)
|
||||
list(APPEND _sources ${_arch_sources})
|
||||
endif()
|
||||
|
||||
target_sources(${target} PRIVATE ${_sources})
|
||||
endfunction()
|
||||
|
||||
|
||||
@@ -12,7 +12,12 @@ xe_target_defaults(xenia-core)
|
||||
# All subdirectories
|
||||
add_subdirectory(base)
|
||||
add_subdirectory(cpu)
|
||||
add_subdirectory(cpu/backend/x64)
|
||||
if(XE_TARGET_X86_64)
|
||||
add_subdirectory(cpu/backend/x64)
|
||||
endif()
|
||||
if(XE_TARGET_AARCH64)
|
||||
add_subdirectory(cpu/backend/a64)
|
||||
endif()
|
||||
add_subdirectory(apu)
|
||||
add_subdirectory(apu/nop)
|
||||
add_subdirectory(gpu)
|
||||
|
||||
@@ -24,17 +24,19 @@ if(WIN32)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/main_resources.rc
|
||||
${PROJECT_SOURCE_DIR}/src/xenia/base/app_win32.manifest
|
||||
)
|
||||
if(XE_TARGET_X86_64)
|
||||
# main_init_win.cc needs SSE2 only (not AVX)
|
||||
set_source_files_properties(
|
||||
${PROJECT_SOURCE_DIR}/src/xenia/base/main_init_win.cc
|
||||
PROPERTIES COMPILE_OPTIONS "/arch:SSE2"
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
target_sources(xenia-app PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/src/xenia/base/main_init_posix.cc
|
||||
${PROJECT_SOURCE_DIR}/src/xenia/ui/windowed_app_main_posix.cc
|
||||
)
|
||||
if(NOT MSVC)
|
||||
if(NOT MSVC AND XE_TARGET_X86_64)
|
||||
set_source_files_properties(
|
||||
${PROJECT_SOURCE_DIR}/src/xenia/base/main_init_posix.cc
|
||||
PROPERTIES COMPILE_OPTIONS "-msse2;-mno-avx"
|
||||
@@ -88,9 +90,11 @@ target_link_libraries(xenia-app PRIVATE
|
||||
xenia-hid-sdl
|
||||
)
|
||||
|
||||
# x64 backend
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
# Architecture-specific backend
|
||||
if(XE_TARGET_X86_64)
|
||||
target_link_libraries(xenia-app PRIVATE xenia-cpu-backend-x64)
|
||||
elseif(XE_TARGET_AARCH64)
|
||||
target_link_libraries(xenia-app PRIVATE xenia-cpu-backend-a64)
|
||||
endif()
|
||||
|
||||
# Platform-specific libraries
|
||||
|
||||
@@ -29,6 +29,57 @@ constexpr size_t kMaxHandlerCount = 8;
|
||||
// Executed in order.
|
||||
std::pair<ExceptionHandler::Handler, void*> handlers_[kMaxHandlerCount];
|
||||
|
||||
static void CaptureThreadContext(HostThreadContext& thread_context,
|
||||
PCONTEXT ctx) {
|
||||
#if XE_ARCH_AMD64
|
||||
thread_context.rip = ctx->Rip;
|
||||
thread_context.eflags = ctx->EFlags;
|
||||
std::memcpy(thread_context.int_registers, &ctx->Rax,
|
||||
sizeof(thread_context.int_registers));
|
||||
std::memcpy(thread_context.xmm_registers, &ctx->Xmm0,
|
||||
sizeof(thread_context.xmm_registers));
|
||||
#elif XE_ARCH_ARM64
|
||||
thread_context.pc = ctx->Pc;
|
||||
thread_context.pstate = ctx->Cpsr;
|
||||
thread_context.sp = ctx->Sp;
|
||||
std::memcpy(thread_context.x, &ctx->X0, sizeof(thread_context.x));
|
||||
std::memcpy(thread_context.v, &ctx->V[0], sizeof(thread_context.v));
|
||||
#endif
|
||||
}
|
||||
|
||||
static void RestoreThreadContext(PCONTEXT ctx,
|
||||
const HostThreadContext& thread_context,
|
||||
const Exception& ex) {
|
||||
#if XE_ARCH_AMD64
|
||||
ctx->Rip = thread_context.rip;
|
||||
ctx->EFlags = thread_context.eflags;
|
||||
uint32_t modified_register_index;
|
||||
uint16_t modified_int_registers_remaining = ex.modified_int_registers();
|
||||
while (xe::bit_scan_forward(modified_int_registers_remaining,
|
||||
&modified_register_index)) {
|
||||
modified_int_registers_remaining &=
|
||||
~(UINT16_C(1) << modified_register_index);
|
||||
(&ctx->Rax)[modified_register_index] =
|
||||
thread_context.int_registers[modified_register_index];
|
||||
}
|
||||
uint16_t modified_xmm_registers_remaining = ex.modified_xmm_registers();
|
||||
while (xe::bit_scan_forward(modified_xmm_registers_remaining,
|
||||
&modified_register_index)) {
|
||||
modified_xmm_registers_remaining &=
|
||||
~(UINT16_C(1) << modified_register_index);
|
||||
std::memcpy(&ctx->Xmm0 + modified_register_index,
|
||||
&thread_context.xmm_registers[modified_register_index],
|
||||
sizeof(vec128_t));
|
||||
}
|
||||
#elif XE_ARCH_ARM64
|
||||
ctx->Pc = thread_context.pc;
|
||||
ctx->Cpsr = thread_context.pstate;
|
||||
ctx->Sp = thread_context.sp;
|
||||
std::memcpy(&ctx->X0, thread_context.x, sizeof(thread_context.x));
|
||||
std::memcpy(&ctx->V[0], thread_context.v, sizeof(thread_context.v));
|
||||
#endif
|
||||
}
|
||||
|
||||
LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
|
||||
// Visual Studio SetThreadName.
|
||||
if (ex_info->ExceptionRecord->ExceptionCode == 0x406D1388) {
|
||||
@@ -36,12 +87,7 @@ LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
|
||||
}
|
||||
|
||||
HostThreadContext thread_context;
|
||||
thread_context.rip = ex_info->ContextRecord->Rip;
|
||||
thread_context.eflags = ex_info->ContextRecord->EFlags;
|
||||
std::memcpy(thread_context.int_registers, &ex_info->ContextRecord->Rax,
|
||||
sizeof(thread_context.int_registers));
|
||||
std::memcpy(thread_context.xmm_registers, &ex_info->ContextRecord->Xmm0,
|
||||
sizeof(thread_context.xmm_registers));
|
||||
CaptureThreadContext(thread_context, ex_info->ContextRecord);
|
||||
|
||||
// https://msdn.microsoft.com/en-us/library/ms679331(v=vs.85).aspx
|
||||
// https://msdn.microsoft.com/en-us/library/aa363082(v=vs.85).aspx
|
||||
@@ -78,26 +124,7 @@ LONG CALLBACK ExceptionHandlerCallback(PEXCEPTION_POINTERS ex_info) {
|
||||
for (size_t i = 0; i < xe::countof(handlers_) && handlers_[i].first; ++i) {
|
||||
if (handlers_[i].first(&ex, handlers_[i].second)) {
|
||||
// Exception handled.
|
||||
ex_info->ContextRecord->Rip = thread_context.rip;
|
||||
ex_info->ContextRecord->EFlags = thread_context.eflags;
|
||||
uint32_t modified_register_index;
|
||||
uint16_t modified_int_registers_remaining = ex.modified_int_registers();
|
||||
while (xe::bit_scan_forward(modified_int_registers_remaining,
|
||||
&modified_register_index)) {
|
||||
modified_int_registers_remaining &=
|
||||
~(UINT16_C(1) << modified_register_index);
|
||||
(&ex_info->ContextRecord->Rax)[modified_register_index] =
|
||||
thread_context.int_registers[modified_register_index];
|
||||
}
|
||||
uint16_t modified_xmm_registers_remaining = ex.modified_xmm_registers();
|
||||
while (xe::bit_scan_forward(modified_xmm_registers_remaining,
|
||||
&modified_register_index)) {
|
||||
modified_xmm_registers_remaining &=
|
||||
~(UINT16_C(1) << modified_register_index);
|
||||
std::memcpy(&ex_info->ContextRecord->Xmm0 + modified_register_index,
|
||||
&thread_context.xmm_registers[modified_register_index],
|
||||
sizeof(vec128_t));
|
||||
}
|
||||
RestoreThreadContext(ex_info->ContextRecord, thread_context, ex);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
}
|
||||
|
||||
12
src/xenia/cpu/backend/a64/CMakeLists.txt
Normal file
12
src/xenia/cpu/backend/a64/CMakeLists.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
add_library(xenia-cpu-backend-a64 STATIC)
|
||||
xe_platform_sources(xenia-cpu-backend-a64 ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_definitions(xenia-cpu-backend-a64 PRIVATE
|
||||
CAPSTONE_HAS_ARM64
|
||||
CAPSTONE_USE_SYS_DYN_MEM
|
||||
)
|
||||
target_include_directories(xenia-cpu-backend-a64 PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/third_party/capstone/include
|
||||
${PROJECT_SOURCE_DIR}/third_party/xbyak_aarch64/xbyak_aarch64
|
||||
)
|
||||
target_link_libraries(xenia-cpu-backend-a64 PUBLIC capstone fmt xenia-base xenia-cpu xbyak_aarch64)
|
||||
xe_target_defaults(xenia-cpu-backend-a64)
|
||||
143
src/xenia/cpu/backend/a64/a64_assembler.cc
Normal file
143
src/xenia/cpu/backend/a64/a64_assembler.cc
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/cpu/backend/a64/a64_assembler.h"
|
||||
|
||||
#include <climits>
|
||||
|
||||
#include "third_party/capstone/include/capstone/aarch64.h"
|
||||
#include "third_party/capstone/include/capstone/capstone.h"
|
||||
#include "xenia/base/profiling.h"
|
||||
#include "xenia/base/reset_scope.h"
|
||||
#include "xenia/base/string.h"
|
||||
#include "xenia/cpu/backend/a64/a64_backend.h"
|
||||
#include "xenia/cpu/backend/a64/a64_code_cache.h"
|
||||
#include "xenia/cpu/backend/a64/a64_emitter.h"
|
||||
#include "xenia/cpu/backend/a64/a64_function.h"
|
||||
#include "xenia/cpu/cpu_flags.h"
|
||||
#include "xenia/cpu/hir/hir_builder.h"
|
||||
#include "xenia/cpu/hir/label.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
namespace backend {
|
||||
namespace a64 {
|
||||
|
||||
using xe::cpu::hir::HIRBuilder;
|
||||
|
||||
A64Assembler::A64Assembler(A64Backend* backend)
|
||||
: Assembler(backend), a64_backend_(backend), capstone_handle_(0) {
|
||||
if (cs_open(CS_ARCH_AARCH64, CS_MODE_ARM, &capstone_handle_) != CS_ERR_OK) {
|
||||
assert_always("Failed to initialize capstone for ARM64");
|
||||
}
|
||||
cs_option(capstone_handle_, CS_OPT_DETAIL, CS_OPT_OFF);
|
||||
}
|
||||
|
||||
A64Assembler::~A64Assembler() {
|
||||
emitter_.reset();
|
||||
|
||||
if (capstone_handle_) {
|
||||
cs_close(&capstone_handle_);
|
||||
}
|
||||
}
|
||||
|
||||
bool A64Assembler::Initialize() {
|
||||
if (!Assembler::Initialize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
emitter_.reset(new A64Emitter(a64_backend_, &allocator_));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void A64Assembler::Reset() {
|
||||
string_buffer_.Reset();
|
||||
Assembler::Reset();
|
||||
}
|
||||
|
||||
bool A64Assembler::Assemble(GuestFunction* function, HIRBuilder* builder,
|
||||
uint32_t debug_info_flags,
|
||||
std::unique_ptr<FunctionDebugInfo> debug_info) {
|
||||
SCOPE_profile_cpu_f("cpu");
|
||||
|
||||
// Reset when we leave.
|
||||
xe::make_reset_scope(this);
|
||||
|
||||
// Lower HIR -> ARM64.
|
||||
void* machine_code = nullptr;
|
||||
size_t code_size = 0;
|
||||
if (!emitter_->Emit(function, builder, debug_info_flags, debug_info.get(),
|
||||
&machine_code, &code_size, &function->source_map())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stash generated machine code.
|
||||
if (debug_info_flags & DebugInfoFlags::kDebugInfoDisasmMachineCode) {
|
||||
DumpMachineCode(machine_code, code_size, function->source_map(),
|
||||
&string_buffer_);
|
||||
debug_info->set_machine_code_disasm(xe_strdup(string_buffer_.buffer()));
|
||||
string_buffer_.Reset();
|
||||
}
|
||||
|
||||
function->set_debug_info(std::move(debug_info));
|
||||
static_cast<A64Function*>(function)->Setup(
|
||||
reinterpret_cast<uint8_t*>(machine_code), code_size);
|
||||
|
||||
// Install into indirection table.
|
||||
uint64_t host_address = reinterpret_cast<uint64_t>(machine_code);
|
||||
assert_true((host_address >> 32) == 0);
|
||||
reinterpret_cast<A64CodeCache*>(backend_->code_cache())
|
||||
->AddIndirection(function->address(),
|
||||
static_cast<uint32_t>(host_address));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void A64Assembler::DumpMachineCode(
|
||||
void* machine_code, size_t code_size,
|
||||
const std::vector<SourceMapEntry>& source_map, StringBuffer* str) {
|
||||
if (source_map.empty()) {
|
||||
return;
|
||||
}
|
||||
auto source_map_index = 0;
|
||||
uint32_t next_code_offset = source_map[0].code_offset;
|
||||
|
||||
const uint8_t* code_ptr = reinterpret_cast<uint8_t*>(machine_code);
|
||||
size_t remaining_code_size = code_size;
|
||||
uint64_t address = uint64_t(machine_code);
|
||||
cs_insn insn = {0};
|
||||
while (remaining_code_size &&
|
||||
cs_disasm_iter(capstone_handle_, &code_ptr, &remaining_code_size,
|
||||
&address, &insn)) {
|
||||
// Look up source offset.
|
||||
auto code_offset =
|
||||
uint32_t(code_ptr - reinterpret_cast<uint8_t*>(machine_code));
|
||||
if (code_offset >= next_code_offset &&
|
||||
source_map_index < source_map.size()) {
|
||||
auto& source_map_entry = source_map[source_map_index];
|
||||
str->AppendFormat("{:08X} ", source_map_entry.guest_address);
|
||||
++source_map_index;
|
||||
next_code_offset = source_map_index < source_map.size()
|
||||
? source_map[source_map_index].code_offset
|
||||
: UINT_MAX;
|
||||
} else {
|
||||
str->Append(" ");
|
||||
}
|
||||
|
||||
str->AppendFormat("{:08X} {:<6} {}\n", uint32_t(insn.address),
|
||||
insn.mnemonic, insn.op_str);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace a64
|
||||
} // namespace backend
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
60
src/xenia/cpu/backend/a64/a64_assembler.h
Normal file
60
src/xenia/cpu/backend/a64/a64_assembler.h
Normal 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_
|
||||
869
src/xenia/cpu/backend/a64/a64_backend.cc
Normal file
869
src/xenia/cpu/backend/a64/a64_backend.cc
Normal 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
|
||||
175
src/xenia/cpu/backend/a64/a64_backend.h
Normal file
175
src/xenia/cpu/backend/a64/a64_backend.h
Normal 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_
|
||||
48
src/xenia/cpu/backend/a64/a64_code_cache.cc
Normal file
48
src/xenia/cpu/backend/a64/a64_code_cache.cc
Normal 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
|
||||
54
src/xenia/cpu/backend/a64/a64_code_cache.h
Normal file
54
src/xenia/cpu/backend/a64/a64_code_cache.h
Normal 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_
|
||||
325
src/xenia/cpu/backend/a64/a64_code_cache_posix.cc
Normal file
325
src/xenia/cpu/backend/a64/a64_code_cache_posix.cc
Normal 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
|
||||
419
src/xenia/cpu/backend/a64/a64_code_cache_win.cc
Normal file
419
src/xenia/cpu/backend/a64/a64_code_cache_win.cc
Normal 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
|
||||
646
src/xenia/cpu/backend/a64/a64_emitter.cc
Normal file
646
src/xenia/cpu/backend/a64/a64_emitter.cc
Normal 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
|
||||
192
src/xenia/cpu/backend/a64/a64_emitter.h
Normal file
192
src/xenia/cpu/backend/a64/a64_emitter.h
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#ifndef XENIA_CPU_BACKEND_A64_A64_EMITTER_H_
|
||||
#define XENIA_CPU_BACKEND_A64_A64_EMITTER_H_
|
||||
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/arena.h"
|
||||
#include "xenia/cpu/backend/code_cache_base.h"
|
||||
#include "xenia/cpu/function.h"
|
||||
#include "xenia/cpu/function_trace_data.h"
|
||||
#include "xenia/cpu/hir/hir_builder.h"
|
||||
#include "xenia/cpu/hir/instr.h"
|
||||
#include "xenia/cpu/hir/value.h"
|
||||
#include "xenia/cpu/xex_module.h"
|
||||
#include "xenia/memory.h"
|
||||
|
||||
#include "xbyak_aarch64.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
class Processor;
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
namespace backend {
|
||||
namespace a64 {
|
||||
|
||||
class A64Backend;
|
||||
class A64CodeCache;
|
||||
|
||||
enum class FPCRMode : uint32_t { Unknown, Fpu, Vmx };
|
||||
|
||||
// Unfortunately due to the design of xbyak we have to pass this to the ctor.
|
||||
class XbyakA64Allocator : public Xbyak_aarch64::Allocator {
|
||||
public:
|
||||
virtual bool useProtect() const { return false; }
|
||||
};
|
||||
|
||||
class A64Emitter;
|
||||
using TailEmitCallback =
|
||||
std::function<void(A64Emitter& e, Xbyak_aarch64::Label& lbl)>;
|
||||
struct TailEmitter {
|
||||
Xbyak_aarch64::Label label;
|
||||
uint32_t alignment;
|
||||
TailEmitCallback func;
|
||||
};
|
||||
|
||||
class A64Emitter : public Xbyak_aarch64::CodeGenerator {
|
||||
public:
|
||||
A64Emitter(A64Backend* backend, XbyakA64Allocator* allocator);
|
||||
virtual ~A64Emitter();
|
||||
|
||||
Processor* processor() const { return processor_; }
|
||||
A64Backend* backend() const { return backend_; }
|
||||
|
||||
bool Emit(GuestFunction* function, hir::HIRBuilder* builder,
|
||||
uint32_t debug_info_flags, FunctionDebugInfo* debug_info,
|
||||
void** out_code_address, size_t* out_code_size,
|
||||
std::vector<SourceMapEntry>* out_source_map);
|
||||
|
||||
public:
|
||||
// Reserved: sp, x20 (context), x21 (membase)
|
||||
// Scratch: x0-x18 (caller-saved), v0-v3
|
||||
// Available GPRs for register allocator: x19, x22-x28
|
||||
static constexpr int GPR_COUNT = 8;
|
||||
// Available VEC regs: v4-v7, v16-v31
|
||||
static constexpr int VEC_COUNT = 20;
|
||||
static constexpr size_t kStashOffset = 32;
|
||||
|
||||
static void SetupReg(const hir::Value* v, Xbyak_aarch64::WReg& r) {
|
||||
auto idx = gpr_reg_map_[v->reg.index];
|
||||
r = Xbyak_aarch64::WReg(idx);
|
||||
}
|
||||
static void SetupReg(const hir::Value* v, Xbyak_aarch64::XReg& r) {
|
||||
auto idx = gpr_reg_map_[v->reg.index];
|
||||
r = Xbyak_aarch64::XReg(idx);
|
||||
}
|
||||
static void SetupReg(const hir::Value* v, Xbyak_aarch64::SReg& r) {
|
||||
auto idx = vec_reg_map_[v->reg.index];
|
||||
r = Xbyak_aarch64::SReg(idx);
|
||||
}
|
||||
static void SetupReg(const hir::Value* v, Xbyak_aarch64::DReg& r) {
|
||||
auto idx = vec_reg_map_[v->reg.index];
|
||||
r = Xbyak_aarch64::DReg(idx);
|
||||
}
|
||||
static void SetupReg(const hir::Value* v, Xbyak_aarch64::QReg& r) {
|
||||
auto idx = vec_reg_map_[v->reg.index];
|
||||
r = Xbyak_aarch64::QReg(idx);
|
||||
}
|
||||
static void SetupReg(const hir::Value* v, Xbyak_aarch64::VReg& r) {
|
||||
auto idx = vec_reg_map_[v->reg.index];
|
||||
r = Xbyak_aarch64::VReg(idx);
|
||||
}
|
||||
|
||||
Xbyak_aarch64::Label& epilog_label() { return *epilog_label_; }
|
||||
|
||||
FunctionDebugInfo* debug_info() const { return debug_info_; }
|
||||
size_t stack_size() const { return stack_size_; }
|
||||
|
||||
void MarkSourceOffset(const hir::Instr* i);
|
||||
|
||||
void DebugBreak();
|
||||
void Trap(uint16_t trap_type = 0);
|
||||
void UnimplementedInstr(const hir::Instr* i);
|
||||
|
||||
void Call(const hir::Instr* instr, GuestFunction* function);
|
||||
void CallIndirect(const hir::Instr* instr, int reg_index);
|
||||
void CallExtern(const hir::Instr* instr, const Function* function);
|
||||
void CallNative(void* fn);
|
||||
void CallNativeSafe(void* fn);
|
||||
void SetReturnAddress(uint64_t value);
|
||||
|
||||
// Context register = x20.
|
||||
const Xbyak_aarch64::XReg& GetContextReg() const { return x20; }
|
||||
// Memory base register = x21.
|
||||
const Xbyak_aarch64::XReg& GetMembaseReg() const { return x21; }
|
||||
|
||||
void ReloadMembase();
|
||||
|
||||
void PushStackpoint();
|
||||
void PopStackpoint();
|
||||
void EnsureSynchronizedGuestAndHostStack();
|
||||
|
||||
static void HandleStackpointOverflowError(ppc::PPCContext* context);
|
||||
|
||||
void ForgetFpcrMode() { fpcr_mode_ = FPCRMode::Unknown; }
|
||||
bool ChangeFpcrMode(FPCRMode new_mode, bool already_set = false);
|
||||
|
||||
Xbyak_aarch64::Label& AddToTail(TailEmitCallback callback,
|
||||
uint32_t alignment = 0);
|
||||
Xbyak_aarch64::Label& NewCachedLabel();
|
||||
|
||||
// Get or create a xbyak_aarch64 label for a HIR label ID.
|
||||
Xbyak_aarch64::Label& GetLabel(uint32_t label_id);
|
||||
|
||||
XexModule* GuestModule() { return guest_module_; }
|
||||
|
||||
protected:
|
||||
void* Emplace(const EmitFunctionInfo& func_info,
|
||||
GuestFunction* function = nullptr);
|
||||
bool Emit(hir::HIRBuilder* builder, EmitFunctionInfo& func_info);
|
||||
|
||||
protected:
|
||||
Processor* processor_ = nullptr;
|
||||
A64Backend* backend_ = nullptr;
|
||||
A64CodeCache* code_cache_ = nullptr;
|
||||
XbyakA64Allocator* allocator_ = nullptr;
|
||||
XexModule* guest_module_ = nullptr;
|
||||
uint32_t current_guest_function_ = 0;
|
||||
|
||||
Xbyak_aarch64::Label* epilog_label_ = nullptr;
|
||||
|
||||
hir::Instr* current_instr_ = nullptr;
|
||||
|
||||
FunctionDebugInfo* debug_info_ = nullptr;
|
||||
uint32_t debug_info_flags_ = 0;
|
||||
FunctionTraceData* trace_data_ = nullptr;
|
||||
Arena source_map_arena_;
|
||||
|
||||
size_t stack_size_ = 0;
|
||||
|
||||
static const uint32_t gpr_reg_map_[GPR_COUNT];
|
||||
static const uint32_t vec_reg_map_[VEC_COUNT];
|
||||
|
||||
std::vector<TailEmitter> tail_code_;
|
||||
std::vector<Xbyak_aarch64::Label*> label_cache_;
|
||||
|
||||
// Map from HIR label IDs to xbyak_aarch64 Labels.
|
||||
std::unordered_map<uint32_t, Xbyak_aarch64::Label*> label_map_;
|
||||
|
||||
FPCRMode fpcr_mode_ = FPCRMode::Unknown;
|
||||
bool synchronize_stack_on_next_instruction_ = false;
|
||||
};
|
||||
|
||||
} // namespace a64
|
||||
} // namespace backend
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
|
||||
#endif // XENIA_CPU_BACKEND_A64_A64_EMITTER_H_
|
||||
48
src/xenia/cpu/backend/a64/a64_function.cc
Normal file
48
src/xenia/cpu/backend/a64/a64_function.cc
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* Xenia : Xbox 360 Emulator Research Project *
|
||||
******************************************************************************
|
||||
* Copyright 2026 Ben Vanik. All rights reserved. *
|
||||
* Released under the BSD license - see LICENSE in the root for more details. *
|
||||
******************************************************************************
|
||||
*/
|
||||
|
||||
#include "xenia/cpu/backend/a64/a64_function.h"
|
||||
|
||||
#include "xenia/cpu/backend/a64/a64_backend.h"
|
||||
#include "xenia/cpu/processor.h"
|
||||
#include "xenia/cpu/thread_state.h"
|
||||
|
||||
namespace xe {
|
||||
namespace cpu {
|
||||
namespace backend {
|
||||
namespace a64 {
|
||||
|
||||
A64Function::A64Function(Module* module, uint32_t address)
|
||||
: GuestFunction(module, address) {}
|
||||
|
||||
A64Function::~A64Function() {
|
||||
// machine_code_ is freed by code cache.
|
||||
}
|
||||
|
||||
void A64Function::Setup(uint8_t* machine_code, size_t machine_code_length) {
|
||||
machine_code_ = machine_code;
|
||||
machine_code_length_ = machine_code_length;
|
||||
}
|
||||
|
||||
bool A64Function::CallImpl(ThreadState* thread_state, uint32_t return_address) {
|
||||
auto backend =
|
||||
reinterpret_cast<A64Backend*>(thread_state->processor()->backend());
|
||||
auto thunk = backend->host_to_guest_thunk();
|
||||
if (!thunk || !machine_code_) {
|
||||
return false;
|
||||
}
|
||||
thunk(machine_code_, thread_state->context(),
|
||||
reinterpret_cast<void*>(uintptr_t(return_address)));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace a64
|
||||
} // namespace backend
|
||||
} // namespace cpu
|
||||
} // namespace xe
|
||||
44
src/xenia/cpu/backend/a64/a64_function.h
Normal file
44
src/xenia/cpu/backend/a64/a64_function.h
Normal 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_
|
||||
419
src/xenia/cpu/backend/a64/a64_op.h
Normal file
419
src/xenia/cpu/backend/a64/a64_op.h
Normal 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_
|
||||
504
src/xenia/cpu/backend/a64/a64_seq_control.cc
Normal file
504
src/xenia/cpu/backend/a64/a64_seq_control.cc
Normal 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
|
||||
1210
src/xenia/cpu/backend/a64/a64_seq_memory.cc
Normal file
1210
src/xenia/cpu/backend/a64/a64_seq_memory.cc
Normal file
File diff suppressed because it is too large
Load Diff
342
src/xenia/cpu/backend/a64/a64_seq_util.h
Normal file
342
src/xenia/cpu/backend/a64/a64_seq_util.h
Normal 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_
|
||||
1920
src/xenia/cpu/backend/a64/a64_seq_vector.cc
Normal file
1920
src/xenia/cpu/backend/a64/a64_seq_vector.cc
Normal file
File diff suppressed because it is too large
Load Diff
4843
src/xenia/cpu/backend/a64/a64_sequences.cc
Normal file
4843
src/xenia/cpu/backend/a64/a64_sequences.cc
Normal file
File diff suppressed because it is too large
Load Diff
52
src/xenia/cpu/backend/a64/a64_sequences.h
Normal file
52
src/xenia/cpu/backend/a64/a64_sequences.h
Normal 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_
|
||||
85
src/xenia/cpu/backend/a64/a64_stack_layout.h
Normal file
85
src/xenia/cpu/backend/a64/a64_stack_layout.h
Normal 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_
|
||||
231
src/xenia/cpu/backend/a64/a64_tracers.cc
Normal file
231
src/xenia/cpu/backend/a64/a64_tracers.cc
Normal 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
|
||||
77
src/xenia/cpu/backend/a64/a64_tracers.h
Normal file
77
src/xenia/cpu/backend/a64/a64_tracers.h
Normal 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_
|
||||
@@ -29,9 +29,11 @@ target_link_libraries(xenia-cpu-ppc-tests PRIVATE
|
||||
capstone fmt imgui mspack
|
||||
)
|
||||
|
||||
# x64 backend
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
# Architecture-specific backend
|
||||
if(XE_TARGET_X86_64)
|
||||
target_link_libraries(xenia-cpu-ppc-tests PRIVATE xenia-cpu-backend-x64)
|
||||
elseif(XE_TARGET_AARCH64)
|
||||
target_link_libraries(xenia-cpu-ppc-tests PRIVATE xenia-cpu-backend-a64)
|
||||
endif()
|
||||
|
||||
xe_target_defaults(xenia-cpu-ppc-tests)
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
|
||||
#if XE_ARCH_AMD64
|
||||
#include "xenia/cpu/backend/x64/x64_backend.h"
|
||||
#elif XE_ARCH_ARM64
|
||||
#include "xenia/cpu/backend/a64/a64_backend.h"
|
||||
#endif // XE_ARCH
|
||||
|
||||
#if XE_COMPILER_MSVC
|
||||
@@ -248,11 +250,17 @@ class TestRunner {
|
||||
if (cvars::cpu == "x64") {
|
||||
backend.reset(new xe::cpu::backend::x64::X64Backend());
|
||||
}
|
||||
#elif XE_ARCH_ARM64
|
||||
if (cvars::cpu == "a64") {
|
||||
backend.reset(new xe::cpu::backend::a64::A64Backend());
|
||||
}
|
||||
#endif // XE_ARCH
|
||||
if (cvars::cpu == "any") {
|
||||
if (!backend) {
|
||||
#if XE_ARCH_AMD64
|
||||
backend.reset(new xe::cpu::backend::x64::X64Backend());
|
||||
#elif XE_ARCH_ARM64
|
||||
backend.reset(new xe::cpu::backend::a64::A64Backend());
|
||||
#endif // XE_ARCH
|
||||
}
|
||||
}
|
||||
@@ -310,6 +318,21 @@ class TestRunner {
|
||||
x64_backend->BackendContextForGuestContext(thread_state_->context());
|
||||
bctx->flags &= ~(1U << xe::cpu::backend::x64::kX64BackendMXCSRModeBit);
|
||||
}
|
||||
#elif XE_ARCH_ARM64
|
||||
// Reset FPCR and backend flags to default FPU state before each test.
|
||||
{
|
||||
auto* a64_backend = static_cast<xe::cpu::backend::a64::A64Backend*>(
|
||||
processor_->backend());
|
||||
auto* bctx =
|
||||
a64_backend->BackendContextForGuestContext(thread_state_->context());
|
||||
bctx->flags &= ~(1U << xe::cpu::backend::a64::kA64BackendFPCRModeBit);
|
||||
// Explicitly reset the hardware FPCR to default FPU mode (0 = round
|
||||
// nearest, no flush-to-zero, no default-NaN). Without this, a previous
|
||||
// test that set VMX mode (FZ|DN) leaves the hardware FPCR dirty, and
|
||||
// subsequent scalar FP tests produce wrong NaN results because DN=1
|
||||
// causes ARM64 to return the default NaN instead of propagating inputs.
|
||||
a64_backend->SetGuestRoundingMode(thread_state_->context(), 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Execute test.
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
#include "xenia/cpu/xex_module.h"
|
||||
|
||||
// TODO(benvanik): based on compiler support
|
||||
#if XE_ARCH_AMD64
|
||||
#include "xenia/cpu/backend/x64/x64_backend.h"
|
||||
#elif XE_ARCH_ARM64
|
||||
#include "xenia/cpu/backend/a64/a64_backend.h"
|
||||
#endif
|
||||
|
||||
#if 0 && DEBUG
|
||||
#define DEFAULT_DEBUG_FLAG true
|
||||
|
||||
@@ -173,40 +173,71 @@ class Win32StackWalker : public StackWalker {
|
||||
} else {
|
||||
// Copy thread context local. We will be modifying it during stack
|
||||
// walking, so we don't want to mess with the incoming copy.
|
||||
#if XE_ARCH_AMD64
|
||||
thread_context.Rip = in_host_context->rip;
|
||||
thread_context.EFlags = in_host_context->eflags;
|
||||
std::memcpy(&thread_context.Rax, in_host_context->int_registers,
|
||||
sizeof(in_host_context->int_registers));
|
||||
std::memcpy(&thread_context.Xmm0, in_host_context->xmm_registers,
|
||||
sizeof(in_host_context->xmm_registers));
|
||||
#elif XE_ARCH_ARM64
|
||||
thread_context.Pc = in_host_context->pc;
|
||||
thread_context.Cpsr = in_host_context->pstate;
|
||||
thread_context.Sp = in_host_context->sp;
|
||||
std::memcpy(&thread_context.X0, in_host_context->x,
|
||||
sizeof(in_host_context->x));
|
||||
std::memcpy(&thread_context.V[0], in_host_context->v,
|
||||
sizeof(in_host_context->v));
|
||||
#endif
|
||||
}
|
||||
|
||||
if (out_host_context) {
|
||||
// Write out the captured thread context if the caller asked for it.
|
||||
#if XE_ARCH_AMD64
|
||||
out_host_context->rip = thread_context.Rip;
|
||||
out_host_context->eflags = thread_context.EFlags;
|
||||
std::memcpy(out_host_context->int_registers, &thread_context.Rax,
|
||||
sizeof(out_host_context->int_registers));
|
||||
std::memcpy(out_host_context->xmm_registers, &thread_context.Xmm0,
|
||||
sizeof(out_host_context->xmm_registers));
|
||||
#elif XE_ARCH_ARM64
|
||||
out_host_context->pc = thread_context.Pc;
|
||||
out_host_context->pstate = thread_context.Cpsr;
|
||||
out_host_context->sp = thread_context.Sp;
|
||||
std::memcpy(out_host_context->x, &thread_context.X0,
|
||||
sizeof(out_host_context->x));
|
||||
std::memcpy(out_host_context->v, &thread_context.V[0],
|
||||
sizeof(out_host_context->v));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Setup the frame for walking.
|
||||
STACKFRAME64 stack_frame = {};
|
||||
stack_frame.AddrPC.Mode = AddrModeFlat;
|
||||
stack_frame.AddrPC.Offset = thread_context.Rip;
|
||||
stack_frame.AddrFrame.Mode = AddrModeFlat;
|
||||
stack_frame.AddrFrame.Offset = thread_context.Rbp;
|
||||
stack_frame.AddrStack.Mode = AddrModeFlat;
|
||||
#if XE_ARCH_AMD64
|
||||
stack_frame.AddrPC.Offset = thread_context.Rip;
|
||||
stack_frame.AddrFrame.Offset = thread_context.Rbp;
|
||||
stack_frame.AddrStack.Offset = thread_context.Rsp;
|
||||
DWORD machine_type = IMAGE_FILE_MACHINE_AMD64;
|
||||
#elif XE_ARCH_ARM64
|
||||
stack_frame.AddrPC.Offset = thread_context.Pc;
|
||||
stack_frame.AddrFrame.Offset = thread_context.Fp;
|
||||
stack_frame.AddrStack.Offset = thread_context.Sp;
|
||||
DWORD machine_type = IMAGE_FILE_MACHINE_ARM64;
|
||||
#endif
|
||||
|
||||
// Walk the stack.
|
||||
// Note that StackWalk64 is thread safe, though other dbghelp functions are
|
||||
// not.
|
||||
// TODO(has207): verify StackWalk64 + dbghelp callbacks work for JIT'd
|
||||
// ARM64 code with dynamically registered unwind info
|
||||
// (RtlAddGrowableFunctionTable).
|
||||
size_t frame_index = 0;
|
||||
while (frame_index < frame_count &&
|
||||
stack_walk_64_(IMAGE_FILE_MACHINE_AMD64, GetCurrentProcess(),
|
||||
thread_handle, &stack_frame, &thread_context, nullptr,
|
||||
stack_walk_64_(machine_type, GetCurrentProcess(), thread_handle,
|
||||
&stack_frame, &thread_context, nullptr,
|
||||
XSymFunctionTableAccess64, XSymGetModuleBase64,
|
||||
nullptr) == TRUE) {
|
||||
if (frame_index >= frame_offset) {
|
||||
|
||||
@@ -3,5 +3,6 @@ xe_test_suite(xenia-cpu-tests ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
capstone fmt imgui
|
||||
xenia-base xenia-core xenia-cpu xenia-gpu
|
||||
xenia-kernel xenia-ui xenia-patcher
|
||||
$<$<OR:$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},x86_64>,$<STREQUAL:${CMAKE_SYSTEM_PROCESSOR},AMD64>>:xenia-cpu-backend-x64>
|
||||
$<$<BOOL:${XE_TARGET_X86_64}>:xenia-cpu-backend-x64>
|
||||
$<$<BOOL:${XE_TARGET_AARCH64}>:xenia-cpu-backend-a64>
|
||||
)
|
||||
|
||||
@@ -13,7 +13,11 @@
|
||||
#include <vector>
|
||||
|
||||
#include "xenia/base/platform.h"
|
||||
#if XE_ARCH_AMD64
|
||||
#include "xenia/cpu/backend/x64/x64_backend.h"
|
||||
#elif XE_ARCH_ARM64
|
||||
#include "xenia/cpu/backend/a64/a64_backend.h"
|
||||
#endif // XE_ARCH
|
||||
#include "xenia/cpu/hir/hir_builder.h"
|
||||
#include "xenia/cpu/ppc/ppc_context.h"
|
||||
#include "xenia/cpu/ppc/ppc_frontend.h"
|
||||
@@ -38,6 +42,8 @@ class TestFunction {
|
||||
std::unique_ptr<xe::cpu::backend::Backend> backend;
|
||||
#if XE_ARCH_AMD64
|
||||
backend.reset(new xe::cpu::backend::x64::X64Backend());
|
||||
#elif XE_ARCH_ARM64
|
||||
backend.reset(new xe::cpu::backend::a64::A64Backend());
|
||||
#endif // XE_ARCH
|
||||
if (backend) {
|
||||
auto processor = std::make_unique<Processor>(memory.get(), nullptr);
|
||||
|
||||
@@ -970,6 +970,7 @@ void DebugWindow::DrawRegistersPane() {
|
||||
} break;
|
||||
case RegisterGroup::kHostGeneral: {
|
||||
ImGui::BeginChild("##host_general");
|
||||
#if XE_ARCH_AMD64
|
||||
for (int i = 0; i < 18; ++i) {
|
||||
auto reg = static_cast<X64Register>(i);
|
||||
ImGui::BeginGroup();
|
||||
@@ -990,10 +991,37 @@ void DebugWindow::DrawRegistersPane() {
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
#elif XE_ARCH_ARM64
|
||||
// x0-x30, sp, pc, pstate
|
||||
for (int i = 0; i < 34; ++i) {
|
||||
auto reg = static_cast<Arm64Register>(i);
|
||||
ImGui::BeginGroup();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%5s", HostThreadContext::GetRegisterName(reg));
|
||||
ImGui::SameLine();
|
||||
ImGui::Dummy(ImVec2(4, 0));
|
||||
ImGui::SameLine();
|
||||
if (i < 31) {
|
||||
dirty_host_context |=
|
||||
DrawRegisterTextBox(i, &thread_info->host_context.x[i]);
|
||||
} else if (i == 31) {
|
||||
dirty_host_context |=
|
||||
DrawRegisterTextBox(i, &thread_info->host_context.sp);
|
||||
} else if (i == 32) {
|
||||
dirty_host_context |=
|
||||
DrawRegisterTextBox(i, &thread_info->host_context.pc);
|
||||
} else {
|
||||
dirty_host_context |=
|
||||
DrawRegisterTextBox(i, &thread_info->host_context.pstate);
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
#endif
|
||||
ImGui::EndChild();
|
||||
} break;
|
||||
case RegisterGroup::kHostVector: {
|
||||
ImGui::BeginChild("##host_vector");
|
||||
#if XE_ARCH_AMD64
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
auto reg =
|
||||
static_cast<X64Register>(static_cast<int>(X64Register::kXmm0) + i);
|
||||
@@ -1007,6 +1035,21 @@ void DebugWindow::DrawRegistersPane() {
|
||||
i, thread_info->host_context.xmm_registers[i].f32);
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
#elif XE_ARCH_ARM64
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
auto reg = static_cast<Arm64Register>(
|
||||
static_cast<int>(Arm64Register::kV0) + i);
|
||||
ImGui::BeginGroup();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%5s", HostThreadContext::GetRegisterName(reg));
|
||||
ImGui::SameLine();
|
||||
ImGui::Dummy(ImVec2(4, 0));
|
||||
ImGui::SameLine();
|
||||
dirty_host_context |=
|
||||
DrawRegisterTextBoxes(i, thread_info->host_context.v[i].f32);
|
||||
ImGui::EndGroup();
|
||||
}
|
||||
#endif
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
|
||||
#if XE_ARCH_AMD64
|
||||
#include "xenia/cpu/backend/x64/x64_backend.h"
|
||||
#elif XE_ARCH_ARM64
|
||||
#include "xenia/cpu/backend/a64/a64_backend.h"
|
||||
#endif // XE_ARCH
|
||||
|
||||
DEFINE_double(time_scalar, 1.0,
|
||||
@@ -232,11 +234,17 @@ X_STATUS Emulator::Setup(
|
||||
if (cvars::cpu == "x64") {
|
||||
backend.reset(new xe::cpu::backend::x64::X64Backend());
|
||||
}
|
||||
#elif XE_ARCH_ARM64
|
||||
if (cvars::cpu == "a64") {
|
||||
backend.reset(new xe::cpu::backend::a64::A64Backend());
|
||||
}
|
||||
#endif // XE_ARCH
|
||||
if (cvars::cpu == "any") {
|
||||
if (!backend) {
|
||||
#if XE_ARCH_AMD64
|
||||
backend.reset(new xe::cpu::backend::x64::X64Backend());
|
||||
#elif XE_ARCH_ARM64
|
||||
backend.reset(new xe::cpu::backend::a64::A64Backend());
|
||||
#endif // XE_ARCH
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ if(XENIA_BUILD_MISC)
|
||||
xenia-kernel xenia-patcher xenia-ui xenia-ui-d3d12 xenia-vfs
|
||||
aes_128 capstone dxbc fmt imgui libavcodec libavutil mspack snappy xxhash
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
if(XE_TARGET_X86_64)
|
||||
target_link_libraries(xenia-gpu-d3d12-trace-viewer PRIVATE xenia-cpu-backend-x64)
|
||||
endif()
|
||||
xe_target_defaults(xenia-gpu-d3d12-trace-viewer)
|
||||
@@ -34,7 +34,7 @@ if(XENIA_BUILD_MISC)
|
||||
xenia-kernel xenia-patcher xenia-ui xenia-ui-d3d12 xenia-vfs
|
||||
aes_128 capstone dxbc fmt imgui libavcodec libavutil mspack snappy xxhash
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
if(XE_TARGET_X86_64)
|
||||
target_link_libraries(xenia-gpu-d3d12-trace-dump PRIVATE xenia-cpu-backend-x64)
|
||||
endif()
|
||||
xe_target_defaults(xenia-gpu-d3d12-trace-dump)
|
||||
|
||||
@@ -32,7 +32,7 @@ if(XENIA_BUILD_MISC)
|
||||
xenia-kernel xenia-patcher xenia-ui xenia-ui-vulkan xenia-vfs
|
||||
aes_128 capstone fmt glslang-spirv imgui libavcodec libavutil mspack snappy xxhash
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
if(XE_TARGET_X86_64)
|
||||
target_link_libraries(xenia-gpu-vulkan-trace-viewer PRIVATE xenia-cpu-backend-x64)
|
||||
endif()
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
@@ -61,7 +61,7 @@ if(XENIA_BUILD_MISC)
|
||||
xenia-kernel xenia-patcher xenia-ui xenia-ui-vulkan xenia-vfs
|
||||
aes_128 capstone fmt glslang-spirv imgui libavcodec libavutil mspack snappy xxhash
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
if(XE_TARGET_X86_64)
|
||||
target_link_libraries(xenia-gpu-vulkan-trace-dump PRIVATE xenia-cpu-backend-x64)
|
||||
endif()
|
||||
xe_target_defaults(xenia-gpu-vulkan-trace-dump)
|
||||
|
||||
@@ -75,7 +75,13 @@ struct HostExceptionReport {
|
||||
: ExceptionInfo(_ExceptionInfo),
|
||||
Report_Scratchpos(0u),
|
||||
last_win32_error(GetLastError()),
|
||||
#if XE_ARCH_AMD64
|
||||
last_ntstatus(__readgsdword(0x1250)),
|
||||
#elif XE_ARCH_ARM64
|
||||
// TEB.LastStatusValue at offset 0x1250 (same as x64).
|
||||
// ARM64 uses x18 register for TEB base instead of GS segment.
|
||||
last_ntstatus(__readx18dword(0x1250)),
|
||||
#endif
|
||||
errno_value(errno),
|
||||
address_format_ring_index(0)
|
||||
|
||||
@@ -168,9 +174,13 @@ static bool exception_pointers_handler(HostExceptionReport* report) {
|
||||
PVOID exception_addr =
|
||||
report->ExceptionInfo->ExceptionRecord->ExceptionAddress;
|
||||
|
||||
#if XE_ARCH_AMD64
|
||||
DWORD64 last_stackpointer = report->ExceptionInfo->ContextRecord->Rsp;
|
||||
|
||||
DWORD64 last_rip = report->ExceptionInfo->ContextRecord->Rip;
|
||||
#elif XE_ARCH_ARM64
|
||||
DWORD64 last_stackpointer = report->ExceptionInfo->ContextRecord->Sp;
|
||||
DWORD64 last_rip = report->ExceptionInfo->ContextRecord->Pc;
|
||||
#endif
|
||||
DWORD except_code = report->ExceptionInfo->ExceptionRecord->ExceptionCode;
|
||||
|
||||
std::string build = (
|
||||
|
||||
187
third_party/CMakeLists.txt
vendored
187
third_party/CMakeLists.txt
vendored
@@ -7,6 +7,27 @@ else()
|
||||
add_compile_options(-w)
|
||||
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
|
||||
# ==============================================================================
|
||||
@@ -21,25 +42,37 @@ target_include_directories(aes_128 PUBLIC aes_128)
|
||||
add_library(capstone STATIC
|
||||
capstone/cs.c
|
||||
capstone/MCInst.c
|
||||
capstone/MCInstPrinter.c
|
||||
capstone/MCInstrDesc.c
|
||||
capstone/MCRegisterInfo.c
|
||||
capstone/SStream.c
|
||||
capstone/utils.c
|
||||
capstone/Mapping.c
|
||||
)
|
||||
# Glob X86 arch files
|
||||
file(GLOB _capstone_x86_c "capstone/arch/X86/*.c")
|
||||
file(GLOB _capstone_x86_h "capstone/arch/X86/*.h")
|
||||
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_inc EXCLUDE REGEX "reduce\\.inc$")
|
||||
target_sources(capstone PRIVATE ${_capstone_x86_c} ${_capstone_x86_h} ${_capstone_x86_inc})
|
||||
target_compile_definitions(capstone PRIVATE
|
||||
# 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_h "capstone/arch/X86/*.h")
|
||||
file(GLOB _capstone_x86_inc "capstone/arch/X86/*.inc")
|
||||
list(FILTER _capstone_x86_c EXCLUDE REGEX "X86ATTInstPrinter\\.c$")
|
||||
list(FILTER _capstone_x86_inc EXCLUDE REGEX "reduce\\.inc$")
|
||||
target_sources(capstone PRIVATE ${_capstone_x86_c} ${_capstone_x86_h} ${_capstone_x86_inc})
|
||||
target_compile_definitions(capstone PRIVATE
|
||||
CAPSTONE_X86_ATT_DISABLE
|
||||
CAPSTONE_HAS_X86
|
||||
CAPSTONE_USE_SYS_DYN_MEM
|
||||
)
|
||||
)
|
||||
endif()
|
||||
target_include_directories(capstone PUBLIC capstone/include PRIVATE capstone)
|
||||
# Force all capstone sources to compile as 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.h
|
||||
)
|
||||
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
||||
if(XE_TARGET_X86_64)
|
||||
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)
|
||||
endif()
|
||||
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
|
||||
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(
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DSNAPPY_BUILD_TESTS=OFF
|
||||
-DSNAPPY_BUILD_BENCHMARKS=OFF
|
||||
-DSNAPPY_REQUIRE_AVX=ON
|
||||
${_snappy_avx_flag}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/snappy
|
||||
-B${CMAKE_CURRENT_SOURCE_DIR}/snappy
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
@@ -244,21 +282,32 @@ if(_zlibng_needs_configure)
|
||||
)
|
||||
endif()
|
||||
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")
|
||||
add_library(zlib-ng STATIC
|
||||
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
|
||||
${_zlibng_c}
|
||||
${_zlibng_x86}
|
||||
${_zlibng_generic}
|
||||
)
|
||||
target_compile_definitions(zlib-ng PRIVATE
|
||||
)
|
||||
target_compile_definitions(zlib-ng PRIVATE
|
||||
X86_FEATURES
|
||||
X86_HAVE_XSAVE_INTRIN
|
||||
X86_SSSE3
|
||||
X86_SSE42
|
||||
WITH_GZFILEOP
|
||||
)
|
||||
if(WIN32)
|
||||
)
|
||||
if(WIN32)
|
||||
target_compile_definitions(zlib-ng PRIVATE
|
||||
X86_SSE2
|
||||
X86_AVX2
|
||||
@@ -267,7 +316,7 @@ if(WIN32)
|
||||
X86_PCLMULQDQ_CRC
|
||||
X86_VPCLMULQDQ_CRC
|
||||
)
|
||||
else()
|
||||
else()
|
||||
# Remove AVX2/AVX512 files on non-Windows
|
||||
set(_zlibng_exclude_patterns
|
||||
"adler32_avx2" "adler32_avx512" "adler32_avx512_vnni"
|
||||
@@ -282,6 +331,7 @@ else()
|
||||
get_target_property(_zlibng_srcs zlib-ng SOURCES)
|
||||
set_target_properties(zlib-ng PROPERTIES SOURCES "")
|
||||
target_sources(zlib-ng PRIVATE ${_zlibng_c} ${_zlibng_x86} ${_zlibng_generic})
|
||||
endif()
|
||||
endif()
|
||||
target_include_directories(zlib-ng PUBLIC zlib-ng)
|
||||
|
||||
@@ -389,6 +439,66 @@ endif()
|
||||
# ==============================================================================
|
||||
# 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.
|
||||
function(ffmpeg_common target)
|
||||
@@ -515,7 +625,7 @@ add_library(libavutil STATIC
|
||||
FFmpeg/libavutil/timestamp.c
|
||||
)
|
||||
# x86 platform files
|
||||
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
|
||||
if(XE_TARGET_X86_64)
|
||||
target_sources(libavutil PRIVATE
|
||||
FFmpeg/libavutil/x86/cpu.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
|
||||
)
|
||||
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)
|
||||
|
||||
# --- libavcodec ---
|
||||
@@ -602,7 +730,7 @@ if(WIN32)
|
||||
target_sources(libavcodec PRIVATE FFmpeg/libavcodec/file_open.c)
|
||||
endif()
|
||||
# x86 platform files
|
||||
if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
|
||||
if(XE_TARGET_X86_64)
|
||||
target_sources(libavcodec PRIVATE
|
||||
FFmpeg/libavcodec/x86/constants.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
|
||||
)
|
||||
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
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/FFmpeg/libavcodec
|
||||
)
|
||||
|
||||
14
third_party/ffmpeg-xenia/config.h
vendored
14
third_party/ffmpeg-xenia/config.h
vendored
@@ -32,9 +32,13 @@
|
||||
#define ARCH_X86 0
|
||||
#define ARCH_X86_64 0
|
||||
|
||||
#if defined(__aarch64__)
|
||||
/* ARM64 (Android) */
|
||||
#if defined(__aarch64__) || defined(_M_ARM64)
|
||||
/* ARM64 */
|
||||
#if defined(_WIN32)
|
||||
#define SLIBSUF ".dll"
|
||||
#else
|
||||
#define SLIBSUF ".so"
|
||||
#endif
|
||||
#undef ARCH_AARCH64
|
||||
#define ARCH_AARCH64 1
|
||||
#define HAVE_ARMV8 1
|
||||
@@ -43,9 +47,15 @@
|
||||
#define HAVE_ARMV8_EXTERNAL 1
|
||||
#define HAVE_NEON_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_NEON_INLINE 1
|
||||
#define HAVE_VFP_INLINE 1
|
||||
#endif
|
||||
#define HAVE_INTRINSICS_NEON 1
|
||||
#define HAVE_AS_FUNC 0
|
||||
#define HAVE_AS_ARCH_DIRECTIVE 0
|
||||
|
||||
236
xenia-build.py
236
xenia-build.py
@@ -9,11 +9,12 @@ Run with --help or no arguments for possible commands.
|
||||
from datetime import datetime
|
||||
from multiprocessing import Pool
|
||||
from functools import partial
|
||||
from argparse import ArgumentParser
|
||||
from argparse import ArgumentParser, ArgumentTypeError
|
||||
from glob import glob
|
||||
from json import loads as jsonloads
|
||||
import os
|
||||
from re import findall as re_findall
|
||||
import platform
|
||||
from shutil import rmtree
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -444,10 +445,11 @@ def shell_call(command, throw_on_error=True, stdout_path=None, stderr_path=None,
|
||||
return result
|
||||
|
||||
|
||||
def generate_version_h():
|
||||
"""Generates a build/version.h file that contains current git info.
|
||||
def generate_version_h(build_dir="build"):
|
||||
"""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
|
||||
|
||||
if git_is_repository():
|
||||
@@ -686,8 +688,32 @@ def get_clang_format_binary():
|
||||
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,
|
||||
extra_args=None):
|
||||
extra_args=None, target_arch=None):
|
||||
"""Runs cmake configure on the project.
|
||||
|
||||
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').
|
||||
build_tests: If True, enables building test suites.
|
||||
extra_args: Additional arguments to pass to cmake (e.g. -D flags).
|
||||
target_arch: Target architecture override (e.g. 'arm64' for cross-compile).
|
||||
|
||||
Returns:
|
||||
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 = [
|
||||
"cmake",
|
||||
"-S", ".",
|
||||
"-B", "build",
|
||||
"-B", build_dir,
|
||||
"-G", "Ninja Multi-Config",
|
||||
]
|
||||
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_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:
|
||||
args += ["-DXENIA_BUILD_TESTS=ON"]
|
||||
if extra_args:
|
||||
@@ -722,7 +806,7 @@ def run_cmake_configure(build_type="Release", cc=None, build_tests=False,
|
||||
ret = subprocess.call(args)
|
||||
|
||||
if ret == 0:
|
||||
generate_version_h()
|
||||
generate_version_h(build_dir)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -739,8 +823,9 @@ def get_build_bin_path(args):
|
||||
"""
|
||||
config = args["config"].title()
|
||||
platform = "Windows" if sys.platform == "win32" else "Linux"
|
||||
# Multi-config: build/bin/<Platform>/<Config>
|
||||
return os.path.join(self_path, "build", "bin", platform, config)
|
||||
build_dir = get_build_dir(args.get("target_arch"))
|
||||
# Multi-config: <build_dir>/bin/<Platform>/<Config>
|
||||
return os.path.join(self_path, build_dir, "bin", platform, config)
|
||||
|
||||
|
||||
def create_clion_workspace():
|
||||
@@ -919,6 +1004,9 @@ class SetupCommand(Command):
|
||||
name="setup",
|
||||
help_short="Setup the build environment.",
|
||||
*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):
|
||||
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("\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)
|
||||
return ret
|
||||
|
||||
@@ -998,11 +1086,15 @@ class PremakeCommand(Command):
|
||||
self.parser.add_argument(
|
||||
"--build-tests", action="store_true", default=False,
|
||||
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):
|
||||
print("Running cmake configure...\n")
|
||||
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)
|
||||
|
||||
return ret
|
||||
@@ -1037,6 +1129,9 @@ class BaseBuildCommand(Command):
|
||||
"--cmake-define", dest="cmake_defines", action="append",
|
||||
default=[], metavar="KEY=VALUE",
|
||||
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):
|
||||
config = args["config"].title()
|
||||
@@ -1045,17 +1140,21 @@ class BaseBuildCommand(Command):
|
||||
|
||||
if not args["no_premake"]:
|
||||
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"],
|
||||
target_arch=args["target_arch"],
|
||||
extra_args=extra_args)
|
||||
if ret:
|
||||
return ret
|
||||
print("")
|
||||
|
||||
build_dir = get_build_dir(args.get("target_arch"))
|
||||
print("- building (%s):%s..." % (
|
||||
"all" if not len(args["target"]) else ", ".join(args["target"]),
|
||||
args["config"]))
|
||||
build_args = [
|
||||
"cmake",
|
||||
"--build", "build",
|
||||
"--build", build_dir,
|
||||
"--config", config,
|
||||
]
|
||||
if args["target"]:
|
||||
@@ -1511,9 +1610,10 @@ class CleanCommand(Command):
|
||||
def execute(self, args, pass_args, cwd):
|
||||
print("Cleaning build artifacts...")
|
||||
# Clean all build directories
|
||||
if os.path.isdir("build"):
|
||||
print("- cleaning build...")
|
||||
subprocess.call(["cmake", "--build", "build", "--target", "clean"])
|
||||
for build_dir in ["build", "build-arm64"]:
|
||||
if os.path.isdir(build_dir):
|
||||
print(f"- cleaning {build_dir}...")
|
||||
subprocess.call(["cmake", "--build", build_dir, "--target", "clean"])
|
||||
|
||||
# Also clean generated files
|
||||
clean_generated_files()
|
||||
@@ -1921,6 +2021,9 @@ class DevenvCommand(Command):
|
||||
name="devenv",
|
||||
help_short="Launches the development environment.",
|
||||
*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):
|
||||
if sys.platform == "win32":
|
||||
@@ -1934,27 +2037,106 @@ class DevenvCommand(Command):
|
||||
else:
|
||||
print("IDE not detected. CMakeLists.txt is in the project root.")
|
||||
|
||||
target_arch = args.get("target_arch", None)
|
||||
|
||||
print("\n- running cmake configure...")
|
||||
run_cmake_configure()
|
||||
run_cmake_configure(target_arch=target_arch)
|
||||
|
||||
print("\n- launching devenv...")
|
||||
if sys.platform == "win32":
|
||||
# Generate a VS .sln for IDE use (normal builds still use Ninja)
|
||||
vs_build_dir = os.path.join("build", "vs")
|
||||
subprocess.call([
|
||||
is_native_arm64 = platform.machine() in ("ARM64", "aarch64")
|
||||
# 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",
|
||||
"-S", ".",
|
||||
"-B", vs_build_dir,
|
||||
"-A", "x64",
|
||||
"-A", vs_arch,
|
||||
"-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...")
|
||||
shell_call(["devenv", slnx_path])
|
||||
if is_cross:
|
||||
# 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"):
|
||||
shell_call(["clion", "."])
|
||||
elif has_bin("clion.sh"):
|
||||
|
||||
Reference in New Issue
Block a user