WIP: fix(runtime): add macOS waits and deadlines, shared by host and renderer #437
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!437
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/waits"
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 Win32 layer's waits (
host/posix/win32_posix.c) use Linuxfutexes, a condition-variable clock attribute and
clock_nanosleep, none of which macOS has. This PR gives that layermacOS 14.4 waits, deadlines and SRW locks, for both CPU routes. The executable and the renderer each link their own copy
of the layer, so the address waits also share one set of wait state through the executable: without that, a wake in
one image never reached a waiter in the other (20 of 20 such waits stuck, per architecture). The Windows and Linux code
is unchanged. On its own it does not make the macOS host build or run. What is measured: a new game-free test builds
the real wait functions (cut from
win32_posix.c) withdarwin_wait.cand runs them with real locks, waits, wakes andclocks. Its two-image mode builds two separately linked dylibs plus the executable and checks every waiting image against
every waking image. It fails on
main(no Darwin waits) and passes here, on arm64 and on x86_64 under Rosetta, twointerleaved runs each. The cross-image failure is shown on this PR's commits without the shared state. Also measured:
the full game-free suite on an Apple M4 Max (macOS 26.7.1), compared by test name with
main. Not measured: Windows,Linux, a macOS build or game run, macOS 14.4 itself, an Intel Mac, sleep and resume, and the Rosetta timing campaign
on a quiet machine (below).
Why
A Mac cannot build LS's POSIX host while its waits are futex calls. And a renderer and an executable that each keep
private wait state lose each other's wakes.
Problem
main,win32_posix.cincludes<linux/futex.h>(line 12), so the whole file stops there on macOS:host/posix/win32_posix.c:12:10: fatal error: 'linux/futex.h' file not found.futex()(:81) issyscall(SYS_futex, …), used byWakeConditionVariable,SleepConditionVariable*,WaitOnAddressand bothWakeByAddress*(:88-137);monitor_init(:64) sets the condition variable's clock withpthread_condattr_setclock, which Darwin lacks;:489) isclock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, …), which Darwin lacks;SRWLOCKis a barepthread_rwlock_twhose zeroed storage counts as initialized (windows.h:153-156). On Darwin azeroed rwlock is not a valid lock.
CLOCK_MONOTONICcounts system sleep, unlike Linux's. A deadline on it would expire while the Mac sleeps.win32_posix.c's first comment).A futex is keyed by the address, so on Linux the two copies wake each other. macOS's public address wait
(
os_sync_wait_on_address) also keys by address, but a Win32WaitOnAddresscompares 1 to 8 bytes, so the Darwin sidewaits on a sequence word that each wake bumps. With one sequence table per image, a wake through the executable's copy
never bumped the word that the renderer's copy was waiting on.
main, the renderer's recorder idle (RX_VK_RECORDER_IDLE=wait,gpu/vk/vk_replay.cpp:135-136) usesWaitOnAddressandWakeByAddressSinglefrom the renderer's copy.What changed
host/posix/darwin_wait.cand.h(new), all inside#if defined(__APPLE__):darwin_now_ns: awake uptime (mach_absolute_time), so sleep does not use up a deadline. FILETIME stays realtime.darwin_monitor_initanddarwin_cond_timedwait: relative pthread timed waits, recomputed from one fixed uptimedeadline after every return.
darwin_wait_until_ns:mach_wait_untilwith the timebase rounded up andKERN_ABORTEDretried, for the timer.darwin_wait_word/darwin_wake_word:os_sync_wait_on_addresswith a timeout, for condition variables.darwin_wait_address/darwin_wake_address: Win32 address waits over 1, 2, 4 or 8 bytes, upper-half andcross-word changes included. The waiter snapshots its sequence word before comparing; a wake bumps the word, then
wakes all. Both Win32 wake names broadcast, as the Linux layer does.
darwin_srw_init,darwin_srw_nativeanddarwin_srw_trylock: zeroedSRWLOCKstorage gets a realpthread_rwlock_initthe first time it is used. A try-lock never waits for another thread's initialization.host/posix/darwin_wait_state.c(new):hos_posix_address_wait_sequences()returns the process's one array of256 zero-initialized sequence words. It belongs in the executable only. Each image's
darwin_wait.cfinds it oncewith
dlsym(RTLD_MAIN_ONLY, …). If the executable lacks it, the first address wait or wake aborts withfatal: main executable has no Darwin address-wait state provider; there is no private fallback, and a provider inany other image is not used.
host/posix/win32_posix.c:#if defined(__APPLE__)branches at the futex include,now_ns,abs_after_ms,monitor_init, the monitor wait, the condition-variable wake and wait, SRW release and reacquisition,WaitOnAddress,WakeByAddress*,Sleepand the timer wait. Each#elseismain's text unchanged. Events, semaphores and themultiple-object polling are unchanged.
host/posix/windows.h: on Apple only,SRWLOCKisDarwinSRWLockwith wrappers that initialize it, andQueryPerformanceCounter/GetTickCount*read awake uptime. The pause instruction is untouched.tests/test_darwin_waits.py(new) cuts the synchronization functions out ofwin32_posix.cand links them withdarwin_wait.c,darwin_wait_state.cand the realwindows.h. No fake locks, waits, wakes or clocks.under spurious wakes, zero, finite and infinite waits, both SRW modes, events, semaphores, the absolute timer, thread
completion and the polling limits. Its timing campaign has fixed tolerances: a 10 ms wait may end 0.1 ms early or
50 ms late.
tests/darwin_wait_sharing_test.c(new), with--shared-only: the executable and two separately linked dylibs,each with its own copy of the wait functions, and the real provider in the executable. Every waiting image against
every waking image, 8 layouts from 1 to 8 bytes, both wake names, a waiter already queued and a waiter just entering:
288 cases of 4 rounds. Also four-waiter broadcasts across images, polls, and nine runs of an executable without the
provider (a fourth dylib exports one), which must abort.
--archselects both routes and--source-treeanother tree. It skips on Windows and prints
SKIPon Linux.tests/test_display_pointer.py: on macOS only, it compilesdarwin_wait.cand links it into its fixture, whoseSRW locks now need it. The Linux and Windows command and every assertion are unchanged.
docs/development.md: the macOS wait subset, its clock, the shared address-wait state, the test command, and thebuild's source lists.
nx2/build.py. The macOS build PR in this series (build(macos): classify Darwin helpers …) putsdarwin_wait.cinto both images anddarwin_wait_state.cinto the executable only. The executable has to exporthos_posix_address_wait_sequences; the macOS build flags PR (build(macos): discover dual-architecture toolchains …)exports
_hos_posix_*with-exported_symbol, as Linux already does with--export-dynamic-symbol=hos_posix_*.Flags and settings
No game, renderer or build switch. The new test's own options:
--arch(repeatable)--arch arm64 --arch x86_64, where Rosetta is installed--shared-only--source-treemain, for the failing side--outMeasurements
main:
5e072fc8c2adadba5ea610148be1c47800ba0cca(2026-11-14)this PR:
mac/waits@0d62666d45bba74fe344eac19e73b179481e85c1Hardware: Apple M4 Max, 16 cores, 64 GiB, macOS 26.7.1 (25G241); Apple clang 21.0.0; Python 3.14.10. No GPU used.
Scene: none. Game-free.
Method:
tests/test_darwin_waits.py --shared-only --arch arm64 --arch x86_64onmain's tree (--source-tree, a cleancheckout of
5e072fc8) and on this PR, interleaved: main, PR, main, PR. For the cross-image defect, the waits commitsof this PR without the shared state (
35ef0284) ran a separate two-dylib probe that links the same wait functionstwice. Then
tests/run_all.py --jobs 6on this PR, compared by test name with amainrun on the same Mac.How to reproduce:
python -I -B tests/test_darwin_waits.py --shared-only --arch arm64 --arch x86_64, then the same with--source-tree <a checkout of main>.5e072fc835ef0284, without the shared state0d62666dtests/test_darwin_waits.py --shared-only, both routes, run 1host/posix/darwin_wait.cThe stuck waiters at
35ef0284were each woken through their own image afterwards (same_image_rescued=10), whichshows they were waiting, not lost.
decoy_wake_increments=0: a dylib that also exports a provider is never used.Testing
Tested:
The two-image test, both ways, both routes, twice each, and the cross-image probe on
35ef0284, as above.The whole of
win32_posix.cnow gets past its includes on macOS (syntax only). It still fails, with 75 errors oneach route, in code that other macOS PRs port: the fiber
ucontext.hroutines, CPU affinity, thread names,MAP_STACK, another thread's CPU clock,dlinfoand the fault context.The whole suite,
tests/run_all.py, 6 at once, on the same Mac (the runner labels macOSlinux):tests/test_darwin_waits.py(pass, native arm64 with its timingcampaign, as
run_allruns it).tests/test_display_pointer.pypasses on both. No other check changed status.main, which other macOS pull requestsaddress:
test_appimage_package_retry,test_collect_wide,test_control,test_framegen_resources,test_game_layout,test_low_memory_notice,test_mem_gate,test_mem_gate_children,test_mirror_trim,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_prof_symbols,test_program_cache_cleanup,test_program_cache_one_variant.test_game_layoutgets further here:mainstops atlinux/futex.h, this PR at the fiberucontext.hinclude.Not tested:
win32_posix.candwindows.hismain's; that is not a build there.1da4f9b3, on a heavily loaded Mac,one 10 ms absolute-timer wait under Rosetta took 68.4 ms, over the 60 ms ceiling; native arm64 passed. The ceiling
stays; the quiet rerun has not been done. The suite runs the timing campaign natively only.
adjustment, and every possible thread interleaving: the wake checks are repeated observations.
Needs other testers:
to open).
test_display_pointer; expect no change, and the newtest printing
SKIP(request to open).python -I -B tests/test_darwin_waits.py --arch arm64 --arch x86_64on a quiet machine;expect
PASS, with no late sample (request to open).Screenshots
Nothing on screen.
Risk and rollback
#if defined(__APPLE__)or in a new file.darwin_wait_state.c, or without exporting its function, aborts at its firstaddress wait. That is on purpose: a private fallback would bring back the lost wakes.
recheck their condition after a wake, as the Linux layer already requires.
limit, periodic and APC timers; a polling wait-all that can take one object before all are ready.
Reviewer notes
5e072fc8..0d62666d: four commits and no dependency.2e939743is the waits;35ef0284linksdarwin_wait.cinto the pointer test's fixture;3861478fmoves the address-wait state into the executable;0d62666ddocuments that.darwin_wait.c's address waits andinit_address_sequences, then the Apple branches inwin32_posix.c's monitor and condition-variable waits.os/os_sync_wait_on_address.h,pthread/pthread.h,clock_gettime(3),mach/mach_time.h,dlsym(3)). Nothing was copied from another project.Issues
See #420.
Checklist
For everyone
type(scope): description).main. Based on5e072fc8.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above). On macOS:526 passed, 17 skipped, and the same 19 failures as
main, named under Testing.requests are still to open.
the median reported. For a
fix: the new test fails onmainand passes here, in two interleaved runs each.third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.docs/development.md.mainand passing here.tests/that reads changed source updated in this PR, not weakened.test_display_pointer.pylinksthe new helper on macOS; its assertions are unchanged.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.