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(orVK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT) - Source Access Mask:
VK_ACCESS_SHADER_WRITE_BIT(orVK_ACCESS_2_SHADER_STORAGE_WRITE_BIT) - Destination Stage:
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT(orVK_PIPELINE_STAGE_2_DRAW_INDIRECT_BIT) - Destination Access Mask:
VK_ACCESS_INDIRECT_COMMAND_READ_BIT(orVK_ACCESS_2_INDIRECT_COMMAND_READ_BIT)
Once the barrier completes, the buffer is dispatched using
vkCmdDrawIndexedIndirect or
vkCmdDrawIndexedIndirectCount, completely bypassing CPU
synchronization.