Vulkan Learning — 4. Pipeline. Descriptor Sets. Push Constants

Следующий большой объект в объектной модели Vulkan — это Pipeline. Представляет он собой некий конвейер, через который проходят данные (вершины, текстуры и прочее), чтобы на выходе получилось изображение (то, которое мы увидим на экране). Несколько стадий пайплайна (и очень важных стадий) являются программируемыми — для них пишутся программы, которые называются «шейдеры». Этих стадий сейчас четыре:

  • Vertex shader
  • Geometry shader
  • Tesselation shader
  • Fragment shader

Но есть и не программируемые, однако конфигурируемые стадии, например следующие:

  • Primitive assembly
  • Clipping
  • Rasterization
  • Blending
  • Depth test

Объект pipeline аккумулирует в себе программы для программируемых стадий и различные настройки непрограммируемых. Но на входе pipeline также получает входную информацию, и формат ее должен быть описан. Что это за входная информация:

  • Вершины (vertices, vertex data, mesh data) — задают геометрию 3-хмерного объекта.
  • Переменные (в OpenGL это называется uniform variables) — хранятся в буферах памяти. Примеры: матрица Model-View-Projection, координаты и яркость источников света и прочее.
  • Текстуры (textures) — изображения, пиксели которых считывают шейдеры. Обычно эти изображения накладываются на какие либо поверхности в сцене.
  • Сэмплеры текстур (texture samplers) — объекты, которые хранят настройки алгоритма считывания пикселей из текстуры (например, фильтрация, уровни детализации и пр.)
  • Push constants (переменные, которые передаются через командные буферы, передаются более эффективно, но сильно ограничены по размеру)

Где используется ссылка на pipeline. При записи в буфер команд (об это речь в следующей заметке) команды, которая «активирует» данный pipeline — это делается путем вызова функции CommandBuffer::bindPipeline().

Шейдеры

Шейдеры — это кусочки кода, которые формируют программу, которую выполняет GPU. Код этот пишется на таких языках как GLSL или HLSL. Обычно в графических программах используется как минимум два шейдера (vertex shader и fragment shader). Шейдеры должны быть скомпилированы в единое целое — программу. В OpenGL компиляция шейдеров осуществляется программно путем вызова функций, в которые передается непосредственно исходный текст шейдеров на языке GLSL. В Vulkan исходный текст на языке GLSL предварительно компилируется в промежуточный код на языке SPIR-V, и делается это при помощи компилятора glslc.exe, который входит в состав Vulkan SDK. Для компиляции шейдеров я написал простенький батник:

@echo off

Rem The advantage of glslc is that it uses the same parameter format as

Rem well-known compilers like GCC and Clang and includes some extra
Rem functionality like includes. Both of them are already included in the
Rem Vulkan SDK, so you don't need to download anything extra.
Rem (https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipeline_basics/Shader_modules)
set GLCC=glslc.exe

%GLCC% shader.vert -o vert.spv
%GLCC% shader.frag -o frag.spv

pause

Здесь shader.vert и shader.frag — это текстовые файлы с исходным кодом на языке GLSL. После компиляции мы будем иметь два файла vert.spv и frag.spv.

Ниже приведен кусочек псевдокода, касающийся загрузки кода шейдеров и использования их при создании пайплайна:

vk::Pipeline CreateGraphicsPipeline(
    ...,
    const std::string& vertexShaderFile,
    const std::string& fragmentShaderFile)
{
    // Read in SPIR-V code of shaders
    auto vertexShaderCode = ReadAllBin(vertexShaderFile);
    auto fragmentShaderCode = ReadAllBin(fragmentShaderFile);

    // Build shader modules
    auto vertexShader = CreateShaderModule(logicalDevice, vertexShaderCode);
    auto fragmentShader = CreateShaderModule(logicalDevice, fragmentShaderCode);

    // Describe shader stages
    std::array<vk::PipelineShaderStageCreateInfo, 2> shaderStageCreateInfos
    {
        vk::PipelineShaderStageCreateInfo
        {
            .stage = vk::ShaderStageFlagBits::eVertex,
            .module = vertexShader,
            .pName = "main",               // shader entry point
            .pSpecializationInfo = nullptr // allows you to specify values for shader constants
        },
        vk::PipelineShaderStageCreateInfo
        {
            .stage = vk::ShaderStageFlagBits::eFragment,
            .module = fragmentShader,
            .pName = "main",               // shader entry point
            .pSpecializationInfo = nullptr // allows you to specify values for shader constants
        }
    };

    ...........................................................................................

    // -- GRAPHICS PIPELINE CREATION --
    vk::GraphicsPipelineCreateInfo pipelineInfo
    {
        .stageCount = shaderStageCreateInfos.size(), // Number of shader stages
        .pStages = shaderStageCreateInfos.data(), // List of shader stages
        ...
    };
    vk::Pipeline pipeline = logicalDevice.createGraphicsPipeline(
        vk::PipelineCache(), pipelineInfo).value;

    // Destroy shader modules because they are no longer needed
    logicalDevice.destroyShaderModule(vertexShader);
    logicalDevice.destroyShaderModule(fragmentShader);

    return pipeline;
}

vk::ShaderModule CreateShaderModule(
    const vk::Device& logicalDevice,
    const std::vector<unsigned char>& compiledCode)
{
    return logicalDevice.createShaderModule(
        vk::ShaderModuleCreateInfo
        {
            .codeSize = compiledCode.size(),
            .pCode = reinterpret_cast<const uint32_t*>(compiledCode.data())
        });
}

std::vector<unsigned char> ReadAllBin(const std::string& filename)
{
    std::ifstream fileStream(filename, std::ios::binary);

    if (!fileStream)
        throw std::runtime_error("util::File::ReadAllBin() -> Cannot open file: " + filename);

    return std::vector<unsigned char>(
        std::istreambuf_iterator<char>(fileStream),
        std::istreambuf_iterator<char>());
}

Вершины (vertices), вершинные атрибуты (Vertex attributes)

Один из наиболее важных и очевидных видов входной информации для пайплайна — это вершинные данные (vertices) — они описывают геометрию 3-хмерного объекта или, другими словами, формируют 3-хмерную сетку (каркас, mesh). Каждая вершина характеризуется в первую очередь своими координатами в 3-хмерном пространстве (пространство модели — model space), но помимо координат также может содержать и другую информацию, чаще всего это — координаты текстуры (uv-coordinates), реже — нормали, цвета и прочее — все эти отдельные кусочки информации называются вершинными атрибутами (vertex attributes). Обычно вершинные данные мы описываем на языке C++ в виде структуры:

struct VertexPT
{
    alignas(16) vec3 Position;
    alignas(8)  vec2 TextureCoords;
}

Формат куска данных, который характеризует отдельную вершину должен быть описан при создании пайплайна в виде массива структур vk::VertexInputAttributeDescription. Создание этого массива можно поместить в отдельную функцию:

std::vector<vk::VertexInputAttributeDescription> VertexAttributes(uint32_t binding)
{
    using vertex_type = engine::VertexPT;

    // How the data fo an attribute is defined within a vertex
    return std::vector<vk::VertexInputAttributeDescription>
    {
        vk::VertexInputAttributeDescription
        {
            .location = 0, // Location in shader where data will be read from
            .binding = binding, // Which binding the data is at
            .format = vk::Format::eR32G32B32Sfloat, // Format the data will take (also helps define size of data)
            .offset = offsetof(vertex_type, vertex_type::Position) // Where this attribute is defined in the data for a single vertex
        },
        vk::VertexInputAttributeDescription
        {
            .location = 1, // Location in shader where data will be read from
            .binding = binding, // Which binding the data is at
            .format = vk::Format::eR32G32Sfloat, // Format the data will take (also helps define size of data)
            .offset = offsetof(vertex_type, vertex_type::TextureCoords) // Where this attribute is defined in the data for a single vertex
        }
    };
}

