fix(vk): use update-after-bind sets past Metal's per-stage limits on MoltenVK #646
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!646
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/mvk-pushlimits"
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: low · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head · Needs: Windows, Linux
Why
On a Mac, a shader function has fixed tables of resources: 31 buffers, 128 textures and 16 samplers. MoltenVK binds a pushed
descriptor set straight into those tables and keeps the top buffer slots for its own use, but it advertises a push limit
of 287 descriptors and checks nothing when the layout is made. A shader stage of the renderer's with more uniform and
storage buffers, textures or combined image samplers than fit then fails to compile ("'sampler' attribute parameter is
out of bounds"), and its draw or dispatch is left out. The translator can produce such stages: the game may bind up to 32
textures a stage, and indexed texture arrays are sized up to 1024. In an ordinary set (a Metal 3 argument buffer) the
resources take no function slots. This PR gives such stages an ordinary set on MoltenVK, under #643's switch, and makes
MoltenVK's ordinary sets update-after-bind layouts, so that a set with more than 16 samplers is also within Vulkan's
limits. Other drivers are unchanged. It is part of the macOS port and stacks on #645. See #420.
How to reproduce
Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2 (read from MoltenVK v1.4.2's and SPIRV-Cross's sources and
MoltenVK issues #2094 and #2426,
reports/2026-11-14-mvk-pushlimits.md§1; main does not build on macOS yet)Settings and switches: defaults
Steps: game-free: in #645's tree (this PR's base), copy in this PR's
tests/test_set_for_tables.pyandtests/test_vendor_fixes.py, and runpython -I -B tests/test_set_for_tables.py --cxx clang++ --headers <Vulkan-Headers/include>. In the game: a program with a stage of 17 or more combined image samplers, 129 or moretextures, or more buffers than the stage's budget below.
Expected: such a stage gets an ordinary set on MoltenVK, an update-after-bind layout from an update-after-bind pool,
and its pipeline is made.
Actual: it is pushed; Metal's compiler refuses its out-of-range
[[buffer]],[[texture]]or[[sampler]]index andMoltenVK fails the pipeline (
VK_ERROR_INVALID_SHADER_NV/VK_ERROR_INITIALIZATION_FAILED), and LS logsvk: graphics pipeline for … not made. The test pushes 10 of its 237 layouts that need a set, and gives 16 sets no update-after-bind flag(26 cases fail).
What changed
Under #643's
set_for_tablesfix (MoltenVK), a program is no longer pushed when one of its stages would not fit a Metalfunction's own tables, counted per Vulkan stage bit as MoltenVK numbers them: buffers past 7 in a vertex stage, 23 in a
fragment stage or 27 in a compute stage (the push constants' buffer counted, MoltenVK's top slots kept), textures past
128, samplers past 16. Every ordinary set under the fix is now an update-after-bind layout from update-after-bind pools,
which Vulkan counts against MoltenVK's argument-buffer limits (500,000 samplers here) instead of the 16 for ordinary sets.
A line at close counts the moves by class. The SPIR-V, the translator's stamp, the program cache and the vendor fixes' log
line are unchanged.
gpu/vk/vk_draw.cpp:kPushTextures = 128,kPushSamplers = 16andpush_buffers(stage)(vertex 31 − 16 vertexbuffers − 8 implicit = 7, fragment 31 − 8 = 23, compute 31 − 4 = 27), each with its source. In
layout_pushes, afterthe table and read_write checks (#643, #645), the bindings are summed per stage bit (
ArgTables): uniform and storagebuffers as buffers, combined image samplers as a texture and a sampler, texel buffers and storage images as textures,
each by
descriptorCount; a stage over a budget gets a set, counted inst_sets_for_buffers,_texturesor_samplers(one count per program).draw_layouts_report()logsvk: Metal argument tables (RX_VK_SET_FOR_TABLES): ….ensure_layoutan ordinary (not pushed) set layout getsVK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BITunder the fix, and
make_poolcreates the recordings' pools withVK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT(every program set comes from them). No binding carries the update-after-bind binding flag, so no device feature is
needed; MoltenVK 1.4.2 reads neither flag.
gpu/vk/vk_vendor.handgpu/vk/vk_draw.h(comments),docs/development.md(theRX_VK_SET_FOR_TABLESrow),gpu/vk/VULKAN.md(Descriptors bullet).tests/test_set_for_tables.pygrows from 134 to 237 layouts: compute 26 buffers pushed and 27 a set; vertex 1 plusfragment 22 and 23; vertex 6 and 7; the mesh fallback's task 13 plus mesh 13 and 14 (one stage bit); 128 and 129 textures;
16 and 17 sampled images, and a sampler array of 16 and 17; vertex 16 plus fragment 16 samplers pushed; task 8 plus mesh
9 samplers a set; the switch both ways; ten other drivers as before. Every case also checks the layout's flags whole (the
push bit, or the update-after-bind pool bit wherever the fix is on), and the four counters must be
rw_textures=7 buffers=5 textures=1 samplers=4.rw.sampled(36 samplers in one stage, expected pushed, which MoltenVK cannot take)now has 16 and still shows that read-only textures do not count as read_write.
tests/test_vendor_fixes.pychecks thecounters and
make_pool's flag.tests/recording_admission_test.cpp(third commit): its device stub, which compilesmake_pool, getsfixes.set_for_tables, off.Measurements
Verdict: passes: the new test fails on #645's head (26 of 237 cases: 10 layouts pushed that need a set, 16 sets without
the update-after-bind flag, and no counters) and passes at this head, two interleaved runs each. In the game (history, an
integration build) the close line counted 3,366 programs moved for buffers, 0 for textures and 1,755 for samplers.
Baseline: #645 @
fc5a86dc, this PR's base in the stack. Main lacks #643'slayout_pushesand #645's per-stage count, whichthis PR extends, so main against this PR would measure them too.
Hardware: macOS 26.7.1, Apple M4 Max, 16-core CPU, 64 GB, Apple clang 21.0.0 (game-free: no GPU, no game)
Scene: none for the tests (a layout fix: its proof is the test). The game row below is history: start 12's precompile,
windowless, defaults
Method:
tests/test_set_for_tables.pyandtests/test_vendor_fixes.pyrun as this head, base, this head, base (arm64,fresh HOME and TMPDIR,
tools/slot.py), the base tree with this PR's two test files copied in (SHA-256 equal)How to reproduce:
python -I -B tests/test_set_for_tables.py --cxx clang++ --headers <Vulkan-Headers/include>andpython -I -B tests/test_vendor_fixes.py --cxx clang++ --headers <Vulkan-Headers/include>, in this PR's tree and in#645's with the two test files copied in
fc5a86dc4c2ce8b7tests/test_set_for_tables.py, arm64, 2 runs eachwant=set got=push, 16flags=0/0x2); no counterstests/test_vendor_fixes.py, arm64, 2 runs eachthe programs set_for_tables gives a set are not counted for the close's log (st_sets_for_buffers)History, not this head: start 12 (
reports/2026-11-14-mac-startup-12.md, arm64, integration build a7711f3c, which carriesthese commits as
5b44fb3d,df23d59aand427ee186), the first start whose precompile laid out real pipelines: theclose line counted 3,366 programs given a set for buffers, 0 for textures and 1,755 for samplers; starts 16, 18
and 19 counted the same. No GPU probe pushed a stage past these tables; the failure mode rests on MoltenVK's and SPIRV-Cross's
sources and MoltenVK issue #2094's reports.
Flags and settings
No new switch: this widens #643's
RX_VK_SET_FOR_TABLES(a pipeline MoltenVK cannot make when pushed, which a set takes),and its sets become update-after-bind layouts. (
RX_VK_SET_FOR_LENGTHS, in this PR's diff until #644 merges, is #644'sand unchanged here.)
RX_VK_SET_FOR_TABLES0or1(#643, widened by #645): which programs get an ordinary descriptor set instead of a pushed one0turns all of it off,1turns it on with any driver=1on another driver in the game=0only to see the failures on a MacTesting
Tested: on macOS 26.7.1, Apple M4 Max, at
4c2ce8b7, this PR's head before the rebases ontoe090cc48andaa95d35f(main
6c9edffeplus #643, #644, #645 and this PR), game-free:tests/test_set_for_tables.pyandtests/test_vendor_fixes.py, arm64: pass at this head (2 runs each); fail on #645'shead with this PR's test files (2 runs each), as in Measurements. The failing side ran on arm64 only (the owner's rule for
these fixtures' negatives, after the employer's endpoint agent killed the x86_64 build of
test_set_for_tables'negative in an earlier lane).
clang++ -arch x86_64, run under Rosetta) at this head: both pass (237layouts; 17 devices).
tests/test_recording_admission.py(the third commit's fixture): passes at this head on arm64 and x86_64 (22 PASSlines each); on an archive of the second commit (
5ef59f28, without the third) its fixture does not build (no member named 'fixes', arm64), the regression the third commit fixes.tests/test_set_for_lengths.py(#644's, whose fixture compiles the same code), at this head: passes on arm64 and x86_64(138 layouts, 0 not as expected).
tools/ls-suite.pyat this head and on main6c9edffe:here 123 passed and 10 failed, main 121 passed and 10 failed. The same 10 fail on both (main's macOS gaps: the
pipeline-cache and program-cache fixtures on
posix_fadvise/posix_fallocate,test_appimage_player_build,test_crash_dialog,test_pipeline_tour, and the two Windows-only guard checks); the only status changes are thestack's two new tests passing (
related-P32-4c2ce8b7/compare-main-names-summary.txt).vk_draw.cpp,vk_main.cppandvk_progcache.cppcompiled for arm64 and x86_64 at this head with main's rendererflags (
nx2/build.py'svflagsplus-arch) and a compile-only prelude declaringposix_fallocateandposix_fadvise, which main's renderer calls and macOS lacks (#476 gives them Darwin paths): exit 0 for all six.implicit buffers; no update-after-bind layout) are caught by the test; the code they mutate (
kPushSizeto the end ofensure_layout, andmake_pool) is byte for byte the topic's after the rebase, so they were not run again.nx2/translator_files.pystamp()) isebbf160ce9626041on main and at this head.git merge-treeof this head with each open macOS PR's head (#431, #437, #438, #479, #480, #490, #491, #492): noconflict.
reports/PubWave-files/PubMvk/proof/P32-*.log,related-P32-*,objects2-P32-*,merge-tree.txt.aa95d35f, at374c55a0(4c2ce8b7before the rebases ontoe090cc48andaa95d35f):Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
apply) gave the same results as before;
python -I -B tests/run_all.pyon wave B's mergedb0e2e16, which carriesthis head: 569/32/18 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
(
reports/2026-11-15-rebase-sweep-3.md).78ec1c47, which carries every MacPR through #708 and the I41 fixes: a start from the field-dawn save to a 1,197 s hold in play, the runner's 12 of 12
checks passed, 0 skipped draws in the hold, 0 XPC errors, a clean exit (
reports/2026-11-15-mac-perf-E9.md); x86_64under Rosetta 2: run #34 on I38
3f872b0c: 16 m 52 s of play, 19 of 19 checks, 0 skipped draws, 1,906 draws a frame,no device loss, a clean exit (
reports/2026-11-14-mac-startup-34-x86_64.md).vk: Metal argument tables (RX_VK_SET_FOR_TABLES): … 3274 for buffers (…), 0 for textures (over 128), 1490 for samplers (over 16), and 0 lines withattribute parameter is out of bounds. UnderRosetta, #34's 19 of 19 checks stand for it; that log's lines for this PR are not quoted here.
Not tested: not built on Windows or Linux; its test request covers it. The game at this head (per-PR game runs are
not planned; the stack's runs with it, E9a on arm64 and #34 under Rosetta, are under Tested). No GPU probe has pushed a
stage with 17 samplers, 129 textures or 31 buffers, nor run a set with 17 or more samplers (that it works rests on
MoltenVK's source); nor a vertex stage whose descriptor buffer shares a vertex buffer's index, the one case MoltenVK
does not check. KosmicKrisp; Intel Macs (legacy AMD GPUs keep more buffer slots for reversed depth). Windows and Linux:
nothing was built or run there; their layouts do not change (the test's ten other drivers).
Must be tested:
374c55a0, it carries #643, #644 and #645) and mainaa95d35fthe same way,start each to the title and quit, expecting the build to succeed, no
vk: Metal argument tablesline at close, thevk: vendor fixesline with the chain off (not pushed 0for tables and for lengths), and every other line to match main's(by hand: test request T-ZX7B)
Risks
What could break: on MoltenVK many more programs now get a set (3,366 for buffers and 1,755 for samplers in start 12):
each allocates, writes and binds a set per draw or dispatch instead of a push, a cost not measured. The buffer budgets keep
every slot MoltenVK may give its own buffers on any Mac, so some programs that would have pushed safely on Apple GPUs get a
set too (the exact, smaller budgets would need the vertex input and the GPU in
layout_pushes). Update-after-bind costsnothing in MoltenVK 1.4.2, which reads neither flag. Other drivers: unchanged (
set_for_tablesis off there); withRX_VK_SET_FOR_TABLES=1their sets become update-after-bind too, which is valid.Turning it off:
RX_VK_SET_FOR_TABLES=0turns off this, #645's and #643's moves together, or revert the PR.Compatibility: Nothing changes in saves,
longshot_options.ini, the vk ABI (MKWVK_API_VERSION) or the shader caches(none of the changed files is in the translator's stamp,
ebbf160ce9626041before and after; the program cache's formatis the same). No log line changes off macOS. On macOS the moved programs' pipelines change layout, so Metal compiles them
once.
Reviewer notes
fc5a86dc..4c2ce8b7(this PR's three commits, cherry-picked with-xfrommacos/mvk-pushlimits:7cfa3cf6from
5b44fb3d, the budgets;5ef59f28fromdf23d59a, the update-after-bind layouts and pools;4c2ce8b7from427ee186,test_recording_admission's stub, which the second commit broke and the full suite caught).fc5a86dc, and shows their commitsuntil they merge.
docs/development.mdconflicts (#1917ec25fd1, the rows nextto it) were taken care of in #643's and #644's.
tests/recording_admission_test.cppchanged on main since the topic'sbase and merged on its own. No semantic rework:
layout_pushes,ensure_layoutandmake_poolare byte for byte thetopic's. Main's #252 Intel compiler gate wraps compiles, not layouts or pools.
in
tests/test_set_for_tables.py: #663's--sourceoption and segmented case against this PR's cases, counters andflag checks, in the docstring, the
CASESsum andmain(). Keep both sides (the integration's resolution: the sum ofboth sides' cases,
excerpt(vk)from--source, the counters). #663'sseg_layout_countinensure_layout'saddiscounted by these budgets as by #645's, with no further change.
git merge-treeagainst their branch tips (#650f5905178meets only #643's and#644's
vk_main.cppline).platform), and the device's limits in
host_info()(which would cap indexed arrays below 1024 on every platform).Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head374c55a0, on #645b8b1ba99, without aconflict; this PR's own commits, old → new:
7cfa3cf6→6c9e3c9f,5ef59f28→3e7d8d3d,4c2ce8b7→374c55a0;the commit names elsewhere in this description are from before the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge of every macOS PR built with this one,453eb678: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own excepttest_split_ub_flush, which #658's tree at the time broke and its head fixes;test_collect_widenow passes; the new tests pass andtest_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/waveA/. After the rebase onto mainaa95d35f(head374c55a0): wave B's mergedb0e2e16, 569/32/18 against main aa95d35f's 543/33/17, no new failure.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here. (Failing on #645's head, this PR's base.)tests/that reads changed source updated in this PR, not weakened.4c2ce8b7a78a5c10223f8a5c10223f374c55a031WIP: fix(vk): use update-after-bind sets past Metal's per-stage limits on MoltenVKto fix(vk): use update-after-bind sets past Metal's per-stage limits on MoltenVKFolded into #847 (fix(vk): MoltenVK's descriptor limits in descriptor sets)
Pull request closed