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

VkBuffer

-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

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

:

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

VkDescriptorSetAllocateInfo

… 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

vkAllocateDescriptorSets

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

vkAllocateDescriptorSets

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

VkDescriptorBufferInfo

… 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

vkUpdateDescriptorSets

which takes an array of structures as a parameter

VkWriteDescriptorSet

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

vkUpdateDescriptorSets

… The function accepts two arrays: an array of structures

VkWriteDescriptorSet

and the array

VkCopyDescriptorSet

… 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

vkCmdBindDescriptorSets

bind a suitable set of descriptors to descriptors in the shader. This must be done before calling

vkCmdDrawIndexed

:

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:

You can find a complete list of alignment requirements at

specifications

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 vec2which 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 alignasadded 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.

Код С++ / Вершинный шейдер / Фрагментный шейдер

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *