Итак, нам осталось рассмотреть привязку данных наших трехмерных объектов (mesh, uniform variables, textures etc.) к пайплайну. Делается это при помощи различных команд, которые записываются в командный буфер. Начнем рассмотрение записи команд в буфер с некоего базового скелета:
// The commands bind resources (pipeline, uniforms, textures) and do drawing.
// The usual practice is to prerecord a set of commands to the command buffer once
// and then in the rendering loop submit them to the command queue on each iteration.
void RecordCommands(
const vk::CommandBuffer& commandBuffer,
const vk::Framebuffer& framebuffer,
const vk::RenderPass& renderPass,
const vk::Extent2D& swapchainExtent,
const engine::mat4& worldToScreen)
{
#ifdef USE_DYNAMIC_STATES
// Viewports define the transformation from the image to the framebuffer
commandBuffer.setViewport(0, vk::Viewport { ... });
// Any pixels outside the scissor rectangles will be discarded by the rasterizer
commandBuffer.setScissor(0, vk::Rect2D { ... });
#endif
// Information about how to begin a render pass (only needed for graphical applications)
// List of clear values
std::array<vk::ClearValue, 2> clearValues
{
vk::ClearColorValue(std::array<float, 4>{0.0f, 0.0f, 0.0f, 1.0f}), // black
vk::ClearDepthStencilValue{1.0f} // Depth Attachment Clear Value
};
vk::RenderPassBeginInfo renderPassBeginInfo
{
.renderPass = renderPass, // Render Pass to begin
.framebuffer = framebuffer,
.renderArea =
{
.offset = {0, 0}, // Start point of render pass in pixels
.extent = swapchainExtent // Size of region to run render pass on (starting at offset)
},
// pClearValues is a pointer to an array of clearValueCount VkClearValue structures that contains
// clear values for each attachment, if the attachment uses a loadOp value of
// VK_ATTACHMENT_LOAD_OP_CLEAR or if the attachment has a depth/stencil format and
// uses a stencilLoadOp value of VK_ATTACHMENT_LOAD_OP_CLEAR. The array is indexed
// by attachment number. Only elements corresponding to cleared attachments are used.
// Other elements of pClearValues are ignored.
// https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkRenderPassBeginInfo.html
.clearValueCount = clearValues.size(),
.pClearValues = clearValues.data(),
};
// The second parameter:
// VK_SUBPASS_CONTENTS_INLINE
// The render pass commands will be embedded in the primary command buffer itself and no
// secondary command buffers will be executed.
// VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS
// The render pass commands will be executed from secondary command buffers.
commandBuffer.beginRenderPass(renderPassBeginInfo, vk::SubpassContents::eInline);
{
// Binding pipeline here tells the GPU how to render the graphics primitives that are coming later.
// VK_PIPELINE_BIND_POINT_GRAPHICS tells the GPU that this is a graphics pipeline instead of a
// compute pipeline. Note that since this command is a command buffer command, it is possible
// for a program to define several graphics pipelines and switch between them in a single command
// buffer. https://vulkan.lunarg.com/doc/view/1.2.170.0/linux/tutorial/html/15-draw_cube.html
commandBuffer.bindPipeline(
vk::PipelineBindPoint::eGraphics,
m_GraphicsPipeline.Pipeline());
for (int iModel; iModel < nModels; iModel++)
{
// *** BIND 3D MODEL'S DATA ***
// bind vertex buffer
// bind index buffer
// bind descriptor sets
// push constants
// *** DRAW ***
}
}
commandBuffer.endRenderPass();
}
Итак, перед тем, как записать в командный буфер команды, которые привязывают данные трехмерной модели к пайплайну, мы записываем в буфер команды, которые:
- Задают значение настроек, указанных при создании пайплайна как dynamic states (например, команды vkCmdSetViewport и vkCmdSetScissor).
- Начинают указанный RenderPass. Указывается также Framebuffer, в который будет осуществляться отрисовка.
- Привязывают указанный пайплайн к командному буферу.
Заметим, что порядок записи команд важен (см. Specification — 7.2. Implicit Synchronization Guarantees), так как от него, в оговоренных в спецификации случаях, зависит порядок выполнения команд. Например, перед тем, как привязывать данные трехмерной модели к пайплайну, нужно привязать сам пайплайн к командному буферу, иначе всё сломается.
После записи всех нужных команд, включая команды привязывания данных трехмерных моделей и команд рисования, мы должны завершить RenderPass путем записи команды vkCmdEndRenderPass. Теперь перейдем к командам привязывания данных трехмерных моделей:
Vertex Buffer Binding (привязка вершинных данных)
Привязка вершинных данных проста — мы указываем буфер, из которого берутся данные и смещение в нем, начиная с которого они берутся. Но мы также указываем точку привязки (binding point) вершинных данных: дело в том, что эти данные могут быть, в порядке архитектурного решения, разбиты на несколько буферов памяти — тогда каждый буфер привязывается к своей точке привязки. Вопрос с точками привязки вершинных данных мы уже рассматривали в заметке Vulkan Learning — 4. Pipeline. Descriptor Sets. Push Constants.
const uint32_t VERTEX_BINDING_POINT = 0;
// Buffers to bind
std::array<vk::Buffer, VERTEX_BINDING_COUNT> vertexBuffers
{
model3d.VertexBuffer
};
// Offsets into buffers being bound
std::array<vk::DeviceSize, VERTEX_BINDING_POINT> offsets
{
0
};
commandBuffer.bindVertexBuffers(
VERTEX_BINDING_POINT, // first binding point
VERTEX_BINDING_COUNT, // binding count
vertexBuffers.data(),
offsets.data());
С привязкой индексов всё совсем просто — мы указываем буфер, из которого берутся индексы, смещение, начиная с которого они берутся, и тип данных, который соответствует одному индексу:
model3d.IndexBuffer, // buffer
0, // offset
vk::IndexType::eUint16); // index type
Descriptor Sets Binding (привязка дескрипторов)
О дескрипторах у нас тоже уже шла речь в заметке Vulkan Learning — 4. Pipeline. Descriptor Sets. Push Constants. Дескрипторы содержат ссылки на uniform variables, текстуры, textures sampler’ы и input attachment’ы (с последними будем разбираться в следующей заметке). Их может быть много и они могут быть сгруппированы в несколько descriptor set’ов, причем каждый descriptor set привязывается к нашему пайплайну независимо от других. Пайплайн же ожидает, что к нему поступит определенный набор descriptor set’ов — поэтому при создании пайплайна ему передается массив descriptor set layout’ов. Иметь набор из нескольких descriptor set’ов может быть удобно с той точки зрения, что ряд дескрипторов может оказаться одним и тем же сразу для нескольких (или всех) трехмерных моделей, в то время как другой набор дескрипторов может оказаться у каждой модели уникальным — тогда имеет смысл хранить два различных descriptor set’а. Эта тема хорошо освещена в заметках Vulkan Shader Resource Binding и Kyle Halladay — Lessons Learned While Building a Vulkan Material System. Команда привязки descriptor set’ов умеет привязывать сразу целый массив descriptor set’ов, который передается в качестве параметра в функцию vkCmdBindDescriptorSets:
vk::PipelineBindPoint::eGraphics, // We need to specify if we want to bind descriptor sets to the graphics or compute pipeline
pipelineLayout, // Pipeline layout object used to program the bindings
0, // First descriptor set
1, // The number of descriptor sets to bind
&globalDescriptorSet,
0, nullptr); // Dynamic offsets for a dynamic uniform buffer
for (int iModel; iModel < nModels; iModel++)
{
commandBuffer.bindDescriptorSets(
vk::PipelineBindPoint::eGraphics, // We need to specify if we want to bind descriptor sets to the graphics or compute pipeline
pipelineLayout, // Pipeline layout object used to program the bindings
1, // First descriptor set
1, // The number of descriptor sets to bind
&perModelDescriptorSet[iModel],
0, nullptr); // Dynamic offsets for a dynamic uniform buffer
...
}
Напомним, что индекс descriptor set’а и binding обязательно указывается в шейдерах, чтобы можно было сопоставить дескрипторы, которые мы предоставляем пайплайну, с переменными внутри шейдеров. Тут уместно будет рассмотреть несколько примеров корректного использования layout qualifier’ов set и binding (см. также GLSL Specification — 12.2.3. Vulkan Only: Descriptor Sets):
layout(set = 0, binding = 0) uniform UniformVar1 { ... };
layout(set = 0, binding = 1) uniform UniformVar2 { ... };
// If "set" layout qualifier is not specified then it defaults to "set = 0"
layout(binding = 2) uniform UniformVar3 { ... };
layout(set = 1, binding = 0) uniform UniformVar4 { ... };
layout(set = 1, binding = 1) uniform UniformVar5 { ... };
layout(set = 1, binding = 2) uniform UniformVar6 { ... };
// ****************** FRAGMENT SHADER ******************
layout(set = 3, binding = 0) uniform UniformVar7 { ... };
layout(set = 3, binding = 1) uniform UniformVar8 { ... };
layout(set = 3, binding = 2) uniform UniformVar9 { ... };
// ALSO CORRECT: variables belonging to the same descriptor set can still be defined in different shaders
layout(set = 0, binding = 4) uniform UniformVar9 { ... };
// AND THIS IS ALSO CORRECT: the same variable can be used in several shaders
layout(set = 0, binding = 0) uniform UniformVar1 { ... };
Создание Descriptor Sets’ов
Мы рассмотрели привязку descriptor set’ов, но ведь их нужно ещё и создать. Для создания descriptor set’ов нужно сначала создать объект под названием descriptor pool. Descriptor pool — это некая абстракция, которая ведет себя примерно как куча — из нее можно создать descriptor set (иногда его можно также освободить, т. е. вернуть занятую память в pool). Descriptor pool имеет определенные ограничения, которые указываются при его создании, а именно: максимальное число descriptor set’ов, которые могут быть созданы из этого pool’а; перечисление типов дескрипторов (uniform variable, texture, texture sampler, input attachment, etc.) с указанием максимального числа дескрипторов каждого типа, которые могут быть созданы из данного pool’а. Хорошее и короткое объяснение этих ограничений можно почитать на форуме Reddit. Приведу простой пример, в котором создается пул, из которого планируется затем создавать один descriptor set с одной uniform variable и одной текстурой:
{
std::array<vk::DescriptorPoolSize, 2> poolSizes
{
vk::DescriptorPoolSize
{
.type = vk::DescriptorType::eUniformBuffer, // descriptor type (uniform variable)
.descriptorCount = MAX_UNIFORMS // the number of descriptors of the specified type which can be allocated in total from the pool
},
vk::DescriptorPoolSize
{
.type = vk::DescriptorType::eCombinedImageSampler, // descriptor type (combined Image descriptor + Sampler descriptor)
.descriptorCount = MAX_TEXTURES // the number of descriptors of the specified type which can be allocated in total from the pool
}
};
vk::DescriptorPoolCreateInfo poolInfo
{
.maxSets = MAX_MODELS, // the maximum number of descriptor sets that may be allocated from the pool
.poolSizeCount = poolSizes.size(),
.pPoolSizes = poolSizes.data(),
};
return logicalDevice.createDescriptorPool(poolInfo);
}
Теперь создадим descriptor set. Для этого нам нужно будет указать его DescriptorSetLayout — тот самый, который также указывается при создании Pipeline’а:
{
// We need all the copies of the layout because the next function expects an array matching the number of sets.
std::array<vk::DescriptorSetLayout, 1> layouts
{
descriptorSetLayout
};
vk::DescriptorSetAllocateInfo allocInfo
{
.descriptorPool = descriptorPool,
.descriptorSetCount = layouts.size(),
.pSetLayouts = layouts.data()
};
auto descriptorSets = m_VkGraphicsContext.LogicalDevice().allocateDescriptorSets(allocInfo);
...
}
Но такими действиями мы только выделили память для DescriptorSet’а. Теперь надо записать в него дескрипторы. Дескрипторы создаются для разных типов ресурсов (переменная, текстура и пр.), поэтому и сами они содержат отличающуюся информацию (например структуры DescriptorBufferInfo и DescriptorImageInfo — см. ниже). Для примера запишем в DescriptorSet два дескриптора для одной uniform variable и одной текстуры:
{
...
auto descriptorSets = m_VkGraphicsContext.LogicalDevice().allocateDescriptorSets(allocInfo);
// ******** UNIFORM VARIABLE DESCRIPTOR ********
// This structure specifies the buffer and the region within it that contains the data for the descriptor
vk::DescriptorBufferInfo bufferInfo
{
.buffer = uniformBuffer,
.offset = 0, // offset to the buffer where the data begin
.range = uniformBufferSize // size of the data
};
// Info about what to write to a single descriptor (there can be many of them in a descriptor set)
vk::WriteDescriptorSet descriptorWriteUniformVariable
{
.dstSet = descriptorSets[0], // Descriptor set to update
.dstBinding = 0, // Uniform buffer binding
.dstArrayElement = 0, // Descriptors can be arrays, so we also need to specify
// the first index in the array that we want to update.
.descriptorCount = 1, // How many array elements you want to update
.descriptorType = vk::DescriptorType::eUniformBuffer,
.pBufferInfo = &bufferInfo
};
// ******** TEXTURE + TEXTURE SAMPLER DESCRIPTOR ********
vk::DescriptorImageInfo descriptorImageInfo
{
.sampler = textureSampler,
.imageView = textureImageView,
.imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal // Image layout when it is in use
};
// Info about what to write to a single descriptor (there can be many of them in a descriptor set)
vk::WriteDescriptorSet descriptorWriteTexture
{
.dstSet = descriptorSets[0], // Descriptor set to update
.dstBinding = 1, // Binding index inside shader
.dstArrayElement = 0, // Descriptors can be arrays, so we also need to specify
// the first index in the array that we want to update.
.descriptorCount = 1, // How many array elements you want to update
.descriptorType = vk::DescriptorType::eCombinedImageSampler,
.pImageInfo = &descriptorImageInfo
};
// ******** WRITE DESCRIPTORS TO DESCRIPTOR SET ********
std::array<vk::DescriptorImageInfo, 2> descriptors
{
descriptorWriteUniformVariable,
descriptorWriteTexture
}
logicalDevice.updateDescriptorSets(
descriptors.size(), // the number of descriptors to write to
descriptors.data(), // array of WriteDescriptorSet
0, nullptr); // array of CoypDescriptorSet
return descriptorSets[0];
}
Pushing Push Constants
О push constant’ах мы уже говорили в заметке Vulkan Learning — 4. Pipeline. Descriptor Sets. Push Constants. Повторим, что константы хранятся непосредственно в командном буфере, поэтому при их изменении весь командный буфер надо перезаписывать с самого начала. Команда, которая запихивает константу в командный буфер, выглядит так:
commandBuffer.pushConstants(
pipelineLayout,
vk::ShaderStageFlagBits::eVertex, // Shader stage to push constants to
0, // offset into the data
sizeof(pushConstant), // size of constant being pushed
&pushConstant); // pValues
Drawing
Поскольку мы привязали к пайплайну все необходимые данные, можно переходить непосредственно к рисованию. Есть несколько команд, которые его выполняют, почитать о них можно в спецификации: 21.3. Programmable Primitive Shading. Для отрисовки одного объекта логично воспользоваться командой vkCmdDrawIndexed. Напомним, что при отрисовке важную роль играет параметр primitive topology, который при создании пайплайна мы указываем в структуре vk::PipelineInputAssemblyStateCreateInfo — то, как из вершин будут собираться примитивы (треугольники); в нашем примере primitive topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST.
commandBuffer.drawIndexed(
mesh.IndexCount, // index count
1, // instance count
0, // first index
0, // vertex offset
0); // first instance
Теперь нарисуем окончательную объектную модель программы:
На этом всё. В заключительной заметке мы рассмотрим multipass rendering.