GPU Particle Simulation and Dead-List Recycling in GLSL
This article details the compute shader architecture used to manage dynamic GPU particle simulation entirely on the graphics hardware. By using Shader Storage Buffer Objects (SSBOs), atomic counters, and an index-based dead-list recycling algorithm, developers can achieve high-performance particle emission, numerical lifespan integration, and memory recycling in GLSL without costly CPU-to-GPU data transfers.
The Index List Architecture
Managing GPU particles without transferring state back to the CPU relies on an index-list architecture using GLSL Compute Shaders and Shader Storage Buffer Objects (SSBOs). Instead of allocating and freeing memory dynamically, the system pre-allocates a fixed-capacity pool of particle data along with two auxiliary index buffers:
- Particle Pool (Data Buffer): An array of structs holding physical attributes such as position, velocity, remaining lifetime, and color.
- Dead List (Free List Stack): An array of integers storing the indices of inactive particles available for allocation.
- Alive List: An array of integers storing the indices of currently active particles that require simulation and rendering.
- Counter Buffer: An SSBO containing atomic uint counters that track the count of items in the Dead List and the Alive List, as well as arguments for indirect dispatching and rendering.
Dynamic Emission via the Dead List
Emission runs as a compute shader pass before the simulation step. To spawn \(N\) new particles:
An atomic decrement is performed on the Dead List counter to claim available slots:
uint deadIndex = atomicAdd(deadCount, -1) - 1;The thread reads the particle index stored at
deadList[deadIndex].The emitter initializes the particle's state in the main Particle Pool using this index (setting position, initial velocity, maximum lifespan, and current age).
The thread pushes the initialized particle index into the current frame's Alive List and increments the Alive counter using
atomicAdd.
If the Dead List counter is zero or insufficient, emission requests are discarded by early-exiting the thread, preventing buffer overflow.
Lifespan Integration and Death Detection
The simulation phase operates over the Alive List generated by the previous frame or the emission stage. A compute shader is dispatched with a thread count matching the active particle count.
Each thread processes one active particle:
State Update: The thread reads the particle index from the Alive List, fetches its data from the Particle Pool, and updates its lifespan:
particle.life -= deltaTime;Kinematic Integration: If
particle.life > 0.0, numerical integration (typically Velocity Verlet or Semi-implicit Euler) updates position and velocity based on active forces like gravity and drag. The particle index is appended to a New Alive List for the upcoming render pass:uint writeIndex = atomicAdd(nextAliveCount, 1); nextAliveList[writeIndex] = particleIndex;Recycling (Dead-List Push): If
particle.life <= 0.0, the particle is marked inactive. The thread recycles the slot by atomically pushing the index back onto the Dead List:uint recycleIndex = atomicAdd(deadCount, 1); deadList[recycleIndex] = particleIndex;
Indirect Dispatch and Rendering
To eliminate CPU synchronization, the atomic counters updated during
the simulation pass are bound directly to an
IndirectDrawArgs buffer. Using OpenGL's
glDrawArraysIndirect or
glDrawElementsIndirect, the GPU uses the
nextAliveCount directly as the instance count or vertex
count for the rasterization stage. Once rendering completes, the roles
of the current and next alive lists are swapped, maintaining a fully
autonomous, zero-overhead simulation loop on the GPU.