Skip to content

Apple Silicon (macOS arm64) dynarec port#632

Open
AurisDSP wants to merge 2 commits into
libretro:developfrom
AurisDSP:apple-silicon-dynarec
Open

Apple Silicon (macOS arm64) dynarec port#632
AurisDSP wants to merge 2 commits into
libretro:developfrom
AurisDSP:apple-silicon-dynarec

Conversation

@AurisDSP

@AurisDSP AurisDSP commented May 6, 2026

Copy link
Copy Markdown

Summary

Brings the aarch64 dynarec online on macOS Apple Silicon (M1/M2/M3/M4). Resolves #561.

The host code ports cleanly with a handful of platform shims:

  • Build (Makefile): enable WITH_DYNAREC=aarch64 on osx for native arm64; keep -DNO_ASM only when no dynarec backend is in use.

  • Assembly (linkage_arm64.S): Apple's clang assembler treats ; as a comment delimiter, so the GAS-style cpp macros that emit multiple directives on one line don't work. Switched GLOBAL_FUNCTION / LOCAL_FUNCTION / GLOBAL_VARIABLE to assembler-side .macro definitions on Apple, with an _-prefixed Mach-O symbol and .private_extern. Added an ADRP_LO12 macro that selects between :lo12: (GAS) and @PAGE/@PAGEOFF (Apple) syntax. Internal b.ne new_dyna_stop was rerouted through a local L-label so Apple's assembler doesn't reject the conditional branch's external target.

  • JIT memory (new_dynarec.c): macOS Hardened Runtime forbids RWX in a single mapping, so on Apple we mmap the cache with MAP_JIT and toggle protection per-thread via pthread_jit_write_protect_np(). To handle set_jump_target() being called from inside new_recompile_block (where the page is already in write mode), the BEGIN/END pair nests via a per-thread depth counter (`_Thread_local`); END clamps to zero so cache_flush() called from invalidation paths still lands the page in execute mode.

  • W^X helpers (new_dynarec.h): `M64P_JIT_WRITE_BEGIN` / `M64P_JIT_WRITE_END` macros wrap `pthread_jit_write_protect_np` with the nesting counter on Apple, no-ops elsewhere. The counter is `_Thread_local` since the underlying syscall is per-thread.

  • Cache flush (assem_arm64.c): on Apple, replaced the manual `dc civac` / `ic ivau` loop with `sys_icache_invalidate()` and a paired `M64P_JIT_WRITE_END`. `arch_init()` and `set_jump_target()` now wrap their writes with BEGIN/END.

  • Portability (custom/dependencies):

    • `pngpriv.h`'s `TARGET_OS_MAC` branch pulls in `<fp.h>` (a Mac OS 9 header), so it's been excluded on modern macOS.
    • `zutil.h`'s `MACOS` branch redefines `fdopen`, which collides with Apple's stdio.h `__memcpy_chk`-style macros; skip the redefinition on `APPLE`.

The second commit is hardening that fell out of an audit while looking at the Apple AudioConverter path — same fixes apply to the non-Apple sinc-resampler branch (NULL checks after malloc, zero-frequency guard, oversized-input truncation, drain-loop break on `audio_batch_cb` returning 0).

