You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It'd be wonderful having support with macOS as I am an intel Mac user.
For vulkan shaders load, you could attempt to use MoltenVK to process everything to load under Metal.
I'll probably make pull requests to introduce support for this project.
I'm not against a pull request for macOS support if:
It doesn't require reworking the overall rendering logic for a less efficient version.
MoltenVk won't build on Linux and Windows (as far as I know, that's easy to do).
It's x86-64, like yours.
But for macOS, I'm more concerned about the libc.prx (especially the work of exceptions) and libkernel.prx implementation. These libraries contain a lot of platform-specific code.
Reacted by TheGuyWhoIsIdiot, Vladislav Kalinkin, WoysfuL, Guilherme Athayde, Archisman, exefer, Gabriel Silva Rodrigues, Sam, lin72h, Majid Hojati and 9 more
Reacted by SAURABH VAIRAGADE and rNADOMROOM22stopmotion
Hello. Just about macOS. I'm implementing a similar project on Mac related to PS4 (a static recompiler on native Apple Silicon). If you want to support macOS, I won't mind if you use my code to support the processor (wie-project/PS4Recomp).
Reacted by boykopovar, WoysfuL, Samuel Gagarin, Cadence251, Yasir Yilmazcoban, Gaphilly, Akuien Wol, MarianSWA, Marcos Carvalho, Wionsito and 2 more
Reacted by TheGuyWhoIsIdiot
Reacted by komodo, MarianSWA and SAURABH VAIRAGADE
Hi! I was interested in macOS support, so I started experimenting in my own fork to see how far a port could get. It went further than I expected (I have a game booting and running with sound), so I'm sharing the progress here in case it's useful.
Your conditions from this issue
Rendering: the rendering logic is unchanged. Vulkan runs through MoltenVK. The only shader-side change: MoltenVK has no subgroup operations in vertex shaders, so the recompiler emits single-lane equivalents for stages the device reports as unsupported. The default is all stages supported, so nothing changes on Linux or Windows.
MoltenVK: only fetched and staged on Apple; nothing is built on Linux or Windows.
x86-64: yes, x86-64 under Rosetta 2.
libc exceptions and libkernel (the concerns you mentioned)
The exception runtime now reads RTTI through its own Itanium-ABI structs instead of libstdc++ internals. Only the host glue for exception_ptr differs between libstdc++ and libc++.
libc.prx remains the process-wide exception runtime
The libkernel and libc changes are mostly #if APPLE branches.
I also fixed a few problems that also affect other platforms (happy to send these as separate PRs)
Not verified yet: the Linux and Windows builds of the shared changes (exception runtime, TLS scan, unwinder helpers). I'd check those before opening any PR.
If this direction works for you, I'd be glad to split it into reviewable PRs.
Reacted by James St-Pierre, MarianSWA and Wionsito
Reacted by TheGuyWhoIsIdiot, Sam, James St-Pierre, Moha Abdi, Antony Ngure and rNADOMROOM22stopmotion
It's surprising that everyone heard that Rosetta 2 is dying, but its active implementation continues. Given that this is not a JIT emulator, it would be logical to make at least a fork with static translation of x64 instructions into C, and from it to ARM and adaptation to the specifics of macOS. It's much more difficult, of course, but I think it's cleaner. I don't know if I'll be able to finish my project (there is no such patience and incentive, because I don't play), but I managed to get to the first Unreal Engine 4 error in Hogwarts Legacy in my PS4 project. That is, at least the processor part is possible.
Reacted by lariat-institute, MarianSWA and Wionsito
It's surprising that everyone heard that Rosetta 2 is dying, but its active implementation continues. Given that this is not a JIT emulator, it would be logical to make at least a fork with static translation of x64 instructions into C, and from it to ARM and adaptation to the specifics of macOS. It's much more difficult, of course, but I think it's cleaner. I don't know if I'll be able to finish my project (there is no such patience and incentive, because I don't play), but I managed to get to the first Unreal Engine 4 error in Hogwarts Legacy in my PS4 project. That is, at least the processor part is possible.
I genuinely think this goes against the idea of the project which targets native execution rather than being transformation recompilation.
But tell me. And what about, for example, those who buy new Macs, which initially have the minimum version - 28? Even if you imagine the possibility of a rollback, most of you won't even know about it. And also what to do with Wine, Shadps4 and many other projects? “We won't support your version anymore?”
Hi @boykopovar, I have a macOS port working locally, rebased on current main. It runs x86-64 under Rosetta 2 and was tested on an M5 Max with macOS 26.5. Before opening pull requests I'd like to know how you want it split, and to avoid overlapping with @franpeza's port.
Your conditions:
Rendering is unchanged, and Vulkan goes through MoltenVK. The recompiler changes emit SPIR-V that means the same on every driver but survives SPIRV-Cross's translation to Metal:
ordered not-equal;
NaN tests on the bits;
subgroup results that SPIRV-Cross would otherwise move into ?: / &&;
a texture fence before image atomics.
MoltenVK is not built anywhere. It comes from the Vulkan SDK at run time, and a packaging script copies it into an .app. The Linux and Windows builds are unchanged.
Only x86-64.
libc and libkernel:
Exceptions:
On macOS, host code keeps libc++abi, because libc++ binds __cxa_throw two-level. libc.prx's runtime serves guest code.
Guest frames are found through their PT_GNU_EH_FRAME, using a dl_iterate_phdr emulation over the original program headers that the relinker keeps.
Host frames are found through __TEXT,__eh_frame; the prx are linked with -no_compact_unwind.
The libkernel and libc changes are __APPLE__ branches:
shm_open instead of memfd;
mach_vm_map instead of MAP_FIXED_NOREPLACE;
guest TLS through a per-image pthread key: macOS cannot set the fs base, so the relinker rewrites %fs accesses into stubs;
the guest starts on a secondary thread, so AppKit keeps the main thread.
Relinker:
--macos writes Mach-O executables and guest modules: rebases, flat-namespace binds, the export trie, guest TLS (including general-dynamic TLS across modules) and init/fini.
--to-intel places its stubs there too. For Rosetta it also lowers RDSEED, RDPID and CLWB. Rosetta runs AVX2, FMA and BMI, but not SSE4a, SHA, ADX, RDSEED, RDPID, CLWB, CLZERO or MONITORX.
Tested on macOS:
All non-GPU tests pass.
148 of the 166 AGC and shader tests pass through MoltenVK. 16 need f64, 64-bit atomics or the shader clock, which Metal does not have. One is still open: a signed zero in an SDWA f16 multiply.
I have no PS5, so no commercial title was tested. Testing used the fixtures and a small Breakout guest built like a PS5 title (video out, pad, audio out), which runs at 60 fps as a double-clickable app.
The code was written with AI assistance; I reviewed and tested it.
Proposed split, each against main:
fix(recompiler): the SPIRV-Cross-safe emission above, neutral for other drivers.
fix(libc): __gcc_personality_v0 exported for guest C code (it is missing on every platform), and strtoimax/strtoumax errno.
feat(macos): the build and the prx libraries.
feat(macos): relinker --macos (depends on 3).
feat(macos): app bundle packaging.
@franpeza, you mentioned a port that boots a game. I'd rather combine our work than duplicate it: if you are about to open PRs, tell me which parts you cover and I'll adapt mine.
Reacted by Cadence251, jeank93 and Arryan Bhatnagar
I'm currently helping other similar project that supports MacOS, but are still very early stage https://github.com/mattias800/prosper . Basically the idea is to work like Wine but for PS5, it doesn't need to rebuild the binaries to the platform.
@mugurc Have you tested libc exception runtime modified on windows or linux? I'm also interested in whether there is any dependency on Rosetta, and whether your version will work without emulation on macOS with x86-64 (it turns out there are some). I think you can open a pr to support exceptions first.
@boykopovar Not yet: I only have this Mac, so it was tested on macOS only. Here is what the exception changes touch outside macOS:
__gcc_personality_v0 is also exported to guest code now, because guest C built with -fexceptions imports it, and CallPersonality recognizes that export. On Linux and Windows the host __gxx_personality_v0 and __gcc_personality_v0 are unchanged; they forward to the same code.
The exception_ptr model and the type_info declarations are only compiled with libc++/libc++abi (_LIBCPP_VERSION, _LIBCPPABI_VERSION). The Linux and Windows builds use libstdc++, so they don't see them.
Finding host frames through __TEXT,__eh_frame and the dl_iterate_phdr emulation over the guest program headers are __APPLE__ only.
The PR's CI will run exception_runtime and exception_c_cleanup on Linux. They are UNIX-only, so Windows only gets the build.
Rosetta: the build is plain x86-64 macOS, and nothing checks for Rosetta at run time. The one Rosetta-specific step is in the relinker: --macos lowers RDSEED to RDRAND, drops CLWB and turns RDPID into 0, because Rosetta lacks them. That output still runs on an Intel Mac, where it's just unnecessary, so I can put it behind its own flag and leave Intel output as it is. I have no Intel Mac, so I can't test there.
I'll open the exceptions PR first, against current main.
guest_host_standard_error is test(libkernel): throw through the guest runtime on macOS in the host standard error test #9654, a test-only change. Its last check looks for libstdc++'s what(): report kept, and on macOS libc++abi words the same report libc++abi: terminating due to uncaught exception of type std::runtime_error: report kept. The report does reach the host's stderr. Correction to my note above: the unwind tables don't matter for this one. The waitpid failure I saw was EINTR from running it under lldb; outside the debugger it only ever failed at the wording.
guest_math_exact passes here when the test is built at -O0 and fails at the optimised build, with the same libc. Every check in it compares 80-bit long double bits (signalling NaN pass-through, errno), so I suspect Rosetta's x87 emulation rather than libc. I'm not taking it; if someone has an Intel Mac, a run there would settle it.
Balatro (PPSA21402) on a fanless M2: what limited it to ~40 FPS, and 60 FPS with two changes. Update to my 13 October note above. Setup: MacBook Air M2 (no fan), 16 GB, macOS 27.2, MoltenVK 1.4.2, x86-64 under Rosetta 2, 1920x1080, the macOS series plus local changes (not main, which does not build on macOS).
Symptoms. 60 FPS on the menu at first, falling to ~40 after 1.5–3 minutes, and 11–22 FPS on the shop and booster-pack screens. powermetrics showed the drop is thermal: the GPU was ~95% busy at full clock, then the Air lowered its GPU clock (1365 → 840 MHz) with the same work per frame. So the fix had to be less GPU work per frame, not a higher frame cap.
Cause 1 — barycentric emulation on sprite draws (shop and pack screens). The title's sprite/text pixel shader keeps I/J live after its v_interp pairs, only as the value lanes outside the export mask keep. That sends every such draw through the generated tessellation stages, and on MoltenVK each one restarts the Metal render encoder (a full 1080p tile store and load). The pack screen issues ~730 of these per frame. A 1x1 scissor on them changed nothing; skipping only their draw call took the screen from ~11 to ~34 FPS. #9193 (imellag) fixes this by reading I/J from BaryCoordKHR on MoltenVK. On the same untouched pack scene with warm caches: 45.4 → 60.0 FPS, frame p99 25.4 → 18.4 ms, GPU busy 21.3 → 12.4 ms per frame; afterwards only the title's rect-list draws still use tessellation (about 4 per frame, under 1 ms). Details and traces are on #9193. I had written a narrower fix for the same case and am not opening it.
Cause 2 — exact GCN float emulation in full-screen shaders. The menu background (a procedural swirl, ~7 ms) and the CRT pass (~3.8 ms) are full-screen 1080p shaders, and almost all of their cost is per-pixel ALU: every f32 operation carries NaN-propagation and denormal-flush selects, and each v_sin/v_cos computes the native result plus two or three polynomial series. MoltenVK fast math made no difference. A local, opt-in experiment that drops those (native min/max, plain mul/add/mad, native sin/cos) cut menu GPU time ~22%. It deliberately gives up bit-exact GCN behaviour, so it stays a local experiment, not a PR; I'd welcome views on whether a per-title or user option for that would be acceptable.
Result. With the float experiment plus my narrower fix for cause 1 (the session predates testing #9193, which measured better on the pack scene), a 5-minute session with shop, packs and normal play held 58–60 FPS throughout, including past the point where the Air used to throttle, with GPU time at 10–11 ms per frame and no visual problems seen. Clip of the #9193 build in normal play, FPS in the title bar:
balatro-m2-pr9193-60fps.mp4
What's left. The command submission thread is 94–99% busy at 60 FPS on the pack screen (~455 draws per frame at ~20 µs each; vertex data is already used in place), so CPU is now the tightest margin. A finding worth knowing for anyone profiling on Mac: per-pass GPU timestamps (pass splits) cost several FPS by themselves here, and the overlapping per-pass ranges are not exclusive GPU time; the fence-based gpu_busy_ms_per_present in the trace summary is the reliable number.
AI-assisted: yes (Claude Code). Measurements are from my M2; gameplay observations are mine.
Proposal: keep the f32 NaN emulation exact, but pay for it only when a NaN appears. This is about the same cost as @krishalivaibhav's "cause 2" above. It's a design I'd like feedback on before writing it.
What it costs today.TranslationContext::nanResultF32 wraps every f32 add, sub, mul and fma: if a source is NaN, the result is the first NaN source (quieted in IEEE mode), otherwise a generated NaN becomes 0xffc00000. In MSL that is three or four (x & 0x7fffffff) > 0x7f800000 tests and selects per operation. Apple GPUs return 0x7fc00000 for every NaN result, so the payload can't come from the hardware.
Measured on an M4 Pro (macOS 27.0.1, MoltenVK 1.4.2), main + #8658 + #9193 + #9263 + #8612. The off arm uses a local switch that returns the raw result; it is for measurement only and not exact:
Same scene, warm caches, 10 s windows
NaN emulation on
off
GPU per frame
God of War Sons of Sparta, Cyclops fight
39.45 / 39.47 fps
46.59 fps, one run so far (+18%)
22.8 → 18.8 ms
Planet of Lana, village
29.40 / 30.04 fps
31.69 / 31.48 fps (+6%)
CPU-bound here
The f32 denormal flush already runs in hardware on this stack (#8578: 0 of 5472 probe results differ), so these selects are the main per-operation emulation cost left.
What I tried. A Metal compute microbenchmark: 16M threads, 4 independent chains of 48 mul/add/fma each, mathMode = .safe, variants interleaved over 40 rounds after warm-up, median GPU time. Every exact variant's output is compared bit for bit with the current form.
Variant
no NaN
0.1% of threads NaN
5% of threads NaN
A: plain operations (not exact)
1.01 ms
1.01 ms
1.01 ms
B: today's select chain (as SPIRV-Cross emits it)
5.81 ms
6.01 ms
5.78 ms
C: per-operation if (isnan(r)) r = chain
4.53 ms
4.70 ms
4.53 ms
D: plain region, exact re-run if any result was NaN
1.03 ms
1.12 ms
4.85 ms
A branch per operation (C) saves only about a fifth. Doing a region plain and re-running it exactly only when a NaN shows up (D) costs about the same as plain code while NaNs are rare. It falls back to the cost of today's form when many lanes in a SIMD group hit a NaN.
Proposal (D as an IR pass, exact for every input):
Split each block into regions of emulated f32 operations: a region ends at a side effect (store, export, atomic, barrier), a cross-lane operation and a size cap.
Fast path: the region's operations without the NaN selects.
Check: OR together isnan of each fast result whose NaN could leave the region or be hidden inside it. A result whose only users are other emulated add/sub/mul/fma in the same region needs no check, because those operations always return a NaN when a source is NaN, and their own results are checked. Results used by anything else (legacy multiply's zero select, min/max, compares, conversions, bitcasts, live-outs) are checked.
If the check is true, run the region again from its inputs with today's exact nanResultF32 chains and use those results.
Why it is exact. When no checked result is NaN, no emulated operation in the region produced a NaN, so every select in today's chain returns the raw result. The fast path's bits are then the exact path's bits. When any result is NaN, the region runs today's code. So the existing emulation stays the only definition of the NaN behaviour, and no new RDNA hardware oracle is needed.
Costs and risks to measure:
Region inputs stay live until the check, so register pressure goes up; the size cap bounds it.
The re-run doubles the region's code size.
Shaders that hit NaNs often (rare in practice) pay about what they pay today.
Tests: the existing f32_arithmetic_nan, f32_separate_rounding and denormal tests, plus new cases with a NaN produced in the middle of a region and hidden by a min/max, a compare and the legacy multiply.
Questions:
Is an IR pass that clones a region under a branch acceptable in the recompiler, or should the split happen in the SPIR-V backend, which already emits inline selections (SpirvBdaFault.cpp)?
Is anyone already working on the NaN selects?
@krishalivaibhav, would this cover the NaN part of your opt-in experiment without giving up exactness?
AI-assisted: yes (Claude Code). The measurements are from my M4 Pro.
@imellag yes: your design would cover the NaN part of my experiment exactly, and on Balatro that is the largest single piece of it. I split the experiment apart to check.
Setup: MacBook Air M2 (fanless), macOS 27.2, MoltenVK 1.4.2, the macOS series (42c3fd0 plus local changes; main doesn't build on macOS), Balatro (PPSA21402) main menu, which is dominated by two full-screen 1080p shaders: a procedural background (27 v_sin/v_cos, no samples) and the CRT pass (51 samples). Same build for every arm, each arm with its own warm shader cache, APS5_TRACE_SECONDS=12:15, no other trace or profile switch. A local measurement switch returns the raw result from TranslationContext::nanResultF32 (like your off arm, not exact).
The f32 denormal flush is already in hardware on the M2 too:[float-controls] f32 denormal flush: hardware (0 of 5472 probe results differ from the emulated flush), so as on your M4 Pro the NaN selects are what is left of the per-operation cost.
Generated code (SPIR-V from the shader dump, through spirv-cross --msl), the three heaviest pixel shaders:
Shader
MSL lines
selects
> 0x7f800000 NaN tests
background (27 sin/cos)
1190 → 901 (−24%)
708 → 419 (−41%)
289 → 48
CRT (51 samples, 23 sin/cos)
2255 → 1918 (−15%)
1642 → 1287 (−22%)
588 → 287
third (12 sin/cos)
978 → 854 (−13%)
386 → 249 (−35%)
127 → 21
GPU time per frame (fence-based gpu_busy_ms_per_present; the menu is vsync-locked, so FPS stays at 60 either way):
runs
GPU busy per present
NaN emulation on
4
12.73, 12.77, 13.63, 13.81 ms (the last with other load on the machine)
nanResultF32 off
4
11.83, 11.85, 11.89, 11.96 ms
So about 0.8–1.7 ms per frame (7–13%) on this scene. For comparison, my earlier opt-in experiment as a whole (NaN, denormal and tiny-product flush off, native min/max/med3/clamp, v_mac as one fma, native sin/cos) measured 12.9 → 10.7–10.9 ms on the same menu; native sin/cos alone was about 1.1 ms of that, and native min/max and the fused v_mac were within noise.
What your design would not reach, from the dumps: the NaN tests left after dropping nanResultF32 come from other paths: the signaling-NaN quieting around FPMin/Max/Med3 (ieeeMinMaxF32), conversions, and the range handling inside the v_sin/v_cos emulation (each computes the native result plus two or three polynomial series and selects between them). Those would stay at today's cost. The trig series is the other cost worth a separate look for shaders like this background.
On the region split: these are long, mostly straight-line shaders (the background's only branches are a few bounds-checked uniform loads at the top), so one region per block would be large; the size cap and register pressure you mention will matter here. If you build it, I can run it on this scene on the M2 and post the same numbers.
AI-assisted: yes (Claude Code). The measurements are from my M2.
@rooklz on guest_math: here (M5 Max, macOS 26.5.1, the series at 53db0ba) the rand_r(nullptr) case passes with the test binary as it is, and the first check that fails is acosf(0.5f) (0x3f860a91), which your #9668 covers. rand_r throws a host std::invalid_argument, which the system unwinder handles with the test's compact unwind. The test's own tables matter only where libc's unwinder walks its frame, that is libc's __cxa_throw (as in guest_bad_optional_access) and the _Unwind_* functions.
So I would keep #8311 to the targets that need the tables, rather than change every test executable on macOS. If the rand_r case still ends in terminate on your machine, could you post what it prints, and whether your libc.prx shows __eh_frame under size -m? If it reproduces, I'll add guest_math_tests to #8311.
Follow-up on my 10-18 note about the two tests that are intermittent on Apple. I said agc_driver_cross_workgroup_flag didn't look like store visibility, and that was only partly right. Setup: M2 Max, macOS 27.2, x86-64 under Rosetta, #8658 @ f48446a, MoltenVK 1.4.2 and MoltenVK main + SPIRV-Cross #2704.
agc_driver_cross_workgroup_flag: two causes. In a local copy of the test, the reader stores the poll count at which it saw the flag (40 wave64 and 40 wave32 dispatches per process, 6 rounds, plain store and atomic store alternating in the same binary):
Visibility.fix(shader): read GLC/DLC BDA dwords with device-scope atomic loads #2259 made the GLC load an OpAtomicLoad, but the GLC store is still OpStore ... Volatile in EmitBdaStoreAt. In the MSL it is a plain *(device uint*)p = 1u. On Apple the reader never sees that store on its first poll; it takes 300 to 1M polls. With the coherent aligned dword store as an OpAtomicStore at device scope, it sees the flag on the first poll in about 95% of wave32 dispatches.
Forward progress. The atomic store still leaves gave-ups, mostly in wave64. To check, I had the writing workgroup do one GLC load of the result word before it stores the flag. With that change the reader gives up in about 95% of dispatches, and the writer read 0 every time. So the writer started before the reader gave up, but its store did not land within 1M polls even as an atomic. The writing workgroup stalls while the other one spins, and neither Vulkan nor Metal guarantees forward progress between workgroups.
So a store-side change like #2259's would be correct by the memory model, but it would not make this test reliable on Apple. I'm not proposing it on its own.
agc_driver_uint8x4_color_target fails in 19 of 150 runs on MoltenVK 1.4.2 and 23 of 150 on MoltenVK main. The _no_resident variant passes 150 of 150 on both. It is nearly always one of the XOR cases (SW_64KB_D_X target with XOR: byte 0 is 90, expected 49, or the Z_X one): the target still holds the fill byte (0x5a), so the draw never reached guest memory. In 100 runs each on MoltenVK 1.4.2:
run
failures
default (the probe says host imports resolve write protection: no ... imported ranges stay watched)
14-16
APS5_WRITE_WATCH_IMPORTS=watch
11
APS5_WRITE_WATCH_IMPORTS=unwatch
0 (also 0/100 on MoltenVK main)
APS5_NO_HOST_IMPORT=1
0
APS5_NO_WRITE_WATCH=1
0
APS5_PACKET_EPOCH=1
14
no address reuse between cases (blocks leaked)
11
So on macOS, with host imports that stay watched, the resident target's write-back sometimes doesn't show up in the guest read. The full ^agc set with unwatch gives the same 18 failures as the default, so unwatch costs nothing in the suite. I haven't found where the write-back is lost: the probe may not cover this case on macOS, or the flush may race with re-arming the protection.
Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.
@rooklz on guest_getrlimit: it passes on the M4, 3 runs of 3 (same tree as my earlier list), which fits your explanation. Here kern.maxfiles is 184320, kern.maxfilesperproc 92160 and kern.num_files about 8550 before and after the runs, so the per-process limit is reached long before the system table and the pipe loop ends with EMFILE. On your machine 122880 − ~70000 open leaves fewer than the 61440 per-process limit, hence ENFILE.
Both fixes restored: guest_sce_net 5/5 PASS again.
These results support that both fixes are required for the test to pass on this M1 configuration.
Limitation: CTest recorded the aborts but did not print the exact failing assertion or source line. I also attempted LLDB, but it stalled while launching the x86_64 executable under Rosetta 2, so I cannot independently confirm the specific assertion locations you observed on M5 Pro.
The worktree was restored to a clean state. Detailed logs, patches, and hashes are retained locally.
AI-assisted: yes. Codex was used to perform and document the validation.
M5 data point: 32 catalog homebrew titles run natively, converted with --macos --to-rosetta, on MoltenVK.
Same tree as my earlier notes (ci/macos-series-2 at 53db0badb with #7524, the code of #7937 and the libSceSsl change of #7332), macOS 26.2, Apple clang 21, x86-64 under Rosetta 2; MoltenVK built from the MBridge copy with an x86-64 Vulkan loader, 75 s per title, no game data supplied. The emulator-side stops, grouped:
Stop
Titles
AGC graphics: device does not support mesh shaders (this MoltenVK reports no mesh-shader feature)
AGC driver: a pixel shader on the mesh path reads barycentrics, and this device has neither VK_KHR_fragment_shader_barycentric nor mesh shaders (the extension is listed, its feature reports false)
PPSA98511 ReKHCoM, PPSA99310 Mario 64 (Ghostship), PPSA99320 Ship of Harkinian (Vulkan)
AGC graphics: last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled (listed, feature false; MVK_CONFIG_USE_METAL_PRIVATE_API=1 does not change it on this build)
PPSA98001 PokeMMO, PPSA99993 Pulse
vkCreateGraphicsPipelines: Vulkan result -3, Metal refusing a float4 output on a UINT attachment: #9783
PPSA99313 Doom 3 BFG, PPSA59322 ccleste
Guest TLS access without a non-empty PT_TLS from the tree's relinker (empty PT_TLS plus fs: accesses); main's relinker converts all five, so this is already fixed there
The rest stop in their own code: PPSA99620 and PPSA99621 (Ship of Harkinian, 2 Ship 2 Harkinian) crash in a guest thread reading 0x20(%r12) with r12 null (EXC_BAD_ACCESS, all frames in the title until libkernel's RunGuest), which looks like a missing-asset null since they have no OTR data here; the others ask for their game files and exit. PPSA99666 DOOM, PPSA47402 Syobon Action and PPSA99429 ApricotGS render frames (AGC compute presentation on DOOM, gpu-display flip counters on ApricotGS). Bring-up threads exist for PPSA98512 (#9758) and PPSA99630 (#9762).
The first four rows are driver or MoltenVK capability gaps rather than title bugs; the mesh-shader and barycentric rows alone account for nine titles. I'm not taking any of them without asking here first.
Thanks @Mauro42K, that's exactly what I was hoping for: both blocks needed on an M1 with 27.0.1 as well as on the M5 Pro with 27.2. I'll add it to #9231's Tested section, crediting you.
@mugurc, this one is in the series rather than main: the macOS sceKernelGetdirentries in File/src/Stdio.cpp opens openat(fd, ".", O_DIRECTORY), which fails with ENOTDIR for a regular file, so sceKernelGetdents on a plain file returns 0x80020014 where the test (and FreeBSD) expect EINVAL (0x80020016). Locally if (listing < 0) return SceErrorFromErrno(errno == ENOTDIR ? GUEST_EINVAL : errno); makes guest_filesystem pass, and the full suite shows no new failure. (feat: macOS host support (Apple Silicon via Rosetta 2, Vulkan on MoltenVK) #8658's __getdirentries64 version may already behave differently here; I haven't checked it.) The Linux branch also returns EINVAL early for a guest socket via GuestSockets::IsOpen, which the macOS branch doesn't do.
M4 data point with Vulkan, and four of the remaining stable failures.
Setup: Apple M4 (10-core GPU), macOS 27.0.1, Apple clang 21, x86-64 under Rosetta 2, the same series tree as my earlier list. Unlike @rooklz's M5 setup, no MoltenVK build was needed: in the LunarG Vulkan SDK 1.4.363.0 the loader, MoltenVK 1.4.2, vulkaninfo and the validation layer are already universal (x86_64 + arm64). I installed it headless into the home folder (--root ~/VulkanSDK/1.4.363.0 ... install copy_only=1, no admin rights), and DYLD_LIBRARY_PATH=<sdk>/macOS/lib plus VK_DRIVER_FILES=<sdk>/macOS/share/vulkan/icd.d/MoltenVK_icd.json is enough. The x86-64 vulkaninfo reports Apple M4, MoltenVK 1.4.2, API 1.4.357.
ctest -j4 --timeout 120: 1648 tests, 58 failed, 95 skipped (M5: 87 failed, 93 skipped). The non-GPU failures are my earlier list and the two flaky tests. The 44 GPU failures, by first failing check:
f16 results lose the sign of zero (0x8000 comes back 0x0000), 13: dpp16_operands, f16_fma_denormal_modes, f16_conversion_float_mode, float16_fma_rounding, float16_misc, float16_modifiers, float16_rounding, ldexp_f16_inline_constants, packed_alu, packed_f16_denormal_modes, sdwa_float_selectors, sdwa_ldexp_f16, vop2_modifier_forms.
f32 signed zero and tininess: f32_denormal_flush, f32_output_modifier, f32_constant_factor_tininess. f64: f64_add_mul_denormal_modes, f64_rounding_denormal_modes (the other f64 tests skip).
No shaderClipDistance/shaderCullDistance: vertex_clip_cull_distances, vertex_clip_cull_draw, vertex_clip_cull_draw_no_key, vertex_user_data_tail.
agc_performance_overlay_statistics: taking it, PR today. It is @jotitadev's measurement from #7886: ReadProcessPrivateBytes reads /proc/self/status, and task_info(TASK_VM_INFO).phys_footprint on macOS makes it pass. #7332 had it and is closed; #8658 has it too. I'm splitting it out with both credited. Here it makes agc_performance_overlay_statistics pass, and agc_performance_overlay too once #7886 is applied.
guest_flexible_memory_declared: taking it, PR today. The declared case loads guest_flexible_memory_module.guest.prx, which is host-built, and expects its 512 MiB array in GuestImageSegmentBytes. macOS's dl_iterate_phdr (specifics/macos/GuestImages.cpp) only reports images with __APS5DATA,__meta. Every relinked module has that section, but the host-built fixture doesn't, so it isn't counted and the budget check fails. I'd fix the fixture rather than the runtime: on macOS it fills its own __meta at load with one PT_LOAD for the array, as ProcessParametersFixture.hpp does for the test program. #7201 would also make it pass, by synthesising headers in dl_iterate_phdr for every host image; the fixture change is test-only, and with it all five guest_flexible_memory_* pass here. @mugurc, you listed it as known in #7631; say so if you'd rather fix it in the runtime.
agc_write_tracking: the cause, not a claim yet. It isn't page size. On macOS, ImportWatched brackets the whole import window in GuestArena::HostWrite, which opens and marks every page because a driver write into a read-only page fails with EFAULT. So every block of the window gets the CPU stamp. A probe here gives changed = {0, 1, 0} and cpu = {1, 1, 1}, so line 1151's cpu[2] == 0 can't hold, although the untouched block still reads as unchanged. #7201 has the matching __APPLE__ test change (UnchangedSince instead of cpu[2] == 0). With it the test gets past that check but then stops in CheckUnwatchedImport: an import that stays watched did not read its range before the import. I'll look at that one next and announce here before any PR.
guest_itimer and guest_kernel_app_archive_mmap: a design question for @mugurc. Both install a guest terminate handler (_ZSt13set_terminatePFvvE_nid_postfix) and expect it to see an exception that libkernel throws: Signals.cpp's SIGALRM "not implemented", and ArchivedFile.cpp's mmap refusal out of the noexceptmmap_nid_postfix. On Linux, GCC_GLOBAL_ALIAS sends those throws through libc.prx's runtime. On macOS, host code keeps libc++abi by design, so libc++abi's terminate runs, prints libc++abi: terminating due to uncaught exception of type std::runtime_error: ... and aborts. The guest handler never runs. I see two ways to fix it:
Runtime-side: on macOS the guest set_terminate also installs a host handler that forwards to the guest one, and __cxa_current_exception_type/__cxa_current_primary_exception fall back to libc++abi's when libc.prx has no current exception. A title's own terminate handler would then see library throws as on Linux, but this mixes the two runtimes.
Which one do you want? I'll only write it once you've picked.
guest_math_exact on an Intel Mac (@rooklz's question) is running on a macos-15-intel runner; I'll post the result when it's done.
The second stop is in the series' describePages, not on main. With #7201's test change for line 1151, the test stops in CheckUnwatchedImport with an import that stays watched did not read its range before the import. The test makes one page PROT_NONE with a plain mprotect. The write watch isn't told, so its logical protection for that page is still read-write. On Apple, describePages (Execution/src/GuestMemory.cpp in ci/macos-series-2, both mach_vm_region loops) then sets readable = (logical & PROT_READ) != 0. That replaces the kernel's answer instead of narrowing it, so CheckRange sees the page as readable and doesn't throw. The read that follows faults, and the watch's handler (Watch::record) restores the logical read-write protection, so the import runs anyway. A probe gives threw=0 ran=1, logical protection 3 for the page.
Arming only removes PROT_WRITE, so the kernel's read bit stays correct. Taking readable = kernel readable && (logical & PROT_READ) and writable = readable && (logical & PROT_WRITE) keeps the watch's write bit without losing a real PROT_NONE. #7201 already does this (region.readable && (watched < 0 || watched & PROT_READ)). #8658 has the same override as the series (GuestMemory.cpp around line 686 at its head). @mugurc@iamthehimansh, that's the change for your branches. Making the test's ProtectPage call GuestWriteWatchProtectionChanged also gets past the check, but it only hides the runtime behaviour.
With #7201's line-1151 change plus the ProtectPage notification, the whole of agc_write_tracking passes here (I haven't run the describePages change itself), so nothing else in the test fails on macOS. So the only main piece is the line-1151 test change. It only matters once a macOS port with a correct describePages lands, so I'll leave it in #7201 unless someone wants it split out.
@rooklzguest_math_exact on a real Intel Mac: it fails there too, at the same check as on the M4, so it isn't Rosetta's x87 emulation.
Intel: GitHub macos-15-intel runner, Intel Core i7-8700B, macOS 15.7.9, Xcode 26.3 (Apple clang 17.0.0, clang-1700.6.4.2), native x86-64. Same tree as our runs: ci/macos-series-2 at 53db0ba with #7524, the code of #7937 and the libSceSsl change of #7332 (tree d071648659). Release build, so -O3 (plus -g for line info). run
M4: Apple clang 21, Rosetta 2, the same build as my earlier notes.
The test's Require only calls abort(), so on both machines I temporarily made it print its line. Both fail first at CheckClassification's first check (line 101):
Together with your result that it passes at -O0, my guess (not checked in the disassembly) is that the optimised build moves the pseudo-denormal through x87 registers. An x87 load and store doesn't keep that encoding, so the library sees a different value from the one the test built. Real Intel hardware would do the same, which fits both machines failing. If so, it's the test (or how the argument reaches libc), not libc.
guest_math also aborts on the Intel runner, with no output. I haven't looked into that one.
@tkcm01 thanks, the Intel run settles it, and your guess checks out on the M5. A standalone probe that builds the same pseudo-denormal (se = 0x0000, mantissa 0x8000000000000000) with memcpy and reads its exponent field back through a const long double*:
At -O3 Apple clang materialises the constant through fld/fstp (8 of them in the assembly of a 10-line file), and the x87 load of a pseudo-denormal normalises it: it comes back as 0x0001 with the same mantissa, a plain denormal-range normal, which is what _LDtest and __fpclassifyl then see. So the library gets a different operand from the one the test built, on Rosetta and on real Intel alike, and the test passes at -O0 only because nothing moves the value until the call. It is the test's construction, not libc: the operand has to reach the call through memory the compiler won't route through the FPU (build it in the callee from bytes, or keep it in a byte array and memcpy into the argument at the call). GCC on Linux keeps the bytes as they are, which is why main's CI never saw it.
Activity
boykopovar commentedon Sep 24, 2026
I'm not against a pull request for macOS support if:
But for macOS, I'm more concerned about the libc.prx (especially the work of exceptions) and libkernel.prx implementation. These libraries contain a lot of platform-specific code.
Vladislav-Kalinkin commentedon Sep 25, 2026
Hello. Just about macOS. I'm implementing a similar project on Mac related to PS4 (a static recompiler on native Apple Silicon). If you want to support macOS, I won't mind if you use my code to support the processor (wie-project/PS4Recomp).
franpeza commentedon Sep 26, 2026
Hi! I was interested in macOS support, so I started experimenting in my own fork to see how far a port could get. It went further than I expected (I have a game booting and running with sound), so I'm sharing the progress here in case it's useful.
Your conditions from this issue
libc exceptions and libkernel (the concerns you mentioned)
I also fixed a few problems that also affect other platforms (happy to send these as separate PRs)
Not verified yet: the Linux and Windows builds of the shared changes (exception runtime, TLS scan, unwinder helpers). I'd check those before opening any PR.
If this direction works for you, I'd be glad to split it into reviewable PRs.
theChaosCoder commentedon Sep 26, 2026
Fyi Apple is phasing out Rosetta 2, with full support ending after macOS 27 (Golden Gate).
TheGuyWhoIsIdiot commentedon Sep 26, 2026
Support only ends at macOS 28, though users can always dual boot older Mac versions.
theChaosCoder commentedon Sep 26, 2026
nope ends with 27. https://developer.apple.com/news/?id=w5ngl9k2
Vladislav-Kalinkin commentedon Sep 26, 2026
It's surprising that everyone heard that Rosetta 2 is dying, but its active implementation continues. Given that this is not a JIT emulator, it would be logical to make at least a fork with static translation of x64 instructions into C, and from it to ARM and adaptation to the specifics of macOS. It's much more difficult, of course, but I think it's cleaner. I don't know if I'll be able to finish my project (there is no such patience and incentive, because I don't play), but I managed to get to the first Unreal Engine 4 error in Hogwarts Legacy in my PS4 project. That is, at least the processor part is possible.
TheGuyWhoIsIdiot commentedon Sep 26, 2026
After this update, which refers to macOS 28. I still can use Rosetta on a silicon chip Mac running this version, though with an explicit warning.
TheGuyWhoIsIdiot commentedon Sep 26, 2026
I genuinely think this goes against the idea of the project which targets native execution rather than being transformation recompilation.
Vladislav-Kalinkin commentedon Sep 26, 2026
But tell me. And what about, for example, those who buy new Macs, which initially have the minimum version - 28? Even if you imagine the possibility of a rollback, most of you won't even know about it. And also what to do with Wine, Shadps4 and many other projects? “We won't support your version anymore?”
mugurc commentedon Oct 5, 2026
Hi @boykopovar, I have a macOS port working locally, rebased on current
main. It runs x86-64 under Rosetta 2 and was tested on an M5 Max with macOS 26.5. Before opening pull requests I'd like to know how you want it split, and to avoid overlapping with @franpeza's port.Your conditions:
?:/&&;.app. The Linux and Windows builds are unchanged.libc and libkernel:
__cxa_throwtwo-level. libc.prx's runtime serves guest code.PT_GNU_EH_FRAME, using adl_iterate_phdremulation over the original program headers that the relinker keeps.__TEXT,__eh_frame; the prx are linked with-no_compact_unwind.__APPLE__branches:%fsaccesses into stubs;Relinker:
--macoswrites Mach-O executables and guest modules: rebases, flat-namespace binds, the export trie, guest TLS (including general-dynamic TLS across modules) and init/fini.--to-intelplaces its stubs there too. For Rosetta it also lowers RDSEED, RDPID and CLWB. Rosetta runs AVX2, FMA and BMI, but not SSE4a, SHA, ADX, RDSEED, RDPID, CLWB, CLZERO or MONITORX.Tested on macOS:
The code was written with AI assistance; I reviewed and tested it.
Proposed split, each against
main:__gcc_personality_v0exported for guest C code (it is missing on every platform), and strtoimax/strtoumax errno.relinker --macos(depends on 3).@franpeza, you mentioned a port that boots a game. I'd rather combine our work than duplicate it: if you are about to open PRs, tell me which parts you cover and I'll adapt mine.
ymatrix1 commentedon Oct 5, 2026
I'm currently helping other similar project that supports MacOS, but are still very early stage https://github.com/mattias800/prosper . Basically the idea is to work like Wine but for PS5, it doesn't need to rebuild the binaries to the platform.
boykopovar commentedon Oct 6, 2026
@mugurc Have you tested libc exception runtime modified on windows or linux? I'm also interested in whether there is any dependency on Rosetta, and whether your version will work without emulation on macOS with x86-64 (it turns out there are some). I think you can open a pr to support exceptions first.
mugurc commentedon Oct 6, 2026
@boykopovar Not yet: I only have this Mac, so it was tested on macOS only. Here is what the exception changes touch outside macOS:
__gcc_personality_v0is also exported to guest code now, because guest C built with-fexceptionsimports it, andCallPersonalityrecognizes that export. On Linux and Windows the host__gxx_personality_v0and__gcc_personality_v0are unchanged; they forward to the same code.exception_ptrmodel and thetype_infodeclarations are only compiled with libc++/libc++abi (_LIBCPP_VERSION,_LIBCPPABI_VERSION). The Linux and Windows builds use libstdc++, so they don't see them.__TEXT,__eh_frameand thedl_iterate_phdremulation over the guest program headers are__APPLE__only.The PR's CI will run
exception_runtimeandexception_c_cleanupon Linux. They areUNIX-only, so Windows only gets the build.Rosetta: the build is plain x86-64 macOS, and nothing checks for Rosetta at run time. The one Rosetta-specific step is in the relinker:
--macoslowers RDSEED to RDRAND, drops CLWB and turns RDPID into 0, because Rosetta lacks them. That output still runs on an Intel Mac, where it's just unnecessary, so I can put it behind its own flag and leave Intel output as it is. I have no Intel Mac, so I can't test there.I'll open the exceptions PR first, against current
main.GeorgeHulpoi commentedon Oct 6, 2026
+1. It would be nice to have support for MacOS.
164 remaining items
rooklz commentedon Oct 22, 2026
Two more, and a correction:
guest_host_standard_erroris test(libkernel): throw through the guest runtime on macOS in the host standard error test #9654, a test-only change. Its last check looks for libstdc++'swhat(): report kept, and on macOS libc++abi words the same reportlibc++abi: terminating due to uncaught exception of type std::runtime_error: report kept. The report does reach the host's stderr. Correction to my note above: the unwind tables don't matter for this one. Thewaitpidfailure I saw wasEINTRfrom running it under lldb; outside the debugger it only ever failed at the wording.guest_math_exactpasses here when the test is built at-O0and fails at the optimised build, with the same libc. Every check in it compares 80-bitlong doublebits (signalling NaN pass-through,errno), so I suspect Rosetta's x87 emulation rather than libc. I'm not taking it; if someone has an Intel Mac, a run there would settle it.krishalivaibhav commentedon Oct 22, 2026
Balatro (PPSA21402) on a fanless M2: what limited it to ~40 FPS, and 60 FPS with two changes. Update to my 13 October note above. Setup: MacBook Air M2 (no fan), 16 GB, macOS 27.2, MoltenVK 1.4.2, x86-64 under Rosetta 2, 1920x1080, the macOS series plus local changes (not
main, which does not build on macOS).Symptoms. 60 FPS on the menu at first, falling to ~40 after 1.5–3 minutes, and 11–22 FPS on the shop and booster-pack screens. powermetrics showed the drop is thermal: the GPU was ~95% busy at full clock, then the Air lowered its GPU clock (1365 → 840 MHz) with the same work per frame. So the fix had to be less GPU work per frame, not a higher frame cap.
Cause 1 — barycentric emulation on sprite draws (shop and pack screens). The title's sprite/text pixel shader keeps I/J live after its
v_interppairs, only as the value lanes outside the export mask keep. That sends every such draw through the generated tessellation stages, and on MoltenVK each one restarts the Metal render encoder (a full 1080p tile store and load). The pack screen issues ~730 of these per frame. A 1x1 scissor on them changed nothing; skipping only their draw call took the screen from ~11 to ~34 FPS. #9193 (imellag) fixes this by reading I/J fromBaryCoordKHRon MoltenVK. On the same untouched pack scene with warm caches: 45.4 → 60.0 FPS, frame p99 25.4 → 18.4 ms, GPU busy 21.3 → 12.4 ms per frame; afterwards only the title's rect-list draws still use tessellation (about 4 per frame, under 1 ms). Details and traces are on #9193. I had written a narrower fix for the same case and am not opening it.Cause 2 — exact GCN float emulation in full-screen shaders. The menu background (a procedural swirl, ~7 ms) and the CRT pass (~3.8 ms) are full-screen 1080p shaders, and almost all of their cost is per-pixel ALU: every f32 operation carries NaN-propagation and denormal-flush selects, and each
v_sin/v_coscomputes the native result plus two or three polynomial series. MoltenVK fast math made no difference. A local, opt-in experiment that drops those (native min/max, plain mul/add/mad, native sin/cos) cut menu GPU time ~22%. It deliberately gives up bit-exact GCN behaviour, so it stays a local experiment, not a PR; I'd welcome views on whether a per-title or user option for that would be acceptable.Result. With the float experiment plus my narrower fix for cause 1 (the session predates testing #9193, which measured better on the pack scene), a 5-minute session with shop, packs and normal play held 58–60 FPS throughout, including past the point where the Air used to throttle, with GPU time at 10–11 ms per frame and no visual problems seen. Clip of the #9193 build in normal play, FPS in the title bar:
balatro-m2-pr9193-60fps.mp4
What's left. The command submission thread is 94–99% busy at 60 FPS on the pack screen (~455 draws per frame at ~20 µs each; vertex data is already used in place), so CPU is now the tightest margin. A finding worth knowing for anyone profiling on Mac: per-pass GPU timestamps (pass splits) cost several FPS by themselves here, and the overlapping per-pass ranges are not exclusive GPU time; the fence-based
gpu_busy_ms_per_presentin the trace summary is the reliable number.AI-assisted: yes (Claude Code). Measurements are from my M2; gameplay observations are mine.
imellag commentedon Oct 22, 2026
Proposal: keep the f32 NaN emulation exact, but pay for it only when a NaN appears. This is about the same cost as @krishalivaibhav's "cause 2" above. It's a design I'd like feedback on before writing it.
What it costs today.
TranslationContext::nanResultF32wraps every f32 add, sub, mul and fma: if a source is NaN, the result is the first NaN source (quieted in IEEE mode), otherwise a generated NaN becomes0xffc00000. In MSL that is three or four(x & 0x7fffffff) > 0x7f800000tests and selects per operation. Apple GPUs return0x7fc00000for every NaN result, so the payload can't come from the hardware.Measured on an M4 Pro (macOS 27.0.1, MoltenVK 1.4.2),
main+ #8658 + #9193 + #9263 + #8612. The off arm uses a local switch that returns the raw result; it is for measurement only and not exact:The f32 denormal flush already runs in hardware on this stack (#8578:
0 of 5472 probe results differ), so these selects are the main per-operation emulation cost left.What I tried. A Metal compute microbenchmark: 16M threads, 4 independent chains of 48 mul/add/fma each,
mathMode = .safe, variants interleaved over 40 rounds after warm-up, median GPU time. Every exact variant's output is compared bit for bit with the current form.if (isnan(r)) r = chainA branch per operation (C) saves only about a fifth. Doing a region plain and re-running it exactly only when a NaN shows up (D) costs about the same as plain code while NaNs are rare. It falls back to the cost of today's form when many lanes in a SIMD group hit a NaN.
Proposal (D as an IR pass, exact for every input):
isnanof each fast result whose NaN could leave the region or be hidden inside it. A result whose only users are other emulated add/sub/mul/fma in the same region needs no check, because those operations always return a NaN when a source is NaN, and their own results are checked. Results used by anything else (legacy multiply's zero select, min/max, compares, conversions, bitcasts, live-outs) are checked.nanResultF32chains and use those results.Why it is exact. When no checked result is NaN, no emulated operation in the region produced a NaN, so every select in today's chain returns the raw result. The fast path's bits are then the exact path's bits. When any result is NaN, the region runs today's code. So the existing emulation stays the only definition of the NaN behaviour, and no new RDNA hardware oracle is needed.
Costs and risks to measure:
Tests: the existing
f32_arithmetic_nan,f32_separate_roundingand denormal tests, plus new cases with a NaN produced in the middle of a region and hidden by a min/max, a compare and the legacy multiply.Questions:
SpirvBdaFault.cpp)?AI-assisted: yes (Claude Code). The measurements are from my M4 Pro.
krishalivaibhav commentedon Oct 22, 2026
@imellag yes: your design would cover the NaN part of my experiment exactly, and on Balatro that is the largest single piece of it. I split the experiment apart to check.
Setup: MacBook Air M2 (fanless), macOS 27.2, MoltenVK 1.4.2, the macOS series (42c3fd0 plus local changes;
maindoesn't build on macOS), Balatro (PPSA21402) main menu, which is dominated by two full-screen 1080p shaders: a procedural background (27v_sin/v_cos, no samples) and the CRT pass (51 samples). Same build for every arm, each arm with its own warm shader cache,APS5_TRACE_SECONDS=12:15, no other trace or profile switch. A local measurement switch returns the raw result fromTranslationContext::nanResultF32(like your off arm, not exact).The f32 denormal flush is already in hardware on the M2 too:
[float-controls] f32 denormal flush: hardware (0 of 5472 probe results differ from the emulated flush), so as on your M4 Pro the NaN selects are what is left of the per-operation cost.Generated code (SPIR-V from the shader dump, through
spirv-cross --msl), the three heaviest pixel shaders:> 0x7f800000NaN testsGPU time per frame (fence-based
gpu_busy_ms_per_present; the menu is vsync-locked, so FPS stays at 60 either way):nanResultF32offSo about 0.8–1.7 ms per frame (7–13%) on this scene. For comparison, my earlier opt-in experiment as a whole (NaN, denormal and tiny-product flush off, native min/max/med3/clamp,
v_macas one fma, native sin/cos) measured 12.9 → 10.7–10.9 ms on the same menu; native sin/cos alone was about 1.1 ms of that, and native min/max and the fusedv_macwere within noise.What your design would not reach, from the dumps: the NaN tests left after dropping
nanResultF32come from other paths: the signaling-NaN quieting aroundFPMin/Max/Med3(ieeeMinMaxF32), conversions, and the range handling inside thev_sin/v_cosemulation (each computes the native result plus two or three polynomial series and selects between them). Those would stay at today's cost. The trig series is the other cost worth a separate look for shaders like this background.On the region split: these are long, mostly straight-line shaders (the background's only branches are a few bounds-checked uniform loads at the top), so one region per block would be large; the size cap and register pressure you mention will matter here. If you build it, I can run it on this scene on the M2 and post the same numbers.
AI-assisted: yes (Claude Code). The measurements are from my M2.
mugurc commentedon Oct 22, 2026
@rooklz on
guest_math: here (M5 Max, macOS 26.5.1, the series at 53db0ba) therand_r(nullptr)case passes with the test binary as it is, and the first check that fails isacosf(0.5f)(0x3f860a91), which your #9668 covers.rand_rthrows a hoststd::invalid_argument, which the system unwinder handles with the test's compact unwind. The test's own tables matter only where libc's unwinder walks its frame, that is libc's__cxa_throw(as inguest_bad_optional_access) and the_Unwind_*functions.So I would keep #8311 to the targets that need the tables, rather than change every test executable on macOS. If the
rand_rcase still ends interminateon your machine, could you post what it prints, and whether yourlibc.prxshows__eh_frameundersize -m? If it reproduces, I'll addguest_math_teststo #8311.jotitadev commentedon Oct 22, 2026
Follow-up on my 10-18 note about the two tests that are intermittent on Apple. I said
agc_driver_cross_workgroup_flagdidn't look like store visibility, and that was only partly right. Setup: M2 Max, macOS 27.2, x86-64 under Rosetta, #8658 @ f48446a, MoltenVK 1.4.2 and MoltenVK main + SPIRV-Cross #2704.agc_driver_cross_workgroup_flag: two causes. In a local copy of the test, the reader stores the poll count at which it saw the flag (40 wave64 and 40 wave32 dispatches per process, 6 rounds, plain store and atomic store alternating in the same binary):OpAtomicStoredevice scopeOpAtomicStoredevice scopeOpAtomicLoad, but the GLC store is stillOpStore ... VolatileinEmitBdaStoreAt. In the MSL it is a plain*(device uint*)p = 1u. On Apple the reader never sees that store on its first poll; it takes 300 to 1M polls. With the coherent aligned dword store as anOpAtomicStoreat device scope, it sees the flag on the first poll in about 95% of wave32 dispatches.So a store-side change like #2259's would be correct by the memory model, but it would not make this test reliable on Apple. I'm not proposing it on its own.
agc_driver_uint8x4_color_targetfails in 19 of 150 runs on MoltenVK 1.4.2 and 23 of 150 on MoltenVK main. The_no_residentvariant passes 150 of 150 on both. It is nearly always one of the XOR cases (SW_64KB_D_X target with XOR: byte 0 is 90, expected 49, or theZ_Xone): the target still holds the fill byte (0x5a), so the draw never reached guest memory. In 100 runs each on MoltenVK 1.4.2:host imports resolve write protection: no ... imported ranges stay watched)APS5_WRITE_WATCH_IMPORTS=watchAPS5_WRITE_WATCH_IMPORTS=unwatchAPS5_NO_HOST_IMPORT=1APS5_NO_WRITE_WATCH=1APS5_PACKET_EPOCH=1So on macOS, with host imports that stay watched, the resident target's write-back sometimes doesn't show up in the guest read. The full
^agcset withunwatchgives the same 18 failures as the default, sounwatchcosts nothing in the suite. I haven't found where the write-back is lost: the probe may not cover this case on macOS, or the flush may race with re-arming the protection.Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.
tkcm01 commentedon Oct 22, 2026
@rooklz on
guest_getrlimit: it passes on the M4, 3 runs of 3 (same tree as my earlier list), which fits your explanation. Herekern.maxfilesis 184320,kern.maxfilesperproc92160 andkern.num_filesabout 8550 before and after the runs, so the per-process limit is reached long before the system table and thepipeloop ends withEMFILE. On your machine 122880 − ~70000 open leaves fewer than the 61440 per-process limit, henceENFILE.AI-assisted: yes
Mauro42K commentedon Oct 22, 2026
Hi @Gonic0 — I completed the requested negative controls on my Apple M1 MacBook Pro (8 GB RAM, macOS 27.0.1).
Tested commit: e0a3cce.
I used a separate worktree and build, reverting each of the two macOS-specific blocks in core/libs/prx/libSceNet/Export.cpp independently.
Results:
These results support that both fixes are required for the test to pass on this M1 configuration.
Limitation: CTest recorded the aborts but did not print the exact failing assertion or source line. I also attempted LLDB, but it stalled while launching the x86_64 executable under Rosetta 2, so I cannot independently confirm the specific assertion locations you observed on M5 Pro.
The worktree was restored to a clean state. Detailed logs, patches, and hashes are retained locally.
AI-assisted: yes. Codex was used to perform and document the validation.
rooklz commentedon Oct 22, 2026
M5 data point: 32 catalog homebrew titles run natively, converted with
--macos --to-rosetta, on MoltenVK.Same tree as my earlier notes (
ci/macos-series-2at53db0badbwith #7524, the code of #7937 and the libSceSsl change of #7332), macOS 26.2, Apple clang 21, x86-64 under Rosetta 2; MoltenVK built from the MBridge copy with an x86-64 Vulkan loader, 75 s per title, no game data supplied. The emulator-side stops, grouped:AGC graphics: device does not support mesh shaders(this MoltenVK reports no mesh-shader feature)AGC driver: a pixel shader on the mesh path reads barycentrics, and this device has neither VK_KHR_fragment_shader_barycentric nor mesh shaders(the extension is listed, its feature reports false)AGC graphics: last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled(listed, feature false;MVK_CONFIG_USE_METAL_PRIVATE_API=1does not change it on this build)vkCreateGraphicsPipelines: Vulkan result -3, Metal refusing afloat4output on a UINT attachment: #9783Guest TLS access without a non-empty PT_TLSfrom the tree's relinker (emptyPT_TLSplusfs:accesses);main's relinker converts all five, so this is already fixed thereForbidden syscall instruction; converts with--trap-sdk-syscall-stubsThe rest stop in their own code: PPSA99620 and PPSA99621 (Ship of Harkinian, 2 Ship 2 Harkinian) crash in a guest thread reading
0x20(%r12)withr12null (EXC_BAD_ACCESS, all frames in the title until libkernel'sRunGuest), which looks like a missing-asset null since they have no OTR data here; the others ask for their game files and exit. PPSA99666 DOOM, PPSA47402 Syobon Action and PPSA99429 ApricotGS render frames (AGC compute presentation on DOOM,gpu-display flipcounters on ApricotGS). Bring-up threads exist for PPSA98512 (#9758) and PPSA99630 (#9762).The first four rows are driver or MoltenVK capability gaps rather than title bugs; the mesh-shader and barycentric rows alone account for nine titles. I'm not taking any of them without asking here first.
AI-assisted: yes
Gonic0 commentedon Oct 22, 2026
Thanks @Mauro42K, that's exactly what I was hoping for: both blocks needed on an M1 with 27.0.1 as well as on the M5 Pro with 27.2. I'll add it to #9231's Tested section, crediting you.
AI-assisted: yes
tkcm01 commentedon Oct 22, 2026
guest_filesystemon the M4 (series at 53db0ba): two causes, and with both fixed it passes.EIO. libc++'sfs::equivalentreportserrc::not_supported(notENOENT) when a path doesn't resolve, andRenamePath_nid_no_patchonly clearsENOENT. fix(libc): rename over a dangling symbolic link on macOS #9911 splits that guard out of Macos port #7201/feat: macOS host support (Apple Silicon via Rosetta 2, Vulkan on MoltenVK) #8658 on its own.main: the macOSsceKernelGetdirentriesinFile/src/Stdio.cppopensopenat(fd, ".", O_DIRECTORY), which fails withENOTDIRfor a regular file, sosceKernelGetdentson a plain file returns0x80020014where the test (and FreeBSD) expectEINVAL(0x80020016). Locallyif (listing < 0) return SceErrorFromErrno(errno == ENOTDIR ? GUEST_EINVAL : errno);makesguest_filesystempass, and the full suite shows no new failure. (feat: macOS host support (Apple Silicon via Rosetta 2, Vulkan on MoltenVK) #8658's__getdirentries64version may already behave differently here; I haven't checked it.) The Linux branch also returnsEINVALearly for a guest socket viaGuestSockets::IsOpen, which the macOS branch doesn't do.AI-assisted: yes
tkcm01 commentedon Oct 23, 2026
M4 data point with Vulkan, and four of the remaining stable failures.
Setup: Apple M4 (10-core GPU), macOS 27.0.1, Apple clang 21, x86-64 under Rosetta 2, the same series tree as my earlier list. Unlike @rooklz's M5 setup, no MoltenVK build was needed: in the LunarG Vulkan SDK 1.4.363.0 the loader, MoltenVK 1.4.2,
vulkaninfoand the validation layer are already universal (x86_64 + arm64). I installed it headless into the home folder (--root ~/VulkanSDK/1.4.363.0 ... install copy_only=1, no admin rights), andDYLD_LIBRARY_PATH=<sdk>/macOS/libplusVK_DRIVER_FILES=<sdk>/macOS/share/vulkan/icd.d/MoltenVK_icd.jsonis enough. The x86-64vulkaninforeportsApple M4, MoltenVK 1.4.2, API 1.4.357.ctest -j4 --timeout 120: 1648 tests, 58 failed, 95 skipped (M5: 87 failed, 93 skipped). The non-GPU failures are my earlier list and the two flaky tests. The 44 GPU failures, by first failing check:0x8000comes back0x0000), 13:dpp16_operands,f16_fma_denormal_modes,f16_conversion_float_mode,float16_fma_rounding,float16_misc,float16_modifiers,float16_rounding,ldexp_f16_inline_constants,packed_alu,packed_f16_denormal_modes,sdwa_float_selectors,sdwa_ldexp_f16,vop2_modifier_forms.f32_denormal_flush,f32_output_modifier,f32_constant_factor_tininess. f64:f64_add_mul_denormal_modes,f64_rounding_denormal_modes(the other f64 tests skip).shaderClipDistance/shaderCullDistance:vertex_clip_cull_distances,vertex_clip_cull_draw,vertex_clip_cull_draw_no_key,vertex_user_data_tail.libvulkan.so.1, 7: the sixagc_driver_guest_memory_lifetime*(tests/GuestMemoryLifetimeSupport.hpp) andagc_driver_indirect_arguments_pending_producer. @jotitadev, these two files aren't in test(agc): load the Vulkan loader with portability enumeration on macOS #7886 (feat: macOS host support (Apple Silicon via Rosetta 2, Vulkan on MoltenVK) #8658 changes both); would you add them there? Also, with test(agc): load the Vulkan loader with portability enumeration on macOS #7886 applied andVK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, its instances logVUID-VkDeviceCreateInfo-pProperties-04451(VK_KHR_portability_subsetnot enabled), for example inagc_performance_overlay.pixel_interlock,pixel_sample_coverage.image_storedlc/tfe leaves0xdeadbeef:image_cache_bits,image_texel_status.ds_permute,compute_fs_table_calls,image_srgb_sample_copy(and_forced),image_plane_bias_on_single_level_volume,multisample_retirement,sample_mask_multisample,pixel_sample_iteration,rect_list_indirect,stencil_ref_export_unused,tessellation.agc_performance_overlay_statistics: taking it, PR today. It is @jotitadev's measurement from #7886:ReadProcessPrivateBytesreads/proc/self/status, andtask_info(TASK_VM_INFO).phys_footprinton macOS makes it pass. #7332 had it and is closed; #8658 has it too. I'm splitting it out with both credited. Here it makesagc_performance_overlay_statisticspass, andagc_performance_overlaytoo once #7886 is applied.guest_flexible_memory_declared: taking it, PR today. Thedeclaredcase loadsguest_flexible_memory_module.guest.prx, which is host-built, and expects its 512 MiB array inGuestImageSegmentBytes. macOS'sdl_iterate_phdr(specifics/macos/GuestImages.cpp) only reports images with__APS5DATA,__meta. Every relinked module has that section, but the host-built fixture doesn't, so it isn't counted and the budget check fails. I'd fix the fixture rather than the runtime: on macOS it fills its own__metaat load with onePT_LOADfor the array, asProcessParametersFixture.hppdoes for the test program. #7201 would also make it pass, by synthesising headers indl_iterate_phdrfor every host image; the fixture change is test-only, and with it all fiveguest_flexible_memory_*pass here. @mugurc, you listed it as known in #7631; say so if you'd rather fix it in the runtime.agc_write_tracking: the cause, not a claim yet. It isn't page size. On macOS,ImportWatchedbrackets the whole import window inGuestArena::HostWrite, which opens and marks every page because a driver write into a read-only page fails withEFAULT. So every block of the window gets the CPU stamp. A probe here giveschanged = {0, 1, 0}andcpu = {1, 1, 1}, so line 1151'scpu[2] == 0can't hold, although the untouched block still reads as unchanged. #7201 has the matching__APPLE__test change (UnchangedSinceinstead ofcpu[2] == 0). With it the test gets past that check but then stops inCheckUnwatchedImport:an import that stays watched did not read its range before the import. I'll look at that one next and announce here before any PR.guest_itimerandguest_kernel_app_archive_mmap: a design question for @mugurc. Both install a guest terminate handler (_ZSt13set_terminatePFvvE_nid_postfix) and expect it to see an exception that libkernel throws:Signals.cpp's SIGALRM "not implemented", andArchivedFile.cpp'smmaprefusal out of thenoexceptmmap_nid_postfix. On Linux,GCC_GLOBAL_ALIASsends those throws through libc.prx's runtime. On macOS, host code keeps libc++abi by design, so libc++abi's terminate runs, printslibc++abi: terminating due to uncaught exception of type std::runtime_error: ...and aborts. The guest handler never runs. I see two ways to fix it:std::set_terminateand read the exception throughstd::current_exception.set_terminatealso installs a host handler that forwards to the guest one, and__cxa_current_exception_type/__cxa_current_primary_exceptionfall back to libc++abi's when libc.prx has no current exception. A title's own terminate handler would then see library throws as on Linux, but this mixes the two runtimes.Which one do you want? I'll only write it once you've picked.
guest_math_exacton an Intel Mac (@rooklz's question) is running on amacos-15-intelrunner; I'll post the result when it's done.AI-assisted: yes
tkcm01 commentedon Oct 23, 2026
Follow-up on
agc_write_tracking(M4, same tree).The second stop is in the series'
describePages, not onmain. With #7201's test change for line 1151, the test stops inCheckUnwatchedImportwithan import that stays watched did not read its range before the import. The test makes one pagePROT_NONEwith a plainmprotect. The write watch isn't told, so its logical protection for that page is still read-write. On Apple,describePages(Execution/src/GuestMemory.cppinci/macos-series-2, bothmach_vm_regionloops) then setsreadable = (logical & PROT_READ) != 0. That replaces the kernel's answer instead of narrowing it, soCheckRangesees the page as readable and doesn't throw. The read that follows faults, and the watch's handler (Watch::record) restores the logical read-write protection, so the import runs anyway. A probe givesthrew=0 ran=1, logical protection 3 for the page.Arming only removes
PROT_WRITE, so the kernel's read bit stays correct. Takingreadable = kernel readable && (logical & PROT_READ)andwritable = readable && (logical & PROT_WRITE)keeps the watch's write bit without losing a realPROT_NONE. #7201 already does this (region.readable && (watched < 0 || watched & PROT_READ)). #8658 has the same override as the series (GuestMemory.cpparound line 686 at its head). @mugurc @iamthehimansh, that's the change for your branches. Making the test'sProtectPagecallGuestWriteWatchProtectionChangedalso gets past the check, but it only hides the runtime behaviour.With #7201's line-1151 change plus the
ProtectPagenotification, the whole ofagc_write_trackingpasses here (I haven't run thedescribePageschange itself), so nothing else in the test fails on macOS. So the onlymainpiece is the line-1151 test change. It only matters once a macOS port with a correctdescribePageslands, so I'll leave it in #7201 unless someone wants it split out.AI-assisted: yes
tkcm01 commentedon Oct 23, 2026
@rooklz
guest_math_exacton a real Intel Mac: it fails there too, at the same check as on the M4, so it isn't Rosetta's x87 emulation.Intel: GitHub
macos-15-intelrunner, Intel Core i7-8700B, macOS 15.7.9, Xcode 26.3 (Apple clang 17.0.0, clang-1700.6.4.2), native x86-64. Same tree as our runs:ci/macos-series-2at 53db0ba with #7524, the code of #7937 and the libSceSsl change of #7332 (treed071648659). Release build, so-O3(plus-gfor line info). runM4: Apple clang 21, Rosetta 2, the same build as my earlier notes.
The test's
Requireonly callsabort(), so on both machines I temporarily made it print its line. Both fail first atCheckClassification's first check (line 101):Together with your result that it passes at
-O0, my guess (not checked in the disassembly) is that the optimised build moves the pseudo-denormal through x87 registers. An x87 load and store doesn't keep that encoding, so the library sees a different value from the one the test built. Real Intel hardware would do the same, which fits both machines failing. If so, it's the test (or how the argument reaches libc), not libc.guest_mathalso aborts on the Intel runner, with no output. I haven't looked into that one.AI-assisted: yes
rooklz commentedon Oct 23, 2026
@tkcm01 thanks, the Intel run settles it, and your guess checks out on the M5. A standalone probe that builds the same pseudo-denormal (
se = 0x0000, mantissa0x8000000000000000) withmemcpyand reads its exponent field back through aconst long double*:At
-O3Apple clang materialises the constant throughfld/fstp(8 of them in the assembly of a 10-line file), and the x87 load of a pseudo-denormal normalises it: it comes back as0x0001with the same mantissa, a plain denormal-range normal, which is what_LDtestand__fpclassifylthen see. So the library gets a different operand from the one the test built, on Rosetta and on real Intel alike, and the test passes at-O0only because nothing moves the value until the call. It is the test's construction, not libc: the operand has to reach the call through memory the compiler won't route through the FPU (build it in the callee from bytes, or keep it in a byte array andmemcpyinto the argument at the call). GCC on Linux keeps the bytes as they are, which is whymain's CI never saw it.AI-assisted: yes