GLSL Atomic Counters for Per-Pixel Linked Lists

Atomic counters in OpenGL Shading Language (GLSL) act as thread-safe, global dynamic allocators that grant unique buffer indices to concurrent fragment shader threads. In the context of per-pixel linked lists—a common technique used for Order-Independent Transparency (OIT)—they allow arbitrary numbers of overlapping fragments to claim storage space within a shared linear buffer without data races or memory corruption.

The Challenge of Per-Pixel Linked Lists

Rendering transparent geometry in arbitrary order requires recording every fragment that covers a specific screen coordinate, sorting those fragments by depth, and blending them in back-to-front order. Because the number of overlapping surfaces per pixel cannot be known ahead of time, modern GPU pipelines use a two-part data structure: a 2D screen-sized head-pointer texture and a large, flat node buffer (implemented via Shader Storage Buffer Objects, or SSBOs).

Thousands of fragment shader invocations execute simultaneously across GPU compute units. When multiple fragments need to write to the shared node buffer at the same time, allocating memory without synchronization causes race conditions, resulting in threads overwriting each other’s data and corrupting the linked lists.

How Atomic Counters Allocate Nodes

An atomic counter solves this concurrency problem by functioning as a high-performance "bump allocator." It maintains a single, globally synchronized unsigned integer value stored in fast on-chip or device memory.

The allocation workflow operates as follows:

  1. Requesting a Unique Index: When a fragment shader runs, it executes the GLSL built-in function atomicCounterIncrement(counter) (or atomicAdd on a buffer variable). This instruction reads the counter's current value and increments it by one as a single, indivisible hardware operation.
  2. Guaranteed Collision-Free Slot: Even if hundreds of threads call the increment function in the exact same clock cycle, the GPU hardware serializes the increments. Each thread receives a completely distinct integer representing an available index in the global node buffer.
  3. Writing Node Payload: The fragment uses this returned index to write its attributes—such as color, alpha, depth, and a pointer to the previous node—into the node buffer without any risk of another fragment overwriting its data.
  4. Updating the Head Pointer: The fragment then links its newly written node to the head of the corresponding pixel's linked list using imageAtomicExchange on the 2D head-pointer texture. The old head index is preserved inside the new node as the next pointer, while the new node's index becomes the current head.

Memory Safety and Overflow

Because atomic counters provide a monotonically increasing index, they make memory bounds checking trivial. A fragment shader can evaluate the index returned by the atomic counter against the maximum capacity of the node buffer:

uint nodeIndex = atomicCounterIncrement(nodeCounter);

if (nodeIndex < MAX_NODES) {
    // Write fragment payload and update head pointer
}

If nodeIndex exceeds MAX_NODES, the fragment can cleanly discard its payload, preventing out-of-bounds memory writes that could crash the GPU driver or induce undefined rendering artifacts.

Conclusion

In GLSL per-pixel linked lists, atomic counters serve as the critical synchronization primitive. By handling dynamic index generation atomically at the hardware level, they turn a flat, pre-allocated GPU buffer into a safe, lock-free memory pool capable of tracking complex overlapping surface geometry in real time.