WIP: feat(macos): native Darwin threads, fibers, restart, process I/O and profiler #490
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!490
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/darwin-host"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is for: the macOS port (#420). The POSIX layer (
host/posix/win32_posix.c) is written for Linux: onmacOS it does not compile (Linux
ucontextfibers,cpu_set_t,/proc,dlinfo), and the profiler, restart,child processes, the menu's share functions, the memory notice and Discord presence had no macOS route. This PR gives
the host a native Darwin implementation of each: Mach thread identities and times, guarded native fibers that may
migrate between threads, executable and module paths from dyld, same-process restart and child processes through
posix_spawn, Darwin memory counters, the low-memory notice fromhw.memsize, and a profiler that samples interruptedthreads through Mach. It also changes Linux in three places, listed under "Changes on Linux": Discord IPC, the
thread-creation error paths, and
scripts/prof.py. What is measured:tests/test_darwin_host.pybuilds the realhost objects and runs seven groups of checks on arm64 and on x86_64 under Rosetta; it fails on
main, and onmainwith the six pull requests this one carries, and passes here, two interleaved runs each.
tests/test_low_memory_notice.pyand
tests/test_prof_symbols.pyfail onmainon macOS and pass here. And the full game-free suite on an Apple M4 Max(macOS 26.7.1), compared by test name with
main. Not measured: a game run on this branch; Windows and Linux runs;Discord itself; macOS 14.4; an Intel Mac.
Depends on #454 (run-all), #477 (arm64-host), #478 (guest-memory), #436 (fault-contexts), #437 (waits) and #438
(vk-admission). This branch carries their commits unchanged, merged in that order (
9ebd6ae8,56b5ed6b,9c679145,7731dc0c,0d62666d,91d8cc35); its own commits start at5d3abc9a.Why
With the six pull requests above,
win32_posix.cstill stops on macOS at its Linux-only parts, and the host has nomacOS answer for the thread, process and profiling calls the game and the renderer make:
Problem
ucontext, which Darwin deprecates and does not implement as an arm64 fiber switch.The game's
nn::osfibers run on them (host/hos_thread.c:CreateFiberEx,SwitchToFiber), andhos_setjmp.c's nonlocal slots belong to the running thread, not to the fiber.SYS_gettid, names frompthread_setname_npon any thread, priorities fromper-thread nice, affinity and topology from
cpu_set_t. Darwin has none of these in that form./proc/self/exeand a module's fromdlinfo/link_map.Restart reads the startup command and environment back from
/proc/self/cmdlineand/proc/self/environ, closesdescriptors through
/proc/self/fdand re-executes/proc/self/exe; the menu's child processes usepipe2and/proc/self/fdtoo. macOS has no/procand nopipe2.GetProcessMemoryInforeads/proc/self/status, the low-memory notice readssysinfo,and the profiler (
HOS_PROFILE,HOS_SAMPLE,HOS_THREAD_TIMES) uses Linux signals and/proc.scripts/prof.pyreads ELF and Windows maps only.
xclip/xdg-open; the Discord client connects withsizeofof thesocket address and no close-on-exec.
What changed
macOS (everything below is inside
#if defined(__APPLE__)or in new Darwin-only files)win32_posix.c): real pthread and Mach identities. The Win32DWORDthread id is the Mach thread portname; the full 64-bit native id is kept separately (
darwin_thread_id) and the profiler uses it. Names are set onthe kernel for the calling thread; a name set on another thread is kept and queryable but returns
S_FALSE, sinceDarwin only lets a thread name itself; an oversized name is refused. Priorities use
THREAD_PRECEDENCE_POLICY(a per-thread relative importance; the process's nice is untouched).GetThreadTimesreads live Mach times and keeps an exited thread's.GetSystemInforeports the real architectureand page size. Stacks are rounded to the host page with a guard page. Affinity and logical-CPU topology fail with
ERROR_NOT_SUPPORTEDinstead of inventing masks;HOS_PIN_BY_COREand the renderer's reserved core say once in thelog that Darwin has no binding mask;
CREATE_SUSPENDEDis refused.host/posix/darwin_fiber.c, new): native arm64 and x86-64 System V contexts with guard pages at both endsof each stack. A fiber can resume on another thread; a thread's own stack cannot. The outgoing fiber is published as
resumable only once its registers are saved and the stack pointer is on the new stack.
x18, the thread pointer, TLSand signal masks are never carried across. Deleting or leaving the running fiber first returns to the thread's root
stack, so cleanup never unmaps the stack it runs on.
hos_setjmp.ctakes its nonlocal slots from the current fiber,so a suspended fiber's slots outlive the thread that made it.
win32_posix.c): the executable's path from dyld and libproc;GetModuleHandleA(NULL)is theexecutable's Mach-O header even when called from the renderer; address lookups use
dladdr; loaded handles arerecorded with their path instead of casting a handle to
link_map. A packaged library beside the executable winsover an installed one, and a broken packaged file fails instead of falling back. A failed load keeps
dlerror()'stext for
hos_load_error()(main's #34), as on Linux.host/hos_relaunch.c,host/posix/darwin_process.c, new):mainkeeps thestartup command line and a copy of the environment before anything changes them (after #321's
hos_game_layout(),which changes neither). Restart re-executes that snapshot in the same process through
POSIX_SPAWN_SETEXEC, withPOSIX_SPAWN_CLOEXEC_DEFAULTso only the standard descriptors survive. Child processes remap 0/1/2 explicitly; pipesuse
pipeplusfcntlclose-on-exec andF_SETNOSIGPIPE; waits retry onEINTRand reap their children.gpu/vk/vk_menu.cpp):share_spawn,share_clipboardandshare_openuse/usr/bin/pbcopyand/usr/bin/openwith separated arguments.host/posix/psapi.h): resident, peak (Darwin's peak is in bytes) and the physical footprint ledger;the private and pagefile fields are the footprint, which includes compressed memory. They are not Linux's
RssAnonor Windows' commit.
host/hos_main.c): RAM fromhw.memsize, allocated swap fromvm.swapusage. The warningdepends on physical RAM alone (below 10240 MiB), and its text says macOS grows swap as needed rather than treating
the current swap as a limit. When the swap query fails, the notice still shows, without a swap figure.
host/posix/darwin_prof.c, new; executable only):HOS_PROFILE,HOS_THREAD_TIMES,HOS_SAMPLE,HOS_SAMPLE_ALL,HOS_SAMPLE_FILEandHOS_CHAINas on Linux. Sampling suspends only its target thread, reads theinterrupted PC, SP and FP from its Mach thread state, walks the frame records, and resumes it before any logging or
allocation; no signal handler is installed. This is a frame-record walker, not an unwinder: callers without frame
records may be missing, though the interrupted PC is always recorded.
HOS_WATCHsays that hardware watchpoints areunavailable and none were armed.
scripts/prof.py: reads thin 64-bit Mach-O symbols (chosen by file magic, so the extensionlesslongshotworks)relative to the image's preferred header address, names C++ frames through
c++filt -n, and countslongshot_vk.dylibas the renderer.RX_MAPSmust name the same build and architecture as the capture; a fat orunreadable image is reported on stderr.
host/hos_main.c): on Apple, after the game thread is joined,mainends throughExitProcess(0), asthe game's other exits do, instead of returning and running C++ static destructors under the remaining threads.
docs/development.md):RX_MAPSon macOS.Changes on Linux
host/discord_rpc.c(Linux and macOS share this client):SOCK_CLOEXEC, so a child process no longer inherits the Discord connection;connectgets the exactsockaddr_unlength (path plus its NUL) instead ofsizeofthe structure;sun_pathis skipped withENAMETOOLONGinstead of connecting to the truncated path;sendinterrupted by a signal (EINTR) is retried instead of closing the connection.host/posix/win32_posix.c, thread error paths: a failed allocation of a thread record now returnsNULLwithERROR_NOT_ENOUGH_MEMORYfromCreateThread(before: a NULL dereference), andGetCurrentThreadIdreturns 0 in thatcase; a failed
pthread_createdestroys the record's mutex and condition variable before freeing it;GetThreadIdof a bad handle setsERROR_INVALID_HANDLE. The thread record'stypedefgains a tag (struct HThread),and
FreeLibrary/GetProcAddressare reformatted; neither changes behaviour.scripts/prof.py, every platform: a frame from any Longshot module (now alsolongshot_vk.so:andlongshot_vk.dylib:) decides whether a sample outside Longshot is a wait, and a wait frame matches with its argumentlist too (
recording_wait(int), as demangled names print). An empty.mapfile (not MSVC's) no longer registers aWindows module that hides the native executable in
hotlist.py.tests/test_prof_symbols.py's Linux ELF fixturenow also separates a wait-named frame from libc work under a non-wait frame.
host/hos_thread.cincludes<emmintrin.h>on x86-64 only and readsSetThreadAffinityMask's result into an unused variable;host/posix/windows.hdefinesERROR_INSUFFICIENT_BUFFER(122, Windows' value); comments.
How this was checked: every changed C file, preprocessed with
unifdef -U__APPLE__ -D__linux__ -U_WIN32, comparedwith
main's.host/hos_main.c,host/hos_relaunch.c,host/posix/psapi.handgpu/vk/vk_menu.cppare identical;the differences in the others are exactly the ones listed. That is text, not a Linux build.
Rebase onto
mainhos_game_layout()inmain): kept first, then the startup snapshot. The snapshot readsargv,environand the executable path and writes nothing, and the layout changes neither, so the order keeps "before anything
writes" for the layout and "before anything changes the environment" for the snapshot.
load_error/hos_load_error):load_erroris declared for every POSIX host, and the Darwinload_darwin_library, which returns beforemain'sdlopen, writesdlerror()'s text into it too. A new check intests/darwin_host_test.c(commitb22debb8) covers it.test_shader_module_guard.pymacOS skip is kept intests/test_run_all_declarations.py, beside thisPR's removal of
test_prof_symbols.pyfrom the macOS exclusions.Tests
tests/test_darwin_host.py(new) withdarwin_host_test.c,darwin_fiber_registers.S,darwin_process_test.c,darwin_ipc_test.c,darwin_share_test.cppanddarwin_memory_notice_test.c: compiles the real host objects(
win32_posix.c,darwin_fiber.c,darwin_process.c,darwin_prof.c,hos_thread.c,hos_main.c,hos_relaunch.c, the IPC client) for each architecture and runs threads, fibers, modules, memory, sampling, restart,child processes, the menu's share functions, the IPC client and the memory notice. Child processes, the share targets
and the Discord listener are local stand-ins: no clipboard, browser, Finder or Discord action happens.
tests/test_prof_symbols.py: builds Mach-O executables and dylibs for arm64 and x86_64 and checks the symboloffsets against
otool/nm, then runsprof.py,crit.py,hotlist.pyandchainon authored traces. It nowruns on macOS instead of being skipped.
tests/test_low_memory_notice.py: a macOS branch with asysctlbynamestand-in that answers onlyhw.memsizeand
vm.swapusage; the same nine RAM/swap cases under macOS's rule and wording, both query failures, and--arch.tests/test_darwin_waits.py(the thread record is now a taggedstruct HThread),tests/display_startup_failure_test.cpp(an Apple stand-in for the startup snapshot that checks itran once with the real
argv/environ),tests/test_arm64_host.py(linksdarwin_fiber.cwhen present),tests/test_run_all_declarations.py. Their Windows and Linux text is unchanged.Flags and settings
None new. On macOS the existing
HOS_PROFILE,HOS_THREAD_TIMES,HOS_SAMPLE,HOS_SAMPLE_ALL,HOS_SAMPLE_FILE,HOS_CHAINandRX_MAPSnow work;HOS_WATCH,HOS_PIN_BY_CORE,HOS_PINandHOS_RESERVE_CORElog that they are unavailable there.docs/development.mddescribesRX_MAPSon macOS.Measurements
main:
5e072fc8c2adadba5ea610148be1c47800ba0cca(2026-11-14)this PR:
mac/darwin-host@b22debb8090c4fad3c9ad8f8708bb24afce64b3bdeps:
9439c31a,mainplus the six pull requests this one carries (the branch just before its own commits)Hardware: Apple M4 Max, 16 cores, 64 GiB, macOS 26.7.1; Apple clang 21.0.0; Python 3.14.10. No GPU, no game.
Scene: none. Game-free.
Method: each test from this PR, on this PR's source and on
main's (a clean checkout of5e072fc8, through thetests'
--source), interleaved: PR, main, PR, main.tests/test_darwin_host.pyalso on the deps tree, interleavedwith two more PR runs.
tests/test_prof_symbols.pyhas no--source: itsmainside is a copy ofmain's tree withthis PR's test file. Every run through
tools/slot.py, x86_64 executables under Rosetta.How to reproduce:
python -I -B tests/test_darwin_host.py --arch arm64 --arch x86_64 [--source <tree>],python -I -B tests/test_low_memory_notice.py --cc clang --arch arm64 [--source <tree>/host/hos_main.c],python -I -B tests/test_prof_symbols.py --cc clang --arch arm64 --arch x86_64.5e072fc89439c31ab22debb8tests/test_darwin_host.py, arm64 + x86_64, 2 runs per side (4 on the PR)hos_setjmp.c:108, x86 assembly)win32_posix.cdoes not compile;darwin_process.c,darwin_prof.cabsent; IPC link; memory notice)darwin_host_checks architectures=2 failed=0tests/test_low_memory_notice.py, arm64 and x86_64, 2 runs eachFAIL: host/hos_main.c reads macOS's memory without <sys/sysctl.h>tests/test_prof_symbols.py, arm64 + x86_64, 2 runsFAIL: shared running/waiting classification: longshot_vk.dylibThe sampling and classification figures are fixture checks, not performance measurements.
Testing
Tested:
The three tests, as above.
The fixture fixes, before and after, on this PR's source: the waits PR's
test_darwin_waits.pystops atunknown type name 'HThread', this PR's passes (--shared-only, arm64 and x86_64);main'sdisplay_startup_failure_test.cppstops atenvironandhos_relaunch_keep_startupundeclared, this PR's passes(11 cases, 0 failed).
The
hos_load_errorresolution: withload_darwin_library'sdlerror()copy removed (a throwaway copy of thisPR),
tests/test_darwin_host.pyfails at the new check (FAIL line=444: hos_load_error()[0] && strstr(...)).The Linux text, as described under "Changes on Linux" (
unifdef).The whole suite,
tests/run_all.py --jobs 6, on the same Mac:tests/test_darwin_host.py(new) passes, andtest_low_memory_noticeandtest_prof_symbolsgo from FAIL to pass.test_arm64_host,test_darwin_fault_context,test_darwin_guest_memory,test_darwin_waits,test_vk_darwin_admissionandtest_vk_darwin_sdl(new) pass;test_collect_wideandtest_mirror_trimFAIL to pass,test_dav1d_av1skip to pass,test_appimage_package_retryFAIL to skip (#454).maintoo:test_control,test_framegen_resources,test_game_layout,test_mem_gate,test_mem_gate_children,test_module_ids,test_oot_state_tap,test_pad_layout,test_pcache_header_read,test_pipeline_cache_file,test_pipeline_cache_save,test_pipeline_play_cache,test_program_cache_cleanup,test_program_cache_one_variant.History, not rerun on this branch: these commits were first made on the macOS integration branch (
23095796tof465faef), with the same checks on both routes; integration builds that carry them have since built and started thegame on macOS. That speaks for those builds, not for this branch alone.
Not tested:
error paths (they need an allocation or
pthread_createfailure), andscripts/prof.pyon a real Linux capture.win32_posix.canddiscord_rpc.c's POSIX half are not compiled there.real Discord client on macOS.
HOS_WATCH) and CPU pinning are unavailable on macOS by design; callers without frame recordsare not recovered by the sampler.
Needs other testers:
python -I scripts/prof.pyon aHOS_SAMPLE_FILEcapture: the same functions as before at the top (request to open).open).
Screenshots
Nothing on screen.
Risk and rollback
address length (what
connectexpects), a long-path guard and anEINTRretry; the thread changes only touchfailure paths.
scripts/prof.pymay classify a few more samples as waits on Linux, where a wait frame carries itsargument list or a Longshot frame is named by its
.so.a game run on this branch.
5d3abc9a..b22debb8); the six pull requests it carries stand alone.Reviewer notes
9439c31a..b22debb8(nine commits); the merges before it are the six pull requests this one depends on.5d3abc9ais the host itself (conflicts with #321 and #34 resolved as above);b7c9aebdthe profiler consumers andthe joined-main exit;
1bb0a62ddocs;8aa8c182,7e0f9407,84959356and08f6dba1fixtures that cut code thisPR changes;
a19cd2ffkeeps the low-RAM notice when the swap query fails;b22debb8thehos_load_errorcheck.darwin_fiber.c, then the thread and module parts ofwin32_posix.c, thendarwin_prof.c.rx_backend.c's "did not load" line (#34) still turns.dllinto.soin the file name; thedlerror()text after it names the real
.dylib. Not changed here (the macOS build PR names the renderer).tests/test_game_layout.py(#321) still fails on macOS, now later:win32_posix.ccompiles, and the link lacks theDarwin helpers (the macOS build PR lists them) and
hos_sdl_audio_open(the renderer-correctness PR resolves it onmacOS). It fails on
maintoo.tests/test_oot_state_tap.pyneeds the control-sockets PR (#456), which this one does not carry; it fails onmaintoo.
posix_spawn,libproc,mach/thread_act.h,mach-o/loader.h,sysctl). Nothing was copied from another project.mainatdc390e59(7 commits after this branch's base),git merge-treefinds conflicts ingpu/vk/vk_api.handhost/rx_backend.c(from #438, which this branch carries; the branch before this PR's commitsmeets the same two) and in
host/posix/win32_posix.c(this PR's own). A rebase before ready resolves them. #438'svk_api.hedit rebuilds the shader caches on5e072fc8but not after #370; this PR's own files are not stamped.Issues
See #420.
Checklist
For everyone
type(scope): description).main. Based on5e072fc8, with the six pull requests it depends on merged in;mainhassince moved to
dc390e59(conflicts named under Reviewer notes). Rebased before ready.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above). On macOS:537 passed, 17 skipped, and 14 failures that also fail on
main, named under Testing.requests are still to open.
the median reported. For a
featwithout a scene: the new tests fail onmainand pass here, in two interleaved runseach.
third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.RX_MAPSonmacOS is in
docs/development.md.mainand passing here.tests/that reads changed source updated in this PR, not weakened.DarwinHost tags the thread record (typedef struct HThread) for its Darwin thread registry. tests/test_darwin_waits.py still searched for an untagged "typedef struct {" after the threads marker, found a later struct and cut an empty HThread definition, so the fixture could not compile. Found when this branch was merged into macos/integrated (receipt i2-check-02-darwin-waits-shared; the same fix is 1cb8752f there). The fixture now cuts the first typedef of the threads section, tagged or not, and stops with a FAIL when that cut does not hold the HThread definition. No production source or wait behavior changes. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.