Каждый вершинный атрибут идентифицируется целочисленным индексом location, который указывается в вершинном шейдере (vertex shader):

// Vertex shader
layout(location = 0) in vec3 position;
layout(location = 1) in vec4 color;
layout(location = 2) in vec2 texture_coord;

Важный момент. Обычно вершинные данные берутся из одного единственного буфера, но это не обязательно. Мы можем создать например такой pipeline, который будет брать первые два атрибута (position и color) из одного буфера, а третий атрибут (texture_coord) из другого буфера. Наглядный пример подобного пайплайна показан в коде SashaWillems’а. Каждому буферу будет соответствовать уникальный индекс так называемой точки привязки (binding point). Заметим, что целочисленные индексы точек привязки вершинных буферов еще понадобятся нам при записи команд в командный буфер (kCmdBindVertexBuffers).

Рис. 1 — Vertex binding points and locations

Ожидаемый пайплайном формат данных в каждом из используемых вершинных буферов (каждый из которых привязывается к определенной точке привязки), описывается в массиве структур vk::VertexInputBindingDescription. В примере ниже этот массив состоит из одного элемента, поскольку используется всего один вершинный буфер. Ниже приведен кусочек псевдокода, относящийся к описанию вершинных данных при создании пайплайна:

vk::Pipeline CreateGraphicsPipeline(...)
{
    // -- VERTEX INPUT --

    uint32_t vertex_binding = 0;

    // How the data for a single vertex (position, color, texture coords,
    // normals, etc) is as a whole
    std::array<vk::VertexInputBindingDescription, 1> vertexBindingDescriptions
    {
        vk::VertexInputBindingDescription
        {
            .binding = vertex_binding, // The binding point to which a vertex buffer will be bound
            .stride = sizeof(VertexPT), // Size of a single vertex object
            .inputRate = vk::VertexInputRate::eVertex // No instanced drawing
        }
    };

    // How the data of an attribute is defined within a vertex
    auto vertexAttribDescriptions = VertexAttributes(vertex_binding);

    vk::PipelineVertexInputStateCreateInfo vInputStateCreateInfo
    {
        // spacing between data and whether the data is per-vertex or per-instance
        .vertexBindingDescriptionCount = vertexBindingDescriptions.size(),
        .pVertexBindingDescriptions = vertexBindingDescriptions.data(),

        // type of the attributes passed to the vertex shader, which binding
        // to load them from and at which offset
        .vertexAttributeDescriptionCount = vertexAttribDescriptions.size(),
        .pVertexAttributeDescriptions = vertexAttribDescriptions.data(),
    };

    // -- GRAPHICS PIPELINE CREATION --
    vk::GraphicsPipelineCreateInfo pipelineInfo
    {
        .pVertexInputState = &vInputStateCreateInfo
        ...
    };
    vk::Pipeline pipeline = logicalDevice.createGraphicsPipeline(
        vk::PipelineCache(), pipelineInfo).value;

    return pipeline;
}

И наконец, забегая в тему следующей заметки, упомянем следующее. Мы создали пайплайн и определили в нем точки привязки вершинных данных. Для отрисовки 3-хмерного объекта мы должны будем в какой-то момент «пристыковать» к пайплайну конкретные вершинные буферы (в примере ниже буфер всего один). Делается это путем выполнения команд, которые записываются в командный буфер (это и есть тема следующей заметки):

void RecordCommands()
{
    ...
    const size_t BINDING_COUNT = 1;
    // Buffers to bind
    std::array<vk::Buffer, BINDING_COUNT> vertexBuffers
    {
        vertexBuffer
    };
    // Offsets into buffers being bound
    std::array<vk::DeviceSize, BINDING_COUNT> offsets
    {
        0
    };
    commandBuffer.bindVertexBuffers(
        0,             // first binding point
        BINDING_COUNT, // binding count
        vertexBuffers.data(),
        offsets.data());
    ...
}

Fixed-Function Part of The Pipeline

