GLSL subgroupBallot: Generating Lane Bitmasks
In GLSL, subgroupBallot provides a hardware-accelerated
mechanism for cross-lane communication by evaluating a boolean
expression across all active invocations within a subgroup and
aggregating the results into a compact bitmask. This article explains
how subgroupBallot functions, how it constructs this lane
bitmask, and how developers utilize it to eliminate branch divergence,
perform stream compaction, and inspect execution states without relying
on shared memory or atomic operations.
Understanding the Role of subgroupBallot
A subgroup (often referred to as a warp on NVIDIA GPUs or a wavefront on AMD GPUs) consists of a collection of invocations that execute concurrently in lockstep. During execution, individual invocations often evaluate conditions independently, which can lead to divergent control flow.
The primary role of subgroupBallot(bool value) is to
collect the boolean result evaluated by each invocation in the subgroup
and pack those values into a single uvec4 bitmask. If
invocation \(N\) evaluates the provided
expression as true, the \(N\)-th bit of the resulting vector is set
to 1; if false or inactive, it is set to
0.
Bitmask Representation and Indexing
The result returned by subgroupBallot is a
four-component unsigned 32-bit integer vector (uvec4),
providing up to 128 bits:
- Bits 0 to 31: Stored in
.x, representing invocations 0 through 31. - Bits 32 to 63: Stored in
.y, representing invocations 32 through 63. - Bits 64 to 95: Stored in
.z, representing invocations 64 through 95. - Bits 96 to 127: Stored in
.w, representing invocations 96 through 127.
Because hardware subgroups are typically sized between 16 and 64
lanes, only the lower components (usually .x or
.x and .y) are populated, while the remaining
bits default to zero. The bit corresponding to an individual lane
matches its gl_SubgroupInvocationID.
Querying and Operating on the Bitmask
Once the bitmask is generated, GLSL provides specialized intrinsic functions to query the mask directly:
subgroupBallotBitCount(uvec4 ballot): Returns the total number of lanes where the condition evaluated totrue. This replaces the need for atomic counters when calculating population counts.subgroupBallotInclusiveBitCount(uvec4 ballot): Returns the number oftruelanes up to and including the current invocation's ID, which is essential for parallel prefix sums and stream compaction.subgroupBallotExclusiveBitCount(uvec4 ballot): Returns the number oftruelanes strictly before the current invocation's ID, serving as an index assignment mechanism for writing compact output arrays.subgroupBallotFindLSB(uvec4 ballot)andsubgroupBallotFindMSB(uvec4 ballot): Identify the lowest or highest lane index that satisfied the condition, enabling leader-election patterns.
Practical Applications
Stream Compaction
In filtering tasks, such as culling bounding boxes or collecting
collision data, threads determine whether their element is valid.
Instead of writing valid elements using slow global atomic memory
counters, threads execute subgroupBallot(isValid).
Invocations then compute their write offsets using
subgroupBallotExclusiveBitCount, writing valid items
contiguously to memory with zero memory contention.
Dynamic Branch Convergence
When an expensive branch is conditional on dynamic data,
subgroupBallot allows the subgroup to check if any
invocation needs to execute the path:
uvec4 activeMask = subgroupBallot(needsProcessing);
if (subgroupBallotBitCount(activeMask) > 0) {
// Execute complex code path only if at least one lane requires it
}Ballot Bit Testing
An invocation can inspect whether another specific lane satisfied the
condition by checking the corresponding bit in the bitmask using helper
functions like
subgroupBallotBitExtract(uvec4 ballot, uint index).
Performance Advantages
By shifting communication from local/shared memory
(shared memory in compute shaders) to register-level
hardware interconnects, subgroupBallot eliminates latency,
avoids memory barrier stalls (barrier()), and bypasses
atomic serializations. This makes it a fundamental tool for optimizing
data-parallel compute passes in modern graphics and compute
pipelines.