Post 66. VK_KHR_ray_tracing_pipeline is a whole new dispatch model —
shader binding tables, vkCmdTraceRaysKHR, ray-gen/miss/hit shaders.
For shadows, ambient occlusion or picking that’s overkill. VK_KHR_ray_query
puts the BVH traversal inside any existing shader: build an
acceleration structure, bind it like a texture, and walk it with a
few GLSL calls — from a fragment shader.
$ ./app --headless out.ppm
device: AMD Radeon RX 7800 XT (RADV NAVI32)
wrote out.ppm
A checkerboard ground plane with a perfectly hard triangle shadow — every pixel cast a hardware-accelerated ray toward the light.
Example: VulkanExamples/p66_rayquery
Three enabling features
Ray query needs a three-struct feature chain — buffer device address
(AS buffers are referenced by GPU address, not descriptor),
accelerationStructure, and rayQuery itself:
vk::PhysicalDeviceBufferDeviceAddressFeatures bda{};
bda.setBufferDeviceAddress(true);
vk::PhysicalDeviceAccelerationStructureFeaturesKHR asf{};
asf.setAccelerationStructure(true).setPNext(&bda);
vk::PhysicalDeviceRayQueryFeaturesKHR rqf{};
rqf.setRayQuery(true).setPNext(&asf);
createDevice({VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME,
VK_KHR_RAY_QUERY_EXTENSION_NAME,
VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME}, &rqf);VK_KHR_deferred_host_operations is a dependency of the AS extension —
no deferred ops are used, it just has to be listed.
BLAS + TLAS
A bottom-level AS holds geometry; a top-level AS holds instances pointing at BLASes. The dance for the triangle BLAS:
- Vertex buffer with
eAccelerationStructureBuildInputReadOnlyKHR | eShaderDeviceAddress— GPU-addressed. vkGetAccelerationStructureBuildSizesKHR→ AS storage + scratch sizes.vkCreateAccelerationStructureKHRon a storage buffer,vkCmdBuildAccelerationStructuresKHRto build.- One
VkAccelerationStructureInstanceKHR(identity transform,accelerationStructureReference=vkGetAccelerationStructureDeviceAddressKHR(blas)) → TLAS built the same way over an instances buffer.
Two traps I hit on RADV:
maxVertexis the highest vertex index, not the count. 3 vertices →maxVertex = 2. Setting 3 makes the driver read a garbage fourth vertex — the shadow becomes non-deterministic noise.- Bind the TLAS, not the BLAS. Binding the BLAS directly is nominally legal, but on RADV it produced randomly flickering hits. The instanced TLAS path renders bit-identical frames.
The shader side
GLSL needs #extension GL_EXT_ray_query and a SPIR-V 1.4+ target
(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_3 in
shaderc). Then a shadow ray is five calls:
layout(set = 0, binding = 0) uniform accelerationStructureEXT scene;
rayQueryEXT q;
rayQueryInitializeEXT(q, scene, gl_RayFlagsTerminateOnFirstHitEXT,
0xFF, origin + dir * 0.02, 0.0, dir, tmax - 0.04);
while (rayQueryProceedEXT(q)) {}
bool shadow = rayQueryGetIntersectionTypeEXT(q, true)
!= gl_RayQueryCommittedIntersectionNoneEXT;TerminateOnFirstHit is the shadow-ray optimization — traversal stops
at the first opaque hit instead of finding the closest one. The
0.02 origin offset and tmax - 0.04 are the classic ray-tracing
epsilon: push the ray off the surface and stop just short of the light
so it doesn’t hit itself.
The AS binds through a normal descriptor with type
eAccelerationStructureKHR — the write needs a
VkWriteDescriptorSetAccelerationStructureKHR chained into pNext
instead of pImageInfo/pBufferInfo.
When ray query wins
- Shadows / AO / picking in a rasterizer — no SBT, no new pipeline type, one descriptor.
- Works in any stage — fragment, compute, even vertex.
- Costs: you manage BLAS/TLAS lifetime and updates yourself, and every ray pays BVH traversal in-shader (fine for one shadow ray; full path tracing still wants the RT pipeline).