Vulkan API — урок 28. Буферы команд

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

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

Пулы команд (Command pools)

Создадим пул команд перед созданием буфера команд. Пул команд управляет памятью, которая используется для хранения буферов и буферы команд будут размещаться в них.

Добавим новый член класса для хранения VkCommandPool:

VDeleter<VkCommandPool> commandPool{device, vkDestroyCommandPool};

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

void initVulkan() {
    createInstance();
    setupDebugCallback();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
}

...

void createCommandPool() {

}

Для создание пула команд нам понадобится только два параметра:

QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);

VkCommandPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily;
poolInfo.flags = 0; // Optional

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

Имеется два возможных флага для пулов команд:

  • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT: Подсказывает, что буферы команд  перезаписываются новыми командами очень часто (может изменить принцип выделения памяти)
  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT: Позволяет буферам команд быть перезаписанными по отдельности, без этого флага они все будут переустанавливаться (очищаться) вместе.

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

Ну и используем функцию vkCreateCommandPool для создание пула. Никаких особенных команд для нее не нужно.

if (vkCreateCommandPool(device, &poolInfo, nullptr, commandPool.replace()) != VK_SUCCESS) {
    throw std::runtime_error("failed to create command pool!");
}

Размещение буферов команд (Command buffer allocation)

Теперь можно приступить к размещению (allocating) буферов команд и записи команд отрисовки. Поскольку одна из команд отрисовки привязана к верному VkFramebuffer, нужно записать буфер команд для каждого изображения в swap chain. С этой целью создадим член класса – список VkCommandBuffer объектов. Буферы команд будут автоматически подчищены, когда пул команд будет уничтожен, так что используем VDeleter.

std::vector<VkCommandBuffer> commandBuffers;

Теперь начнем работать над функцией createCommandBuffers которая будет распределять  и записывать команды для каждого изображения swap chain.

void initVulkan() {
    createInstance();
    setupDebugCallback();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
    createCommandBuffers();
}

...

void createCommandBuffers() {
    commandBuffers.resize(swapChainFramebuffers.size());
}

Очистка буферов команд включает в себя несколько иную функцию, нежели другие объекты. Функция vkFreeCommandBuffers принимает пул команд и массив буферов команд.

Буферы команд резервируются (allocate) с помощью функции vkAllocateCommandBuffers, которая принимает структуру VkCommandBufferAllocateInfo struct, определяющую пул команд и количество буферов для выделения:

VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();

if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate command buffers!");
}

Параметр level определяет, будет ли выделенный буфер команд первичным или вторичным буфером команд:

  • VK_COMMAND_BUFFER_LEVEL_PRIMARY: Может быть передан очереди для исполнения, но не может быть вызван из других буферов команд.
  • VK_COMMAND_BUFFER_LEVEL_SECONDARY: не может быть передан непосредственно, но может быть вызван из первичных буферов команд.

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

Запись буфера команд

Начнем записывать буфер команд с вызова vkBeginCommandBuffer со структурой VkCommandBufferBeginInfo в качестве аргумента, определяющего некоторые детали использования конкретного буфера команд.

for (size_t i = 0; i < commandBuffers.size(); i++) {
    VkCommandBufferBeginInfo beginInfo = {};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
    beginInfo.pInheritanceInfo = nullptr; // Optional

    vkBeginCommandBuffer(commandBuffers[i], &beginInfo);
}

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

  • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT: Буфер команд будет перезаписан сразу после первого выполнения.
  • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT: Это вторичный буфер команд, который будет в единственном render pass.
  • VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT: Буфер команд может быть представлен еще раз, если он так же уже находится в ожидании исполнения.

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

Если буфер команд уже был записан, тогда вызов vkBeginCommandBuffer неявно перезапишет его. Не возможно добавлять команды в буфер в дальнейшем.

Запуск render pass

Отрисовки начинается с vkCmdBeginRenderPass. Render pass настраивается со структуры VkRenderPassBeginInfo, первые параметры которой это сам render pass и вложения. Мы создали фреймбуфер для каждого изображения swap chain, определяемых как color attachment:

VkRenderPassBeginInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];

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

renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;

Последние два параметра определяют значения для очистки, для использования в  VK_ATTACHMENT_LOAD_OP_CLEAR, который используется как операция загрузки для color attachment. В данном случае это просто черный цвет с непрозрачностью в 100%.

VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;

Ну и функция для начала render pass. Все функции, которые записывают команды, могут быть узнаны по префиксу vkCmd. Все они ничего не возвращают, так что не будет никакой обработки ошибки, пока мы не закончим запись.

Первый параметр для каждой команды – это всегда буфер команд, в который команда будет записываться. Второй параметр определяет детали render pass, что мы предоставили. Последний параметр определяет, как команды рисования будут выполняться в render pass:

  • VK_SUBPASS_CONTENTS_INLINE: Команды render pass будут включены в первичный буфер команд и вторичные буферы команд не будут задействованы.
  • VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS: Команды render pass будут выполняться из вторичных буферов.

Т.к. вторичных буферов команд нет, то выбор очевиден.

vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

Базовые команды отрисовки

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

vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

Второй параметр определяет, конвейер будет для графики или расчетов. Теперь нарисуем треугольник:

vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);

Помимо буфера команд, она содержит следующие параметры:

  • vertexCount: Технически буфера вершин нет, но 3 вершины все таки были заданы.
  • instanceCount: Используется для instanced рендеринга, 1 если не нужно.
  • firstVertex: Смещение в буфере вершин, определяет наименьшее значение gl_VertexIndex.
  • firstIntance: Используется как смещение для instanced рендеринга, определяет наименьшее значение gl_InstanceIndex.

Теперь render pass может быть завершен:

vkCmdEndRenderPass(commandBuffers[i]);

И мы закончили записывать буфер команд:

if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {
    throw std::runtime_error("failed to record command buffer!");
}

В следующем уроке напишем основной цикл, который получит изображение из swap chain, выполнит буфер команд и вернет готовое изображение в swap chain.

Main Admin

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

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