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.