Vulkan Learning — 7. Дескрипторы. Запись команд в командный буфер

Итак, нам осталось рассмотреть привязку данных наших трехмерных объектов (mesh, uniform variables, textures etc.) к пайплайну. Делается это при помощи различных команд, которые записываются в командный буфер. Начнем рассмотрение записи команд в буфер с некоего базового скелета:

// Record commands to the command buffers.
// 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();
}

Итак, перед тем, как записать в командный буфер команды, которые привязывают данные трехмерной модели к пайплайну, мы записываем в буфер команды, которые:

  1. Задают значение настроек, указанных при создании пайплайна как dynamic states (например, команды vkCmdSetViewport и vkCmdSetScissor).
  2. Начинают указанный RenderPass. Указывается также Framebuffer, в который будет осуществляться отрисовка.
  3. Привязывают указанный пайплайн к командному буферу.

Заметим, что порядок записи команд важен (см. 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_COUNT = 1;
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());

С привязкой индексов всё совсем просто — мы указываем буфер, из которого берутся индексы, смещение, начиная с которого они берутся, и тип данных, который соответствует одному индексу:

commandBuffer.bindIndexBuffer(
    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:

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
    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):

// ******************* VERTEX SHADER *******************
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 и одной текстурой:

vk::DescriptorPool CreateDescriptorPool()
{
    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’а:

vk::DescriptorSet CreateDescriptorSet()
{
    // 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 и одной текстуры:

vk::DescriptorSet CreateDescriptorSet()
{
    ...
    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. Повторим, что константы хранятся непосредственно в командном буфере, поэтому при их изменении весь командный буфер надо перезаписывать с самого начала. Команда, которая запихивает константу в командный буфер, выглядит так:

// Push constants to given shader stage directly
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.

// Command to execute pipeline
commandBuffer.drawIndexed(
    mesh.IndexCount, // index count
    1,  // instance count
    0,  // first index
    0,  // vertex offset
    0); // first instance

Теперь нарисуем окончательную объектную модель программы:

Рис. 1 — Объектная модель

На этом всё. В заключительной заметке мы рассмотрим multipass rendering.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *