Repository navigation
feat(libkernel): start the guest off the main thread on macOS - #11048
Conversation
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 emits a call to Aps5StartGuest as the entry of a relinked executable; no library 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. Co-authored-by: mugurc <mceker@gmail.com>
MarcZX10
left a comment
There was a problem hiding this comment.
Reviewed exact head 30cc4ea. The guest entry moves to a pthread with an adequate stack while the original main thread runs the CFRunLoop. Aps5RunOnMainThread executes inline on the main thread and synchronously dispatches from the guest thread, carrying exceptions back. VideoOut SDL and AppKit operations and input-object lifecycle are routed through that helper; the remaining SDL_GetWindowSize reads SDL cached dimensions. The author reports 34/34 focused VideoOut tests passing with matching validation logs on macOS and a 10-run Planet of Lana session with #11047. Hosted platform product jobs are skipped by path policy; I could not independently run the Apple build or runtime here.
|
Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.
Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.
|
imellag's earlier boykopovar#11047 (guest TLS) and boykopovar#11048 (guest start, main-thread SDL) carry the same libkernel imports this PR added in libc, and boykopovar#10876 carries the same ApplicationProcessParameters change. Drop those parts here and keep only what no open PR has: the 0x400-byte reserve after the Mach-O load commands, so codesign's LC_CODE_SIGNATURE no longer overwrites the entry stub.
jotitadev
left a comment
There was a problem hiding this comment.
Ran head b05ca4e4b on a second Apple Silicon machine. I didn't run video_out_flip: I keep window and present tests off this machine. Instead I checked Aps5StartGuest and Aps5RunOnMainThread directly.
Machine: Apple M2 Max (32 GB), macOS 27.2 (26B5112a), Apple clang 21.0.0, x86_64 Release under Rosetta 2 (-DCMAKE_OSX_ARCHITECTURES=x86_64, Ninja, BUILD_TESTING=ON, Python 3.9 from Xcode).
Base: main b986c11a34 does not build the libraries on macOS by itself, so the tree is that main plus the open macOS build PRs (#10366, #7631, #10659, #10660, #11249, #11358, #10669, #10552, #7804, #10676, #11447, #11437) and #11554, plus #11705 (applied locally). Only this PR is toggled, with git merge --no-commit on top.
The harness is a small x86_64 program linked against the built libkernel.prx, resolving both symbols with dlsym. Results, 3/3 each with this PR:
Aps5StartGuest(entry, block): the entry gets(block, nullptr), runs off the main thread (pthread_main_np() == 0), and has a stack of at least 8 MiB.- From the guest thread,
Aps5RunOnMainThreadruns on the main thread. 4 guest threads × 2000 concurrent calls all run on the main thread (8000/8000). - A guest that calls
exitwith anatexithandler that itself callsAps5RunOnMainThread(as a static destructor that shuts SDL down would): the handler runs on the main thread, with no deadlock. - Without
Aps5StartGuest, a call from a non-main thread runs in place. - An entry that returns aborts with
guest entry returned(rc 134), as described.
Without this PR, the symbols are missing from libkernel.prx.
Non-GPU suite (ctest -E "^agc|gpu|vulkan|video_out", 1372 tests): 78 fail on the base and the same 78 fail with this PR. The full tree, including video_out_flip_tests, builds.
Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.
|
Checked the diff: all SDL/AppKit calls and the input objects' lifecycle go through |
What
This is mugurc's
8191f76dd6from his macOS series (ci/macos-series-2, listed on #1), rebased ontomain. mugurc is co-author of the commit.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.
main's macOS relinker makesAps5StartGuest_nid_no_patchthe entry of a relinked executable (MacOsImage.cpp), and no library defines it.MainThreadMacOs.cpp(libkernel, under__APPLE__):Aps5StartGuestruns the guest entry on a new thread with the main thread's stack size (at least 8 MiB) and serves the main dispatch queue from the main thread's run loop. The guest ends the process withexit; a guest entry that returns aborts.Aps5RunOnMainThreadruns a function on the main thread and waits for it, or in place when the caller is the main thread orAps5StartGuestwas never called.OnMainThread(MainThread.hpp), which carries exceptions back to the caller: SDL init and shutdown, the window, its Vulkan surface and drawable size, event polling, and the pad, mouse, keyboard and IME input, which are now created and destroyed there (std::optional). On Linux and WindowsOnMainThreadcalls the function in place, so the order of the calls is the same as before.Aps5StartGueston macOS, as a relinked guest does.The libkernel debt file gets an entry for the macOS guest thread.
Tested
Current
main(2026-10-26): I looked for the smallest set of open PRs that boots titles on macOS frommain035ac96edd: #10366, #10552, #7631, #7804, #11249, #10669, #10540, #11447, #11047, this PR, #10876 and #9263, plus a localVulkanDevice.cpppatch that no open PR covers yet. With it, Planet of Lana and God of War Sons of Sparta, relinked with that tree, start from New Game and run without errors (Lana in gameplay for 120 s, GoW to the Cyclops fight in 145 s). Without this PR, the relinked eboot stops in dyld:symbol not found in flat namespace '_Aps5StartGuest_nid_no_patch'.Apple M4 Pro, macOS 27.0.1, Apple clang 21, x86-64 Release under Rosetta 2 (
-DCMAKE_OSX_ARCHITECTURES=x86_64, Ninja,BUILD_TESTING=ON).mainalone doesn't build the libraries on macOS, so the tree ismain61fc572ceewith #10423, the open macOS build PRs (#10366, #10419, #10552, #7631, #7804, #4630, #10540, #10659, #10660, #10669, #10676), #10889, #10898 and the parts of mugurc's series that #7201 also carries. Planet of Lana (PPSA19450) is relinked with that tree's relinker (--macos --to-rosetta).DisplayWindow: NSWindow should only be instantiated on the main thread!.ctest -R 'video_out|flip' -VwithANYPS5_REQUIRE_VULKAN=1,ANYPS5_NO_SHADER_CACHE=1andVK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, without and with this change on the same tree: 34 of 34 pass in both, with the same skips (the flip tests run on SDL's offscreen driver, which has no Vulkan on macOS). Neither log haswas not found but was requested by env var VK_INSTANCE_LAYERSor anyVUID-.Linux:
VideoOutDriver.cpp,Flip.cppandMainThreadMacOs.cpppassg++ -std=gnu++20 -fsyntax-onlywith GCC 14 (gcc:14image, x86-64, both values ofAPS5_ENABLE_TIMING_LOG). Not built on Windows.check_conventions.py --base upstream/mainpasses.pr_overlap.pyreports #7622 (an include next to the new one) and #8984 (the SDL hint at the start of the constructor, now insideOnMainThread); whichever lands second takes the other's line into the moved block. It also lists #5021, which adds files with the same names.#7201 (the whole macOS port) and #5021 (the native ARM64 AOT draft) carry their own versions of these functions. #7201's head (
6687cbac29, 2026-10-16) conflicts withmain, needs a rebase anyway, and is being split into smaller PRs such as #10552 and #10419. #5021 is a draft of more than 43,000 lines that conflicts withmainand says no merge intomainis implied. If either lands first with these functions, this PR is not needed and I will close it.Checklist
main; no other open PR implements the same functions (except Macos port #7201 and feat(aot): consolidate native ARM64 compiler, runtime and consumers #5021, see the end of Tested)NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)