RT Graphics Programming
Lesson 28 of 28

Part 2 · Vulkan implementation

28. The Ray Tracing Pipeline and the Complete Renderer

Move traversal into the ray-tracing pipeline, construct its shader binding table and audit what still belongs to the renderer.

Graphics glossary 61 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

The ray tracing pipeline changes how ray work is launched and divided between shader stages. A ray-generation shader creates the initial questions. A miss shader handles an unanswered query. A closest-hit shader handles the committed triangle answer. The shader binding table connects records in device memory to those shader groups.

None of this changes what a primary ray means. The same camera, TLAS, materials, light, secondary-ray intervals and accumulation rules remain. The pipeline therefore becomes a third route beside the manual compute tracer and compute ray query, to be compared before deciding where it belongs.

What you should be able to account for

  • Create ray-generation, miss and triangle closest-hit groups in a ray tracing pipeline.
  • Lay out shader binding table records using the device's handle size and alignment properties.
  • Trace, synchronize and measure the three renderer routes without confusing a faster isolated stage with a correct complete frame.

Enable a separate optional feature set

Query VkPhysicalDeviceRayTracingPipelineFeaturesKHR and require rayTracingPipeline for this route, in addition to the acceleration-structure and buffer-device-address support established in Lesson 27. Enable VK_KHR_ray_tracing_pipeline and its required dependencies according to the device's advertised API version and extension set. Retain the non-pipeline routes when the feature is unavailable.

Also query VkPhysicalDeviceRayTracingPipelinePropertiesKHR. The shader group handle size, handle alignment, base alignment and maximum recursion depth are device properties, not constants to copy from another machine.

Divide one trace into shader responsibilities

Create three SPIR-V shader modules:

  • The ray-generation shader maps its launch ID to a pixel and sample, constructs the camera ray and calls traceRayEXT.
  • The miss shader writes the environment result into the ray payload.
  • The closest-hit shader reads instance, primitive and barycentric built-ins, reconstructs the surface and writes the shaded or continued result into the payload.

A compact first payload can contain radiance, throughput, next origin and direction, plus flags. Payload location numbers in the shader declarations must match across stages. Start with primary visibility and a simple colour before adding the secondary-ray loop; this keeps pipeline creation separate from shading complexity.

// Ray-generation shader, abbreviated.
#extension GL_EXT_ray_tracing : require

layout(set = 0, binding = 8) uniform accelerationStructureEXT topLevel;
layout(location = 0) rayPayloadEXT Payload payload;

void main() {
    ivec2 pixel = ivec2(gl_LaunchIDEXT.xy);
    Ray primary = makePrimaryRay(uvec2(pixel));
    initialisePayload(payload);

    traceRayEXT(
        topLevel,
        gl_RayFlagsOpaqueEXT,
        0xff,
        0, 0, 0,
        primary.origin,
        control.rayEpsilon,
        primary.direction,
        control.maximumDistance,
        0);

    storeSample(pixel, payload.radiance);
}

The three shader-binding-table parameters choose the hit-group record offset, stride and miss index. All are zero in this one-hit-group, one-miss-group example. They become part of material or geometry selection only when the table deliberately contains multiple records.

Create shader groups, not only stages

The pipeline stage array contains ray-generation, miss and closest-hit stages. The group array contains two general groups, one for ray generation and one for miss, followed by one triangle hit group whose closest-hit index names the third stage.

VkRayTracingShaderGroupCreateInfoKHR raygenGroup{
    .sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
    .type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
    .generalShader = raygenStageIndex,
    .closestHitShader = VK_SHADER_UNUSED_KHR,
    .anyHitShader = VK_SHADER_UNUSED_KHR,
    .intersectionShader = VK_SHADER_UNUSED_KHR
};

VkRayTracingShaderGroupCreateInfoKHR hitGroup{
    .sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
    .type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR,
    .generalShader = VK_SHADER_UNUSED_KHR,
    .closestHitShader = closestHitStageIndex,
    .anyHitShader = VK_SHADER_UNUSED_KHR,
    .intersectionShader = VK_SHADER_UNUSED_KHR
};

Create the pipeline with vkCreateRayTracingPipelinesKHR, the shared pipeline layout, all stages, all groups and a declared maximum recursion depth. Begin with depth one while the ray-generation shader launches only primary rays. Increasing the declared maximum does not create secondary rays; shader code must trace them.

Construct the shader binding table from properties

Retrieve group handles with vkGetRayTracingShaderGroupHandlesKHR. Each handle occupies shaderGroupHandleSize meaningful bytes, but records and table regions must satisfy the corresponding alignment requirements. Compute aligned strides rather than writing the handles back-to-back by assumption.

auto alignUp = [](VkDeviceSize value, VkDeviceSize alignment) {
    return (value + alignment - 1) & ~(alignment - 1);
};

VkDeviceSize handleStride = alignUp(
    properties.shaderGroupHandleSize,
    properties.shaderGroupHandleAlignment);
VkDeviceSize raygenStride = alignUp(
    handleStride,
    properties.shaderGroupBaseAlignment);

The ray-generation region uses raygenStride as both its stride and size because that region names one non-indexed record. Miss and hit records may use handleStride, while the starting device address of every non-empty region is aligned to shaderGroupBaseAlignment. Create a shader-binding-table buffer with shader-binding-table and shader-device-address usage. Allocate device-address-capable memory, copy each group handle into its correctly aligned record through a host-visible path or staging transfer, and retain the buffer while commands use its address. Padding bytes are not additional handles.

