Skip to content

fix(shader): order buffer and image memory at s_barrier - #5117

Closed
nikowolf123 wants to merge 1 commit into
boykopovar:mainfrom
nikowolf123:fix/shader-barrier-global-memory
Closed

nikowolf123 wants to merge 1 commit into
boykopovar:mainfrom
nikowolf123:fix/shader-barrier-global-memory

Conversation

@nikowolf123

@nikowolf123 nikowolf123 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

Outside vertex-path and hull programs, s_barrier became OpControlBarrier(Workgroup, Workgroup, AcquireRelease | WorkgroupMemory): semantics 0x108, or 0x148 when LDS lives in device memory. A SPIR-V barrier orders only the storage classes its semantics name. The SPIR-V specification, 3.2.24 Memory Semantics, says: "Storage classes not selected are not being constrained." UniformMemory (0x40) covers StorageBuffer and PhysicalStorageBuffer, and ImageMemory (0x800) covers image contents. The guest's V# buffers, GPU-selected (BDA) buffers, GDS and storage images all live there. So a store that one wave made before the barrier was not ordered against a load that another wave of the work-group made after it. The Vulkan memory model (Memory Model appendix, inter-thread-happens-before) orders accesses through a release or acquire barrier only when the barrier's semantics include their storage classes. Without that, such an exchange is a data race and the load may return the old value.

How the guest gets this ordering on the console:

  • RDNA 2 ISA guide (30 November 2020):
    • §4.3 Workgroups: S_BARRIER makes each wave wait until the other waves of the work-group reach it.
    • §4.4 Data Dependency Resolution: the program inserts S_WAITCNT so that earlier memory operations have completed.
    • So the barrier itself does not order memory.
  • LLVM AMDGPUUsage, Memory Model GFX10-GFX11:
    • In WGP mode the waves of one work-group can run on both CUs of a WGP. Those CUs have separate L0 caches, so work-group coherence needs buffer_gl0_inv.
    • The work-group fence rows of the GFX10-GFX11 code-sequence table are s_waitcnt lgkmcnt(0) & vmcnt(0) & vscnt(0) for release, and the same followed by buffer_gl0_inv for acquire.
    • In CU mode all waves of a work-group share one L0, so the acquire omits its vmcnt(0)/vscnt(0) and the buffer_gl0_inv. The release keeps its waits in both modes.
  • The recompiler translates s_waitcnt, s_waitcnt_vscnt and buffer_gl0_inv to nothing (EmitWaitcnt and EmitControlNop in SpirvFlowEmitter.cpp). It also does not know the CU or WGP mode. So the translated barrier is the only place left to carry the work-group release and acquire.

On RADV this has an effect. In Mesa ACO, setup_barrier and finish_barrier_internal (src/amd/compiler/aco_insert_waitcnt.cpp at Mesa 9b9e14ed5c99) make a workgroup barrier wait only for outstanding accesses in the storage classes named in its semantics, and only in WGP mode. With WorkgroupMemory alone, the barrier does not wait for a pending buffer store. RADV compiles a compute program in WGP mode on gfx10 and later only when it uses no LDS (radv_should_use_wgp_mode in src/amd/vulkan/radv_shader.c, same commit); a program with LDS runs in CU mode, where ACO drops these waits at a workgroup barrier.

Change:

  • EmitBarrier adds UniformMemory | ImageMemory. The semantics become 0x948 (AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory), still with Workgroup execution and memory scope.
    • Workgroup scope is what the guest's work-group fences give.
    • Ordering at agent scope still comes from the existing device-scope OpMemoryBarrier after atomics.
  • Every such barrier gets these semantics. That includes barriers in programs that do not write buffers or images, and barriers the guest did not precede with a vmcnt(0)/vscnt(0) wait. s_waitcnt reaches the IR as an operand-less Waitcnt, so the barrier cannot tell these cases apart. Those barriers get an ordering the console does not guarantee. That is allowed, but it may cost host waits (below).
  • These are unchanged:
    • Hull programs: Invocation memory scope, no semantics.
    • Vertex-path programs: no barrier.
    • The LDS-only barriers: the wave LDS barriers and the 64-bit LDS lock initialization.
  • Possible cost, not measured: on an AMD host in WGP mode, a barrier now also waits for the wave's outstanding buffer and image accesses. That includes loads the guest issued before a barrier for which it waited only on lgkmcnt. On RADV this concerns only programs without LDS, the ones it runs in WGP mode.

