Skip to content

feat: macOS on Apple Silicon host support (Rosetta 2 and MoltenVK) - #7332

Open
ycarogabriel-lgtm wants to merge 282 commits into
boykopovar:mainfrom
ycarogabriel-lgtm:feat/macos-host-support
Open

ycarogabriel-lgtm wants to merge 282 commits into
boykopovar:mainfrom
ycarogabriel-lgtm:feat/macos-host-support

Conversation

@ycarogabriel-lgtm

@ycarogabriel-lgtm ycarogabriel-lgtm commented Oct 18, 2026 •

Copy link
Copy Markdown

What

macOS on Apple Silicon as a host: relinked titles run as x86-64 Mach-O under Rosetta 2 and draw through MoltenVK. This branch brings the community's open macOS pull requests together on current main, makes them pass the whole test suite on an Apple Silicon Mac, and adds the fixes that only showed up once everything ran together. It is meant as one reviewable picture of the port; each part can still be taken from its own pull request, and I am glad to split it further, reduce it onto whichever of the overlapping PRs the maintainers prefer, or close it.

Integrated pull requests (merged or cherry-picked, conflicts resolved against current main)

Ported from #7201 (marion909's port; each commit is co-authored by @marion909, as @blawesom suggested reconciling the two)

  • every SPIR-V loop gets its own continue block, because SPIRV-Cross otherwise folds a continue construct holding the loop body into the for increment and drops the header phi updates, which hung the GPU
  • the last 32 draws and dispatches are listed when the GPU fails, and APS5_DUMP_DRAW_SHADER dumps every stage of the draws that use one shader
  • HTILE fills that no pending GPU work touches are written by the CPU, so the next draw does not wait for the open batch (not measured; no speed claim)
  • host frames without DWARF unwind info (the prx dylibs and system libraries carry only compact unwind) are stepped with the system libunwind; exception_runtime now runs on macOS
  • the Vulkan library is found by the loader's and MoltenVK's usual names, or named with ANYPS5_VULKAN_LIBRARY
  • zero-area rect-list rectangles are culled instead of faulting on MoltenVK

Not ported from #7201, with the reason: AF_UNIX seqpacket pairs emulated over datagram sockets (XNU reports ECONNRESET and EDESTADDRREQ where FreeBSD's seqpacket gives EOF and EPIPE, so they stay refused as the debt file says); sendto with an address on a connected UDP socket turned into send (FreeBSD returns EISCONN, which guest_udp checks); presenting from the host main thread and its own guest runtime base (this branch already has equivalent, tested paths from mugurc and yripper); the AvPlayer end-of-stream wake-up (generic logic gated on __APPLE__ with no test that reproduces the problem).

From the review and title runs on this PR (thanks @jotitadev and @imellag)

  • guest modules are opened RTLD_GLOBAL on the macOS host, because relinked modules bind imports by flat lookup and dyld leaves RTLD_LOCAL images out of it; this is what stopped FMOD Studio from loading in Planet of Lana and God of War: Sons of Sparta. The guest's own dlsym scope is unchanged and guest_dynamic_loader now checks it
  • fix(libc): give the scripting heap hooks blocks from libc's own heap #6842 (milescoviello) is merged in, which fixes the IL2CPP heap-hook crash at God of War's start-up
  • precise waits sleep three quarters of the interval on macOS and spin the rest, since XNU adds about a quarter of leeway to every timed sleep; precise_wait now passes 10 of 10 on the M1 (it was 3 of 6, and 0 of 10 on the M4 Pro)
  • tools/macos/setup-game.sh unwraps with tools/unwrap_self.py, which accepts both SELF magics, instead of a duplicate that refused 0xEEF51454 dumps
  • XNU's occasional reset on a full loopback accept backlog (guest_refused_connect) is recorded in the libkernel debt file: it did not reproduce here, and a retry could not be checked without a reproduction

