Всем привет! Для тех, кто со мной не знаком, я — технический переводчик в IT-компании CG Tribe в Ижевске. Я занимаюсь переводом руководства Vulkan Tutorial на русский язык и выкладываю переводы на Хабр.
Я остановилась на разделе Uniform buffers и сегодня хочу поделиться переводом заключительной статьи раздела, которая называется Descriptor pool and sets.
Содержание
1. Вступление
2. Краткий обзор
3. Настройка окружения
4. Рисуем треугольник
5. Вершинные буферы
6. Uniform-буферы
7. Текстурирование
8. Буфер глубины
9. Загрузка моделей
10. Создание мип-карт
11. Multisampling
FAQ
Политика конфиденциальности
2. Краткий обзор
3. Настройка окружения
4. Рисуем треугольник
-
Подготовка к работе
- Отображение на экране
-
Графический конвейер (pipeline)
- Отрисовка
- Пересоздание swap chain
5. Вершинные буферы
6. Uniform-буферы
7. Текстурирование
- Изображения
- Image view и image sampler
- Комбинированный image sampler
8. Буфер глубины
9. Загрузка моделей
10. Создание мип-карт
11. Multisampling
FAQ
Политика конфиденциальности
Пул дескрипторов и сеты дескрипторов
- Вступление
- Пул дескрипторов
- Сеты дескрипторов
- Использование сетов дескрипторов
- Требования к выравниванию
- Множества сетов дескрипторов
Вступление
В предыдущей главе мы создали layout дескрипторов. Он определяет количество и типы дескрипторов, которые будут использованы для рендера. В этой главе мы свяжем наши
VkBuffer
-ы с соответствующими им дескрипторами. Для этого нам потребуется создать сет дескрипторов для каждого буфера. Пул дескрипторов
Сеты дескрипторов не могут быть созданы напрямую, их необходимо выделить из пула, как и буферы команд. Для создания пула дескрипторов напишем новую функцию
createDescriptorPool
.void initVulkan() {
...
createUniformBuffers();
createDescriptorPool();
...
}
...
void createDescriptorPool() {
}
Сначала нужно описать, какие типы дескрипторов содержатся в сетах дескрипторов, и их количество. Для этого используем структуру
VkDescriptorPoolSize
.VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());
Мы будем выделять по одному дескриптору для каждого кадра. Указание на структуру
VkDescriptorPoolSize
содержится в VkDescriptorPoolCreateInfo
:VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
Нам нужно указать не только максимальное количество отдельных доступных дескрипторов, но и максимальное количество сетов дескрипторов, которое можно выделить:
poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());
Структура содержит поле
flags
. Вы можете заполнить его значением VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
, которое позволит освобождать сеты по отдельности. Мы не будем трогать сет дескрипторов после его создания, поэтому этот флаг нам не нужен. Вы можете оставить здесь значение по умолчанию 0
.Добавим новый метод
vkCreateDescriptorPool
и поле descriptorPool
для создания и хранения пула дескрипторов.VkDescriptorPool descriptorPool;
...
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor pool!");
}
Пул дескрипторов должен быть уничтожен при пересоздании swap chain, поскольку он зависит от количества 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);
}
А затем заново создан в
recreateSwapChain
:void recreateSwapChain() {
...
createUniformBuffers();
createDescriptorPool();
createCommandBuffers();
}
Сеты дескрипторов
Теперь мы можем выделить сеты дескрипторов. Для этого добавим функцию
createDescriptorSets
:void initVulkan() {
...
createDescriptorPool();
createDescriptorSets();
...
}
void recreateSwapChain() {
...
createDescriptorPool();
createDescriptorSets();
...
}
...
void createDescriptorSets() {
}
Выделение сетов дескрипторов описывается с помощью структуры
VkDescriptorSetAllocateInfo
. Необходимо указать пул дескрипторов, из которого будут выделены сеты дескрипторов, количество выделяемых сетов и layout дескрипторов: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();
Мы создадим один сет дескрипторов для каждого image из swap chain, все с одним layout-ом. Нам потребуется сделать несколько копий хэндла layout-а, так как функция
vkAllocateDescriptorSets
принимает массив layout-ов, по одному на каждый создаваемый сет. Добавим поле
descriptorSets
для хранения сетов дескрипторов и выделим их с помощью 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!");
}
Нет необходимости явно уничтожать сеты дескрипторов, поскольку они будут автоматически освобождены при уничтожении пула дескрипторов. Вызов
vkAllocateDescriptorSets
выделит сеты дескрипторов, каждый из которых будет содержать дескриптор uniform-буфера.Сеты дескрипторов выделены, однако дескрипторы внутри нуждаются в настройке. Добавим цикл, чтобы заполнить каждый дескриптор:
for (size_t i = 0; i < swapChainImages.size(); i++) {
}
Дескрипторы, которые ссылаются на буферы, как наш дескриптор uniform-буфера, настраиваются с помощью структуры
VkDescriptorBufferInfo
. Эта структура указывает буфер и область внутри него, в которой содержатся данные для дескриптора.for (size_t i = 0; i < swapChainImages.size(); i++) {
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffers[i];
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
}
Если вы полностью перезаписываете буфер, как мы в данном случае, то вы можете использовать значение
VK_WHOLE_SIZE
для поля range. Настройки дескрипторов обновляются с помощью функции vkUpdateDescriptorSets
, которая принимает в качестве параметра массив структур VkWriteDescriptorSet
.VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
Первые два поля указывают сет дескрипторов, который нужно обновить, и привязку (binding). Мы присвоили нашей привязке индекс
0
. Обратите внимание, что одной привязке может соответствовать массив ресурсов, а значит и дескрипторы внутри сета будут сгруппированы в массив. Поэтому мы должны указать индекс первого элемента, с которого надо начать обновление. Мы не используем массив, поэтому индекс равен 0
.descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
Теперь снова укажем тип дескриптора. Можно обновить сразу несколько дескрипторов в массиве, начиная с индекса
dstArrayElement
. Поле descriptorCount
указывает, сколько элементов массива нужно обновить.descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr; // Optional
descriptorWrite.pTexelBufferView = nullptr; // Optional
Последнее поле ссылается на массив структур, которые настраивают дескрипторы. Массив должен иметь длину
descriptorCount
. Поле pBufferInfo
используется для дескрипторов, которые ссылаются на данные буфера, pImageInfo
используется для дескрипторов, которые ссылаются на данные image, а pTexelBufferView
используется для дескрипторов, которые ссылаются на buffer views. Наш дескриптор ссылается на буферы, поэтому мы используем pBufferInfo
.vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
Обновления применяются с помощью
vkUpdateDescriptorSets
. Функция принимает два массива: массив структур VkWriteDescriptorSet
и массив VkCopyDescriptorSet
. Последний может использоваться для копирования дескрипторов, что следует из его названия.Использование сетов дескрипторов
Теперь нужно обновить функцию
createCommandBuffers
, чтобы с помощью vkCmdBindDescriptorSets
привязать подходящий сет дескрипторов к дескрипторам в шейдере. Это необходимо выполнить перед вызовом 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);
В отличие от вершинных и индексных буферов, сеты дескрипторов могут использоваться не только с графическими конвейерами. Именно поэтому нужно указать, к какому конвейеру мы хотим привязать сеты — к графическому или вычислительному. Следующий параметр — это layout дескрипторов. Следующие три параметра указывают индекс первого сета дескрипторов, количество сетов и массив сетов, которые нужно привязать. Скоро мы к ним вернемся. Последние два параметра указывают массив смещений, используемых для динамических дескрипторов. Их мы рассмотрим в следующих главах.
Если запустить программу сейчас, то окажется, что ничего не отображается. Проблема в том, что из-за инвертирования Y, которое мы сделали в матрице проекции, вершины теперь следуют в порядке «против часовой стрелки» вместо «по часовой». Из-за этого треугольники отбраковываются на стадии кулинга (backface culling) и геометрия не отрисовывается. Чтобы это исправить, в функции
createGraphicsPipeline
изменим frontFace
в VkPipelineRasterizationStateCreateInfo
:rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
Снова запустим программу, в результате чего должно появиться следующее:
Прямоугольник превратился в квадрат, т.к. матрица проекции скорректировала отношение сторон. Функция
updateUniformBuffer
следит за изменением размеров экрана, поэтому нам не нужно пересоздавать сет дескрипторов в recreateSwapChain
.Требования к выравниванию
До сих пор мы не рассматривали, как именно данные в структуре C ++ должны соответствовать определению uniform-ы в шейдере. Кажется достаточно очевидным просто использовать одни и те же типы в обоих:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
Однако это еще не все. Попробуем изменить структуру и шейдер следующим образом:
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;
Перекомпилируем шейдер и программу. Теперь, если ее запустить, обнаружится, что цветовой квадрат, с которым мы работали все это время, исчез! А все потому, что мы не учли требования к выравниванию.
Vulkan ожидает, что данные в структуре будут выровнены в памяти определенным образом, например:
- Скаляры должны быть выровнены по N (= 4 байта или 32-битное число с плавающей запятой)
-
vec2
должен быть выровнен по 2N (= 8 байтов) -
vec3
илиvec4
должны быть выровнены по 4N (= 16 байтов) - Матрица
mat4
должна иметь такое же выравнивание, как иvec4
.
Вы можете найти полный список требований к выравниванию в спецификации.
Наш исходный шейдер, в котором было всего три поля
mat4
, отвечал требованиям выравнивания. Размер каждого mat4
равен 64 байта (4 x 4 x 4 = 64), а значит смещение model-матрицы = 0
, смещение view-матрицы = 64
, а proj-матрицы = 128
. Все они кратны 16, поэтому все работало нормально.Новая структура начинается с
vec2
, размер которого всего 8 байт, поэтому все смещения портятся. Теперь смещение model-матрицы = 8
, смещение view-матрицы = 72
, а proj-матрицы = 136
, но ни одно из них не кратно 16. Чтобы решить эту проблему, мы можем использовать спецификатор alignas
, добавленный в C ++ 11:struct UniformBufferObject {
glm::vec2 foo;
alignas(16) glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
Если мы заново скомпилируем и запустим программу, то увидим, что теперь шейдер корректно получает значения матрицы, как раньше.
К счастью, есть способ не думать об этих требованиях к выравниванию постоянно. Мы можем добавить дефайн
GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
прямо перед GLM:#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
#include <glm/glm.hpp>
Это заставит GLM использовать версию
vec2
и mat4
, в которой уже заданы требования к выравниванию. Если вы добавите это определение, то сможете удалить спецификатор alignas
, и программа все равно будет работать.Однако этот метод может не сработать при использовании вложенных структур. Рассмотрим следующее определение в коде C ++:
struct Foo {
glm::vec2 v;
};
struct UniformBufferObject {
Foo f1;
Foo f2;
};
И определение в шейдере:
struct Foo {
vec2 v;
};
layout(binding = 0) uniform UniformBufferObject {
Foo f1;
Foo f2;
} ubo;
В этом случае
f2
будет иметь смещение 8
, тогда как оно должно быть 16
, поскольку это вложенная структура. В таком случае вам нужно самим указать выравнивание:struct UniformBufferObject {
Foo f1;
alignas(16) Foo f2;
};
Как видите, выравнивание лучше прописывать явно. Таким образом, вы не будете застигнуты врасплох неожиданными ошибками.
struct UniformBufferObject {
alignas(16) glm::mat4 model;
alignas(16) glm::mat4 view;
alignas(16) glm::mat4 proj;
};
Не забудьте перекомпилировать шейдер после удаления поля
foo
.Множества сетов дескрипторов
Вы можете привязать несколько сетов дескрипторов одновременно. Для этого при создании layout-а конвейера необходимо указать несколько layout-ов дескрипторов (по одному на каждый сет). После этого шейдеры могут ссылаться на определенные сеты дескрипторов следующим образом:
layout(set = 0, binding = 0) uniform UniformBufferObject { ... }
Вы можете использовать это, чтобы разделить сеты на общие для всех объектов и специфичные для каждого объекта. В таком случае вам удастся избежать повторной привязки большинства дескрипторов между вызовами отрисовки, что потенциально более эффективно.
Код С++
/ Вершинный шейдер
/ Фрагментный шейдер
lazil
Это не перевод, а какой-то транслит.