Skip to content
codingncaffeine edited this page Jun 16, 2026 · 1 revision

macOS (Apple Silicon)

Emutastic for macOS is an Apple Silicon port of Emutastic for Linux — the same Avalonia/.NET 10 codebase, same look, same features, same data formats. The differences are platform plumbing, listed here; where this page doesn't note a difference, behaviour matches the Linux/Windows builds.

Apple Silicon only. There is no Intel build (cores ship as arm64 .dylibs). PlayStation 2 is unsupported — no arm64 libretro core exists.

Architecture

Concern Linux macOS
UI Avalonia (.NET 10), X11/Wayland Avalonia (.NET 10), Cocoa/Metal
2D / PlayStation 1 rendering OpenGL OpenGL
3D cores (N64, GameCube, 3DS, Dreamcast, PSP) OpenGL Vulkan via MoltenVK (Apple GL is too old)
Audio SDL3 SDL3
Input SDL3 gamepad (evdev) SDL3 gamepad (GameController framework / HIDAPI)
Recording ffmpeg (software) VideoToolbox (native, hardware)
Core loading dlopen (.so) dlopen (.dylib)
Game window separate --game-host process separate --game-host process

3D consoles render through Vulkan/MoltenVK

Apple's OpenGL is frozen at 4.1 core profile, which the modern 3D cores' GL renderers can't use (they come up black or fail to initialise). So Nintendo 64 (ParaLLEl-RDP), GameCube (Dolphin), Nintendo 3DS (Azahar), Dreamcast (Flycast) and PSP (PPSSPP) render through a Vulkan hardware-render backend layered on MoltenVK. PlayStation 1 (Beetle PSX HW) stays on OpenGL — its GL renderer works on Apple 4.1, while its Vulkan path renders no frames. Tune Internal Resolution, Texture Filter and Anti-Aliasing live from the in-game cog → Visuals (resolution applies on game restart). Developer details are in macOS porting notes below.

Controllers

Input uses SDL3 like the other platforms, but Apple's controller stack behaves differently from Linux evdev in ways that needed macOS-specific handling — this was the fiddliest part of the port:

  • A controller already connected when a game starts just works.
  • A controller connected after a game starts (or switched on mid-launch) is now picked up too. Apple's GameController framework only delivers input to the foreground app, and the game runs in a separate child process — so this needed an explicit "background events" opt-in. Earlier builds showed "controls dead until you restart the game"; that's fixed.
  • The library and a running game never fight over the controller — the library hands it to the game on launch and takes it back on exit (Linux can share one controller across processes; macOS can't).

If a controller ever doesn't respond, the in-game cog → Edit Controls shows your bindings.

Recording

Gameplay recording is native on macOS — it uses Apple's VideoToolbox (the same hardware encoder the system Screen Recording uses), so there is nothing to download (Windows fetches ffmpeg; Linux uses the system ffmpeg). Record from the in-game cog → Record (or F9). In Preferences → Media, the Encoder picker offers:

  • Auto — H.264 (.mp4), most compatible
  • HEVC — smaller files (.mp4)
  • ProRes — near-lossless, for editing (.mov)

Quality, Internal-resolution scale, High chroma and Audio bitrate behave as on other platforms (Quality drives the H.264/HEVC bitrate; Lossless or High chroma promote to ProRes). View Recordings (cog) opens the folder in Finder.

Paths

Follows the macOS Application Support convention instead of XDG:

Linux macOS
~/.config/Emutastic/config.json ~/Library/Application Support/Emutastic/config.json
~/.local/share/Emutastic/ ~/Library/Application Support/Emutastic/
Logs/ under the data dir ~/Library/Application Support/Emutastic/Logs/

Portable Mode works the same (portable.txt beside the executable inside the app bundle, or --portable).

Install

Apple Silicon only. Download Emutastic-<version>-osx-arm64.zip from the releases page, unzip, and move Emutastic.app to Applications. Builds aren't notarized yet, so on first launch right-click the app → Open (or run xattr -dr com.apple.quarantine /Applications/Emutastic.app).

macOS porting notes

Hard-won, macOS-specific gotchas, recorded in case they help anyone porting an SDL/Avalonia app or a libretro frontend to Apple Silicon.

Threading (SDL + Cocoa). SDL's macOS backend requires video init, the window, and event/gamepad pumping on the process main thread (off-main → "No available video device"). The game-host therefore presents on the main thread and runs the emulator core loop on a worker that reads the already-pumped input state.

3D cores → Vulkan/MoltenVK.

  • Link the Vulkan loader (libvulkan), not MoltenVK directly — cores resolve promoted KHR/EXT function aliases through the loader, and MoltenVK-direct returns NULL for them (Vulkan-Hpp dispatchers then crash). The loader reaches MoltenVK via an ICD manifest whose library_path needs a ./ prefix when bundled, or you get VK_ERROR_INCOMPATIBLE_DRIVER.
  • Blit the core's (often heavily upscaled) image down to window size on the GPU before the CPU readback, so readback cost stays constant regardless of internal resolution.
  • Some cores keep a static Vulkan context whose destructor runs vkDeviceWaitIdle at process exit and crashes — call _exit() after flushing saves to skip the native atexit teardown.

Controllers — Apple's GameController framework. Two non-obvious traps cost the most time:

  • Foreground-only delivery. GameController only delivers input to the active/foreground app. A game in a child process that isn't "active" when a controller connects gets an opened-but-silent gamepad. Fix: SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS=1. Re-opening or re-initialising the gamepad in place does not help — only this hint (or relaunching, so the new process is foreground when the pad is enumerated, which is why "restart the game" was the accidental workaround).
  • HIDAPI enumeration can hang the UI. SDL's HIDAPI driver enumerates HID devices inside SDL_PumpEvents, and IOKit's plug-in creation for the GameController framework's synthetic controller can block for seconds (IOCreatePlugInInterfaceForServiceAppleSyntheticGameController) — a beachball if you pump SDL on your UI thread. For a foreground GUI that only needs the controller while active, set SDL_HINT_JOYSTICK_HIDAPI=0 so SDL uses the notification-driven GameController/IOKit driver (no blocking enumeration); a child process that needs raw, non-foreground input keeps HIDAPI. When something beachballs, sample <pid> prints the blocked stack and points straight at the cause.
  • Cross-process sharing. Linux evdev lets two processes read one controller concurrently; macOS doesn't — so the library process releases the controller while a game runs and re-acquires it on exit.

Recording → VideoToolbox. A small AVFoundation shim (AVAssetWriter + the hardware H.264/HEVC/ProRes encoder) replaces the ffmpeg path: no external binary to download or bundle, hardware-accelerated, native MP4/MOV.

Opening files/folders/URLs. macOS uses open (and open -R to reveal a file in Finder), not xdg-open.

Building from source

Requires the .NET 10 SDK, a C/Objective-C toolchain for the small vendored native shims, and the Vulkan loader + MoltenVK (Homebrew: brew install vulkan-loader molten-vk). Repository: Emutastic-for-Mac.

Clone this wiki locally