Upstream tests that only ran on Linux, now run on macOS

  • named shared memory: shm_open/shm_unlink of named objects work on macOS as files in $TMPDIR/anyps5-shm (XNU's POSIX shm takes names of at most 31 characters and refuses fchmod, O_TRUNC and a second ftruncate); guest_named_shm_open, _lifecycle, _permissions and _concurrency run there. Guest mmap of files and shm objects now uses the Linux path on macOS, and EBADF for empty-vector writes on read-only descriptors matches the console
  • the guest errno is the host errno, and on macOS the C++ runtime can change it while releasing an exception, so the posix memory calls now set it again after their catch blocks (a refused mprotect reported ENOENT instead of EACCES)
  • sceLibcBacktraceGetBufferSize/sceLibcBacktraceSelf work on macOS (guest_backtrace); exception_c_cleanup and guest_getlogin_no_session run too
  • still Linux-only, with the reason: the audio port-lock and resampling tests and the two shm descriptor-reuse tests interpose host functions through ELF symbol preemption, which the two-level namespace does not allow; cpu_topology, guest_image_anonymous, guest_module_info and the ELF relinker execution tests read Linux-only interfaces (sched_getaffinity, /proc, ELF fixtures)

Fixes added here (XNU and Apple libc++ differences, each with a test that failed before)

  • sockets: SO_ACCEPTCONN through proc_pidfdinfo; shutdown(SHUT_RD) after the peer's SHUT_WR and on unconnected datagram sockets; urgent TCP data reported to poll through select; datagram byte counts from SO_NREAD in kevent, fstat and ioctl, because XNU's FIONREAD adds the sender address; IPv4-mapped answers dropped from IPv6 lookups
  • memory: JIT shared memory through a file kept linked while it is mapped; RWX file maps through RW then mprotect; munlockall unlocks each recorded range
  • libc: acosf through double; mktime before 1900; fflush leaves reading streams alone; host terminations forwarded to the guest's terminate; statics that guest threads still use at exit kept alive
  • files: getdirentries refuses plain files with EINVAL, as upstream now requires
  • SSL: Mbed TLS stays private to libSceSsl (hidden visibility, the macOS equivalent of --exclude-libs), so a program linking Mbed TLS keeps its own allocator
  • AGC on Metal: integer color targets (raw 32-bit exports bitcast to the target's signedness), per-sample sample masks, the input layer without the Geometry capability, image and sampler tables sized from update-after-bind limits, and the 11_11_10 float packing kept apart from raw integer exports
  • tests: the shared process-parameter fixture and the host import lifetime tests work on macOS, and the Bessel references use libm's jn/yn, as Apple's libc++ has no C++17 special math functions
  • build: libusb kept off pipe2, which the Xcode 27 SDK weak-links and macOS 26 lacks
  • web converter: macOS on Apple Silicon as a conversion target (Intel/Rosetta lowering forced on, macOS libraries optional)
  • docs/user/MACOS.md and tools/macos scripts to set up and launch a title
  • new upstream code on macOS: the performance overlay reads the private memory from TASK_VM_INFO; sRGB storage images keep their UNORM format because Metal encodes stores to sRGB textures; the AGC driver memory that feat(agc): link calls through the AGC FS table #6160 maps at 0xfe0000000 is held by Rosetta, so the driver starts without it and says so (AGC debt file)

Known limits (in the debt files)

  • Metal has no 64-bit floats or 64-bit atomics and MoltenVK has no VK_KHR_shader_clock, so those GPU tests fail by design.
  • Shared direct memory is copied instead of imported, so GPU stores through FLAT/GLOBAL addresses do not reach it.
  • KosmicKrisp in Vulkan SDK 1.4.363.0 is arm64-only and cannot be loaded by the x86-64 process under Rosetta.

Investigation notes (kept out of the repository): with MoltenVK 1.4.2 the remaining GPU failures, the same on the M1 and on @imellag's M4 Pro, are:

  • the sign of zero is lost (0x8000/0x80000000 read as zero) in f16/f32 denormal, rounding and output-modifier tests, packed_alu, sdwa_float_selectors and dpp16_operands (SPIRV-Cross Ratchet & Clank: Rift Apart: SIGSEGV since #861, an address-less 64 GiB reservation now takes the addresses the title hints next #2704), and a product that rounds up to the smallest normal reads zero in f32_constant_factor_tininess
  • the four clip/cull distance tests: MoltenVK offers no shaderCullDistance
  • tessellation: the instanced patch in column 0 does not take its colour from the relative patch index
  • pixel_interlock: MoltenVK does not compile raster_order_group on an argument-buffer field
  • rect_list_indirect: draws instead of aborting since the zero-area culling, but differs from the direct draw
  • image_plane_bias_on_single_level_volume: the device has no min/max sampler reduction

Title runs by @imellag on an M4 Pro (details in the comments): Planet of Lana (PPSA19450) and God of War: Sons of Sparta (PPSA28997) stopped on the FMOD Studio load and, for God of War, the IL2CPP heap crash, both addressed above; with them they reach the menu and gameplay at about 10 and 15 to 27 fps, with background corruption that mostly follows the macOS write watch and is not solved here. ccleste (PPSA59322) stops on a pixel shader that reads barycentrics without a vertex shader before it, as in #5079. A native Metal backend and KosmicKrisp under a native arm64 host were assessed and are not part of this pull request.

Tested

From a231179ac on, main comes into this branch by merge, not rebase, so the hashes in the review comments stay valid.

macOS 26.6.2 (25G83) on an Apple M1 (8-core GPU, 16 GiB), x86-64 build under Rosetta 2, AppleClang 21, Vulkan SDK 1.4.363.0 with MoltenVK 1.4.2:

  • suite without the GPU tests: 740/740 (ctest -E "^agc|video_?out|flip|display" -j4) on this head (57bccfbfc, main at d71853cf2 merged in), counting patched_libraries, which timed out on the first load of the libraries after the build and passed alone three times. precise_wait, guest_socket_read_write and guest_kernel_fcntl are timing-sensitive and have failed now and then in earlier runs, passing alone
  • GPU suite (ctest -R '^agc' with the MoltenVK ICD, excluding agc_display_buffer_dcc, agc_driver_flip and agc_driver_lds_atomics64, which need a window or 64-bit LDS atomics): 708/727 on 4a1c11a91, the previous merge; 18 of the failures are the ones listed under Investigation notes, and agc_driver_cross_workgroup_flag, which fails now and then under load, passed alone 5/5. On this head the multisample tests that the newer main commits touch pass, 15/15; agc_driver_multisample_retirement checks storage promotion only where the device has multisampled storage images, which MoltenVK lacks
  • reviewers also built and ran the suites on an Apple M2 Max with macOS 27.2 (@jotitadev) and an Apple M4 Pro with macOS 27.0.1 (@imellag); see their comments
  • python3 tools/check_conventions.py --base origin/main: passes
  • Linux and Windows not tested locally; their code paths are unchanged except for shared fixes that their own tests cover, and CI runs them
  • no title was run on the M1; @imellag ran three titles on the M4 Pro, see Investigation notes

Checklist

🤖 Generated with Claude Code

Zheorgyan and others added 30 commits October 10, 2026 00:20
The slide fix only trimmed the part of a rounded range that no registered
range covers. The guest rounds the range it protects to 16 KiB on the
absolute address, and an image dyld loads at a multiple of 4 KiB is not
on that grid: the end of a RELRO region, rounded up, reached into the
writable data behind it in the same segment, made it read-only, and the
first store there faulted (SIGBUS, KERN_PROTECTION_FAILURE). Roughly half
the launches of a real title died this way.

DoMprotect now rounds relative to where the ELF address 0 of the image
containing the range ended up, as the console does with its 16 KiB
aligned images (GuestAllocationsGuestPages). Ranges outside a guest image
keep the absolute rounding. The trim stays as a safeguard.

The mprotect-pages fixture has a 32 KiB data segment whose first 16 KiB
page is made read-only, then stores into the second page; the runner
launches it 16 times. It exits with SIGBUS without the change and 150
launches over all four slides give the expected exit code with it.

Reported by morten-holm, who cleared MH_PIE and saw the crashes stop.
- sceKernelGetModuleInfoForUnwind left eh_frame_addr and eh_frame_size
  at zero for a guest image. A title's own unwinder asks for the frame it
  is in, finds no table, and a C++ throw ends in terminate. The fields are
  now read from the eh_frame_hdr the image keeps in its program headers:
  eh_frame_ptr is decoded (absolute or pc-relative), and the records are
  walked to the terminator, inside the segment that holds them, the way
  the Windows branch does.
- sceSysmoduleGetModuleInfoForUnwind read /proc/self/maps outside Windows
  and threw on every call on macOS. It answers from mach_vm_region and
  dladdr, with the region as seg0.

The unwind-info fixture has a PT_GNU_EH_FRAME and one CIE, asks for its
own information and exits with eh_frame_size (16); it exits with 0 without
the change.

Reported by Cloud-Multiplayer.
…writable directory

- The relinker names a bundled guest module @executable_path/app0/sce_module/...,
  and the packager put the title's app0 under Contents/Resources/game, so dyld
  aborted at launch with "Library not loaded" for any title with a bundled
  module. Contents/MacOS/app0 is now a relative link to it.
- The launcher entered the bundle's own game folder, because AnyPS5 maps /app0
  and the mount points under the working directory. Saves were written inside
  the .app, and /download0 did not exist for a title that writes there without
  mounting it. The title now runs from ~/Library/Application Support/AnyPS5/<title id>,
  where app0 links to the bundle's game folder and download0 exists; the bundle
  stays read-only.

test_package_macos_app builds a bundle from fake inputs, checks that the module
resolves through Contents/MacOS/app0, and runs the launcher with a temporary HOME
and a stand-in eboot: it must start in the run directory, see app0/sce_sys/param.json
and find download0. A relinked bundled-module fixture packaged this way runs and
exits with its code; before, dyld aborted.

Reported by DavidSamir and mrzacarias.
main moved 1242 commits since the port's base. The relinker, NID patcher and
--to-rosetta now come from the merged pull requests, so their files take main's
version; the port keeps only the macOS fixtures and their runner there.

Conflicts resolved:
- VideoOutDriver.cpp, Flip.cpp: main's restructured flip path with the port's
  main-thread wrappers (OnMainThread) and runTests/Aps5StartGuest around the
  test environment.
- Strings.cpp, RuntimeSupport.cpp: the prefix-letter check of main with the
  port's errno-preserving ConvertInteger; strtoimax/strtoumax stay in Strings.cpp.
- ModuleInfo.cpp: sceKernelGetModuleInfoFromAddr, sceKernelGetModuleList and
  sceKernelGetModuleInfo throw as not implemented on macOS.
- DirectMemory.cpp, GuestAllocations.cpp, CMake files and the docs: both sides.
- SpirvAluEmitterMath.cpp takes main's ordered not-equal; FloatInstructions.cpp
  keeps the port's sign fix on top of main's overflow clamp.

Code main added for Linux or Windows, made to build and work on macOS:
- Time.cpp, General.cpp, SharedMemory.cpp, Stdio.cpp, Open.cpp, Resolver.cpp,
  Process.cpp, Thread.cpp, SignalSets.cpp, ThreadPriority.cpp, GuestArena.cpp,
  GuestBufferMemory.cpp: tm_zone, clock and st_mtimespec spellings, an anonymous
  file without memfd, fstatfs without sys/vfs.h, fcntl F_FULLFSYNC for fdatasync,
  open(O_CREAT) on a directory is EISDIR, argv from the Mach-O instead of
  /proc/self/cmdline, per-thread CPU time from thread_info (the thread CPU clock
  barely advances under Rosetta), the signal-set assembly through the shared
  Apple-aware macros, a mach thread precedence for APS5_THREAD_PRIORITY, and no
  chunked dma-buf import.
- GuestWriteWatch.cpp: a Fresh state for pages nothing has collected, so that
  CollectArmed and CollectFresh behave as on Linux; a re-registered page keeps the
  watch's own logical protection only while the watch holds it protected.
- DirectMemory.cpp reports a guest mprotect to the write watch, and GuestMemory.cpp
  reads the watch's logical protection for watched pages.
- Rename onto a dangling symlink, getdirentries leaving errno alone on success,
  and the wide integer conversions keeping errno.

Tests: the arena-, rtkit-, dma-buf- and import-specific checks run only where
they exist; run_macos_fixtures.py relinks the Rosetta fixture with --to-rosetta
and checks that --to-intel alone still faults. ctest without a Vulkan loader:
712 tests, 0 failed, 264 skipped, twice in a row.
Cloudberry Kingdom (PPSA23827) imports kqueue and kevent from libkernel
to watch its content directories: it opens a directory, registers
EVFILT_VNODE with EV_ADD | EV_ENABLE | EV_CLEAR and the delete, write,
extend, link and rename notes, and polls the queue with a timeout. No
library exported either function, so the title stopped at load.

On a macOS host both now pass through to the host's kqueue. Guest
descriptors are host descriptors there, and EVFILT_VNODE, the change
flags up to EV_DISPATCH and the vnode notes up to NOTE_REVOKE have
FreeBSD 11's values, which static assertions pin. Events are returned
through a host buffer and copied to the guest. Every other filter,
later flags and notes, and guest sockets throw. Other hosts do not
build the file, so the import stays absent there.

guest_kqueue watches a temporary file through a write, an unlink and
the removal of the watch, and checks the rejected changes and the
EFAULT and EBADF results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ut reading st_size

The unwinder in a title's own libc.prx asks sceKernelGetModuleInfoFromAddr
for the eh_frame of the frame it is in. On macOS the call was not
implemented, and on every host it threw when the caller's st_size was
not the full size. Cloudberry Kingdom (PPSA23827) passes a stale st_size
(8, or a pointer), so its first C++ throw, a minute or two in, ended the
process with `unsupported st_size`.

- The caller's st_size is no longer read: shadPS4's process.cpp fills
  the structure whatever it holds. The result still carries the full
  size.
- On macOS the image is found through dl_iterate_phdr over the program
  headers a converted image keeps, with the code the Linux branch uses
  for the name, the load segments, the TLS image and the eh_frame. The
  id comes from the image's Mach-O header. tls_index, init_proc_addr
  and fini_proc_addr stay 0, and an image opened through the loader gets
  a stable id of its own; TechnicalDebt.md records both.

The module-info fixture asks for its own information with flags 2 and
st_size 8 and exits with eh_frame_size, 20 with the terminator; it is
the unwind-info fixture with another import. guest_module_info expects
a short st_size to succeed; it is not built on macOS and was not run.
The title now runs past its first throw (230 s without a stop).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Relinked titles run on Apple silicon as x86-64 code under Rosetta, but
CI runs the write watch tests there only as arm64 and the register
context tests only on Intel macOS and Linux. Add x86_64 builds of both
on macos-15, installing Rosetta first, as BUILD.md documents for local
runs.
…kqueue

feat(libkernel): kqueue and kevent on macOS hosts
…module-info-macos

feat(libkernel): sceKernelGetModuleInfoFromAddr on macOS hosts, without reading st_size
Resolves eleven conflicts with 874 commits of main (0a8b1d3):

- Asm helpers: main's AsmMacros.hpp gains the Mach-O spellings
  (APS5_ASM_SYMBOL, APS5_ASM_FUNCTION_END) and replaces our AsmFunction.hpp,
  whose users now include AsmMacros.hpp.
- GuestWriteWatch (macOS): main's per-page "queried" bookkeeping on
  re-registration is kept together with the Fresh bit.
- Stdio: main's guest-socket routing for writev/pread/pwrite comes first, the
  macOS empty-iovec check after it.
- GuestMemory: main's shared PROCMAP_QUERY lookup replaces ProcMapsQueryFd.
- vLdexpF16: main's rewrite is kept and the input sign is OR-ed in, because
  Metal returns +0.0 for -0.0 scaled up.
- TechnicalDebt.md: main's wording with the macOS notes added.
- FileStream and StandardStreams call feof_unlocked, ferror_unlocked,
  getc_unlocked and putc_unlocked unqualified, as they are macros on macOS.
- Udp: equeue wait and readiness use poll, as macOS has no ppoll or POLLRDHUP.
- Exception: sceKernelRaiseException is not implemented on macOS and the
  guest_raise_exception test skips there.
- UserContext: the getcontext assembly calls its helper through
  APS5_ASM_SYMBOL; AtomicSharedPtr gains compare_exchange_strong for the
  mutex fallback.
- Equeue: main's kqueue and kevent stubs are left out on macOS, where
  Kqueue.cpp implements them.
- DirectoryDescriptor: a directory descriptor's path comes from F_GETPATH, so
  fchdir works.
- Tests adapted to macOS: guest_udp (a connected UDP socket rejects an
  addressed send with EISCONN, as on the console), guest_kernel_stack_protect
  (page query through mach_vm_region), guest_ampr_markers (AMM checks need the
  guest arena), guest_ppsa01530_exports (no kqueue and kevent stubs),
  agc_descriptor_pool_reset_failure (host std::set_terminate) and
  guest_pthread_mutexattr (<initializer_list>).
Deliver the guest exception on macOS the way Linux does: pthread_kill sends
the host SIGUSR1 and the handler converts the Darwin x86-64 mcontext
(__darwin_mcontext64: __ss registers, FXSAVE image from __fs.__fpu_fcw) to
the guest's FreeBSD layout and back. Darwin does not expose the stack
segment, so ss reads as 0. A static_assert checks that the host float state
is laid out like an FXSAVE image.

guest_raise_exception no longer skips on macOS. The test's global assembly
label gets the Mach-O underscore and its SIGUSR1 constant is renamed, since
SIGUSR1 is a macro on Darwin.
Conflicts: the write watch takes main's fresh-page tracking (boykopovar#3703) in place of
the port's own; main's comparison fix replaces the port's ordered not-equal;
the kqueue, getc_unlocked and socketpair notes in TechnicalDebt.md and the
Rosetta sentence in BUILD.md are kept next to main's new entries; the Apple
branches of ModuleInfo.cpp, Stdio.cpp and SharedMemory.cpp stay beside main's
additions. The macOS build of what main added follows in the next commit.
dl_iterate_phdr reports how many images were added and removed, from dyld's
callbacks, because the unwind table cache in sceKernelGetModuleInfoFromAddr
is dropped when an image is unloaded.

Named shm_open and shm_unlink keep throwing as not implemented: main's host
names are 52 characters where macOS allows 31, and its permission check reads
/dev/shm. The tests follow the platforms they cover: log_macros uses a datagram
pair with 1 MiB buffers, since macOS has no SOCK_SEQPACKET pair; guest_amm_placement
skips without the guest arena; guest_shm_open expects the throw and leaves out
the size-bounded write, which only Linux implements; guest_memory leaves out
the flexible memory accounting of a plain dylib, which dl_iterate_phdr does
not describe.
# Conflicts:
#	core/libs/tests/GuestAmpr.cpp
#	docs/user/USAGE.md
O_DIRECT is Linux-only, the physical span index is also used by the macOS write watch, the stack protect test needs sys/mman.h and the getrlimit test initializer_list. The kernel write command test needs the guest arena like the other AMM tests.
…rate_phdr; test(libkernel): ask the arena once in the guest memory test

The callbacks were registered by a constructor attribute, which the conventions refuse; dyld reports the images that are loaded at the first call, so the counts are the same. The guest memory test reads GuestArenaAvailable once.
# Conflicts:
#	core/libs/prx/libSceFont/src/SystemFont.cpp
#	core/libs/prx/libSceNpEntitlementAccess/Export.cpp
# Conflicts:
#	core/libs/prx/libc/src/Strings.cpp
feof_unlocked, ferror_unlocked, getc_unlocked and putc_unlocked are macros
on macOS, so they are called without the global scope qualifier. The
remaining differences are the ones Darwin's headers and libSystem have:

- CLOCK_REALTIME_COARSE and st_mtim do not exist; the write mark uses
  CLOCK_REALTIME and st_mtimespec.
- tm_zone is a non-const char*.
- __cxa_thread_atexit returns void, so the wrapper reports success.
- Darwin's stack protector reads __stack_chk_guard from libSystem; the
  guard of the guest libc has the postfixed name, which receives the NID
  of the original.
- APS5_EXPORT spells the symbols with the leading underscore of Mach-O.
- GuestArenaSetProtection and PosixProtection are built on macOS as on Linux.
- The fclose failure test fills /dev/full, which exists on Linux only.

exception_runtime relies on the Itanium unwinder of libc, which is not
registered on macOS (the host libc++abi is used), so its test is only
registered elsewhere.
The exception is delivered like on Linux: the host SIGUSR1 interrupts the
target thread and the handler runs on it. Darwin hands the handler an
mcontext_t that points to separate general register and float state
structures instead of holding the registers itself. The general registers
are copied one by one; the float state of x86-64 Darwin starts with an
FXSAVE image (checked by static_assert against the offsets of the control
word, MXCSR and the first and last XMM register), so the 512 bytes the
guest context carries are copied from and to it, and Rosetta applies the
modified context when the handler returns.

The test's SIGUSR1 constant is renamed, as the macro of the same name
exists on macOS, and its spin function uses the assembly macros.
The process, thread and clock functions that read /proc or call glibc
extensions:

- The program arguments come from _NSGetArgc and _NSGetArgv instead of
  /proc/self/cmdline.
- The current CPU comes from pthread_cpu_number_np.
- getrusage for a thread, the CPU clock of a thread and the CPU time of a
  guest thread (clock_gettime with the thread CPU clock) read the Mach
  thread_info of the thread; macOS has no pthread_getcpuclockid.
- The stack limits of a thread come from pthread_get_stackaddr_np and
  pthread_get_stacksize_np.
- sleeping with nanosleep restores errno, because macOS leaves ETIMEDOUT
  in it on success and the guest's errno is the host's.

The first local of a guest thread lies 4,464 bytes below the top of its
stack on macOS, more than the frame the pthread stack test allowed, so the
test allows two frames there.
The guest socket layer relies on a few Linux calls:

- The equeue wait uses poll with the timeout rounded up to a millisecond;
  macOS has no ppoll.
- A readable stream socket whose peer closed is reported with POLLHUP by
  peeking one byte; macOS has no POLLRDHUP.
- FIONREAD on a datagram socket reports SO_NREAD, the size of the next
  datagram; macOS adds the source address to FIONREAD.
- socketpair rejects SOCK_SEQPACKET, which has no AF_UNIX pair on macOS.
- gethostbyname replaces gethostbyname_r, which macOS lacks.

The UDP test sends to an address from an unconnected socket on macOS, where
sendto with an address on a connected socket fails with EISCONN, and the log
test uses a datagram pair with enlarged buffers instead of SOCK_SEQPACKET.
libSceAudioIn and libSceAudioOut linked SDL2 statically, as every library
that uses it does. On macOS the Objective-C runtime registers the classes
of SDL from each copy it finds, and when two libraries of one process hold
a copy the runtime warns that one of the two is used and which one is
undefined. They link libSceAgcDriver instead, which holds the SDL of the
process. The other platforms are unchanged.
AppKit, which SDL uses for windows, Metal surfaces, event pumping and game
controllers, works only on the main thread, and a guest never hands its
thread back. The macOS relinker already emits a call to Aps5StartGuest as
the entry of a relinked executable; nothing provided it.

- Aps5StartGuest runs the guest entry on a new thread with the stack size
  of the main thread (at least 8 MiB) and serves the main dispatch queue
  from the run loop of the main thread. The guest ends the process with
  exit; a guest entry that returns aborts.
- Aps5RunOnMainThread runs a function on the main thread and waits for it,
  or in place where the main thread is the caller or Aps5StartGuest was
  never called.
- libSceVideoOut sends its SDL calls through OnMainThread, which carries
  the exceptions back to the caller: the initialisation and shutdown of the
  video and game controller subsystems, the window, its Vulkan surface and
  drawable size, the event polling and the pad, mouse and keyboard input,
  which are created and destroyed there. On the other platforms the
  functions run in place.
- The flip test starts its tests through Aps5StartGuest on macOS, as a
  relinked guest does.
macOS has no /proc/self/maps or MAP_FIXED_NOREPLACE, so the arena never came up and every guest allocation threw. Enumerate host regions with mach_vm_region and reserve free chunks with a fixed mach_vm_map, which fails instead of replacing. This skips dyld, the Rosetta runtime, and ranges the kernel never lets a process map (0x10_0000_0000-0x70_0000_0000). The POSIX commit/protection paths now build on macOS unchanged. Adds standalone guest_arena tests (x86_64 under Rosetta and arm64).
Move the explanatory comments into docs/dev/debt/libc.md, as the conventions require.
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@imellag

imellag commented Oct 18, 2026

Copy link
Copy Markdown

Rechecked on the same M4 Pro (macOS 27.0.1, Rosetta 2): head df24fc821 merged into main (376c04029), Release, no Vulkan loader on the path.

precise_wait: yes, the M4 Pro agrees. 10/10 run directly and 10/10 through ctest (on 690c044a6 it failed 10/10 and 4/5), and the full suite passes, 1455/1455 with the GPU tests skipped.

The leeway is a quarter only up to about 16 ms. Beyond that nanosleep oversleeps by 3 to 5 ms whatever the interval (+4 ms at 33 ms, +5 ms at 100 ms, +3 ms at 1 s, natively and under Rosetta). Sleeping three quarters then leaves a quarter to the spin: about 0.6 ms per vblank tick, but about 245 ms for a guest poll with no descriptors and a 1 s timeout. Keeping the spin to a few milliseconds above the leeway would avoid that; I have not tried it.

The new guest_dynamic_loader lines pass without the filter. The fixture is a .dylib and FromGuestModule rejects anything not ending in .prx, so the default-scope lookup is null either way. With a .prx copy, as the test already does for hostLoaded, it fails without the DefaultScopeSymbol check and passes with it:

-    void* localOnly = dlopen_nid_postfix(argv[1], 2);
+    const std::filesystem::path localPath = "anyps5-local-module-for-test.prx";
+    std::filesystem::copy_file(argv[1], localPath, std::filesystem::copy_options::overwrite_existing);
+    void* localOnly = dlopen_nid_postfix(localPath.string().c_str(), 2);
     Require(localOnly != nullptr && dlsym_nid_postfix(localOnly, "GuestModuleAdd") != nullptr);
     Require(dlsym_nid_postfix(nullptr, "GuestModuleAdd") == nullptr);
     Require(dlclose_nid_postfix(localOnly) == 0);
+    std::filesystem::remove(localPath);

0xfc0000000 is not only Rosetta's on macOS 27: native arm64 processes cannot map it either (#7634). The fallback works either way; only the debt line and the [gpu] message name Rosetta.

Not checked: the GPU suite and titles on this head (no Vulkan SDK installed right now), so not yet the FMOD Studio fix in a title; Linux and Windows.

AI-assisted: yes (Claude Code). I ran all of the above on my machine.

ycarogabriel-lgtm added a commit to ycarogabriel-lgtm/AnyPS5 that referenced this pull request Oct 18, 2026
Keeps the fork's main on the same upstream base as the macOS pull
request branch (boykopovar#7332), so the two do not drift apart and later
community macOS merges resolve against current code. The merge was
conflict-free here and leaves the code identical to the PR branch,
including upstream's port-zero connect check, which the PR branch had
to restore by hand after its boykopovar#7559 merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
ycarogabriel-lgtm and others added 11 commits October 18, 2026 18:36
Brings the pull request up to the upstream commits since its last update; they merge without conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a keeps free space beside host regions (@imellag)

Includes imellag's guest arena test and the macOS 27 measurement of the unmappable range, as on the fork's main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on macOS hosts (@mugurc)

Takes mugurc's NativeListening, shared by getsockopt, connect and the listening read event, in place of this branch's equivalent HostSocketListening helper, so the branch and the open pull request carry the same code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Guest mmap of a file or shared memory object went through a Linux-only path: on macOS it accepted only descriptors recognized as shared memory and then threw ENOTSUP, so MAP_SHARED of an anonymous shm_open object failed with EOPNOTSUPP. The Linux path (descriptor pinning, access-mode checks, regular files, placement in the guest arena) now also builds for macOS, where the helpers it uses already exist.

The guest's errno is the host errno. The posix memory calls set it inside catch blocks, and on macOS the C++ runtime's release of the exception when the handler ends can change errno, so a refused mprotect reported ENOENT instead of EACCES. mmap, munmap, mprotect, msync and mincore now record the error and set it again when they return, after that release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit f766181)
…n macOS

XNU refuses readv and writev with no vectors, so the macOS path answers them itself after checking that the descriptor is open; for writev and pwritev it returned 0 even on a read-only descriptor, where the console and Linux return EBADF, as guest_named_shm_lifecycle checks. The write variants now check the access mode first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b9dd063)
shm_open and shm_unlink of a named object threw NotImplemented on macOS, and the Linux path cannot be reused as is: XNU's POSIX shared memory takes names of at most 31 characters (ours are 52), refuses fchmod, O_TRUNC and a second ftruncate, and is never executable. On macOS named objects are now files in $TMPDIR/anyps5-shm, the role /dev/shm plays on Linux, locked with flock on that directory; anonymous objects are created there too, so both are recognized as shared memory through F_GETPATH and get the console's write and seek rules (a write past the end does not extend the object).

guest_named_shm_open, _lifecycle, _permissions and _concurrency and guest_getlogin_no_session now run on macOS. The two descriptor-reuse tests stay Linux-only because they interpose the host fcntl through ELF symbol preemption, which macOS's two-level namespace does not allow. guest_shm_open now expects named objects to work on macOS, and the lifecycle test skips the one check that macOS's copy-at-map MAP_PRIVATE cannot meet; both differences are in the libkernel debt file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8d9e057)
sceLibcBacktraceGetBufferSize and sceLibcBacktraceSelf threw NotImplemented outside Linux. They capture frames with backtrace() and name modules with dladdr, which macOS provides with the same behaviour, so the Linux implementation now builds there and guest_backtrace runs and passes on macOS. exception_c_cleanup, which was kept off macOS while host frames with only compact unwind info could not be stepped, also passes since that unwinding was ported from boykopovar#7201 and now runs on macOS; guest_image_anonymous stays off, as it reads /proc symlinks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ac61fad)
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 18, 2026
ycarogabriel-lgtm added a commit to ycarogabriel-lgtm/AnyPS5 that referenced this pull request Oct 18, 2026
…#7332

The fork's main had fallen behind the pull request branch on two
fronts: the 169 upstream commits (53 on its first-parent line) since the last sync, and the commits
made on the PR branch from the M4 Pro review (the precise wait capped
at 6 ms of spin above 24 ms, the guest scope test loading a real .prx,
the 0xfc0000000 range attributed to the macOS host rather than
Rosetta) and the web converter's macOS batch fixes. Each conflict is
resolved to the PR branch's version, which already combines upstream
with the macOS paths, and the files the PR branch changed outside the
conflicts are taken from it too. The relinker CLI now keeps
--macos-fixed-base beside upstream's --patches and --verify-patches,
and refuses it in verify mode like the other fixed bases.

The code is identical to the PR branch afterwards; only the fork's own
notes (docs/metal-exploration, the macOS validation section) differ.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ycarogabriel-lgtm and others added 4 commits October 18, 2026 20:33
Brings the branch up to main at 384840a with a merge instead of
another rebase, so the commits reviewers have already tested and cited
keep their hashes and the branch can be updated without force pushes.

Conflicts and macOS adaptations:
- SpirvModuleSetup.cpp: main's shader layer change (boykopovar#6164, which this
  branch carried) did not keep the guard that skips the
  SPV_EXT_shader_viewport_index_layer declaration when the Layer output
  already emitted it; the branch's version, which has it, is kept.
