4.1 KiB
4.1 KiB
Descriptors: Builders, Allocators, and Layouts
Utilities to define descriptor layouts, write descriptor sets, and efficiently allocate them per-frame or globally.
Overview
- Layouts:
DescriptorLayoutBuilderassemblesVkDescriptorSetLayoutwith staged bindings. - Writing:
DescriptorWritercollects buffer/image writes and updates a set in one call. - Pools:
DescriptorAllocatorGrowablemanages a growable pool-of-pools for resilient allocations;FrameResourceskeeps one per overlapping frame. - Common layouts:
DescriptorManagerpre-creates reusable layouts such asgpuSceneDataLayout()andsingleImageLayout().
Quick Start — Transient Per-Frame Set
// 1) Create/update a small uniform buffer for the frame
AllocatedBuffer ubuf = context->getResources()->create_buffer(
sizeof(GPUSceneData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
context->currentFrame->_deletionQueue.push_function([=, &ctx=*context]{ ctx.getResources()->destroy_buffer(ubuf); });
VmaAllocationInfo ai{}; vmaGetAllocationInfo(context->getDevice()->allocator(), ubuf.allocation, &ai);
*static_cast<GPUSceneData*>(ai.pMappedData) = context->getSceneData();
// 2) Allocate a set from the frame allocator using a common layout
VkDescriptorSet set = context->currentFrame->_frameDescriptors.allocate(
context->getDevice()->device(), context->getDescriptorLayouts()->gpuSceneDataLayout());
// 3) Write the buffer binding
DescriptorWriter writer;
writer.write_buffer(0, ubuf.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
writer.update_set(context->getDevice()->device(), set);
Defining a Custom Layout
DescriptorLayoutBuilder lb;
lb.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
lb.add_binding(1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER);
VkDescriptorSetLayout myLayout = lb.build(device, VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT);
// ... remember to vkDestroyDescriptorSetLayout(device, myLayout, nullptr) in cleanup
Global Growable Allocator
For long-lived sets (e.g., materials, compute instances), use EngineContext::descriptors which wraps a growable allocator shared across modules.
VkDescriptorSet persistent = context->getDescriptors()->allocate(device, myLayout);
DescriptorWriter writer;
writer.write_image(0, albedoView, sampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(device, persistent);
// Freeing: call `context->getDescriptors()->destroy_pools(device)` at engine shutdown; sets die with their pools
Compute Integration
ComputeManager uses an internal DescriptorAllocatorGrowable and offers higher-level bindings:
- Build pipeline: specify
descriptorTypesandpushConstantSizeinComputePipelineCreateInfo. - Ad-hoc dispatch: fill
ComputeDispatchInfo.bindingswithComputeBinding::{uniformBuffer, storageBuffer, sampledImage, storeImage}. - Persistent instances: create via
createInstance()and set bindings withsetInstance*(); descriptor sets are auto-updated and reused across dispatches.
See PipelineManager.md for a full compute quick start using the unified API.
API Summary
DescriptorLayoutBuilder:add_binding(binding, type),build(device, stages[, pNext, flags]),clear().DescriptorWriter:write_buffer(binding, buffer, size, offset, type),write_image(binding, view, sampler, layout, type),update_set(device, set),clear().DescriptorAllocatorGrowable:init(device, initialSets, ratios),allocate(device, layout[, pNext]),clear_pools(device),destroy_pools(device).DescriptorManager:gpuSceneDataLayout(),singleImageLayout().
Best Practices
- Use per-frame allocator (
currentFrame->_frameDescriptors) for transient sets to avoid lifetime pitfalls. - Keep
DescriptorLayoutBuildersmall and local; free custom layouts in your pass/modulecleanup(). - Tune pool ratios to match workload; see how frames are initialized in
VulkanEngine::init_frame_resources(). - For persistent compute resources, prefer
ComputeManagerinstances over manual descriptor lifecycle.