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 to true. This replaces the need for atomic counters when calculating population counts.
  • subgroupBallotInclusiveBitCount(uvec4 ballot): Returns the number of true lanes 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 of true lanes strictly before the current invocation's ID, serving as an index assignment mechanism for writing compact output arrays.
  • subgroupBallotFindLSB(uvec4 ballot) and subgroupBallotFindMSB(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.