Still open, recorded in docs/dev/debt/shader.md next to the vertex-path s_barrier entry:

  • The semantics are not measured on hardware.
  • Some accesses are neither Coherent nor Volatile in the SPIR-V:
    • plain buffer accesses through the buffers array when it is not Coherent;
    • BDA accesses without GLC or DLC;
    • storage images.
  • Those accesses are private in the Vulkan memory model, and no shader barrier orders them between invocations. An example is a program that exchanges data with plain loads after buffer_gl0_inv and has no GLC or DLC access. Making those accesses coherent is a separate change with its own cost (perf(shader): load from buffers without Coherent when the load has no GLC or DLC #2369 measured what Coherent costs on NVIDIA).

Test: agc_driver_barrier_global_memory (tests/execution/BarrierGlobalMemory.cpp).

  • Each run dispatches 64 work-groups of 128 threads:
    • 4 waves in wave32, 2 waves in wave64;
    • 4 rounds per wave size;
    • the exchange buffer starts as 0xdeadbeef.
  • The dword code was assembled with llvm-mc 21, -mcpu=gfx1030:
    v_lshlrev_b32 v1, 2, v0
    v_sub_nc_u32 v2, 0x7f, v0
    v_lshlrev_b32 v2, 2, v2
    s_lshl_b32 s9, s8, 9
    v_add_nc_u32 v3, s9, v0
    buffer_store_dword v3, v1, s[0:3], s9 offen glc
    s_waitcnt_vscnt null, 0x0
    s_barrier
    buffer_load_dword v4, v2, s[0:3], s9 offen glc
    s_waitcnt vmcnt(0)
    buffer_store_dword v4, v1, s[4:7], s9 offen
    s_endpgm
    
  • Thread tid of group g must read g * 512 + 127 - tid, the value that thread 127 - tid stored.
  • The accesses use GLC:
    • In WGP mode, LLVM's GFX10 work-group atomic loads are glc=1 loads.
    • In this recompiler, GLC accesses go through the Coherent buffer view.
    • So the test isolates the barrier semantics.
  • On lavapipe the runtime part passes with and without the change, so the race does not show there. The test therefore also reads the emitted module. Every OpControlBarrier must have Workgroup execution and memory scope, and semantics that contain 0x948.

Tested

Not measured on AMD hardware. Everything below ran on lavapipe, except the last run in the list, on an NVIDIA RTX 5080. Reviewers with a gfx103x GPU, please confirm:

  1. With an adapted copy of the exchange body above in tools/hw-oracle, in work-groups of 128 lanes, wave32 and wave64, the hardware reads 127 - tid.
  2. agc_driver_barrier_global_memory passes on RADV. If you can, also run it with the change reverted. The test program declares no workgroup memory, so RADV runs it in WGP mode, where that build may read 0xdeadbeef; on lavapipe it does not.

Setup:

  • WSL2 Ubuntu 26.04, GCC 15.2.0, CMake 4.2.3, main at c8eb1f5.
  • Configured like CI: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DANYPS5_ENABLE_SPIRV_TOOLS=ON, then cmake --build build -j6.
  • Vulkan: lavapipe from Mesa 26.0.8 (llvmpipe, LLVM 21.1.8, 256 bits, subgroup size 8), with VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json ANYPS5_REQUIRE_VULKAN=1 GALLIUM_OVERRIDE_CPU_CAPS=avx.
  • With ANYPS5_ENABLE_SPIRV_TOOLS=ON, SPIRV-Tools validates every recompiled module for the target's Vulkan environment, including the new barrier semantics.

Results:

  • New test on main plus the test only (ctest -R agc_driver_barrier_global_memory --output-on-failure): fails with wave32 s_barrier semantics 0x00000108 do not order buffer, workgroup and image memory with acquire-release (0x00000948). It gets there after the 4 runtime rounds have passed.
  • New test with the change: passes in 0.08 s.
  • Negative control, not committed: I removed the OpControlBarrier from EmitBarrier. The runtime part then fails on lavapipe with wave32 round 0 group 0 thread 0 read 0xdeadbeef after s_barrier, expected 0x0000007f. So the runtime exchange does depend on the barrier.
  • ctest -R 'agc_shader|agc_driver' -j6 --timeout 120:
    • main: 377 tests, 341 passed, 36 skipped;
    • this branch: 378 tests, 342 passed, 36 skipped.
    • The same 36 tests are skipped on both, for device features lavapipe lacks.
  • Full ctest -j6 --timeout 120 on this branch: 1044 tests, 0 failed, 43 skipped.
  • On main 14036cb (the branch now sits on 0f2683c with the same three-file change, no conflicts). Native Windows 11, WinLibs GCC 15.2.0, NVIDIA GeForce RTX 5080 (driver 616.92): the new test passes 3 of 3 runs, and ctest -R 'agc_shader|agc_driver|shader|recompiler' passes 383 of 383.

Checklist

  • Based on current main; no other open PR implements the same functions
  • 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
  • Unblocks: none
  • GPUs tested: lavapipe (Mesa 26.0.8, llvmpipe) on WSL2 Ubuntu 26.04; NVIDIA GeForce RTX 5080, driver 616.92, Windows 11
  • AI-assisted: yes. Written with Claude Code; I reviewed every line and ran the tests above.

🤖 Generated with Claude Code

s_barrier became an OpControlBarrier whose semantics named only
Workgroup memory (and Uniform memory when LDS lives in device memory),
so a buffer, BDA or storage image store made by one wave before the
barrier was not ordered against a load by another wave of the
work-group after it. On RDNA the guest makes the barrier a work-group
release with s_waitcnt vmcnt(0) & vscnt(0) before it and, in WGP mode,
an acquire with buffer_gl0_inv after it (AMDGPUUsage, Memory Model
GFX10-GFX11, work-group fence rows). The recompiler translates neither
instruction and does not know the mode, so the translated barrier now
carries AcquireRelease over Workgroup, Uniform and Image memory at
Workgroup scope in every program except vertex-path and hull programs.
It orders the accesses the SPIR-V makes Coherent, such as the GLC
accesses through the coherent buffer array; plain buffer, BDA and
storage image accesses stay private in the Vulkan memory model, which
the new technical debt entry records.

The new execution test passes dwords between the waves of 128-thread
work-groups through a GLC buffer around s_barrier in wave32 and wave64
and checks the barrier semantics in the emitted SPIR-V.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nikowolf123
nikowolf123 force-pushed the fix/shader-barrier-global-memory branch from 599e1f8 to 41aa1c9 Compare October 13, 2026 12:51
@github-actions github-actions Bot added shader core/shader docs agc libSceAgc* labels Oct 13, 2026

@CnRJay CnRJay 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.

Careful write-up, and I agree with the reading of the memory model. OpControlBarrier with only WorkgroupMemory doesn't order StorageBuffer, PhysicalStorageBuffer or image accesses between invocations. Since s_waitcnt / buffer_gl0_inv translate to nothing, the barrier is the only place left to carry the release/acquire. The ACO detail (a workgroup barrier waits only for the storage classes it names) explains why this shows up on RADV first.

On the cost, which the description leaves unmeasured: this applies to every s_barrier in compute and pixel-path programs, including the many that only exchange through LDS (reductions, prefix sums, tiled blurs), where the guest's own s_waitcnt covers only lgkmcnt. On those, the barrier now also waits for every outstanding VMEM load and store of the wave, including loads issued early to hide latency, which is a common pattern before an LDS barrier. In a tight LDS loop that can remove most of the memory/ALU overlap the shader was scheduled for. Other vendors may do the same, which is why I would measure it on more than RADV.

Two suggestions:

  • Measure it. A title with heavy compute (any UE4/UE5 title with GPU culling or post-processing) on RADV and NVIDIA, with GPU time per dispatch (APS5_PROFILE_GPU [gputime] by program) for the top programs before and after, would show whether the cost is real.
  • If it is, narrow the extra semantics to programs that can need them. The recompiler knows statically whether a program has any buffer, BDA or image store (ResourcePlan / binding usage). A program whose only stores are LDS stores gains nothing from UniformMemory | ImageMemory at its barriers, because there is nothing across invocations for them to order. That keeps the fix exact for programs that exchange through memory and costs nothing for the LDS-only ones.

@tibzejoker

Copy link
Copy Markdown
Contributor

Intel run for this PR: Intel UHD Graphics 770 (Raptor Lake iGPU of an i9-13900K), driver 32.0.101.6129, Windows 11, MinGW-w64 GCC 15.2.0 Release, device picked with ANYPS5_GPU=Intel, at head 41aa1c926.

  • agc_driver_barrier_global_memory passes.
  • ctest -R "agc_driver|agc_masked|agc_shader" -j4: no failures out of 380 (352 pass, 28 skipped). These are the same 28 skips as main has on this GPU (f64, 64-bit and float atomics, fp16 interpolation and dot, mesh, shader clock and the like, which it does not support), and no new ones.

AI-assisted (Claude Code).

RINNECODER added a commit to RINNECODER/AnyPS5 that referenced this pull request Oct 14, 2026
…t) (#481)

* fix(recompiler): give vertex-path shaders the NGG group info

Ported from upstream PR boykopovar#5081

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): put the compute wave index in TG_SIZE[11:6]

