How GLSL Coherent Qualifier Guarantees Memory Visibility

In modern graphics and compute pipelines, shader invocations execute concurrently across independent processing units with distinct cache hierarchies. This article explains how the coherent qualifier in the OpenGL Shading Language (GLSL) bypasses or synchronizes local hardware caches to guarantee that buffer and image modifications made by one shader invocation become visible to other invocations across the GPU. It breaks down GPU caching behaviors, the specific guarantees provided by coherent, and the critical relationship between memory qualifiers and GLSL memory barriers.

The GPU Caching Problem

Modern GPUs execute code through massively parallel hardware threads (such as warps on NVIDIA or wavefronts on AMD). Each compute unit contains its own execution cores and fast, private Level 1 (L1) data caches, while sharing a larger Level 2 (L2) cache or global memory.

By default, GLSL assumes that memory transactions inside shader storage buffer objects (SSBOs) and image variables only need to be consistent within the single invocation performing them. Consequently, the compiler and hardware optimize memory access by storing writes in private L1 caches or registers. If Invocation A writes a value to an SSBO, that value may reside exclusively in its local L1 cache. If Invocation B, running on a separate compute unit, attempts to read that same memory address, it reads stale data from its own local cache or L2, unaware that Invocation A updated the location.

How the coherent Qualifier Modifies Memory Access

Declaring a variable with the coherent qualifier alters the low-level instructions generated for that memory address:

  1. Bypassing Incoherent Caches: When a resource is qualified as coherent, the GPU routes load and store operations through a shared memory path. On most architectures, this means loads and stores bypass the core-private L1 cache entirely and interact directly with the globally shared L2 cache or device memory.
  2. Preventing Compiler Reordering and Caching: The GLSL compiler treats coherent variables similarly to volatile memory in CPU programming. It inhibits optimizations that would cache the memory location in local registers across iterations or reorder memory operations in ways that break expected data access patterns.
  3. Establishing a Common View of Data: Because all coherent reads and writes converge at the globally shared cache level, an updated value committed by one processing unit becomes physically accessible to any other unit querying that same memory address.

The Necessity of Memory Barriers

The coherent qualifier guarantees visibility upon access, but it does not guarantee execution ordering or timing across asynchronous invocations.

Because shader invocations run independently and memory controllers buffer requests, a writing invocation might not complete its memory transaction before a reading invocation executes its load instruction. To resolve this race condition, coherent must be used alongside memory barriers and execution synchronization:

  • Memory Barriers (memoryBarrier(), memoryBarrierBuffer(), memoryBarrierImage()): These functions force the calling invocation to wait until all pending memory writes are completely pushed out to the coherent cache level.
  • Execution Barriers (barrier()): Used in compute shaders, this synchronizes the execution timing of all invocations within a single workgroup.

To safely pass data from Invocation A to Invocation B across workgroups:

  1. Invocation A writes to a coherent buffer or image.
  2. Invocation A executes a memory barrier function to ensure writes leave the local pipeline and reach the globally visible cache.
  3. Invocation B waits for a completion signal (often coordinated via atomic operations or multi-pass dispatch boundaries).
  4. Invocation B executes a memory barrier to invalidate any pre-fetched stale data.
  5. Invocation B reads from the coherent buffer or image, retrieving the updated value from the shared cache.

Coherent vs. Atomic Operations

Atomic operations (such as atomicAdd or atomicExchange) in GLSL intrinsically operate coherently; they execute directly against the shared cache or memory controller to ensure data integrity. However, regular load and store instructions (bufferVar = x; or imageStore(...)) do not exhibit this behavior by default. Applying the coherent qualifier ensures that non-atomic reads and writes follow the same centralized, globally visible memory path, enabling safe communication between distinct shader invocations.