Skip to content

fix(shader): order an image store before a later image atomic on the same texel - #10652

Open
maximilianfv wants to merge 1 commit into
boykopovar:mainfrom
maximilianfv:fix/shader-image-store-before-atomic-barrier
Open

maximilianfv wants to merge 1 commit into
boykopovar:mainfrom
maximilianfv:fix/shader-image-store-before-atomic-barrier

Conversation

@maximilianfv

@maximilianfv maximilianfv commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What

An invocation that stores to a storage image and then runs a 32-bit image atomic on the same texel got the value from before the store under MoltenVK. This emits OpMemoryBarrier (Device scope, AcquireRelease | ImageMemory) before OpImageTexelPointer in the 32-bit path of EmitAtomicOp (SpirvImageEmitter.cpp), so it covers every 32-bit image atomic, the compare-exchange loops included.

Cause, from a MoltenVK shader dump of the two failing tests: the recompiler emitted OpImageWrite, then OpImageTexelPointer + OpAtomicIAdd, with no barrier between them (the only OpMemoryBarrier, Device | AcquireRelease | UniformMemory, comes after the atomic). MoltenVK 1.4.2's SPIRV-Cross turned that into image.write(...) directly followed by image.atomic_fetch_add(...) with no fence. SPIRV-Cross's readwrite_texture_fences fences a write before a later read only, not before an atomic, and Metal makes a thread's texture write visible to its own later texture atomic only after a texture fence. So the atomic returned the texel from before the store (the tests' 0xdeadbeef fill). SPIRV-Cross writes the new barrier as atomic_thread_fence(mem_flags::mem_texture, ...) only when the target is MSL 3.2 or later (CompilerMSL::emit_barrier emits atomic_thread_fence when the barrier has no execution scope and supports_msl_version(3, 2) holds, and a threadgroup_barrier otherwise). Upstream issue for the missing fence: KhronosGroup/SPIRV-Cross#2723. The gap this leaves is recorded in docs/dev/debt/shader.md.

Vulkan already orders an invocation's store and its later atomic by program order, so other drivers gave the right result before. The barrier is unconditional, so it costs one memory barrier per 32-bit image atomic on every driver; I made no performance measurement.

Limits:

  • The 64-bit image atomic path gets no barrier, as in the original commit; the tested GPU path has no 64-bit image atomics.
  • SPIRV-Cross's CompilerMSL::emit_barrier emits nothing outside compute, task, mesh and tessellation control shaders, so on MoltenVK a store followed by an atomic on the same texel in a pixel or vertex shader can still return the old value. The fix that covers every stage belongs in SPIRV-Cross (MSL: no texture fence before an image atomic that follows a write to the same read_write texture KhronosGroup/SPIRV-Cross#2723).
  • On an MSL target older than 3.2, SPIRV-Cross would write the barrier as a threadgroup_barrier inside the per-lane divergent control flow that EmitValueOrZeroIfCondition emits around the atomic, which Metal does not allow. Only macOS 27.0.1 (MoltenVK 1.4.2, LunarG SDK 1.4.363) was tested.

Prior art and credit:

Tested

Apple M1 Pro, macOS 27.0.1, x86_64 build under Rosetta 2, Vulkan through MoltenVK 1.4.2 from the LunarG Vulkan SDK 1.4.363. main does not build on macOS by itself, so I tested on mugurc's macOS series (ci/macos-series-2, 27e9689) merged with main at 522e32e, plus local changes identical in both runs: the macOS build parts of #10419, #10540, #10562, #10641 and #10642, and two of my unopened fixes (the posix_spawnp child in LazyResolvedAbi.cpp, and the image table reuse chain-pool check, which the image test filter also covers). Only this change was toggled between the two runs.

Both runs: ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ctest --test-dir build -R 'image|atomic|shader_recompiler|spirv|recompiler' -E 'audio_in|voice' -j4 --timeout 300 -V (108 tests).

  • Without: 103 passed, 5 failed. With: 105 passed, 3 failed.
  • Fixed: agc_driver_image_cache_bits (image_store dlc: thread 0 is 0xdeadbeef, expected 0x00000000) and agc_driver_image_texel_status (image_store tfe: thread 0 is 0xdeadbeef, expected 0x00000000).
  • Failing both ways, unchanged: agc_driver_image_plane_bias_on_single_level_volume, agc_driver_image_srgb_sample_copy, agc_driver_image_srgb_sample_copy_forced. No new failures.
  • Validation: neither log has the was not found but was requested by env var VK_INSTANCE_LAYERS line. Both logs have 130 Validation Error lines with the same count for every VUID (54 imageViewFormatSwizzle-04465, 50 imageViewFormatReinterpretation-04466, 15 vkCreateDescriptorSetLayout-support-09582, 9 imageCreateMaxMipLevels-02251, 1 imageView2DOn3DImage-04459, 1 pProperties-04451), so the change adds no validation errors.

Not tested on any other driver or GPU, and not with any title.

