fix(vk): use descriptor sets for stages with more than 8 read_write textures on MoltenVK #645

Closed
mooki wants to merge 3 commits from mooki/Longshot:mac/mvk-rwtex 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, Metal lets one shader function use at most 8 textures it can both read and write. MoltenVK binds a pushed
descriptor set's images directly to the function, and every storage image and storage texel buffer the renderer's shaders
declare is read-and-write there (the back end marks none of them read-only or write-only). So a shader stage with more than
8 of them fails to compile when its set is pushed, and the draw or dispatch that needs it is left out. In an ordinary set
(a Metal 3 argument buffer) the same stage compiles. This PR gives such stages an ordinary set on MoltenVK, under #643's
switch. Other drivers are unchanged. It is part of the macOS port and stacks on #644. See #420.

How to reproduce

Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2, arm64 and x86_64 under Rosetta: TexelDescProbe's TD5 arm, a
compute stage with 40 storage images and 9 storage texel buffers (reports/2026-11-14-texel-desc.md; main does not build
on macOS yet)
Settings and switches: defaults
Steps: game-free: in #644'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>. On the GPU: the probe's TD5 arm, pushed and in a set.
Expected: a stage with more than 8 storage images and storage texel buffers gets a set on MoltenVK, and its pipeline
is made.
Actual: with a push layout Metal refuses the function: [mvk-error] VK_ERROR_INITIALIZATION_FAILED: Shader library compile failed (Error code 3): … error: number of textures with read_write access exceeds maximum supported (8), then
Compute shader function could not be compiled into pipeline, on both CPU routes; in a set the same stage works. The test
pushes 7 of its 134 layouts that need a set (want=set got=push).

What changed