Ported from upstream PR boykopovar#5214

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): take the 32_AR export alpha from channel 1 as GFX10 does

Ported from upstream PR boykopovar#5110

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): order buffer and image memory at s_barrier

Ported from upstream PR boykopovar#5117

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): decode GFX10 scratch offsets as one dword

Ported from upstream PR boykopovar#5426

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): accept MIMG sampler tuples ending in VCC

Ported from upstream PR boykopovar#4996

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(recompiler): name the attribute of an out-of-range interpolation reference

Ported from upstream PR boykopovar#5450

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): decode the high-half result of v_interp_p2_f16

Ported from upstream PR boykopovar#5471. The Vop3InterpDecode unit-test hunk is not ported (that test file is not on main); the execution test InterpolationF16 carries the coverage.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): clear the bits of inactive lanes in VALU carry-out and v_div_scale masks

Ported from upstream PR boykopovar#5162

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): apply the f32 output modifier and denormal flush to v_cvt_f32_f64

Ported from upstream PR boykopovar#5159

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): update per-invocation LDS without atomics

Ported from upstream PR boykopovar#5160. Conflicts resolved against main: kept main's barrier condition for non-LDS shared atomics; the unit-test hunk for SRT read patching (a different upstream change, not on main) is dropped.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): flush ds_add_f32 denormals and NaNs as the hardware does