Checklist

  • Based on current main; no other open PR implements the same functions (except feat(aot): consolidate native ARM64 compiler, runtime and consumers #5021, a 324-file consolidation that also carries mugurc's line; see What)
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: Apple M1 Pro, MoltenVK 1.4.2 (Vulkan SDK 1.4.363), macOS 27.0.1
  • AI-assisted: yes

🤖 Generated with Claude Code

…same texel

An invocation that stores to a storage image and then reads the texel
back with an image atomic got the value from before the store under
MoltenVK: Vulkan orders the two by program order, but Metal makes a
thread's texture write visible to its own later texture atomic only
after a texture fence, and SPIRV-Cross fences texture writes before
reads but not before texture atomics. Emit OpMemoryBarrier with device
scope and AcquireRelease | ImageMemory before each 32-bit image atomic;
MoltenVK turns it into a texture fence, and other drivers already give
the ordering.

Co-authored-by: mugurc <20699965+mugurc@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 720af6ea444f3e7b7f441946a35db9c844fab919. The targeted MoltenVK report is detailed, but I’m leaving this review commented pending three items. First, the exact barrier line is also present in open #5021; your comment there proposes #10652 first, but I see no agreement yet, and this checklist still says Depends on: none. Please record the agreed landing order in both checklists so the second PR retains only one copy. Second, the PR body says MSL targets older than 3.2 translate this barrier into an illegal threadgroup_barrier inside divergent atomic control flow; please establish the project’s supported MSL floor or make this path valid for supported older targets. Third, this inserts a barrier before every 32-bit image atomic on every backend, and the PR reports no performance measurement. Please bound that cost with a hot-path measurement or narrow the emitted barrier to the backend/conditions that need it. The full GPU matrix was not run here; hosted policy checks pass and product jobs are skipped.

@MaxLikesCode

Copy link
Copy Markdown
Contributor

An NVIDIA data point, since the barrier is emitted on every driver.

Setup: Windows 11 Pro 26200, RTX 4090 (617.42), WinLibs GCC 15.2.0 r7, Release, full build with BUILD_TESTING=ON. I compared main b607bfd against 720af6e merged onto it (clean merge), running your command (ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ctest -R 'image|atomic|shader_recompiler|spirv|recompiler' -E 'audio_in|voice' -j4 --timeout 300 -V, Vulkan-ValidationLayers vulkan-sdk-1.4.363.0) twice on each side:

Passed Validation Error lines
main b607bfd 109/109, 109/109 0
with this PR 109/109, 109/109 0
  • agc_driver_image_cache_bits and agc_driver_image_texel_status pass on both sides here, as you expected for drivers other than MoltenVK, so no test result changes on this GPU. The three tests that fail both ways on MoltenVK pass on both sides too.
  • The layer loaded on both sides: with VK_LOADER_DEBUG=layer the loader log shows it inserted, and neither log has was not found but was requested by env var VK_INSTANCE_LAYERS.
  • I made no performance measurement.

AI-assisted: yes (Claude Code)

@github-actions github-actions Bot added shader core/shader docs labels Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 9193 docs/dev/debt/shader.md L112

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 907 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
PR 5021 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
docs/dev/debt/shader.md
PR 5371 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
docs/dev/debt/shader.md
PR 6235 docs/dev/debt/shader.md
PR 6280 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
docs/dev/debt/shader.md
PR 6412 docs/dev/debt/shader.md
PR 6488 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
docs/dev/debt/shader.md
PR 6924 docs/dev/debt/shader.md
PR 6959 docs/dev/debt/shader.md
PR 7410 docs/dev/debt/shader.md
PR 7484 docs/dev/debt/shader.md
PR 7642 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
PR 7684 docs/dev/debt/shader.md
PR 8004 docs/dev/debt/shader.md
PR 8077 docs/dev/debt/shader.md
PR 8998 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
PR 9335 docs/dev/debt/shader.md
PR 9387 docs/dev/debt/shader.md
PR 9889 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
docs/dev/debt/shader.md
PR 10193 core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp
PR 10233 docs/dev/debt/shader.md

@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for the RTX 4090 run. It adds useful cross-vendor evidence: all 109 focused tests pass twice with and without the change, and the validation layer reports no errors on either side. That supports the claim that MoltenVK is the only observed behavior change so far. It does not yet bound the cost of emitting a memory barrier before every 32-bit image atomic, since the report explicitly says no performance measurement was made; a macOS-only/backend-specific emission path would also avoid imposing that barrier on Vulkan drivers that already provide the required ordering. The #5021 landing order and the MSL <3.2 compatibility concern are still unresolved, so my review remains commented.

@tablefrogpillow

Copy link
Copy Markdown

Linux/RADV, first AMD run — the added barrier is a clean no-regression across three AMD architectures.

I compared the PR's base d098eca3d against its head 720af6ea4 (so the diff is exactly this change, not unrelated main movement). agc_driver_.*(atomic|image) — 87 tests — with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ctest -V, RADV Mesa 25.2.8, kernel 6.8.

GPU (RADV) base d098eca3d this PR 720af6ea4
RX 7900 GRE (NAVI31, 1002:744c, RDNA3) 87/87 pass 87/87 pass
Ryzen 7 7800X3D iGPU (RAPHAEL, 1002:164e, RDNA2) 87/87 pass 87/87 pass
RX Vega 56 (VEGA10, 1002:687f, GCN5) 87/87 pass 87/87 pass

Identical on both sides on every arm: the only VUID- is the pre-existing VUID-VkDeviceCreateInfo-pNext-pNext (same count base and PR), no new validation errors. So the OpMemoryBarrier added before OpImageTexelPointer in the 32-bit image-atomic path is a no-op for correctness and validation on RADV — adding the AMD vendor to the RTX 4090 and Apple/MoltenVK data, consistent with MoltenVK being the only behaviour that changes.

AI-assisted: yes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants