Single Pass Downsampling for GLSL Mipmaps

Generating mipmap chains traditionally requires multiple compute or render passes with pipeline barriers inserted between every downsampled level. The Single Pass Downsampler (SPD) algorithm—originally developed by AMD as part of FidelityFX and widely implemented in modern GLSL compute pipelines—solves this bottleneck by constructing an entire mipmap pyramid within a single compute shader dispatch. By leveraging atomic counters in shared storage buffers alongside workgroup shared memory, SPD synchronizes workgroups dynamically on the GPU to process successive mip levels without returning control to the CPU host or requiring multiple dispatch calls.

The Multi-Pass Bottleneck

In standard mipmap generation, generating level \(N+1\) requires level \(N\) to be fully written and synchronized. On graphics APIs like Vulkan or OpenGL, this typically involves:

  1. Binding level \(N\) as a texture input and level \(N+1\) as a framebuffer/storage image.
  2. Executing a dispatch or draw call.
  3. Inserting an execution and memory barrier.
  4. Repeating this process for every level down to \(1\times1\).

This approach introduces significant CPU driver overhead and forces the GPU pipeline to drain repeatedly, leaving compute units underutilized during the final, lower-resolution mip levels.

The Single Pass Downsampler (SPD) Architecture

The SPD algorithm circumvents separate dispatches by tracking completion dynamically across workgroups using global atomic operations. The process operates in two primary execution phases:

1. Local Reduction and Tile Processing

Each compute workgroup is assigned a distinct region of the base image (Mip 0), typically a \(64 \times 64\) texel tile.

  • The workgroup loads texels from Mip 0 into workgroup shared memory (shared variables in GLSL).
  • Using parallel reduction and workgroup barriers (barrier() and memoryBarrierShared()), the workgroup downsamples the tile locally to produce Mips 1 through 5.
  • Each computed level is written to its respective storage image (image2D) using imageStore.

2. Global Atomic Synchronization

Once a workgroup finishes processing its local tile up to Mip 5, it cannot proceed to Mip 6 independently because Mip 6 requires input from multiple adjacent workgroups.

To resolve this dependency:

  • The workgroup issues a memoryBarrierImage() to ensure its image writes are globally visible.
  • The workgroup then executes an atomic increment (atomicAdd) on a global counter allocated in a Shader Storage Buffer Object (SSBO).
  • All workgroups except the last one to finish terminate their execution.
  • The final workgroup—identified when the atomic counter reaches the expected number of completed tiles—loads the downsampled data from Mip 5 across all previous tiles into its shared memory.
  • This surviving workgroup completes the remaining levels (Mip 6 through the \(1\times1\) tail) alone in the same dispatch.

GLSL Implementation Requirements

Implementing SPD in GLSL requires specific layout qualifiers and synchronization primitives:

  • Storage Images: Output mip levels are bound as an array of storage images or individual image bindings using layout(binding = ..., [format]) writeonly uniform image2D u_Mips[].

  • Atomic Counter Storage: A shared SSBO holds the atomic counter array:

    layout(std430, binding = 1) buffer CounterBuffer {
        uint workgroupCounter[];
    };
  • Memory Coherence: Ensuring that image writes from early workgroups are visible to the final workgroup requires matching memory qualifiers (such as coherent on storage images if supported, or invoking memoryBarrierImage() prior to updating the atomic counter).

  • Workgroup Size: Thread configurations are commonly set to 256 threads (layout(local_size_x = 256) in;), enabling efficient downsampling patterns for \(64 \times 64\) pixel inputs using \(2 \times 2\) or \(4 \times 4\) quad-reductions.

Performance Benefits

By consolidating the mip chain generation into a single GPU invocation, the SPD algorithm achieves several performance benefits:

  • Elimination of Host Overhead: Only a single draw or dispatch command is recorded into the command buffer.
  • Reduced Pipeline Stalls: Eliminates intermediate pipeline barriers between mip levels.
  • Bandwidth Conservation: Higher mip levels are downsampled directly through fast LDS (Local Data Share/shared memory) rather than round-tripping through VRAM repeatedly.