Вот мы и подошли к пожалуй самому важному и объемному объекту Vulkan API освещенному в этой статье - конвейеру.
Конвейер описывает все без исключения детали вывода примитивов, в том числе и операции реализованные в OpenGL как глобальные состояния, такие как тест глубины или смешивание.
Планировка и кэш конвейера
Для создания самого конвейера нам понадобятся два объекта: планировка конвейера и кэш конвейера.
Первый - дает информацию о дескрипторах и константах, второй - позволяет повторно использовать результат построения конвейера между несколькими конвейерами и даже между отдельными запусками приложения! Но наша вводная статья, конечно же, не об этом...
Планировка конвейера создается посредством следующих функций:
Как видно из описания, поля setLayoutCount/pSetLayouts и pushConstantRangeCount/pPushConstantRanges отвечают за планировку дескрипторов и констант.
Для нас, к сожалениюсчастью, все это не пригодится, т.к. наш пример не включает в себя ни константы, ни объекты, требующие дескрипторы.
Еще более формальной вещью тут выступает кэш конвейера:
Будем честны, задача не из легких. Обилие полей и возможное число комбинаций делает эту область особо привлекательной для багов. Если не иметь под рукой спецификации и не пользоваться всеми предоставленными Vulkan API средствами отладки - можно убить часы или даже дни на решение трудностей.
Большинство полей представляет из себя структуру из дюжины параметров, а потому далее не будет приводится их описание, а лишь их использование в исходном коде.
Этапы обработки примитивов
Поля stageCount/pStages отвечают за используемые конвейером шейдеры. Само собой, шейдер каждого типа должен встречаться не боле одного раза:
pVertexInputState задает правила выборки информации о вершинах. Он использует информацию предоставляемую двумя массивами: один описывает используемые буферы, другой - атрибуты.
Мы явно указываем, что будем использовать буфер на позиции 0 (vk_vertexInputBindingDescription.binding = 0) как буфер с информацией о вершинах (vk_vertexInputBindingDescription.inputRate = VkVertexInputRate::VK_VERTEX_INPUT_RATE_VERTEX). Расстояние между соседними вершинами, кстати говоря, два floatа (vk_vertexInputBindingDescription.stride = sizeof(float)*2).
Эта часть связывает определенный буфер с атрибутом в шейдере. Мы указываем, что расположение атрибута = 0 (vk_vertexInputAttributeDescription.location = 0), информация в него поступает из буфера на позиции 0 (vk_vertexInputAttributeDescription.binding = 0), формат нашего атрибута - двумерный вектор floatов (vk_vertexInputAttributeDescription.format = VkFormat::VK_FORMAT_R32G32_SFLOAT), а смещение данных отсутствует (vk_vertexInputAttributeDescription.offset = 0).
Информация о режиме рисования
Эта часть описывает тип выводимых примитивов (точки, линии, треугольники, etc.) и режим повторения (для рисования стрипами):
Окно задает рамки для вывода, т.е. минимальные и максимальные возможные значения для экранных координат (включая глубину).
Если урезать параметры этого области вывода наполовину, то выводимое изображение сохранит целостность, но будет вдвое меньше.
Операция ножниц отсекает изображение, не попадающее в определенную область области вывода.
Если урезать параметры операции отсечения вдвое, то выводимое изображение сохранит размер, но потеряет целостность в результате отсечения.
Мы задаем режимы ограничения глубины (vk_pipelineRasterizationStateCreateInfo.depthClampEnable), отмены растеризации (vk_pipelineRasterizationStateCreateInfo.rasterizerDiscardEnable), заполнения примитивов (vk_pipelineRasterizationStateCreateInfo.polygonMode), отбраковки (vk_pipelineRasterizationStateCreateInfo.cullMode) и лицевой стороны примитива (vk_pipelineRasterizationStateCreateInfo.frontFace). От описания остальных параметров, пожалуй, воздержимся.
Этап множественной выборки
В пределах этой статьи, наверное, стоит упомянуть только о том, что мы не используем множественную выборку (мульисемплинг).
Т.к. мы выводим лишь треугольник, никакие тесты нам не нужны. Мы отключаем этот этап (vk_pipelineDepthStencilStateCreateInfo.depthCompareOp = VkCompareOp::VK_COMPARE_OP_ALWAYS) и отменяем запись в буфер глубины (которого у нас вообще говоря и нет).
Этап смешивания
Этот этап может использоваться для создания эффектов полупрозрачности, суммирования цвета, коррекции, определения максимального значения, etc.
Само собой, нам ничего из этого не нужно, а потому мы отключаем смешивание и выставляем единичные коэффициенты для преобразований:
Однажды созданный конвейер можно повторно использовать для вывода разного рода геометрии. По своей функциональности они напоминают связку программ и большого количества глобальных состояний (тест глубины, смешивание, отбраковка, режим вывода) из OpenGL, а значит их переключение является одной из самых ресурсоемких операций.
Модификация командных буферов
Последний штрих - добавить команду, которая запустит обработку определенного буфера определенным конвейером и произведет вывод всего этого на экран. Изменения следует добавить непосредственно в код пасса вывода:
Как видите, мы делаем конвейер активным (vkCmdBindPipeline), после чего привязываем наш буфер данных на позицию 0 (vkCmdBindVertexBuffers) и производим вывод трех вершин (vkCmdDraw).
Результатом нашей долгой работы является зеленый треугольник на красном фоне: