The Shader Binding Table (SBT) is a fundamental component in Vulkan ray tracing that serves as the “blueprint” for the ray tracing process. Unlike traditional rasterization where shaders are bound sequentially for different objects, ray tracing requires all shaders to be available simultaneously since rays can hit any surface in the scene at any time.
The nvvk::SBTGenerator
class is a helper utility that simplifies the complex process of creating and managing the SBT. It handles the intricate details of buffer creation, alignment calculations, and shader handle retrieval, making it easier to create the information that will be stored in the SBT buffer and retrieve the handles used by the ray tracer when calling vkCmdTraceRaysKHR
.
Here’s the minimal code to get an SBT working in just 5 steps:
// 1. Initialize the SBT generator
nvvk::SBTGenerator sbtGen;
sbtGen.init(device, rayProperties);
// 2. Calculate required buffer size
size_t bufferSize = sbtGen.calculateSBTBufferSize(rtPipeline, pipelineInfo);
// 3. Create the SBT buffer
nvvk::Buffer sbtBuffer;
allocator.createBuffer(sbtBuffer, bufferSize,
VK_BUFFER_USAGE_2_SHADER_BINDING_TABLE_BIT_KHR | VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT,
VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
VMA_ALLOCATION_CREATE_MAPPED_BIT,
sbtGen.getBufferAlignment());
// 4. Populate the SBT buffer with shader handles
sbtGen.populateSBTBuffer(sbtBuffer.address, bufferSize, sbtBuffer.mapping);
// 5. Use the SBT in ray tracing
vkCmdTraceRaysKHR(cmd,
&sbtGen.getSBTRegions().raygen, // Ray generation shader
&sbtGen.getSBTRegions().miss, // Miss shader
&sbtGen.getSBTRegions().hit, // Hit shader
&sbtGen.getSBTRegions().callable, // Callable shader (optional)
width, height, 1); // Dimensions
What this does:
Prerequisites:
pipelineInfo
) which was used to create the pipelinertPipeline
) with shader groupsallocator
) for buffer creationrayProperties
) from your physical deviceThis minimal setup handles all the complex alignment requirements and gives you a working SBT. See the sections below for advanced features like custom data, multiple shader groups, and performance optimization.
The SBT allows us to:
hitGroupId
in the TLASThe SBT consists of up to four arrays, each containing handles to shader groups:
---
config:
theme: 'neutral'
---
flowchart TB
subgraph RG["RayGen Group"]
RG1["Handle<br>(64 bytes)"]
end
subgraph MG["Miss Group"]
MG1["Handle<br>(64 bytes)"]
end
subgraph HG["Hit Group"]
HG1["Handle<br>(64 bytes)"]
HG2["Data<br>(variable)"]
end
subgraph CG["Callable Group"]
CG1["Handle<br>(64 bytes)"]
end
subgraph SBT["Shader Binding Table Buffer"]
RG
MG
HG
CG
end
RG --- MG
MG --- HG
HG --- CG
style RG1 fill:#FFFFFF
style MG1 fill:#FFFFFF
style HG1 fill:#FFFFFF
style HG2 fill:#FFFFFF
style CG1 fill:#FFFFFF
style RG fill:#FFCDD2
style MG fill:#BBDEFB
style HG fill:#C8E6C9
style CG fill:#E1BEE7
style SBT fill:#FFF
Note: While all group types can technically have user data attached, Hit groups are the primary use case for data attachment (e.g., material properties, per-instance data). RayGen, Miss, and Callable groups typically only contain shader handles.
Example of a raygen, miss and hit shader. (02_basic)
Example where there are two miss shaders. (05_shadow_miss)
What are “indices”?
In the context of SBT, “indices” refer to the position (index) of shader groups within the ray tracing pipeline. When you create a pipeline with multiple shader groups, each group gets a sequential index starting from 0.
Example Pipeline Layout:
Pipeline Groups: [RayGen:0, Miss:1, Hit:2, Miss:3, Hit:4, Hit:5]
Group Types: RayGen[0], Miss[1,3], Hit[2,4,5]
Why This Matters:
hitGroupId
to reference specific SBT entriesHow Indices Are Used:
addIndices()
automatically finds all group indices from pipeline creation infoaddIndex()
allows manual specification for custom layoutsaddData()
Special Cases:
---
config:
layout: elk
theme: 'neutral'
---
flowchart TD
subgraph subGraph0["Ray Tracing Pipeline Groups"]
B["Group 0: RayGen"]
A["Pipeline Creation"]
C["Group 1: Miss"]
D["Group 2: Hit"]
E["Group 3: Miss"]
F["Group 4: Hit"]
G["Group 5: Hit"]
end
subgraph subGraph1["SBT Mapping"]
I["RayGen[0] → SBT Entry 0"]
H["SBT Entries"]
J["Miss[1,3] → SBT Entries 0,1"]
K["Hit[2,4,5] → SBT Entries 0,1,2"]
end
subgraph subGraph2["TLAS Instance Association"]
M["hitGroupId = 0<br>→ Hit SBT Entry 0<br>→ Pipeline Group 2"]
L["TLAS Instance 0"]
O["hitGroupId = 1<br>→ Hit SBT Entry 1<br>→ Pipeline Group 4"]
N["TLAS Instance 1"]
Q["hitGroupId = 2<br>→ Hit SBT Entry 2<br>→ Pipeline Group 5"]
P["TLAS Instance 2"]
end
subgraph subGraph3["Ray Tracing Execution"]
S["Use hitGroupId = 0"]
R["Ray hits Instance 0"]
T["Look up Hit SBT Entry 0"]
U["Execute Pipeline Group 2<br>(Hit Shader)"]
end
A --> B
B --> C & I
C --> D & J
D --> E & K
E --> F & J
F --> G & K
H --> I
I --> J
J --> K
L --> M
N --> O
P --> Q
R --> S
S --> T
T --> U
G --> K
K --> M & O & Q
M --> S
style A fill:#e1f5fe
style H fill:#f3e5f5
style L fill:#e8f5e8
style R fill:#fff3e0
style subGraph0 fill:#fff
The SBT relies on several Vulkan ray tracing properties:
shaderGroupHandleSize
- Size of a program identifier (handle)shaderGroupHandleAlignment
- Alignment in bytes for each SBT entryshaderGroupBaseAlignment
- Alignment for starting addresses of each groupshaderGroupBaseAlignment
shaderGroupBaseAlignment
shaderGroupHandleAlignment
---
config:
layout: elk
theme: 'neutral'
---
flowchart LR
subgraph subGraph0["Alignment Requirements"]
B["Handle Alignment<br>shaderGroupHandleAlignment<br>(e.g., 64 bytes)"]
A["shaderGroupHandleSize<br>(e.g., 32 bytes)"]
C["Base Alignment<br>shaderGroupBaseAlignment<br>(e.g., 256 bytes)"]
end
subgraph subGraph1["Stride Calculation Process"]
E["handleSize aligned to<br>shaderGroupHandleAlignment"]
D["Base Stride Calculation"]
F{"User Data Exists?"}
G["handleSize + dataSize<br>aligned to shaderGroupHandleAlignment"]
H["handleSize aligned to<br>shaderGroupHandleAlignment"]
I["Final Stride = MAX of all strides<br>for that group type"]
end
subgraph subGraph2["Memory Layout Example"]
K["RayGen Section<br>Aligned to 256 bytes"]
J["Buffer Start"]
L["Miss Section<br>Aligned to 256 bytes"]
M["Hit Section<br>Aligned to 256 bytes"]
N["Callable Section<br>Aligned to 256 bytes"]
end
subgraph subGraph3["Individual Entry Alignment"]
P["Padding to<br>shaderGroupHandleAlignment"]
O["Entry 0<br>Handle + Data"]
Q["Entry 1<br>Handle + Data"]
R["Padding to<br>shaderGroupHandleAlignment"]
S["Entry 2<br>Handle + Data"]
end
A --> B
B --> C
D --> E
E --> F
F -- Yes --> G
F -- No --> H
G --> I
H --> I
J --> K
K --> L
L --> M
M --> N
O --> P
P --> Q
Q --> R
R --> S
style B fill:#e8f5e8
style A fill:#ffebee
style C fill:#e3f2fd
style I fill:#fff3e0
// For each group type
stride = align_up(handleSize + dataSize, shaderGroupHandleAlignment);
size = align_up(count * stride, shaderGroupBaseAlignment);
// Total buffer size
totalSize = align_up(raygenSize, bufferAlignment) +
align_up(missSize, bufferAlignment) +
align_up(hitSize, bufferAlignment) +
align_up(callableSize, bufferAlignment);
Why Stride Matters:
shaderGroupHandleAlignment
to ensure proper memory accessshaderGroupBaseAlignment
because it’s the entry pointStride Calculation Process:
handleSize
aligned to handleAlignment
handleSize + dataSize
aligned to handleAlignment
shaderGroupBaseAlignment
The nvvk::SBTGenerator
class is a helper utility that simplifies the complex process of creating and managing the Shader Binding Table. It handles the intricate details of buffer creation, alignment calculations, and shader handle retrieval, making it easier to create the information that will be stored in the SBT buffer.
SBTGenerator sbtGenerator;
sbtGenerator.init(device, rayProperties);
size_t bufferSize = sbtGenerator.calculateSBTBufferSize(
rtPipeline, rayPipelineInfo, librariesInfo);
This calculates:
What happens internally:
handleSize
aligned to shaderGroupHandleAlignment
handleSize + dataSize
aligned to shaderGroupHandleAlignment
shaderGroupBaseAlignment
shaderGroupBaseAlignment
, ensuring proper memory layout for the ray tracer.// Create SBT buffer with proper usage flags
VkBufferUsageFlags usage = VK_BUFFER_USAGE_2_SHADER_BINDING_TABLE_BIT_KHR |
VK_BUFFER_USAGE_2_SHADER_DEVICE_ADDRESS_BIT;
allocator.createBuffer(sbtBuffer, bufferSize, usage,
VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
VMA_ALLOCATION_CREATE_MAPPED_BIT,
sbtGenerator.getBufferAlignment());
Important: The user is responsible for creating the buffer using the size and alignment information provided by calculateSBTBufferSize()
. The SBT generator only calculates the requirements and populates the buffer - it does not create the buffer itself.
sbtGenerator.populateSBTBuffer(sbtBuffer.address, bufferSize, sbtBuffer.mapping);
This process:
Technical Details:
vkGetRayTracingShaderGroupHandlesKHR
to get all shader handles from the pipelineaddData()
(optional)vkCmdTraceRaysKHR
The buffer contains all the necessary data, and the handle provided to the ray tracer specifies where in the buffer the ray tracer should access this data.
The SBT can store custom data alongside shader handles:
struct HitRecordBuffer {
std::array<float, 4> color;
};
// Add data to specific hit groups
sbtGenerator.addData(SBTGenerator::eHit, 0, hitData0);
sbtGenerator.addData(SBTGenerator::eHit, 1, hitData1);
See the tutorial 07_multi_closest_hit for an example.
You can have more SBT entries than pipeline groups by duplicating entries:
// Pipeline has 2 hit groups, but SBT has 3
sbtGenerator.addIndices(rayPipelineInfo); // Add groups 0, 1
sbtGenerator.addIndex(SBTGenerator::eHit, 2); // Duplicate group 1
sbtGenerator.addData(SBTGenerator::eHit, 2, customData); // Custom data for entry 2
See the tutorial 07_multi_closest_hit for an example.
Instead of using addIndices()
, you can manually specify group indices for complete control:
// Manually define group indices
sbtGenerator.addIndex(SBTGenerator::eRaygen, 0); // RayGen group 0
sbtGenerator.addIndex(SBTGenerator::eMiss, 1); // Miss group 1
sbtGenerator.addIndex(SBTGenerator::eMiss, 2); // Miss group 2
sbtGenerator.addIndex(SBTGenerator::eHit, 3); // Hit group 3
sbtGenerator.addIndex(SBTGenerator::eHit, 4); // Hit group 4
// Add custom data to specific groups
sbtGenerator.addData(SBTGenerator::eHit, 3, hitData0);
sbtGenerator.addData(SBTGenerator::eHit, 4, hitData1);
The SBT generator supports pipeline libraries:
std::vector<VkRayTracingPipelineCreateInfoKHR> libraries = {lib1, lib2};
sbtGenerator.addIndices(rayPipelineInfo, libraries);
const SBTGenerator::Regions sbtRegions = sbtGenerator.getSBTRegions();
// Access individual regions
VkStridedDeviceAddressRegionKHR raygenRegion = sbtRegions.raygen;
VkStridedDeviceAddressRegionKHR missRegion = sbtRegions.miss;
VkStridedDeviceAddressRegionKHR hitRegion = sbtRegions.hit;
VkStridedDeviceAddressRegionKHR callableRegion = sbtRegions.callable;
vkCmdTraceRaysKHR(cmdBuffer,
&sbtRegions.raygen, // Ray generation shader
&sbtRegions.miss, // Miss shader
&sbtRegions.hit, // Hit shader
&sbtRegions.callable, // Callable shader
width, height, 1); // Dimensions
VK_BUFFER_USAGE_2_SHADER_BINDING_TABLE_BIT_KHR