WIP: fix(vk): MoltenVK's descriptor limits in descriptor sets #847
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!847
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/topic-mvk-descriptor-limits"
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 (arm64; x86_64 under Rosetta) · Needs: Windows and Linux, any GPU
Why
On a Mac the Vulkan renderer runs on MoltenVK, and MoltenVK binds a pushed descriptor set straight into a Metal function's own
resource tables, outside Metal's argument buffers. Four kinds of shader program break there when their set is pushed: a
program that reads the game's memory through a storage buffer table fails to build (the first Mac start lost 256 compute
pipelines that way); a program that reads a storage buffer's length reads 0 for every buffer, so its writes and atomics to
game memory are silently dropped; a stage with more than 8 storage images and storage texel buffers fails to compile (Metal
allows 8 read_write textures a function); and a stage with more buffers, textures or samplers than a Metal function's
tables hold fails to compile. Whatever those programs draw or compute is missing or wrong. In an ordinary descriptor set,
which MoltenVK keeps in a Metal 3 argument buffer, every one of them works. This PR gives those programs ordinary sets on
MoltenVK; every other driver is unchanged. 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, arm64 and x86_64 under Rosetta (main does not build on macOS yet:
seen on the macOS port's integration builds and in game-free tests and GPU probes)
Settings and switches: defaults
Steps: start the game on a fresh profile and let the first start prepare its shaders, then read longshot_log.txt.
Game-free: copy this PR's
tests/test_set_for_tables.py,tests/test_set_for_lengths.py,tests/set_for_lengths_ir.cppand
tests/test_vendor_fixes.pyinto main's tree and runpython -I -B tests/test_set_for_tables.py --cxx clang++ --headers <Vulkan-Headers/include>andpython -I -B tests/test_set_for_lengths.py --cxx clang++ --spirv-headers <SPIRV-Headers/include> --headers <Vulkan-Headers/include>.Expected: every program's pipeline is made and reads exact storage buffer lengths; the tests pass.
Actual: 256 pairs of
[mvk-error] SPIR-V to MSL conversion error: Attempting to alias same binding with a descriptor array which is not implemented through argument buffersandCompute shader function could not be compiled into pipeline(the first start,
mac-arm64-a5f2d6f9-01); the NullRangeProbe GPU check read 0 for every pushed storage buffer'sOpArrayLengthon both CPU routes; a pushed compute stage with 40 storage images and 9 storage texel buffers is refused withnumber of textures with read_write access exceeds maximum supported (8); a stage past a Metal function's table is refusedat its out-of-range
[[buffer]],[[texture]]or[[sampler]]index. The tests push layouts that need a set (Measurements).What changed
On MoltenVK a program is no longer pushed when its set holds a storage buffer table, when any stage's SPIR-V reads a storage
buffer's length, when one stage has more than 8 storage images and storage texel buffers, or when one stage would not fit a
Metal function's buffer, texture or sampler table; it gets an ordinary descriptor set from the recording's pool instead
(update-after-bind, from update-after-bind pools), the path programs over the push limit already take. Every other program
is still pushed, the bindings are the same either way, and the SPIR-V, the translator's stamp and the program cache's format
are unchanged. Two switches force it either way; the
vk: vendor fixesline says whether each is on, and a line at closecounts the programs moved. The four original pull requests, in commit order:
063d87c4; Was: #643):layout_pushes()ingpu/vk/vk_draw.cppsends a set with a storage buffer array to an ordinary set under the new vendorfix
set_for_tables(gpu/vk/vk_vendor.h, on with MoltenVK only;RX_VK_SET_FOR_TABLES);gpu/vk/vk_main.cpp'svk: vendor fixesline gains its field. Tests:tests/test_set_for_tables.py(new),tests/test_vendor_fixes.py.02ad928d; Was: #644):gpu/vk/spirv_lengths.h(new) scans a stage's SPIR-V once forOpArrayLengthwhere the program cache already handles thecode (
gpu/vk/vk_progcache.cpp,Stage::array_lengthingpu/vk/vk_shader.h); such a program gets a set under the vendorfix
set_for_lengths(RX_VK_SET_FOR_LENGTHS, MoltenVK only); a compute program read from the cache without its code isdecided by its global memory use until the code is read;
draw_layouts_report()(gpu/vk/vk_draw.h) prints the closeline. Tests:
tests/test_set_for_lengths.py,tests/set_for_lengths_ir.cpp(new); the program cache fixtures follow.5b523fef; Was: #645):under
set_for_tables, storage images and storage texel buffers are counted per stage (every array element; the meshfallback's task and mesh stages as two functions, from
stage_first), and a stage over 8 gets a set.792a7d8a; Was: #646): underset_for_tables, a stage past 7 buffers (vertex), 23 (fragment) or 27 (compute), 128 textures or 16 samplers gets a set(MoltenVK's top buffer slots kept), every set under the fix is update-after-bind from update-after-bind pools, and the
close line counts the moves by class.
tests/recording_admission_test.cppfollows the newdev.fixesfield.docs/development.md(the two switches' rows),gpu/vk/VULKAN.md.Measurements
Verdict: passes. Each member's new test fails before it and passes after it (two interleaved runs each in the members'
own runs), and every one passes at this PR's head on arm64 and on x86_64 under Rosetta; in the game (history, integration
builds) the 256 alias errors went to 0 on arm64 and under Rosetta, and at close 2,023 programs were moved for lengths and
3,366 for buffers, 0 for textures and 1,755 for samplers. The full suite at this head: 545 passed, 33 failed, 17 skipped against
main's 543/33/17 on this Mac, the only differences this PR's two new tests passing. No timing claimed: the cost
of a set against a push is not measured.
Baseline: the rows of #644, #645 and #646 compare with each member's own base in the stack (the heads of #643, #644 and
#645), as those pull requests measured them: main lacks the
layout_pushesthey extend. The game rows are the macOS port'sintegration builds (main does not run on macOS).
Hardware: macOS 26.7.1, Apple M4 Max, 16-core CPU, 64 GB, Apple clang 21.0.0; MoltenVK 1.4.2 for the game rows
Scene: none for the tests (a layout fix: its proof is the test); the game rows: the start-up precompile of integration
starts 01 and 12, windowless, defaults
Method: each test before and after its pull request, two interleaved runs each (the members' runs); at this head each test
once on arm64 and once with its fixture built for x86_64 and run under Rosetta; the suite once through
tools/ls-suite.pyHow to reproduce:
python -I -B tests/test_set_for_tables.py --cxx clang++ --headers <Vulkan-Headers/include>,python -I -B tests/test_set_for_lengths.py --cxx clang++ --spirv-headers <SPIRV-Headers/include> --headers <Vulkan-Headers/include> --jobs 1,python -I -B tests/test_vendor_fixes.py --cxx clang++ --headers <Vulkan-Headers/include>aa95d35f792a7d8atest_set_for_tables.py, storage buffer table layouts pushed where a set is neededtest_set_for_lengths.py, length-reading layouts pushedFlags and settings
Two new switches; by default both are on with MoltenVK only (unset), off with every other driver.
RX_VK_SET_FOR_TABLES0or1: programs with a storage buffer table, a stage over 8 storage images and storage texel buffers, or a stage past a Metal function's buffer, texture or sampler table get an ordinary (update-after-bind) set instead of a push0off,1on whatever the driver=1on another driver in the game=0only to see the failures on a MacRX_VK_SET_FOR_LENGTHS0or1: a program whose SPIR-V reads a storage buffer's length in any stage gets an ordinary set0off,1on whatever the driver=0only to compareTesting
Tested: on macOS 26.7.1, Apple M4 Max, at this head
792a7d8a(a clean worktree):clang++ -arch x86_64):test_set_for_tables,test_set_for_lengths,test_vendor_fixes,test_program_cache_one_variantand
test_program_cache_cleanup(with a no-opposix_fadviseprelude, as the members ran them),test_recording_admission:12 runs, 12 exit 0 (237 layouts; 138 layouts, 0 not as expected; 17 devices; the program cache's byte counts as before;
22 PASS lines).
test_set_for_tables' x86_64 fixture was killed once by the system (exit -9, no crash report), as in themembers' own runs of #643-#645, and passed on its one rerun.
python -I -B tests/run_all.pythroughtools/ls-suite.pyat this head: 545 passed, 33 failed, 17 skipped, against mainaa95d35f's 543/33/17 on this Mac, compared by test name: the only differences aretest_set_for_tablesandtest_set_for_lengths, new and passing. The 33 failures are main's own on this Mac (main without the macOS host topic: therunner still calls a Mac
linux, and main's renderer and host checks that need the macOS PRs fail as on main).scratch merge of this head with the macOS PRs that make it build, #476, #492 and #490, with main's renderer flags): 0 of 595
declared
mkwvk::names lack a definition, 66 of 66 objects compiled (arm64 and x86_64), 542 cross-file symbols perarchitecture all defined.
unifdef -U__APPLE__of every changed non-test C/C++ file, main against this head, compared as tokens): therenderer files differ there by design, since this fix is chosen at run time by the driver (
dev.fixes), not at compiletime:
vk_draw.cpp(layout_pushes, the counts, the update-after-bind layout and pool,draw_layouts_report),vk_draw.h,vk_main.cpp(thevk: vendor fixesline's two new fields),vk_vendor.h(the two fixes, off but forMoltenVK),
vk_progcache.cppandvk_shader.h(theOpArrayLengthscan, run on every platform), and the newspirv_lengths.h. With the fixes off, other drivers' layouts are the same (the tests' ten other drivers).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). The cost of a set against a push
in a real scene (
RX_VK_PROF_FINE=1counts the sets allocated); the game with either switch at0; KosmicKrisp; an IntelMac (where MoltenVK may run without argument buffers); a GPU probe of a pushed stage with 17 samplers, 129 textures or 31
buffers (that a set takes them rests on MoltenVK's source).
Must be tested:
aa95d35fthe same way, runpython -I -B tests/run_all.pyon each andstart each to the title, expecting
test_set_for_tablesandtest_set_for_lengthsto pass, no other check to change status,and the
vk:lines to match main's but for thevk: vendor fixesline's two new fields at 0 (Test Farm: @Beanstalk windowslinux, build, run_all and start to the title)
Risks
What could break: on MoltenVK many more programs now allocate, write and bind a set per draw or dispatch instead of a
push (2,023, 3,366 and 1,755 programs in start 12), a cost not measured; a recording's pool that runs out is followed by
another of the same size. The buffer budgets keep every slot MoltenVK may take for itself, so some programs that would have
pushed safely get a set too. On other drivers: the layouts are identical, each stage's SPIR-V gets one more walk over its words
where the program cache already handles it, and the
vk: vendor fixesline has two more fields.Turning it off:
RX_VK_SET_FOR_TABLES=0andRX_VK_SET_FOR_LENGTHS=0(MoltenVK then fails or misreads these programs ason main), or revert the PR.
Compatibility: nothing changes in saves,
longshot_options.inior the vk ABI (MKWVK_API_VERSION). The shader caches arekept: none of the changed files is in the translator's stamp (
ebbf160ce9626041before and after) and the program cache'sformat is the same. On Windows and Linux the
vk: vendor fixesline has two more fields (0). On macOS the moved programs'pipelines change layout, so Metal compiles them once.
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.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (macOS: 545/33/17, the 33 failures exactly main's on this Mac,reports/Baseline-files/main-aa95d35f; Windows and Linux: the test request.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.792a7d8a3288e914ad4eRebased onto
main@7bacf846. Conflicts ingpu/vk/vk_shader.hin three commits (main'scode_at/record_atmembers ofStageand this PR'sarray_lengthmember, both added at the end of the struct): kept both. No tests were run here; the test boxes stay yours.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.