Populate VkDrawIndexedIndirectCommand in Compute GLSL

Populating a VkDrawIndexedIndirectCommand buffer via a GLSL compute shader allows modern rendering pipelines to perform GPU-driven culling, dynamic Level of Detail (LOD) selection, and batching without CPU intervention. This article explains how to define the corresponding Vulkan indirect command structure in GLSL, configure the storage buffer, write command parameters directly in shader code, and synchronize the results for subsequent indirect draw calls.

The Command Structure Layout

In the Vulkan API, VkDrawIndexedIndirectCommand is defined with specific 32-bit integer fields:

typedef struct VkDrawIndexedIndirectCommand {
    uint32_t indexCount;
    uint32_t instanceCount;
    uint32_t firstIndex;
    int32_t  vertexOffset;
    uint32_t firstInstance;
} VkDrawIndexedIndirectCommand;

To mirror this layout safely in GLSL, you must use the std430 layout qualifier. The std430 alignment rules ensure that a structure composed of four-byte scalars (uint and int) remains packed tightly with 4-byte alignment, yielding a total struct size of 20 bytes:

struct DrawIndexedIndirectCommand {
    uint indexCount;
    uint instanceCount;
    uint firstIndex;
    int  vertexOffset;
    uint firstInstance;
};

Setting Up the Buffer

The destination Vulkan buffer must be created with both VK_BUFFER_USAGE_STORAGE_BUFFER_BIT (to allow compute shader writes) and VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT (so it can be consumed by vkCmdDrawIndexedIndirect).

In your compute shader, bind the buffer as a writable Shader Storage Buffer Object (SSBO):

layout(std430, set = 0, binding = 0) buffer IndirectDrawBuffer {
    DrawIndexedIndirectCommand commands[];
};

If multiple threads dynamically append instances or visible meshes into an array of draw calls, you will typically pair this buffer with an atomic counter:

layout(std430, set = 0, binding = 1) buffer CommandCountBuffer {
    uint drawCount;
};

Populating the Fields in Shader Code

The fields can be populated using two primary paradigms: culling instances within a single draw call or generating dynamic draw calls.

1. Modifying instanceCount for Dynamic Instancing

When drawing a known mesh where instances are filtered (e.g., frustum or occlusion culling), the command parameters are partially static, while instanceCount is incremented atomically:

// Pre-initialize commands on CPU or in a reset pass:
// commands[meshID].indexCount = meshIndexCount;
// commands[meshID].instanceCount = 0;
// commands[meshID].firstIndex = meshFirstIndex;
// commands[meshID].vertexOffset = meshVertexOffset;
// commands[meshID].firstInstance = meshBaseInstance;

// Compute shader execution (one invocation per instance):
if (isInstanceVisible(instanceIndex)) {
    atomicAdd(commands[meshID].instanceCount, 1);
}

2. Generating Entire Draw Commands

When building dynamic command lists from scratch, a thread that identifies a valid draw item reserves a command slot and writes all fields directly:

if (isMeshVisible) {
    uint commandIndex = atomicAdd(drawCount, 1);
    
    commands[commandIndex].indexCount    = meshData.indexCount;
    commands[commandIndex].instanceCount = 1;
    commands[commandIndex].firstIndex    = meshData.indexOffset;
    commands[commandIndex].vertexOffset  = meshData.vertexOffset;
    commands[commandIndex].firstInstance = instanceID;
}

Synchronization and Execution

Before reading the generated commands in a draw pass, a pipeline barrier must be inserted using vkCmdPipelineBarrier2 or vkCmdPipelineBarrier.

The synchronization must define:

  • Source Stage: VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT (or VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT)
  • Source Access Mask: VK_ACCESS_SHADER_WRITE_BIT (or VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT)
  • Destination Stage: VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT (or VK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT)
  • Destination Access Mask: VK_ACCESS_INDIRECT_COMMAND_READ_BIT (or VK_ACCESS_2_INDIRECT_COMMAND_READ_BIT)

Once the barrier completes, the buffer is dispatched using vkCmdDrawIndexedIndirect or vkCmdDrawIndexedIndirectCount, completely bypassing CPU synchronization.