Under #643's set_for_tables fix (MoltenVK), a program is no longer pushed when any one of its stages has more than 8
storage images and storage texel buffers, counting every element of an array. Stages are counted one by one, as Metal
counts per function: the task and mesh stages of the mesh fallback share a stage bit but are two functions. A line at
close says how many programs it moved. The SPIR-V, the translator's stamp, the program cache and the vendor fixes' log line
are unchanged.

  • vk: layout_pushes(p, bindings, stage_first, total) in gpu/vk/vk_draw.cpp: kPushReadWriteTextures = 8, with its
    source; with dev.fixes.set_for_tables, after the table check, it sums each stage's STORAGE_IMAGE and
    STORAGE_TEXEL_BUFFER descriptorCount and refuses the push past 8, counted in st_sets_for_rw_textures.
    ensure_layout records where each stage's bindings begin (stage_first) and passes it. draw_layouts_report() adds
    vk: read_write textures (RX_VK_SET_FOR_TABLES): N programs given a descriptor set, not pushed, … when the fix is on.
  • vk: gpu/vk/vk_vendor.h (set_for_tables' comment covers both shapes, with the reason), gpu/vk/vk_draw.h (the
    report's comment).
  • docs: the RX_VK_SET_FOR_TABLES row in docs/development.md covers the read_write case and the close line;
    gpu/vk/VULKAN.md's Descriptors bullet.
  • tests: tests/test_set_for_tables.py grows from 79 to 134 layouts: on MoltenVK 8 storage images, 5 images and 3 texel
    buffers, an image array of 8, vertex 8 plus fragment 8, and the mesh fallback's task 5 plus mesh 5 stay pushed; 9
    images, 5 plus 4, 9 texel buffers, an array of 9, a fragment stage of 9 and task 4 plus mesh 9 get a set; read-only
    textures (20 uniform texel buffers, sampled images and arrays) do not count; the switch works both ways; other drivers
    push as before; and the fixture's counter must equal 7. tests/test_vendor_fixes.py checks that ensure_layout
    decides through the new call.

Measurements

Verdict: passes: the new test fails on #644's head (7 of 134 layouts pushed where a set is needed, and no counter) and
passes at this head, two interleaved runs each.

Baseline: #644 @ e90ba9a6, this PR's base in the stack. Main lacks #643's layout_pushes, which this PR extends, and #644
changed its signature, 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 (a layout fix: its proof is the test; the game's count of such programs is in the history below)
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
#644's with the two test files copied in

Before: #644 @ e90ba9a6 After: this PR @ fc5a86dc
tests/test_set_for_tables.py, arm64, 2 runs each fails both runs: 7 of 134 layouts want=set got=push; no read_write count passes both runs: 134 of 134, the counter 7
tests/test_vendor_fixes.py, arm64, 2 runs each fails both runs: ensure_layout does not decide the push through layout_pushes (the new call) passes both runs: 17 devices
#644 @ e90ba9a6 (this PR's test):
  CASE moltenvk.rw.images9 driver=14 want=set got=push flag=1 FAIL
  CASE moltenvk.rw.images5_texels4 driver=14 want=set got=push flag=1 FAIL
  CASE moltenvk.rw.texels9 driver=14 want=set got=push flag=1 FAIL
  CASE moltenvk.rw.array9 driver=14 want=set got=push flag=1 FAIL
  CASE moltenvk.rw.graphics0_9 driver=14 want=set got=push flag=1 FAIL
  CASE moltenvk.rw.fallback4_9 driver=14 want=set got=push flag=1 FAIL
  CASE nvidia.switch1.rw.images9 driver=4 RX_VK_SET_FOR_TABLES=1 want=set got=push flag=1 FAIL
  vk_draw.cpp: the layout code keeps no count of the programs given a set for their read_write textures
  SUMMARY cases=134 failures=7
this PR @ fc5a86dc:
ok: 134 layouts; on MoltenVK a set with a storage buffer table, or a stage with more than 8 storage images and storage texel buffers, 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 read_write moves are counted

History, not this head: TexelDescProbe (reports/2026-11-14-texel-desc.md, TD5, arm64 and x86_64 under Rosetta): pushed,
the 49-texture stage failed to compile with Metal's "read_write access exceeds maximum supported (8)"; in a set it was
exact (1000 writer/reader pairs). Start 12 (reports/2026-11-14-mac-startup-12.md, integration build a7711f3c carrying
this commit as 162aca1b) counted 0 programs moved for read_write textures in its precompile: the game's programs seen
so far stay under 8, and this PR guards the shape the translator can produce.

Flags and settings

No new switch: this widens #643's RX_VK_SET_FOR_TABLES, whose failure mode is the same (a pipeline MoltenVK cannot
make when pushed, which a set takes). (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): which programs get an ordinary descriptor set instead of a pushed one unset: on with MoltenVK; moves programs with a storage buffer table unset: on with MoltenVK; also moves programs with a stage of more than 8 storage images and storage texel buffers; 0 turns both off, 1 turns both on with any driver on: these programs' pipelines are made on MoltenVK; a line at close counts the read_write moves helps: MoltenVK; doesn't help: other drivers, which allow more; untested: a game stage over 8 (none counted in start 12) leave unset; =0 only to see the failures on a Mac

Testing

Tested: on macOS 26.7.1, Apple M4 Max, at fc5a86dc, this PR's head before the rebases onto e090cc48 and aa95d35f
(main 6c9edffe plus #643, #644 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 #644'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: test_vendor_fixes
    passes (17 devices). test_set_for_tables' x86_64 fixture was killed (SIGKILL, exit -9, no crash report: the endpoint
    agent [inference]); by policy it was not retried. The same test at the top of this stack (#646's version, which keeps all
    134 of these cases, with rw.sampled held at 16 samplers) passed on x86_64.
  • 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-P28-fc5a86dc/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 mutation the topic ran (the whole program counted as one function) is caught by the test; the code it mutates is
    byte for byte the topic's after the rebase (kPushSize to the end of ensure_layout), so it was 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/P28-*.log, related-P28-*, objects2-P28-*, merge-tree.txt.
  • After the rebase onto main aa95d35f, at b8b1ba99 (fc5a86dc 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 65747): vk: read_write textures (RX_VK_SET_FOR_TABLES): 0 programs given a descriptor set, not pushed, because a stage has more storage images and storage texel buffers than the 8 read_write textures …,
    and 0 lines with read_write access exceeds: no program in this scene needed the path, so the game did not exercise
    it; the game-free test above covers it. Under Rosetta, #34's 19 of 19 checks stand for it; that log's lines for this
    PR are not quoted here.

Not tested: 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). A pushed vertex or fragment stage with 9 read_write textures was not run on the GPU:
only a compute kernel was observed failing (Apple documents read_write textures for all three function kinds).
KosmicKrisp. Windows and Linux: not built or run there (its test request covers the build and a start); their layouts do
not change (the test's ten other drivers).

Must be tested:

  • Windows and Linux, any GPU: build this PR (head b8b1ba99, it carries #643 and #644) and main aa95d35f the same way, start
    each to the title and quit, expecting the build to succeed, no vk: read_write textures line at close, the vk: vendor fixes line as #643 and #644 leave it (not pushed 0), and every other line to match main's (by hand: test request
    T-N2VG)

Risks

What could break: on MoltenVK a program with a stage over 8 now allocates, writes and binds a set per draw or dispatch
instead of a push (none were counted in start 12). The count is per stage, from stage_first; counting by stage bit
instead would have moved mesh fallback programs that push fine (the test's mutation shows it). Other drivers: unchanged
(set_for_tables is off there).

Turning it off: RX_VK_SET_FOR_TABLES=0 turns off this and #643's tables together, or revert the commit.

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 a moved program's pipeline is made now, so Metal compiles it once.

Reviewer notes

  • Review fc5a86dc (this PR's one commit, cherry-picked with -x from macos/mvk-rwtex 162aca1b): layout_pushes,
    ensure_layout's stage_first and draw_layouts_report in vk_draw.cpp.
  • Depends on #644 (and through it #643): this branch is stacked on #644's head, e90ba9a6, and shows both their commits
    until they merge. #646 stacks on this one.
  • Conflicts with main: none. The plan's docs/development.md conflict (#191 7ec25fd1, the row next to it) was taken
    care of in #643's and #644's cherry-picks, so this commit applied without one. No semantic rework: the layout code is
    byte for byte the topic's.
  • With the 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). #663 (seg-bind, a later wave) is to rebase after this PR: its edits to
    tests/test_set_for_tables.py (--source, a segmented case) conflict with this PR's in three blocks (docstring, the
    CASES sum, main()); keep both sides, as the integration did. Once #663 sets segmented pieces in the bindings'
    counts, this rule counts each piece with no further change.
  • Not done, a lighter fix for later: declaring read-only images NonWritable and write-only ones NonReadable in the
    back end would leave only images both read and written in the count, but changes the SPIR-V and the stamp 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-N2VG.)
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head b8b1ba99, on #644 f08eb702, without a
    conflict; this PR's own commits, old → new: fc5a86dc → b8b1ba99; 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 b8b1ba99): 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 #644'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, Metal lets one shader function use at most 8 textures it can both read and write. MoltenVK binds a pushed descriptor set's images directly to the function, and every storage image and storage texel buffer the renderer's shaders declare is read-and-write there (the back end marks none of them read-only or write-only). So a shader stage with more than 8 of them fails to compile when its set is pushed, and the draw or dispatch that needs it is left out. In an ordinary set (a Metal 3 argument buffer) the same stage compiles. This PR gives such stages an ordinary set on MoltenVK, under #643's switch. Other drivers are unchanged. It is part of the macOS port and stacks on #644. See #420. ## How to reproduce **Seen on:** macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2, arm64 and x86_64 under Rosetta: TexelDescProbe's TD5 arm, a compute stage with 40 storage images and 9 storage texel buffers (`reports/2026-11-14-texel-desc.md`; main does not build on macOS yet) **Settings and switches:** defaults **Steps:** game-free: in #644'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>`. On the GPU: the probe's TD5 arm, pushed and in a set. **Expected:** a stage with more than 8 storage images and storage texel buffers gets a set on MoltenVK, and its pipeline is made. **Actual:** with a push layout Metal refuses the function: `[mvk-error] VK_ERROR_INITIALIZATION_FAILED: Shader library compile failed (Error code 3): … error: number of textures with read_write access exceeds maximum supported (8)`, then `Compute shader function could not be compiled into pipeline`, on both CPU routes; in a set the same stage works. The test pushes 7 of its 134 layouts that need a set (`want=set got=push`). ## What changed Under #643's `set_for_tables` fix (MoltenVK), a program is no longer pushed when any one of its stages has more than 8 storage images and storage texel buffers, counting every element of an array. Stages are counted one by one, as Metal counts per function: the task and mesh stages of the mesh fallback share a stage bit but are two functions. A line at close says how many programs it moved. The SPIR-V, the translator's stamp, the program cache and the vendor fixes' log line are unchanged. - vk: `layout_pushes(p, bindings, stage_first, total)` in `gpu/vk/vk_draw.cpp`: `kPushReadWriteTextures = 8`, with its source; with `dev.fixes.set_for_tables`, after the table check, it sums each stage's `STORAGE_IMAGE` and `STORAGE_TEXEL_BUFFER` `descriptorCount` and refuses the push past 8, counted in `st_sets_for_rw_textures`. `ensure_layout` records where each stage's bindings begin (`stage_first`) and passes it. `draw_layouts_report()` adds `vk: read_write textures (RX_VK_SET_FOR_TABLES): N programs given a descriptor set, not pushed, …` when the fix is on. - vk: `gpu/vk/vk_vendor.h` (`set_for_tables`' comment covers both shapes, with the reason), `gpu/vk/vk_draw.h` (the report's comment). - docs: the `RX_VK_SET_FOR_TABLES` row in `docs/development.md` covers the read_write case and the close line; `gpu/vk/VULKAN.md`'s Descriptors bullet. - tests: `tests/test_set_for_tables.py` grows from 79 to 134 layouts: on MoltenVK 8 storage images, 5 images and 3 texel buffers, an image array of 8, vertex 8 plus fragment 8, and the mesh fallback's task 5 plus mesh 5 stay pushed; 9 images, 5 plus 4, 9 texel buffers, an array of 9, a fragment stage of 9 and task 4 plus mesh 9 get a set; read-only textures (20 uniform texel buffers, sampled images and arrays) do not count; the switch works both ways; other drivers push as before; and the fixture's counter must equal 7. `tests/test_vendor_fixes.py` checks that `ensure_layout` decides through the new call. ## Measurements **Verdict:** passes: the new test fails on #644's head (7 of 134 layouts pushed where a set is needed, and no counter) and passes at this head, two interleaved runs each. Baseline: #644 @ e90ba9a6, this PR's base in the stack. Main lacks #643's `layout_pushes`, which this PR extends, and #644 changed its signature, 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 (a layout fix: its proof is the test; the game's count of such programs is in the history below) 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 #644's with the two test files copied in | | Before: #644 @ e90ba9a6 | After: this PR @ fc5a86dc | |---|---|---| | `tests/test_set_for_tables.py`, arm64, 2 runs each | fails both runs: 7 of 134 layouts `want=set got=push`; no read_write count | passes both runs: 134 of 134, the counter 7 | | `tests/test_vendor_fixes.py`, arm64, 2 runs each | fails both runs: `ensure_layout does not decide the push through layout_pushes` (the new call) | passes both runs: 17 devices | ``` #644 @ e90ba9a6 (this PR's test): CASE moltenvk.rw.images9 driver=14 want=set got=push flag=1 FAIL CASE moltenvk.rw.images5_texels4 driver=14 want=set got=push flag=1 FAIL CASE moltenvk.rw.texels9 driver=14 want=set got=push flag=1 FAIL CASE moltenvk.rw.array9 driver=14 want=set got=push flag=1 FAIL CASE moltenvk.rw.graphics0_9 driver=14 want=set got=push flag=1 FAIL CASE moltenvk.rw.fallback4_9 driver=14 want=set got=push flag=1 FAIL CASE nvidia.switch1.rw.images9 driver=4 RX_VK_SET_FOR_TABLES=1 want=set got=push flag=1 FAIL vk_draw.cpp: the layout code keeps no count of the programs given a set for their read_write textures SUMMARY cases=134 failures=7 this PR @ fc5a86dc: ok: 134 layouts; on MoltenVK a set with a storage buffer table, or a stage with more than 8 storage images and storage texel buffers, 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 read_write moves are counted ``` History, not this head: TexelDescProbe (`reports/2026-11-14-texel-desc.md`, TD5, arm64 and x86_64 under Rosetta): pushed, the 49-texture stage failed to compile with Metal's "read_write access exceeds maximum supported (8)"; in a set it was exact (1000 writer/reader pairs). Start 12 (`reports/2026-11-14-mac-startup-12.md`, integration build a7711f3c carrying this commit as `162aca1b`) counted **0** programs moved for read_write textures in its precompile: the game's programs seen so far stay under 8, and this PR guards the shape the translator can produce. ### Flags and settings No new switch: this widens #643's `RX_VK_SET_FOR_TABLES`, whose failure mode is the same (a pipeline MoltenVK cannot make when pushed, which a set takes). (`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): which programs get an ordinary descriptor set instead of a pushed one | unset: on with MoltenVK; moves programs with a storage buffer table | unset: on with MoltenVK; also moves programs with a stage of more than 8 storage images and storage texel buffers; `0` turns both off, `1` turns both on with any driver | on: these programs' pipelines are made on MoltenVK; a line at close counts the read_write moves | helps: MoltenVK; doesn't help: other drivers, which allow more; untested: a game stage over 8 (none counted in start 12) | leave unset; `=0` only to see the failures on a Mac | ## Testing **Tested:** on macOS 26.7.1, Apple M4 Max, at `fc5a86dc`, this PR's head before the rebases onto e090cc48 and aa95d35f (main 6c9edffe plus #643, #644 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 #644'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: `test_vendor_fixes` passes (17 devices). `test_set_for_tables`' x86_64 fixture was killed (SIGKILL, exit -9, no crash report: the endpoint agent [inference]); by policy it was not retried. The same test at the top of this stack (#646's version, which keeps all 134 of these cases, with `rw.sampled` held at 16 samplers) passed on x86_64. - `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-P28-fc5a86dc/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 mutation the topic ran (the whole program counted as one function) is caught by the test; the code it mutates is byte for byte the topic's after the rebase (`kPushSize` to the end of `ensure_layout`), so it was 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/P28-*.log`, `related-P28-*`, `objects2-P28-*`, `merge-tree.txt`. - **After the rebase onto main aa95d35f**, at `b8b1ba99` (`fc5a86dc` 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 65747): `vk: read_write textures (RX_VK_SET_FOR_TABLES): 0 programs given a descriptor set, not pushed, because a stage has more storage images and storage texel buffers than the 8 read_write textures …`, and 0 lines with `read_write access exceeds`: no program in this scene needed the path, so the game did not exercise it; the game-free test above covers it. Under Rosetta, #34's 19 of 19 checks stand for it; that log's lines for this PR are not quoted here. **Not tested:** 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). A pushed vertex or fragment stage with 9 read_write textures was not run on the GPU: only a compute kernel was observed failing (Apple documents read_write textures for all three function kinds). KosmicKrisp. Windows and Linux: not built or run there (its test request covers the build and a start); their layouts do not change (the test's ten other drivers). **Must be tested:** - Windows and Linux, any GPU: build this PR (head b8b1ba99, it carries #643 and #644) and main aa95d35f the same way, start each to the title and quit, expecting the build to succeed, no `vk: read_write textures` line at close, the `vk: vendor fixes` line as #643 and #644 leave it (`not pushed 0`), and every other line to match main's (by hand: test request T-N2VG) ## Risks **What could break:** on MoltenVK a program with a stage over 8 now allocates, writes and binds a set per draw or dispatch instead of a push (none were counted in start 12). The count is per stage, from `stage_first`; counting by stage bit instead would have moved mesh fallback programs that push fine (the test's mutation shows it). Other drivers: unchanged (`set_for_tables` is off there). **Turning it off:** `RX_VK_SET_FOR_TABLES=0` turns off this and #643's tables together, or revert the commit. **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 a moved program's pipeline is made now, so Metal compiles it once. ## Reviewer notes - Review `fc5a86dc` (this PR's one commit, cherry-picked with `-x` from `macos/mvk-rwtex` `162aca1b`): `layout_pushes`, `ensure_layout`'s `stage_first` and `draw_layouts_report` in `vk_draw.cpp`. - Depends on #644 (and through it #643): this branch is stacked on #644's head, e90ba9a6, and shows both their commits until they merge. #646 stacks on this one. - Conflicts with main: none. The plan's `docs/development.md` conflict (#191 `7ec25fd1`, the row next to it) was taken care of in #643's and #644's cherry-picks, so this commit applied without one. No semantic rework: the layout code is byte for byte the topic's. - With the 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). #663 (seg-bind, a later wave) is to rebase after this PR: its edits to `tests/test_set_for_tables.py` (`--source`, a segmented case) conflict with this PR's in three blocks (docstring, the `CASES` sum, `main()`); keep both sides, as the integration did. Once #663 sets segmented pieces in the bindings' counts, this rule counts each piece with no further change. - Not done, a lighter fix for later: declaring read-only images `NonWritable` and write-only ones `NonReadable` in the back end would leave only images both read and written in the count, but changes the SPIR-V and the stamp 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-N2VG.) - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `b8b1ba99`, on #644 `f08eb702`, without a conflict; this PR's own commits, old → new: `fc5a86dc` → `b8b1ba99`; 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 `b8b1ba99`): 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 #644'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)
mooki force-pushed mac/mvk-rwtex from fc5a86dcba
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
to bff8bdc326
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 18s
2026-11-15 07:16:23 +00:00
Compare
mooki force-pushed mac/mvk-rwtex from bff8bdc326
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 18s
to b8b1ba998a
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 5s
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:53 +00:00
Compare
mooki changed title from WIP: fix(vk): use descriptor sets for stages with more than 8 read_write textures on MoltenVK to fix(vk): use descriptor sets for stages with more than 8 read_write textures on MoltenVK 2026-11-15 14:18:03 +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:48 +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 5s
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!645
No description provided.