Skip to content

feat(libkernel): start the guest off the main thread on macOS - #11048

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
imellag:feat/libkernel-macos-main-thread
Oct 27, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
imellag:feat/libkernel-macos-main-thread

Conversation

@imellag

@imellag imellag commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What

This is mugurc's 8191f76dd6 from his macOS series (ci/macos-series-2, listed on #1), rebased onto main. 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 makes Aps5StartGuest_nid_no_patch the entry of a relinked executable (MacOsImage.cpp), and no library defines it.

  • MainThreadMacOs.cpp (libkernel, under __APPLE__): Aps5StartGuest runs 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 with exit; a guest entry that returns aborts. Aps5RunOnMainThread runs a function on the main thread and waits for it, or in place when the caller is the main thread or Aps5StartGuest was never called.
  • libSceVideoOut sends its SDL calls through 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 Windows OnMainThread calls the function in place, so the order of the calls is the same as before.
  • The flip test starts its tests through Aps5StartGuest on 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 from main 035ac96edd: #10366, #10552, #7631, #7804, #11249, #10669, #10540, #11447, #11047, this PR, #10876 and #9263, plus a local VulkanDevice.cpp patch 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). main alone doesn't build the libraries on macOS, so the tree is main 61fc572cee with #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).

  • Without this PR (with feat(libkernel): thread-local storage for Mach-O guests #11047): the guest aborts when the presenter creates its window: DisplayWindow: NSWindow should only be instantiated on the main thread!.
  • With both: Lana boots, loads the save and plays in the village (27 to 30 fps from 85 s in each of 10 runs of 120 s), and the scripted pad presses that start the game arrive through the main thread's event polling.
  • VideoOut tests, ctest -R 'video_out|flip' -V with ANYPS5_REQUIRE_VULKAN=1, ANYPS5_NO_SHADER_CACHE=1 and VK_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 has was not found but was requested by env var VK_INSTANCE_LAYERS or any VUID-.

Linux: VideoOutDriver.cpp, Flip.cpp and MainThreadMacOs.cpp pass g++ -std=gnu++20 -fsyntax-only with GCC 14 (gcc:14 image, x86-64, both values of APS5_ENABLE_TIMING_LOG). Not built on Windows.

check_conventions.py --base upstream/main passes. pr_overlap.py reports #7622 (an include next to the new one) and #8984 (the SDL hint at the start of the constructor, now inside OnMainThread); 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 with main, 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 with main and says no merge into main is implied. If either lands first with these functions, this PR is not needed and I will close it.

Checklist

  • Based on current 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)
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: Apple M4 Pro, MoltenVK 1.4.2 (Vulkan SDK 1.4.363.0), macOS 27.0.1
  • AI-assisted: yes

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 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

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.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 5021 core/libs/prx/libSceVideoOut/include/MainThread.hpp
core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp
PR 7622 core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp L15
PR 8984 core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp L300

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.

Pull request Shared files not compared
PR 674 core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp
PR 4563 core/libs/prx/libkernel/CMakeLists.txt
PR 4630 docs/dev/debt/libkernel.md
PR 5021 core/libs/prx/libSceVideoOut/include/MainThread.hpp
core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp
core/libs/prx/libSceVideoOut/tests/Flip.cpp
core/libs/prx/libkernel/CMakeLists.txt
core/libs/prx/libkernel/Pthread/src/MainThreadMacOs.cpp
docs/dev/debt/libkernel.md
PR 6149 docs/dev/debt/libkernel.md
PR 6936 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 6977 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 7088 docs/dev/debt/libkernel.md
PR 7105 docs/dev/debt/libkernel.md
PR 7121 docs/dev/debt/libkernel.md
PR 7149 docs/dev/debt/libkernel.md
PR 7192 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7447 docs/dev/debt/libkernel.md
PR 7509 docs/dev/debt/libkernel.md
PR 7804 docs/dev/debt/libkernel.md
PR 8605 docs/dev/debt/libkernel.md
PR 9156 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9199 docs/dev/debt/libkernel.md
PR 9242 docs/dev/debt/libkernel.md
PR 9284 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9293 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9297 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9302 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9383 core/libs/prx/libkernel/CMakeLists.txt
PR 9389 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9431 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9432 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9491 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9557 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9566 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9575 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9672 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9673 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9674 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9675 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9676 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9842 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9891 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9892 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9893 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9895 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 9947 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10192 docs/dev/debt/libkernel.md
PR 10249 core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp
PR 10319 docs/dev/debt/libkernel.md
PR 10435 core/libs/prx/libSceVideoOut/tests/Flip.cpp
PR 10442 docs/dev/debt/libkernel.md
PR 10558 docs/dev/debt/libkernel.md
PR 10725 docs/dev/debt/libkernel.md
PR 10749 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11002 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11042 docs/dev/debt/libkernel.md
PR 11085 docs/dev/debt/libkernel.md
PR 11215 docs/dev/debt/libkernel.md
PR 11223 docs/dev/debt/libkernel.md
PR 11264 docs/dev/debt/libkernel.md
PR 11267 docs/dev/debt/libkernel.md
PR 11341 docs/dev/debt/libkernel.md
PR 11414 core/libs/prx/libSceVideoOut/tests/Flip.cpp

iamthehimansh added a commit to iamthehimansh/AnyPS5 that referenced this pull request Oct 26, 2026
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 jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, Aps5RunOnMainThread runs on the main thread. 4 guest threads × 2000 concurrent calls all run on the main thread (8000/8000).
  • A guest that calls exit with an atexit handler that itself calls Aps5RunOnMainThread (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.

@adriadam10
adriadam10 merged commit 5c356f4 into boykopovar:main Oct 27, 2026
12 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the diff: all SDL/AppKit calls and the input objects' lifecycle go through OnMainThread (in place on Linux and Windows, so call order there is unchanged), exceptions are carried back to the caller, the guest-entry thread gets the main thread's stack size, and the debt entry is in place. The macOS build and title runs are as reported in Tested (and by @MarcZX10); the Linux and Windows builds are covered by the queue's Build run; merged.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants