Skip to content

new_dynarec: native Apple Silicon (darwin-arm64) support#1184

Open
jeremyw wants to merge 1 commit into
mupen64plus:masterfrom
jeremyw:darwin-arm64-dynarec
Open

new_dynarec: native Apple Silicon (darwin-arm64) support#1184
jeremyw wants to merge 1 commit into
mupen64plus:masterfrom
jeremyw:darwin-arm64-dynarec

Conversation

@jeremyw

@jeremyw jeremyw commented Jul 19, 2026

Copy link
Copy Markdown

The problem: on Apple Silicon Macs, mupen64plus falls back to the cached interpreter — the arm64 dynarec has been hard-disabled on macOS (NO_ASM = 1) since native arm64 support landed in #910. So the machines that could benefit most from the recompiler are the ones that don't get it. The dynarec itself is fine on arm64 (it works on Linux); the blockers were all macOS-specific.

Prior art: #910 got the core building natively on Apple Silicon but had to disable the dynarec to do it. #831 (@MaddTheSane, 2021) tried to close the gap using gas-preprocessor to translate the ELF assembly syntax, but hit "continuous SIGILLs" at runtime and stalled. This PR picks that effort back up. It turns out no extra tooling is needed — clang's integrated assembler handles linkage_arm64.S fine once the syntax differences are handled with an #ifdef — and the SIGILLs had a specific, fixable cause (see below).

Three separate problems needed solving:

Assembler syntax. I added an #ifdef __APPLE__ variant of the symbol macros in linkage_arm64.S: leading underscores on C symbols, .private_extern in place of .hidden/.type/.size, and @PAGE/@PAGEOFF instead of :lo12:. One gotcha worth writing down: the Darwin arm64 assembler treats ; as a comment character (the statement separator is %%), so multi-statement macros silently drop everything after the first statement. I suspect that's why earlier attempts ended up with undefined symbols. I cross-assembled for aarch64-linux to confirm the ELF output is unchanged.

W^X. Apple Silicon refuses RWX mappings and won't execute unsigned shared memory, so neither FIXED_CACHE_ADDR nor DOUBLE_CACHE_ADDR can work — I tried both. Apple builds now use DYNAMIC_CACHE_ADDR with MAP_JIT, plus small reentrant jit_write_begin/end helpers that toggle pthread_jit_write_protect_np() around the six C functions all cache writes go through (new_recompile_block, dynamic_linker{,_ds}, invalidate_block, invalidate_all_pages, arch_init). The cache stays executable by default, so the asm stubs and the generated code itself didn't need any changes.

Cache flush. cache_flush() reads ctr_el0, which traps from userspace on macOS. I believe this is the actual source of the "continuous SIGILLs" reported in #831, not the 16KB page size. Apple builds now call sys_icache_invalidate() instead.

Tested on an M-series Mac (macOS 26.5) with a plain make all: two commercial ROMs run under the stock 2.6.0 frontend and plugins with --emumode 2, and sampling the process shows it executing out of the MAP_JIT region, so it really is running recompiled code. The minimum macOS version for the arm64 build goes from 10.16 to 11.0, which pthread_jit_write_protect_np requires anyway.

Everything is behind __APPLE__/__aarch64__ guards, so other platforms build exactly what they built before. Happy to adjust anything if a different approach is preferred.

@jeremyw
jeremyw force-pushed the darwin-arm64-dynarec branch from 2caaf0d to 312e6b4 Compare July 19, 2026 01:33
Enable the arm64 new_dynarec on macOS, previously hard-disabled with
NO_ASM=1. Three independent blockers, three fixes:

1. linkage_arm64.S only assembled with ELF toolchains. Add a Mach-O
   variant of the symbol macros: leading-underscore C symbols,
   .private_extern instead of .hidden (.type/.size dropped, ELF-only),
   sym@PAGE/sym@PAGEOFF instead of :lo12: adrp relocations, and '%%'
   statement separators because the Darwin arm64 assembler treats ';'
   as a comment. Dot-prefixed local labels (ELF-only) become numbered
   locals. ELF output is unchanged (verified by cross-assembling for
   aarch64-linux before/after).

2. Apple Silicon enforces W^X. The code cache is now allocated with
   MAP_JIT at a dynamic address (static memory cannot be made
   executable on this platform), and every code path that writes to
   the cache is bracketed with pthread_jit_write_protect_np() via
   reentrant jit_write_begin/end helpers. The bracket points are the
   five C entry points all cache writes funnel through
   (new_recompile_block, dynamic_linker{,_ds}, invalidate_block,
   invalidate_all_pages) plus the arch_init trampoline setup; the
   assembly stubs need no changes because exec mode is the default.

3. cache_flush() reads ctr_el0, which traps (SIGILL) from EL0 on
   Apple Silicon. Use the system-provided sys_icache_invalidate()
   instead. This was the cause of the "continuous SIGILLs" reported
   when compiling the dynarec on M-series machines.

Tested on an M-series Mac (macOS 26.5): two commercial ROMs run on
the dynarec under the stock mupen64plus 2.6.0 frontend/plugins, with
generated code confirmed executing from the MAP_JIT region.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeremyw
jeremyw force-pushed the darwin-arm64-dynarec branch from 312e6b4 to 3af70b7 Compare July 19, 2026 01:33
@jeremyw
jeremyw marked this pull request as ready for review July 19, 2026 17:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant