Рубрики
3D мир Программирование

Vulkan API 4: графический конвейер

В данной заметке рассматривается устройство графического конвейера

Введение

Графический конвейер с точки зрения Vulkan API это последовательность операций, преобразующих массив вершин в пиксели.

Структура графического конвейера

Графический конвейер состоит из 12 этапов: 9 основных представлены на рисунке 1.

Рисунок 1 — Этапы графического конвейера

На рисунке 1 серым цветом обозначены непрограммируемые этапы, а белым обозначены программируемые. Пунктирная линия ведет к блоку с входными данными, под этапом изображен результат выполнения. Стоит заметить что схема является упрощенной — некоторые этапы объединены. Рассмотрим конвейер подробнее:

  • входной сборщик — компонует вершины для создания полигонов из массивов индексов (если используется) и вершин;
  • вершинный шейдер — вызывается для каждой вершины и осуществляет преобразования координат;
  • тесселяция — позволяет увеличить количество полигонов и состоит из нескольких шагов:
    • управляющий шейдер — подготовка параметров тесселяции,
    • генерация примитивов — использует параметры для разбиения примитивов на множество мелких,
    • вычислительный шейдер — работает аналогично вершинному шейдеру и вызывается для каждой вершины, образованной на шаге генерации примитивов,
  • геометрический шейдер — обрабатывает и изменяет примитивы (треугольники, линии, точки);
  • сборка примитивов — группировка вершин, созданные предыдущими шейдерами в примитивы, подходящие для растеризации;
  • отсечение и отбрасывание — определение частей примитивов, влияющих на результат изображения (отбрасываются невидимые примитивы);
  • растеризация — преобразование примитивов к фрагментам, которые становятся пикселями;
  • перфрагментные операции (не отображены на рисунке) — тесты глубины и трафарета (если включены);
  • сборка фрагментов (не отображена на рисунке) — сборка фрагментов растеризации и отправка на вход фрагментного шейдера;
  • фрагментный шейдер — вызывается для каждого фрагмента и определяет в какие фреймбуферы и с каким значением цвета записываются фрагменты;
  • постфрагментные операции (не отображены на рисунке) — перфрагментные операции в случае, если были изменены данные глубины или трафарета фрагментным шейдером;
  • смешение цветов — совмещение различных фрагментов и выходного буфера с учетом прозрачности.

Проходы рендера (renderpass)

Перед созданием графического конвейера потребуется создать объект прохода рендера — объект, содержащий информацию об используемых буферах.

Первое время будет использоваться лишь один цветовой буфер, представляющий из себя одно из изображений цепочки показа.

Важной частью прохода являются прикрепления (attachments) — структура, которая определяет одно изображение в качестве или входного, или выходного, или входного-выходного одновременно одного или нескольких подпроходов.

Примечание: автор считает, что корректно назвать attachment — буфером, но во избежание проблем с пониманием контекста будет использоваться название прикрепление.

Подпроходы (subpasses) — это последовательные операции рендера, зависящие от содержимого фреймбуферов на предыдущих проходах. Первое время в заметках будет использоваться только один подпроход.

Когда в одном проходе рендера содержатся несколько подпроходов Vulkan API способен самостоятельно установить зависимости проходов друг от друга, отслеживая ссылки на подключения и входы/выходы, которые делают их зависимыми между собой. В случае, если зависимости не могут быть представлены простым отношением вход-выход, то необходимо предоставить информацию о зависимостях (dependencies). Работа с зависимостями будет описана подробнее в будущей заметке о многопроходном рендере.

Для создания проходов рендера необходимо заполнить объект структуры о создаваемом объекте VkRenderPassCreateInfo.

Рассмотрим структуру подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • flags — битовая маска флагов, представленная перечислением (enum) VkRenderPassCreateFlagBits (не используется в заметках);
  • attachmentCount — количество прикреплений (attachment);
  • pAttachments — указатель на массив структур VkAttachmentDescription, содержащий информацию об используемых прикреплениях;
  • subpassCount — количество подпроходов (subpass);
  • pSubpasses — указатель на массив структур VkSubpassDescription, содержащий информацию об используемых подпроходах;
  • dependencyCount — количество зависимостей (dependency), в заметке не используется (= 0);
  • pDependencies — указатель на массив структур VkSubpassDependency, содержащий информацию о зависимостях.

Рассмотрим структуру VkAttachmentDescription, содержащую информацию об используемых прикреплениях:

  • flags — битовая маска флагов, представленная перечислением (enum) VkAttachmentDescriptionFlagBits (не используется в заметках);
  • format — формат цветового буфера, соответствует формату списка показа;
  • samples — число образцов (сэмплов) при мультисэмплинге (в заметках используется только 1 образец — VK_SAMPLE_COUNT_1_BIT);
  • loadOp — определяет действия с данными буфера перед рендером, которые представлены перечислением (enum) VkAttachmentLoadOp:
    • VK_ATTACHMENT_LOAD_OP_LOAD — буфер будет содержать те данные прошлого прохода,
    • VK_ATTACHMENT_LOAD_OP_CLEAR — буфер очищается в начале прохода рендера,
    • VK_ATTACHMENT_LOAD_OP_DONT_CARE — содержимое буфера не определено;
  • storeOp — определяет действия с данными буфера цвета и глубины после рендера, которые представлены перечислением (enum) VkAttachmentStoreOp:
    • VK_ATTACHMENT_STORE_OP_STORE — содержимое буфера сохраняется в память для дальнейшего использования,
    • VK_ATTACHMENT_STORE_OP_DONT_CARE — после рендеринга буфер больше не используется;
  • stencilLoadOp — определяет действия с данными буфера трафарета перед рендером (возможные значения соответствуют loadOp);
  • stencilStoreOp — определяет действия с данными буфера трафарета после рендера (возможные значения соответствуют storeOp);
  • initialLayout — размещение изображения перед началом рендера;
  • finalLayout — размещение изображения после рендера.

Поля initialLayout и finalLayout будут рассмотрены подробнее в будущей заметке о многопроходном рендере. Сейчас им будет задано значение VK_IMAGE_LAYOUT_UNDEFINED и VK_IMAGE_LAYOUT_PRESENT_SRC_KHR.

Рассмотрим структуру VkSubpassDescription, содержащую информацию об используемых подпроходах:

  • flags — битовая маска флагов, представленная перечислением (enum) VkSubpassDescriptionFlagBits (не используется в заметках);
  • pipelineBindPoint — предназначение подпрохода, должно быть VK_PIPELINE_BIND_POINT_GRAPHICS;
  • inputAttachmentCount — количество входных прикреплений (из которых читают данные);
  • pInputAttachments — указатель на массив с информацией о входных прикреплений;
  • colorAttachmentCount — количество выходных прикреплений (в которые пишут данные);
  • pColorAttachments — указатель на массив с информацией о выходных прикреплений;
  • pResolveAttacments — используется для мультисэмплинга, в заметке не используется (= NULL);
  • pDepthStencilAttachment — указатель на прикрепление для глубины/трафарета;
  • preserveAttachmentCount — количество хранимых на протяжении всего подпрохода прикреплений (не используются в процессе);
  • pPreserveAttachements — указатель массив индексов хранимых на протяжении всего подпрохода прикреплений.

Примечание: pipelineBindPoint может принимать значения VK_PIPELINE_BIND_POINT_COMPUTE для конвейера вычислений и VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR для конвейера трассировки лучей.

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

  • attachment — либо VK_ATTACHMENT_UNUSED, если прикрепление не используется, либо индекс в массиве VkRenderPassCreateInfo::pAttachments;
  • layout — перечисление (enum) VkImageLayout, определяющее макет (layout) буфера.

Примечание: слово layout в контексте структуры VkAttachmentReference определяет тип доступа к буферу, влияющее на производительность, и как следствие используется слово макет. В рамках заметки будет использоваться значение VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL.

