Skip to content

Bindless rendering

Sample 08

Samples/Bindless ยท builds on basic rendering and uniform arrays.

With one descriptor set per object, drawing many objects means binding many sets. Bindless rendering turns this around: all objects live in one array that the shader indexes itself, and nothing has to be rebound between draw calls. The sample draws 100,000 instances of the same object, each with its own transform, color and rotation axis.

Unbounded arrays

The instance data is a runtime array, an array whose size the shader does not know:

struct InstanceData
{
    float4x4 Transform;
    float4 Color;
    float4 Axis;
};

ConstantBuffer<CameraData> camera  : register(b0, space0);
ConstantBuffer<DrawData> drawData  : register(b0, space1);
StructuredBuffer<InstanceData> instanceBuffer[] : register(t0, space2);

The vertex shader picks its instance by the index the GPU provides for each instance of a draw call:

VertexData main(in VertexInput input, uint id : SV_InstanceID)
{
    InstanceData instance = instanceBuffer[NonUniformResourceIndex(id)].Load(0);
    // ... transform the vertex with instance.Transform ...
}

NonUniformResourceIndex tells the GPU that neighboring threads may read different elements, which it must know to access the array correctly.

Since the size is missing from the shader, reflection cannot determine it either, so the pipeline layout is given a hint:

.layout(shaderProgram->reflectPipelineLayout(std::array { PipelineBindingHint::runtimeArray(DescriptorSets::InstanceData, 0u, NUM_INSTANCES) }))

One descriptor for everything

The buffer holds all instances, and a single descriptor set covers the whole array:

auto& instanceBindingLayout = geometryPipeline.layout()->descriptorSet(DescriptorSets::InstanceData);
auto instanceBuffer = m_device->factory().createBuffer("Instance Buffer", instanceBindingLayout, 0, ResourceHeap::Resource, sizeof(InstanceBuffer), NUM_INSTANCES);
auto instanceBinding = instanceBindingLayout.allocate(NUM_INSTANCES, { { 0, *instanceBuffer } });
commandBuffer->transfer(static_cast<const void*>(&instanceData), sizeof(instanceData), *instanceBuffer, 0, NUM_INSTANCES);

Instanced drawing

All instances are drawn with one call, whose second argument is the number of instances:

commandBuffer->bind({ &cameraBindings, &drawDataBindings, &instanceBindings });
commandBuffer->bind(vertexBuffer);
commandBuffer->bind(indexBuffer);
commandBuffer->drawIndexed(indexBuffer.elements(), NUM_INSTANCES);

The animation is computed in the shader, so the instance data stays unchanged after the upload. Only the current time is written per frame, in a small per-frame buffer:

drawData.Time = time;
drawData.Speed = glm::radians(42.0f);
drawDataBuffer.map(static_cast<const void*>(&drawData), sizeof(drawData), backBuffer);

Each instance rotates around its own axis, so the shader builds the rotation matrix from instance.Axis and the elapsed time.

The other way around

Shader model 6.6 offers an alternative that needs no array binding at all. The dynamic descriptors tutorial shows the same sample with ResourceDescriptorHeap.