fix(vk): image, image-buffer and texture-buffer arrays are arrays in the SPIR-V #450

Open
mooki wants to merge 2 commits from mooki/Longshot:fix/descriptor-image-arrays into main
Member

Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks, no game run) · Needs: Windows or Linux, any GPU (AMD preferred)

Why

A shader that picks a texture buffer, an image buffer or a storage image by an index (a handle read at a computed offset
of a constant buffer) is never drawn right on any GPU. With a texture buffer it is not translated at all (Array of texture buffers is not implemented). With an image buffer or a storage image the translation makes invalid SPIR-V, so
what the driver does with it is undefined, and an atomic on one is not translated (Image indexing is not implemented).

How to reproduce

Seen on: macOS 26.7.1, Apple M4 Max, in the game-free test (nx2/test_image_arrays.py --tree main). The SPIR-V back
end is the same code on every OS, so every OS translates these programs the same way. Not seen in the game: whether it
has such an array is not known (its programs were not read).
Settings and switches: defaults (the renderer's profile indexes these three kinds of array as dynamically uniform).
Steps: python -I -B nx2/test_image_arrays.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --cxx clang++ --tree <a tree of main>: 28 synthetic compute programs with such arrays go through the real texture pass and back end.
Expected: every module translates, passes spirv-val --target-env vulkan1.3 and declares each binding with the count
Shader::Info gives it.
Actual: 18 failures over 14 of the 28 cases: the 10 with a texture buffer array or an atomic on an array are refused
(Array of texture buffers is not implemented, Image indexing is not implemented), and the 4 image buffer and storage
image array modules fail spirv-val (OpAccessChain reached non-composite type), each array declared as one image.

What changed

The SPIR-V back end now declares a texture buffer, image buffer or storage image descriptor with a count above 1 as an
array of that many images, as it already did for textures, and reaches an element through a pointer to one image. An
atomic on such an array takes its element instead of refusing. A count of 1 translates exactly as before.

  • Read first: DefineTextureBuffers, DefineImageBuffers and DefineImages against DefineTextures, then
    TextureImage's buffer branch and Image() in emit_spirv_image_atomic.cpp.
  • vk (the SPIR-V back end): gpu/vk/eden/shader_recompiler/backend/spirv/spirv_emit_context.cpp.
    • The texture pass already makes such a descriptor an array (Shader::Info's count), and the renderer already
      declares and writes count descriptors for it (ensure_layout and build_descriptors in gpu/vk/vk_draw.cpp). Only
      the back end did not.
    • DefineTextureBuffers, DefineImageBuffers and DefineImages now declare a descriptor of count above 1 as an
      OpTypeArray of count images, through DescType, as DefineTextures already did for textures.
      DefineTextureBuffers no longer throws Array of texture buffers.
    • A count of 1 declares exactly what it did before.
  • vk: TextureImage (emit_spirv_image.cpp) reaches a texture buffer array's element through a pointer to one image
    (TextureBufferDefinition::pointer_type, new in spirv_emit_context.h), not through the image type itself. Image(),
    for image buffer and storage image loads and stores, was already right once the variable is an array.
  • vk: the image atomics (emit_spirv_image_atomic.cpp) take an array's element the same way, and decorate it and the
    texel pointer NonUniform where the profile indexes that kind of array so. They used to throw Image indexing for any
    index but 0, and the index of an array is its element. A single image takes no index, as before.
  • tests: nx2/test_image_arrays.py (new) with its fixture tests/image_arrays_ir.cpp (new).
    • Synthetic compute programs, no game data: a 2-element image buffer array and a 3-element storage image array read
      and written, the same two added to atomically, a 2-element texture buffer array fetched, and one program with all
      four kinds (a 2-element texture array too).
    • Each also with single descriptors (the handle at a constant offset).
    • Each translated with no kind of array indexed nonuniformly and with every kind indexed nonuniformly (the renderer's
      profile does it for textures only); the texture buffer programs also with the out-of-bounds fetch guard every GPU but
      NVIDIA's gets (fetch_bounds, vk_vendor.h). 28 modules.
    • They go through the real texture pass, dead code elimination, CollectShaderInfoPass and the back end.
    • Every module must translate and pass spirv-val --target-env vulkan1.3, and declare each binding with the count
      Shader::Info gives it. With nonuniform indexing, each array kind must have its capability and its NonUniform
      decorations, an atomic's texel pointer included.
    • --tree runs the same programs through another checkout's back end; --base-tree also requires the 14
      single-descriptor modules to be byte for byte that checkout's.
  • Design: the texture pass, the renderer's layouts and its descriptor writes are unchanged: they already handled count.
  • Left out of the checklist: the box for checks under tests/ that read changed source. The checks that cut code out of
    these files (test_vendor_translation, test_native_float_atomic_add, test_cas_loop_ceiling,
    test_global_atomic_add_f32, test_global_table_loop, test_mesh_point_topology, test_mesh_unreachable_outputs,
    nx2/test_mesh_first_types) needed no change and pass. Nor is the Windows and Linux build box there: this was built
    on macOS only (Testing).

Measurements

Verdict: fixed, game-free: the new test fails on main (18 failures over 14 of its 28 cases) and passes here, with the
14 single-descriptor modules byte for byte main's; the suite at the stack top #502 adds no failure. A translation fix:
nothing to time.

Commits: main 6c9edffe5d (2026-11-14); this PR fix/descriptor-image-arrays @
a66f16b0fb.
Hardware: Apple M4 Max (16 CPUs), macOS 26.7.1, Apple clang 21, spirv-val of SPIRV-Tools v2026.4 (Homebrew spirv-tools 1.4.363.0). Game-free: no GPU, no game
Scene: none (a translation fix; its proof is the test, as CONTRIBUTING's fix row allows)
Method: nx2/test_image_arrays.py with this PR's back end and with main's (--tree, a git archive of main); every module
the repository's real-IR fixtures emit, built against both back ends and compared byte for byte; the full game-free suite
on both
How to reproduce: python -I -B nx2/test_image_arrays.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --cxx clang++ --base-tree <a tree of main> (add --tree <a tree of main> for main's side)

Before: main @ 6c9edffe After: this PR @ a66f16b0
nx2/test_image_arrays.py: modules that translate 18 of 28: the 10 with a texture buffer array or an atomic on an array are refused 28 of 28
modules that pass spirv-val 14 of the 18: the 4 image buffer and storage image array modules fail (OpAccessChain reached non-composite type) 28 of 28
bindings declared with Info's count not the 4 arrays (declared as one image) all
single-descriptor modules 14 the same 14, byte for byte
every module the other real-IR fixtures emit (depth_sample_types, unsigned_image_types, texture_phi_dominance, global_load_16 and its compare-exchange loop, native_suatom, f2i_immediate) 100 modules the same 100, byte for byte (compared before the rebase; neither side's back end changed in it)
full game-free suite on macOS (tests/run_all.py --jobs 6) 531 passed, 32 failed, 17 skipped not run on this head alone: #502 @ f8f7695c, which carries it, 533 passed, 32 failed, 17 skipped
main @ 6c9edffe (nx2/test_image_arrays.py --tree <main>):
FAIL: imgbuf-2: spirv-val: error: line 73: OpAccessChain reached non-composite type while indexes still remain to be traversed.
FAIL: imgbuf-2: the module declares {'texbuf': [], 'imgbuf': [1], 'tex': [], 'img': []} where Shader::Info has {'texbuf': [], 'imgbuf': [2], 'tex': [], 'img': []}
FAIL: imgbuf-2-nonuniform: spirv-val: error: line 81: OpAccessChain reached non-composite type while indexes still remain to be traversed.
FAIL: imgbuf-2-nonuniform: the module declares {'texbuf': [], 'imgbuf': [1], 'tex': [], 'img': []} where Shader::Info has {'texbuf': [], 'imgbuf': [2], 'tex': [], 'img': []}
FAIL: img-3: spirv-val: error: line 74: OpAccessChain reached non-composite type while indexes still remain to be traversed.
FAIL: img-3: the module declares {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [1]} where Shader::Info has {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [3]}
FAIL: img-3-nonuniform: spirv-val: error: line 82: OpAccessChain reached non-composite type while indexes still remain to be traversed.
FAIL: img-3-nonuniform: the module declares {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [1]} where Shader::Info has {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [3]}
FAIL: texbuf-2: translation refused: Array of texture buffers is not implemented
FAIL: texbuf-2-nonuniform: translation refused: Array of texture buffers is not implemented
FAIL: texbuf-2-bounds: translation refused: Array of texture buffers is not implemented
FAIL: imgbuf-atomic-2: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented
FAIL: imgbuf-atomic-2-nonuniform: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented
FAIL: img-atomic-3: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented
FAIL: img-atomic-3-nonuniform: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented
FAIL: mixed: translation refused: Array of texture buffers is not implemented
FAIL: mixed-nonuniform: translation refused: Array of texture buffers is not implemented
FAIL: mixed-bounds: translation refused: Array of texture buffers is not implemented

this PR @ a66f16b0 (nx2/test_image_arrays.py --base-tree <main>):
imgbuf-2: imgbuf [2]
img-3: img [3]
texbuf-2: texbuf [2] imgbuf [1]
imgbuf-atomic-2: imgbuf [1, 2]
img-atomic-3: imgbuf [1] img [3]
mixed: texbuf [2] imgbuf [1, 2] tex [2] img [3]
PASS: image buffer, storage image and texture buffer arrays declared as arrays; 28 modules validate; 14 single-descriptor modules byte for byte the base tree's

Screenshots: none; nothing on screen (no game run).

Flags and settings

None.

Testing

Tested:

  • On an Apple M4 Max with macOS 26.7.1, game-free only.
  • nx2/test_image_arrays.py fails on main (18 failures over 14 of the 28 cases) and passes here, with all 14 single-descriptor
    modules byte for byte main's.
  • Every module the repository's other real-IR fixtures emit (100: depth_sample_types, unsigned_image_types,
    texture_phi_dominance, global_load_16 and its compare-exchange loop, native_suatom and f2i_immediate), built
    against main's back end and against this PR's with the same fixture sources: all 100 byte for byte the same, and all
    128 of this PR's modules (with the 28 above) pass spirv-val --target-env vulkan1.3 --uniform-buffer-standard-layout,
    as CI's SPIR-V fixture job validates them. This ran before the rebase, on 5e072fc8; main has not changed
    gpu/vk/eden, sirit or compat since, and this PR's back-end files are the same after the rebase.
  • The full suite on both sides, compared by test name.
    • By test name, against main 6c9edffe's run on the same Mac, at the stack top #502 @ f8f7695c: 2 statuses changed:
      test_image_arrays, absent on main, passes here; test_nonuniform_image_arrays, absent on main, passes here.
    • These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull
      requests, some are checks main added or changed since 5e072fc8): test_appimage_package_retry,
      test_appimage_player_build, test_collect_wide, test_control, test_cpm_discovery, test_crash_dialog,
      test_crash_record, test_env_on, test_game_layout, test_lift_cleanup_error, test_linux_preflight,
      test_longshot_ctl, test_low_memory_notice, test_make_game_cache, test_mem_gate, test_mem_gate_children,
      test_mirror_trim, test_oot_state_tap, test_pcache_header_read, test_pipeline_cache_file,
      test_pipeline_cache_save, test_pipeline_play_cache, test_pipeline_tour, test_prof_symbols,
      test_program_cache_cleanup, test_program_cache_one_variant, test_readback_late, test_report_run,
      test_split_giants, test_vk_pgo, test_vram_budget_row, test_walk_key.
    • The same 17 checks skipped as on main.
    • Crash reports during the run: recorder_sync_test.exe-2026-11-14-191118.ips (main's run:
      fault-arm64-2026-11-14-180704.ips, recorder_sync_test.exe-2026-11-14-180744.ips).
  • Object compiles of the 21 back-end files that include spirv_emit_context.h (the three changed .cpp files among them)
    with the renderer's flags (nx2/build.py), Apple clang 21, arm64 and x86_64: all 42 exit 0 on main and all 42 here,
    without the compile-only prelude main's vk_shader.cpp and pipeline_cache_file.h need on macOS.
    • Before the rebase: with that prelude this PR's 42 objects were byte for byte the same as without it, and 17 of the
      21 objects were byte for byte main's on both architectures. The other four are the three changed files and
      emit_spirv_context_get_set.cpp, which only includes the changed header.
  • After the rebase onto main aa95d35f, at 38ba67dd (a66f16b0 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 S1's merge 11a5b4e7, which carries
    this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
    (reports/2026-11-15-rebase-sweep-3.md).

Not tested:

  • A GPU run. Whether the game has any such array is not known: its programs were not read. A program with one now
    translates to valid SPIR-V where it was refused or invalid before; nothing else changes.
  • Windows and Linux: not run. run_all.py skips the new test on Windows, as it skips the other spirv-val checks; on
    Linux it runs like them. The change is in the translation alone, which the test checks on any OS with spirv-val.
  • Not built on Windows or Linux. The change uses only Sirit calls, std::tuple and the definitions around it.

Must be tested:

  • Windows or Linux, any GPU (AMD preferred), any driver: build this PR (head 38ba67dd) and main aa95d35f the same way;
    on Linux run python -I -B tests/run_all.py image_arrays (it needs spirv-val); start each build to the title and
    play a minute, expecting the test to pass, this PR's first start to translate every program again (the translator
    changed), its vk: prepared N shader programs (... M failed ...) line to count no more failed programs than main's,
    no not translated line naming Array of texture buffers or Image indexing where main's log may have them, and no
    new error in longshot_log.txt (by hand: test request T-XKYH)

Risks

What could break: low. Only programs with a texture buffer, image buffer or storage image array change, and on main
those are refused or invalid SPIR-V. Every other module is byte for byte the same.

  • The renderer's profile (make_profile in gpu/vk/vk_shader.cpp) indexes only sampled image arrays nonuniformly. These
    three kinds are therefore indexed as dynamically uniform, as the profile already decides for them; the test covers
    both profiles. The device is created with every feature it reports (vk_main.cpp), dynamic indexing of these arrays
    included.
  • Landing order: #502 (fix/nonuniform-image-arrays) is stacked on this PR and lands after it; landing both in one
    release makes the cache rebuild below happen once. After the rebase on 6c9edffe it conflicts with none of the
    author's other open PRs as rebased there (git merge-tree of the heads, 2026-11-14).

Turning it off: revert the commits.

Compatibility: the back end is in gpu/vk/eden, one of the translator stamp's folders (nx2/translator_files.py
since main's #370), so the first start after updating translates every program again, as any change to gpu/vk/eden
does. Programs without these arrays translate to the same SPIR-V as before. The program cache's format and
Shader::Info are unchanged; saves, the options file and the vk ABI are unchanged.

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.
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head 38ba67dd, without a conflict; this PR's own
    commits, old → new: 092b9b39 → 26d59e76, a66f16b0 → 38ba67dd; 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). At the stack top #502, on
    macOS: 533 passed, 17 skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing. After
    the rebase onto main aa95d35f (head 38ba67dd): wave S1's merge 11a5b4e7, 568/22/17 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.
  • A game-free test added for the fix, failing on main and passing here.
**Type:** fix · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (game-free checks, no game run) · **Needs:** Windows or Linux, any GPU (AMD preferred) ## Why A shader that picks a texture buffer, an image buffer or a storage image by an index (a handle read at a computed offset of a constant buffer) is never drawn right on any GPU. With a texture buffer it is not translated at all (`Array of texture buffers is not implemented`). With an image buffer or a storage image the translation makes invalid SPIR-V, so what the driver does with it is undefined, and an atomic on one is not translated (`Image indexing is not implemented`). ## How to reproduce **Seen on:** macOS 26.7.1, Apple M4 Max, in the game-free test (`nx2/test_image_arrays.py --tree` main). The SPIR-V back end is the same code on every OS, so every OS translates these programs the same way. Not seen in the game: whether it has such an array is not known (its programs were not read). **Settings and switches:** defaults (the renderer's profile indexes these three kinds of array as dynamically uniform). **Steps:** `python -I -B nx2/test_image_arrays.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --cxx clang++ --tree <a tree of main>`: 28 synthetic compute programs with such arrays go through the real texture pass and back end. **Expected:** every module translates, passes `spirv-val --target-env vulkan1.3` and declares each binding with the count `Shader::Info` gives it. **Actual:** 18 failures over 14 of the 28 cases: the 10 with a texture buffer array or an atomic on an array are refused (`Array of texture buffers is not implemented`, `Image indexing is not implemented`), and the 4 image buffer and storage image array modules fail spirv-val (`OpAccessChain reached non-composite type`), each array declared as one image. ## What changed The SPIR-V back end now declares a texture buffer, image buffer or storage image descriptor with a count above 1 as an array of that many images, as it already did for textures, and reaches an element through a pointer to one image. An atomic on such an array takes its element instead of refusing. A count of 1 translates exactly as before. - Read first: `DefineTextureBuffers`, `DefineImageBuffers` and `DefineImages` against `DefineTextures`, then `TextureImage`'s buffer branch and `Image()` in `emit_spirv_image_atomic.cpp`. - vk (the SPIR-V back end): `gpu/vk/eden/shader_recompiler/backend/spirv/spirv_emit_context.cpp`. - The texture pass already makes such a descriptor an array (`Shader::Info`'s `count`), and the renderer already declares and writes `count` descriptors for it (`ensure_layout` and `build_descriptors` in `gpu/vk/vk_draw.cpp`). Only the back end did not. - `DefineTextureBuffers`, `DefineImageBuffers` and `DefineImages` now declare a descriptor of count above 1 as an `OpTypeArray` of count images, through `DescType`, as `DefineTextures` already did for textures. `DefineTextureBuffers` no longer throws `Array of texture buffers`. - A count of 1 declares exactly what it did before. - vk: `TextureImage` (`emit_spirv_image.cpp`) reaches a texture buffer array's element through a pointer to one image (`TextureBufferDefinition::pointer_type`, new in `spirv_emit_context.h`), not through the image type itself. `Image()`, for image buffer and storage image loads and stores, was already right once the variable is an array. - vk: the image atomics (`emit_spirv_image_atomic.cpp`) take an array's element the same way, and decorate it and the texel pointer `NonUniform` where the profile indexes that kind of array so. They used to throw `Image indexing` for any index but 0, and the index of an array is its element. A single image takes no index, as before. - tests: `nx2/test_image_arrays.py` (new) with its fixture `tests/image_arrays_ir.cpp` (new). - Synthetic compute programs, no game data: a 2-element image buffer array and a 3-element storage image array read and written, the same two added to atomically, a 2-element texture buffer array fetched, and one program with all four kinds (a 2-element texture array too). - Each also with single descriptors (the handle at a constant offset). - Each translated with no kind of array indexed nonuniformly and with every kind indexed nonuniformly (the renderer's profile does it for textures only); the texture buffer programs also with the out-of-bounds fetch guard every GPU but NVIDIA's gets (`fetch_bounds`, `vk_vendor.h`). 28 modules. - They go through the real texture pass, dead code elimination, `CollectShaderInfoPass` and the back end. - Every module must translate and pass `spirv-val --target-env vulkan1.3`, and declare each binding with the count `Shader::Info` gives it. With nonuniform indexing, each array kind must have its capability and its `NonUniform` decorations, an atomic's texel pointer included. - `--tree` runs the same programs through another checkout's back end; `--base-tree` also requires the 14 single-descriptor modules to be byte for byte that checkout's. - Design: the texture pass, the renderer's layouts and its descriptor writes are unchanged: they already handled `count`. - Left out of the checklist: the box for checks under `tests/` that read changed source. The checks that cut code out of these files (`test_vendor_translation`, `test_native_float_atomic_add`, `test_cas_loop_ceiling`, `test_global_atomic_add_f32`, `test_global_table_loop`, `test_mesh_point_topology`, `test_mesh_unreachable_outputs`, `nx2/test_mesh_first_types`) needed no change and pass. Nor is the Windows and Linux build box there: this was built on macOS only (Testing). ## Measurements **Verdict:** fixed, game-free: the new test fails on main (18 failures over 14 of its 28 cases) and passes here, with the 14 single-descriptor modules byte for byte main's; the suite at the stack top #502 adds no failure. A translation fix: nothing to time. Commits: main 6c9edffe5da1f5b5383977aeab3f544c0de25d4c (2026-11-14); this PR fix/descriptor-image-arrays @ a66f16b0fbc17dd5602e4cf2c297b2fe3b9756cf. Hardware: Apple M4 Max (16 CPUs), macOS 26.7.1, Apple clang 21, spirv-val of SPIRV-Tools v2026.4 (Homebrew spirv-tools 1.4.363.0). Game-free: no GPU, no game Scene: none (a translation fix; its proof is the test, as CONTRIBUTING's `fix` row allows) Method: `nx2/test_image_arrays.py` with this PR's back end and with main's (`--tree`, a `git archive` of main); every module the repository's real-IR fixtures emit, built against both back ends and compared byte for byte; the full game-free suite on both How to reproduce: `python -I -B nx2/test_image_arrays.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --cxx clang++ --base-tree <a tree of main>` (add `--tree <a tree of main>` for main's side) | | Before: main @ 6c9edffe | After: this PR @ a66f16b0 | |---|---|---| | `nx2/test_image_arrays.py`: modules that translate | 18 of 28: the 10 with a texture buffer array or an atomic on an array are refused | 28 of 28 | | modules that pass spirv-val | 14 of the 18: the 4 image buffer and storage image array modules fail (`OpAccessChain reached non-composite type`) | 28 of 28 | | bindings declared with Info's count | not the 4 arrays (declared as one image) | all | | single-descriptor modules | 14 | the same 14, byte for byte | | every module the other real-IR fixtures emit (`depth_sample_types`, `unsigned_image_types`, `texture_phi_dominance`, `global_load_16` and its compare-exchange loop, `native_suatom`, `f2i_immediate`) | 100 modules | the same 100, byte for byte (compared before the rebase; neither side's back end changed in it) | | full game-free suite on macOS (`tests/run_all.py --jobs 6`) | 531 passed, 32 failed, 17 skipped | not run on this head alone: #502 @ f8f7695c, which carries it, 533 passed, 32 failed, 17 skipped | ``` main @ 6c9edffe (nx2/test_image_arrays.py --tree <main>): FAIL: imgbuf-2: spirv-val: error: line 73: OpAccessChain reached non-composite type while indexes still remain to be traversed. FAIL: imgbuf-2: the module declares {'texbuf': [], 'imgbuf': [1], 'tex': [], 'img': []} where Shader::Info has {'texbuf': [], 'imgbuf': [2], 'tex': [], 'img': []} FAIL: imgbuf-2-nonuniform: spirv-val: error: line 81: OpAccessChain reached non-composite type while indexes still remain to be traversed. FAIL: imgbuf-2-nonuniform: the module declares {'texbuf': [], 'imgbuf': [1], 'tex': [], 'img': []} where Shader::Info has {'texbuf': [], 'imgbuf': [2], 'tex': [], 'img': []} FAIL: img-3: spirv-val: error: line 74: OpAccessChain reached non-composite type while indexes still remain to be traversed. FAIL: img-3: the module declares {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [1]} where Shader::Info has {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [3]} FAIL: img-3-nonuniform: spirv-val: error: line 82: OpAccessChain reached non-composite type while indexes still remain to be traversed. FAIL: img-3-nonuniform: the module declares {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [1]} where Shader::Info has {'texbuf': [], 'imgbuf': [], 'tex': [], 'img': [3]} FAIL: texbuf-2: translation refused: Array of texture buffers is not implemented FAIL: texbuf-2-nonuniform: translation refused: Array of texture buffers is not implemented FAIL: texbuf-2-bounds: translation refused: Array of texture buffers is not implemented FAIL: imgbuf-atomic-2: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented FAIL: imgbuf-atomic-2-nonuniform: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented FAIL: img-atomic-3: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented FAIL: img-atomic-3-nonuniform: translation refused: Image indexing is not implemented (ImageAtomicIAdd32) is not implemented FAIL: mixed: translation refused: Array of texture buffers is not implemented FAIL: mixed-nonuniform: translation refused: Array of texture buffers is not implemented FAIL: mixed-bounds: translation refused: Array of texture buffers is not implemented this PR @ a66f16b0 (nx2/test_image_arrays.py --base-tree <main>): imgbuf-2: imgbuf [2] img-3: img [3] texbuf-2: texbuf [2] imgbuf [1] imgbuf-atomic-2: imgbuf [1, 2] img-atomic-3: imgbuf [1] img [3] mixed: texbuf [2] imgbuf [1, 2] tex [2] img [3] PASS: image buffer, storage image and texture buffer arrays declared as arrays; 28 modules validate; 14 single-descriptor modules byte for byte the base tree's ``` Screenshots: none; nothing on screen (no game run). ### Flags and settings None. ## Testing **Tested:** - On an Apple M4 Max with macOS 26.7.1, game-free only. - `nx2/test_image_arrays.py` fails on main (18 failures over 14 of the 28 cases) and passes here, with all 14 single-descriptor modules byte for byte main's. - Every module the repository's other real-IR fixtures emit (100: `depth_sample_types`, `unsigned_image_types`, `texture_phi_dominance`, `global_load_16` and its compare-exchange loop, `native_suatom` and `f2i_immediate`), built against main's back end and against this PR's with the same fixture sources: all 100 byte for byte the same, and all 128 of this PR's modules (with the 28 above) pass `spirv-val --target-env vulkan1.3 --uniform-buffer-standard-layout`, as CI's SPIR-V fixture job validates them. This ran before the rebase, on 5e072fc8; main has not changed `gpu/vk/eden`, `sirit` or `compat` since, and this PR's back-end files are the same after the rebase. - The full suite on both sides, compared by test name. - By test name, against main 6c9edffe's run on the same Mac, at the stack top #502 @ f8f7695c: 2 statuses changed: `test_image_arrays`, absent on main, passes here; `test_nonuniform_image_arrays`, absent on main, passes here. - These 32 checks fail here and on main on this Mac, by name (not triaged here: some need the macOS port's other pull requests, some are checks main added or changed since 5e072fc8): `test_appimage_package_retry`, `test_appimage_player_build`, `test_collect_wide`, `test_control`, `test_cpm_discovery`, `test_crash_dialog`, `test_crash_record`, `test_env_on`, `test_game_layout`, `test_lift_cleanup_error`, `test_linux_preflight`, `test_longshot_ctl`, `test_low_memory_notice`, `test_make_game_cache`, `test_mem_gate`, `test_mem_gate_children`, `test_mirror_trim`, `test_oot_state_tap`, `test_pcache_header_read`, `test_pipeline_cache_file`, `test_pipeline_cache_save`, `test_pipeline_play_cache`, `test_pipeline_tour`, `test_prof_symbols`, `test_program_cache_cleanup`, `test_program_cache_one_variant`, `test_readback_late`, `test_report_run`, `test_split_giants`, `test_vk_pgo`, `test_vram_budget_row`, `test_walk_key`. - The same 17 checks skipped as on main. - Crash reports during the run: recorder_sync_test.exe-2026-11-14-191118.ips (main's run: fault-arm64-2026-11-14-180704.ips, recorder_sync_test.exe-2026-11-14-180744.ips). - Object compiles of the 21 back-end files that include `spirv_emit_context.h` (the three changed `.cpp` files among them) with the renderer's flags (`nx2/build.py`), Apple clang 21, arm64 and x86_64: all 42 exit 0 on main and all 42 here, without the compile-only prelude main's `vk_shader.cpp` and `pipeline_cache_file.h` need on macOS. - Before the rebase: with that prelude this PR's 42 objects were byte for byte the same as without it, and 17 of the 21 objects were byte for byte main's on both architectures. The other four are the three changed files and `emit_spirv_context_get_set.cpp`, which only includes the changed header. - **After the rebase onto main aa95d35f**, at `38ba67dd` (`a66f16b0` 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 S1's merge `11a5b4e7`, which carries this head: 568/22/17 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). **Not tested:** - A GPU run. Whether the game has any such array is not known: its programs were not read. A program with one now translates to valid SPIR-V where it was refused or invalid before; nothing else changes. - Windows and Linux: not run. `run_all.py` skips the new test on Windows, as it skips the other spirv-val checks; on Linux it runs like them. The change is in the translation alone, which the test checks on any OS with spirv-val. - Not built on Windows or Linux. The change uses only Sirit calls, `std::tuple` and the definitions around it. **Must be tested:** - Windows or Linux, any GPU (AMD preferred), any driver: build this PR (head 38ba67dd) and main aa95d35f the same way; on Linux run `python -I -B tests/run_all.py image_arrays` (it needs spirv-val); start each build to the title and play a minute, expecting the test to pass, this PR's first start to translate every program again (the translator changed), its `vk: prepared N shader programs (... M failed ...)` line to count no more failed programs than main's, no `not translated` line naming `Array of texture buffers` or `Image indexing` where main's log may have them, and no new error in `longshot_log.txt` (by hand: test request T-XKYH) ## Risks **What could break:** low. Only programs with a texture buffer, image buffer or storage image array change, and on main those are refused or invalid SPIR-V. Every other module is byte for byte the same. - The renderer's profile (`make_profile` in `gpu/vk/vk_shader.cpp`) indexes only sampled image arrays nonuniformly. These three kinds are therefore indexed as dynamically uniform, as the profile already decides for them; the test covers both profiles. The device is created with every feature it reports (`vk_main.cpp`), dynamic indexing of these arrays included. - Landing order: #502 (`fix/nonuniform-image-arrays`) is stacked on this PR and lands after it; landing both in one release makes the cache rebuild below happen once. After the rebase on 6c9edffe it conflicts with none of the author's other open PRs as rebased there (`git merge-tree` of the heads, 2026-11-14). **Turning it off:** revert the commits. **Compatibility:** the back end is in `gpu/vk/eden`, one of the translator stamp's folders (`nx2/translator_files.py` since main's #370), so the first start after updating translates every program again, as any change to `gpu/vk/eden` does. Programs without these arrays translate to the same SPIR-V as before. The program cache's format and `Shader::Info` are unchanged; saves, the options file and the vk ABI are unchanged. ## 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. - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `38ba67dd`, without a conflict; this PR's own commits, old → new: `092b9b39` → `26d59e76`, `a66f16b0` → `38ba67dd`; 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). At the stack top #502, on macOS: 533 passed, 17 skipped, and 32 failures that also fail on main 6c9edffe on this Mac, named under Testing. After the rebase onto main aa95d35f (head `38ba67dd`): wave S1's merge `11a5b4e7`, 568/22/17 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] A game-free test added for the fix, failing on `main` and passing here.
A descriptor of count above 1 (a handle read at a dynamic constant buffer offset) is an array in Shader::Info, and the
renderer writes count descriptors to its binding. The SPIR-V back end declared image buffer and storage image arrays as
one image and indexed it with an OpAccessChain, which is invalid SPIR-V, and refused texture buffer arrays outright
("Array of texture buffers"). DefineTextureBuffers, DefineImageBuffers and DefineImages now declare DescType's array,
as DefineTextures does, and a texture buffer's element is reached through its own pointer type. A count of 1 declares
what it did before, so those modules are byte for byte the same.

nx2/test_image_arrays.py (tests/image_arrays_ir.cpp): synthetic compute programs with 2-element image buffer and
texture buffer arrays and a 3-element storage image array, plain, indexed nonuniformly and with bounded fetches, and
the same programs with single descriptors, through the real texture pass and back end. Every module must validate and
declare the counts Shader::Info has; --base-tree compares the single-descriptor modules with another tree's.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fix(vk): an atomic on an image or image buffer array takes its element
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 7s
PR description / Description follows the template (pull_request_target) Successful in 9s
CI / Workflow lint (actionlint) (pull_request) Successful in 10s
Third-party parts / As their notes say (pull_request) Successful in 10s
Website / Website on Cloudflare (pull_request_target) Has been skipped
8c9cee0df8
The arrays DefineImages and DefineImageBuffers now declare were still refused by the image atomics: ImageAtomicU32 threw
"Image indexing" for any index but 0, and the texture pass's index of an array is the element (UMin(element, count - 1)).
An atomic on an array now takes the element's pointer with an OpAccessChain, as Image() does for loads and stores, and
decorates it and the texel pointer NonUniform where the profile indexes that kind of array so. A single image takes no
index and is refused for one as before; its module is byte for byte the same.

nx2/test_image_arrays.py: a 2-element image buffer array and a 3-element storage image array added to atomically, with
their single-descriptor programs; a nonuniform atomic's texel pointer must be decorated NonUniform.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki force-pushed fix/descriptor-image-arrays from 8c9cee0df8
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 7s
PR description / Description follows the template (pull_request_target) Successful in 9s
CI / Workflow lint (actionlint) (pull_request) Successful in 10s
Third-party parts / As their notes say (pull_request) Successful in 10s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to a66f16b0fb
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
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 6s
CI / Workflow lint (actionlint) (pull_request) Successful in 8s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 14s
2026-11-15 01:51:34 +00:00
Compare
mooki changed title from WIP: fix(vk): image, image-buffer and texture-buffer arrays are arrays in the SPIR-V to fix(vk): image, image-buffer and texture-buffer arrays are arrays in the SPIR-V 2026-11-15 03:34:00 +00:00
mooki force-pushed fix/descriptor-image-arrays from a66f16b0fb
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
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 6s
CI / Workflow lint (actionlint) (pull_request) Successful in 8s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 14s
to 8063f59501
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 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
2026-11-15 07:01:37 +00:00
Compare
mooki force-pushed fix/descriptor-image-arrays from 8063f59501
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 4s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
to 38ba67ddc9
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 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 7s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 5s
2026-11-15 13:02:53 +00:00
Compare
HumanBean approved these changes 2026-11-15 19:04:14 +00:00
HumanBean left a comment

Read the back-end diff against DefineTextures/DescType, TextureImage and Image() in emit_spirv_image.cpp, and the atomics. Count above 1 is now an OpTypeArray for texture buffers, image buffers and storage images, an element is reached through a pointer to one image (pointer_type, new for texture buffers), and a count of 1 is declared as before. The atomic path indexes the element and decorates index, chain and texel pointer when the profile asks, and the capability flags it sets are the ones emit_spirv.cpp already turns into capabilities. The change is in gpu/vk/eden, so the translator stamp changes with it, as the description says. I did not run the tests.

Fits with #502: its flags (support_storage_image_array_nonuniform_indexing, the two texel buffer ones) are the ones Image() here and MarkNonUniform read, and the uses_nonuniform_* members already exist. No conflict seen.

Nit: the description names head a66f16b0 in places; the rebase note says 38ba67dd. Fine, but a reader following the Measurements commits should know.

Read the back-end diff against `DefineTextures`/`DescType`, `TextureImage` and `Image()` in emit_spirv_image.cpp, and the atomics. Count above 1 is now an OpTypeArray for texture buffers, image buffers and storage images, an element is reached through a pointer to one image (`pointer_type`, new for texture buffers), and a count of 1 is declared as before. The atomic path indexes the element and decorates index, chain and texel pointer when the profile asks, and the capability flags it sets are the ones emit_spirv.cpp already turns into capabilities. The change is in `gpu/vk/eden`, so the translator stamp changes with it, as the description says. I did not run the tests. Fits with #502: its flags (`support_storage_image_array_nonuniform_indexing`, the two texel buffer ones) are the ones `Image()` here and `MarkNonUniform` read, and the `uses_nonuniform_*` members already exist. No conflict seen. Nit: the description names head a66f16b0 in places; the rebase note says 38ba67dd. Fine, but a reader following the Measurements commits should know.
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 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 7s
Third-party parts / As their notes say (pull_request) Successful in 12s
PR description / Description follows the template (pull_request_target) Successful in 5s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'fix/descriptor-image-arrays:mooki-fix/descriptor-image-arrays'
git switch 'mooki-fix/descriptor-image-arrays'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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!450
No description provided.