- CliArgs.cpp: main's --patches and --verify-patches options sit beside
  --macos-fixed-base, which verify mode now refuses like the other
  fixed bases, and the usage text lists both.
- the other conflicts (precise wait, NetCtl test, web converter, socket
  options, libkernel debt) resolve as on the previous update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps the pull request mergeable after 33 more upstream commits, by
merge so the hashes reviewers cite stay valid.

The one conflict is in sceKernelWritev and sceKernelPwritev: main now
flushes pending GPU results into the written buffers before a file
write (a64d038), and this branch returns early on macOS for an empty
vector so a write to a read-only descriptor fails with EBADF, as on the
console. Both are kept; the early return comes first, and with no
vectors there is nothing for the flush to do. The new compiler-rt
builtins and fmaf build and pass on macOS without changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps the pull request mergeable after 16 more upstream commits, again
by merge so cited hashes stay valid.

The only conflict is in the libkernel debt file, where main extends
the kqueue line for read events on descriptor 0 and this branch adds
the macOS part of the JIT shared memory line beside it; both are kept.
The new kevent, 16-byte __sync and FreeType code builds and passes on
macOS unchanged. The new multisample retirement test needs a follow-up,
since it expects multisampled storage images, which MoltenVK lacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…has it

agc_driver_multisample_retirement failed on macOS: its last stage
promotes a 4x color target to a storage image, and MoltenVK reports no
shaderStorageImageMultisample because Metal cannot write multisampled
textures from shaders. The driver already refuses that access with a
clear message when the feature is off, which is the correct behaviour,
so the test, not the driver, assumed too much. The storage stage now
runs only when the context has multisampled storage images; the idle,
recorded and sampled retirement stages still run everywhere and pass
on MoltenVK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ycarogabriel-lgtm added a commit to ycarogabriel-lgtm/AnyPS5 that referenced this pull request Oct 19, 2026
Brings the fork's main to the state of boykopovar#7332 after the upstream merges
of today (main at d71853c), so the fork keeps the same code as the
pull request. That includes the empty-vector write check living beside
main's GPU flush in Stdio.cpp, the libkernel debt lines from both
sides, and the multisample retirement test that skips storage promotion
on devices without multisampled storage images.