What works

  • Mario Kart 64 boots and plays with audio + video on M-series hardware (`cached_interpreter` and `dynamic_recompiler` both work; `angrylion` + HLE RSP avoids a paraLLEl-RDP / MoltenVK threading bug we'll file separately).
  • Apple Silicon dylib loads cleanly with both video drivers (`glcore` and `vulkan` via MoltenVK).
  • All Apple-specific code is guarded behind `APPLE` so Linux / Windows / Android paths are unchanged.

What doesn't work yet (out of scope)

Some homebrew ROMs (e.g. BikeRace '98, Wet Dreams POM '99) hit a SIGSEGV inside generated code with `LR=1`-shaped corruption. The crash isn't macOS-specific — it reproduces on this branch's tree on Linux with the same recompiler — so it's a pre-existing aarch64 codegen edge case rather than something the macOS port introduced. Worth filing separately.

Tested on

  • M4 MacBook Air, macOS 26.x, RetroArch 1.22.2

Test plan

  • Cached interpreter mode runs Mario Kart 64 end-to-end
  • Dynamic recompiler mode runs Mario Kart 64 end-to-end (boot, audio, video, input)
  • Build with `make platform=osx` succeeds clean
  • Linux paths unchanged (all changes are `APPLE`-guarded)

AurisDSP and others added 2 commits May 5, 2026 20:37
Brings the aarch64 dynarec online on macOS Apple Silicon. The host code
ports cleanly with a handful of platform shims:

* Build (Makefile): enable WITH_DYNAREC=aarch64 on osx for native arm64;
  keep -DNO_ASM only when no dynarec backend is in use.

* Assembly (linkage_arm64.S): Apple's clang assembler treats ';' as a
  comment delimiter, so the GAS-style cpp macros that emit multiple
  directives on one line don't work. Switched GLOBAL_FUNCTION /
  LOCAL_FUNCTION / GLOBAL_VARIABLE to assembler-side .macro definitions
  on Apple, with an _-prefixed Mach-O symbol and .private_extern. Added
  an ADRP_LO12 macro that selects between :lo12: (GAS) and @PAGE/@PAGEOFF
  (Apple) syntax. Internal references to jump_vaddr changed to use the
  underscored Mach-O symbol; b.ne/b.eq new_dyna_stop now goes through a
  local L-label so Apple's assembler doesn't reject the conditional
  branch's external target.

* JIT memory (new_dynarec.c): macOS Hardened Runtime forbids RWX in a
  single mapping, so on Apple we mmap the cache with MAP_JIT and toggle
  protection per-thread via pthread_jit_write_protect_np(). To handle
  set_jump_target() being called from inside new_recompile_block (where
  the page is already in write mode), the BEGIN/END pair nests via a
  per-thread depth counter; END clamps to zero so cache_flush() called
  from invalidation paths still lands the page in execute mode.

* W^X helpers (new_dynarec.h): M64P_JIT_WRITE_BEGIN / M64P_JIT_WRITE_END
  macros wrap pthread_jit_write_protect_np with the nesting counter on
  Apple, no-ops elsewhere.

* Cache flush (assem_arm64.c): on Apple, replaced the manual
  dc civac / ic ivau loop with sys_icache_invalidate() and a paired
  M64P_JIT_WRITE_END so freshly-emitted code is visible and executable
  before it's invoked. arch_init() and set_jump_target() now wrap their
  writes with M64P_JIT_WRITE_BEGIN / M64P_JIT_WRITE_END.

* Portability (custom/dependencies): pngpriv.h's TARGET_OS_MAC branch
  pulls in <fp.h> (a Mac OS 9 header), so it's been excluded on modern
  macOS. zutil.h's MACOS branch redefines fdopen, which collides with
  Apple's stdio.h __memcpy_chk-style macros; skip the redefinition on
  __APPLE__.

Mario Kart 64 boots and runs with audio + video on M-series hardware
(cached_interpreter and dynamic_recompiler both work; angrylion + HLE
RSP avoids paraLLEl-RDP's macOS mutex thread bug).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These don't address any specific reported bug, just hardening that
fell out of an audit while looking at the Apple AudioConverter path.
Same fixes apply to the non-Apple sinc-resampler branch.

* malloc() return values are checked at init time; on failure the
  buffers are freed and reset to NULL so subsequent push paths can
  bail instead of dereferencing garbage.

* aiDacrateChanged() returns early when the N64 reports a DAC rate
  of zero. The non-Apple resampler branch additionally guards the
  44100.0 / GameFreq division with the same check, since GameFreq
  could still be left at zero by a concurrent reset.

* aiLenChanged() truncates an oversized incoming chunk to fit
  MAX_AUDIO_FRAMES * 2 stereo frames before appending to the
  accumulation buffer. The previous clamp only handled the buffer
  fullness side, so a single push larger than the buffer capacity
  could memcpy past the end.

* The audio_batch_cb drain loops now break on a zero return rather
  than spinning forever. RetroArch returns 0 when the host can't
  accept samples (paused, fast-forward window full); the previous
  code would hang the emulation thread.

* Dropped a stale '#if 0' debug printf and fixed a 'vlaues' typo
  in a comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@m4xw

m4xw commented May 6, 2026

Copy link
Copy Markdown
Collaborator

This will take some time to review

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.

no dynarec on macOS 11?

2 participants