Рассмотрим структуру VkSubpassDependency, содержащую информацию о зависимостях:

  • srcSubpass — исходный подпроход, создающий данные;
  • dstSubpass — целевой подпроход, использующий данные;
  • srcStageMask — битовая маска флагов, представленная перечислением (enum) VkPipelineStageFlagBits и определяющая какие стадии конвейера в исходном подпроходе создают данные;
  • dstStageMask — аналогично с srcStageMask битовая маска флагов, определяющая какие стадии подпрохода используют данные;
  • srcAccessMask — битовая маска флагов, представленная перечислением (enum) VkAccessFlagBits и определяющая как каждый из исходных и целевых подпроходов обращается к данным;
  • dstAccessMask — аналогично srcAccessMask;
  • dependencyFlags — битовая маска флагов, представленная перечислением (enum) VkDependencyFlagBits и определяющая область зависимостей для подпрохода:
    • VK_DEPENDENCY_BY_REGION_BIT — подпроход имеет локальные для фреймбуфера зависимости,
    • VK_DEPENDENCY_VIEW_LOCAL_BIT — подпроход имеет более одного представления,
    • VK_DEPENDENCY_DEVICE_GROUP_BIT — подпроход имеет не локальными для устройства.

Для создания проходов рендера добавим в приватную часть класса Vulkan поле VkRenderPass renderPass и метод createRenderpass. Функция vkCreateRenderPass предназначена для создания проходов рендера и принимает в качестве аргументов дескриптор логического устройства, адрес объекта структуры VkRenderPassCreateInfo с данными о создаваемом объекте, адрес аллокатора и адрес дескриптора VkRenderPass. В случае ошибки создадим исключение.

Ниже приведен пример создания проходов рендера:

