GLSL Compute Shaders for Indirect Drawing

This article explores the purpose and benefits of generating indirect drawing parameters using GLSL compute shaders in modern graphics pipelines. Indirect rendering shifts draw call preparation from the CPU to the GPU, allowing geometry processing, visibility culling, and instance count determination to happen entirely on the graphics hardware. By offloading these tasks to compute shaders, developers can eliminate CPU-GPU synchronization bottlenecks, minimize API call overhead, and build high-performance, GPU-driven rendering architectures.

Understanding Indirect Drawing

In traditional OpenGL rendering, the CPU determines what needs to be rendered and specifies draw parameters—such as vertex count, instance count, and offset indices—via functions like glDrawElements or glDrawArraysInstanced. This approach requires the CPU to manage visibility and issue separate commands for every object, creating significant driver overhead.

Indirect drawing via functions such as glDrawElementsIndirect or glMultiDrawElementsIndirect changes this paradigm. Instead of reading drawing parameters directly from CPU-side arguments, the GPU reads them directly from a buffer object—specifically the GL_DRAW_INDIRECT_BUFFER. The buffer stores the exact binary layout required by the draw command (such as count, instanceCount, firstIndex, baseVertex, and baseInstance).

The Role of GLSL Compute Shaders

GLSL compute shaders provide general-purpose parallel computing directly on the GPU. When paired with indirect drawing buffers, compute shaders take over the responsibility of evaluating scene data and writing the required drawing parameters into the buffer before the graphics pipeline consumes them.

The primary purposes and advantages of this technique include:

1. Enabling Fully GPU-Driven Pipelines

The primary goal of compute-generated indirect parameters is to eliminate the CPU from the per-frame rendering decision loop. The CPU issues a static set of dispatches and draw calls, while compute shaders dynamically determine what gets drawn. This decouples scene complexity from CPU performance, preventing draw-call-bound frame rate drops.

2. GPU-Based Visibility Culling

Compute shaders can evaluate hundreds of thousands of bounding volumes in parallel against camera frustum planes, depth pyramids (hierarchical Z-buffering for occlusion culling), or distance thresholds.

Instead of reading visibility results back to the CPU—which forces expensive pipeline stalls—the compute shader:

  • Tests whether an object is visible.
  • Uses atomic operations to increment a visible instance counter.
  • Writes the visibility data or updates the instanceCount field in the indirect command structure directly in the indirect buffer.

If an entire mesh or group of instances is culled, the compute shader simply writes an instanceCount of zero, causing the GPU to bypass the draw call with virtually no processing cost.

3. Dynamic Level of Detail (LOD) Selection

For massive open worlds, compute shaders can compute the distance between individual objects and the camera to select an appropriate LOD mesh. The shader then appends the instance index directly to the indirect command buffer corresponding to that specific LOD group. This ensures that only relevant geometry is passed down the pipeline without any CPU intervention.

4. Efficient Particle and Simulation Rendering

When running physical simulations (such as fluids, debris, or particle systems) entirely in compute shaders, particles are continuously created, killed, or sorted. Generating indirect parameters allows the compute shader to update the exact number of active particles to be rendered into the indirect buffer's count or instanceCount. The subsequent draw call renders precisely the living particles without needing a CPU readback.

5. Multi-Draw Compaction

With multi-draw indirect commands (glMultiDrawElementsIndirect), a single API call can execute thousands of distinct draw commands. Compute shaders can act as a compaction filter, assembling tightly packed arrays of valid draw commands from a larger set of potentially visible objects, optimizing vertex fetch and state-machine throughput on the hardware.