Understanding nonuniformEXT in GLSL Texture Arrays
This article provides an overview of the nonuniformEXT
qualifier in GLSL, explaining its role when indexing arrays of textures
or descriptors. You will learn why GPUs require this keyword for
dynamically divergent texture indexing within a subgroup, what problems
occur when it is omitted, and how modern graphics drivers generate code
differently to handle per-thread resource access safely.
The Purpose of
nonuniformEXT
The nonuniformEXT keyword explicitly informs the
compiler and GPU that an array index used to access a resource (such as
a texture or buffer descriptor) can vary across invocations within the
same execution wave or subgroup.
In modern APIs like Vulkan and Direct3D 12, dynamic resource
indexing—commonly used in "bindless" rendering architectures—allows
shaders to select different textures from an array at runtime. However,
GPUs optimize descriptor loads under the assumption that all threads in
a wave will access the exact same descriptor. When that assumption is
false, nonuniformEXT tells the compiler to emit special
instructions to handle divergent execution paths safely.
Hardware Execution and the Uniformity Requirement
Modern GPUs run shader threads in lockstep groups, known as warps on NVIDIA hardware or wavefronts on AMD hardware. By default, resource descriptors (such as sampler and texture pointers) are fetched using scalar units designed to load a single descriptor once for the entire warp:
- Dynamically Uniform: A value is dynamically uniform if it evaluates to the same value across all active invocations within an execution wave. Under this condition, the GPU can load a single descriptor into shared registers, saving memory bandwidth and execution cycles.
- Non-Uniform: A value is non-uniform if different threads in the same wave compute different values. For example, if two adjacent pixels rendered simultaneously use different materials, their texture array indices will diverge.
Without nonuniformEXT, passing a divergent index into a
texture array violates the dynamic uniformity rule. This results in
undefined behavior, which often manifests as screen tearing, rendering
the wrong texture, GPU hangs, or memory access faults.
What
nonuniformEXT Does Under the Hood
When an index is marked with nonuniformEXT, the compiler
cannot assume scalar access. Instead, it generates divergence-handling
code to read the descriptors properly for each thread.
Depending on the underlying GPU architecture, the compiler typically resolves divergence in one of two ways:
- Scalar Looping (Waterfall Loop): The GPU creates an internal loop over the unique indices present in the wave. In each iteration, it runs the descriptor load only for the subset of threads sharing that specific index, repeating until all threads have completed their respective texture fetches.
- Vector Descriptor Loads: If the hardware supports reading descriptors directly via vector registers, the compiler utilizes vector instructions to load distinct descriptors per thread without requiring a scalar loop.
How to Use
nonuniformEXT in GLSL
To use the qualifier, you must enable the
GL_EXT_nonuniform_qualifier extension (or target a modern
Vulkan environment where it is supported by default) and wrap the
dynamic index where the resource array is sampled:
#version 450
#extension GL_EXT_nonuniform_qualifier : require
layout(set = 0, binding = 0) uniform sampler2D textures[];
layout(location = 0) in flat int inTextureIndex;
layout(location = 1) in vec2 inUV;
layout(location = 0) out vec4 outColor;
void main()
{
// Wrap the dynamic index with nonuniformEXT
outColor = texture(textures[nonuniformEXT(inTextureIndex)], inUV);
}Performance Considerations
While nonuniformEXT makes per-thread resource indexing
safe, it introduces a performance cost compared to uniform descriptor
access. If adjacent pixels routinely fetch from different textures, the
resulting divergent loops will serialize execution, reducing SIMD
efficiency. To maintain high performance, group draw calls or mesh
primitives by material wherever possible to maximize uniformity across
waves.