Custom Framebuffer Blending With GLSL Interlock
Custom programmable blending in GLSL fragment shaders frequently
suffers from read-modify-write data hazards when multiple overlapping
fragments execute concurrently on the GPU. The
GL_ARB_fragment_shader_interlock extension addresses this
issue through the beginInvocationInterlockARB() and
endInvocationInterlockARB() intrinsics, which establish a
synchronized critical section per fragment coordinate. This article
details the mechanics behind how invocation interlocks prevent race
conditions, enforce primitive submission ordering, and facilitate
deterministic custom framebuffer blending without hardware-level data
corruption.
The Read-Modify-Write Concurrency Hazard
Standard fixed-function hardware blending operates late in the graphics pipeline at the Render Output (ROP) stage, which has built-in atomic arbitration. However, when implementing programmable blending, order-independent transparency (OIT), or custom accumulation buffers, fragment shaders must read from an image or buffer, compute an arbitrary blend operation, and write the result back into storage.
Because modern GPUs execute hundreds of fragment invocations
concurrently across multiple compute units, fragments belonging to
overlapping primitives often target identical (x, y) pixel
coordinates at the same time. This concurrency produces a classic
read-modify-write race condition:
- Fragment A reads the pixel color from the framebuffer.
- Fragment B reads the same original pixel color before Fragment A writes its update.
- Both fragments calculate their custom blend equations locally.
- Both fragments write back their results, causing one fragment's contribution to overwrite and permanently discard the other's.
Establishing the Critical Section
beginInvocationInterlockARB() prevents this hazard by
establishing a hardware-enforced critical section within the shader
invocation. Any instructions placed between
beginInvocationInterlockARB() and
endInvocationInterlockARB() are guaranteed to execute with
mutual exclusion relative to other fragments targeting the same spatial
coordinate:
#version 450
#extension GL_ARB_fragment_shader_interlock : require
layout(pixel_interlock_ordered) in;
layout(rgba8, binding = 0) uniform image2D framebufferImage;
void main() {
ivec2 pixelCoord = ivec2(gl_FragCoord.xy);
vec4 incomingColor = calculateFragmentColor();
// Critical section begins
beginInvocationInterlockARB();
vec4 currentColor = imageLoad(framebufferImage, pixelCoord);
vec4 blendedColor = customBlend(currentColor, incomingColor);
imageStore(framebufferImage, pixelCoord, blendedColor);
endInvocationInterlockARB();
// Critical section ends
}When a fragment invocation reaches
beginInvocationInterlockARB(), the GPU checks whether
another invocation is currently operating on that same pixel coordinate.
If an active invocation is inside its critical section, subsequent
arriving fragments targeting that pixel stall their execution until the
prior fragment calls endInvocationInterlockARB().
Enforcing Primitive Submission Order
Preventing overlapping writes is often insufficient for blending algorithms; non-commutative operations like standard over-operator alpha blending require operations to occur in the exact sequence specified by the CPU draw calls.
The interlock extension resolves this by supporting ordered
qualifiers, such as layout(pixel_interlock_ordered) in.
When ordered qualifiers are declared,
beginInvocationInterlockARB() ensures that overlapping
fragments enter the critical section strictly in API primitive
submission order.
Under the hood, this relies on GPU rasterization hardware
features—such as Rasterizer Order Views (ROVs) on Direct3D-compatible
hardware or Intel Pixel Sync. As primitives are rasterized, each
fragment inherits a sequence tag reflecting its submission order. The
interlock hardware logic evaluates these tags, pausing any fragment that
reaches beginInvocationInterlockARB() out of order until
all preceding overlapping fragments have completed and exited their
interlock blocks.
Scope and Granularity
The synchronization scope is governed by layout qualifiers configured in the shader:
pixel_interlock_ordered/pixel_interlock_unordered: Evaluates locks at whole-pixel granularity ((x, y)). All fragments covering the same pixel coordinate are serialized.sample_interlock_ordered/sample_interlock_unordered: Evaluates locks at multisample sample granularity ((x, y, sample_id)). This limits serialization only to fragments modifying the exact same sample within a multisampled render target (MSAA), enabling higher concurrency across neighboring samples.
Unordered variations eliminate the sequence tag verification, providing mutual exclusion without enforcing primitive submission order. This mode is optimal for commutative operations, such as additive blending or building spatial hash maps, where execution order does not affect the final mathematical result.
Performance Considerations
Because beginInvocationInterlockARB() stalls GPU threads
that map to identical coordinates, expansive critical sections can
degrade performance through thread divergence and pipeline
serialization.
To maximize throughput, all memory accesses, texture lookups,
lighting evaluations, and discard tests that do not depend on the target
framebuffer data should be computed before invoking
beginInvocationInterlockARB(). Confining the interlock
solely to the final image load, math operation, and image store
minimizes thread residency inside the critical section, freeing
execution units to resume concurrent execution across the rest of the
wavefront.