Давно хотел извиниться перед читателями за мешанину из русского текста и англоязычных терминов. Просто я понятия не имею как можно было бы их перевести на русский (см. заголовок выше). В этом разделе я просто перечислю ряд настроек, которые относятся к непрограммируемой (но конфигурируемой) части пайплайна:

  • Input Assembly State — описывает то, каким образом собирать вершины в примитивы (треугольники, линии, точки). Самые ходовые опции: eTriangleList, eTriangleStrip, eTriangleFan).
  • Viewport State — описывает то, каким образом нормализованные координаты (-1…+1) трансформируются в координаты пикселей фреймбуфера.
  • Rasterization State — настраивает этап растеризации. В частности такие вещи как тип примитивов (линии, треугольники или точки) и face culling (отображение только передних или только задних граней объектов).
  • Multisample State — как вычислять цвет пикселя, который накрывается более чем одним примитивом (более чем одним треугольником). Это имеет место на границах примитивов, где проявляются так называемые эффекты aliasing’а.
  • Depth Stencil State — настраивает depth testing — механизм, который необходим для того, чтобы более близкие к камере объекты отрисовывались поверх более далеких.
  • Color Blend State — настраивает так называемое смешивание, которое используется, чтобы создать эффект прозрачности (или полупрозрачности) отрисовываемых обхектов. В этом случае цвета пикселей таких объектов нужно смешать по определенному алгоритму.

Все эти настройки довольно самоочевидны, поэтому далее я приведу только исходный код с комментариями.

vk::Pipeline CreateGraphicsPipeline(...)
{
    // -- INPUT ASSEMBLY --
    // Describes two things: what kind of geometry will be drawn
    // from the vertices and if primitive restart should be enabled
    vk::PipelineInputAssemblyStateCreateInfo inputAssemblyCreateInfo
    {
        .topology = vk::PrimitiveTopology::eTriangleList, // type of primitives to be assembled from vertices
        .primitiveRestartEnable = VK_FALSE                // relevant for strips and fans (to start new strip or fan)
    };

    // -- VIEWPORT & SCISSOR --
    // Viewports define the transformation from the image to the framebuffer
    vk::Viewport viewport
    {
        .x = 0.0f,
        .y = 0.0f,
        .width = static_cast<float>(swapchainExtent.width),
        .height = static_cast<float>(swapchainExtent.height),
        .minDepth = 0.0f,
        .maxDepth = 1.0f
    };

    // Any pixels outside the scissor rectangles will be discarded by the rasterizer
    vk::Rect2D scissor
    {
        .offset = { 0, 0 },
        .extent = swapchainExtent
    };

    // Viewport and scissor rectangle need to be combined into a viewport state.
    // It is possible to use multiple viewports and scissor rectangles on some graphics cards.
    vk::PipelineViewportStateCreateInfo viewportStateCreateInfo
    {
        .viewportCount = 1,
        .pViewports = &viewport,
        .scissorCount = 1,
        .pScissors = &scissor
    };

    // -- RASTERIZER --
    // Many features here if enabled require enabling certain device features (see PhysicalDeviceFeatures).
    vk::PipelineRasterizationStateCreateInfo rasterizerCreateInfo
    {
        .depthClampEnable = VK_FALSE,                  // when set to VK_TRUE: if fragment depth > max_depth then set depth = max_depth
        .rasterizerDiscardEnable = VK_FALSE,           // if set to VK_TRUE, then geometry never passes through the rasterizer stage (useful when you don't need to draw anything on the screen)
        .polygonMode = vk::PolygonMode::eFill,         // fill, lines (wireframe), points
        .cullMode = vk::CullModeFlagBits::eBack,       // don't draw back faces
        .frontFace = vk::FrontFace::eCounterClockwise, // which face is considered front
        .depthBiasEnable = VK_FALSE,                   // whether to add depth bias to fragments (good for stopping shadow acne)
        .lineWidth = 1.0f,                             // describes the thickness of lines in terms of number of fragments
    };

    // -- MULTISAMPLING --
    // It works by combining the fragment shader results of multiple polygons that rasterize to the same pixel.
    // This mainly occurs along edges, which is also where the most noticeable aliasing artifacts occur.
    vk::PipelineMultisampleStateCreateInfo multisamplingCreateInfo
    {
        .rasterizationSamples = vk::SampleCountFlagBits::e1, // number of samples to use per fragment
        .sampleShadingEnable = VK_FALSE                      // enable multisample shading or not
    };

    // -- BLENDING --

    // Blend attachment state contains the configuration for each attached color buffer.
    vk::PipelineColorBlendAttachmentState colorBlendingState
    {
        .blendEnable = VK_TRUE, // enable blending

        // Blending uses equation: (srcCoorBlendFactor * newColor) colorBlendOp (dstColorBlendFactor * oldColor)
        .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha,
        .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha,
        .colorBlendOp = vk::BlendOp::eAdd,

        .srcAlphaBlendFactor = vk::BlendFactor::eOne,  // replace old alpha with the new one
        .dstAlphaBlendFactor = vk::BlendFactor::eZero, // get rid of the old alpha
        .alphaBlendOp = vk::BlendOp::eAdd,

        .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG // which colors to apply blending operations to
                        | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
    };

    // The PipelineColorBlendStateCreateInfo structure references the array of structures for all color attachments.
    vk::PipelineColorBlendStateCreateInfo colorBlendCreateInfo
    {
        .logicOpEnable = VK_FALSE, // alternative to calculations is to use logical operations
        // Each element of the pAttachments array is a VkPipelineColorBlendAttachmentState structure
        // specifying per-target blending state for each individual color attachment
        // of the subpass in which this pipeline is used.
        .attachmentCount = 1,
        .pAttachments = &colorBlendingState
    };

    // -- DEPTH STENCIL TESTING --
    vk::PipelineDepthStencilStateCreateInfo depthStencil
    {
        .depthTestEnable = VK_TRUE,  // Enable checking depth to determine fragment write
        .depthWriteEnable = VK_TRUE, // Enable writing to depth buffer (to replace old values)
        .depthCompareOp = vk::CompareOp::eLess, // If the new depth value is LESS it replaces the old one
        .depthBoundsTestEnable = VK_FALSE, // Depth bounds test: does the depth test fall into the bounds [minDepth...maxDepth]
        .stencilTestEnable = VK_FALSE,
        .minDepthBounds = 0.0f, // Optional
        .maxDepthBounds = 1.0f, // Optional
    };

    // -- GRAPHICS PIPELINE CREATION --
    vk::GraphicsPipelineCreateInfo pipelineInfo
    {
        ...
        .pInputAssemblyState = &inputAssemblyCreateInfo,
        .pViewportState = &viewportStateCreateInfo,
        .pRasterizationState = &rasterizerCreateInfo,
        .pMultisampleState = &multisamplingCreateInfo,
        .pDepthStencilState = &depthStencil,
        .pColorBlendState = &colorBlendCreateInfo
        ...
    };

    vk::Pipeline pipeline = logicalDevice.createGraphicsPipeline(
        vk::PipelineCache(), pipelineInfo).value;

    return pipeline;
}

Dynamic States

Оказывается, что настройки, заданные при создании пайплайна, вообще говоря нельзя изменить без пересоздания самого пайплайна. Однако ограниченный набор настроек можно изменять без пересоздания пайплайна — это делается путем выполнения команд (напомним, что команды записываются в т. н. командный буфер). Эти настройки, которые мы собираемся изменять динамически, должны быть перечислены в виде массива значений vk::DynamicState. В примере ниже мы собираемся изменять настройки viewport’а и scissor’а, потому, что из приходится изменять каждый раз, когда пользователь изменяет размер окна. Соответственно, если мы не хотим пересоздавать pipeline каждый раз, когда это происходит, мы можем воспользоваться dynamic states.

vk::Pipeline CreateGraphicsPipeline(...)
{
    // A limited amount of the state that we've specified in the previous structs can actually
    // be changed without recreating the pipeline. Examples are the size of the viewport, line
    // width and blend constants. If you want to do that, then you'll have to fill in a
    // vk::DynamicState. This will cause the configuration of these values to be ignored and
    // you will be required to specify the data at drawing time.
    // But anyway you still need to recreate the swapchain when the window is resized.
    std::array<vk::DynamicState, 2> dynamicStates
    {
        // Dynamic Viewport: Can resize in command buffer
        // with vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
        vk::DynamicState::eViewport,
        // Dynamic Scissor: Can resize in command buffer
        // with vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
        vk::DynamicState::eScissor
    };

    vk::PipelineDynamicStateCreateInfo dynamicState
    {
        .dynamicStateCount = dynamicStates.size(),
        .pDynamicStates = dynamicStates.data()
    };

    // -- GRAPHICS PIPELINE CREATION --
    vk::GraphicsPipelineCreateInfo pipelineInfo
    {
        ...
        .pDynamicState = &dynamicState
        ...
    };

    vk::Pipeline pipeline = logicalDevice.createGraphicsPipeline(
        vk::PipelineCache(), pipelineInfo).value;

    return pipeline;
}

Теперь мы обязательно должны записать в командный буфер команды, которые зададут настройки, которые мы указали в структуре vk::PipelineDynamicStateCreateInfo:

void RecordCommands()
{
    ...
    const auto& swapchainExtent = m_VulkanSwapchain.SwapChainExtent();

    // Viewports define the transformation from the image to the framebuffer
    vk::Viewport viewport
    {
        .x = 0.0f,
        .y = 0.0f,
        .width = static_cast<float>(swapchainExtent.width),
        .height = static_cast<float>(swapchainExtent.height),
        .minDepth = 0.0f,
        .maxDepth = 1.0f
    };
    commandBuffer.setViewport(0, 1, &viewport);

    // Any pixels outside the scissor rectangles will be discarded by the rasterizer
    vk::Rect2D scissor
    {
        .offset = { 0, 0 },
        .extent = swapchainExtent
    };
    commandBuffer.setScissor(0, 1, &scissor);
    ...
}

Другие поля структуры GraphicsPipelineCreateInfo

В заключении отметим, что помимо различных настроек при создании пайплайна, мы должны указать PipelineLayout (список дескрипторов и констант, см. ниже), а также RenderPass и Subpass, который выполняет наш pipeline (см. предыдущую заметку):

vk::Pipeline CreateGraphicsPipeline(
    ...,
    const vk::PipelineLayout& pipelineLayout,
    const vk::RenderPass& renderPass,
    ...)
{
    ...
    // -- GRAPHICS PIPELINE CREATION --
    vk::GraphicsPipelineCreateInfo pipelineInfo
    {
        ...
        .layout = pipelineLayout, // Pipeline layout pipeline should use
        .renderPass = renderPass, // Pipeline can be used with any render pass compatible with the provided one
        .subpass = 0, // Subpass of render pass to use with pipeline (a pipeline can only be attached to one particular subpass)
                      // Different subpass requires different pipeline.

        // Pipeline Derivatives: Can create multiple pipelines that derive from one another for optimization
        .basePipelineHandle = VK_NULL_HANDLE, // Existing pipeline to derive from...
        .basePipelineIndex = -1, // ... or index of pipeline being created to derive from (in case creating multiple at once)
    };

    vk::Pipeline pipeline = logicalDevice.createGraphicsPipeline(
        vk::PipelineCache(), pipelineInfo).value;

    return pipeline;
}

Дескрипторы (Descriptor Sets)

В самом начале этой заметке упоминалась входная информация, которую получает пайплайн. Среди прочего там были переменные, текстуры и сэмплеры текстур. Формат этой входной информации описывается при помощи объектов, называемых DescriptorSetLayout. Поясним взаимоотношения между понятиями Descriptor, DescriptorSet, DescriptorSetLayout и PipelineLayout (рис. 2).

Рис. 2 — Descriptors, DescriptorSets, DescriptorSetLayouts и PipelineLayout

Каждая переменная (uniform variable) содержится в некоем буфере памяти. Ссылка на этот буфер помещается в Descriptor этой переменной. Несколько переменных группируются в набор под названием DescriptorSet. DescriptorSet прикручивается к Pipeline’у динамически в ходе записи команд в командный буфер (функция vkCmdBindDescriptorSets). Все ресурсы (переменные, текстуры, сэмплеры), которые ожидает наш Pipeline, могут находиться в одном DesctorSet’е или могут быть разбиты на несколько DescriptorSet’ов. В структуре PipelineLayout, которая используется при создании пайплайна, мы указываем, сколько и какие ресурсы ожидает наш пайплайн. PipelineLayout содержит массив структур DescriptorSetLayout, в каждой из которых описывается формат одного из DescriptorSet’ов, который может быть прикручен к пайплайну. DescriptorSetLayout содержит массив структур DescriptorSetLayoutBinding, в которой указан индекс binding соответствующего ресурса (он указан в шейдере) и тип ресурса (переменная, текстура, сэмплер и пр.). Каждая из структур DescriptorSetLayoutBinding соответствует, как нетрудно догадаться, ровно одному Descriptor’у. Итак, все эти Layout’ы задают формат входной информации и указываются при создании Pipeline’а, а DescriptorSet’ы (и Descriptor’ы соответственно), которые должны соответствовать этому формату, прикручиваются к пайплайну динамически в ходе записи команд в командный буфер. Ниже покажем код для создания неких гипотетических Layout’ов:

// Descriptor set layout is a structure describing several (but not necessarily all)
// uniform objects and texture samplers existing inside the shader program. Each descriptor
// set is associated with a certain descriptor set layout. It's possible to have several
// descriptor sets bound to the pipeline that have different descriptor set layouts (those
// layouts are specifiedin the PipelineLayoutCreateInfo (see CreatePipelineLayout).
// Two sets with the same layout are considered to be compatible and interchangeable [Sellers].
static vk::DescriptorSetLayout CreateDescriptorSetLayout(
    const vk::Device& logicalDevice)
{
    // Each uniform buffer or texture sampler has exactly one binding
    std::array<vk::DescriptorSetLayoutBinding, 2> layoutBindings
    {
        // Uniform variable
        vk::DescriptorSetLayoutBinding
        {
            .binding = 0, // Binding index in the shader
            .descriptorType = vk::DescriptorType::eUniformBuffer,
            .descriptorCount = 1, // It is possible for the shader variable to represent an array of uniform buffer objects,
                                  // and descriptorCount specifies the number of values in the array
            .stageFlags = vk::ShaderStageFlagBits::eVertex // In which shader stages the descriptor is going to be referenced
        },
        // Texture + Texture sampler
        vk::DescriptorSetLayoutBinding
        {
            .binding = 1, // Binding index in the shader
            .descriptorType = vk::DescriptorType::eCombinedImageSampler,
            .descriptorCount = 1,
            .stageFlags = vk::ShaderStageFlagBits::eFragment
        }
    };

    // Descriptor set layout describes multiple uniform buffer objects and texture samplers
    // that exist inside the shader program.
    vk::DescriptorSetLayoutCreateInfo layoutInfo
    {
        .bindingCount = layoutBindings.size(),
        .pBindings = layoutBindings.data()
    };

    return logicalDevice.createDescriptorSetLayout(layoutInfo);
}

