How Is gl_NumWorkGroups Used in GLSL?

In OpenGL Shading Language (GLSL) compute shaders, gl_NumWorkGroups is a built-in variable that provides the total dimensions of the work group grid dispatched by the host application. This article covers the definition of gl_NumWorkGroups, its relationship to host dispatch commands, how it interacts with other built-in compute variables, and practical implementation patterns for boundary checks and global indexing.

What is gl_NumWorkGroups?

In GLSL compute shaders, gl_NumWorkGroups is declared implicitly as a uvec3:

in uvec3 gl_NumWorkGroups;

It contains the number of work groups passed to the compute dispatch call from the CPU application side (such as glDispatchCompute(num_groups_x, num_groups_y, num_groups_z) in OpenGL). Each component (x, y, and z) corresponds directly to the parameters supplied in that call.

Relationship with Other Compute Built-ins

To understand how gl_NumWorkGroups is utilized, it helps to understand how it fits within the GLSL compute hierarchy:

While gl_GlobalInvocationID gives the current thread's position, gl_NumWorkGroups represents the upper boundary of the active group grid.

Primary Use Cases

1. Calculating Total Grid Dimensions

When a compute shader needs to know the total global invocation count across all dimensions dynamically, it multiplies gl_NumWorkGroups by gl_WorkGroupSize:

uvec3 totalInvocations = gl_NumWorkGroups * gl_WorkGroupSize;

This is useful when the dispatch size varies at runtime and the shader needs to adapt its computations or normalization factors to the full dataset size without hardcoding uniform values.

2. Flattening Multi-Dimensional Indices

When flattening 2D or 3D grids into a 1D linear buffer, gl_NumWorkGroups helps compute stride lengths and total capacity:

#version 430 core

layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;

layout(std430, binding = 0) buffer OutputBuffer {
    float data[];
};

void main() {
    uvec3 totalSize = gl_NumWorkGroups * gl_WorkGroupSize;
    
    // Compute 1D linear index from 2D coordinates
    uint linearIndex = gl_GlobalInvocationID.y * totalSize.x + gl_GlobalInvocationID.x;
    
    data[linearIndex] = float(linearIndex);
}

3. Grid-Stride Loops and Work Distribution

In scenarios where the number of data elements exceeds the total number of launched threads, compute shaders use a grid-stride loop. In this pattern, each thread processes multiple elements spaced apart by the total grid size:

#version 430 core

layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in;

layout(std430, binding = 0) buffer DataBuffer {
    uint elements[];
};

uniform uint totalElementCount;

void main() {
    uint stride = gl_NumWorkGroups.x * gl_WorkGroupSize.x;
    uint index = gl_GlobalInvocationID.x;

    while (index < totalElementCount) {
        elements[index] *= 2;
        index += stride;
    }
}

Utilizing gl_NumWorkGroups directly inside the shader allows compute pipelines to remain flexible and decoupled from fixed buffer dimensions, adapting automatically to whatever dispatch dimensions the host application submits.