WIP: feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation #848
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/tooling
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!848
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/topic-shader-backend"
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: feat · 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 the game's shaders go from LS's SPIR-V back end through MoltenVK (SPIRV-Cross) to Metal, and many of them came out
wrong or not at all: texel formats MoltenVK cannot view read zeros; an empty descriptor slot is not empty (MoltenVK has no
nullDescriptor); a storage image read could return the value of a later write; Metal refused programs nesting more than 256brackets or braces; vertex stages could not use the guest's warp operations; and a few giant programs took Metal minutes to
compile. Under them, one MoltenVK device allocation holds at most about 41.7 GB while the game's memory window is larger, so
a guest span past one allocation could not be bound at all; and under Rosetta host imports failed on split pages and were
asked again every frame, leaving about 1,640 draws a frame out. This PR is the shader and binding layer of the macOS port that
fixes those. See #420.
What changed
The back end can bind a descriptor as an array of pieces and read texel formats through integer carrier codecs; the renderer
admits guest buffers per operation, imports a span past one allocation as segments and binds what straddles a cut as
pieces; on MoltenVK the back end emulates null descriptors, keeps image reads in program order, bounds SPIRV-Cross's
expression and control-flow nesting, runs vertex stages' warp operations as one-lane warps, and searches storage buffer
tables and zeroes local memory in loops. Each is chosen by a
Profileflag thatmake_profilesets on Apple (MoltenVK, somealso KosmicKrisp) or by
alias_isolation(macOS); with them off every module, layout, push range and command is what it was.The PR sits on topic 2 (
vulkan-on-metal) and topic 3 (mvk-descriptor-limits), and merges #431 (fix(vk): scan the import table as far back as its largest live import, an every-platform fix with its own pull request, which #658 builds on: review itthere). The eighteen original pull requests, in commit order, one port and one merged-together fix:
a0afe121; Was: #647):gpu/vk/span_segments.h(new, header only, no Vulkan): where to cut a guest span into segments. Test:
tests/test_span_segments.py.ad8133a3; Was: #648):RuntimeInfo::segmented; a listed descriptor becomes an array of pieces, each access through a helper walking the pieces bytheir lengths with constant indices (
spirv_emit_context.cpp,emit_spirv_*.cpp,vk_shader.cpp). Test:tests/test_seg_shader.py.ff007b3d; Was: #649): a texel buffer'scodec is part of the variant; the back end decodes and encodes ten formats MoltenVK cannot view through R8/R16/R32 UINT views
with integer operations only (
texel_codec.h, new, both levels);RX_VK_TEXEL_CODECS;kProgramCacheFormat1 to 2;nx2/bake_ds/bake_ds.cppstamps the switch.cfb67020; Was: #658):on macOS (
alias_isolation) each operation's guest buffers are admitted before it binds them, so two imports of the samebytes are never live in one submission, and an over-cap span is imported as segments (
vk_memory.cpp,vk_draw.cpp,vk_replay.cpp,vk_xfer.cpp,vk_temporal.cpp,vk_texture.cpp). Tests:test_alias_admission.py,test_admission_handoff.py,test_segmented_domains.pyand others (new).39a916d6; Was: #662):inert interfaces with their contracts (presence words, piece plans), and two fixes to
gpu_span_bind's segmented path.7eb53a2d; Was: #663): storage and texelbuffers, vertices, indices, indirect records, uniform buffers, copies, fills and inline writes that straddle a cut are bound
as piece arrays or split at it (
span_xfer.h, new); every empty slot gets a stand-in on Apple withoutnullDescriptor(
null_dummies.h, new;null_dummies_make).3ccdc8af; Was: #674): withProfile::emulate_null_descriptorsa stand-in slot reads zero, drops writes and atomics and reports size 0, by a pushedpresence word;
kProgramCacheFormat2 to 3 (the record head gainspresence_words).2843f77e; Was: #675):Profile::materialize_image_readsstores each storage image read at the read and puts an image barrier before each write andatomic (MoltenVK).
85b977e3; Was: #676):Profile::bound_forwarded_depth, a pass that keeps SPIRV-Cross from nesting an expression past 32 instructions(
msl_depth.cpp, new).48af6a54; Was: #686): frees and importsat whole 16 KiB hardware pages under Rosetta, and a refused import asked again only when its backing changes
(
darwin_mem.c,import_hold.h,darwin_import.h); Apple host ABI 26 (size 408).6ada3608; Was: #692): a value is atemporary only where SPIRV-Cross certainly reads it twice.
4c8dcc77; Was: #693):Profile::bound_scope_nestingmoves the outermost deep construct into its own function until a function nests at most 128levels (
msl_scope.cpp, new).1e976d5f; Was: #694): onMoltenVK the vertex-side stages the device does not list in
subgroupSupportedStagesrun the guest's warp operations aslane 0 of a 32-lane warp.
RX_VK_SPV_PROFILE(d87bcd25; Was: #695): a log line per bigmodule saying what its words are made of (
spirv_profile.h, new); off by default.d58c00e2; Was: #696):Profile::search_tables_in_loopson MoltenVK and KosmicKrisp in every global memory fallback (RX_VK_GM_LOOP).51da27ed; Was: #697):Profile::zero_local_memory_by_loopon MoltenVK (RX_VK_LOCAL_ZERO_LOOP=0turns it off).234bb376; Was: #698):materialize_image_readsfor KosmicKrisp's driver id too.1bf322f4; Was: #787): atest-only generator of programs in that shape through the real back end (
tests/giant_shape_ir.cpp,tests/test_giant_shape.py).55cfa887): the Mac-only part offix/async-safe-frees-i40(c151fcea, MvkAsyncFix's F5c,reports/2026-11-15-mvk-async-fixes.md), which the every-platform #788 does not carry becausenull_dummies_makeexists onlyon this stack: a stand-in clear not done in 5 s is waited for without a limit before its fence and command pool are destroyed
(
vk_texture.cpp);tests/test_null_dummies.py'smake-slow-clearcase.70cc5ba1, tests only): where #698's Apple-drivers fixture intests/test_vendor_fixes.pymeets #694'sdev.subgroup_stagesandShader::Stageand #696's and #697's switches, andtests/test_subgroup_vertex.py's stub meets KosmicKrisp's driver id inmake_profile, the fixtures did not build at themembers' merge; each passes at its own pull request's head. The fixtures get
stage.h,subgroup_stages, thetable_loops_onandlocal_zero_loop_onstubs and the KosmicKrisp constant (28), as the integration build I41 has them(
02b10861and one line of56092389).Measurements
Verdict: works game-free on both Mac routes and, where the members probed the GPU (history), on MoltenVK: with its flag
off every member's modules, layouts and commands are its base's byte for byte; with it on, each member's fixture passes and
fails on its base. On the GPU the probes' failing shapes were refused or wrong before and made and exact after, and two members
cut Metal's pipeline creation (15.36 s to 1.45 s; 7,518 ms to 15-16 ms). The full suite at this head: 603 passed, 15 failed,
17 skipped against main's 543/33/17 on this Mac (topic 2's 576/15/17), with no check failing that passes on main.
Baseline: each member compared with its own base in the stack (as its description says), because each extends the back end and
the renderer of the members before it, which main has none of; the GPU rows are those members' probes (history), on modules
this stack's tests reproduce byte for byte; the port's row compares the port
55cfa887with its parent1bf322f4.Hardware: macOS 26.7.1, Apple M4 Max (40-core GPU), MoltenVK 1.4.2, KosmicKrisp (SDK 1.4.363.1) for #698, 16-core CPU, 64 GB
Scene: none for the tests (game-free fixtures); the GPU probes are standalone programs, no game scene
Method: each member's fixture before and after (two interleaved runs in the members' own runs); at this head each once per
architecture; the probes once per CPU route; the suite once through
tools/ls-suite.pyHow to reproduce: the commands under Testing (
tests/test_seg_shader.py,test_null_descriptors.py,test_msl_depth.py,test_msl_scope.py,test_subgroup_vertex.py,test_gm_loop.py,test_local_zero.py,test_null_dummies.py...)aa95d35f70cc5ba1test_null_dummies.pymake-slow-clear1bf322f4: one wait, none unboundedFlags and settings
Three new switches (one changes a default on Apple), and one existing switch's default changes on Apple; none changes
anything on Windows or Linux by default.
RX_VK_TEXEL_CODECS0or1: texel buffers of a format the GPU cannot view read and written through an R8/R16/R32 UINT view with the codec in the shader=1on Windows or LinuxRX_VK_SPV_PROFILE=1: onevk: spv profileline per big module translated (at most 200 a run)RX_VK_SPV_PROFILE_MIN=<words>RX_VK_GM_LOOP(existing)0or1: storage buffer tables searched in a loop or written outRX_VK_LOCAL_ZERO_LOOP=0: local memory at computed offsets zeroed by its null initializer againExisting switches #658 reads at one more place, unchanged:
RX_VK_DLSS_ACTORS,RX_VK_GPU_DUMP,RX_VK_MESH_IND_COUNT_WORD,RX_VK_NO_UPSCALE_STUB.Testing
Tested: on macOS 26.7.1, Apple M4 Max, at this head
70cc5ba1(a clean worktree), or at55cfa887for the runs before thefix commit, which changes only
tests/test_vendor_fixes.pyandtests/test_subgroup_vertex.py:--arch x86_64or its fixture built with
-arch x86_64):test_seg_shaderagainst each member's own base tree (git archives of the 11bases the members used: 16 of 16, 32 of 32, 57 of 57, 125 of 139 (#675's 14 new modules) and 139 of 139 modules
byte-identical, as each member had them),
test_span_segments,test_null_descriptors,test_null_layout_push,test_native_float_atomic_add,test_program_record_head,test_amd_subgroup_profiles,test_msl_depth,test_msl_scope,test_subgroup_vertex(three bases),test_spv_profile,test_gm_loop,test_local_zero,test_vendor_fixes,test_split_ub_flush,test_import_buffer_binding,test_rosetta_import_geometry,test_import_hold,nx2/test_import_windows,test_vk_darwin_admission,test_darwin_renderer,test_darwin_guest_memory,test_macos_build_flags,test_giant_shape, the pure-Pythontest_bake_ds,test_program_cache_formatandtest_program_stamp_knobs, #658's 14 renderer checks (all exit 0,test_mirror_trimamong them now) and its 20 fixturecases, two of them controls, on both arches (40 runs, all exit 0), and #663's 60 consumer checks (positive and negative,
both arches): 102 logged jobs (the 14 checks, the 40 cases and the 60 consumer checks one job each). Every one passes
but these, each explained:
test_vendor_fixesandtest_subgroup_vertexdid not build their fixtures at55cfa887(themembers' meeting the fix commit repairs) and pass at
70cc5ba1on both arches (8 runs);test_seg_shaderagainst #662'sbase
5d169c3bdoes not build its fixture at this head, as at #674's head (#674's test reads a base with codecs as #663'sgeneration), and passes at #662's head; the three Rosetta consumer positives the system killed (exit -9, no crash report)
passed on their one rerun, as in #663's own runs;
seg-bind's two positives differ from #663's base918e6214only by#646's update-after-bind layout flag (the fixture's device is MoltenVK) and pass against that base with #646 merged in;
nx2/bake_ds/bake_ds.cppdoes not compile on macOS (environundeclared: the Linux offline tool, as at #648's and #649'sheads) and compiles on both arches with a one-line
extern char **environ;prelude.tests/test_null_dummies.py --cxx clang++ --headers <Vulkan-Headers>at55cfa887: pass on arm64 and with--arch x86_64under Rosetta (make-slow-clear: waited for without a limit after its 5 s wait); at its parent1bf322f4with the port's two test files (a scratch worktree): FAIL at
make-slow-clearon both (one wait, none unbounded).gpu/vk/vk_texture.cppcompiled whole at55cfa887with the tree's ownnx2/build.pymacOS commands, arm64 and x86_64:exit 0.
python -I -B tests/run_all.pythroughtools/ls-suite.pyat this head70cc5ba1: 603 passed, 15 failed, 17 skipped,against main
aa95d35f's 543/33/17 on this Mac, compared by test name: no check that passes on main fails here. Againsttopic 2's head (576/15/17) the only differences are 27 new tests, all passing: this PR's 24, topic 3's
test_set_for_tablesand
test_set_for_lengths, and #431'stest_import_reach; against main also topic 1's differences as its description liststhem. The 15 failures fail on main too, on this Mac:
test_readback_late,test_appimage_player_build,test_options_budget_once,test_vk_pgo,test_crash_dialog,test_walk_key,test_lift_cleanup_error,test_game_layout,test_options_path,test_make_game_cache,test_cpm_discovery,test_pipeline_tour,test_linux_preflight,test_env_on,test_split_giants.55cfa887(the fix commit changes tests only; everymkwvk::name one file declares andanother defines; each defining
.cppcompiled with the tree's own
renderer_flags(), arm64 and x86_64): 0 of 629 declared names lack a definition, 66 of 66objects compiled, 573 cross-file symbols per architecture all defined.
unifdef -U__APPLE__of every changed non-test C/C++ file against this PR's basec1229ac2, compared as tokens):7 files are the base's text; 30 differ and 10 new files have code off Apple. These are by design: the back end's new paths
are chosen at run time by
Profileflags and the renderer's byalias_isolation,gpu_span_any()andnull_dummies_on(),all off on Windows and Linux, so the code is compiled there and not reached:
emit_spirv*.cpp,spirv_emit_context.*,profile.h,runtime_info.h,bindings.h,emit_spirv.h,emit_spirv_instructions.h(#648, #649, #662, #674-#676,#693, #694, #696, #697), the new
msl_depth.*,msl_scope.*,texel_codec.h(both),span_segments.h,span_xfer.h,null_dummies.h,spirv_profile.h;vk_shader.cpp/.handvk_progcache.cpp(the variant keys, stamp words andkProgramCacheFormat3, every platform);vk_draw.cpp,vk_memory.cpp,vk_replay.cpp,vk_xfer.cpp,vk_temporal.cpp,vk_texture.cpp/.h,vk_internal.h(#658, #662, #663: each path starts with a test that is false off macOS; the port'stwo lines are in
null_dummies_make, which returns at once off Apple);vk_main.cpp(#694 readssubgroupSupportedStages);#686's
darwin_import.h,import_hold.h,host/posix/darwin_mem.hand itsvk_memory.cpp/vk_replay.cpplines (comments,an unused type, an empty report function);
nx2/bake_ds/bake_ds.cpp(#649: one more switch name in its stamp's list).E9a on I41
78ec1c47: a 1,197 s hold in play, 12 of 12 must-shows, 0 skipped draws, a clean end(
reports/2026-11-15-mac-perf-E9.md); x86_64 under Rosetta: run #34 on I383f872b0c: 16 m 52 s of play, every drawdrawn, 19 of 19 checks, a clean exit (
reports/2026-11-14-mac-startup-34-x86_64.md).Not tested: Windows and Linux: nothing was built or run there (the test request below). No GPU run at this head (the probes
are history, on modules the tests reproduce byte for byte); no game run has reached a segmented domain yet; the cost of the
null-descriptor guards, the stored image reads and the one-lane warps in a real frame;
RX_VK_TEXEL_CODECS=1off macOS;KosmicKrisp in a game.
Must be tested:
aa95d35fthe same way, runpython -I -B tests/run_all.pyon each andstart each to the title, expecting the new tests to pass or skip with their macOS reason, no other check to change status,
and the
vk:lines to match main's (the shaders and the program cache are prepared again once) (Test Farm: @Beanstalkwindows linux, build, run_all and start to the title)
Risks
What could break: every platform: the back end's emit paths and the renderer's binding paths changed under flags that are
off there; a mistake in a flag's gate would change modules or commands off macOS (the fixtures check them byte for byte
against the bases). On macOS: the null-descriptor guards, stored image reads and barriers, one-lane warps and nesting bounds
cost instructions (not measured in a frame); segmented binding has not run in a game.
Turning it off:
RX_VK_TEXEL_CODECS=0,RX_VK_GM_LOOP=0,RX_VK_LOCAL_ZERO_LOOP=0; the other fixes follow the driverwith no switch: revert the PR.
Compatibility: every platform prepares its shaders again once (the translator's stamp goes from
6241b7736c8060e3toba2e3585119b57a6) and rebuilds its program cache once (kProgramCacheFormat1 to 3). The vk ABI:MKWVK_API_VERSIONis 26on macOS only (size 408; the macOS executable and renderer are built together); Windows and Linux keep main's 23. Saves and
longshot_options.iniare unchanged.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 topics 2 and 3 and #431).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (macOS: 603/15/17, the 15 failures also fail on main, named under Testing; Windows and Linux: the test request.)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).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.