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:
- 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. - Preventing Compiler Reordering and Caching: The
GLSL compiler treats
coherentvariables 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. - Establishing a Common View of Data: Because all
coherentreads 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:
- Invocation A writes to a
coherentbuffer or image. - Invocation A executes a memory barrier function to ensure writes leave the local pipeline and reach the globally visible cache.
- Invocation B waits for a completion signal (often coordinated via atomic operations or multi-pass dispatch boundaries).
- Invocation B executes a memory barrier to invalidate any pre-fetched stale data.
- Invocation B reads from the
coherentbuffer 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.