Vulkan acceleration structures explained: BLAS, TLAS, build and update

Ray tracing shaders trace rays against geometry — but they never see the geometry. They see the acceleration structure: a spatial index (BVH) the driver builds once so a ray can skip 99 % of triangles in O(log n) steps. Vulkan splits this into two levels:

as.txt
BLAS (bottom)   mesh geometry: vertices + indices, per-object
   ▲
TLAS (top)      scene: instances — transform + BLAS pointer + mask
  • BLAS = “what is the object” — one per unique mesh. Expensive to build, cheap to update in place.
  • TLAS = “where is the object” — instances referencing BLASes with a VkTransformMatrixKHR each. Rebuilt every frame for moving objects.

The minimal recipe

blas.cpp
// 1. Geometry description (vertex buffer + triangle layout)
vk::AccelerationStructureGeometryKHR geo{};
geo.geometryType = vk::GeometryTypeKHR::eTriangles;
geo.geometry.triangles.vertexFormat = vk::Format::eR32G32B32Sfloat;
geo.geometry.triangles.vertexData   = {vtxAddr};
geo.geometry.triangles.vertexStride = sizeof(Vertex);
geo.geometry.triangles.maxVertex    = vtxCount;
geo.geometry.triangles.indexType    = vk::IndexType::eUint32;
geo.geometry.triangles.indexData    = {idxAddr};
geo.flags = vk::GeometryFlagBitsKHR::eOpaque; // no anyhit needed

// 2. Size query — you do NOT guess; ask the device
vk::AccelerationStructureBuildGeometryInfoKHR build{};
build.type     = vk::AccelerationStructureTypeKHR::eBottomLevel;
build.geometryCount = 1;
build.pGeometries   = &geo;
auto sizes = dev.getAccelerationStructureBuildSizesKHR(
    build, 1, &primCount);
// sizes.accelerationStructureSize  + buildScratchSize

// 3. Allocate the AS buffer + scratch
auto asBuf   = allocBuf(sizes.accelerationStructureSize,
                        eStorageBuffer | eAccelerationStructureStorage);
auto scratch = allocBuf(sizes.buildScratchSize);

// 4. Create + build on GPU
vk::AccelerationStructureCreateInfoKHR ci{};
ci.type   = vk::AccelerationStructureTypeKHR::eBottomLevel;
ci.buffer = asBuf;
ci.size   = sizes.accelerationStructureSize;
auto blas = dev.createAccelerationStructureKHRUnique(ci);
build.scratchData = scratchAddr;
cmd.buildAccelerationStructuresKHR(build, &range);

The TLAS is identical in shape but eTopLevel with instance buffer instead of triangles.

Building is a GPU job

build.txt
build  = first-time construction, into scratch + dest
update = modify in place (eUpdate flag), for moved instances
copy   = vkCmdCopyAccelerationStructureKHR — clone or compact

Per frame you typically rebuild the TLAS (few instances) and update or rebuild only the changed BLASes (e.g., skinned meshes stay in-place-update, new objects get a new BLAS).

What the ray shader sees

ray.rgen
#extension GL_EXT_ray_tracing : require
layout(set = 0, binding = 0) uniform accelerationStructureEXT tlas;

rayQueryEXT q;
rayQueryInitializeEXT(q, tlas, gl_RayFlagsOpaqueEXT,
                      0xFF, origin, tmin, dir, tmax);
while (rayQueryProceedEXT(q)) {}
if (rayQueryGetIntersectionTypeEXT(q, true) !=
        gl_RayQueryCommittedIntersectionNoneEXT) { /* hit */ }

The shader never knows about triangles — only about the TLAS handle you bound as a descriptor (VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR).

Practical rules

  • TLAS rebuild every frame is normal — it’s a few thousand bytes; BLAS rebuilds are the expensive part.
  • eOpaque on geometry is the single biggest perf win — skips anyhit evaluation for the entire object.
  • Scratch buffers can be shared per-build if you serialize; for parallel builds keep one scratch per in-flight frame.
  • Raytracing needs VK_KHR_acceleration_structure + VK_KHR_ray_tracing_pipeline (or VK_KHR_ray_query) — check accelerationStructure feature bit first.

Check out similar posts by category: Vulkan C++