From 62b21054584ec539e1a59bbfc6747e3ebd02370b Mon Sep 17 00:00:00 2001 From: "Julio C. Rocha" Date: Thu, 17 Sep 2026 00:35:30 -0700 Subject: [PATCH] Implement macOS (Intel) support --- CMakeLists.txt | 123 +++++++++++++++++++++++++++++++--- README.md | 10 +-- docs/building.md | 22 ++++-- docs/dev/findings.md | 48 +++++++++++++ docs/porting-notes.md | 24 +++++++ port/fast3d/gfx_sdl2.cpp | 7 ++ port/include/pc_protos.h | 11 +++ port/shim/PR/os.h | 12 ++++ port/src/crash.c | 22 ++++-- port/src/dram.c | 17 ++++- port/src/libultra.c | 2 +- port/src/macho_weak_aliases.s | 25 +++++++ port/src/romdata.c | 11 ++- port/src/video.c | 8 ++- scripts/gen_macho_syms.py | 44 ++++++++++++ scripts/strip_weak_pragmas.py | 25 +++++++ 16 files changed, 376 insertions(+), 35 deletions(-) create mode 100644 port/src/macho_weak_aliases.s create mode 100644 scripts/gen_macho_syms.py create mode 100644 scripts/strip_weak_pragmas.py diff --git a/CMakeLists.txt b/CMakeLists.txt index fe0c9500..1d481989 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,11 +25,26 @@ list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) # name as an inherited one — CCTVRecord's `pad` — see docs/internals.md.) # An explicit -DCMAKE_C_COMPILER=... always wins. if(NOT DEFINED CMAKE_C_COMPILER) - find_program(_GE007_GCC NAMES gcc) + if(CMAKE_HOST_APPLE) + # Homebrew installs real GNU GCC with a version suffix; /usr/bin/gcc is + # Apple Clang and does not support the Plan 9 struct extensions required + # by the decomp's `inherits` pattern. + find_program(_GE007_GCC + NAMES gcc-16 gcc-15 gcc-14 gcc-13 gcc-12 gcc-11 gcc) + else() + find_program(_GE007_GCC NAMES gcc) + endif() if(_GE007_GCC) set(CMAKE_C_COMPILER "${_GE007_GCC}" CACHE FILEPATH "C compiler" FORCE) get_filename_component(_GE007_GCC_DIR "${_GE007_GCC}" DIRECTORY) - find_program(_GE007_GXX NAMES g++ PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + if(CMAKE_HOST_APPLE) + find_program(_GE007_GXX + NAMES g++-16 g++-15 g++-14 g++-13 g++-12 g++-11 g++ + PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + else() + find_program(_GE007_GXX NAMES g++ + PATHS "${_GE007_GCC_DIR}" NO_DEFAULT_PATH) + endif() if(_GE007_GXX) set(CMAKE_CXX_COMPILER "${_GE007_GXX}" CACHE FILEPATH "CXX compiler" FORCE) endif() @@ -163,7 +178,18 @@ configure_file( # the toolchain include dir from the compiler location (MinGW/MSYS2 layout: # bin/../include) and generate an absolute-path include. get_filename_component(_PORT_GCC_DIR "${CMAKE_C_COMPILER}" DIRECTORY) -get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE) +if(APPLE) + execute_process( + COMMAND xcrun --show-sdk-path + OUTPUT_VARIABLE _PORT_MACOS_SDK OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(NOT _PORT_MACOS_SDK) + message(FATAL_ERROR "Cannot locate the macOS SDK; install the Xcode Command Line Tools") + endif() + set(PORT_HOST_STRING_H "${_PORT_MACOS_SDK}/usr/include/string.h") +else() + get_filename_component(PORT_HOST_STRING_H "${_PORT_GCC_DIR}/../include/string.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststring.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststring.h" @ONLY) @@ -172,7 +198,11 @@ configure_file( # the include path and shadows the system header; libstdc++'s /gthr # headers in the fast3d C++ TUs need the real (cpu_set_t, # sched_yield). Both MinGW and Linux keep at bin/../include. -get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE) +if(APPLE) + set(PORT_HOST_SCHED_H "${_PORT_MACOS_SDK}/usr/include/sched.h") +else() + get_filename_component(PORT_HOST_SCHED_H "${_PORT_GCC_DIR}/../include/sched.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hostsched.h.in" "${CMAKE_BINARY_DIR}/port/include/hostsched.h" @ONLY) @@ -182,7 +212,11 @@ configure_file( # one C++ TU (fast3d): MinGW's own sec_api/stdlib_s.h does `#include # `, which resolves to the N64 header via -I. The shim routes C++ # TUs to the host header by absolute path (same trick as hoststring.h). -get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE) +if(APPLE) + set(PORT_HOST_STDLIB_H "${_PORT_MACOS_SDK}/usr/include/stdlib.h") +else() + get_filename_component(PORT_HOST_STDLIB_H "${_PORT_GCC_DIR}/../include/stdlib.h" ABSOLUTE) +endif() configure_file( "${CMAKE_SOURCE_DIR}/port/include/hoststdlib.h.in" "${CMAKE_BINARY_DIR}/port/include/hoststdlib.h" @ONLY) @@ -328,6 +362,13 @@ elseif(UNIX AND NOT APPLE) # relocations are fine (unlike a 0x140000000 base, which needs mcmodel=large). # This is the ELF analogue of the Windows build's fixed 0x140000000 base. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-Ttext-segment=0x20000000") +elseif(APPLE) + # Mach-O executables normally reserve [0, 4 GiB) as __PAGEZERO. The port + # needs fixed mappings at 0x10000000 (ROM), 0x70000000 (s32-safe DRAM), and + # 0x80000000 (KSEG0 mirror), so retain null/low-pointer protection only up + # while leaving ample space before the first required mapping. + set(CMAKE_EXE_LINKER_FLAGS + "${CMAKE_EXE_LINKER_FLAGS} -Wl,-pagezero_size,0x10000") endif() # port/shim first: PC-only shadow headers (bondtypes.h, bondconstants.h, @@ -342,6 +383,7 @@ include_directories( "${CMAKE_SOURCE_DIR}/src/game" "${CMAKE_SOURCE_DIR}/src/libultra" "${CMAKE_SOURCE_DIR}/src/libultra/audio" + "${CMAKE_SOURCE_DIR}/src/libultra/gu" "${CMAKE_SOURCE_DIR}/port/include" "${CMAKE_BINARY_DIR}/port/include" ) @@ -395,6 +437,8 @@ if(NOT EXTRA_LIBRARIES) set(EXTRA_LIBRARIES dbghelp -static-libstdc++ pthread) elseif(WIN32) set(EXTRA_LIBRARIES dbghelp -static-libstdc++) + elseif(APPLE) + set(EXTRA_LIBRARIES stdc++ m pthread) else() set(EXTRA_LIBRARIES stdc++ m dl pthread) endif() @@ -414,14 +458,41 @@ file(GLOB_RECURSE SRC_FAST3D "${CMAKE_SOURCE_DIR}/port/fast3d/*.cpp" # scripts/gen_romassets.py). Defines every obseg/ramrom/music label and the # ge007.ld segment markers as 0x10000000 + rom_offset, so `&symbol` / romCopy() # work once romdata.c maps the .z64 at 0x10000000. -set(SRC_ROMASSETS "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s") -if(NOT EXISTS ${SRC_ROMASSETS}) - message(FATAL_ERROR "Missing ${SRC_ROMASSETS} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}") +set(SRC_ROMASSETS_INPUT "${CMAKE_SOURCE_DIR}/port/src/romassets_${ASSET_REGION}.s") +if(NOT EXISTS ${SRC_ROMASSETS_INPUT}) + message(FATAL_ERROR "Missing ${SRC_ROMASSETS_INPUT} — run: python3 scripts/gen_romassets.py ${ASSET_REGION}") endif() # Absolute symbols inside the reserved N64-DRAM region (port/src/dram.c): # cfb_16 (replaces src/cfb.c) and _bssSegmentEnd (mempool start marker). -set(SRC_DRAM_SYMS "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s") +set(SRC_DRAM_SYMS_INPUT "${CMAKE_SOURCE_DIR}/port/src/dram_syms.s") + +if(APPLE) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + set(SRC_ROMASSETS "${CMAKE_BINARY_DIR}/port/src/romassets_${ASSET_REGION}_macho.s") + set(SRC_DRAM_SYMS "${CMAKE_BINARY_DIR}/port/src/dram_syms_macho.s") + add_custom_command( + OUTPUT "${SRC_ROMASSETS}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + "${SRC_ROMASSETS_INPUT}" "${SRC_ROMASSETS}" + DEPENDS "${SRC_ROMASSETS_INPUT}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + VERBATIM) + add_custom_command( + OUTPUT "${SRC_DRAM_SYMS}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + "${SRC_DRAM_SYMS_INPUT}" "${SRC_DRAM_SYMS}" + DEPENDS "${SRC_DRAM_SYMS_INPUT}" + "${CMAKE_SOURCE_DIR}/scripts/gen_macho_syms.py" + VERBATIM) + set_source_files_properties(${SRC_ROMASSETS} ${SRC_DRAM_SYMS} + PROPERTIES GENERATED TRUE) +else() + set(SRC_ROMASSETS "${SRC_ROMASSETS_INPUT}") + set(SRC_DRAM_SYMS "${SRC_DRAM_SYMS_INPUT}") +endif() # Game code — compiled unmodified. file(GLOB_RECURSE SRC_GAME "${CMAKE_SOURCE_DIR}/src/game/*.c") @@ -452,6 +523,39 @@ file(GLOB SRC_LIBULTRARE_AUDIO "${CMAKE_SOURCE_DIR}/src/libultrare/audio/*.c") # gu matrix helpers. file(GLOB SRC_GU "${CMAKE_SOURCE_DIR}/src/libultra/gu/*.c") +# GCC for Darwin crashes while lowering the decomp's ELF-style +# `#pragma weak alias = target` directives. Compile generated copies with +# those directives removed, then provide their aliases in Mach-O assembly. +set(SRC_MACHO_ALIASES "") +if(APPLE) + set(_MACHO_GENERATED_SOURCES "") + set(_MACHO_WEAK_INPUTS + "${CMAKE_SOURCE_DIR}/src/game/lv.c" + "${CMAKE_SOURCE_DIR}/src/game/objective_status.c" + "${CMAKE_SOURCE_DIR}/src/game/spectrum.c" + "${CMAKE_SOURCE_DIR}/src/libultra/gu/sinf.c" + "${CMAKE_SOURCE_DIR}/src/libultra/gu/cosf.c") + foreach(_INPUT IN LISTS _MACHO_WEAK_INPUTS) + get_filename_component(_NAME "${_INPUT}" NAME) + set(_OUTPUT "${CMAKE_BINARY_DIR}/port/src/macho_${_NAME}") + add_custom_command( + OUTPUT "${_OUTPUT}" + COMMAND "${Python3_EXECUTABLE}" + "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py" + "${_INPUT}" "${_OUTPUT}" + DEPENDS "${_INPUT}" "${CMAKE_SOURCE_DIR}/scripts/strip_weak_pragmas.py" + VERBATIM) + list(APPEND SRC_MACHO_ALIASES "${_OUTPUT}") + list(APPEND _MACHO_GENERATED_SOURCES "${_OUTPUT}") + list(REMOVE_ITEM SRC_GAME "${_INPUT}") + list(REMOVE_ITEM SRC_GU "${_INPUT}") + endforeach() + list(APPEND SRC_MACHO_ALIASES + "${CMAKE_SOURCE_DIR}/port/src/macho_weak_aliases.s") + set_source_files_properties(${_MACHO_GENERATED_SOURCES} + PROPERTIES GENERATED TRUE) +endif() + # VI mode table (DATA, not a driver): fr.c references osViModeTable ~15x. # This is the only libultra/io file we compile — the rest are hardware # drivers that get shimmed in port/src/libultra.c. @@ -569,6 +673,7 @@ set(SRC ${SRC_LIBULTRARE_LIBC} ${SRC_ASSETS} ${SRC_GU} + ${SRC_MACHO_ALIASES} ${SRC_ENGINE} ) diff --git a/README.md b/README.md index 13d0201f..6d066f48 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ listed plainly there. **v0.2.2 - playable, with known rough edges.** The full single-player campaign is completable end to end (all 21 missions, Agent difficulty, playtested), at a steady 60 fps; all 21 solo missions load, render and run -crash-free, verified on Windows, Linux and real Steam Deck hardware. Feedback -is very welcome. +crash-free, verified on Windows, Linux and real Steam Deck hardware. Native +Intel macOS builds are supported. Feedback is very welcome. **Working:** boot sequence and front end (menu → mission select → briefing → start), front-end menu navigation on the left stick to match the F10 overlay @@ -87,7 +87,7 @@ dual-stick controller layout; file-backed saves; faithful N64 progression by def (F10 → *All unlocked* opens every level, 007 mode and the full cheat menu); F10 in-game options overlay (resolution, frame cap, MSAA, filtering, FOV, sensitivity, quit to desktop); -Windows and Linux. +Windows, Linux, and Intel macOS. **Known issues:** @@ -130,7 +130,7 @@ Windows and Linux. capture full faulting registers in `ge007.crash.log`. - Assorted further cosmetic defects are tracked in [`docs/dev/GRAPHICS-BACKLOG.md`](docs/dev/GRAPHICS-BACKLOG.md). -- No macOS or ARM support; no controller rebinding UI. +- No ARM support; no controller rebinding UI. Root causes and fix status for every item: the [release notes](https://github.com/jkdansereau/goldeneye-pc-port/releases) and the finding log in [`docs/dev/findings.md`](docs/dev/findings.md). @@ -173,7 +173,7 @@ directionally, on the way to v1.0: - **PAL and JP ROM support** ([issue #85](https://github.com/jkdansereau/goldeneye-pc-port/issues/85)); NTSC-U is the only supported region today. - **Real widescreen** (properly expanding the field of view at 16:9, rather than today's 4:3-stretch). -- **Controller rebinding UI**, and macOS/ARM builds. +- **Controller rebinding UI**, and ARM builds. - **LAN multiplayer**: reviving GoldenEye's original split-screen/deathmatch netplay across multiple PCs on a local network. Genuinely under consideration, but early and not started; no ETA. diff --git a/docs/building.md b/docs/building.md index 5fdf93b4..36e30d36 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,6 +1,6 @@ --- title: Building -description: Full build and asset-extraction guide for the GoldenEye 007 PC port, covering Windows (MSYS2) and Linux. +description: Full build and asset-extraction guide for the GoldenEye 007 PC port on Windows, Linux, and macOS. --- ## Building the PC port @@ -27,21 +27,29 @@ for accepted versions and hashes. ### Port build -> **The Windows (MSYS2 MINGW64) path is the primary one.** The Linux build is -> compiled by CI on every push and ships in the release bundle (also -> playtested on Steam Deck hardware); the macOS column is best-effort -> guidance and has never been built or run there. Expect to fix build breaks -> yourself on untested platforms. +> **The Windows (MSYS2 MINGW64) path is the primary one.** Linux is compiled +> by CI and ships in the release bundle. Native Intel macOS builds are also +> supported; Apple Silicon remains untested. | Need | Windows (MSYS2 MINGW64) | Debian/Ubuntu | macOS (Homebrew) | |------|------------------------|---------------|------------------| -| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT / `gcc` | +| toolchain | `mingw-w64-x86_64-toolchain` | `build-essential` | Xcode CLT + Homebrew `gcc` | | CMake | `mingw-w64-x86_64-cmake` | `cmake` | `cmake` | | SDL2 | `mingw-w64-x86_64-SDL2` | `libsdl2-dev` | `sdl2` | | zlib | `mingw-w64-x86_64-zlib` | `zlib1g-dev` | `zlib` | | OpenGL | (in the toolchain) | `libgl1-mesa-dev` | (system) | | Python 3 | `mingw-w64-x86_64-python` | `python3` | `python3` | +On macOS, install the build dependencies with: + +```sh +brew install cmake gcc sdl2 zlib python3 +``` + +The project requires real GNU GCC for the decomp's Plan 9 struct extensions. +Apple's `/usr/bin/gcc` is Clang and is not compatible. CMake automatically +selects Homebrew's versioned `gcc-N`/`g++-N` executables. + ### Asset extraction (decompilation toolchain) The extraction scripts need `binutils-mips-linux-gnu` (or an equivalent MIPS diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 9f1f8e30..26b8267d 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -561,6 +561,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D258 | **PAL/JP asset conversion is broken at the source-data level — `scripts/filelist.e.csv` and `filelist.j.csv` carry naming defects, so `gen_romassets.py e/j` (and…** — full `## D258` entry at file tail | OPEN — repair path verified, not applied (out of v0.2.0 scope). | | D285 | **Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: the `sndHandleEvent` preempt-scan walks the active-sound list unlocked (M-140; fixed M-141).** — full `## D285` entry at file tail | **FIXED (M-141, `src/snd.c`)** — `osSetIntMask` lock added around the preempt-scan, matching `sndSetupSound`'s existing unconditional (no `#ifdef PORT`) pattern in the same file; build green, `-level_09`/`-level_20` unregressed (1400+ frames, no crash, no D152 lock-stuck warnings). Not yet gdb/live-confirmed against a real reproduced crash — see M-141 body. Do not conflate with D255 — different thread, different subsystem. | | D293 | **QoL ask: no discoverable way to quit the game from the UI (M-141, Steam Deck user playtest — "couldn't see a way to exit the game. Maybe build this into the…** — full `## D293` entry at file tail | FIXED (M-141) — "Quit to desktop" row added to the F10 overlay; build-verified Win+Linux, not yet live-confirmed.… | +| D294 | **Native Intel macOS build support.** Apple Clang lacks the required Plan 9 struct extensions; Darwin also needs SDK header discovery, Mach-O absolute/weak-alias symbol spelling, POSIX shared-memory backing instead of Linux `memfd_create`, and Darwin signal-context handling. | **FIXED** — CMake selects versioned Homebrew GNU GCC, generates Mach-O assembly/source adapters in the build tree, and the port layer now handles Darwin libc macros, shared memory, and crash diagnostics. Fresh `build-pc.sh ntsc-final` build and `--version` smoke test pass on x86_64 macOS 26.6.2. | Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS threads, compiled GE's real `src/sched.c`, and brought in PD's fast3d software @@ -10149,3 +10150,50 @@ do { ## D293 — QoL ask (M-141, Steam Deck user playtest): no discoverable way to quit the game from the UI on a controller-only setup **Context.** Deck/controller-only setups had only window-close/Alt+F4 to quit; no in-UI affordance. **FIXED.** Added a `ROW_ACTION` row kind (not config-backed, same pattern as the existing `__Resolution` row) to the F10 options overlay ("Quit to desktop") that triggers the identical `configSave()`+`exit(0)` path `SDL_QUIT` already uses. Build-verified on Windows and Linux; `verify.sh dam` PASS, no regression. + +## D294 — Native Intel macOS build support +**Context.** The CMake file contained nominal Apple branches, but had never +completed a Darwin build. `/usr/bin/gcc` is Apple Clang and rejects the +decomp's required `-fplan9-extensions`; host-header paths assumed MinGW/Linux; +ELF-style absolute symbols and weak aliases are invalid in Mach-O; the DRAM +mirror used Linux-only `memfd_create`; and crash diagnostics only implemented +Windows/Linux. + +**FIXED.** CMake now selects Homebrew's versioned GNU `gcc-N`/`g++-N`, locates +libc headers through the macOS SDK, omits Linux-only `-ldl`, and generates +Mach-O-spelled copies of the ROM/DRAM absolute symbols. Five sources containing +ELF weak-alias pragmas are copied into the build tree with only those pragmas +removed; a Mach-O assembly file supplies equivalent indirect symbols. Darwin +fortify/`errno` macros are suppressed while legacy declarations parse. +`dramReserve` uses an immediately-unlinked POSIX shared-memory object, and the +POSIX crash handler supports Darwin's x86-64 signal context. + +**Verified.** On x86_64 macOS 26.6.2 with Homebrew GCC 16.2.0, both an +incremental native build and a fresh build through `build-pc.sh` completed at +100%. `file` identifies the output as a Mach-O 64-bit x86_64 executable, +`otool -L` resolves SDL2, zlib, libstdc++, Cocoa, and OpenGL, and +`ge007.x86_64 --version` reports `x86_64-osx`. Runtime gameplay was not +exercised because ROM-derived user data is not part of build verification. +Apple Silicon remains unsupported/untested. + +**Runtime follow-up.** The first real launch exposed a link-time/runtime gap: +Darwin's default Mach-O `__PAGEZERO` segment reserved `[0, 0x100000000)`. +Consequently both the cartridge mapping at `0x10000000` and the mirrored DRAM +mapping at `0x70000000`/`0x80000000` failed despite a successful build. +The Apple link now sets `-pagezero_size 0x10000`, preserving null-page +protection while leaving all three N64 address windows available. Darwin also +lacks Linux's `MAP_FIXED_NOREPLACE`; the ROM mapping +therefore uses `MAP_FIXED` after the linker has guaranteed the cartridge range +is vacant. Without it, `mmap` relocated the cartridge despite the now-free +range, boot fell back to a heap copy, and direct absolute ROM-symbol reads +crashed during level initialization. With fixed ROM mapping working, the next +launch reached rendering and exposed AppKit's main-thread-only event rule: +fast3d polled SDL from the scheduler/render thread even though the port's host +main loop already owns a complete event pump. The fast3d pump is now disabled +only on macOS; `videoPumpEvents()` continues to process input/window events. +After generating all three required sidecar sets from the user's NTSC ROM, +`-level_09` ran for more than 1,500 rendered frames with the ROM fixed at +`0x10000000`, both DRAM views active, all 512 model and 73 stage sidecars +patched, and no crash. The missing packed-vertex OpenGL entry-point messages +are non-fatal capability noise from the GL loader; rendering proceeds on the +created macOS OpenGL 4.1 core context. diff --git a/docs/porting-notes.md b/docs/porting-notes.md index 729ef510..8e63160b 100644 --- a/docs/porting-notes.md +++ b/docs/porting-notes.md @@ -485,6 +485,30 @@ through a converter or a runtime bswap fixup reads scrambled. ## C2. Port-layer / SDL shims +- **Darwin assembly uses Mach-O symbol spelling.** GAS absolute symbols that + are `.global foo` / `.set foo, address` on ELF/PE must become `.globl _foo` + / `.set _foo, address`; Darwin also rejects the ELF `.section .data` + spelling used by these symbol-only files. Generate transformed build-tree + copies rather than changing the cross-platform source files (D294). +- **Darwin GCC cannot lower ELF-style `#pragma weak alias = target`.** It can + ICE in `assemble_alias`; compile generated copies with those pragmas removed + and provide Mach-O indirect symbols (`.set _alias, _target`) separately + (D294). +- **Mach-O's default `__PAGEZERO` reserves the entire low 4 GiB.** A build + can compile and link successfully yet fail every fixed N64 mapping at + runtime (`0x10000000`, `0x70000000`, `0x80000000`). Link with + `-Wl,-pagezero_size,0x10000` so null-page pointers remain protected while + the cartridge and DRAM ranges are available (D294). +- **Darwin has no `MAP_FIXED_NOREPLACE`; a fixed-address hint can relocate.** + Once `__PAGEZERO` is deliberately bounded below the cart range, use + `MAP_FIXED` for the `0x10000000` cartridge mapping. Retaining the advisory + hint silently falls back to a heap ROM and later crashes on direct absolute + ROM-symbol reads (D294). +- **AppKit event polling is main-thread-only.** This port has both a host-main + `videoPumpEvents()` loop and fast3d's render-thread event pump. The latter + must be disabled on macOS or `SDL_PollEvent` raises + `NSInternalInconsistencyException`; the main-thread loop already handles + the complete event set (D294). - `#include ` in a port `.c`/`.h` that also sees `` breaks: `OSContStatus`/`OSContPad` have a `u8 errno;` field vs errno.h's macro. libultra.c wraps the include in `#pragma push_macro("errno")` / diff --git a/port/fast3d/gfx_sdl2.cpp b/port/fast3d/gfx_sdl2.cpp index a43552a7..a61d95fb 100644 --- a/port/fast3d/gfx_sdl2.cpp +++ b/port/fast3d/gfx_sdl2.cpp @@ -309,6 +309,12 @@ static void gfx_sdl_get_dimensions(uint32_t* width, uint32_t* height, int32_t* p } static void gfx_sdl_handle_events(void) { +#if defined(__APPLE__) + /* AppKit requires event polling on the process main thread. The host + * loop in videoPumpEvents owns the complete SDL queue, so the scheduler + * render thread must not call SDL_PollEvent on macOS. */ + return; +#else SDL_Event event; while (SDL_PollEvent(&event)) { switch (event.type) { @@ -357,6 +363,7 @@ static void gfx_sdl_handle_events(void) { break; } } +#endif } /* The window/context are created on the host main thread, but the game's diff --git a/port/include/pc_protos.h b/port/include/pc_protos.h index 6d4096e8..26ce1745 100644 --- a/port/include/pc_protos.h +++ b/port/include/pc_protos.h @@ -28,9 +28,20 @@ * "using typedef-name after 'struct'"), and no C++ TU has an implicit * declaration needing a fix here. C++ TUs keep their pre-D38 header exposure. */ + #ifndef _PC_PROTOS_H_ #define _PC_PROTOS_H_ +#if defined(__APPLE__) +/* Darwin SDK headers expose these libc calls as fortify function-like + * macros. This legacy declaration list needs their actual symbol names. */ +#undef memcpy +#undef snprintf +#undef strcat +#undef strcpy +#undef strncpy +#endif + #if defined(PORT) && defined(__x86_64__) && !defined(__cplusplus) #include /* u8..s32, f32, size_t (host on PC) */ diff --git a/port/shim/PR/os.h b/port/shim/PR/os.h index 5378e3ec..56d01a6c 100644 --- a/port/shim/PR/os.h +++ b/port/shim/PR/os.h @@ -28,7 +28,19 @@ #define _PORT_SHIM_OS_H_ #if defined(PORT) +# if defined(__APPLE__) +/* Darwin exposes errno and sprintf as function-like macros. Suppress them + * while parsing the N64 API's errno fields and sprintf declaration. */ +# pragma push_macro("errno") +# pragma push_macro("sprintf") +# undef errno +# undef sprintf +# endif # include "include/PR/os.h" +# if defined(__APPLE__) +# pragma pop_macro("sprintf") +# pragma pop_macro("errno") +# endif # undef OS_K0_TO_PHYSICAL # define OS_K0_TO_PHYSICAL(x) ((u32)((char *)(x) - 0x70000000)) diff --git a/port/src/crash.c b/port/src/crash.c index 9cfab783..136efec3 100644 --- a/port/src/crash.c +++ b/port/src/crash.c @@ -11,6 +11,12 @@ #ifndef _GNU_SOURCE #define _GNU_SOURCE 1 #endif +#if defined(__APPLE__) && !defined(_XOPEN_SOURCE) +#define _XOPEN_SOURCE 700 +#endif +#if defined(__APPLE__) && !defined(_DARWIN_C_SOURCE) +#define _DARWIN_C_SOURCE 1 +#endif #include #include @@ -303,7 +309,7 @@ static LONG __stdcall crashHandler(PEXCEPTION_POINTERS exinfo) return EXCEPTION_CONTINUE_EXECUTION; } -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) #include #include @@ -341,7 +347,7 @@ static void crashStackTrace(char *msg, int sig, void *pc, ucontext_t *ucontext, if (siginfo && (sig == SIGSEGV || sig == SIGBUS)) { CRASH_MSG("FAULT ADDR: %p\n", (void *)siginfo->si_addr); } -#if defined(PLATFORM_X86_64) +#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64) if (ucontext) { CRASH_MSG("REGS: Rax=%p Rcx=%p Rdx=%p Rsi=%p Rdi=%p R8=%p R9=%p R10=%p R11=%p\n", (void *)ucontext->uc_mcontext.gregs[REG_RAX], @@ -410,10 +416,12 @@ static void crashHandler(int sig, siginfo_t *siginfo, void *ctx) ucontext_t *ucontext = ctx ? (ucontext_t *)ctx : NULL; void *pc = NULL; if (ucontext) { -#ifdef PLATFORM_X86 +#if defined(PLATFORM_LINUX) && defined(PLATFORM_X86) pc = (void *)ucontext->uc_mcontext.gregs[REG_EIP]; -#elif defined(PLATFORM_X86_64) +#elif defined(PLATFORM_LINUX) && defined(PLATFORM_X86_64) pc = (void *)ucontext->uc_mcontext.gregs[REG_RIP]; +#elif defined(PLATFORM_MACOS) && defined(PLATFORM_X86_64) + pc = (void *)ucontext->uc_mcontext->__ss.__rip; #endif } @@ -470,7 +478,7 @@ void crashDumpThreads(const unsigned long *tids, const char **names, int count) } } -#endif /* PLATFORM_WINDOWS / PLATFORM_LINUX */ +#endif /* PLATFORM_WINDOWS / POSIX */ int g_CrashEnabled = 0; @@ -480,7 +488,7 @@ void crashInit(void) SetErrorMode(SEM_FAILCRITICALERRORS); prevExFilter = SetUnhandledExceptionFilter(crashHandler); g_CrashEnabled = 1; -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) struct sigaction sigact = { 0 }; sigact.sa_flags = SA_SIGINFO | SA_ONSTACK; sigact.sa_sigaction = crashHandler; @@ -501,7 +509,7 @@ void crashShutdown(void) if (prevExFilter) { SetUnhandledExceptionFilter(prevExFilter); } -#elif defined(PLATFORM_LINUX) +#elif defined(PLATFORM_LINUX) || defined(PLATFORM_MACOS) sigaction(SIGSEGV, &prevSigAction, NULL); sigaction(SIGABRT, &prevSigAction, NULL); sigaction(SIGBUS, &prevSigAction, NULL); diff --git a/port/src/dram.c b/port/src/dram.c index fef2b6cd..fadf463e 100644 --- a/port/src/dram.c +++ b/port/src/dram.c @@ -53,7 +53,10 @@ #include #else #define _GNU_SOURCE /* memfd_create — must precede all system headers */ +#include +#include #include +#include #include #endif @@ -101,10 +104,20 @@ void *dramReserve(void) CloseHandle(hSec); return v1; #else - /* memfd + two MAP_SHARED mmaps = one backing store, two views. */ + /* One shared file descriptor + two MAP_SHARED mmaps = one backing store, + * two views. Linux can keep it fully anonymous with memfd; Darwin uses a + * POSIX shared-memory object and unlinks its name immediately. */ +#if defined(PLATFORM_MACOS) + char shmName[64]; + snprintf(shmName, sizeof(shmName), "/ge007_dram_%ld", (long)getpid()); + int fd = shm_open(shmName, O_RDWR | O_CREAT, 0600); + if (fd >= 0) + shm_unlink(shmName); +#else int fd = memfd_create("ge007_dram", 0); +#endif if (fd < 0) { - sysFatalError("dram: memfd_create failed"); + sysFatalError("dram: could not create shared backing store"); } if (ftruncate(fd, DRAM_SIZE) != 0) { sysFatalError("dram: ftruncate failed"); diff --git a/port/src/libultra.c b/port/src/libultra.c index 60991a04..215a0d46 100644 --- a/port/src/libultra.c +++ b/port/src/libultra.c @@ -972,7 +972,7 @@ static void piServiceDma(s32 direction, u32 srcPA, void *dstVA, u32 size) char win[1200] = ""; char *wp = win; for (int i = 0; i < 32; i++) { - wp += snprintf(wp, win + sizeof(win) - (wp - win), + wp += snprintf(wp, sizeof(win) - (size_t)(wp - win), " %p", (void *)sp[i]); } #if defined(PLATFORM_WINDOWS) diff --git a/port/src/macho_weak_aliases.s b/port/src/macho_weak_aliases.s new file mode 100644 index 00000000..f68f862b --- /dev/null +++ b/port/src/macho_weak_aliases.s @@ -0,0 +1,25 @@ +/* Mach-O equivalents of the decomp's `#pragma weak alias = target`. */ + +.globl _g_DebugPortalsInputBufferSource1 +.set _g_DebugPortalsInputBufferSource1, _g_DebugPortalsInputBuffer1 +.globl _g_DebugPortalsInputBufferSource2 +.set _g_DebugPortalsInputBufferSource2, _g_DebugPortalsInputBuffer2 +.globl _g_DebugPortalsInputBufferSource3 +.set _g_DebugPortalsInputBufferSource3, _g_DebugPortalsInputBuffer3 +.globl _g_DebugPortalsInputBufferSource4 +.set _g_DebugPortalsInputBufferSource4, _g_DebugPortalsInputBuffer4 + +.globl _objectiveGetStatus_WEAK +.set _objectiveGetStatus_WEAK, _get_status_of_objective + +.globl _spec_keyboard_row_caps_z_x_c_v +.set _spec_keyboard_row_caps_z_x_c_v, _spec_keyboard_buffer + +.globl _fsin +.set _fsin, ___sinf +.globl _sinf +.set _sinf, ___sinf +.globl _fcos +.set _fcos, ___cosf +.globl _cosf +.set _cosf, ___cosf diff --git a/port/src/romdata.c b/port/src/romdata.c index d5012af6..785f2d35 100644 --- a/port/src/romdata.c +++ b/port/src/romdata.c @@ -36,6 +36,7 @@ extern int snprintf(char *str, size_t maxsize, const char *format, ...); /* POSIX: mmap the ROM at the fixed cart address (mirrors the VirtualAlloc * path). is a host header the decomp include path does not * shadow. */ + #include #include #endif @@ -382,9 +383,15 @@ int romdataInit(void) int flags = MAP_PRIVATE | MAP_ANONYMOUS; #ifdef MAP_FIXED_NOREPLACE flags |= MAP_FIXED_NOREPLACE; +#elif defined(PLATFORM_MACOS) + /* __PAGEZERO ends at CART_BASE in the macOS build, so this range + * is deliberately vacant. Darwin has no MAP_FIXED_NOREPLACE and + * otherwise treats CART_BASE only as a hint, relocating it. */ + flags |= MAP_FIXED; #endif void *at = mmap((void *)(uintptr_t)CART_BASE, maplen, PROT_READ | PROT_WRITE, flags, -1, 0); + int mapErrno = errno; if (at != MAP_FAILED && at == (void *)(uintptr_t)CART_BASE) { mappedLen = maplen; return romdataFinishCartMap(tok, img, sideTotal, cgTotal); @@ -392,8 +399,8 @@ int romdataInit(void) if (at != MAP_FAILED) munmap(at, maplen); sysLogPrintf(LOG_WARNING, "romdataInit: could not map 0x%08X " - "(ASLR/kernel refused the fixed address); using heap " - "copy — direct ROM reads will fail", CART_BASE); + "(mmap result=%p errno=%d); using heap copy — direct " + "ROM reads will fail", CART_BASE, at, mapErrno); #endif } diff --git a/port/src/video.c b/port/src/video.c index 8318981e..46b5ad1b 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -12,6 +12,9 @@ #include #include +/* Prevent GCC's implicit declaration warning */ +extern void exit(int status); + #if defined(_WIN32) #include #define GE_MKDIR(p) _mkdir(p) @@ -496,8 +499,9 @@ void videoStartFrame(void) * CREATED the window pumps them — and every game thread can be blocked on a * message queue at any time. So the host main thread (which created the * window in videoInit) must keep pumping; otherwise the window goes - * "Not Responding" and ESC/close never arrive. fast3d's own handle_events - * (which runs during rendering) remains as a backstop. + * "Not Responding" and ESC/close never arrive. On macOS AppKit strictly + * requires this main-thread pump, so fast3d's render-thread event handler is + * disabled there; on other platforms it remains as a backstop. */ void videoPumpEvents(void) { diff --git a/scripts/gen_macho_syms.py b/scripts/gen_macho_syms.py new file mode 100644 index 00000000..c2e528e8 --- /dev/null +++ b/scripts/gen_macho_syms.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Convert ELF/PE GAS absolute symbols to Mach-O spelling.""" + +import pathlib +import re +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(sys.argv[1]) + output = pathlib.Path(sys.argv[2]) + lines = [] + + for line in source.read_text(encoding="utf-8").splitlines(): + if re.match(r"^\s*\.section\s+\.data\s*$", line): + continue + + match = re.match(r"^(\s*)\.global\s+([A-Za-z_]\w*)\s*$", line) + if match: + lines.append(f"{match.group(1)}.globl _{match.group(2)}") + continue + + match = re.match( + r"^(\s*)\.set\s+([A-Za-z_]\w*)(\s*,.*)$", line + ) + if match: + lines.append( + f"{match.group(1)}.set _{match.group(2)}{match.group(3)}" + ) + continue + + lines.append(line) + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/strip_weak_pragmas.py b/scripts/strip_weak_pragmas.py new file mode 100644 index 00000000..335a1631 --- /dev/null +++ b/scripts/strip_weak_pragmas.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Remove ELF-style weak aliases from a C source copied for Mach-O.""" + +import pathlib +import re +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT OUTPUT", file=sys.stderr) + return 2 + + source = pathlib.Path(sys.argv[1]) + output = pathlib.Path(sys.argv[2]) + text = source.read_text(encoding="utf-8") + text = re.sub(r"^[ \t]*#pragma[ \t]+weak[^\n]*\n", "", text, + flags=re.MULTILINE) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(text, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())