WIP: fix(macos): MoltenVK's precompile and pipelines on Metal #849
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
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/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!849
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/topic-precompile-metal"
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?
Type: fix · Risk: medium · Tested on: macOS 26.7.1, Apple M4 Max (arm64; x86_64 under Rosetta) · Needs: Windows and Linux, any GPU
Why
On a Mac, Metal compiles every pipeline in its compiler service, a few processes the whole game shares, and the game asks it for
about 42,000 pipelines and compute programs, a handful of them giants that take minutes and gigabytes each. Started as on any
other GPU, the game crawled or never reached the title: the shader preparation compiled one pipeline at a time behind MoltenVK's
one pipeline-cache lock (39 programs in its first 180 s); a renderer thread's 512 KiB stack overflowed in SPIRV-Cross and the
game crashed; the precompile never let the title come while giants held every worker (run #12); the game's first frame waited
minutes behind 16 giants on the compiler's connections; giants compiled together ran the service out of memory and died, so
three of the four giant mesh programs were never made (run #31); a compile that failed beside a giant was lost for the session;
and every start compiled again what Metal's own shader cache did not keep. This PR folds the macOS port's twelve precompile PRs
into one: the preparation and precompile shaped for Metal's compiler service, on MoltenVK and KosmicKrisp only. It is part of
the macOS port. See #420.
How to reproduce
Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2
Settings and switches: defaults (the macOS port's stack without this PR: main does not start on macOS)
Steps: start the game on topic 4 (
shader-backend, this PR's base) from an empty program cache, then again from the one it left; watchlongshot_log.txt'svk: preparation:andvk: precompilelines and the first presentExpected: the preparation and precompile finish within minutes, the title comes, the first frame follows within seconds, every program is made
Actual: 39 of 38,809 programs prepared in 180 s; a SIGILL at about 93 s in SPIRV-Cross (start #6); the precompile stalls with no title (run #12: 4,600 items for 160 s, 7,698 for 80 s); the first frame about 3 minutes after the hand-off (start 17); service processes ended under the giants and three giant mesh programs not made (run #31)
What changed
Under the vendor fix
compiler_service(on by default on MoltenVK and KosmicKrisp, off on every other driver), the preparationtranslates and keeps every program but compiles no compute pipeline, each worker with a pipeline cache of its own on every CPU;
the precompile orders the work for Metal's compiler service, lets the title come while the last giants build behind the game,
keeps at most four giants and one heavy compile in flight, holds giants until the first frame and while memory is short, tries a
failed compile again alone, and puts a dispatch's compile ahead of them. An opt-in switch keeps MoltenVK's pipelines in Metal
binary archives, and two log-only switches name each module's private memory and each slow compile. Every other driver runs the
code it ran before (the off-Apple text changes are listed under Testing). One commit per original PR, in order, then the ports
and the merge fixes:
perf(vk): a pipeline cache per preparation worker, every CPU, and no saved pipeline cache on MoltenVK(
gpu/vk/worker_caches.h,gpu/vk/prepare_workers.h,vk_vendor.hworker_caches/pcache_no_load;RX_VK_WORKER_CACHES,RX_VK_PCACHE_LOAD's MoltenVK default). Was: #650fix(macos): renderer threads start with a 16 MiB stack on macOS (gpu/vk/thread_stack.h, every thread the renderer starts;std::threaditself elsewhere). Was: #651fix(macos): the precompile goes on behind the title on MoltenVK, slow compiles named, a compile the service lost tried oncemore (
gpu/vk/precompile_handoff.h, the precompile invk_draw.cpp,vk_vendor.hcompiler_service;RX_VK_COMPILER_SERVICE,RX_VK_SLOW_COMPILE_S,RX_VK_PRECOMPILE_STALL_S). Was:#652
fix(macos): the preparation leaves MoltenVK's compute pipelines to the precompile, which keeps at most 4 giants in flight(
seed_computeinvk_shader.cpp, the giant bound inprecompile_handoff.h;RX_VK_GIANT_COMPILES,RX_VK_GIANT_WORDS).Was: #664
fix(macos): giants held behind the game until its first frame, at QoS class utility; a failed giant retried alone(
RX_VK_GIANT_HOLD_PRESENTS,RX_VK_GIANT_HOLD_S,RX_VK_COMPILE_QOS). Was:#665
fix(macos): a dispatch's compute pipeline goes ahead of the giants and is retried alone, not failed for good (the compileboard in
precompile_handoff.h;RX_VK_ALONE_TRIES,RX_VK_SKIP_LOG,gpu/vk/skip_log.h). Was:#666
feat(macos):RX_VK_METAL_ARCHIVE_DIRkeeps MoltenVK's pipelines in Metal binary archives, macOS only, off by default(
gpu/vk/metal_archive.h, its hook invk_main.cpp, all inside#if defined(__APPLE__)). Was:#667
fix(macos): giants start behind the game only while memory allows; a lost device said as one, with the system's memory(
gpu/vk/system_memory.h,RX_VK_GIANT_ROOM_MB). Was: #669feat(macos):RX_VK_LMEM_LOGnames each module's private memory and each pipeline's first bind, log-only, macOS only(
gpu/vk/private_memory.h,vk_progcache.cpp;RX_VK_LMEM_LOG_MIN). Was:#681
fix(vk): heavy compiles run alone under Metal's compiler service; the first giant behind the game no longer waits for 16 GB(
behind_workerinvk_draw.cpp,heavy_compileinprecompile_handoff.h, the local memory weight invk_progcache.cpp;RX_VK_HEAVY_WEIGHT,RX_VK_HEAVY_WORDS). Was: #784fix(macos): KosmicKrisp's compiles go through the compiler service's precompile, as MoltenVK's do (vk_vendor.h). Was:#785
fix(macos): Metal's shader cache folder (RX_VK_METAL_CACHE_DIR) asked at the host's start, before SDL's window(
host/posix/darwin_metal_cache.h,hos_main.c,rx_backend.c). Was:#786
fix(vk): a compile that never ends keeps the heavy compiles and the giants back forRX_VK_STALE_COMPILE_S(240 s) atmost (
BoardandGiantsinprecompile_handoff.h). Frommacos/giant-starve5292596d (reports/2026-11-15-giant-starve.md),placed in main's dispatch build (#252).
fix(vk): what an earlier run found slow for minutes compiles first, and is play's from the hand-off(
RX_VK_SLOW_FIRST_S,Giants::hand_over). Frommacos/slow-first40a86bae (reports/2026-11-15-slow-first.md).docs(vk):metal_archive.h's notes on Metal's cache and staging say only what runs and public notes showed. Frommacos/integrated63fc3d2c; no code change.test_giant_alone cuts main's dispatch build (the giant-starve port); test_warm_handoff finds
seed_compute's save with what#695's line adds (I41 1cbb594a); test_vendor_fixes runs its checks once, after every check is defined; and the merge of topic
4's fixture fix (test_vendor_fixes, test_subgroup_vertex).
Measurements
Verdict: fixes the Mac's start: with these changes the macOS stack reaches the title and plays, every program made. On I41
78ec1c47(every member here, before Sweep3's rebase, and the giant-starve port), E9a handed off 8 m 14 s after the start,made all four giant mesh programs and the four other heavy compiles, made 44511d69 (the tree leaves' program) 887 s into the
hold, and held 1,197 s with 12 of 12 must-shows, 0 skipped draws and 0 crash reports. Each member measured its own part on the
integration build of its day:
cache: 45.41 against 10.35 compute a second), a later start's pipelines in 1.41 s compiled anew against 6.65 s through the
saved cache, and the first start at 230 programs in 180 s against 39 (integration b7a7c95f run #2 against aefe6b0a run #4).
game-free tests show the rules, and #669's lost-device line named the memory at start 23's loss.
from archives measured).
RX_VK_METAL_CACHE_DIRfrom start to hand-off in 6 m 53 s (run #36) against 24 m 28 s (run #35).932-1,031 s; reports/2026-11-15-slow-first.md).
Hardware: macOS 26.7.1 (25G241), Apple M4 Max, 64 GB; MoltenVK 1.4.2, and MoltenVK main 5189f8de with the argbuf-slots patch from run #31 on
Scene: the title and the field-dawn save's first area, windowed; LS's default settings
Method: each run's
longshot_log.txt(thevk: preparation:,vk: precompile, hand-off, slow-compile and close lines, the FPS counter's Dev level in the hold); one start each, so the counts and times are what each start reached, not an interleaved medianHow to reproduce: the macOS runner (
tools/ls-run.py) on an integration build, a seeded or empty program cache as named, START and A at the titleBaseline: main does not start on a Mac, so the Before column is integration a7711f3c (run #12), the macOS stack with #650 and #651 and none of #652-#786; the After column is I41 78ec1c47 (E9a), which carries every member (as before Sweep3's rebase onto
aa95d35f) and the giant-starve port, not slow-first's port or the comment rewording. The two starts' program caches were not the same.Flags and settings
Eighteen new switches and two new defaults, every one read on MoltenVK or KosmicKrisp only (most through
compiler_service), ormacOS only; nothing changes on another driver.
HOS_HEADLESS,RX_VK_NO_WARM,RX_VK_PCACHE_FETCH_LOCKandRX_VK_SKIP_PROGappear in the diff as main's switches, read as before.
RX_VK_WORKER_CACHES=0one shared,=1on any driver with pipelineCreationCacheControl)RX_VK_PCACHE_LOAD=0neither,=1both)RX_VK_COMPILER_SERVICE=0as on other drivers,=1on any)=0only to compareRX_VK_SLOW_COMPILE_S0none)RX_VK_PRECOMPILE_STALL_SRX_VK_GIANT_COMPILES0no bound)RX_VK_GIANT_WORDSRX_VK_GIANT_HOLD_PRESENTS0no hold)RX_VK_GIANT_HOLD_SRX_VK_COMPILE_QOSutility,background,default)defaultin the gameRX_VK_ALONE_TRIES0none)RX_VK_SKIP_LOGRX_VK_METAL_ARCHIVE_DIRRX_VK_GIANT_ROOM_MB0no gate)RX_VK_LMEM_LOGRX_VK_LMEM_LOG_MINRX_VK_LMEM_LOGnames a moduleRX_VK_HEAVY_WEIGHT0none by weight)RX_VK_HEAVY_WORDS0none by size)RX_VK_STALE_COMPILE_S0never)RX_VK_SLOW_FIRST_S0none first)RX_VK_METAL_CACHE_DIRmain()before SDLTesting
Tested: on macOS 26.7.1, Apple M4 Max, at this PR's head
e3f55e1bon topic 4:member ran it (eight arm64-only): every one passes. Three failed at the pre-fix tip where two members met (test_vendor_fixes,
test_warm_handoff, test_giant_alone) and pass after the merge fixes above, both arches; test_metal_cache_dir's #667 form
(
--headers) is #786's form now, which passes both arches. The ports' own checks: giant-starve's ten at310fbb8eandslow-first's test_slow_first at the head, both arches, pass (test_giant_alone after its fix). Logs:
reports/Consolidate-files/proof/c5-*,c5gs-*,c5sf-*,c5f-*,c5m-*.vk_draw.cppandvk_main.cppcompiled whole on arm64 and x86_64 with nx2/build.py's own macOScommands at the pre-port tip, at
310fbb8eand at7e1d702f: 0 failures; no undefined symbol added against the pre-port tip.python -I -B tests/run_all.pyat the head against main aa95d35f's 543/33/17 on this Mac: 615 passed, 15 failed, 17skipped; by name nothing fails here that passes on main; 17 of main's failures pass (the macOS host's and caches' fixes from
topics 1-4), 54 new checks pass,
test_appimage_package_retryis skipped on macOS (#454) andtest_dav1d_av1runs (#454);the 15 failures fail on main too: test_appimage_player_build, test_cpm_discovery, test_crash_dialog, test_env_on,
test_game_layout, test_lift_cleanup_error, test_linux_preflight, test_make_game_cache, test_options_budget_once,
test_options_path, test_pipeline_tour, test_readback_late, test_split_giants, test_vk_pgo, test_walk_key
(
reports/Consolidate-files/checks/compare-precompile-metal.txt).mkwvk::name declared in one gpu/vk file and defined in another,each defining file compiled on both arches): 0 not exported, 0 undefined across the 33 objects of each arch.
unifdef -U__APPLE__of every changed non-test C/C++ file, against topic 4): 5 files the same, 18 differ, 7 new,1 gone, all of them the members' runtime-gated text, none a behaviour change off the Mac: #651's
thread_stack.hinclude inthe files that start threads (
thread_stack::threadisstd::threadoff macOS); the compiler-service, worker-cache and giantcode in
vk_draw.cpp,vk_vendor.h,vk_shader.h,prepare_workers.h(#650: half the CPUs off macOS, as before) and the newprecompile_handoff.h,worker_caches.handskip_log.h, behind vendor fixes that are false on every driver but MoltenVKand KosmicKrisp (
RX_VK_SKIP_LOGoff by default everywhere);vk_progcache.cpp's private-memory and weight notes (#681,#784, read only on macOS); the new
metal_archive.handprivate_memory.h;host/posix/darwin_metal_cache.h, macOS's own,replacing
metal_cache.h(#786).78ec1c47(arm64, 1,197 s hold, 12/12 must-shows, clean end, 0 skipped draws), an integration buildthat carries every member here and the giant-starve port; run #34 on I38 (x86_64 under Rosetta, 16 m 52 s, every draw drawn,
19/19, clean exit), which carried #650-#681 as they were then (not #784-#786 or the ports).
Not tested: Windows and Linux (no such machine here): every change is behind
compiler_service(MoltenVK and KosmicKrisp)or
__APPLE__, butvk_draw.cpp,vk_progcache.cppand the headers listed above compile there with new text. Slow-first'sport in a game run. A warm game start from Metal binary archives (
RX_VK_METAL_ARCHIVE_DIRstays off). KosmicKrisp in a gamestart.
Must be tested:
e3f55e1b) and mainaa95d35fthe same way, runpython -I -B tests/run_all.pyon each and start each to the title, expecting the build to succeed, the new tests to pass with no other check changing status, thevk: vendor fixesline withoutcompiler_service, and no newvk: precompileorvk: private memoryline (Test Farm: @Beanstalk windows linux, build, run_all and start unchanged)Risks
What could break: on MoltenVK and KosmicKrisp, the start's order: which compiles run when, how many giants run at once and
when the title comes; a rule held wrong could delay a program behind the game (none is dropped: each is built later or by its
first dispatch). On Windows and Linux nothing should change; the code is compiled there, gated off at run time.
Turning it off:
RX_VK_COMPILER_SERVICE=0(the precompile and preparation as on every other driver),RX_VK_WORKER_CACHES=0,RX_VK_PCACHE_LOAD=1, the giant switches at0,RX_VK_SLOW_FIRST_S=0,RX_VK_STALE_COMPILE_S=0; or revert the commits.Compatibility: on MoltenVK the pipeline cache files are no longer read or written (
RX_VK_PCACHE_LOAD=1reads them); a newfile
longshot_pipelines.bin.slowbeside the program cache lists slow compiles. The vk ABI on macOS:MKWVK_API_VERSION26 →27 (#786's
metal_cache_checkhost slot), so the host and the renderer of one macOS build go together; Windows and Linux stay at23. No change to saves,
longshot_options.inior the program cache's format. Nothing changes for a Windows or Linux player.Issues
See #420. The macOS topic PRs, merged in this order; "topic N" in each description means these: 1 #845 (macos-host), 2 #846 (vulkan-on-metal, on 1), 3 #847 (mvk-descriptor-limits), 4 #848 (shader-backend, on 2, 3 and #431), 5 #849 (precompile-metal, on 4), 6 #850 (draw-path, on 4), 7 #851 (draw-path-quads-mesh, on 6, #705 and #707), 8 #852 (mac-perf, on 7).
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f, through topic 4 (shader-backend).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On this Mac at the head: 615/15/17 against main aa95d35f's 543/33/17; the 15 failures, named under Testing, fail on main too; no check fails here that passes on main.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.MKWVK_API_VERSIONbumped (gpu/vk/vk_api.h'sMkwVkHostorMkwVkRendererchanged). (Apple 26 → 27 for the new host slot; Windows and Linux unchanged at 23.)mainand passing here.tests/that reads changed source updated in this PR, not weakened.Every scan down vk_memory.cpp's import table by start (pool_lookup's covering import and merge plan, bg_drain, prefetch_chunks, import_in_background, memory_note_gpu_write and memory_guest_imported) stopped once an import ended more than 1 GiB below its address, on the belief that no import is larger than that. pool_lookup's own merge escape leaves an import above 1 GiB over the smaller ones inside it ("left overlapping"), and the device allows imports up to max_import_size: a scan stopped at a small import inside a large one, missed the large one, and the same bytes were imported again, two memories over the same bytes. import_reach.h counts the live imports by span class; its bound is above every live span, within twice the largest. imported_add and imported_take, now the table's only ways in and out, keep the count. A scan stops only where no import further down can reach its address, so it also stops sooner than before when the imports are small. The covering-import checks in pool_lookup and import_in_background look past the last import at or below the range for the same reason. Tests: test_import_reach.py runs the actual scans with a 4 GiB import over a 16 MiB chunk, left by the merge escape (imports are recorded ranges: no payload, no Vulkan, no game). Its lookup, query, note, drain, background and prefetch cases fail on main5e072fc8and pass here, and a source check keeps every change to the table in the two helpers. test_import_windows runs pool_lookup with the helpers; test_mirror_gpu_only names the new publication point. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>This conflicts with
mainand is built on top of the macOS series (it carries the commits of #845 and others below it).nx2/build.py,nx2/make_game.pyanddocs/development.mdconflict with #577 (the WebGPU renderer removed from the Windows build). Please rebase onto currentmainonce its base PR #845 lands or is rebased; I didn't touch it because a rebase here would have to be redone after that.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.