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:

  1. Fragment A reads the pixel color from the framebuffer.
  2. Fragment B reads the same original pixel color before Fragment A writes its update.
  3. Both fragments calculate their custom blend equations locally.
  4. 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.