From 4dc0f227587b5582a82a518679b74949596dde05 Mon Sep 17 00:00:00 2001 From: DarthM Date: Thu, 25 Jun 2026 23:45:23 +0800 Subject: [PATCH 01/10] waterboxhost: add macOS (x86_64) support Port the waterbox host to build and run on macOS x86_64 (natively on Intel, under Rosetta 2 on Apple silicon). All `cargo test --target x86_64-apple-darwin` pass and melonDS boots and runs. - memory_block/pal.rs: use an unlinked temp file (mkstemp) instead of memfd_create/shm for the block handle. macOS forbids mprotect-ing a MAP_SHARED shm region executable (EACCES), but a regular file maps executable like a dylib, so the existing mirror/W^X model works unchanged. Also handle the macOS errno location (__error) and that MAP_FIXED_NOREPLACE is Linux-only. - memory_block/tripguard.rs: also hook SIGBUS (macOS raises SIGBUS, not SIGSEGV, for protection violations on mapped memory) and read the faulting access type from the macOS mcontext (__es.__err); drop the Linux-only sa_restorer field. - context/interop_macos.s (+ generated interop_macos.bin), context/mod.rs: macOS can't repoint the %gs base (no arch_prctl; rdgsbase/wrgsbase #UD under Rosetta) and %gs is the OS TSD. The guest only reads its Context ptr from gs:0x18; the macOS trampoline leaves gs:0x08/0x10 (errno/mig_reply) alone and swaps only gs:0x18 against a free TSD slot at each host/guest boundary. - host.rs, threading.rs: wrap std::intrinsics::breakpoint in unsafe (newer nightly). - build-release-macos.sh: cross-build the dylib (pinned nightly, low deployment target). --- waterbox/waterboxhost/build-release-macos.sh | 40 ++++ waterbox/waterboxhost/src/context/Makefile | 9 +- .../src/context/interop_macos.bin | Bin 0 -> 2088 bytes .../waterboxhost/src/context/interop_macos.s | 185 ++++++++++++++++++ waterbox/waterboxhost/src/context/mod.rs | 19 +- waterbox/waterboxhost/src/host.rs | 2 +- waterbox/waterboxhost/src/memory_block/pal.rs | 48 ++++- .../src/memory_block/tripguard.rs | 28 ++- waterbox/waterboxhost/src/threading.rs | 2 +- 9 files changed, 317 insertions(+), 16 deletions(-) create mode 100755 waterbox/waterboxhost/build-release-macos.sh create mode 100644 waterbox/waterboxhost/src/context/interop_macos.bin create mode 100644 waterbox/waterboxhost/src/context/interop_macos.s diff --git a/waterbox/waterboxhost/build-release-macos.sh b/waterbox/waterboxhost/build-release-macos.sh new file mode 100755 index 00000000000..907e5eb14e4 --- /dev/null +++ b/waterbox/waterboxhost/build-release-macos.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Build libwaterboxhost for macOS as an x86_64 Mach-O dylib (runs under Rosetta 2 on +# Apple Silicon). Cross-compiles from any host; only needs rustup + the x86_64-apple-darwin +# std for a compatible nightly. See build-release.sh for the Linux/.so equivalent. +# +# NOTE: this builds the host. Guest-entry on macOS is not yet fully correct; see the +# TODO[macOS] in src/context/mod.rs (the %gs / TLS scratch mechanism still needs work). +set -e +if [ -z "$BIZHAWKBUILD_HOME" ]; then export BIZHAWKBUILD_HOME="$(realpath "$(dirname "$0")/../..")"; fi +cd "$(dirname "$0")" + +# This crate predates several nightly API changes (try_trait_v2, unsafe intrinsics), so pin +# a known-good nightly rather than the floating channel in rust-toolchain.toml. +TOOLCHAIN="${WBX_NIGHTLY:-nightly-2024-10-18}" +TARGET="x86_64-apple-darwin" +# Build for an older baseline so the dylib loads on older Macs, not just the build host. +# (The overall floor is set by the Homebrew deps, currently macOS 14.) +export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-11.0}" + +rustup toolchain install "$TOOLCHAIN" --profile minimal +rustup target add --toolchain "$TOOLCHAIN" "$TARGET" + +# cargo invokes a bare `rustc`; if another rustc (e.g. Homebrew's) precedes ~/.cargo/bin on +# PATH it gets picked and lacks the cross std. Pin RUSTC to the toolchain's rustc explicitly. +RUSTC_BIN="$(rustup which --toolchain "$TOOLCHAIN" rustc)" + +# Regenerate the interop blobs (needs nasm: `brew install nasm`). The macOS variant +# (interop_macos.bin) handles %gs differently — see src/context/interop_macos.s. +if command -v nasm >/dev/null 2>&1; then + make -C src/context +fi + +RUSTC="$RUSTC_BIN" cargo "+$TOOLCHAIN" build --release --target "$TARGET" + +OUT="target/$TARGET/release/libwaterboxhost.dylib" +cp "$OUT" "$BIZHAWKBUILD_HOME/Assets/dll/libwaterboxhost.dylib" +if [ -e "$BIZHAWKBUILD_HOME/output" ]; then + cp "$OUT" "$BIZHAWKBUILD_HOME/output/dll/libwaterboxhost.dylib" +fi +printf "copied libwaterboxhost.dylib (%s) into Assets/dll\n" "$TARGET" diff --git a/waterbox/waterboxhost/src/context/Makefile b/waterbox/waterboxhost/src/context/Makefile index 2f147bd6eff..3e1518c592d 100644 --- a/waterbox/waterboxhost/src/context/Makefile +++ b/waterbox/waterboxhost/src/context/Makefile @@ -1,2 +1,7 @@ -interop.bin: interop.s - nasm -f bin -Wall -o $@ $< \ No newline at end of file +all: interop.bin interop_macos.bin + +interop.bin: interop.s + nasm -f bin -Wall -o $@ $< + +interop_macos.bin: interop_macos.s + nasm -f bin -Wall -o $@ $< diff --git a/waterbox/waterboxhost/src/context/interop_macos.bin b/waterbox/waterboxhost/src/context/interop_macos.bin new file mode 100644 index 0000000000000000000000000000000000000000..1a582fdc9c078779094ece5e179a6f590580cf35 GIT binary patch literal 2088 zcmdVbOKTcY7zW@mQ8Q_qbeg7ilG!a%gJh%n^ny>(i9ATl%`zL2_E;QTzlSpo3`B8?BRaWeHYTCt8<&1)joXhdk~g$ zU^#zUf92o${*C|nMwwDIvGZ+vN(vo?r;aAeiRp6ItlgL)wU$oj60Y|+i(JC-cGKx4 zr%G~~dHC~enqOsRm6T(v=ccNsc7AP7qTB3Vs7u&JYS-4J)i7<-?myfM1XIQ1wc3N- z_^TecNe{pcS}LVgqrGs2_Q7S^50~g6xJavTfgXnQ^c6TmUxnlJH8@I-z!7>B4$#+O zKdr%S8Rf}jRd2vedJJyRH{lvR4p-<~aG4IkCHgj8r19#v3iJe=rzhbIeFu)yK{!g^ zg(LJ79H6IRKW)HmNxd`vdvKGUfgALFxJHNI3LS>abObKZ58xs_3m51)I8Q%>GxQ@k zPCtgD^bhost boundary (and on final exit) so host/macOS code sees a valid TSD. +%define REAL_TLS 0x60 + +struc Context + .thread_area resq 1 + .host_rsp resq 1 + .guest_rsp resq 1 + .host_rsp_alt resq 1 + .guest_rsp_alt resq 1 + .dispatch_syscall resq 1 + .host_ptr resq 1 + .extcall_slots resq 64 +endstruc + +times 0x80-($-$$) int3 +; called by guest when it wishes to make a syscall +guest_syscall: + push rbp + mov r10, [gs:0x18] + mov [r10 + Context.guest_rsp], rsp + mov rsp, [r10 + Context.host_rsp] + + ; save context, restore real mach_thread_self for the host call + push r10 + mov r11, [gs:REAL_TLS] + mov [gs:0x18], r11 + + mov r11, [r10 + Context.host_ptr] + push r11 ; arg 8 to dispatch_syscall: host + push rax ; arg 7 to dispatch_syscall: nr + mov rax, [r10 + Context.dispatch_syscall] + call rax + + ; Restore Context ptr for the guest + mov r10, [rsp + 16] + mov [gs:0x18], r10 + + mov rsp, [r10 + Context.guest_rsp] + pop rbp + ret +guest_syscall_end: + +times 0x100-($-$$) int3 ; CALL_GUEST_SIMPLE_ADDR +call_guest_simple: + mov r11, rdi + mov r10, rsi + jmp call_guest_impl + +times 0x200-($-$$) int3 ; CALL_GUEST_IMPL_ADDR +call_guest_impl: + ; check if we need to swap stacks for a reentrant call + mov rax, [r10 + Context.host_rsp] + test rax, rax + je do_tib + mov rax, [r10 + Context.host_rsp_alt] + test rax, rax + je do_swap + int3 ; both stacks exhausted + +do_swap: + mov rax, [r10 + Context.host_rsp] + mov [r10 + Context.host_rsp_alt], rax + mov rax, [r10 + Context.guest_rsp] + xchg rax, [r10 + Context.guest_rsp_alt] + mov [r10 + Context.guest_rsp], rax + +do_tib: + ; keep two pushes so host_rsp layout + stack balance match interop.s exactly + ; (values are unused on macOS; we do not touch gs:0x08/0x10) + push rax + push rax + + ; stash real mach_thread_self, then set Context ptr for the guest + mov rax, [gs:0x18] + mov [gs:REAL_TLS], rax + mov [gs:0x18], r10 + + mov [r10 + Context.host_rsp], rsp + mov rsp, [r10 + Context.guest_rsp] + call r11 ; stack hygiene note - this host address is saved on the guest stack + mov r10, [gs:0x18] + mov [r10 + Context.guest_rsp], rsp ; restore stack so next call using same Context will work + mov rsp, [r10 + Context.host_rsp] + mov r11, 0 + mov [r10 + Context.host_rsp], r11 ; zero out host_rsp so we'll know this callstack is no longer in use + + ; restore real mach_thread_self now that we are back in host land + ; (use r11, NOT rax: rax holds the guest function's return value) + mov r11, [gs:REAL_TLS] + mov [gs:0x18], r11 + + ; check to see if we need to swap back stacks + mov r11, [r10 + Context.host_rsp_alt] + test r11, r11 + je do_restore_tib + + mov [r10 + Context.host_rsp], r11 + mov r11, 0 + mov [r10 + Context.host_rsp_alt], r11 + mov r11, [r10 + Context.guest_rsp_alt] + xchg r11, [r10 + Context.guest_rsp] + mov [r10 + Context.guest_rsp_alt], r11 + +do_restore_tib: + ; discard the two pushed qwords (balance); do NOT touch gs:0x08/0x10 + pop r10 + pop r10 + ret + +times 0x300-($-$$) int3 ; EXTCALL_THUNK_ADDR +%macro guest_extcall_thunk 1 + mov rax, %1 + jmp guest_extcall_impl + align 16, int3 +%endmacro +%assign j 0 +%rep 64 + guest_extcall_thunk j + %assign j j+1 +%endrep + +; called by individual extcall thunks when the guest wishes to make an external call +guest_extcall_impl: + mov r10, [gs:0x18] + mov [r10 + Context.guest_rsp], rsp + mov rsp, [r10 + Context.host_rsp] + + ; save context, restore real mach_thread_self for the host call + push r10 + mov r11, [gs:REAL_TLS] + mov [gs:0x18], r11 + + mov r11, [r10 + Context.extcall_slots + rax * 8] ; get slot ptr + call r11 + + ; Restore Context ptr for the guest + mov r10, [rsp] + mov [gs:0x18], r10 + + mov rsp, [r10 + Context.guest_rsp] + ret +guest_extcall_impl_end: + +times 0x800-($-$$) int3 ; RUNTIME_TABLE_ADDR +; (Windows-only SEH table; unused on macOS but kept for identical byte layout) +runtime_function_table: + dd RVA(guest_syscall) + dd RVA(guest_syscall_end) + dd RVA(guest_syscall_unwind) + + dd RVA(guest_extcall_impl) + dd RVA(guest_extcall_impl_end) + dd RVA(guest_extcall_impl_unwind) +guest_syscall_unwind: + db 1 + db 5 + db 1 + db 0 + + db 5 + db 0x42 + dw 0 +guest_extcall_impl_unwind: + db 1 + db 5 + db 1 + db 0 + + db 5 + db 0x22 + dw 0 diff --git a/waterbox/waterboxhost/src/context/mod.rs b/waterbox/waterboxhost/src/context/mod.rs index ca7f13a5ffd..7ff77a1b5f2 100644 --- a/waterbox/waterboxhost/src/context/mod.rs +++ b/waterbox/waterboxhost/src/context/mod.rs @@ -24,6 +24,9 @@ pub fn get_callback_ptr(slot: usize) -> usize{ fn init_interop_area() -> AddressRange { unsafe { + #[cfg(target_os = "macos")] + let bytes = include_bytes!("interop_macos.bin"); + #[cfg(not(target_os = "macos"))] let bytes = include_bytes!("interop.bin"); let addr = pal::map_anon( AddressRange { start: ORG, size: bytes.len() }.align_expand(), @@ -108,7 +111,7 @@ impl Context { } } -#[cfg(unix)] +#[cfg(target_os = "linux")] thread_local!(static TIB: Box<[usize; 4]> = Box::new([0usize; 4])); /// Prepares this host thread to be allowed to call guest code. Noop if already called. @@ -121,7 +124,7 @@ pub fn prepare_thread() { // We stomp over [gs:0x18] and use it for our own mini-TLS to track the stack marshalling // On windows, that's a (normally unused and free for the plundering?) field in TIB // On linux, that register is not normally in use, so we put some bytes there and then use it - #[cfg(unix)] + #[cfg(target_os = "linux")] unsafe { use libc::*; let mut gs = 0usize; @@ -133,4 +136,16 @@ pub fn prepare_thread() { }); } } + // TODO[macOS]: Unlike Linux, on macOS/x86-64 the %gs base is already established by the + // OS and points at the thread's TSD (thread self data); it must NOT be relocated, and + // there is no arch_prctl. The interop assembly stashes its stack-marshalling scratch at + // [gs:0x18], which collides with reserved TSD slots on macOS. Making guest entry correct + // here requires either claiming a free TSD slot or using an alternate scratch mechanism, + // then re-assembling context/interop.s to match and validating under Rosetta 2. Until + // that's done this is a no-op: the host builds and non-guest-entry paths work, but + // actually entering guest code is not yet correct on macOS. + #[cfg(target_os = "macos")] + { + // intentionally empty; see TODO above + } } diff --git a/waterbox/waterboxhost/src/host.rs b/waterbox/waterboxhost/src/host.rs index b755bc8ef19..40d1dac9e5e 100644 --- a/waterbox/waterboxhost/src/host.rs +++ b/waterbox/waterboxhost/src/host.rs @@ -196,7 +196,7 @@ impl IStateable for WaterboxHost { fn unimp(nr: SyscallNumber) -> SyscallResult { eprintln!("Stopped on unimplemented syscall {}", lookup_syscall(&nr)); - std::intrinsics::breakpoint(); + unsafe { std::intrinsics::breakpoint(); } Err(ENOSYS) } diff --git a/waterbox/waterboxhost/src/memory_block/pal.rs b/waterbox/waterboxhost/src/memory_block/pal.rs index 09f3098a2b8..016a3581128 100644 --- a/waterbox/waterboxhost/src/memory_block/pal.rs +++ b/waterbox/waterboxhost/src/memory_block/pal.rs @@ -160,12 +160,25 @@ mod nix { use super::*; use libc::*; + #[cfg(target_os = "linux")] + unsafe fn errno() -> i32 { *__errno_location() } + #[cfg(target_os = "macos")] + unsafe fn errno() -> i32 { *__error() } + fn error() -> anyhow::Error { unsafe { - let err = *__errno_location(); - anyhow!("Libc failure code: {}", err) + anyhow!("Libc failure code: {}", errno()) } } + + /// mmap flags for a fixed-address mapping. + /// Linux has `MAP_FIXED_NOREPLACE` (fails instead of clobbering an existing mapping); + /// macOS only has `MAP_FIXED` (which silently replaces). Waterbox manages its own + /// reserved address space, so a plain `MAP_FIXED` matches the intended behaviour there. + #[cfg(target_os = "linux")] + fn fixed_flags() -> i32 { MAP_FIXED | MAP_FIXED_NOREPLACE } + #[cfg(target_os = "macos")] + fn fixed_flags() -> i32 { MAP_FIXED } fn ret(code: i32) -> anyhow::Result<()> { match code { 0 => Ok(()), @@ -175,6 +188,7 @@ mod nix { /// Open a file (not backed by the fs) for memory mapping /// Caller must close_handle() later or else leak + #[cfg(target_os = "linux")] pub fn open_handle(size: usize) -> anyhow::Result { unsafe { let s = std::ffi::CString::new("MemoryBlockUnix").unwrap(); @@ -190,6 +204,32 @@ mod nix { } } + /// macOS has no `memfd_create`. We must NOT use POSIX shared memory (`shm_open`) + /// here: macOS refuses to `mprotect` a `MAP_SHARED` shm region to executable + /// (EACCES), and waterbox needs to execute guest code from this handle's mapping. + /// A regular file CAN be mapped executable (like a dylib) even via `MAP_SHARED`, + /// so back the block with an immediately-unlinked temp file (anonymous once + /// unlinked; the fd keeps it alive until closed). + #[cfg(target_os = "macos")] + pub fn open_handle(size: usize) -> anyhow::Result { + unsafe { + let mut template = *b"/tmp/wbxhost-XXXXXX\0"; + let fd = mkstemp(template.as_mut_ptr() as *mut libc::c_char); + if fd == -1 { + return Err(error()) + } + // Unlink right away; the open fd alone backs the mapping from here on. + unlink(template.as_ptr() as *const libc::c_char); + if ftruncate(fd, size as off_t) != 0 { + let e = error(); + close(fd); + Err(e) + } else { + Ok(Handle(fd as usize)) + } + } + } + /// close a handle returned by open_handle() /// Unsafe: Only call with handle returned by open_handle(). Do not call when that handle is mapped pub unsafe fn close_handle(handle: Handle) -> anyhow::Result<()> { @@ -211,7 +251,7 @@ mod nix { unsafe { let mut flags = MAP_SHARED; if addr.start != 0 { - flags |= MAP_FIXED | MAP_FIXED_NOREPLACE; + flags |= fixed_flags(); } let ptr = mmap(addr.start as *mut c_void, addr.size, @@ -251,7 +291,7 @@ mod nix { unsafe { let mut flags = MAP_PRIVATE | MAP_ANONYMOUS; if addr.start != 0 { - flags |= MAP_FIXED | MAP_FIXED_NOREPLACE; + flags |= fixed_flags(); } let ptr = mmap(addr.start as *mut c_void, addr.size, prottoprot(initial_prot), flags, -1, 0); match ptr { diff --git a/waterbox/waterboxhost/src/memory_block/tripguard.rs b/waterbox/waterboxhost/src/memory_block/tripguard.rs index e2e5c7f3150..1696c77e3cb 100644 --- a/waterbox/waterboxhost/src/memory_block/tripguard.rs +++ b/waterbox/waterboxhost/src/memory_block/tripguard.rs @@ -121,18 +121,30 @@ mod trip_pal { type SaHandler = unsafe extern "C" fn(i32) -> (); type SaSigaction = unsafe extern "C" fn(i32, *const siginfo_t, *const ucontext_t) -> (); static mut SA_OLD: Option> = None; + // macOS delivers SIGBUS (not SIGSEGV) for protection violations on mapped memory, + // which is exactly the fault the tripguard catches, so we hook it as well. + #[cfg(target_os = "macos")] + static mut SA_OLD_BUS: Option> = None; pub fn initialize() { use std::mem::{transmute, zeroed}; unsafe extern "C" fn handler(sig: i32, info: *const siginfo_t, ucontext: *const ucontext_t) { let fault_address = (*info).si_addr() as usize; + // Bit 1 (value 2) of the x86 page-fault error code means the access was a write. + // The error code lives in different places depending on the OS' mcontext layout. + #[cfg(target_os = "linux")] let write = (*ucontext).uc_mcontext.gregs[REG_ERR as usize] & 2 != 0; + #[cfg(target_os = "macos")] + let write = ((*(*ucontext).uc_mcontext).__es.__err & 2) != 0; let rethrow = !write || match trip(fault_address) { TripResult::NotHandled => true, _ => false }; if rethrow { + #[cfg(target_os = "macos")] + let sa_old = if sig == SIGBUS { SA_OLD_BUS.as_ref().unwrap() } else { SA_OLD.as_ref().unwrap() }; + #[cfg(not(target_os = "macos"))] let sa_old = SA_OLD.as_ref().unwrap(); if sa_old.sa_flags & SA_SIGINFO != 0 { transmute::(sa_old.sa_sigaction)(sig, info, ucontext); @@ -156,14 +168,18 @@ mod trip_pal { // }; // assert!(sigaltstack(&ss, &mut ss_old) == 0, "sigaltstack failed"); SA_OLD = Some(Box::new(zeroed())); - let mut sa = sigaction { - sa_mask: zeroed(), - sa_sigaction: transmute::(handler), - sa_flags: SA_ONSTACK | SA_SIGINFO, - sa_restorer: None, - }; + // Build via zeroed() + field assignment so this compiles on both Linux + // (which has the extra `sa_restorer` field) and macOS (which does not). + let mut sa: sigaction = zeroed(); + sa.sa_sigaction = transmute::(handler); + sa.sa_flags = SA_ONSTACK | SA_SIGINFO; sigfillset(&mut sa.sa_mask); assert!(sigaction(SIGSEGV, &sa, &mut **SA_OLD.as_mut().unwrap() as *mut sigaction) == 0, "sigaction failed"); + #[cfg(target_os = "macos")] + { + SA_OLD_BUS = Some(Box::new(zeroed())); + assert!(sigaction(SIGBUS, &sa, &mut **SA_OLD_BUS.as_mut().unwrap() as *mut sigaction) == 0, "sigaction SIGBUS failed"); + } } } } diff --git a/waterbox/waterboxhost/src/threading.rs b/waterbox/waterboxhost/src/threading.rs index c319764604a..a37080677a4 100644 --- a/waterbox/waterboxhost/src/threading.rs +++ b/waterbox/waterboxhost/src/threading.rs @@ -176,7 +176,7 @@ impl GuestThreadSet { pub fn exit(&mut self, context: &mut Context) -> SyscallReturn { if self.active_tid == 1 { - std::intrinsics::breakpoint() + unsafe { std::intrinsics::breakpoint() } } let addr = self.threads.get_mut(&self.active_tid).unwrap().tid_address; if addr != 0 { From 3586276e754f233bd45466546d3306143db83928 Mon Sep 17 00:00:00 2001 From: DarthM Date: Thu, 25 Jun 2026 23:45:46 +0800 Subject: [PATCH 02/10] EmuHawk: macOS (x86_64) native-library and graphics handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OS-gated changes so EmuHawk runs under Mono on macOS; no behavioural change on Windows/Linux. - OSTailoredCode: add DllExtension (".dll"/".dylib"/".so"); WaterboxHost uses it to load libwaterboxhost.dylib on macOS. - MemoryBlockLinuxPal: MAP_ANON is 0x20 on Linux/BSD but 0x1000 on macOS; pick the right flag (the previous hardcoded 0x22 made mmap fail on macOS). - SDL2OpenGLContext: still force the x11 video driver on macOS (Mono WinForms is X11/XQuartz), but don't force EGL there — XQuartz provides GLX, not EGL. - OpenGLProvider: report no host OpenGL on macOS. XQuartz's GL is the legacy Apple GLX bridge (capped at 2.1, aborts under Rosetta); probing it crashes, so cores (melonDS, N64, ...) fall back to their software renderers. --- .../OpenGL/SDL2OpenGLContext.cs | 26 +++++++++++-------- .../GraphicsImplementations/OpenGLProvider.cs | 7 ++++- .../MemoryBlock/MemoryBlockLinuxPal.cs | 6 ++++- src/BizHawk.Common/OSTailoredCode.cs | 13 ++++++++++ .../Waterbox/WaterboxHost.cs | 4 ++- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs b/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs index d314b04fac9..ba86751e06c 100644 --- a/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs +++ b/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs @@ -23,19 +23,23 @@ static SDL2OpenGLContext() { if (OSTailoredCode.IsUnixHost) { - // make sure that Linux uses the x11 video driver - // we need this as mono winforms uses x11 + // make sure that we use the x11 video driver + // we need this as mono winforms uses x11 (on macOS, via XQuartz) // and the user could potentially try to force the wayland video driver via env vars SDL_SetHintWithPriority("SDL_VIDEODRIVER", "x11", SDL_HintPriority.SDL_HINT_OVERRIDE); - // try to use EGL if it is available - // GLX is the old API, and is the more or less "deprecated" at this point, and potentially more buggy with some drivers - // we do need to a bit more work, in case EGL is not actually available or potentially doesn't have desktop GL support - SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1"); - // set EGL_PLATFORM, many users have reported crashes with SDL_GL_LoadLibrary - // this is from EGL_PLATFORM being set to wayland by users to workaround wonky autodetection with nvidia drivers - // this is made worse with SDL as it uses eglGetDisplay rather than eglGetPlatformDisplay - // as such EGL assumes the display is a wayland display and proceeds to crash with that assumption - Environment.SetEnvironmentVariable("EGL_PLATFORM", "x11"); + if (OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.macOS) + { + // try to use EGL if it is available + // GLX is the old API, and is the more or less "deprecated" at this point, and potentially more buggy with some drivers + // we do need to a bit more work, in case EGL is not actually available or potentially doesn't have desktop GL support + SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1"); + // set EGL_PLATFORM, many users have reported crashes with SDL_GL_LoadLibrary + // this is from EGL_PLATFORM being set to wayland by users to workaround wonky autodetection with nvidia drivers + // this is made worse with SDL as it uses eglGetDisplay rather than eglGetPlatformDisplay + // as such EGL assumes the display is a wayland display and proceeds to crash with that assumption + Environment.SetEnvironmentVariable("EGL_PLATFORM", "x11"); + // on macOS, XQuartz provides GLX (not EGL), so we leave the above unset and let SDL use GLX + } } // init SDL video diff --git a/src/BizHawk.Client.EmuHawk/GraphicsImplementations/OpenGLProvider.cs b/src/BizHawk.Client.EmuHawk/GraphicsImplementations/OpenGLProvider.cs index b4793c7e8d4..7d6c1526589 100644 --- a/src/BizHawk.Client.EmuHawk/GraphicsImplementations/OpenGLProvider.cs +++ b/src/BizHawk.Client.EmuHawk/GraphicsImplementations/OpenGLProvider.cs @@ -1,4 +1,5 @@ using BizHawk.Bizware.Graphics; +using BizHawk.Common; using BizHawk.Emulation.Common; namespace BizHawk.Client.EmuHawk @@ -9,7 +10,11 @@ namespace BizHawk.Client.EmuHawk public class OpenGLProvider : IOpenGLProvider { public bool SupportsGLVersion(int major, int minor) - => OpenGLVersion.SupportsVersion(major, minor); + // On macOS the only GL available (XQuartz/GLX) is the legacy Apple bridge, which is + // capped at GL 2.1 and aborts under Rosetta; probing it crashes. Report no host GL so + // cores (melonDS, N64, ...) fall back to their software renderers. + => OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.macOS + && OpenGLVersion.SupportsVersion(major, minor); public object RequestGLContext(int major, int minor, bool coreProfile) => new SDL2OpenGLContext(major, minor, coreProfile); diff --git a/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs b/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs index d2b56125393..caa58eb6e88 100644 --- a/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs +++ b/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs @@ -17,9 +17,13 @@ internal sealed class MemoryBlockLinuxPal : IMemoryBlockPal /// /// failed to mmap /// + // MAP_PRIVATE is 0x02 everywhere, but MAP_ANON differs: 0x20 on Linux/BSD, 0x1000 on macOS. + private static readonly int MapPrivateAnon + = 0x02 | (OSTailoredCode.CurrentOS == OSTailoredCode.DistinctOS.macOS ? 0x1000 : 0x20); + public MemoryBlockLinuxPal(ulong size) { - var ptr = mmap(IntPtr.Zero, Z.UU(size), MemoryProtection.None, 0x22 /* MAP_PRIVATE | MAP_ANON */, -1, IntPtr.Zero); + var ptr = mmap(IntPtr.Zero, Z.UU(size), MemoryProtection.None, MapPrivateAnon, -1, IntPtr.Zero); if (ptr == new IntPtr(-1)) { throw new InvalidOperationException($"{nameof(mmap)}() failed with error {Marshal.GetLastWin32Error()}"); diff --git a/src/BizHawk.Common/OSTailoredCode.cs b/src/BizHawk.Common/OSTailoredCode.cs index f5c4f2e2ea6..753de3b4691 100644 --- a/src/BizHawk.Common/OSTailoredCode.cs +++ b/src/BizHawk.Common/OSTailoredCode.cs @@ -12,6 +12,12 @@ public static class OSTailoredCode public static readonly DistinctOS CurrentOS; public static readonly bool IsUnixHost; + /// + /// The host OS' conventional file extension for dynamically-linked libraries, including the leading dot + /// (".dll" on Windows, ".dylib" on macOS, ".so" on Linux/BSD). + /// + public static readonly string DllExtension; + static OSTailoredCode() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -36,6 +42,13 @@ static OSTailoredCode() } IsUnixHost = CurrentOS != DistinctOS.Windows; + + DllExtension = CurrentOS switch + { + DistinctOS.Windows => ".dll", + DistinctOS.macOS => ".dylib", + _ => ".so", // Linux/BSD + }; } private static readonly Lazy<(WindowsVersion, Version?)?> _HostWindowsVersion = new(() => diff --git a/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs b/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs index 4779521bf33..7f71ed88340 100644 --- a/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs +++ b/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs @@ -75,7 +75,9 @@ public sealed class WaterboxHost : IMonitor, IImportResolver, IStatable, IDispos static WaterboxHost() { NativeImpl = BizInvoker.GetInvoker( - new DynamicLibraryImportResolver(OSTailoredCode.IsUnixHost ? "libwaterboxhost.so" : "waterboxhost.dll", hasLimitedLifetime: false), + new DynamicLibraryImportResolver( + (OSTailoredCode.IsUnixHost ? "libwaterboxhost" : "waterboxhost") + OSTailoredCode.DllExtension, + hasLimitedLifetime: false), CallingConventionAdapters.Native); #if !DEBUG NativeImpl.wbx_set_always_evict_blocks(false); From ece6b63e3552f62231dc470c95e268e0e04b7d62 Mon Sep 17 00:00:00 2001 From: DarthM Date: Thu, 25 Jun 2026 23:46:09 +0800 Subject: [PATCH 03/10] macOS: launcher, dependency staging script, and docs - Assets/EmuHawkMonoMacOS.sh: launcher for macOS. Runs the x86_64 Mono directly (NOT via `arch`, which is SIP-protected and strips DYLD_*), forces the X11 WinForms driver (MONO_MWF_MAC_FORCE_X11) and GdiPlus video (--gdi), and sets up DYLD paths incl. /opt/X11/lib for XQuartz. - Dist/stage-macos-dylibs.sh: symlink the Homebrew-provided deps and the Linux-soname aliases (libX11.so.6, etc.) into output/dll, all pointing at one consistent Homebrew libX11. - README: add an experimental macOS (x86_64) section with Intel + Apple silicon setup and a clear "not officially supported, don't file issues unless reproducible on Win/Linux" warning for maintainers. - .gitignore: ignore the Mono launcher's captured stdout/stderr logs. --- .gitignore | 2 ++ Assets/EmuHawkMonoMacOS.sh | 51 ++++++++++++++++++++++++++++++ Dist/stage-macos-dylibs.sh | 65 ++++++++++++++++++++++++++++++++++++++ README.md | 33 ++++++++++++++----- 4 files changed, 143 insertions(+), 8 deletions(-) create mode 100755 Assets/EmuHawkMonoMacOS.sh create mode 100755 Dist/stage-macos-dylibs.sh diff --git a/.gitignore b/.gitignore index 7a60dde55f9..a1f65529aa6 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,5 @@ UpgradeLog.htm /packages launchSettings.json +Assets/EmuHawkMono_laststderr.txt +Assets/EmuHawkMono_laststdout.txt diff --git a/Assets/EmuHawkMonoMacOS.sh b/Assets/EmuHawkMonoMacOS.sh new file mode 100755 index 00000000000..68deb08985e --- /dev/null +++ b/Assets/EmuHawkMonoMacOS.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Launcher for EmuHawk on macOS via Mono (x86_64, under Rosetta 2 on Apple Silicon). +# Mono's System.Windows.Forms is X11-based, so an X server (XQuartz) must be running. +# See also EmuHawkMono.sh (the Linux equivalent this is modelled on). +cd "$(dirname "$(realpath "$0")")" + +# Native libs ship in ./dll; also let the loader find Homebrew-provided deps +# (SDL2, OpenAL, Lua, zstd, libgdiplus, ...) under /usr/local, and XQuartz's X11 +# libs under /opt/X11/lib (Mono's WinForms dlopens libX11 etc. from there). +export DYLD_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" +export DYLD_FALLBACK_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib:/usr/lib${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" + +export MONO_CRASH_NOFILE=1 +export MONO_WINFORMS_XIM_STYLE=disabled # see https://bugzilla.xamarin.com/show_bug.cgi?id=28047#c9 +# Mono's default macOS WinForms backend is the Carbon driver, which is unported to 64-bit +# ("very few parts of Windows.Forms will work properly, or at all"). Force the X11 driver, +# which renders through XQuartz. +export MONO_MWF_MAC_FORCE_X11=1 + +# XQuartz: ensure DISPLAY points at a running X server. +if [ -z "$DISPLAY" ]; then + export DISPLAY=:0 +fi +if ! command -v Xquartz >/dev/null 2>&1 && [ ! -d "/Applications/Utilities/XQuartz.app" ] && [ ! -d "/opt/X11" ]; then + printf "%s\n" "XQuartz does not appear to be installed; Mono WinForms needs an X11 server. Install with: brew install --cask xquartz" >&2 +fi + +# Prefer the x86_64 Mono from Homebrew at /usr/local. Invoke it DIRECTLY (not via `arch`): +# /usr/bin/arch is SIP-protected and strips DYLD_* from the environment, which breaks our +# library paths. An x86_64-only binary auto-runs under Rosetta 2 when exec'd directly. +mono_bin="mono" +if [ -x "/usr/local/bin/mono" ]; then + mono_bin="/usr/local/bin/mono" +fi + +# Force the GdiPlus (software) display method: XQuartz's OpenGL is Apple's legacy GLX bridge, +# which is only GL 2.1 and crashes (CGLSetCurrentContext) under Rosetta. Without this, EmuHawk +# aborts trying to init OpenGL. GdiPlus is full-speed here since the host only blits frames. +set -- --gdi "$@" + +if (ps -A -o "command" | grep -F "EmuHawk.exe" | grep -Fvq "grep"); then + printf "(it seems EmuHawk is already running, NOT capturing output)\n" >&2 + exec $mono_bin EmuHawk.exe "$@" +fi +o="$(mktemp -u)" +e="$(mktemp -u)" +mkfifo "$o" "$e" +printf "(capturing output in %s/EmuHawkMono_last*.txt)\n" "$PWD" >&2 +tee EmuHawkMono_laststdout.txt <"$o" & +tee EmuHawkMono_laststderr.txt <"$e" | sed "s/.*/$(tput setaf 1)&$(tput sgr0)/" >&2 & +exec $mono_bin EmuHawk.exe "$@" >"$o" 2>"$e" diff --git a/Dist/stage-macos-dylibs.sh b/Dist/stage-macos-dylibs.sh new file mode 100755 index 00000000000..86b627eac5d --- /dev/null +++ b/Dist/stage-macos-dylibs.sh @@ -0,0 +1,65 @@ +#!/bin/sh +# Stage the macOS (x86_64 / Rosetta) native-library glue into the EmuHawk output dir. +# +# Run this once after building (Dist/BuildRelease.sh) and before launching with +# Assets/EmuHawkMonoMacOS.sh. It symlinks the Homebrew-provided dependencies (SDL deps, +# OpenAL, Lua, zstd, SQLite, the X11 client stack) and creates the Linux-soname aliases +# (libX11.so.6, etc.) that BizHawk's P/Invokes and Mono expect, all pointing at the SAME +# Homebrew libX11 so cairo/libgdiplus/WinForms/SDL agree (see notes in EmuHawkMonoMacOS.sh). +# +# The dylibs BizHawk itself builds/bundles (libSDL2, libgdiplus, libcimgui, libwaterboxhost) +# ship in Assets/dll and are copied to output/dll by the build, so they are not handled here. +# +# Prereqs (x86_64 Homebrew under Rosetta): +# brew install mono sdl2 openal-soft lua@5.4 zstd sqlite mono-libgdiplus \ +# libx11 libxext libxrender libxcursor libxinerama libxi libxrandr \ +# libxtst libxfixes libxscrnsaver libxau libxdmcp libxcb +# brew install --cask xquartz +set -e + +# Target dll dir: arg 1, else ./dll relative to cwd, else output/dll under the repo. +DLL="${1:-}" +if [ -z "$DLL" ]; then + if [ -d "dll" ]; then DLL="dll" + else DLL="$(cd "$(dirname "$0")/.." && pwd)/output/dll"; fi +fi +if [ ! -d "$DLL" ]; then printf "output dll dir not found: %s\n" "$DLL" >&2; exit 1; fi +cd "$DLL" + +opt() { echo "/usr/local/opt/$1"; } +link() { # link + [ -e "$1" ] && ln -sf "$1" "$2" || printf "WARN missing %s (for %s)\n" "$1" "$2" >&2 +} + +# Homebrew runtime deps under their canonical names +link "$(opt lua@5.4)/lib/liblua5.4.dylib" liblua54.dylib +link "$(opt openal-soft)/lib/libopenal.dylib" libopenal.dylib +link "$(opt openal-soft)/lib/libopenal.1.dylib" libopenal.1.dylib +link "$(opt zstd)/lib/libzstd.1.dylib" libzstd.1.dylib +link "$(opt sqlite)/lib/libsqlite3.dylib" libe_sqlite3.dylib +link "$(opt sqlite)/lib/libsqlite3.dylib" e_sqlite3.dylib + +# X11 client stack (all from Homebrew, so they share one libX11) +link "$(opt libx11)/lib/libX11.6.dylib" libX11.6.dylib +link "$(opt libxext)/lib/libXext.6.dylib" libXext.6.dylib +link "$(opt libxrender)/lib/libXrender.1.dylib" libXrender.1.dylib +link "$(opt libxcursor)/lib/libXcursor.1.dylib" libXcursor.1.dylib +link "$(opt libxinerama)/lib/libXinerama.1.dylib" libXinerama.1.dylib +link "$(opt libxi)/lib/libXi.6.dylib" libXi.6.dylib +link "$(opt libxrandr)/lib/libXrandr.2.dylib" libXrandr.2.dylib +link "$(opt libxtst)/lib/libXtst.6.dylib" libXtst.6.dylib +link "$(opt libxfixes)/lib/libXfixes.3.dylib" libXfixes.3.dylib +link "$(opt libxscrnsaver)/lib/libXss.1.dylib" libXss.1.dylib +link "$(opt libxau)/lib/libXau.6.dylib" libXau.6.dylib +link "$(opt libxdmcp)/lib/libXdmcp.6.dylib" libXdmcp.6.dylib +link "$(opt libxcb)/lib/libxcb.1.dylib" libxcb.1.dylib + +# Linux-soname aliases that BizHawk's P/Invokes and Mono hardcode (XlibImports etc.) +ln -sf libX11.6.dylib libX11.dylib +ln -sf libX11.6.dylib libX11.so.6 +ln -sf libXfixes.3.dylib libXfixes.so.3 +ln -sf libXi.6.dylib libXi.so.6 +ln -sf libzstd.1.dylib libzstd.so.1 +ln -sf libgdiplus.0.dylib libgdiplus.dylib + +printf "staged macOS dylib symlinks into %s\n" "$DLL" diff --git a/README.md b/README.md index 140148fb9ed..805c3e0b8bc 100644 --- a/README.md +++ b/README.md @@ -172,14 +172,31 @@ As with Apple silicon Macs, not available. If you were looking to emulate iOS apps, see [#3956](https://github.com/TASEmulators/BizHawk/issues/3956). -#### macOS (legacy BizHawk) - -EmuHawk depends on certain libraries for graphics, and these don't work on macOS. Users on macOS have three options: -* Use another machine with Windows or Linux, or install either in an x86_64 VM (Wine is not a VM). -* Use an older 1.x release, which was ported to macOS by @Sappharad (with replacements for the missing libraries), via Rosetta. Links and more details are in [this TASVideos forum thread](https://tasvideos.org/Forum/Topics/12659) (jump to last page for latest binaries). See [#3697](https://github.com/TASEmulators/BizHawk/issues/3697) for details. -* For the technically-minded, download the [source](https://github.com/Sappharad/BizHawk/tree/MacUnixMonoCompat) of an older 2.x release. @Sappharad put a lot of work into it but ultimately decided to stop. - * ...or use the Nix expression as a starting point instead. - * Either way, this probably won't work on Apple silicon without a lot more effort. You'll probably want to build for x86_64 and run Mono via Rosetta. See [#4052](https://github.com/TASEmulators/BizHawk/issues/4052) re: Linux AArch64. +#### macOS (experimental x86_64 port) + +> [!WARNING] +> **This macOS port is EXPERIMENTAL and community-maintained — it is *not* officially supported by the BizHawk team.** +> It runs the **x86_64** build under Mono + XQuartz (natively on Intel Macs, via Rosetta 2 on Apple silicon). Expect rough edges: software (GdiPlus) video only, no hardware OpenGL (so no HD/upscaled rendering or GL shader filters), and it needs a manual dependency setup. +> **Please do not open issues about this port on the main tracker** unless you can also reproduce them on Windows/Linux — most of the dev team is on Windows and cannot support it. Discuss macOS-specific problems in the relevant macOS thread instead. + +Requirements: **macOS 14 (Sonoma) or newer** (this is the floor imposed by the Homebrew dependencies; the bundled native libraries themselves target macOS 11). x86_64 only — on Apple silicon everything runs through **Rosetta 2**. + +Setup (all the brew commands must be the **x86_64** Homebrew under `/usr/local`): + +1. Apple silicon only: install Rosetta 2 — `softwareupdate --install-rosetta --agree-to-license`. +2. Install an **x86_64 Homebrew** at `/usr/local`. On Intel Macs the normal Homebrew is already x86_64. On Apple silicon, install a second one under Rosetta: + `arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` + (prefix every command below with `arch -x86_64` and use `/usr/local/bin/brew`). +3. Install the runtime dependencies: + `brew install mono mono-libgdiplus sdl2 openal-soft lua@5.4 zstd sqlite` + `brew install --cask xquartz` (then log out/in so the X server is registered) +4. Get a build: either download the macOS dev build, or build from source (see [*Building*](#building)) and then run `Dist/stage-macos-dylibs.sh` to link the dependencies into `output/dll`. + +Run `EmuHawkMonoMacOS.sh` to start EmuHawk (it forces the X11 WinForms driver and GdiPlus video, which are required on macOS). **XQuartz must be running.** It takes the same command-line arguments as on Windows: see [*Passing command-line arguments*](#passing-command-line-arguments), e.g. `./EmuHawkMonoMacOS.sh --lua=/path/to/script.lua /path/to/rom.nds`. + +What works: most non-GL cores including **Game Boy/Color (Gambatte)**, **GBA (mGBA)**, and **Nintendo DS (melonDS)**, plus Lua scripting. Cores that require host OpenGL fall back to their software renderers. N64 and other GL-only paths are not expected to work. + +If you need an older 1.x macOS build instead, @Sappharad's Rosetta port is described in [this TASVideos forum thread](https://tasvideos.org/Forum/Topics/12659); see also [#3697](https://github.com/TASEmulators/BizHawk/issues/3697) and [#1430](https://github.com/TASEmulators/BizHawk/issues/1430). #### Nix/NixOS From aa99fa41885c9bad906fac8c1bdc3b86ee555ceb Mon Sep 17 00:00:00 2001 From: DarthM Date: Fri, 26 Jun 2026 00:39:38 +0800 Subject: [PATCH 04/10] macOS: Fix keyboard input --- .../KeyMouseInput/KeyMouseInputFactory.cs | 5 ++++- src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/BizHawk.Bizware.Input/KeyMouseInput/KeyMouseInputFactory.cs b/src/BizHawk.Bizware.Input/KeyMouseInput/KeyMouseInputFactory.cs index c7739d8749c..c6abdddb6a4 100644 --- a/src/BizHawk.Bizware.Input/KeyMouseInput/KeyMouseInputFactory.cs +++ b/src/BizHawk.Bizware.Input/KeyMouseInput/KeyMouseInputFactory.cs @@ -9,7 +9,10 @@ internal static class KeyMouseInputFactory public static IKeyMouseInput CreateKeyMouseInput() => OSTailoredCode.CurrentOS switch { OSTailoredCode.DistinctOS.Linux => new X11KeyMouseInput(), - OSTailoredCode.DistinctOS.macOS => new QuartzKeyMouseInput(), + // macOS EmuHawk runs under XQuartz (Mono WinForms is X11), so read the keyboard/mouse + // through X11 like Linux. The Quartz path (CGEventSourceKeyState) needs Accessibility + // permission and Mac HID keycodes, and doesn't integrate with the XQuartz window. + OSTailoredCode.DistinctOS.macOS => new X11KeyMouseInput(), OSTailoredCode.DistinctOS.Windows => new RawKeyMouseInput(), _ => throw new InvalidOperationException("Unknown OS"), }; diff --git a/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs b/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs index 25a04de3d9a..11cc4e1d87a 100644 --- a/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs +++ b/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System.Collections.Generic; using System.Runtime.InteropServices; @@ -23,9 +23,9 @@ internal sealed class X11KeyMouseInput : IKeyMouseInput public X11KeyMouseInput() { - if (OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.Linux) + if (OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.Linux && OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.macOS) { - throw new NotSupportedException("X11 is Linux only"); + throw new NotSupportedException("X11 is Linux/macOS only"); } Display = XOpenDisplay(null); From 0d7ab8f3d07ac9b70a65e1e37ab6c6d901e53fe0 Mon Sep 17 00:00:00 2001 From: DarthM Date: Fri, 26 Jun 2026 13:12:23 +0800 Subject: [PATCH 05/10] macOS: address PR review feedback - Merge EmuHawkMonoMacOS.sh into EmuHawkMono.sh via a `uname` case branch; use PATH for Mono (respects e.g. a Nix install) instead of a hardcoded path. - Centralise the [DllImport]-style native library naming in DynamicLibraryImportResolver.PlatformFileName(); drop OSTailoredCode.DllExtension and use it from WaterboxHost. - SDL2OpenGLContext: split the macOS branch out from the generic Unix one. - MemoryBlockLinuxPal: hoist MAP_PRIVATE/MAP_ANON to named consts. - X11KeyMouseInput: drop the OS check (XOpenDisplay fails cleanly elsewhere; X11 works under XQuartz on macOS). - Revert the .gitignore entries (those logs only appear if run from Assets/). --- .gitignore | 2 - Assets/EmuHawkMono.sh | 55 +++++++++++++------ Assets/EmuHawkMonoMacOS.sh | 51 ----------------- Dist/stage-macos-dylibs.sh | 4 +- README.md | 4 +- .../OpenGL/SDL2OpenGLContext.cs | 32 ++++++----- .../KeyMouseInput/X11KeyMouseInput.cs | 7 +-- src/BizHawk.Common/IImportResolver.cs | 13 +++++ .../MemoryBlock/MemoryBlockLinuxPal.cs | 11 ++-- src/BizHawk.Common/OSTailoredCode.cs | 13 ----- .../Waterbox/WaterboxHost.cs | 2 +- 11 files changed, 81 insertions(+), 113 deletions(-) delete mode 100755 Assets/EmuHawkMonoMacOS.sh diff --git a/.gitignore b/.gitignore index a1f65529aa6..7a60dde55f9 100644 --- a/.gitignore +++ b/.gitignore @@ -48,5 +48,3 @@ UpgradeLog.htm /packages launchSettings.json -Assets/EmuHawkMono_laststderr.txt -Assets/EmuHawkMono_laststdout.txt diff --git a/Assets/EmuHawkMono.sh b/Assets/EmuHawkMono.sh index 8b600082fb3..c5345523ff9 100755 --- a/Assets/EmuHawkMono.sh +++ b/Assets/EmuHawkMono.sh @@ -1,24 +1,45 @@ #!/bin/sh cd "$(dirname "$(realpath "$0")")" -libpath="" -if [ "$(command -v lsb_release)" ]; then - case "$(lsb_release -i | head -n1 | cut -c17- | tr A-Z a-z)" in - "arch"|"artix"|"manjarolinux") libpath="/usr/lib";; - "fedora"|"gentoo"|"nobaralinux"|"opensuse") libpath="/usr/lib64";; - "nixos") libpath="/usr/lib"; printf "Running on NixOS? Why aren't you using the Nix expr?\n" >&2;; - "debian"|"linuxmint"|"pop"|"ubuntu") libpath="/usr/lib/x86_64-linux-gnu";; - esac -else - printf "Distro does not provide LSB release info API! (You've met with a terrible fate, haven't you?)\n" >&2 -fi -if [ -z "$libpath" ]; then - printf "%s\n" "Unknown distro, assuming system-wide libraries are in /usr/lib..." >&2 - libpath="/usr/lib" -fi -export LD_LIBRARY_PATH="$PWD/dll:$PWD:$libpath" +case "$(uname -s)" in +Darwin) + # macOS (x86_64, natively on Intel or via Rosetta 2 on Apple silicon). Mono's WinForms is + # X11-based, so XQuartz must be running; see the macOS section in the readme. + # Native libs are in ./dll; also let the loader find the Homebrew deps under /usr/local and + # XQuartz's X11 libs under /opt/X11/lib. + export DYLD_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + export DYLD_FALLBACK_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib:/usr/lib${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" + # Prefer the x86_64 Homebrew Mono by putting it on PATH (PATH is then respected, so e.g. a Nix + # Mono works too). Don't invoke Mono via `arch`: it's SIP-protected and strips the DYLD_* vars. + [ -d /usr/local/bin ] && export PATH="/usr/local/bin:$PATH" + export MONO_MWF_MAC_FORCE_X11=1 # the default macOS WinForms driver (Carbon) is unported to 64-bit + [ -z "$DISPLAY" ] && export DISPLAY=:0 + # XQuartz's OpenGL is the legacy Apple GLX bridge (GL 2.1, and crashes under Rosetta), so use + # the GdiPlus software display method. + set -- --gdi "$@" + ;; +*) + # GNU+Linux (and other Unix) + libpath="" + if [ "$(command -v lsb_release)" ]; then + case "$(lsb_release -i | head -n1 | cut -c17- | tr A-Z a-z)" in + "arch"|"artix"|"manjarolinux") libpath="/usr/lib";; + "fedora"|"gentoo"|"nobaralinux"|"opensuse") libpath="/usr/lib64";; + "nixos") libpath="/usr/lib"; printf "Running on NixOS? Why aren't you using the Nix expr?\n" >&2;; + "debian"|"linuxmint"|"pop"|"ubuntu") libpath="/usr/lib/x86_64-linux-gnu";; + esac + else + printf "Distro does not provide LSB release info API! (You've met with a terrible fate, haven't you?)\n" >&2 + fi + if [ -z "$libpath" ]; then + printf "%s\n" "Unknown distro, assuming system-wide libraries are in /usr/lib..." >&2 + libpath="/usr/lib" + fi + export LD_LIBRARY_PATH="$PWD/dll:$PWD:$libpath" + ;; +esac export MONO_CRASH_NOFILE=1 export MONO_WINFORMS_XIM_STYLE=disabled # see https://bugzilla.xamarin.com/show_bug.cgi?id=28047#c9 -if (ps -C "mono" -o "cmd" --no-headers | grep -Fq "EmuHawk.exe"); then +if (ps -A -o command= | grep -F "EmuHawk.exe" | grep -Fvq "grep"); then printf "(it seems EmuHawk is already running, NOT capturing output)\n" >&2 exec mono EmuHawk.exe "$@" fi diff --git a/Assets/EmuHawkMonoMacOS.sh b/Assets/EmuHawkMonoMacOS.sh deleted file mode 100755 index 68deb08985e..00000000000 --- a/Assets/EmuHawkMonoMacOS.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh -# Launcher for EmuHawk on macOS via Mono (x86_64, under Rosetta 2 on Apple Silicon). -# Mono's System.Windows.Forms is X11-based, so an X server (XQuartz) must be running. -# See also EmuHawkMono.sh (the Linux equivalent this is modelled on). -cd "$(dirname "$(realpath "$0")")" - -# Native libs ship in ./dll; also let the loader find Homebrew-provided deps -# (SDL2, OpenAL, Lua, zstd, libgdiplus, ...) under /usr/local, and XQuartz's X11 -# libs under /opt/X11/lib (Mono's WinForms dlopens libX11 etc. from there). -export DYLD_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" -export DYLD_FALLBACK_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib:/usr/lib${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" - -export MONO_CRASH_NOFILE=1 -export MONO_WINFORMS_XIM_STYLE=disabled # see https://bugzilla.xamarin.com/show_bug.cgi?id=28047#c9 -# Mono's default macOS WinForms backend is the Carbon driver, which is unported to 64-bit -# ("very few parts of Windows.Forms will work properly, or at all"). Force the X11 driver, -# which renders through XQuartz. -export MONO_MWF_MAC_FORCE_X11=1 - -# XQuartz: ensure DISPLAY points at a running X server. -if [ -z "$DISPLAY" ]; then - export DISPLAY=:0 -fi -if ! command -v Xquartz >/dev/null 2>&1 && [ ! -d "/Applications/Utilities/XQuartz.app" ] && [ ! -d "/opt/X11" ]; then - printf "%s\n" "XQuartz does not appear to be installed; Mono WinForms needs an X11 server. Install with: brew install --cask xquartz" >&2 -fi - -# Prefer the x86_64 Mono from Homebrew at /usr/local. Invoke it DIRECTLY (not via `arch`): -# /usr/bin/arch is SIP-protected and strips DYLD_* from the environment, which breaks our -# library paths. An x86_64-only binary auto-runs under Rosetta 2 when exec'd directly. -mono_bin="mono" -if [ -x "/usr/local/bin/mono" ]; then - mono_bin="/usr/local/bin/mono" -fi - -# Force the GdiPlus (software) display method: XQuartz's OpenGL is Apple's legacy GLX bridge, -# which is only GL 2.1 and crashes (CGLSetCurrentContext) under Rosetta. Without this, EmuHawk -# aborts trying to init OpenGL. GdiPlus is full-speed here since the host only blits frames. -set -- --gdi "$@" - -if (ps -A -o "command" | grep -F "EmuHawk.exe" | grep -Fvq "grep"); then - printf "(it seems EmuHawk is already running, NOT capturing output)\n" >&2 - exec $mono_bin EmuHawk.exe "$@" -fi -o="$(mktemp -u)" -e="$(mktemp -u)" -mkfifo "$o" "$e" -printf "(capturing output in %s/EmuHawkMono_last*.txt)\n" "$PWD" >&2 -tee EmuHawkMono_laststdout.txt <"$o" & -tee EmuHawkMono_laststderr.txt <"$e" | sed "s/.*/$(tput setaf 1)&$(tput sgr0)/" >&2 & -exec $mono_bin EmuHawk.exe "$@" >"$o" 2>"$e" diff --git a/Dist/stage-macos-dylibs.sh b/Dist/stage-macos-dylibs.sh index 86b627eac5d..a4cd721e6ec 100755 --- a/Dist/stage-macos-dylibs.sh +++ b/Dist/stage-macos-dylibs.sh @@ -2,10 +2,10 @@ # Stage the macOS (x86_64 / Rosetta) native-library glue into the EmuHawk output dir. # # Run this once after building (Dist/BuildRelease.sh) and before launching with -# Assets/EmuHawkMonoMacOS.sh. It symlinks the Homebrew-provided dependencies (SDL deps, +# Assets/EmuHawkMono.sh. It symlinks the Homebrew-provided dependencies (SDL deps, # OpenAL, Lua, zstd, SQLite, the X11 client stack) and creates the Linux-soname aliases # (libX11.so.6, etc.) that BizHawk's P/Invokes and Mono expect, all pointing at the SAME -# Homebrew libX11 so cairo/libgdiplus/WinForms/SDL agree (see notes in EmuHawkMonoMacOS.sh). +# Homebrew libX11 so cairo/libgdiplus/WinForms/SDL agree (see notes in EmuHawkMono.sh). # # The dylibs BizHawk itself builds/bundles (libSDL2, libgdiplus, libcimgui, libwaterboxhost) # ship in Assets/dll and are copied to output/dll by the build, so they are not handled here. diff --git a/README.md b/README.md index 805c3e0b8bc..9147272819d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Jump to: * Installing * [Windows](#windows) * [Unix](#unix) - * [macOS](#macos-legacy-bizhawk) + * [macOS](#macos-experimental-x86_64-port) * [Nix/NixOS](#nixnixos) * [Development builds](#development-builds) * [Building](#building) @@ -192,7 +192,7 @@ Setup (all the brew commands must be the **x86_64** Homebrew under `/usr/local`) `brew install --cask xquartz` (then log out/in so the X server is registered) 4. Get a build: either download the macOS dev build, or build from source (see [*Building*](#building)) and then run `Dist/stage-macos-dylibs.sh` to link the dependencies into `output/dll`. -Run `EmuHawkMonoMacOS.sh` to start EmuHawk (it forces the X11 WinForms driver and GdiPlus video, which are required on macOS). **XQuartz must be running.** It takes the same command-line arguments as on Windows: see [*Passing command-line arguments*](#passing-command-line-arguments), e.g. `./EmuHawkMonoMacOS.sh --lua=/path/to/script.lua /path/to/rom.nds`. +Run `EmuHawkMono.sh` to start EmuHawk (it forces the X11 WinForms driver and GdiPlus video, which are required on macOS). **XQuartz must be running.** It takes the same command-line arguments as on Windows: see [*Passing command-line arguments*](#passing-command-line-arguments), e.g. `./EmuHawkMono.sh --lua=/path/to/script.lua /path/to/rom.nds`. What works: most non-GL cores including **Game Boy/Color (Gambatte)**, **GBA (mGBA)**, and **Nintendo DS (melonDS)**, plus Lua scripting. Cores that require host OpenGL fall back to their software renderers. N64 and other GL-only paths are not expected to work. diff --git a/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs b/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs index ba86751e06c..52a26b47053 100644 --- a/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs +++ b/src/BizHawk.Bizware.Graphics/OpenGL/SDL2OpenGLContext.cs @@ -21,25 +21,27 @@ public class SDL2OpenGLContext : IDisposable #pragma warning disable CA1065 // not sure how else to handle failure other than throwing with a good message static SDL2OpenGLContext() { - if (OSTailoredCode.IsUnixHost) + if (OSTailoredCode.CurrentOS is OSTailoredCode.DistinctOS.macOS) + { + // mono winforms uses x11 (via XQuartz) on macOS too, so use the x11 video driver, + // but XQuartz only provides GLX (not EGL), so don't force EGL like on Linux + SDL_SetHintWithPriority("SDL_VIDEODRIVER", "x11", SDL_HintPriority.SDL_HINT_OVERRIDE); + } + else if (OSTailoredCode.IsUnixHost) { // make sure that we use the x11 video driver - // we need this as mono winforms uses x11 (on macOS, via XQuartz) + // we need this as mono winforms uses x11 // and the user could potentially try to force the wayland video driver via env vars SDL_SetHintWithPriority("SDL_VIDEODRIVER", "x11", SDL_HintPriority.SDL_HINT_OVERRIDE); - if (OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.macOS) - { - // try to use EGL if it is available - // GLX is the old API, and is the more or less "deprecated" at this point, and potentially more buggy with some drivers - // we do need to a bit more work, in case EGL is not actually available or potentially doesn't have desktop GL support - SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1"); - // set EGL_PLATFORM, many users have reported crashes with SDL_GL_LoadLibrary - // this is from EGL_PLATFORM being set to wayland by users to workaround wonky autodetection with nvidia drivers - // this is made worse with SDL as it uses eglGetDisplay rather than eglGetPlatformDisplay - // as such EGL assumes the display is a wayland display and proceeds to crash with that assumption - Environment.SetEnvironmentVariable("EGL_PLATFORM", "x11"); - // on macOS, XQuartz provides GLX (not EGL), so we leave the above unset and let SDL use GLX - } + // try to use EGL if it is available + // GLX is the old API, and is the more or less "deprecated" at this point, and potentially more buggy with some drivers + // we do need to a bit more work, in case EGL is not actually available or potentially doesn't have desktop GL support + SDL_SetHint(SDL_HINT_VIDEO_X11_FORCE_EGL, "1"); + // set EGL_PLATFORM, many users have reported crashes with SDL_GL_LoadLibrary + // this is from EGL_PLATFORM being set to wayland by users to workaround wonky autodetection with nvidia drivers + // this is made worse with SDL as it uses eglGetDisplay rather than eglGetPlatformDisplay + // as such EGL assumes the display is a wayland display and proceeds to crash with that assumption + Environment.SetEnvironmentVariable("EGL_PLATFORM", "x11"); } // init SDL video diff --git a/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs b/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs index 11cc4e1d87a..292fc16a80f 100644 --- a/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs +++ b/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs @@ -23,11 +23,8 @@ internal sealed class X11KeyMouseInput : IKeyMouseInput public X11KeyMouseInput() { - if (OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.Linux && OSTailoredCode.CurrentOS != OSTailoredCode.DistinctOS.macOS) - { - throw new NotSupportedException("X11 is Linux/macOS only"); - } - + // no OS check: X11 may work on any host with an X server (e.g. macOS via XQuartz); + // XOpenDisplay below fails cleanly otherwise, and Windows uses RawKeyMouseInput Display = XOpenDisplay(null); if (Display == IntPtr.Zero) diff --git a/src/BizHawk.Common/IImportResolver.cs b/src/BizHawk.Common/IImportResolver.cs index 017b4a784bf..6f1c11ee2df 100644 --- a/src/BizHawk.Common/IImportResolver.cs +++ b/src/BizHawk.Common/IImportResolver.cs @@ -13,6 +13,19 @@ public interface IImportResolver public class DynamicLibraryImportResolver : IDisposable, IImportResolver { + /// + /// Maps a bare library name (as you'd pass to [DllImport]) to + /// the host's conventional filename: name.dll on Windows, libname.dylib on macOS, and + /// libname.so on Linux/BSD. Use this instead of hardcoding IsUnixHost ? "lib…so" : "….dll" + /// at each call site. + /// + public static string PlatformFileName(string baseName) => OSTailoredCode.CurrentOS switch + { + OSTailoredCode.DistinctOS.Windows => $"{baseName}.dll", + OSTailoredCode.DistinctOS.macOS => $"lib{baseName}.dylib", + _ => $"lib{baseName}.so", // Linux/BSD + }; + private IntPtr _p; public readonly bool HasLimitedLifetime; diff --git a/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs b/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs index caa58eb6e88..3401668f946 100644 --- a/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs +++ b/src/BizHawk.Common/MemoryBlock/MemoryBlockLinuxPal.cs @@ -7,6 +7,11 @@ namespace BizHawk.Common { internal sealed class MemoryBlockLinuxPal : IMemoryBlockPal { + private const int MAP_PRIVATE = 0x2; + + // MAP_ANON is 0x20 on Linux/BSD but 0x1000 on macOS + private static readonly int MAP_ANON = OSTailoredCode.CurrentOS is OSTailoredCode.DistinctOS.macOS ? 0x1000 : 0x20; + public ulong Start { get; } private readonly ulong _size; private bool _disposed; @@ -17,13 +22,9 @@ internal sealed class MemoryBlockLinuxPal : IMemoryBlockPal /// /// failed to mmap /// - // MAP_PRIVATE is 0x02 everywhere, but MAP_ANON differs: 0x20 on Linux/BSD, 0x1000 on macOS. - private static readonly int MapPrivateAnon - = 0x02 | (OSTailoredCode.CurrentOS == OSTailoredCode.DistinctOS.macOS ? 0x1000 : 0x20); - public MemoryBlockLinuxPal(ulong size) { - var ptr = mmap(IntPtr.Zero, Z.UU(size), MemoryProtection.None, MapPrivateAnon, -1, IntPtr.Zero); + var ptr = mmap(IntPtr.Zero, Z.UU(size), MemoryProtection.None, MAP_PRIVATE | MAP_ANON, -1, IntPtr.Zero); if (ptr == new IntPtr(-1)) { throw new InvalidOperationException($"{nameof(mmap)}() failed with error {Marshal.GetLastWin32Error()}"); diff --git a/src/BizHawk.Common/OSTailoredCode.cs b/src/BizHawk.Common/OSTailoredCode.cs index 753de3b4691..f5c4f2e2ea6 100644 --- a/src/BizHawk.Common/OSTailoredCode.cs +++ b/src/BizHawk.Common/OSTailoredCode.cs @@ -12,12 +12,6 @@ public static class OSTailoredCode public static readonly DistinctOS CurrentOS; public static readonly bool IsUnixHost; - /// - /// The host OS' conventional file extension for dynamically-linked libraries, including the leading dot - /// (".dll" on Windows, ".dylib" on macOS, ".so" on Linux/BSD). - /// - public static readonly string DllExtension; - static OSTailoredCode() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) @@ -42,13 +36,6 @@ static OSTailoredCode() } IsUnixHost = CurrentOS != DistinctOS.Windows; - - DllExtension = CurrentOS switch - { - DistinctOS.Windows => ".dll", - DistinctOS.macOS => ".dylib", - _ => ".so", // Linux/BSD - }; } private static readonly Lazy<(WindowsVersion, Version?)?> _HostWindowsVersion = new(() => diff --git a/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs b/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs index 7f71ed88340..2205a6519c5 100644 --- a/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs +++ b/src/BizHawk.Emulation.Cores/Waterbox/WaterboxHost.cs @@ -76,7 +76,7 @@ static WaterboxHost() { NativeImpl = BizInvoker.GetInvoker( new DynamicLibraryImportResolver( - (OSTailoredCode.IsUnixHost ? "libwaterboxhost" : "waterboxhost") + OSTailoredCode.DllExtension, + DynamicLibraryImportResolver.PlatformFileName("waterboxhost"), hasLimitedLifetime: false), CallingConventionAdapters.Native); #if !DEBUG From 5165af5af5b1b65e3a75a94ae684a9701de8bf82 Mon Sep 17 00:00:00 2001 From: DarthM Date: Fri, 26 Jun 2026 18:35:10 +0800 Subject: [PATCH 06/10] macOS: auto-stage native libs after build; migrate Encore to PlatformFileName - Dist/.BuildInConfigX.sh: on macOS, run stage-macos-dylibs.sh after the build, since the build overwrites output/dll and doesn't create the native-library symlinks. - Dist/.InvokeCLIOnMainSln.sh: read MainVersion with sed instead of `grep -Po`, which is GNU-only and absent on macOS/BSD. - Encore: resolve the core via DynamicLibraryImportResolver.PlatformFileName("encore"). --- Dist/.BuildInConfigX.sh | 5 +++++ Dist/.InvokeCLIOnMainSln.sh | 2 +- src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.cs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Dist/.BuildInConfigX.sh b/Dist/.BuildInConfigX.sh index fdcafac1997..66251f46576 100755 --- a/Dist/.BuildInConfigX.sh +++ b/Dist/.BuildInConfigX.sh @@ -7,3 +7,8 @@ fi config="$1" shift Dist/.InvokeCLIOnMainSln.sh "build" "$config" "$@" +# macOS: the build overwrites output/dll and doesn't create the native-library symlinks +# (Homebrew deps + Linux-soname aliases), so (re)stage them to keep the output runnable. +if [ "$(uname -s)" = "Darwin" ] && [ -d output/dll ]; then + Dist/stage-macos-dylibs.sh output/dll +fi diff --git a/Dist/.InvokeCLIOnMainSln.sh b/Dist/.InvokeCLIOnMainSln.sh index fa09f310984..c8892ed81bc 100755 --- a/Dist/.InvokeCLIOnMainSln.sh +++ b/Dist/.InvokeCLIOnMainSln.sh @@ -10,6 +10,6 @@ config="$1" shift if [ -z "$NUGET_PACKAGES" ]; then export NUGET_PACKAGES="$HOME/.nuget/packages"; fi printf "running 'dotnet %s' in %s configuration, extra args: %s\n" "$cmd" "$config" "$*" -version=$(grep -Po "MainVersion = \"\K(.*)(?=\")" src/BizHawk.Common/VersionInfo.cs) +version=$(sed -n 's/.*MainVersion = "\([^"]*\)";.*/\1/p' src/BizHawk.Common/VersionInfo.cs) git_hash="$(git rev-parse --verify HEAD 2>/dev/null || printf "0000000000000000000000000000000000000000")" dotnet "$cmd" BizHawk.sln -c "$config" -m -clp:NoSummary -p:Version="$version" -p:SourceRevisionId="$git_hash" "$@" diff --git a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.cs b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.cs index eecb6b7a93b..04d49b1b19d 100644 --- a/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.cs +++ b/src/BizHawk.Emulation.Cores/Consoles/Nintendo/3DS/Encore.cs @@ -33,7 +33,7 @@ public sealed class CIAInstallThrowable(string message) : Exception(message); private static void ResetEncoreResolver() { _resolver?.Dispose(); - _resolver = new(OSTailoredCode.IsUnixHost ? "libencore.so" : "encore.dll", hasLimitedLifetime: true); + _resolver = new(DynamicLibraryImportResolver.PlatformFileName("encore"), hasLimitedLifetime: true); _core = BizInvoker.GetInvoker(_resolver, CallingConventionAdapters.Native); } From cfe0fd9d7b89be460022ba4b512880c00643c041 Mon Sep 17 00:00:00 2001 From: Michael G <10155689+DarthMDev@users.noreply.github.com> Date: Fri, 26 Jun 2026 06:41:56 -0400 Subject: [PATCH 07/10] Remove unnecessary import Hopefully fixing CI build --- src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs b/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs index 292fc16a80f..40ea6f98fca 100644 --- a/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs +++ b/src/BizHawk.Bizware.Input/KeyMouseInput/X11KeyMouseInput.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Runtime.InteropServices; -using BizHawk.Common; - using static BizHawk.Common.XInput2Imports; using static BizHawk.Common.XlibImports; From 0b426d5cf47b93a41788ae60b5cfddafb265aa00 Mon Sep 17 00:00:00 2001 From: DarthM Date: Fri, 26 Jun 2026 23:01:04 +0800 Subject: [PATCH 08/10] macOS: default the display method to GdiPlus XQuartz's OpenGL is Apple's legacy GLX bridge (capped at GL 2.1 and crashes under Rosetta), so default DispMethod to the GdiPlus software renderer on macOS instead of OpenGL. Avoids needing to force --gdi at launch. --- src/BizHawk.Client.Common/config/Config.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BizHawk.Client.Common/config/Config.cs b/src/BizHawk.Client.Common/config/Config.cs index 6df54b0a31a..bf80db7d275 100644 --- a/src/BizHawk.Client.Common/config/Config.cs +++ b/src/BizHawk.Client.Common/config/Config.cs @@ -264,7 +264,9 @@ public void SetWindowScaleFor(string sysID, int windowScale) public int DispPrescale { get; set; } = 1; - public EDispMethod DispMethod { get; set; } = HostCapabilityDetector.HasD3D11 ? EDispMethod.D3D11 : EDispMethod.OpenGL; + public EDispMethod DispMethod { get; set; } = HostCapabilityDetector.HasD3D11 + ? EDispMethod.D3D11 + : OSTailoredCode.CurrentOS is OSTailoredCode.DistinctOS.macOS ? EDispMethod.GdiPlus : EDispMethod.OpenGL; public int DispChromeFrameWindowed { get; set; } = 2; public bool DispChromeStatusBarWindowed { get; set; } = true; From c83346ba26331dd146e82b4e268d6d66fea0a0c5 Mon Sep 17 00:00:00 2001 From: DarthM Date: Fri, 26 Jun 2026 23:01:25 +0800 Subject: [PATCH 09/10] macOS: create the native-lib symlinks in EmuHawkMono.sh The Homebrew/XQuartz deps and the Linux-soname aliases (libX11.so.6, etc.) BizHawk's P/Invokes and Mono expect can't be shipped as symlinks (they point at the user's Homebrew install), so create them in ./dll on each launch instead, like the NixOS launch script. Doing it in the launcher also means they survive a rebuild overwriting output/dll. - Assets/EmuHawkMono.sh: add the symlink setup to the macOS branch; drop the --gdi force (the display method now defaults to GdiPlus on macOS in Config.cs). - Remove Dist/stage-macos-dylibs.sh and the macOS post-build step in Dist/.BuildInConfigX.sh (Package.sh runs in CI and can't make user-specific symlinks). - README: update the macOS deps (X11 client libs) and launch instructions. --- Assets/EmuHawkMono.sh | 50 ++++++++++++++++++++++------- Dist/.BuildInConfigX.sh | 5 --- Dist/stage-macos-dylibs.sh | 65 -------------------------------------- README.md | 6 ++-- 4 files changed, 42 insertions(+), 84 deletions(-) delete mode 100755 Dist/stage-macos-dylibs.sh diff --git a/Assets/EmuHawkMono.sh b/Assets/EmuHawkMono.sh index c5345523ff9..a415d3364b6 100755 --- a/Assets/EmuHawkMono.sh +++ b/Assets/EmuHawkMono.sh @@ -2,20 +2,48 @@ cd "$(dirname "$(realpath "$0")")" case "$(uname -s)" in Darwin) - # macOS (x86_64, natively on Intel or via Rosetta 2 on Apple silicon). Mono's WinForms is - # X11-based, so XQuartz must be running; see the macOS section in the readme. - # Native libs are in ./dll; also let the loader find the Homebrew deps under /usr/local and - # XQuartz's X11 libs under /opt/X11/lib. - export DYLD_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" - export DYLD_FALLBACK_LIBRARY_PATH="$PWD/dll:$PWD:/usr/local/lib:/opt/X11/lib:/usr/lib${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" - # Prefer the x86_64 Homebrew Mono by putting it on PATH (PATH is then respected, so e.g. a Nix - # Mono works too). Don't invoke Mono via `arch`: it's SIP-protected and strips the DYLD_* vars. + # macOS (x86_64; natively on Intel or via Rosetta 2 on Apple silicon). Mono's WinForms is + # X11-based, so XQuartz must be running; see the macOS section in the readme. The display + # method defaults to GdiPlus on macOS (in Config.cs) since XQuartz's OpenGL is unusable. + # + # BizHawk's P/Invokes and Mono load some libraries by Linux/soname-style names, and the + # Homebrew/XQuartz deps have their own versioned names. We can't ship those symlinks (they + # point at the user's Homebrew install), so (re)create them in ./dll at launch, similar to + # the NixOS launch script. They all resolve to ONE Homebrew libX11 so that the X11 clients + # (WinForms, cairo/libgdiplus, SDL) agree. + dll="$PWD/dll" + ln_dep() { [ -e "$1" ] && ln -sf "$1" "$dll/$2"; } # silently skip optional libs that aren't installed + ln_dep /usr/local/opt/lua@5.4/lib/liblua5.4.dylib liblua54.dylib + ln_dep /usr/local/opt/openal-soft/lib/libopenal.dylib libopenal.dylib + ln_dep /usr/local/opt/openal-soft/lib/libopenal.1.dylib libopenal.1.dylib + ln_dep /usr/local/opt/zstd/lib/libzstd.1.dylib libzstd.so.1 + ln_dep /usr/local/opt/sqlite/lib/libsqlite3.dylib libe_sqlite3.dylib + ln_dep /usr/local/opt/libx11/lib/libX11.6.dylib libX11.6.dylib + ln_dep /usr/local/opt/libxext/lib/libXext.6.dylib libXext.6.dylib + ln_dep /usr/local/opt/libxrender/lib/libXrender.1.dylib libXrender.1.dylib + ln_dep /usr/local/opt/libxcursor/lib/libXcursor.1.dylib libXcursor.1.dylib + ln_dep /usr/local/opt/libxinerama/lib/libXinerama.1.dylib libXinerama.1.dylib + ln_dep /usr/local/opt/libxi/lib/libXi.6.dylib libXi.6.dylib + ln_dep /usr/local/opt/libxrandr/lib/libXrandr.2.dylib libXrandr.2.dylib + ln_dep /usr/local/opt/libxtst/lib/libXtst.6.dylib libXtst.6.dylib + ln_dep /usr/local/opt/libxfixes/lib/libXfixes.3.dylib libXfixes.3.dylib + ln_dep /usr/local/opt/libxscrnsaver/lib/libXss.1.dylib libXss.1.dylib + ln_dep /usr/local/opt/libxau/lib/libXau.6.dylib libXau.6.dylib + ln_dep /usr/local/opt/libxdmcp/lib/libXdmcp.6.dylib libXdmcp.6.dylib + ln_dep /usr/local/opt/libxcb/lib/libxcb.1.dylib libxcb.1.dylib + # soname/unversioned aliases hardcoded by BizHawk's P/Invokes and Mono (resolve within ./dll) + [ -e "$dll/libX11.6.dylib" ] && { ln -sf libX11.6.dylib "$dll/libX11.dylib"; ln -sf libX11.6.dylib "$dll/libX11.so.6"; } + [ -e "$dll/libXi.6.dylib" ] && ln -sf libXi.6.dylib "$dll/libXi.so.6" + [ -e "$dll/libXfixes.3.dylib" ] && ln -sf libXfixes.3.dylib "$dll/libXfixes.so.3" + [ -e "$dll/libgdiplus.0.dylib" ] && ln -sf libgdiplus.0.dylib "$dll/libgdiplus.dylib" + # Let the loader find ./dll, the Homebrew deps under /usr/local, and XQuartz under /opt/X11. + export DYLD_LIBRARY_PATH="$dll:$PWD:/usr/local/lib:/opt/X11/lib${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}" + export DYLD_FALLBACK_LIBRARY_PATH="$dll:$PWD:/usr/local/lib:/opt/X11/lib:/usr/lib${DYLD_FALLBACK_LIBRARY_PATH:+:$DYLD_FALLBACK_LIBRARY_PATH}" + # Prefer the x86_64 Homebrew Mono via PATH (so a Nix Mono etc. is still respected). Don't run + # Mono through `arch` — it's SIP-protected and strips the DYLD_* vars. [ -d /usr/local/bin ] && export PATH="/usr/local/bin:$PATH" export MONO_MWF_MAC_FORCE_X11=1 # the default macOS WinForms driver (Carbon) is unported to 64-bit [ -z "$DISPLAY" ] && export DISPLAY=:0 - # XQuartz's OpenGL is the legacy Apple GLX bridge (GL 2.1, and crashes under Rosetta), so use - # the GdiPlus software display method. - set -- --gdi "$@" ;; *) # GNU+Linux (and other Unix) diff --git a/Dist/.BuildInConfigX.sh b/Dist/.BuildInConfigX.sh index 66251f46576..fdcafac1997 100755 --- a/Dist/.BuildInConfigX.sh +++ b/Dist/.BuildInConfigX.sh @@ -7,8 +7,3 @@ fi config="$1" shift Dist/.InvokeCLIOnMainSln.sh "build" "$config" "$@" -# macOS: the build overwrites output/dll and doesn't create the native-library symlinks -# (Homebrew deps + Linux-soname aliases), so (re)stage them to keep the output runnable. -if [ "$(uname -s)" = "Darwin" ] && [ -d output/dll ]; then - Dist/stage-macos-dylibs.sh output/dll -fi diff --git a/Dist/stage-macos-dylibs.sh b/Dist/stage-macos-dylibs.sh deleted file mode 100755 index a4cd721e6ec..00000000000 --- a/Dist/stage-macos-dylibs.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/sh -# Stage the macOS (x86_64 / Rosetta) native-library glue into the EmuHawk output dir. -# -# Run this once after building (Dist/BuildRelease.sh) and before launching with -# Assets/EmuHawkMono.sh. It symlinks the Homebrew-provided dependencies (SDL deps, -# OpenAL, Lua, zstd, SQLite, the X11 client stack) and creates the Linux-soname aliases -# (libX11.so.6, etc.) that BizHawk's P/Invokes and Mono expect, all pointing at the SAME -# Homebrew libX11 so cairo/libgdiplus/WinForms/SDL agree (see notes in EmuHawkMono.sh). -# -# The dylibs BizHawk itself builds/bundles (libSDL2, libgdiplus, libcimgui, libwaterboxhost) -# ship in Assets/dll and are copied to output/dll by the build, so they are not handled here. -# -# Prereqs (x86_64 Homebrew under Rosetta): -# brew install mono sdl2 openal-soft lua@5.4 zstd sqlite mono-libgdiplus \ -# libx11 libxext libxrender libxcursor libxinerama libxi libxrandr \ -# libxtst libxfixes libxscrnsaver libxau libxdmcp libxcb -# brew install --cask xquartz -set -e - -# Target dll dir: arg 1, else ./dll relative to cwd, else output/dll under the repo. -DLL="${1:-}" -if [ -z "$DLL" ]; then - if [ -d "dll" ]; then DLL="dll" - else DLL="$(cd "$(dirname "$0")/.." && pwd)/output/dll"; fi -fi -if [ ! -d "$DLL" ]; then printf "output dll dir not found: %s\n" "$DLL" >&2; exit 1; fi -cd "$DLL" - -opt() { echo "/usr/local/opt/$1"; } -link() { # link - [ -e "$1" ] && ln -sf "$1" "$2" || printf "WARN missing %s (for %s)\n" "$1" "$2" >&2 -} - -# Homebrew runtime deps under their canonical names -link "$(opt lua@5.4)/lib/liblua5.4.dylib" liblua54.dylib -link "$(opt openal-soft)/lib/libopenal.dylib" libopenal.dylib -link "$(opt openal-soft)/lib/libopenal.1.dylib" libopenal.1.dylib -link "$(opt zstd)/lib/libzstd.1.dylib" libzstd.1.dylib -link "$(opt sqlite)/lib/libsqlite3.dylib" libe_sqlite3.dylib -link "$(opt sqlite)/lib/libsqlite3.dylib" e_sqlite3.dylib - -# X11 client stack (all from Homebrew, so they share one libX11) -link "$(opt libx11)/lib/libX11.6.dylib" libX11.6.dylib -link "$(opt libxext)/lib/libXext.6.dylib" libXext.6.dylib -link "$(opt libxrender)/lib/libXrender.1.dylib" libXrender.1.dylib -link "$(opt libxcursor)/lib/libXcursor.1.dylib" libXcursor.1.dylib -link "$(opt libxinerama)/lib/libXinerama.1.dylib" libXinerama.1.dylib -link "$(opt libxi)/lib/libXi.6.dylib" libXi.6.dylib -link "$(opt libxrandr)/lib/libXrandr.2.dylib" libXrandr.2.dylib -link "$(opt libxtst)/lib/libXtst.6.dylib" libXtst.6.dylib -link "$(opt libxfixes)/lib/libXfixes.3.dylib" libXfixes.3.dylib -link "$(opt libxscrnsaver)/lib/libXss.1.dylib" libXss.1.dylib -link "$(opt libxau)/lib/libXau.6.dylib" libXau.6.dylib -link "$(opt libxdmcp)/lib/libXdmcp.6.dylib" libXdmcp.6.dylib -link "$(opt libxcb)/lib/libxcb.1.dylib" libxcb.1.dylib - -# Linux-soname aliases that BizHawk's P/Invokes and Mono hardcode (XlibImports etc.) -ln -sf libX11.6.dylib libX11.dylib -ln -sf libX11.6.dylib libX11.so.6 -ln -sf libXfixes.3.dylib libXfixes.so.3 -ln -sf libXi.6.dylib libXi.so.6 -ln -sf libzstd.1.dylib libzstd.so.1 -ln -sf libgdiplus.0.dylib libgdiplus.dylib - -printf "staged macOS dylib symlinks into %s\n" "$DLL" diff --git a/README.md b/README.md index 9147272819d..1c0a501e68f 100644 --- a/README.md +++ b/README.md @@ -188,11 +188,11 @@ Setup (all the brew commands must be the **x86_64** Homebrew under `/usr/local`) `arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` (prefix every command below with `arch -x86_64` and use `/usr/local/bin/brew`). 3. Install the runtime dependencies: - `brew install mono mono-libgdiplus sdl2 openal-soft lua@5.4 zstd sqlite` + `brew install mono mono-libgdiplus sdl2 openal-soft lua@5.4 zstd sqlite libx11 libxext libxrender libxcursor libxinerama libxi libxrandr libxtst libxfixes libxscrnsaver libxau libxdmcp libxcb` `brew install --cask xquartz` (then log out/in so the X server is registered) -4. Get a build: either download the macOS dev build, or build from source (see [*Building*](#building)) and then run `Dist/stage-macos-dylibs.sh` to link the dependencies into `output/dll`. +4. Get a build: either download the macOS dev build, or build from source (see [*Building*](#building)). -Run `EmuHawkMono.sh` to start EmuHawk (it forces the X11 WinForms driver and GdiPlus video, which are required on macOS). **XQuartz must be running.** It takes the same command-line arguments as on Windows: see [*Passing command-line arguments*](#passing-command-line-arguments), e.g. `./EmuHawkMono.sh --lua=/path/to/script.lua /path/to/rom.nds`. +Run `EmuHawkMono.sh` to start EmuHawk. **XQuartz must be running.** The script forces the X11 WinForms driver and symlinks the Homebrew/XQuartz dependencies into `dll/` on each launch; the display method defaults to GdiPlus (software) on macOS. It takes the same command-line arguments as on Windows: see [*Passing command-line arguments*](#passing-command-line-arguments), e.g. `./EmuHawkMono.sh --lua=/path/to/script.lua /path/to/rom.nds`. What works: most non-GL cores including **Game Boy/Color (Gambatte)**, **GBA (mGBA)**, and **Nintendo DS (melonDS)**, plus Lua scripting. Cores that require host OpenGL fall back to their software renderers. N64 and other GL-only paths are not expected to work. From 721fd97c149d0e01d42fc0bf3e116643214c8751 Mon Sep 17 00:00:00 2001 From: DarthM Date: Fri, 26 Jun 2026 23:33:00 +0800 Subject: [PATCH 10/10] macOS: export MONO_GC_PARAMS with nursery-size=256m to avoid Rosetta 2 GC deadlocks --- Assets/EmuHawkMono.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/Assets/EmuHawkMono.sh b/Assets/EmuHawkMono.sh index a415d3364b6..edbded5d207 100755 --- a/Assets/EmuHawkMono.sh +++ b/Assets/EmuHawkMono.sh @@ -44,6 +44,7 @@ Darwin) [ -d /usr/local/bin ] && export PATH="/usr/local/bin:$PATH" export MONO_MWF_MAC_FORCE_X11=1 # the default macOS WinForms driver (Carbon) is unported to 64-bit [ -z "$DISPLAY" ] && export DISPLAY=:0 + export MONO_GC_PARAMS="nursery-size=256m${MONO_GC_PARAMS:+,}$MONO_GC_PARAMS" ;; *) # GNU+Linux (and other Unix)