void Vulkan::createRenderpass()
{
    // Информация о прикреплении
    VkAttachmentDescription colorAttachment{};
    colorAttachment.format = surface.selectedFormat.format;
    colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
    colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
    colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

    // Информация о выходном прикреплении
    VkAttachmentReference colorAttachmentRef{};
    colorAttachmentRef.attachment = 0;
    colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

    // Информация о подпроходе
    VkSubpassDescription subpass{};
    subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
    subpass.colorAttachmentCount = 1;
    subpass.pColorAttachments = &colorAttachmentRef;

    // Зависимости подпрохода
    VkSubpassDependency dependency{};
    dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
    dependency.dstSubpass = 0;
    dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dependency.srcAccessMask = 0;
    dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

    // Информация о создаваемом проходе рендера
    VkRenderPassCreateInfo renderPassInfo{};
    renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
    renderPassInfo.attachmentCount = 1;
    renderPassInfo.pAttachments = &colorAttachment;
    renderPassInfo.subpassCount = 1;
    renderPassInfo.pSubpasses = &subpass;
    renderPassInfo.dependencyCount = 1;
    renderPassInfo.pDependencies = &dependency;

    // Создание проходов рендера
    if (vkCreateRenderPass(logicalDevice, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) 
    {
        throw std::runtime_error("Unable to create render pass");
    }
}

После создания необходимо дополнить метод Vulkan::destroy вызовом функции vkDestroyRenderPass, которая принимает в качестве аргументов дескриптор логического устройства, дескриптор прохода рендера и адрес аллокатора.

Текущий вариант доступен на теге v0.1 репозитория 04.

Шейдеры

Шейдер — подпрограмма, выполняемая на стороне графического устройства в составе конвейера. В данном курсе заметок используются в основном графические шейдеры.

Шейдеры для Vulkan API представляют собой байт-код стандарта SPIR-V, который так же используется для OpenCL. Байт-код компилируется из языка GLSL, используемого для написания шейдеров для OpenGL, с помощью приложения glslc.exe, которое входит в состав Vulkan SDK, установленного в рамках первой заметки.

Приложение glslc принимает в качестве аргументов имя файла с шейдером на языке GLSL, а так же имя выходного файла с ключом -o. Если в процессе установки SDK в переменную среды Windows PATH был добавлен адрес папки с исполняемыми файлами Vulkan, то команду можно вызвать в консоли без указания пути до SDK, в противном случае необходимо писать полный путь до исполняемого файла приложения glslc. Пример компиляции вершинного и фрагментного шейдеров:

glslc.exe shader.vert -o vert.spv
glslc.exe shader.frag -o frag.spv

Важное примечание: для данного приложения важно расширение файла по которому оно определяет тип шейдера.

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

#include <fstream>
// Считывание бинарного файла, содержащего шейдер
void readFile(const char * filename, std::vector<char>& buffer) 
{
    // откроем файл как бинарный и установим курсор в конец файла
    std::ifstream file(filename, std::ios::ate | std::ios::binary);
    // если файл не открыт - генерируем исключение
    if (!file.is_open()) 
    {
        throw std::runtime_error("Can't open file");
    }
    // определим размер файла
    size_t fileSize = (size_t) file.tellg();
    // создадим буфер
    buffer.resize(fileSize);

    // перенесем курсор в начало файла
    file.seekg(0);
    // считаем данные в буфер
    file.read(buffer.data(), fileSize);
    // закроем файл
    file.close();
}

Для создания шейдерного модуля добавим метод к классу Vulkan::createShaderModule, который принимает имя файла и возвращает объект VkShaderModule.

Для создания шейдерного модуля используется функция vkCreateShaderModule, которая принимает в качестве аргументов дескриптор логического устройства, адрес объекта структуры VkShaderModuleCreateInfo, содержащей информацию о создаваемом объекте, адрес аллокатора и адрес дескриптора VkShaderModule. Рассмотрим структуру VkShaderModuleCreateInfo подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • flags — не используется;
  • codeSize — размер скомпилированного шейдера в байтах;
  • pCode — адрес массива, содержащего байт-код шейдера.

Важное примечание: массив содержащий байт-код шейдера должен быть типа uint32_t и для конвертации из char используется reinterpret_cast — который определяет использование четырех элементов char как одного uint32_t.

Пример метода для создания шейдерного модуля:

VkShaderModule Vulkan::createShaderModule(const char * filename)
{
    // буфер для чтения из файла
    std::vector<char> buffer;
    // считаем шейдер из файла
    readFile(filename, buffer);

    // Информация о создаваемом шейдерном модуле
    VkShaderModuleCreateInfo createInfo{};
    createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
    createInfo.codeSize = buffer.size();
    createInfo.pCode = reinterpret_cast<const uint32_t*>(buffer.data());

    // Создание шейдерного модуля
    VkShaderModule shaderModule;
    if (vkCreateShaderModule(logicalDevice, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
        throw std::runtime_error("Unable to create shader module");
    }

    return shaderModule;
}

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

Ниже приведен текст вершинного шейдера на языке GLSL:

#version 450

layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;

layout(location = 0) out vec3 fragColor;

void main() {
    gl_Position = vec4(inPosition, 0.0, 1.0);
    fragColor = inColor;
}

Ниже приведен фрагментный шейдер на языке GLSL:

#version 450

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(fragColor, 1.0);
}

Разбор шейдеров осуществляется в отдельной заметке (в разработке), так как синтаксис соответствует GLSL, который применим как к Vulkan API, так и к OpenGL.

Для использования шейдеров в графическом конвейере необходимо заполнить данные в структуре VkPipelineShaderStageCreateInfo для каждого шейдера. Рассмотрим её подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • flags — битовая маска флагов, представленная перечислением (enum) VkPipelineShaderStageCreateFlagBits (не используется в заметках);
  • stage — тип шейдерного модуля:
    • VK_SHADER_STAGE_VERTEX_BIT — вершинный шейдер,
    • VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT — шейдер управления тесселяцией,
    • VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT — шейдер вычисления тесселяции,
    • VK_SHADER_STAGE_GEOMETRY_BIT — геометрический шейдер,
    • VK_SHADER_STAGE_FRAGMENT_BIT — фрагментный шейдер,
    • VK_SHADER_STAGE_COMPUTE_BIT — вычислительный шейдер;
  • module — шейдерный модуль (образованный при загрузке методом Vulkan::createShaderModule;
  • pName — адрес си-строки в кодировке UTF-8 с названием входной функции;
  • pSpecializationInfo — nullptr или указатель на объект структуры VkSpecializationInfo, описывающий константы специализации (будет рассмотрено в дальнейших заметках).

Текущий вариант доступен на теге v0.2 репозитория 04.

Непрограммируемые части конвейера

Непрограммируемые части конвейера в Vulkan API представляют собой набор состояний, описываемый следующими структурами:

  • входные данные вершин — структура VkPipelineVertexInputStateCreateInfo описывает формат данных вершин, которые передаются в вершинный шейдер;
  • входной сборщик — структура VkPipelineInputAssemblyStateCreateInfo определяет, какая геометрия образуется из вершин и разрешен ли рестарт геометрии для таких геометрий, как line strip и triangle strip;
  • состояние тесселяции — структура VkPipelineTessellationStateCreateInfo настраивает промежуточный этап тесселяции;
  • состояние области просмотра — структура VkPipelineViewportStateCreateInfo содержит данные:
    • область просмотра (viewport) — структура VkViewport описывает координаты от (0,0) до (width, height) и диапазон буфера глубины;
    • прямоугольник отсечения (scissor rectangle) — структура VkRect2D задает прямоугольник в рамках которого остается изображение;
  • растеризатор — структура VkPipelineRasterizationStateCreateInfo описывает способ преобразования примитивов из вершинного шейдера в фрагменты;
  • мультисэмплинг — структура VkPipelineMultisampleStateCreateInfo настраивает мультисэмплинг (в данной заметке не рассматривается);
  • тесты глубины и трафарета — структура VkPipelineDepthStencilStateCreateInfo определяет параметры для буфера глубины и трафарета (в данной заметке не рассматривается);
  • смешивание цветов — структура VkPipelineColorBlendAttachmentState содержит настройки для каждого подключенного фреймбуфера, а структура VkPipelineColorBlendStateCreateInfo — глобальные настройки смешивания цветов (в данной заметке не рассматривается);
  • динамическое состояние — структура VkPipelineDynamicStateCreateInfo содержит массив перечислений (enum) VkDynamicState определяет какие состояния графического конвейера можно изменить без пересоздания;
  • раскладка конвейера — структура VkPipelineLayoutCreateInfo описывает какие Uniform переменные используются в графическом конвейере (в данной заметке не рассматривается).

Важное замечание! В английской версии используется слово «layout», но автор на протяжении курса заметок будет использовать слово «раскладка». В русскоязычном сообществе не используется перевод слова, автор посчитал это слово более хорошей заменой англицизму.

Рассмотрим упомянутые выше структуры, неиспользуемые типовые поля flags и pNext не упоминаются.

VkPipelineVertexInputStateCreateInfo:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
  • vertexBindingDescriptionCount — количество элементов в массиве с данными о вершинных привязках;
  • pVertexBindingDescriptions — адрес массива структур VkVertexInputBindingDescription с данными о вершинных привязках:
    • binding — индекс привязки,
    • stride — шаг массива (расстояния между началами структур),
    • inputRate — переход к следующему элементу:
      • VK_VERTEX_INPUT_RATE_VERTEX — переход к следующему элементу после каждой вершины,
      • VK_VERTEX_INPUT_RATE_INSTANCE — для instanced rendering (не затрагивается в заметках);
  • vertexAttributeDescriptionCount — количество описаний атрибутов вершин;
  • pVertexAttributeDescriptions — адрес массива структур VkVertexInputAttributeDescription с описаниями атрибутов вершин:
    • location — индекс места нахождения используемый шейдером;
    • binding — индекс привязки из которого берутся данные;
    • format — размер и тип данных
      • VK_FORMAT_R32_SFLOAT для float,
      • VK_FORMAT_R32G32_SFLOAT для vec2,
      • VK_FORMAT_R32G32B32_SFLOAT для vec3,
      • VK_FORMAT_R32G32B32A32_SFLOAT для vec4;
    • offset — смещение.

Для определения offset можно использовать макрос из stddef.h или cstddef под названием offsetof, который возвращает смещение в байтах до указанного поля от начала структуры

Важное замечание: индексы привязки (VkVertexInputBindingDescription::binding) не обязательно должны быть непрерывны.

VkPipelineInputAssemblyStateCreateInfo:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
  • topology — перечисление (enum) VkPrimitiveTopology:
    • VK_PRIMITIVE_TOPOLOGY_POINT_LIST — каждая вершина используется для создания точки,
    • VK_PRIMITIVE_TOPOLOGY_LINE_LIST — вершины образуют пары, соединенные линией,
    • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP — каждая следующая вершина образует отрезок с последней обработанной,
    • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST — вершины группируются в тройки, образующие треугольники,
    • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN — первые три вершины образуют треугольник, каждая новая вершина образует треугольник с первой и последней (выглядит как веер),
    • VK_PRIMITIVE_TOPOLOGY_LINE_LIST_WITH_ADJACENCY — каждые четыре вершины образуют примитив, при этом первая и последняя передаются геометрическому шейдеру, а две центральные соединяются линией,
    • VK_PRIMITIVE_TOPOLOGY_LINE_STRIP_WITH_ADJACENCY — аналогично предыдущему, только каждая следующая вершина образует новый отрезок и использует его как информацию о связности,
    • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY — группа из шести вершин образует один примитив, где нечетные вершины образуют треугольник, а четные являются информацией о связности,
    • VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP_WITH_ADJACENCY — аналогично случаю с линиями, только используются пары новых вершин, где нечетная образует с двумя старыми нечетными треугольник, а четная является информацией о связности,
    • VK_PRIMITIVE_TOPOLOGY_PATCH_LIST — используется при включенной тесселяции и требует дополнительный информации при создании конвейера;
  • primitiveRestartEnable — флаг, определяющий возможность разрывать рисование веером при помощи специального индекса в массиве индекса вершин.

Примечание: топологии со связностью (adjaency) используются в совокупности с геометрическим шейдером.

VkPipelineTessellationStateCreateInfo:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO;
  • patchControlPoints — число контрольных точек, которые будут сгруппированы в один примитив тесселяции (patch).

Важное примечание: структуру требуется заполнить если топология соответствует VK_PRIMITIVE_TOPOLOGY_PATCH_LIST.

VkPipelineViewportStateCreateInfo:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
  • viewportCount — количество элементов в массиве с данными об областях просмотра;
  • pViewports — адрес массива с данными об областях просмотра
  • scissorCount — количество элементов в массиве с данными о прямоугольниках отсечения;
  • pScissors — адрес массива с данными о прямоугольниках отсечения.

Примечание: в заметках используется одна область просмотра и адрес массива может указывать на объект структуры VkViewport.

VkViewport:

  • x, y, width, height — задают прямоугольник в пространстве буфера в который будет рендериться изображение;
  • minDepth, maxDepth — диапазон значений глубины для фреймбуфера.

Пример VkViewport:

VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = surface.selectedExtent.width;
viewport.height = surface.selectedExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;

Пример Scissor Rectangle:

VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = surface.selectedExtent;

VkPipelineRasterizationStateCreateInfo:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
  • depthClampEnable — флаг, который включает и отключает отсечение глубины;
  • rasterizerDiscardEnable — флаг, отключающий растеризацию;
  • polygonMode — перечисление (enum) VkPolygonMode, определяющее режим рисования полигонов:
    • VK_POLYGON_MODE_FILL — заполнение треугольников,
    • VK_POLYGON_MODE_LINE — рисование линий,
    • VK_POLYGON_MODE_POINT — рисование точек;
  • cullMode — битовая маска флагов, представленная перечислением VkCullModeFlagBits, определяет отброшенные полигоны:
    • VK_CULL_MODE_NONE — никакие полигоны не будут отброшены,
    • VK_CULL_MODE_FRONT_BIT — полигоны, которые считаются лицевыми будут отброшены,
    • VK_CULL_MODE_BACK_BIT — полигоны, которые считаются не лицевыми будут отброшены,
    • VK_CULL_MODE_FRONT_AND_BACK — все полигоны будут отброшены;
  • frontFace — перечисление (enum) VkFrontFace, определяющее направление обхода треугольника, которое влияет на отбрасывание треугольников:
    • VK_FRONT_FACE_COUNTER_CLOCKWISE — треугольник с положительной площадью повернут вперед,
    • VK_FRONT_FACE_CLOCKWISE — треугольник с отрицательной площадью повернут вперед;
  • depthBiasEnable — флаг, который включает сдвиги по глубине;
  • depthBiasConstantFactor — скалярный коэффициент, добавляемый к каждому фрагменту;
  • depthBiasClamp — максимальное или минимальное смещение фрагмента по глубине;
  • depthBiasSlopeFactor — скалярный коэффициент, добавляемый к наклону полигона;
  • lineWidth — толщина линий.

Примечание: если depthClampEnable включена, то объекты выходящие за границы области отсечения проектируются на неё и могут использоваться для заполнения дыр в геометрии. В данной заметке не используется.

VkPipelineDynamicStateCreateInfo:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
  • dynamicStateCount — количество элементов в массиве перечисляющем динамические параметры;
  • pDynamicStates — адрес массива содержащего динамические параметры.

Дополнение

Для работы со структурой, содержащей входные данные вершин, необходимо дополнить проект структурой Vertex, определяющей данные вершины. Вершина в первых заметках будет содержать только данные о положении и цвете.

Для хранения векторов и матриц будет использоваться библиотека GLM, скачать её можно с репозитория github. Загруженную библиотеку необходимо поместить в папку dependencies и добавить в файлы конфигурации VSCode.

Пример структуры, содержащей данные вершины:

typedef struct _Vertex
{
    glm::vec3 position, color;    
} Vertex;

Ниже представлен пример заполнения структур с данными непрограммируемых частей графического конвейера для этой заметки:

// Входные данные вершин
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;

// Привязка
VkVertexInputBindingDescription bindingDescription{};
bindingDescription.binding = 0;
bindingDescription.stride = sizeof(Vertex);
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

// Описание атрибута 
VkVertexInputAttributeDescription attributeDescriptions[2];

attributeDescriptions[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, position);

attributeDescriptions[1].binding = 0;
attributeDescriptions[1].location = 1;
attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[1].offset = offsetof(Vertex, color);

vertexInputInfo.vertexBindingDescriptionCount = 1;
vertexInputInfo.vertexAttributeDescriptionCount = 2;
vertexInputInfo.pVertexBindingDescriptions = &bindingDescription;
vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions;

// Входной сборщик
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;

// Область просмотра
VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = surface.selectedExtent.width;
viewport.height = surface.selectedExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;

// Прямоугольник отсечения
VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = surface.selectedExtent;

// Состояние области просмотра
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.pViewports = &viewport;
viewportState.scissorCount = 1;
viewportState.pScissors = &scissor;

// Растеризатор
VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterizer.depthBiasEnable = VK_FALSE;

// Мультисэмплинг 
VkPipelineMultisampleStateCreateInfo multisampling{};
multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO;
multisampling.sampleShadingEnable = VK_FALSE;
multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;

// Смешивание цветов для буфера
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;

// Глобальные настройки смешивания цветов
VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;
colorBlending.blendConstants[1] = 0.0f;
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;

// раскладка конвейера
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 0;
pipelineLayoutInfo.pushConstantRangeCount = 0;

Раскладку конвейера необходимо создать с помощью функции vkCreatePipelineLayout, которая принимает в качестве аргументов дескриптор логического устройства, адрес объекта структуры VkPipelineLayoutCreateInfo, адрес аллокатора и адрес дескриптора VkPipelineLayout. В случае ошибки необходимо создать исключение. Для созданного объекта добавим поле в приватной видимости класса pipelineLayout и дополним метод Vulkan::destroy вызовом vkDestroyPipelineLayout перед уничтожением проходов рендера, которая принимает дескриптор логического устройства, дескриптор раскладки конвейера и адрес аллокатора.

Пример создания раскладки:

if (vkCreatePipelineLayout(logicalDevice, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to create pipeline layout");
}

Текущий вариант доступен на теге v0.3 репозитория 04.

Создание графического конвейера

Для создания графического конвейера требуется заполнить объект структуры с информацией VkGraphicsPipelineCreateInfo. Рассмотрим поля подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • flags — битовая маска флагов, представленная перечислением (enum) VkPipelineCreateFlagBits:
    • VK_PIPELINE_CREATE_DISABLE_OPTIMIZATION_BIT — отключает оптимизацию шейдера для более быстрого создания и меньшего быстродействия,
    • VK_PIPELINE_CREATE_ALLOW_DERIVATIVES_BIT — флаг позволяет создавать новые конвейеры на основе создаваемого,
    • VK_PIPELINE_CREATE_DERIVATIVE_BIT — указывает что создаваемый конвейер создается на основе другого конвейера;
  • stageCount — количество шейдеров;
  • pStages — указатель на массив структур VkPipelineShaderStageCreateInfo, содержащий информацию об используемых шейдерах;
  • pVertexInputState — адрес объекта структуры VkPipelineVertexInputStateCreateInfo, описывающей входные данные вершин;
  • pInputAssemblyState — адрес объекта структуры VkPipelineInputAssemblyStateCreateInfo, описывающей входной сборщик;
  • pTessellationState — адрес объекта структуры VkPipelineTessellationStateCreateInfo, описывающей состояние тесселяции;
  • pViewportState — адрес объекта структуры VkPipelineViewportStateCreateInfo, описывающей состояние области просмотра;
  • pRasterizarionState — адрес объекта структуры VkPipelineRasterizationStateCreateInfo, описывающей растеризатор;
  • pMultisampleState — адрес объекта структуры VkPipelineMultisampleStateCreateInfo, описывающей мультисэмплинг;
  • pDepthStencilState — адрес объекта структуры VkPipelineDepthStencilStateCreateInfo, описывающей тесты глубины и трафарета;
  • pColorBlendState — адрес объекта структуры VkPipelineColorBlendStateCreateInfo, описывающей смешивание цветов;
  • pDynamicState — адрес объекта структуры динамическое состояние, описывающей VkPipelineDynamicStateCreateInfo;
  • layout — раскладка конвейера (VkPipelineLayout);
  • renderPass — проходы рендера;
  • subpass — подпроходы рендера (не используются в заметке);
  • basePipelineHandle — дескриптор базового конвейера, используется для создания нового конвейера на основе существующего конвейера с частичным заимствованием параметров;
  • basePipelineIndex — индекс базового конвейера (аналогично дескриптору).

Примечание: basePipelineHandle и basePipelineIndex требуют значение битовой маски flags VK_PIPELINE_CREATE_DERIVATIVE_BIT. Данный способ повышает производительность при частых переключениях.

Для создания графического конвейера необходимо вызвать функцию vkCreateGraphicsPipelines, которая принимает в качестве аргументов дескриптор логического устройства, дескриптор кэша конвейеров (не используется = VK_NULL_HANDLE), количество создаваемых конвейеров, адрес массива с данными о создаваемых структурах (VkGraphicsPipelineCreateInfo), адрес аллокатора и адрес массива дескрипторов графических конвейеров. В случае возникновения ошибок создания конвейера создается исключение. Необходимо дополнить метод Vulkan::destroy вызовом функции vkDestroyPipeline, которая принимает в качестве аргументов дескриптор логического устройства, дескриптор графического конвейера и адрес аллокатора.

Примечание: функция vkCreateGraphicsPipelines может создавать одновременно несколько конвейеров, но в заметке используется один и заместо массива передается адрес одного объекта.

Пример создания графического конвейера:

// Информация о создаваемом конвейере
VkGraphicsPipelineCreateInfo pipelineInfo{};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.layout = pipelineLayout;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;

// Создание графического конвейера
if (vkCreateGraphicsPipelines(logicalDevice, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to create graphics pipeline");
}

Текущий вариант доступен на теге v0.4 репозитория 04.

Вывод

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

Проект доступен в публичном репозитории: 04
Библиотеки: dependencies

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

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

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.