Understanding subgroupBallotInclusiveBitCount in GLSL

This article provides a direct explanation of the subgroupBallotInclusiveBitCount function in GLSL, covering its exact computation, how it handles invocation masks, and its role in shader programming. In modern graphics and compute pipelines, subgroup operations allow threads within the same SIMD unit to communicate efficiently without accessing shared memory. This guide focuses on how subgroupBallotInclusiveBitCount calculates population counts up to and including the calling invocation, demonstrating its behavior and common use cases like prefix sums and parallel stream compaction.


What subgroupBallotInclusiveBitCount Computes

In GLSL (specifically via the GL_KHR_shader_subgroup_ballot or Vulkan subgroup extensions), subgroupBallotInclusiveBitCount(uvec4 value) computes the number of bits set to 1 in the provided value bitmask from bit index 0 up to and including the current invocation's index (gl_SubgroupInvocationID).

The ballot bitmask is represented as a 128-bit vector (uvec4), where each bit corresponds to an invocation in the subgroup:

  • Bit 0 corresponds to invocation index 0.
  • Bit n corresponds to invocation index n.

The Computation Process

When an invocation calls subgroupBallotInclusiveBitCount(value):

  1. Mask Generation: The hardware constructs an internal mask where all bit indices from 0 to gl_SubgroupInvocationID are set to 1, and all bits with indices greater than gl_SubgroupInvocationID are set to 0.
  2. Bitwise AND: This internal mask is logically ANDed with the input uvec4 value.
  3. Population Count (popcount): The function returns the total number of set bits (1s) remaining after the masking operation.

Because this operation is inclusive, if the current invocation's own bit in value is 1, it is counted in the final result.

Inclusive vs. Exclusive Comparison

To understand the inclusive count clearly, consider the difference between the two standard bit count functions:

  • subgroupBallotInclusiveBitCount(value): Counts bits set to 1 in the range [0, gl_SubgroupInvocationID].
  • subgroupBallotExclusiveBitCount(value): Counts bits set to 1 in the range [0, gl_SubgroupInvocationID - 1].

If a ballot mask has bits set at indices 0, 2, and 3:

Invocation ID (gl_SubgroupInvocationID) Bit in Ballot Inclusive Count Exclusive Count
0 1 1 0
1 0 1 1
2 1 2 1
3 1 3 2

Typical Use Case: Stream Compaction

The primary use case for subgroupBallotInclusiveBitCount is determining the target index when compacting data across a subgroup:

// Evaluate a condition across all invocations
bool condition = localData > threshold;

// Get a bitmask of all invocations where condition is true
uvec4 ballot = subgroupBallot(condition);

if (condition) {
    // Determine the 1-based output rank within this subgroup
    uint rank = subgroupBallotInclusiveBitCount(ballot);

    // Convert to a 0-based index to write out compacted data
    uint outputIndex = baseOffset + (rank - 1);
    outputBuffer[outputIndex] = localData;
}

Using the inclusive variant provides the 1-based rank of the current invocation among active threads, which directly informs how many total elements will be written by threads up to that point.