Add native Windows ARM64 (AArch64) support

Enables building and running Cemu natively on Windows on ARM (e.g.
Snapdragon devices) with clang-cl. The existing AArch64 recompiler
backend is used, so no x64 emulation is involved.

Build system (CMakeLists.txt, dependencies/ih264d, src/Cafe):
- Auto-select the arm64-windows-static vcpkg triplet and detect the
  AArch64 architecture even when the emulated host shell misreports it.
- Disable IPO for clang-cl (LTO would force lld-link, which cannot read
  vcpkg's MSVC /GL objects) and link clang_rt.builtins for the
  compiler-rt calls (e.g. __udivti3) that lld-link does not add.
- Set the static CRT on the xbyak_aarch64 target.
- Locate pkgconf under the actual vcpkg host triplet instead of a
  hardcoded x64-windows path.
- Build the bundled ih264d decoder as portable C on ARM64/Windows; its
  NEON .s files use ELF-only :got: relocations that do not assemble for
  COFF targets.

Source (precompiled.h, ExceptionHandler_win32, Debugger, LatteTextureCache):
- Guard x86-only intrinsics/paths behind ARCH_X86_64. clang-cl defines
  _MSC_VER but not __GNUC__, so several `#if defined(_MSC_VER)` branches
  wrongly selected x86 intrinsics on ARM64. Adds AArch64 register
  dumping to the crash handler.

Manifest: use processorArchitecture="*" for Common-Controls so it
resolves on ARM64.

See BUILD.md for the build steps (Ninja + clang-cl + -DCMAKE_LINKER_TYPE=MSVC).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
rhemf
2026-07-16 14:05:33 +01:00
parent acb105bd58
commit 2dcaac317c
10 changed files with 134 additions and 17 deletions

View File

@@ -42,6 +42,36 @@ Instructions for Visual Studio 2022:
Any other IDE should also work as long as it has CMake and MSVC support. CLion and Visual Studio Code have been confirmed to work.
### Windows on ARM64 (Snapdragon)
Native ARM64 builds are supported and use **clang-cl**. The recompiler uses the
AArch64 backend (`xbyak_aarch64`), so it runs natively rather than under x64
emulation.
Prerequisites (Visual Studio 2022 or Build Tools 2022 with these components):
- MSVC v143 - ARM64/ARM64EC build tools
- C++ Clang Compiler for Windows and *MSBuild support for LLVM (clang-cl) toolset*
- C++ CMake tools for Windows (CMake 3.29 or newer)
- Windows 11 SDK
The Visual Studio CMake generator cannot assemble the codebase, so build from the
command line with Ninja. Open an **"ARM64 Native Tools Command Prompt for VS 2022"**
(or run `vcvarsarm64.bat`), then:
```
git clone --recursive https://github.com/cemu-project/Cemu
cd Cemu
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=release ^
-DCMAKE_C_COMPILER=clang-cl -DCMAKE_CXX_COMPILER=clang-cl ^
-DCMAKE_LINKER_TYPE=MSVC
cmake --build build
```
`-DCMAKE_LINKER_TYPE=MSVC` makes clang link with the MSVC `link.exe` (required so
that vcpkg's static libraries link correctly). The resulting binary is
`bin/Cemu_release.exe`. Note: on ARM64 the bundled H.264 decoder (ih264d) is built
as portable C, and libusb is used via the standard vcpkg package.
## Linux
To compile Cemu, a recent enough compiler and STL with C++20 support is required! Clang-15 or higher is what we recommend.

View File

@@ -51,7 +51,17 @@ if (ENABLE_VCPKG)
# CONFIG option
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
if (WIN32)
set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "")
# Auto-select the vcpkg triplet based on the target architecture.
# Override on the command line with -DVCPKG_TARGET_TRIPLET=...
if (NOT DEFINED VCPKG_TARGET_TRIPLET)
if (CMAKE_GENERATOR_PLATFORM MATCHES "(ARM64|arm64)" OR
CMAKE_SYSTEM_PROCESSOR MATCHES "(ARM64|arm64|aarch64)" OR
"$ENV{VSCMD_ARG_TGT_ARCH}" STREQUAL "arm64")
set(VCPKG_TARGET_TRIPLET "arm64-windows-static" CACHE STRING "")
else()
set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "")
endif()
endif()
endif()
endif()
@@ -72,9 +82,13 @@ add_definitions(-DEMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH})
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# enable link time optimization for release builds
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
# enable link time optimization for release builds.
# Skip it for clang-cl on Windows: IPO forces the LLVM linker (lld-link), which
# cannot consume the MSVC /GL objects inside vcpkg static libraries.
if(NOT (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang"))
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
endif()
if (MSVC)
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT CemuBin)
@@ -256,11 +270,34 @@ add_subdirectory("dependencies/ih264d" EXCLUDE_FROM_ALL)
if (CMAKE_OSX_ARCHITECTURES)
set(CEMU_ARCHITECTURE ${CMAKE_OSX_ARCHITECTURES})
elseif (WIN32 AND (CMAKE_GENERATOR_PLATFORM MATCHES "(ARM64|arm64)" OR "$ENV{VSCMD_ARG_TGT_ARCH}" STREQUAL "arm64"))
# Windows ARM64: the host toolchain / emulated shell can misreport the
# processor, so trust the requested target platform instead.
set(CEMU_ARCHITECTURE "aarch64")
else()
set(CEMU_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR})
endif()
if(CEMU_ARCHITECTURE MATCHES "(aarch64)|(AARCH64)|(arm64)|(ARM64)")
add_subdirectory("dependencies/xbyak_aarch64" EXCLUDE_FROM_ALL)
if(MSVC)
# Match the static CRT used by the rest of the project (vcpkg static triplet)
set_property(TARGET xbyak_aarch64 PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
if(MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
# clang-cl emits calls to compiler-rt builtins (e.g. __udivti3 for 128-bit
# integer division) that lld-link does not link automatically. Link the
# clang_rt.builtins library that ships next to the compiler.
get_filename_component(_llvm_bin "${CMAKE_CXX_COMPILER}" DIRECTORY)
get_filename_component(_llvm_root "${_llvm_bin}" DIRECTORY)
file(GLOB _clang_rt_builtins "${_llvm_root}/lib/clang/*/lib/windows/clang_rt.builtins-aarch64.lib")
if(_clang_rt_builtins)
list(GET _clang_rt_builtins 0 _clang_rt_builtins)
link_libraries("${_clang_rt_builtins}")
message(STATUS "Linking compiler-rt builtins: ${_clang_rt_builtins}")
else()
message(WARNING "clang_rt.builtins-aarch64.lib not found; link may fail with undefined __udivti3")
endif()
endif()
endif()
find_package(ZArchive QUIET)

View File

@@ -123,7 +123,12 @@ else()
set(IH264D_ARCHITECTURE ${CMAKE_SYSTEM_PROCESSOR})
endif()
if (IH264D_ARCHITECTURE STREQUAL "x86_64" OR IH264D_ARCHITECTURE STREQUAL "amd64" OR IH264D_ARCHITECTURE STREQUAL "AMD64")
# Normalize the architecture string so matching is case-insensitive.
# MSVC/CMake reports "ARM64"/"AMD64" (uppercase) whereas the checks below use
# the lowercase names emitted by GCC/Clang toolchains.
string(TOLOWER "${IH264D_ARCHITECTURE}" IH264D_ARCHITECTURE)
if (IH264D_ARCHITECTURE STREQUAL "x86_64" OR IH264D_ARCHITECTURE STREQUAL "amd64")
set(LIBAVCDEC_X86_INCLUDES "common/x86" "decoder/x86")
include_directories("common/" "decoder/" ${LIBAVCDEC_X86_INCLUDES})
target_sources(ih264d PRIVATE
@@ -147,6 +152,18 @@ target_sources(ih264d PRIVATE
"decoder/x86/ih264d_function_selector_ssse3.c"
)
elseif(IH264D_ARCHITECTURE STREQUAL "aarch64" OR IH264D_ARCHITECTURE STREQUAL "arm64")
if(MSVC)
# Windows on ARM64: build the generic C decoder. The NEON .s files are GAS syntax
# and use ELF-only ":got:" relocations that cannot be assembled for COFF targets,
# so we fall back to portable C (only affects H.264/FMV decode performance).
set(LIBAVCDEC_ARM_INCLUDES "common/armv8" "decoder/arm")
include_directories("common/" "decoder/" ${LIBAVCDEC_ARM_INCLUDES})
target_sources(ih264d PRIVATE
"common/armv8/ih264_platform_macros.h"
"decoder/arm/ih264d_function_selector.c"
)
target_compile_definitions(ih264d PRIVATE ARMV8 DISABLE_NEON DEFAULT_ARCH=D_ARCH_ARM_NONEON)
else()
enable_language( C CXX ASM )
set(LIBAVCDEC_ARM_INCLUDES "common/armv8" "decoder/arm")
include_directories("common/" "decoder/" ${LIBAVCDEC_ARM_INCLUDES})
@@ -189,6 +206,7 @@ endif()
if(APPLE)
target_sources(ih264d PRIVATE "common/armv8/macos_arm_symbol_aliases.s")
endif()
endif()
else()
message(FATAL_ERROR "ih264d unknown architecture: ${IH264D_ARCHITECTURE}")
endif()

View File

@@ -61,7 +61,7 @@ void ih264d_init_function_ptr(dec_struct_t *ps_codec)
ih264d_init_function_ptr_generic(ps_codec);
switch(e_proc_arch)
{
#if defined(ARMV8)
#if defined(ARMV8) && !defined(DISABLE_NEON)
case ARCH_ARMV8_GENERIC:
default:
ih264d_init_function_ptr_av8(ps_codec);

View File

@@ -2,7 +2,7 @@
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="amd64" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"></assemblyIdentity>
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"><security><requestedPrivileges>

View File

@@ -668,7 +668,12 @@ endif()
if (ENABLE_LIBUSB)
if (ENABLE_VCPKG)
if(WIN32)
set(PKG_CONFIG_EXECUTABLE "${VCPKG_INSTALLED_DIR}/x64-windows/tools/pkgconf/pkgconf.exe")
# pkgconf is a host tool; locate it under whichever host triplet vcpkg
# used (x64-windows on Intel hosts, arm64-windows on ARM64 hosts).
file(GLOB _cemu_pkgconf "${VCPKG_INSTALLED_DIR}/*/tools/pkgconf/pkgconf.exe")
if(_cemu_pkgconf)
list(GET _cemu_pkgconf 0 PKG_CONFIG_EXECUTABLE)
endif()
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(libusb REQUIRED IMPORTED_TARGET libusb-1.0)

View File

@@ -199,7 +199,7 @@ void debugger_updateMemoryBreakpoint(DebuggerBreakpoint* bp)
{
std::vector<std::thread::native_handle_type> schedulerThreadHandles = coreinit::OSGetSchedulerThreads();
#if BOOST_OS_WINDOWS
#if BOOST_OS_WINDOWS && defined(ARCH_X86_64)
s_debuggerState.activeMemoryBreakpoint = bp;
for (auto& hThreadNH : schedulerThreadHandles)
{

View File

@@ -146,7 +146,7 @@ uint32 LatteTexture_CalculateTextureDataHash(LatteTexture* hostTexture)
bool isCompressedFormat = hostTexture->IsCompressedFormat();
if( isCompressedFormat == false )
{
#if BOOST_OS_WINDOWS
#if BOOST_OS_WINDOWS && defined(ARCH_X86_64)
if (g_CPUFeatures.x86.avx2)
{
__m256i h256 = { 0 };

View File

@@ -209,6 +209,7 @@ void createCrashlog(EXCEPTION_POINTERS* e, PCONTEXT context)
// register info
sprintf(dumpLine, "\n");
cemuLog_writePlainToLog(dumpLine);
#if defined(ARCH_X86_64)
sprintf(dumpLine, "RAX=%016I64x RBX=%016I64x RCX=%016I64x RDX=%016I64x\n", context->Rax, context->Rbx, context->Rcx, context->Rdx);
cemuLog_writePlainToLog(dumpLine);
sprintf(dumpLine, "RSP=%016I64x RBP=%016I64x RDI=%016I64x RSI=%016I64x\n", context->Rsp, context->Rbp, context->Rdi, context->Rsi);
@@ -217,6 +218,17 @@ void createCrashlog(EXCEPTION_POINTERS* e, PCONTEXT context)
cemuLog_writePlainToLog(dumpLine);
sprintf(dumpLine, "R12=%016I64x R13=%016I64x R14=%016I64x R15=%016I64x\n", context->R12, context->R13, context->R14, context->R15);
cemuLog_writePlainToLog(dumpLine);
#else // AArch64
for (int i = 0; i < 28; i += 4)
{
sprintf(dumpLine, "X%-2d=%016I64x X%-2d=%016I64x X%-2d=%016I64x X%-2d=%016I64x\n",
i, context->X[i], i + 1, context->X[i + 1], i + 2, context->X[i + 2], i + 3, context->X[i + 3]);
cemuLog_writePlainToLog(dumpLine);
}
sprintf(dumpLine, "X28=%016I64x FP =%016I64x LR =%016I64x SP =%016I64x PC =%016I64x\n",
context->X[28], context->Fp, context->Lr, context->Sp, context->Pc);
cemuLog_writePlainToLog(dumpLine);
#endif
CrashLog_SetOutputChannels(false, true);
ExceptionHandler_LogGeneralInfo();
@@ -264,11 +276,15 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
if (r != EXCEPTION_CONTINUE_SEARCH)
return r;
#if defined(ARCH_X86_64)
if (GetBits(pExceptionInfo->ContextRecord->Dr6, 0, 1) || GetBits(pExceptionInfo->ContextRecord->Dr6, 1, 1))
debugger_handleSingleStepException(pExceptionInfo->ContextRecord->Dr6);
else if (GetBits(pExceptionInfo->ContextRecord->Dr6, 2, 1) || GetBits(pExceptionInfo->ContextRecord->Dr6, 3, 1))
g_gdbstub->HandleAccessException(pExceptionInfo->ContextRecord->Dr6);
return EXCEPTION_CONTINUE_EXECUTION;
#else // AArch64: hardware debug registers (Dr6) not available; debugger single-step unsupported for now
return EXCEPTION_CONTINUE_SEARCH;
#endif
}
return EXCEPTION_CONTINUE_SEARCH;
}

View File

@@ -268,6 +268,15 @@ typedef union _LARGE_INTEGER {
inline T& operator^= (T& a, T b) { return reinterpret_cast<T&>( reinterpret_cast<std::underlying_type<T>::type&>(a) ^= static_cast<std::underlying_type<T>::type>(b) ); }
#endif
#if defined(_MSC_VER) && defined(_M_ARM64)
// _umul128 is an x86-64 intrinsic and is not provided by <intrin.h> on AArch64
inline uint64 _umul128(uint64 multiplier, uint64 multiplicand, uint64 *highProduct) {
unsigned __int128 x = (unsigned __int128)multiplier * (unsigned __int128)multiplicand;
*highProduct = (uint64)(x >> 64);
return (uint64)(x & 0xFFFFFFFFFFFFFFFF);
}
#endif
template<typename T>
inline T GetBits(T value, uint32 index, uint32 numBits)
{
@@ -343,10 +352,10 @@ inline uint64 _udiv128(uint64 highDividend, uint64 lowDividend, uint64 divisor,
FORCE_INLINE int BSF(uint32 v) // returns index of first bit set, counting from LSB. If v is 0 then result is undefined
{
#if defined(_MSC_VER)
#if defined(__GNUC__) || defined(__clang__)
return __builtin_ctz(v); // clang-cl also defines _MSC_VER, so check this first
#elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86))
return _tzcnt_u32(v); // TZCNT requires BMI1. But if not supported it will execute as BSF
#elif defined(__GNUC__) || defined(__clang__)
return __builtin_ctz(v);
#else
return std::countr_zero(v);
#endif
@@ -610,12 +619,14 @@ inline uint32 GetTitleIdLow(uint64 titleId)
return titleId & 0xFFFFFFFF;
}
#if defined(__GNUC__)
#define memcpy_dwords(__dest, __src, __numDwords) memcpy((__dest), (__src), (__numDwords) * sizeof(uint32))
#define memcpy_qwords(__dest, __src, __numQwords) memcpy((__dest), (__src), (__numQwords) * sizeof(uint64))
#else
// __movsd/__movsq are x86-only MSVC intrinsics; use memcpy everywhere else
// (clang-cl does not define __GNUC__, and AArch64 has no such intrinsics)
#if defined(_MSC_VER) && !defined(__clang__) && defined(ARCH_X86_64)
#define memcpy_dwords(__dest, __src, __numDwords) __movsd((unsigned long*)(__dest), (const unsigned long*)(__src), __numDwords)
#define memcpy_qwords(__dest, __src, __numQwords) __movsq((unsigned long long*)(__dest), (const unsigned long long*)(__src), __numQwords)
#else
#define memcpy_dwords(__dest, __src, __numDwords) memcpy((__dest), (__src), (__numDwords) * sizeof(uint32))
#define memcpy_qwords(__dest, __src, __numQwords) memcpy((__dest), (__src), (__numQwords) * sizeof(uint64))
#endif
// PPC context and memory functions