// -- PIPELINE LAYOUT --
// Uniform values & texture samplers need to be specified during pipeline
// creation by creating a PipelineLayout object because there may be multiple DescriptorSets
// bound and Vulkan wants to know in advance how many and what types of them it should expect.
// Access to descriptor sets from a pipeline is accomplished through a pipeline layout.
// Zero or more descriptor set layouts and zero or more push constant ranges are combined
// to form a pipeline layout object describing the complete set of resources that can be
// accessed by a pipeline. The pipeline layout represents a sequence of descriptor sets
// with each having a specific layout. This sequence of layouts is used to determine the
// interface between shader stages and shader resources. Each pipeline is created using
// a pipeline layout.
static vk::PipelineLayout CreatePipelineLayout(
    const vk::Device& logicalDevice,
    const std::vector<vk::DescriptorSetLayout>& descriptorSetLayouts,
    const vk::PushConstantRange& pushConstantRange)
{
    // It is actually possible to bind multiple descriptor sets simultaneously. You need to
    // specify a descriptor layout for each descriptor set when creating the pipeline layout.
    // Shaders can then reference specific descriptor sets like this:
    //     layout(set = 0, binding = 0) uniform UniformBufferObject { ... }
    // You can use this feature to put descriptors that vary per-object and descriptors
    // that are shared into separate descriptor sets. In that case you avoid rebinding most
    // of the descriptors across draw calls which is potentially more efficient.
    vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo
    {
        .setLayoutCount = descriptorSetLayouts.size(), // the number of different descriptor set layouts inside the shader program
        .pSetLayouts = descriptorSetLayouts.data()     // pointer to an array of descriptor set layouts
    };
    return logicalDevice.createPipelineLayout(pipelineLayoutCreateInfo);
}

Заметим, что индекс descriptor set’а и binging’а обязательно указывается для каждого ресурса, описываемого дескриптором (переменная, текстура, input attachment) прямо внутри шейдера — при помощи layout qualifier’ов:

layout(binding = 0) uniform UniformVar1 { ... };
layout(set = 0, binding = 1) uniform UniformVar2 { ... };
layout(set = 1, binding = 0) uniform UniformVar3 { ... };
layout(set = 1, binding = 1) uniform UniformVar4 { ... };

Если индекс descriptor set’а не указан, то он принимает значение по-умолчанию, равное нулю.

Константы (Push Constants)

Переменные uniform variables хороши, но есть более быстрый механизм передачи входных данных пайплайну, если размер этих данных не превышает 128 байт — это константы (push constants). Удобно использовать константу для передачи в пайплайн матрицы Model-View-Projection. Формат констант должен быть описан все в том же PipelineLayout’е при помощи массива структур PushConstantRange:

// Push constant is a value of a limited size (usually not greater than 128 bytes)
// that is passed as part of commads when recording them into command buffer.
// There can be only one push constant per shader stage.
static vk::PushConstantRange CreatePushConstantRange()
{
    return vk::PushConstantRange
    {
        .stageFlags = vk::ShaderStageFlagBits::eVertex, // Shader stage push constant will go to
        .offset = 0, // Offset into given data to pass to push constant
        .size = sizeof(...) // Size of data being passed
    };
}

static vk::PipelineLayout CreatePipelineLayout(
    const vk::Device& logicalDevice,
    ...,
    const vk::PushConstantRange& pushConstantRange)
{
    vk::PipelineLayoutCreateInfo pipelineLayoutCreateInfo
    {
        ...
        .pushConstantRangeCount = 1,
        .pPushConstantRanges = &pushConstantRange
    };
    return logicalDevice.createPipelineLayout(pipelineLayoutCreateInfo);
}

Константы записываются непосредственно в командный буфер (функция vkCmdPushConstants). Это значит, что каждый раз, когда вы хотите изменить значение константы, вы должны перезаписать командный буфер.

void RecordCommands()
{
    ...
    // Push constants to given shader stage directly
    commandBuffer.pushConstants(
        m_GraphicsPipeline.PipelineLayout(),
        vk::ShaderStageFlagBits::eVertex, // Shader stage to push constants to
        0,                                // offset into the data
        sizeof(pushConstant), // size of constant being pushed
        &pushConstant);       // pValues
    ...
}

Как всегда, напоследок приведу объектную модель программы, дополненную изученной нами информацией:

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

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

Ваш адрес email не будет опубликован.