fix(vk): use update-after-bind sets past Metal's per-stage limits on MoltenVK #646

Closed
mooki wants to merge 6 commits from mooki/Longshot:mac/mvk-pushlimits into main
Member

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.py and
tests/test_vendor_fixes.py, and run python -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 more
textures, 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 and
MoltenVK fails the pipeline (VK_ERROR_INVALID_SHADER_NV / VK_ERROR_INITIALIZATION_FAILED), and LS logs vk: 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_tables fix (MoltenVK), a program is no longer pushed when one of its stages would not fit a Metal
function'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.

  • vk: gpu/vk/vk_draw.cpp: kPushTextures = 128, kPushSamplers = 16 and push_buffers(stage) (vertex 31 − 16 vertex
    buffers − 8 implicit = 7, fragment 31 − 8 = 23, compute 31 − 4 = 27), each with its source. In layout_pushes, after
    the table and read_write checks (#643, #645), the bindings are summed per stage bit (ArgTables): uniform and storage
    buffers 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 in st_sets_for_buffers, _textures or
    _samplers (one count per program). draw_layouts_report() logs vk: Metal argument tables (RX_VK_SET_FOR_TABLES): ….
  • vk: in ensure_layout an ordinary (not pushed) set layout gets VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT
    under the fix, and make_pool creates the recordings' pools with VK_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.
  • vk: gpu/vk/vk_vendor.h and gpu/vk/vk_draw.h (comments), docs/development.md (the RX_VK_SET_FOR_TABLES row),
    gpu/vk/VULKAN.md (Descriptors bullet).
  • tests: tests/test_set_for_tables.py grows from 134 to 237 layouts: compute 26 buffers pushed and 27 a set; vertex 1 plus
    fragment 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.py checks the
    counters and make_pool's flag. tests/recording_admission_test.cpp (third commit): its device stub, which compiles
    make_pool, gets fixes.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's layout_pushes and #645's per-stage count, which
this 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.py and tests/test_vendor_fixes.py run 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> and
python -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

Before: #645 @ fc5a86dc After: this PR @ 4c2ce8b7
tests/test_set_for_tables.py, arm64, 2 runs each fails both runs: 26 of 237 cases (10 want=set got=push, 16 flags=0/0x2); no counters passes both runs: 237 of 237, the counters as expected
tests/test_vendor_fixes.py, arm64, 2 runs each fails both runs: the programs set_for_tables gives a set are not counted for the close's log (st_sets_for_buffers) passes both runs: 17 devices
#645 @ fc5a86dc (this PR's test), the pushes:
  CASE moltenvk.tables.compute27 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE moltenvk.tables.fragment23 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE moltenvk.tables.vertex7 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE moltenvk.tables.fallback13_14 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE moltenvk.tables.textures129 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE moltenvk.tables.samplers17 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE moltenvk.tables.sampler_array17 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE moltenvk.tables.samplers_fallback8_9 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE nvidia.switch1.tables.samplers17 driver=4 RX_VK_SET_FOR_TABLES=1 want=set got=push flag=1 flags=0x1/0x2 FAIL
  CASE nvidia.switch1.tables.vertex7 driver=4 RX_VK_SET_FOR_TABLES=1 want=set got=push flag=1 flags=0x1/0x2 FAIL
  and the sets, for example: CASE moltenvk.table_compute driver=14 want=set got=set flag=0 flags=0/0x2 FAIL
  SUMMARY cases=237 failures=26
this PR @ 4c2ce8b7:
ok: 237 layouts; on MoltenVK a set with a storage buffer table, a stage with more than 8 storage images and storage texel buffers, or a stage past a Metal function's buffer, texture or sampler table is not pushed, and only such a set; every other driver as before; RX_VK_SET_FOR_TABLES forces it both ways; the bindings are the same pushed or not; the moves are counted by class

History, not this head: start 12 (reports/2026-11-14-mac-startup-12.md, arm64, integration build a7711f3c, which carries
these commits as 5b44fb3d, df23d59a and 427ee186), the first start whose precompile laid out real pipelines: the
close 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's
and unchanged here.)

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
RX_VK_SET_FOR_TABLES 0 or 1 (#643, widened by #645): which programs get an ordinary descriptor set instead of a pushed one unset: on with MoltenVK; moves storage buffer tables and stages with more than 8 read_write textures unset: on with MoltenVK; also moves stages past a Metal function's buffer, texture or sampler table, and every set under it is update-after-bind; 0 turns all of it off, 1 turns it on with any driver on: these programs' pipelines are made on MoltenVK; a line at close counts the moves by class helps: MoltenVK (3,366 and 1,755 programs in start 12); doesn't help: other drivers; untested: the cost of the extra sets, and =1 on another driver in the game leave unset; =0 only to see the failures on a Mac

Testing

Tested: on macOS 26.7.1, Apple M4 Max, at 4c2ce8b7, this PR's head before the rebases onto e090cc48 and aa95d35f
(main 6c9edffe plus #643, #644, #645 and this PR), game-free:

  • tests/test_set_for_tables.py and tests/test_vendor_fixes.py, arm64: pass at this head (2 runs each); fail on #645's
    head 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).
  • With their fixtures compiled for x86_64 (clang++ -arch x86_64, run under Rosetta) at this head: both pass (237
    layouts; 17 devices).
  • tests/test_recording_admission.py (the third commit's fixture): passes at this head on arm64 and x86_64 (22 PASS
    lines 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).
  • The 133 tests that read the files this stack changes, through tools/ls-suite.py at this head and on main 6c9edffe:
    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 the
    stack's two new tests passing (related-P32-4c2ce8b7/compare-main-names-summary.txt).
  • vk_draw.cpp, vk_main.cpp and vk_progcache.cpp compiled for arm64 and x86_64 at this head with main's renderer
    flags (nx2/build.py's vflags plus -arch) and a compile-only prelude declaring posix_fallocate and
    posix_fadvise, which main's renderer calls and macOS lacks (#476 gives them Darwin paths): exit 0 for all six.
  • The topic's three mutations (a table per program stage instead of per stage bit; the vertex budget without the eight
    implicit buffers; no update-after-bind layout) are caught by the test; the code they mutate (kPushSize to the end of
    ensure_layout, and make_pool) is byte for byte the topic's after the rebase, so they were not run again.
  • The translator's stamp (nx2/translator_files.py stamp()) is ebbf160ce9626041 on main and at this head.
  • git merge-tree of this head with each open macOS PR's head (#431, #437, #438, #479, #480, #490, #491, #492): no
    conflict.
  • Logs: reports/PubWave-files/PubMvk/proof/P32-*.log, related-P32-*, objects2-P32-*, merge-tree.txt.
  • After the rebase onto main aa95d35f, at 374c55a0 (4c2ce8b7 before the rebases onto e090cc48 and aa95d35f):
    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.py on wave B's merge db0e2e16, which carries
    this 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).
  • macOS game runs with this PR in the stack: arm64, windowless: run E9a on I41 78ec1c47, which carries every Mac
    PR 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_64
    under 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).
  • E9a's log at close (line 65748): 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 with attribute parameter is out of bounds. Under
    Rosetta, #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:

  • Windows and Linux, any GPU: build this PR (head 374c55a0, it carries #643, #644 and #645) and main aa95d35f the same way,
    start each to the title and quit, expecting the build to succeed, no vk: Metal argument tables line at close, the vk: vendor fixes line with the chain off (not pushed 0 for 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 costs
nothing in MoltenVK 1.4.2, which reads neither flag. Other drivers: unchanged (set_for_tables is off there); with
RX_VK_SET_FOR_TABLES=1 their sets become update-after-bind too, which is valid.

Turning it off: RX_VK_SET_FOR_TABLES=0 turns 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, ebbf160ce9626041 before and after; the program cache's format
is the same). No log line changes off macOS. On macOS the moved programs' pipelines change layout, so Metal compiles them
once.

Reviewer notes

  • Review fc5a86dc..4c2ce8b7 (this PR's three commits, cherry-picked with -x from macos/mvk-pushlimits: 7cfa3cf6
    from 5b44fb3d, the budgets; 5ef59f28 from df23d59a, the update-after-bind layouts and pools; 4c2ce8b7 from
    427ee186, test_recording_admission's stub, which the second commit broke and the full suite caught).
  • Depends on #645 (and through it #644 and #643): this branch is stacked on #645's head, fc5a86dc, and shows their commits
    until they merge.
  • Conflicts with main: none in the cherry-picks. The plan's docs/development.md conflicts (#191 7ec25fd1, the rows next
    to it) were taken care of in #643's and #644's. tests/recording_admission_test.cpp changed on main since the topic's
    base and merged on its own. No semantic rework: layout_pushes, ensure_layout and make_pool are byte for byte the
    topic's. Main's #252 Intel compiler gate wraps compiles, not layouts or pools.
  • #663 (seg-bind, a later wave): the plan has this PR rebase after #663. Based here on #645 instead, it will meet #663
    in tests/test_set_for_tables.py: #663's --source option and segmented case against this PR's cases, counters and
    flag checks, in the docstring, the CASES sum and main(). Keep both sides (the integration's resolution: the sum of
    both sides' cases, excerpt(vk) from --source, the counters). #663's seg_layout_count in ensure_layout's add is
    counted by these budgets as by #645's, with no further change.
  • With the other PRs of this wave: none in git merge-tree against their branch tips (#650 f5905178 meets only #643's and
    #644's vk_main.cpp line).
  • Owner items from the topic, not in this PR: separate sampled images and samplers (a SPIR-V and stamp change on every
    platform), and the device's limits in host_info() (which would cap indexed arrays below 1024 on every platform).

Issues

See #420

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how. (Test request T-ZX7B.)
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head 374c55a0, on #645 b8b1ba99, without a
    conflict; 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.py passes (any failure that also fails on main is 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 except test_split_ub_flush, which #658's tree at the time broke and its head fixes; test_collect_wide now passes; the new tests pass and test_darwin_audio_service, Darwin-only, is skipped by the runner; reports/PubWave-files/waveA/. After the rebase onto main aa95d35f (head 374c55a0): wave B's merge db0e2e16, 569/32/18 against main aa95d35f's 543/33/17, no new failure.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • A game-free test added for the fix, failing on main and passing here. (Failing on #645's head, this PR's base.)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.
**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.py` and `tests/test_vendor_fixes.py`, and run `python -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 more textures, 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 and MoltenVK fails the pipeline (`VK_ERROR_INVALID_SHADER_NV` / `VK_ERROR_INITIALIZATION_FAILED`), and LS logs `vk: 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_tables` fix (MoltenVK), a program is no longer pushed when one of its stages would not fit a Metal function'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. - vk: `gpu/vk/vk_draw.cpp`: `kPushTextures = 128`, `kPushSamplers = 16` and `push_buffers(stage)` (vertex 31 − 16 vertex buffers − 8 implicit = 7, fragment 31 − 8 = 23, compute 31 − 4 = 27), each with its source. In `layout_pushes`, after the table and read_write checks (#643, #645), the bindings are summed per stage bit (`ArgTables`): uniform and storage buffers 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 in `st_sets_for_buffers`, `_textures` or `_samplers` (one count per program). `draw_layouts_report()` logs `vk: Metal argument tables (RX_VK_SET_FOR_TABLES): …`. - vk: in `ensure_layout` an ordinary (not pushed) set layout gets `VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT` under the fix, and `make_pool` creates the recordings' pools with `VK_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. - vk: `gpu/vk/vk_vendor.h` and `gpu/vk/vk_draw.h` (comments), `docs/development.md` (the `RX_VK_SET_FOR_TABLES` row), `gpu/vk/VULKAN.md` (Descriptors bullet). - tests: `tests/test_set_for_tables.py` grows from 134 to 237 layouts: compute 26 buffers pushed and 27 a set; vertex 1 plus fragment 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.py` checks the counters and `make_pool`'s flag. `tests/recording_admission_test.cpp` (third commit): its device stub, which compiles `make_pool`, gets `fixes.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's `layout_pushes` and #645's per-stage count, which this 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.py` and `tests/test_vendor_fixes.py` run 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>` and `python -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 | | Before: #645 @ fc5a86dc | After: this PR @ 4c2ce8b7 | |---|---|---| | `tests/test_set_for_tables.py`, arm64, 2 runs each | fails both runs: 26 of 237 cases (10 `want=set got=push`, 16 `flags=0/0x2`); no counters | passes both runs: 237 of 237, the counters as expected | | `tests/test_vendor_fixes.py`, arm64, 2 runs each | fails both runs: `the programs set_for_tables gives a set are not counted for the close's log (st_sets_for_buffers)` | passes both runs: 17 devices | ``` #645 @ fc5a86dc (this PR's test), the pushes: CASE moltenvk.tables.compute27 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE moltenvk.tables.fragment23 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE moltenvk.tables.vertex7 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE moltenvk.tables.fallback13_14 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE moltenvk.tables.textures129 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE moltenvk.tables.samplers17 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE moltenvk.tables.sampler_array17 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE moltenvk.tables.samplers_fallback8_9 driver=14 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE nvidia.switch1.tables.samplers17 driver=4 RX_VK_SET_FOR_TABLES=1 want=set got=push flag=1 flags=0x1/0x2 FAIL CASE nvidia.switch1.tables.vertex7 driver=4 RX_VK_SET_FOR_TABLES=1 want=set got=push flag=1 flags=0x1/0x2 FAIL and the sets, for example: CASE moltenvk.table_compute driver=14 want=set got=set flag=0 flags=0/0x2 FAIL SUMMARY cases=237 failures=26 this PR @ 4c2ce8b7: ok: 237 layouts; on MoltenVK a set with a storage buffer table, a stage with more than 8 storage images and storage texel buffers, or a stage past a Metal function's buffer, texture or sampler table is not pushed, and only such a set; every other driver as before; RX_VK_SET_FOR_TABLES forces it both ways; the bindings are the same pushed or not; the moves are counted by class ``` History, not this head: start 12 (`reports/2026-11-14-mac-startup-12.md`, arm64, integration build a7711f3c, which carries these commits as `5b44fb3d`, `df23d59a` and `427ee186`), the first start whose precompile laid out real pipelines: the close 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's and unchanged here.) | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | `RX_VK_SET_FOR_TABLES` | `0` or `1` (#643, widened by #645): which programs get an ordinary descriptor set instead of a pushed one | unset: on with MoltenVK; moves storage buffer tables and stages with more than 8 read_write textures | unset: on with MoltenVK; also moves stages past a Metal function's buffer, texture or sampler table, and every set under it is update-after-bind; `0` turns all of it off, `1` turns it on with any driver | on: these programs' pipelines are made on MoltenVK; a line at close counts the moves by class | helps: MoltenVK (3,366 and 1,755 programs in start 12); doesn't help: other drivers; untested: the cost of the extra sets, and `=1` on another driver in the game | leave unset; `=0` only to see the failures on a Mac | ## Testing **Tested:** on macOS 26.7.1, Apple M4 Max, at `4c2ce8b7`, this PR's head before the rebases onto e090cc48 and aa95d35f (main 6c9edffe plus #643, #644, #645 and this PR), game-free: - `tests/test_set_for_tables.py` and `tests/test_vendor_fixes.py`, arm64: pass at this head (2 runs each); fail on #645's head 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). - With their fixtures compiled for x86_64 (`clang++ -arch x86_64`, run under Rosetta) at this head: both pass (237 layouts; 17 devices). - `tests/test_recording_admission.py` (the third commit's fixture): passes at this head on arm64 and x86_64 (22 PASS lines 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). - The 133 tests that read the files this stack changes, through `tools/ls-suite.py` at this head and on main 6c9edffe: 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 the stack's two new tests passing (`related-P32-4c2ce8b7/compare-main-names-summary.txt`). - `vk_draw.cpp`, `vk_main.cpp` and `vk_progcache.cpp` compiled for arm64 and x86_64 at this head with main's renderer flags (`nx2/build.py`'s `vflags` plus `-arch`) and a compile-only prelude declaring `posix_fallocate` and `posix_fadvise`, which main's renderer calls and macOS lacks (#476 gives them Darwin paths): exit 0 for all six. - The topic's three mutations (a table per program stage instead of per stage bit; the vertex budget without the eight implicit buffers; no update-after-bind layout) are caught by the test; the code they mutate (`kPushSize` to the end of `ensure_layout`, and `make_pool`) is byte for byte the topic's after the rebase, so they were not run again. - The translator's stamp (`nx2/translator_files.py` `stamp()`) is `ebbf160ce9626041` on main and at this head. - `git merge-tree` of this head with each open macOS PR's head (#431, #437, #438, #479, #480, #490, #491, #492): no conflict. - Logs: `reports/PubWave-files/PubMvk/proof/P32-*.log`, `related-P32-*`, `objects2-P32-*`, `merge-tree.txt`. - **After the rebase onto main aa95d35f**, at `374c55a0` (`4c2ce8b7` before the rebases onto e090cc48 and aa95d35f): 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.py` on wave B's merge `db0e2e16`, which carries this 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`). - **macOS game runs with this PR in the stack:** arm64, windowless: run E9a on I41 `78ec1c47`, which carries every Mac PR 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_64 under 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`). - **E9a's log** at close (line 65748): `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 with `attribute parameter is out of bounds`. Under Rosetta, #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:** - Windows and Linux, any GPU: build this PR (head 374c55a0, it carries #643, #644 and #645) and main aa95d35f the same way, start each to the title and quit, expecting the build to succeed, no `vk: Metal argument tables` line at close, the `vk: vendor fixes` line with the chain off (`not pushed 0` for 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 costs nothing in MoltenVK 1.4.2, which reads neither flag. Other drivers: unchanged (`set_for_tables` is off there); with `RX_VK_SET_FOR_TABLES=1` their sets become update-after-bind too, which is valid. **Turning it off:** `RX_VK_SET_FOR_TABLES=0` turns 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, `ebbf160ce9626041` before and after; the program cache's format is the same). No log line changes off macOS. On macOS the moved programs' pipelines change layout, so Metal compiles them once. ## Reviewer notes - Review `fc5a86dc..4c2ce8b7` (this PR's three commits, cherry-picked with `-x` from `macos/mvk-pushlimits`: `7cfa3cf6` from `5b44fb3d`, the budgets; `5ef59f28` from `df23d59a`, the update-after-bind layouts and pools; `4c2ce8b7` from `427ee186`, `test_recording_admission`'s stub, which the second commit broke and the full suite caught). - Depends on #645 (and through it #644 and #643): this branch is stacked on #645's head, fc5a86dc, and shows their commits until they merge. - Conflicts with main: none in the cherry-picks. The plan's `docs/development.md` conflicts (#191 `7ec25fd1`, the rows next to it) were taken care of in #643's and #644's. `tests/recording_admission_test.cpp` changed on main since the topic's base and merged on its own. No semantic rework: `layout_pushes`, `ensure_layout` and `make_pool` are byte for byte the topic's. Main's #252 Intel compiler gate wraps compiles, not layouts or pools. - **#663 (seg-bind, a later wave):** the plan has this PR rebase after #663. Based here on #645 instead, it will meet #663 in `tests/test_set_for_tables.py`: #663's `--source` option and segmented case against this PR's cases, counters and flag checks, in the docstring, the `CASES` sum and `main()`. Keep both sides (the integration's resolution: the sum of both sides' cases, `excerpt(vk)` from `--source`, the counters). #663's `seg_layout_count` in `ensure_layout`'s `add` is counted by these budgets as by #645's, with no further change. - With the other PRs of this wave: none in `git merge-tree` against their branch tips (#650 `f5905178` meets only #643's and #644's `vk_main.cpp` line). - Owner items from the topic, not in this PR: separate sampled images and samplers (a SPIR-V and stamp change on every platform), and the device's limits in `host_info()` (which would cap indexed arrays below 1024 on every platform). ## Issues See #420 ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. (Test request T-ZX7B.) - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `374c55a0`, on #645 `b8b1ba99`, without a conflict; 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. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is 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 except `test_split_ub_flush`, which #658's tree at the time broke and its head fixes; `test_collect_wide` now passes; the new tests pass and `test_darwin_audio_service`, Darwin-only, is skipped by the runner; `reports/PubWave-files/waveA/`. After the rebase onto main aa95d35f (head `374c55a0`): wave B's merge `db0e2e16`, 569/32/18 against main aa95d35f's 543/33/17, no new failure.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [x] A game-free test added for the fix, failing on `main` and passing here. (Failing on #645's head, this PR's base.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated.
fix(macos): storage buffer tables get descriptor sets on MoltenVK
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
77a91fc936
MoltenVK 1.4.2 binds a push descriptor set's resources one by one, never
through a Metal argument buffer (MVKDescriptorSet.mm pickArgumentBufferMode
returns Off for every push layout, and the pipeline names that set to
SPIRV-Cross as discrete). The SPIR-V back end declares a storage buffer
table (count > 1: Info::global_tables, the task and mesh programs'
c0+0x880 table) as one fixed-size array per typed view, all at one binding
(DefineSsbos; any global memory access uses the U32, U32x2 and U32x4
views), and SPIRV-Cross 6c09849f aliases such arrays only inside an
argument buffer. So every pushed program with a table failed: 256
"Attempting to alias same binding with a descriptor array which is not
implemented through argument buffers" errors and as many failed compute
pipelines in the first Mac start (mac-arm64-a5f2d6f9-01).

vk_vendor.h gets a fix, set_for_tables, on by default only for
VK_DRIVER_ID_MOLTENVK, with RX_VK_SET_FOR_TABLES to force it either way.
ensure_layout's push decision moves into layout_pushes(): with the fix on,
a set that has an array of storage buffers is not pushed but allocated
from the recording's pool, the path programs over the push limit already
take, where MoltenVK's sets are Metal 3 argument buffers. Every other set
is still pushed, the bindings are the same either way, and the SPIR-V,
the translator stamp and the program cache are unchanged. The vendor
fixes' log line at device creation says whether it is on, and a MoltenVK
run with MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=0 is warned that these
programs cannot translate.

tests/test_set_for_tables.py compiles vk_draw.cpp's layout code (kPushSize
to the end of ensure_layout) on made-up programs and devices: on MoltenVK
a set with a table (compute, task, fragment) is a set, a set without one
(texture arrays and a constant buffer of count 2 included) is pushed;
ten other drivers push as before; the switch works both ways. It fails
on the base with 7 of 79 cases. tests/test_vendor_fixes.py adds a MoltenVK
device and the switch, and checks that the fix stays out of the
translation's profile and stamp. Windows and Linux: same layouts as
before; the log line gains one field.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 0c2fea2a303a5158d12ebc87bcd28467486486ef)
fix(macos): programs reading storage buffer lengths get descriptor sets on MoltenVK
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
e90ba9a66c
MoltenVK 1.4.2 writes the buffer sizes SPIRV-Cross reads for OpArrayLength
in a discretely bound set only in bindDescriptorSets
(MVKCommandEncoderState.mm), never in pushDescriptorSet, and it binds every
push set discretely. So every pushed storage buffer's length read 0
whatever its range, while an ordinary set's (a Metal 3 argument buffer
with its own sizes) was exact (GPU probe,
reports/2026-11-14-null-range-probe.md). The back end measures storage
buffers in its global memory search (DefineGlobalMemoryFunctions:
native_global_bounds' candidates and every single storage buffer), so in a
pushed program no buffer ever held a global address: global stores and
atomics were dropped, the atomics returned 0, and loads the page table
does not serve read 0.

vk_vendor.h gets set_for_lengths, on by default only for
VK_DRIVER_ID_MOLTENVK, with RX_VK_SET_FOR_LENGTHS to force it either way.
The new gpu/vk/spirv_lengths.h (no Vulkan, as fallback_size_policy.h)
walks a module's words for OpArrayLength.
Each Stage gets array_length, noted beside its phis wherever the program
cache handles its SPIR-V: program_cache_save after translation (before the
program is published), a whole record read, and complete_from for a
compute program read without its code. The words are not kept anywhere a
layout could scan them later, and a light compute program's layout is made
before its code is read, so until then (array_length -1) a stage counts as
reading a length where it uses global memory: outside segmented variants,
which the cache never keeps, that is the only place the back end measures
a storage buffer. layout_pushes(p, bindings, total) then refuses a push
for any program with a length in any stage. Every other program, every
other driver, the SPIR-V, the translator stamp and the program cache are
unchanged.

The vendor fixes' log line says whether it is on, and at close one line
says how many programs it gave a set (and how many of those were decided
before their code was read).

tests/test_set_for_lengths.py builds real back end modules
(tests/set_for_lengths_ir.cpp: compute and vertex global memory with a
single storage buffer, 18 OpArrayLength each; global memory with nothing to
search; direct storage in compute, vertex and fragment stages, none), then
compiles vk_draw.cpp's layout code with their descriptors and notes:
138 layouts on MoltenVK and ten other drivers, with the switch unset, 0
and 1, stages not read yet, and the bindings equal pushed or not; plus the
renderer's scan on the same modules and on words that are not a module,
and the count kept for the log. test_vendor_fixes.py covers the new fix
on 17 devices, both ways of the switch, and its absence from the
translation's profile and stamp. The program cache fixtures
(program_cache_one_variant_test.cpp, program_cache_cleanup_test.cpp), which
compile vk_progcache.cpp's functions, include spirv_lengths.h beside
fallback_size_policy.h.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bf5ca961c91479da13e8923387012ef6dc7fb2c7)
fix(macos): stages with more than 8 read_write textures get descriptor sets on MoltenVK
All checks were successful
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
fc5a86dcba
On MoltenVK 1.4.2 a pushed set is bound outside an argument buffer
(MVKDescriptorSet.mm pickArgumentBufferMode: push layouts are Off), and
there Metal's compiler takes at most 8 read_write textures in one function:
TexelDescProbe TD5 (40 storage images and 9 storage texel buffers in one
compute stage) failed pipeline creation pushed with "number of textures
with read_write access exceeds maximum supported (8)", and worked in an
ordinary set (Metal 3 argument buffers). The device reports
maxPerStageDescriptorStorageImages 256 (MoltenVK sets
maxPerStageStorageTextureCount to its argument buffers' texture count), so
nothing in Vulkan's limits stops it.

Which descriptors count: SPIRV-Cross 6c09849f declares a storage image
(sampled 2), storage texel buffers included as texture_buffer since
MoltenVK sets texture_buffer_native, access::read_write unless the
variable is decorated NonWritable or NonReadable (spirv_msl.cpp
image_type_glsl), and it never adds those decorations itself. The eden back
end decorates none (DefineImages, DefineImageBuffers), although Info knows
is_read and is_written. So every storage image and storage texel buffer
counts, by descriptor; sampled images and uniform texel buffers do not.
The limit is per Metal function, that is per stage.

layout_pushes now refuses a push under the existing set_for_tables fix
when any stage's storage images and storage texel buffers, counted by the
bindings' descriptorCount, exceed 8. It is the same cause and remedy as a
storage buffer table (a shape a pushed set cannot take on MoltenVK, where
the pipeline fails; an ordinary set takes it), and RX_VK_SET_FOR_TABLES
covers both, so the vendor fixes' log line is unchanged. ensure_layout
records where each stage's bindings begin, so the mesh fallback's task
and mesh stages, both compute, are counted apart. The programs moved for
it are counted and logged at close (draw_layouts_report). Every other
driver, the SPIR-V, the translator stamp and the program cache are
unchanged.

tests/test_set_for_tables.py gains 55 layouts: 8 and 9 storage images,
5+3 and 5+4 images and texel buffers, 9 texel buffers, an image array of
8 and of 9, graphics stages of 8+8 (pushed) and 0+9, the fallback's task
and mesh stages of 5+5 (pushed) and 4+9, and 41 read-only textures beside
8 storage images (pushed), on MoltenVK; the 9s on the ten other drivers
(pushed); the switch both ways; and the count kept for the close. It
fails on 7b82e199. test_vendor_fixes.py follows the new call and checks
the count reaches the close's log.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 162aca1b3cce50adde9250f14b209a4e5b4c1331)
MoltenVK 1.4.2 binds a pushed set outside any argument buffer, numbering each stage's buffers, textures and samplers in the
Metal function's own tables (per Vulkan stage bit, so the mesh fallback's task and mesh stages share one sequence). Metal's
compiler takes buffer, texture and sampler indexes up to 30, 127 and 15, and MoltenVK keeps the top of each stage's buffer
table for its implicit buffers and, in a vertex stage, the vertex buffers, while its Vulkan limits report its argument
buffers' counts (287 push descriptors, 256 sampled images). Under set_for_tables, layout_pushes now gives a stage a set when
it has more than 7 buffers in a vertex stage, 23 in a fragment stage or 27 in a compute stage (the push constants' included),
more than 128 textures, or more than 16 samplers, and counts each class for a line at close.

test_set_for_tables: limit and limit+1 for each class on MoltenVK and the ten other drivers, the switch both ways, the
counts; its read-only-texture case now stays at 16 samplers. test_vendor_fixes: the counts are kept and logged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 5b44fb3df26cae801ef56c395127ba96950c51dc)
A stage that set_for_tables gives an ordinary set for more than 16 combined image samplers (or one with more than 31 storage
buffers) was within what MoltenVK's Metal 3 argument buffers hold, but over Vulkan's maxPerStageDescriptorSamplers (16) and
maxPerStageDescriptorStorageBuffers (31), which count every set layout made without UPDATE_AFTER_BIND_POOL
(VUID-VkPipelineLayoutCreateInfo-descriptorType-03016 and the like). Under set_for_tables an ordinary set is now an
update-after-bind layout and the recording's pools are update-after-bind pools, so the limits that apply are the
update-after-bind ones, which MoltenVK sets to its argument buffers' (500000 samplers a stage on an M4 Max). No device
feature is needed for either flag, and an update-after-bind pool holds ordinary layouts too. MoltenVK 1.4.2 reads neither
flag, so what is bound is the same; pushed sets are unchanged.

test_set_for_tables checks each layout's flags whole; test_vendor_fixes checks make_pool.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit df23d59af0153f6d7a531363de6fc0887df23f6b)
test(vk): recording_admission's device stub has the fixes make_pool now reads
All checks were successful
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 4s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 3s
4c2ce8b7a7
df23d59a made make_pool read dev.fixes.set_for_tables (MoltenVK's update-after-bind pools). test_recording_admission compiles
make_pool from vk_draw.cpp against its own device stub, which had no fixes, so its fixture no longer built (the full suite at
df23d59a: "no member named 'fixes'"). The stub now has set_for_tables, off, as on every driver but MoltenVK.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 427ee1866b30648d04f32e3e6ab66852f397047a)
mooki force-pushed mac/mvk-pushlimits from 4c2ce8b7a7
All checks were successful
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 4s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 3s
to 8a5c10223f
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 18s
CI / Conventional Commit title (pull_request) Successful in 15s
CI / Workflow lint (actionlint) (pull_request) Successful in 21s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 22s
Third-party parts / As their notes say (pull_request) Successful in 32s
2026-11-15 07:16:26 +00:00
Compare
mooki force-pushed mac/mvk-pushlimits from 8a5c10223f
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 18s
CI / Conventional Commit title (pull_request) Successful in 15s
CI / Workflow lint (actionlint) (pull_request) Successful in 21s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 22s
Third-party parts / As their notes say (pull_request) Successful in 32s
to 374c55a031
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 13:09:56 +00:00
Compare
mooki changed title from WIP: fix(vk): use update-after-bind sets past Metal's per-stage limits on MoltenVK to fix(vk): use update-after-bind sets past Metal's per-stage limits on MoltenVK 2026-11-15 14:18:05 +00:00
Author
Member

Folded into #847 (fix(vk): MoltenVK's descriptor limits in descriptor sets)

Folded into #847 (fix(vk): MoltenVK's descriptor limits in descriptor sets)
mooki closed this pull request 2026-11-15 18:07:50 +00:00
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!646
No description provided.