How GLSL gl_LastFragData Reads Framebuffer Colors

In programmable graphics pipelines, reading previously written framebuffer values during the same rendering pass is typically restricted to prevent race conditions. The gl_LastFragData built-in variable resolves this through the GLSL framebuffer fetch extension, allowing fragment shaders to read the current values of bound render targets directly. This article explains the underlying mechanics of gl_LastFragData, how hardware executes these reads without pipeline hazards, and how tile-based rendering architectures optimize the process.

The Read-After-Write Pipeline Challenge

Standard graphics pipelines enforce a strict separation between reading and writing render targets. Typically, a texture cannot be bound as both an input sampler and a render target attachment simultaneously. Doing so produces a Read-After-Write (RAW) hazard, as fragments executing concurrently across multiple shader cores could read undefined, partially written, or out-of-order data.

Historically, implementing operations like custom blending, programmable color blending, or deferred shading required "ping-ponging" between two separate textures across multiple render passes. This approach incurred significant bandwidth overhead, requiring data to be flushed to main system memory (VRAM) and re-read in subsequent passes.

How gl_LastFragData Works

The framebuffer fetch mechanism is exposed through extensions such as GL_EXT_shader_framebuffer_fetch, GL_ARM_shader_framebuffer_fetch, or GL_NV_shader_framebuffer_fetch. When enabled in the fragment shader, it defines the input array gl_LastFragData[]:

#extension GL_EXT_shader_framebuffer_fetch : enable

void main() {
    // Read the existing color in the current fragment's pixel location
    vec4 previousColor = gl_LastFragData[0];

    // Compute a custom blend or effect
    gl_FragColor = previousColor * vec4(1.0, 0.5, 0.2, 1.0);
}

Each index in gl_LastFragData[n] corresponds to the color attachment at index n of the currently bound framebuffer object (FBO).

1. Per-Fragment Primitive Ordering

To prevent race conditions, the GPU runtime and driver enforce strict fragment ordering at the pixel level. While different pixels execute concurrently across independent compute units, overlapping fragments mapped to the exact same pixel coordinates are serialized based on the original API primitive submission order.

When a fragment accesses gl_LastFragData, the pipeline ensures that all earlier primitives touching that pixel coordinate have already committed their outputs to that coordinate's storage location before the current fragment begins its read operation.

2. Tile-Based Architectures (TBDR)

Framebuffer fetch is natively optimized for Tile-Based Deferred Rendering (TBDR) GPUs, which dominate mobile and embedded hardware (such as ARM Mali, Qualcomm Adreno, and Apple Silicon).

In TBDR architectures:

  • The screen is divided into small tiles (e.g., 16x16 pixels).
  • Geometry is binned per tile, and rendering is executed entirely within dedicated, ultra-fast on-chip SRAM (tile memory).
  • Pixel data remains inside the on-chip tile buffer until the entire tile is completed.

When gl_LastFragData is referenced on TBDR hardware, it does not query external VRAM or perform standard texture sampling operations. Instead, it reads the current value directly from the on-chip tile buffer at zero external memory bandwidth cost. This makes programmable blending virtually as performant as fixed-function hardware blending.

3. Immediate-Mode Architectures

On desktop Immediate-Mode Rendering (IMR) GPUs, memory operations are handled differently. Implementations such as GL_NV_shader_framebuffer_fetch rely on internal L1/L2 cache coherency and hardware interlocking mechanisms (such as rasterizer order views or fragment shader interlocks). These mechanisms pause the execution of overlapping fragments until the preceding fragment finishes writing to the cache line, maintaining coherency without flushing to global memory.

Modern Evolution: Subpass Inputs

The concepts pioneered by gl_LastFragData formed the foundation for modern graphics API architectures:

  • In Vulkan, this behavior is standardized as Subpass Inputs read via subpassLoad().
  • In Direct3D 11.3 and 12, it is handled via Rasterizer Ordered Views (ROVs).
  • In Metal, it is represented by passing render target textures as inputs with the [[color(n)]] attribute.

In all cases, the underlying principle remains the same: granting read access to current framebuffer targets by enforcing fragment serialization and leveraging on-chip local storage.