Vulkan. Developer’s guide. Descriptor Pool and Descriptor Sets
Descriptor Pool and Descriptor Sets
Introduction
In the previous chapter, we created layout descriptors. It defines the number and types of descriptors to be used for rendering. In this chapter, we will link our
-s with their corresponding descriptors. To do this, we need to create a set of descriptors for each buffer.
Pool of descriptors
Descriptor sets cannot be created directly, they must be allocated from the pool, just like command buffers. To create a pool of descriptors, we will write a new function
createDescriptorPool
…
void initVulkan() {
...
createUniformBuffers();
createDescriptorPool();
...
}
...
void createDescriptorPool() {
}
First, you need to describe what types of descriptors are contained in the sets of descriptors, and their number. For this we use the structure
…
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());
We will allocate one descriptor for each frame. Structure hint
VkDescriptorPoolSize
contained in
:
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
We need to specify not only the maximum number of individual available descriptors, but also the maximum number of descriptor sets that can be allocated:
poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());
The structure contains a field
flags
… You can fill it with value
VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
, which will allow you to release sets individually. We will not touch the descriptor set after it has been created, so we do not need this flag. You can leave the default here
0
…
Let’s add a new method vkCreateDescriptorPool
and field descriptorPool
to create and store a pool of descriptors.
VkDescriptorPool descriptorPool;
...
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor pool!");
}
The descriptor pool must be destroyed when re-creating the swap chain, as it depends on the number of images:
void cleanupSwapChain() {
...
for (size_t i = 0; i < swapChainImages.size(); i++) {
vkDestroyBuffer(device, uniformBuffers[i], nullptr);
vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}
vkDestroyDescriptorPool(device, descriptorPool, nullptr);
}
And then re-created in
recreateSwapChain
:
void recreateSwapChain() {
...
createUniformBuffers();
createDescriptorPool();
createCommandBuffers();
}
Descriptor sets
We can now highlight the descriptor sets. To do this, add the function
createDescriptorSets
:
void initVulkan() {
...
createDescriptorPool();
createDescriptorSets();
...
}
void recreateSwapChain() {
...
createDescriptorPool();
createDescriptorSets();
...
}
...
void createDescriptorSets() {
}
The allocation of descriptor sets is described using a structure
… It is necessary to specify the pool of descriptors from which the descriptor sets will be allocated, the number of allocated sets and layout descriptors:
std::vector<VkDescriptorSetLayout> layouts(swapChainImages.size(), descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(swapChainImages.size());
allocInfo.pSetLayouts = layouts.data();
We will create one set of descriptors for each image from the swap chain, all with one layout. We need to make several copies of the layout handle, since the function
takes an array of layouts, one for each set you create.
Let’s add a field descriptorSets
for storing sets of descriptors and allocate them using vkAllocateDescriptorSets
:
VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;
...
descriptorSets.resize(swapChainImages.size());
if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate descriptor sets!");
}
There is no need to explicitly destroy the descriptor sets as they will be automatically deallocated when the descriptor pool is destroyed. Call
will allocate sets of descriptors, each containing a descriptor for a uniform buffer.
The descriptor sets are allocated, but the descriptors internally need to be configured. Let’s add a loop to fill each descriptor:
for (size_t i = 0; i < swapChainImages.size(); i++) {
}
The descriptors that refer to the buffers, like our uniform buffer descriptor, are configured with a structure
… This structure specifies the buffer and the area within it that contains the data for the descriptor.
for (size_t i = 0; i < swapChainImages.size(); i++) {
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffers[i];
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
}
If you completely overwrite the buffer, as we do in this case, then you can use the value
VK_WHOLE_SIZE
for the range field. The descriptor settings are updated using the function
which takes an array of structures as a parameter
…
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
The first two fields indicate the set of descriptors to update and the binding. We assigned an index to our binding
0
… Please note that an array of resources can correspond to one binding, which means that the descriptors within the set will be grouped into an array. Therefore, we must specify the index of the first element from which to start the update. We are not using an array, so the index is
0
…
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
Now let’s specify the descriptor type again. You can update several descriptors in an array at once, starting from the index
dstArrayElement
… Field
descriptorCount
indicates how many elements of the array to update.
descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr; // Optional
descriptorWrite.pTexelBufferView = nullptr; // Optional
The last field refers to an array of structures that customize the descriptors. The array must be of length
descriptorCount
… Field
pBufferInfo
used for descriptors that refer to buffer data,
pImageInfo
used for descriptors that refer to image data, and
pTexelBufferView
used for descriptors that refer to buffer views. Our descriptor refers to buffers, so we use
pBufferInfo
…
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
Updates are applied using
… The function accepts two arrays: an array of structures
and the array
… The latter can be used to copy descriptors, as its name suggests.
Using descriptor sets
Now we need to update the function
createCommandBuffers
so that using
bind a suitable set of descriptors to descriptors in the shader. This must be done before calling
:
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[i], 0, nullptr);
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
Unlike vertex and index buffers, descriptor sets can be used with more than just graphics pipelines. That is why it is necessary to indicate to which pipeline we want to bind the sets – to graphics or computational. The next parameter is the layout of the descriptors. The next three parameters indicate the index of the first set of descriptors, the number of sets, and the array of sets to bind. We’ll get back to them soon. The last two parameters specify an array of offsets to use for dynamic descriptors. We’ll look at them in the next chapters.
If you run the program now, it turns out that nothing is displayed. The problem is that due to the Y inversion we did in the projection matrix, the vertices now follow in counterclockwise instead of clockwise order. Because of this, triangles are discarded at the backface culling stage and the geometry is not drawn. To fix this, in the function createGraphicsPipeline
change frontFace
v VkPipelineRasterizationStateCreateInfo
:
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
Run the program again, which should result in the following:
The rectangle has turned into a square because the projection matrix has adjusted the aspect ratio. Function updateUniformBuffer
keeps track of the resizing of the screen, so we do not need to re-create the set of descriptors in recreateSwapChain
…
Alignment requirements
So far, we haven’t looked at exactly how the data in the C ++ structure should match the uniform definition in the shader. It seems obvious enough to just use the same types in both:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
However, this is not all. Let’s try to change the structure and shader as follows:
struct UniformBufferObject {
glm::vec2 foo;
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
layout(binding = 0) uniform UniformBufferObject {
vec2 foo;
mat4 model;
mat4 view;
mat4 proj;
} ubo;
Let’s recompile the shader and the program. Now, if you run it, you will find that the color square with which we have been working all this time has disappeared! This is because we left out the alignment requirements.
Vulkan expects the data in the structure to be aligned in memory in a certain way, for example:
- Scalars must be N-aligned (= 4 bytes or 32-bit floating point)
vec2
must be 2N aligned (= 8 bytes)vec3
orvec4
must be 4N aligned (= 16 bytes)- Matrix
mat4
should have the same alignment asvec4
…
You can find a complete list of alignment requirements at
…
Our original shader, which had only three fields mat4
, met the alignment requirements. The size of each mat4
is 64 bytes (4 x 4 x 4 = 64), which means the offset of the model matrix = 0
, view matrix offset = 64
, and proj-matrices = 128
… They are all multiples of 16, so everything worked fine.
The new structure starts with vec2
which is only 8 bytes in size, so all offsets are corrupted. Now the offset of the model matrix = 8
, view matrix offset = 72
, and proj-matrices = 136
, but none of them is a multiple of 16. To solve this problem, we can use the specifier alignas
added in C ++ 11:
struct UniformBufferObject {
glm::vec2 foo;
alignas(16) glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
If we re-compile and run the program, we will see that now the shader receives the matrix values correctly, as before.
Fortunately, there is a way not to think about these alignment requirements all the time. We can add define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
right before GLM:
#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
#include <glm/glm.hpp>
This will force GLM to use the version
vec2
and
mat4
, in which alignment requirements are already set. If you add this definition, you can remove the qualifier
alignas
and the program will still work.
However, this method may not work when using nested structures. Consider the following definition in C ++ code:
struct Foo {
glm::vec2 v;
};
struct UniformBufferObject {
Foo f1;
Foo f2;
};
And the definition in the shader:
struct Foo {
vec2 v;
};
layout(binding = 0) uniform UniformBufferObject {
Foo f1;
Foo f2;
} ubo;
In this case
f2
will have an offset
8
then how should it be
16
as it is a nested structure. In this case, you need to specify the alignment yourself:
struct UniformBufferObject {
Foo f1;
alignas(16) Foo f2;
};
As you can see, it is better to write the alignment explicitly. This way, you won’t be caught off guard by unexpected errors.
struct UniformBufferObject {
alignas(16) glm::mat4 model;
alignas(16) glm::mat4 view;
alignas(16) glm::mat4 proj;
};
Remember to recompile the shader after removing the field
foo
…
Many sets of descriptors
You can bind multiple sets of descriptors at the same time. To do this, when creating a layout pipeline, you must specify several layout descriptors (one for each set). Shaders can then reference specific descriptor sets as follows:
layout(set = 0, binding = 0) uniform UniformBufferObject { ... }
You can use this to split the sets into common to all objects and specific to each object. In this case, you can avoid re-binding most of the descriptors between draw calls, which is potentially more efficient.