Align one set of handles

Suppose the handle size is 24 bytes, handle alignment is 32 and base alignment is 64. Miss and hit record stride becomes 32. The one-record ray-generation region uses a stride and size of 64, and every region begins at a 64-byte-aligned device address. Copy only the 24 handle bytes into each record and leave the padding controlled. A 24-byte miss or hit stride would violate the reported handle alignment.

Record four table regions

Build VkStridedDeviceAddressRegionKHR values for ray generation, miss and hit records. The callable region is empty in this course pipeline.

VkStridedDeviceAddressRegionKHR raygenRegion{
    .deviceAddress = sbtAddress + raygenOffset,
    .stride = raygenStride,
    .size = raygenSize
};
VkStridedDeviceAddressRegionKHR missRegion{
    .deviceAddress = sbtAddress + missOffset,
    .stride = missStride,
    .size = missSize
};
VkStridedDeviceAddressRegionKHR hitRegion{
    .deviceAddress = sbtAddress + hitOffset,
    .stride = hitStride,
    .size = hitSize
};
VkStridedDeviceAddressRegionKHR callableRegion{};

Bind the ray tracing pipeline and descriptor set, push any per-dispatch control, then launch exactly the output extent:

vkCmdBindPipeline(
    commandBuffer,
    VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR,
    rayTracingPipeline);
vkCmdBindDescriptorSets(
    commandBuffer,
    VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR,
    pipelineLayout, 0, 1, &descriptorSet, 0, nullptr);

vkCmdTraceRaysKHR(
    commandBuffer,
    &raygenRegion,
    &missRegion,
    &hitRegion,
    &callableRegion,
    outputWidth,
    outputHeight,
    1);

The launch dimensions provide gl_LaunchIDEXT and gl_LaunchSizeEXT. Bounds logic should still remain sensible if the output extent and dispatch controls are changed during resize.

Synchronize the output by its actual producer

The ray-generation and hit/miss work ultimately writes storage images. Before a transfer copies the result to a presentation image, use a dependency whose source stage includes VK_PIPELINE_STAGE_2_RAY_TRACING_SHADER_BIT_KHR, source access is shader storage write, destination stage is transfer and destination access is transfer read. Apply the required image layout transition to the copied image.

If accumulation is read and written by successive ray tracing launches, establish the corresponding ray-tracing-shader write to ray-tracing-shader read/write dependency. Reusing the compute stage in that barrier would name the wrong producer.

Keep secondary rays bounded

A closest-hit shader may call traceRayEXT for a shadow or reflection ray, subject to pipeline recursion limits and implementation properties. Alternatively, the ray-generation shader can loop and launch one trace per bounce using a payload as returned state. Choose the form that keeps the course's explicit bounce, interval and throughput policy visible.

For the first complete pipeline route, use a shadow ray that skips closest-hit work and selects a dedicated miss result, then a bounded reflection depth. Do not set recursion depth to the device maximum without a reason; it can affect resource use, and it does not replace an algorithmic stopping rule.

Compare three complete routes

The worked account above separates manual compute traversal, a compute ray query and the dedicated ray tracing pipeline by responsibility. In the Vulkan application, render the same camera, triangle scene, material, light and sample sequence through all available routes. Compare selected hit records and the final linear image before timing.

Use timestamp queries around the GPU command regions, check the queue family's valid timestamp bits and convert timestamp differences using the physical device's timestamp period. Warm pipelines and use enough frames to report a distribution rather than one attractive number. A traversal-stage improvement is not automatically a faster frame if acceleration-structure rebuilds, transfers or synchronization dominate.

Change the handle alignment

A device reports a 32-byte group handle and 64-byte handle alignment. May the shader binding table use a 32-byte record stride?

The answer

No. The record stride must satisfy the reported handle alignment, so it must be at least 64 bytes and appropriately aligned. The handle occupies 32 meaningful bytes inside that record; the remaining bytes are padding or application record data.

Official reference: the Khronos ray tracing chapter defines pipeline and shader-binding-table behaviour, and the official basic ray tracing sample demonstrates the feature chain, groups and trace command.

Complete the evidence table

For each supported route, record feature availability, selected hit identity and distance for the same twenty test rays, final-image error against the manual compute reference, median GPU trace time and whether acceleration-structure build time is included. Repeat after moving only an instance and after changing only a material. Explain which resources and commands must change in each case. Keep failing edge cases visible rather than averaging them into an image-wide score.

What the complete course has established

The manual tracer supplied the operative definitions. The Vulkan compute route exposed memory, descriptors, dispatch and synchronization while preserving those definitions. Ray queries replaced exhaustive triangle search with hardware traversal inside familiar compute work. The ray tracing pipeline then divided that traversal across dedicated stages and an explicit shader binding table. These are different implementations of the same visibility questions, not four unrelated rendering tricks.

You now have a reference renderer, an acceleration comparison and a dedicated pipeline route that can disagree in inspectable ways. That is the useful end of this course sequence. A renderer is not understood because it produces a polished image; it is understood when its data ownership, intervals, hit identity, accumulated estimates and measured costs can all be accounted for.