Ported from upstream PR boykopovar#5492

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(shader): run DS src2 lanes one at a time in lane order

Ported from upstream PR boykopovar#5498

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(agc): compare NOT_EQUAL unordered, reject TRUNC_COORD point sampling and round the array layer half to even in color compare emulation

Ported from upstream PR boykopovar#907 (follow-up to its predecessor). Hand-applied to main's reworked emitter: NOT_EQUAL uses OpFUnordNotEqual, the array layer is clamp(roundEven(z)), and ResourceMaterializer rejects TRUNC_COORD with point filtering. The upstream execution-test rewrite is not ported (it targets a diverged test file).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* chore(shader): bump ShaderDiskCache FormatVersion to 35

The ported DS fixes add MemoryInfo::flushDenormalResults (now encoded in the plan) and new IR opcodes, so cached payloads from format 34 must not load.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): run ported upstream execution tests on Metal through a device shim

The upstream execution tests for the ported fixes compile unchanged against a Metal stand-in for the Vulkan test device (guest buffer descriptors are mirrored into shared MTLBuffers). Covers boykopovar#5162 (VALU carry-out masks), boykopovar#5159 (v_cvt_f32_f64 result modes), boykopovar#5492 (ds_add_f32) and boykopovar#5498 (DS src2 lane order). The LdsSrc2 and LdsSrc2Wide tests use device-memory LDS, which the Metal bridge rejects, and are not registered.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): run ported decoder and collector unit tests in the Metal build

Registers the upstream tests for boykopovar#4996, boykopovar#5426 and boykopovar#5450 (already on main's tree but not built for Metal) and a high-half decode check for boykopovar#5471.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): check per-invocation LDS updates without atomics on the Metal recompiler

Standalone copy of the boykopovar#5160 verifyFunctionLdsUpdates check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): check the TG_SIZE wave index on Metal

Buffer-store variant of the boykopovar#5214 execution test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): run the s_barrier memory-order execution test on Metal

Registers boykopovar#5117's upstream test through the device shim.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): draw a 32_AR export on Metal

Guest-replay draw for boykopovar#5110: alpha comes from export channel 1.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): draw an NGG vertex shader that reads the group info

Guest-replay draw for boykopovar#5081.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): reject TRUNC_COORD point sampling in the color compare emulation

Recompile-level check for boykopovar#907.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(shader): run the src2 lane-order test without the Metal debug layer

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(shader): port upstream debt entries and add the FormatVersion 35 history line

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Closing: no title is known to reach this. With the queue at 700+ open PRs, please hold off on new PRs that don't name a title needing the change until it's down. If a title stops on this on current main, reopen with the title ID and the log line.

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

Labels

agc libSceAgc* docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants