[ARM64] Initial commit for arm64 backend

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

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

View File

@@ -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"],
build_tests=args["build_tests"],
extra_args=extra_args)
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"):