The fork keeps its own layout: the macOS validation section in
TechnicalDebt.md and the guide under tools/macos/README.md, so the
identical copy at docs/user/MACOS.md that the PR branch carries is
removed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ycarogabriel-lgtm

Copy link
Copy Markdown
Author

Thank you @imellag for the M4 Pro recheck. All three points are on the branch now:

  • precise_wait beyond the leeway: above 24 ms the macOS wait sleeps until 6 ms before the deadline and spins only that. Below it, it still sleeps three quarters, which is where your numbers showed the quarter leeway. precise_wait now also checks the CPU time of a 1 s wait, so the 245 ms spin you measured would fail it.
  • guest_dynamic_loader: you were right that the new lines passed without the filter. They now open a .prx copy, as you proposed, so they fail without the DefaultScopeSymbol check.
  • 0xfc0000000: the [gpu] message and the AGC debt entry now say the macOS host holds that range, not Rosetta alone, since native arm64 processes cannot map it either on macOS 27.

@jotitadev, your ECONNRESET analysis of guest_refused_connect is recorded in the libkernel debt file. The test stays registered on macOS until a retry like the Windows path can be checked on a Mac that reproduces it.

About the history: the branch was rebased twice today, so df24fc821 and 690c044a6, the heads you tested, are no longer on it; the same changes are there under new hashes. From a231179ac on, main comes in by merge, so the hashes you cite from now on stay valid. main up to d71853cf2 is merged, including #6164 (the shader layer change this branch carried) and #6317 (clip distances). One new upstream test, agc_driver_multisample_retirement, needed a change for MoltenVK, which has no multisampled storage images. Results for 57bccfbfc are in the description.

@jotitadev jotitadev mentioned this pull request Oct 19, 2026
12 tasks done

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build ci .github docs libc libkernel needs rebase Conflicts with main, rebase needed prx relinker shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.