Skip to content

perf(shader): load from buffers without Coherent when the load has no GLC or DLC - #2369

Merged
adriadam10 merged 4 commits into
boykopovar:mainfrom
karanvora2599:perf/shader-coherent-alias
Oct 11, 2026
Merged

adriadam10 merged 4 commits into
boykopovar:mainfrom
karanvora2599:perf/shader-coherent-alias

Conversation

@karanvora2599

Copy link
Copy Markdown
Contributor

What

A program with a single GLC or DLC buffer load or store (28b09d4) decorates the whole guest buffers descriptor array Coherent. Every other buffer access in the program is then coherent as well, including the loads the guest made without GLC or DLC. On NVIDIA the SM's L1 cache is not coherent between SMs, and the coherent loads lose it: in a compute program that re-reads a 4 KiB window, one GLC store at the end makes it about 2.7 times slower (Tested). GLC/DLC buffer accesses come from globallycoherent buffers and volatile accesses, e.g. decoupled look-back scans (the radix sort behind 28b09d4) and GPU-driven work queues, and these programs usually read a lot of other buffer data with plain loads.

Why the loads without GLC or DLC don't need to be coherent

RDNA does not keep them coherent either. A vector load without GLC or DLC can hit the per-CU L0 and the per-shader-array L1 caches, and neither is coherent between work-groups. The LLVM AMDGPU memory model for GFX10 (https://llvm.org/docs/AMDGPUUsage.html, "Memory Model GFX10-GFX11") describes it:

  • "a buffer_gl1_inv is required for coherence between wavefronts executing in different work-groups as they may be executing on different SAs that access different L1s", and a buffer_gl0_inv "is also required for coherence between wavefronts executing in different work-groups", and in WGP mode for wavefronts of the same work-group.
  • An agent-scope acquiring load is buffer/global_load glc=1 dlc=1, s_waitcnt vmcnt(0), then buffer_gl1_inv; buffer_gl0_inv, which "Ensures that following loads will not see stale global data".
  • On GFX10, GLC and DLC are set on volatile and atomic accesses; a nontemporal load uses slc=1 only.

So guest code that needs another work-group's data either reads it with GLC/DLC, or invalidates the caches and then reads it with plain loads. Only the second case needs the plain loads to be coherent on the host, and it is visible in the program as a cache invalidation.

Change

  • The program metadata records whether the program invalidates a cache: buffer_gl0_inv, buffer_gl1_inv, s_gl1_inv and s_dcache_inv. They still emit nothing.
  • A program with GLC or DLC buffer accesses and no invalidation gets a second descriptor array, buffers_coherent, on the same binding as buffers, decorated Coherent. All buffer arrays of the program are decorated Aliased, as buffers and buffers_u64 already are for 64-bit atomics.
  • Loads without GLC or DLC read through buffers. GLC/DLC loads, every store and the 32-bit atomics go through buffers_coherent, so the program's writes stay coherent as before. buffers_u64 is unchanged and stays Coherent.
  • A program that invalidates a cache keeps the previous lowering: the whole buffers array is Coherent.
Program Before After
No GLC/DLC buffer access buffers, not coherent unchanged
GLC/DLC accesses, no cache invalidation buffers Coherent for every access plain loads: buffers; GLC/DLC loads, stores, atomics: buffers_coherent (Coherent, Aliased)
GLC/DLC accesses and a cache invalidation buffers Coherent unchanged

Plain loads in programs without GLC or DLC accesses were already incoherent; this treats plain loads in programs with GLC/DLC accesses the same way. The Volatile memory operand on GLC/DLC accesses and the BDA path (GPU-selected V#s, SpirvBdaRead.cpp) are unchanged.

Which GPUs gain

The gain comes from plain loads staying in L1. It was measured on Ada (RTX 4060) only. The CUDA C++ Programming Guide says that on compute capability 5.x and 6.x (Maxwell, Pascal) global loads of data that is not read-only are not cached in L1 by default, so those GPUs may gain less. Other vendors were not measured.

Tested

Windows 11, MinGW-w64 GCC 15.2.0 (WinLibs r7), Release, ANYPS5_ENABLE_SPIRV_TOOLS=ON, main at d70b899. GPU tests on an NVIDIA GeForce RTX 4060 Laptop GPU (591.86, subgroup 32) and on lavapipe (Mesa 26.2.4 for Windows, GALLIUM_OVERRIDE_CPU_CAPS=avx).

  • New check in agc_shader_memory (no GPU needed): four wave32 compute programs that load from one guest buffer and store to another, inspected in the emitted SPIR-V:

    • no GLC: one buffers array, not Coherent, used by the load and the store;
    • GLC store: buffers without Coherent and buffers_coherent with it, both Aliased; the load goes through buffers and the store through buffers_coherent;
    • GLC load: the load and the store go through buffers_coherent;
    • GLC store after buffer_gl0_inv: one buffers array, Coherent, as on main.
  • Negative checks, each fails the new check:

    • plain loads through the coherent array: coherent buffers: the two buffer arrays are not decorated Aliased (buffers is then unused and no longer in the module);
    • cache invalidations not recorded: coherent buffers: a program that invalidates the vector caches does not keep every buffer access coherent;
    • the previous lowering (no alias): coherent buffers: a GLC store has no coherent buffer array.
  • Every shader test (agc_driver_*, agc_shader_*, agc_spirv_*, vector_*, agc_masked_select, agc_validate_program, 234 tests):

    • RTX 4060: all pass, 1 skipped (agc_driver_storage_cache_budget). This includes agc_driver_look_back_scan, a decoupled look-back scan across 4096 work-groups with GLC pair loads, atomics and plain loads and no cache invalidation, which now takes the new lowering (checked in its SPIR-V: plain loads on buffers, the GLC loads and stores on buffers_coherent).
    • lavapipe: 208 pass, 26 skipped for missing device features (subgroup operations, mesh, DPP, look_back_scan among them); all 26 pass on the RTX 4060.
  • Full ctest after cmake --build build and --target libs, serial, --timeout 120, RTX 4060: 505 of 511 pass, 2 skipped (agc_driver_storage_cache_budget, guest_video_out_wqhd_detection_no_vulkan), 4 fail. All four are relinker tests, whose build contains no recompiler sources: windows_import_modules (Windows Error Reporting keeps its GUI fixture open, test(relinker): launch the GUI startup fixtures without Windows Error Reporting #2227), and tls_function_coverage, windows_gui and windows_icon, which fail to open an executable they just wrote to the temporary directory because Windows Defender removes it (Defender logged the detections during the run).

  • Throughput on the RTX 4060, not committed: a wave32 compute program whose 32 lanes read 512 dwords each from a 4 KiB window of one buffer (64 batches of 8 buffer_load_dword, L1-resident after the first pass), sum them and write the result with one buffer_store_dwordx4; 8192 work-groups, timed per dispatch including submit and wait, best of 8 dispatches. All variants run in one process, in a rotated order each round, because the laptop's clocks change by up to 4x over a few seconds and separate runs could not be compared. A buffer_gl0_inv at the start selects the previous lowering for the same program (the instruction itself emits nothing), so the last column is what main does for this program:

    Round No GLC No GLC, buffer_gl0_inv GLC store (this PR) GLC store, buffer_gl0_inv (main)
    1 1.11 ms 1.13 ms 1.26 ms 3.09 ms
    2 2.78 ms 1.26 ms 1.14 ms 3.13 ms
    3 6.01 ms 6.87 ms 4.85 ms 17.44 ms
    4 4.80 ms 6.92 ms 7.58 ms 12.59 ms
    5 6.88 ms 7.03 ms 7.36 ms 20.98 ms
    6 3.48 ms 1.33 ms 3.64 ms 17.36 ms

    The previous lowering is the slowest variant in every round, 1.7 to 4.8 times the GLC store with this change (2.7 times in the best rounds: 3.09 ms against 1.14 ms); with the change, the GLC program runs about as fast as the program without GLC. All variants produce the same output (same checksum). An earlier in-process run with a temporary switch that dropped the Volatile memory operand from the GLC store showed no measurable difference, with either lowering; the difference is the Coherent decoration on the loads.

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 in TechnicalDebt
  • 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
  • AI-assisted: yes

… GLC or DLC

A program with one GLC or DLC buffer load or store decorated the whole
guest buffer array Coherent, so its other loads were coherent too. On
NVIDIA that keeps them out of the L1 cache: one GLC store made the
re-reads of a 4 KiB window about 2.7 times slower.

RDNA keeps those loads incoherent. A load without GLC or DLC can hit the
per-CU L0 and per-SA L1 caches, which are not coherent between
work-groups, and code that needs another work-group's data reads it
with GLC/DLC or invalidates the caches first (LLVM AMDGPU memory model,
GFX10: an acquiring load is followed by buffer_gl1_inv; buffer_gl0_inv).

The program metadata now records whether a program invalidates a cache
(buffer_gl0_inv, buffer_gl1_inv, s_gl1_inv, s_dcache_inv). Without one,
a program with GLC/DLC accesses gets a second array on the same binding,
buffers_coherent, decorated Coherent, and both arrays are Aliased as
buffers_u64 already is. Loads without GLC or DLC read buffers; GLC/DLC
loads, stores and atomics use buffers_coherent. A program that
invalidates a cache keeps the whole array Coherent.
@github-actions github-actions Bot added shader core/shader agc libSceAgc* labels Oct 9, 2026
@Kikoarl

Kikoarl commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Linux run: Linux 7.2 (x86_64), GCC 16.2.1, Release, -DBUILD_TESTING=ON, #2369 (d220f48e) tested on current main (0fdda7be).

  • tests/agc_shader_memory_tests: passes cleanly (Shader memory capture, strict validation and deterministic replay passed).
  • tools/check_conventions.py: passes cleanly.
  • Verified non-coherent buffer load optimization on Linux without regressions.

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on Linux (merged onto current main): build + libSceAgc/libSceAgcDriver ctest green (294 tests, agc_shader_memory passes), and the new check fails with the non-test changes reverted. Thanks @Kikoarl for the Linux run.

This changes memory behaviour, so a design call before merging:

  • Plain loads in a program with GLC/DLC accesses and no buffer_gl0_inv/buffer_gl1_inv/s_gl1_inv/s_dcache_inv stop being coherent. That follows the LLVM GFX10 memory model, but nothing here shows that a real title never reads shared data with plain loads after a GLC flag load without invalidating (the HW oracle can't measure cross-work-group coherence either).
  • The speed-up is measured on one GPU (RTX 4060) and with a synthetic program only; the look-back scan test passes but pins the result, not the loads' coherence.

@oneandonlydean could you run agc_driver_look_back_scan and a title on the RTX 4090 with and without this change? @boykopovar: are you ok with relaxing plain-load coherence in these programs?

Before it can land:

  • docs/dev/TechnicalDebt.md: add a line saying that loads without GLC/DLC are not coherent in programs with GLC/DLC accesses and no cache invalidation, and that this follows the LLVM AMDGPU GFX10 memory model, not a measurement (not at the end of its section).

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 10, 2026
…alias

# Conflicts:
#	core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp
@karanvora2599

Copy link
Copy Markdown
Contributor Author

Thanks for checking it. I merged current main (1c75f4d) into the branch, without conflicts, and added the docs/dev/TechnicalDebt.md line (5a16ed1). It sits under "Functional", right after the raw buffer_load_dwordx2 entry rather than at the end of the section:

Buffer loads without GLC or DLC (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no buffer_gl0_inv, buffer_gl1_inv, s_gl1_inv or s_dcache_inv: only its GLC and DLC loads, stores and atomics use the Coherent buffer view, so a plain load can return a value cached before another work-group's write. This follows the LLVM AMDGPU memory model for GFX10, in which a load-acquire is a GLC DLC load followed by buffer_gl1_inv and buffer_gl0_inv; it is not measured on the console or on RDNA hardware.

Retested at 2069fd1 (Windows 11, MinGW-w64 GCC 15.2.0, Release):

  • agc_shader_memory passes on the RTX 4060 and on lavapipe 25.2.8 (the version CI installs); agc_driver_look_back_scan passes on the RTX 4060 and is skipped on lavapipe.
  • Shader tests (agc_driver_*, agc_shader_*, agc_spirv_*, vector_*, agc_masked_select, agc_validate_program, agc_denormal*, agc_float16_*; 319 tests): the only failures are ones main has too.
    • RTX 4060: the device-lost agc_driver_flat_loads*, agc_driver_global_loads*, agc_driver_flat_apertures_2, agc_driver_flat_apertures_4 and agc_driver_pixel_interlock.
    • lavapipe 25.2.8: agc_driver_image_plane_address_on_line_implicit_lod, and an intermittent segfault in agc_driver_depth_fast_clear_submit. Since its fill kernel stores through a buffer, I ran it 12 times each way: it crashed in 5 runs with this branch and in 7 with main's recompiler, and never on lavapipe 26.2.4.

The design question for @boykopovar and the RTX 4090 run from @oneandonlydean are still open; I'll follow whatever you decide.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 11, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked merged onto current main: libSceAgc/libSceAgcDriver build + ctest green (402 tests), agc_driver_look_back_scan passes with the new lowering, and agc_shader_memory fails with the non-test changes reverted. Thanks @Kikoarl for the Linux run.

The design question is settled on our side: plain loads in these programs follow the GFX10 memory model (acquire = GLC+DLC load plus buffer_gl0_inv/buffer_gl1_inv), programs that invalidate keep the old lowering, and the TechnicalDebt line records that this is not measured on hardware. Merging once CI is green.

@adriadam10
adriadam10 merged commit 9a9bd3f into boykopovar:main Oct 11, 2026
14 checks passed
@karanvora2599
karanvora2599 deleted the perf/shader-coherent-alias branch October 12, 2026 13:27
Celegans12 added a commit to Celegans12/AnyPS5-Ghost-of-Yotei that referenced this pull request Oct 13, 2026
The SPIR-V emitter declares extra views of the guest buffers binding:
buffers_coherent for GLC or DLC accesses (boykopovar#2369) and buffers_u64 for
64-bit atomics. Each is another StorageBuffer variable on the same set
and binding, decorated Aliased, which Vulkan allows.

ShaderValidation, which checks the stages of every draw, accepted one
StorageBuffer variable per binding with u32 elements only, so such a
graphics shader was refused with "duplicate SPIR-V resource binding".

A second variable is now accepted when the binding is the guest buffers
array and every variable on it is Aliased; an aliased view may hold u64
elements. Other duplicates are still refused. agc_driver_graphics
validates pixel shaders with a GLC store and a 64-bit atomic, and checks
that the same modules without Aliased are refused.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants