Graphics glossary 43 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.
A shader can contain the correct pixel calculation and still have no image to write. A host program can create the correct image view and still expose it at the wrong binding. Vulkan makes the interface between those two descriptions explicit. The shader declares a descriptor set number, binding number, descriptor-compatible type and access. The host creates a matching descriptor set layout, updates an allocated descriptor set with the actual resource and includes that layout in the pipeline layout.
The tempting proxy is a successfully created pipeline. Pipeline creation establishes that the shader module and declared layout satisfy Vulkan's creation rules. It does not establish that the descriptor set later bound at execution contains the intended image, or that the image is in a layout suitable for storage access. The entire static and resource interface must therefore be accounted for before a dispatch is recorded.
What you should be able to account for
- Compile a compute shader to SPIR-V and create a shader module from the resulting words.
- Make one storage-image declaration agree across shader, descriptor set layout, descriptor update and pipeline layout.
- Explain why pipeline creation, descriptor allocation and descriptor binding are distinct operations.
Declare the smallest useful compute shader
The first shader writes a diagnostic gradient rather than tracing a scene. Its purpose is to prove that each global invocation can address one texel in the storage image. A 16 by 16 local workgroup is a choice, not a claim that 256 invocations is optimal for every device or shader. Lesson 18 will calculate the required workgroup count and guard the edge.
#version 460
layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;
layout(set = 0, binding = 0, rgba8)
uniform writeonly image2D outputImage;
void main() {
ivec2 pixel = ivec2(gl_GlobalInvocationID.xy);
ivec2 extent = imageSize(outputImage);
if (pixel.x >= extent.x || pixel.y >= extent.y) {
return;
}
vec2 samplePosition =
(vec2(pixel) + vec2(0.5)) / vec2(extent);
imageStore(
outputImage,
pixel,
vec4(samplePosition, 0.15, 1.0));
}
The format qualifier and image view format must be compatible. The shader requests a storage image at set zero, binding zero. writeonly states that this declaration does not read existing texels; it does not create synchronisation or change the image layout.
Compile source into the module input
Vulkan's traditional shader ingestion path consumes SPIR-V words. Keep the shader source and compiler command in the project so the binary can be reproduced. For example, the Vulkan SDK's glslc tool can compile the file as a compute shader:
glslc shaders/raytrace.comp -o shaders/raytrace.comp.spv
Read the binary as 32-bit words and reject a byte count that is not divisible by four. The shader module copies or consumes the supplied code during creation as specified, so the file buffer need not remain alive for the life of the pipeline.
std::vector<uint32_t> spirv = readSpirvWords(
"shaders/raytrace.comp.spv");
VkShaderModuleCreateInfo moduleInfo{
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = spirv.size() * sizeof(uint32_t),
.pCode = spirv.data()
};
VkShaderModule shaderModule = VK_NULL_HANDLE;
check(vkCreateShaderModule(
device,
&moduleInfo,
nullptr,
&shaderModule));
A shader module is not a compute pipeline. It supplies code from which a pipeline stage will select the main entry point. The pipeline also needs a layout describing the resources that entry point may access.
Describe the binding before allocating it
VkDescriptorSetLayoutBinding outputBinding{
.binding = 0,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT
};
VkDescriptorSetLayoutCreateInfo setLayoutInfo{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.bindingCount = 1,
.pBindings = &outputBinding
};
VkDescriptorSetLayout setLayout = VK_NULL_HANDLE;
check(vkCreateDescriptorSetLayout(
device,
&setLayoutInfo,
nullptr,
&setLayout));
VkPipelineLayoutCreateInfo pipelineLayoutInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = 1,
.pSetLayouts = &setLayout
};
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
check(vkCreatePipelineLayout(
device,
&pipelineLayoutInfo,
nullptr,
&pipelineLayout));
The descriptor set layout states what a compatible set must contain. The pipeline layout states which set layouts and push-constant ranges belong to the pipeline interface. Neither object contains the output image view. That concrete association belongs to a descriptor set.
Worked Vulkan account
How a Shader Request Becomes a Bound Resource
A shader declaration becomes useful only when the host describes the same interface and supplies the actual resource through a compatible descriptor set.
What the code must establish
The shader source requests a resource at a particular set, binding and descriptor type. Compilation turns that source into SPIR-V, but it does not invent the host-side resource. The descriptor set layout repeats the shader contract, the pipeline layout places that set within the pipeline interface, and the descriptor update supplies the actual image view that a dispatch will use.
What to inspect
Place the shader declaration, descriptor-set-layout binding and descriptor write beside one another. The set number, binding number, descriptor type, descriptor count and stage visibility must agree. Also record which image view and image layout were written into the allocated set. A handle printed on its own proves very little because it does not expose this agreement.
What this establishes, and what it does not: Pipeline creation establishes that Vulkan accepted the declared interface and shader stages. It does not prove that the descriptor set contains the intended image at dispatch time or that the image is in the stated layout.
Create the compute pipeline
VkPipelineShaderStageCreateInfo stageInfo{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT,
.module = shaderModule,
.pName = "main"
};
VkComputePipelineCreateInfo pipelineInfo{
.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.stage = stageInfo,
.layout = pipelineLayout
};
VkPipeline computePipeline = VK_NULL_HANDLE;
check(vkCreateComputePipelines(
device,
VK_NULL_HANDLE,
1,
&pipelineInfo,
nullptr,
&computePipeline));
vkDestroyShaderModule(device, shaderModule, nullptr);
Destroying the shader module after pipeline creation does not destroy the pipeline. The pipeline owns the compiled implementation state it requires. Retain the module only if the application needs it for another pipeline creation. The pipeline layout and descriptor set layout must remain alive while required by their dependent use.
Allocate and update the concrete descriptor set
A descriptor pool supplies storage for descriptor sets. The pool count describes how many storage-image descriptors may be allocated from it; maxSets describes how many sets. Our single set requires one of each.
VkDescriptorPoolSize poolSize{
.type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.descriptorCount = 1
};
VkDescriptorPoolCreateInfo poolInfo{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
.maxSets = 1,
.poolSizeCount = 1,
.pPoolSizes = &poolSize
};
VkDescriptorPool descriptorPool = VK_NULL_HANDLE;
check(vkCreateDescriptorPool(
device, &poolInfo, nullptr, &descriptorPool));
VkDescriptorSetAllocateInfo allocateInfo{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
.descriptorPool = descriptorPool,
.descriptorSetCount = 1,
.pSetLayouts = &setLayout
};
VkDescriptorSet descriptorSet = VK_NULL_HANDLE;
check(vkAllocateDescriptorSets(
device, &allocateInfo, &descriptorSet));
Now associate binding zero with the view created in Lesson 16. The descriptor states VK_IMAGE_LAYOUT_GENERAL because that is the layout in which the compute shader will access it. The recorded transition remains Lesson 18's responsibility.
VkDescriptorImageInfo imageDescriptor{
.sampler = VK_NULL_HANDLE,
.imageView = outputView,
.imageLayout = VK_IMAGE_LAYOUT_GENERAL
};
VkWriteDescriptorSet write{
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
.dstSet = descriptorSet,
.dstBinding = 0,
.dstArrayElement = 0,
.descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
.pImageInfo = &imageDescriptor
};
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
Follow binding zero
The shader says set 0, binding 0, storage image, compute stage. The descriptor set layout says binding 0, storage image, count one, compute-stage visibility. The pipeline layout places that set layout at set 0. The allocated descriptor set conforms to that layout. Its update writes the output view at binding 0 and declares GENERAL. Changing only dstBinding to 1 does not create a second interpretation. It leaves the shader's requested binding without the intended descriptor and violates the interface.
Locate the actual resource
Which object in this lesson contains the association with outputView: the descriptor set layout, pipeline layout, compute pipeline or updated descriptor set?
The answer
The updated descriptor set contains the descriptor association with the image view. The set layout declares what may occupy binding zero. The pipeline layout includes that declaration in the shader interface. The pipeline uses the layout, but none of those static objects substitutes for the concrete descriptor update.
Official reference: see the Khronos chapters on resource descriptors and compute pipelines, together with the guides to mapping data to shaders and high-level shader languages.
Break one part of the contract at a time
With validation enabled, change the host layout binding to 1 while leaving the shader at 0. Restore it, then change the descriptor type to sampled image while leaving the shader declaration as storage image. Record the diagnostic produced at the earliest point and the later operation that would otherwise rely on the mismatch. Finally, restore the matching interface and retain a printed table of set, binding, descriptor type, count and stage flags. This table becomes more valuable when later lessons add camera and scene buffers.
Carry one stable interface forward
Keep output image at set 0, binding 0 throughout the compute sequence. Later resources will take new bindings rather than silently renumbering this one. Stable binding numbers make shader and host diffs easier to inspect. Lesson 18 will bind the pipeline and descriptor set in a command buffer, launch enough workgroups to cover the image and declare when the resulting shader writes become visible to the copy used for presentation.
The pipeline exists, the descriptor set names a real storage image and the shader can calculate a diagnostic colour. Nothing has executed. This distinction is the point at which copied Vulkan samples often become obscure: construction success is mistaken for submitted work. Lesson 18 records the missing commands and makes their dependencies explicit.