This branch combines current main with the community macOS port from mugurc/AnyPS5 macos-port, which upstream is reviewing in pieces (#927, #1597, #1499, #1686). It adds the fixes needed to build and run on top of current main.
Guest code stays x86-64 and runs under Rosetta 2. The relinker writes Mach-O (--macos), the prx libraries build as x86-64 dylibs, and Vulkan runs through MoltenVK. Apple has announced that macOS 27 is the last release with full Rosetta support, so this route has a limited lifetime.
git submodule update --init --recursive
cmake -S . -B build-mac -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DBUILD_TESTING=ON
ninja -C build-mac all libsRequirements:
- Xcode command-line tools
- Rosetta 2
- Ninja
- the Vulkan SDK (universal loader and MoltenVK)
- for the guest fixtures only:
brew install llvm lld
build-mac/core/relinker/relinker --macos --to-rosetta source/eboot.bin out/eboot
python3 tools/package_macos_app.py --relinked out --game source --libs build-mac/core/libs/libs --vulkan ~/VulkanSDK/<version>/macOS Title.appMeasured on an M3 Pro with macOS 26.6, Vulkan SDK 1.4.363.0 and MoltenVK:
| Check | Result |
|---|---|
| Full build (all targets and 115 prx libraries) | builds |
macos_fixtures: argv, imports, TLS, C++ exceptions, C cleanup, threads, modules, TLS across modules, --to-intel |
10/10 run under Rosetta |
Breakout guest (video out, pad, audio out), relinked and packaged as .app |
runs at about 60 fps |
| ctest, full suite on MoltenVK | 832 tests: 775 pass, 43 skip features the device lacks (64-bit atomics, Float64, mesh shaders), 14 fail |
Dreaming Sarah (PPSA02929), relinked with --macos --to-rosetta, packaged as .app and opened from Finder |
runs the animated main menu at 60 fps (about 18 fps before the write watch) |
The 14 tests that fail (macOS 26.1, MoltenVK 1.4.2):
- Apple GPU or Metal limits (4):
f32_dot2_roundingneeds the Float64 capability,f32_constant_factor_tininessgets a product flushed to zero where RDNA2 keeps the smallest normal,float16_roundinggets a differently roundedv_mul_f16product, andpixel_interlockneeds fragment shader interlock, which SPIRV-Cross cannot write as MSL. - A product that should give
-0.0gives+0.0(4):f32_denormal_flush,f32_output_modifier,packed_alu,sdwa_float_selectors. See TechnicalDebt. - Host platform (5):
agc_thread_priority_refused_rt,agc_thread_priority_refused_highandagc_driver_pm4_priority_refusedexpect a refused priority request to throw,agc_shared_writecannot inspect mapping lifetimes, andcheck_skipspasses from the repository root but not from the working directory ctest gives it. patched_libraries: loads all patched libraries and exceeds the 300 s test timeout only when the whole suite runs in parallel after a rebuild; it passes alone.
macos_fixtures passes once on a fresh build and times out in every later run: its argv fixture, which ends with int3, stays exiting in the kernel under Rosetta until a reboot. It was left out of the run above.
Changes on top of the merged port:
- GuestMemory.cpp and DynamicLoader.cpp compile on macOS.
mainhad added Linux-only code to both after the port was based. - When the device has no
robustness2.nullDescriptor(MoltenVK), the empty slots of the typed image heaps bind zeroed placeholder images. Before this, every draw and dispatch failed on macOS. - Shader stages without host subgroups (MoltenVK: vertex and tessellation evaluation) run each invocation as a one-lane wave instead of failing validation.
- Guest thread destructors register through
_tlv_atexit. libSystem's__cxa_thread_atexitreturns void, and reading its result as an int freed a live registration twice. dlsym's default scope finds the main program through dyld.- Guest
mprotectrounds to 16 KiB pages relative to the guest image's load bias. dyld only aligns a slide to 4 KiB, so rounding the absolute address protected the wrong pages (SIGBUS in Dreaming Sarah). Guest segments get maxprot rwx, and the image's no-access padding segments are registered. - Without storage MSAA, multisampled image dimensions are emitted single-sample. This matches the single-sample images the driver creates; Dreaming Sarah's first shader clears an MSAA surface with
image_store. - Merged upstream #1653: no fragment barycentric on MoltenVK. SPIRV-Cross cannot write
PerVertexKHRto MSL. - MoltenVK has no
VK_EXT_depth_range_unrestricted. A draw whose viewport depth leaves [0, 1] is drawn with [0, 1] when the depth cannot be observed (Unity's stencil clears in Kitaria Fables); see TechnicalDebt. Depth-tested draws with such a range still fail, with the range in the message. sceKernelGetModuleInfoForUnwindandsceSysmoduleGetModuleInfoForUnwindreport theeh_frametables of the relinked images, so a title's bundledlibc.prxcan catch C++ exceptions (Kitaria Fables' managed exceptions).- macOS has a guest write watch. Without it the driver compared guest memory before every draw, a third of Dreaming Sarah's frame time; it write-protects watched pages and records the first write to each in a fault handler. See TechnicalDebt for its costs and limits.
- Packaged titles start from Finder:
Contents/MacOS/app0links to the game folder, where the executable loads its guest modules, and the soft open-file limit (256 for apps Finder opens) is raised when direct memory runs out of descriptors. - MoltenVK's Metal argument buffers are enabled through
VK_EXT_layer_settings. Together with upstream #1580 (merged here), they lift the 31 storage buffers per stage limit. - Test fixes for macOS:
- case-insensitive APFS in
guest_path_case - the portability loader in
oversized_descriptor_sets - the fixtures' temporary directory, about 720 MiB per run, is now cleaned up
ProcessParametersFixturegives a test executable process parameters through its own__APS5DATA,__metasection, laid out as the relinker writes it, sodl_iterate_phdrlists them as the main image'sPT_SCE_PROCPARAM
- case-insensitive APFS in
- The AGC driver creates the GPU mutex before itself. Its destructor at exit locks that mutex, and macOS's
pthread_mutex_lockfails on a destroyed one. - libSceJson2 converts a number that
from_charsreports out of range withstrtod_lin the C locale. libc++'s stream extraction fails on everyERANGE, so an underflow to a denormal or zero was rejected as an overflow. - When the AGC driver memory cannot be mapped at 0xF_E0000000, the libSceAgc static buffer functions throw.
- A positioned write on an
O_APPENDregular file (pwrite,pwritev, AIO writes) appends and leaves the offset unchanged, as on FreeBSD; XNU'spwritewrites at the offset. See libkernel debt. sceKernelGetModuleListand the module info calls also list the.prxlibraries built from this repository, reporting their Mach-O segments, as Linux lists them throughdl_iterate_phdr.dlerrorkeeps the whole message. dyld's errors list every path tried and were cut at 512 bytes, before the hint about the relinker's--module-diroption.
tools/macos/spikes holds the probes used to choose this route:
- gs TSD reads
- AVX2, FMA and BMI2
- JIT code
- fs base: unavailable under Rosetta
- the guest arena layout
- MoltenVK features
Run them with run.sh [libMoltenVK.dylib].