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

Vulkan API 6: объекты синхронизации и рендер в буфер кадра

В данной заметке создаются объекты синхронизации кадров и настраивается рендер в буфер кадра

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

Объекты синхронизации

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

Для обеспечения синхронизации существуют следующие объекты:

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

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

В рамках заметки будет использоваться два вектора семафоров и один вектор барьеров размерности surface.imageCount (соответствует размеру списка показа). Рассмотрим предназначение этих векторов:

  • imageAvailableSemaphores, содержащий VkSemaphore — доступность изображений для прохода рендера;
  • renderFinishedSemaphores, содержащий VkSemaphore — доступность изображений для которых рендер закончен;
  • inWorkFences, содержащий VkFence — занятость изображений, рендер которых находится в процессе работы.

Использование барьеров позволяет пропускать через один конвейер несколько кадров.

Добавим эти векторы в приватную область видимости класса Vulkan.

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

Для создания семафора необходимо вызвать функцию vkCreateSemaphore, которая принимает в качестве аргументов дескриптор логического устройства, адрес объекта структуры VkSemaphoreCreateInfo, адрес аллокатора и адрес дескриптора семафора для записи результата. Структура VkSemaphoreCreateInfo содержит информацию о создаваемом семафоре: поле flags зарезервировано для будущих реализаций, поле pNext не используется в заметках, а поле sType должно быть равно VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO. В случае ошибки создания семафора создадим исключение.

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

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

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • flags — битовая маска флагов, представленная перечислением VkFenceCreateFlagBits, определяет начальное состояние и дальнейшее поведение (если установлено VK_FENCE_CREATE_SIGNALED_BIT, то барьер по умолчанию взведен).

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

Функция vkResetFences служит для сброса барьеров в массиве и принимает в качестве аргументов дескриптор логического устройства, размер и адрес массива дескрипторов барьеров для сброса.

Пример создания объектов синхронизации:

imageAvailableSemaphores.resize(surface.imageCount);
renderFinishedSemaphores.resize(surface.imageCount);
inWorkFences.resize(surface.imageCount);

VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;

for (int i = 0; i < surface.imageCount; i++) 
{
    if (vkCreateSemaphore(logicalDevice, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS
    ||  vkCreateSemaphore(logicalDevice, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS
    ||  vkCreateFence(logicalDevice, &fenceInfo, nullptr, &inWorkFences[i]) != VK_SUCCESS) 
    {
        throw std::runtime_error("Unable to create synchronization objects for frame");
    }
}

Необходимо дополнить метод Vulkan::destroy следующими вызовами для удаления семафоров и барьеров:

for (int i = 0; i < surface.imageCount; i++) 
{
    vkDestroySemaphore(logicalDevice, renderFinishedSemaphores[i], nullptr);
    vkDestroySemaphore(logicalDevice, imageAvailableSemaphores[i], nullptr);
    vkDestroyFence(logicalDevice, inWorkFences[i], nullptr);
}

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

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

Рендер в буфер кадра

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

На основании существующих VkImageView списка показа необходимо создать буфер кадра (VkFramebuffer).

Необходимо добавить приватные вектор swapChainFramebuffers для хранения дескрипторов и метод для их создания Vulkan::createFramebuffers. Данный метод необходимо вызвать после создания проходов рендера, так как они потребуются для создания буферов кадра.

Создание производится в цикле, перед началом которого необходимо изменить размер вектора swapChainFramebuffers.

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

Пример массива изображений, передаваемых фреймбуферу:

VkImageView attachments[] = { swapChainImageViews[i] };

Для создания буфера кадра необходимо заполнить объект структуры VkFramebufferCreateInfo, содержащей данные о создаваемом буфере кадра. Рассмотрим данную структуру подробнее:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • flags — битовая маска флагов, представленная перечислением VkFramebufferCreateFlagBits, определяет возможность создания без прикрепления изображений значением VK_FRAMEBUFFER_CREATE_IMAGELESS_BIT;
  • renderPass — дескриптор проходов рендера;
  • attachmentCount — размер массива дескрипторов изображений;
  • pAttachments — адрес массива дескрипторов изображений;
  • width — ширина буфера кадра;
  • height — высота буфера кадра;
  • layers — количество слоев (третье измерение).

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

Пример создания буферов кадра:

swapChainFramebuffers.resize(swapChainImageViews.size());

for (int i = 0; i < swapChainImageViews.size(); i++) 
{
    VkImageView attachments[] = { swapChainImageViews[i] };

    VkFramebufferCreateInfo framebufferInfo{};
    framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
    framebufferInfo.renderPass = renderPass;
    framebufferInfo.attachmentCount = 1;
    framebufferInfo.pAttachments = attachments;
    framebufferInfo.width = surface.selectedExtent.width;
    framebufferInfo.height = surface.selectedExtent.height;
    framebufferInfo.layers = 1;

    if (vkCreateFramebuffer(logicalDevice, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) 
    {
        throw std::runtime_error("Unable to create framebuffer");
    }
}

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

for (auto framebuffer : swapChainFramebuffers) 
{
    vkDestroyFramebuffer(logicalDevice, framebuffer, nullptr);
}

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

После создания буферов кадра можно приступить к созданию командного буфера для рендера кадра.

Для рендера кадра и последующего вывода на экран добавим публичный метод класса Vulkan::renderFrame, который будет вызываться в цикле main функции. Добавим приватное целочисленное поле-счетчик кадров currentFrame, определяющее текущий кадр для рендера.

Первым делом необходимо убедится что кадр не находится в работе, для этого необходимо вызвать функцию vkWaitForFences, которая принимает ожидает взведения барьеров и в качестве аргументов дескриптор логического устройства, размер массива барьеров, адрес массива дескрипторов барьеров, параметр «ожидания всех» (VK_TRUE/VK_FALSE) и таймаут ожидания (в заметке используется UINT64_MAX).

Примечание: барьеры кадра создаются в изначально взведенном состоянии.

Необходимо установить барьер в состояние «сброшен» с помощью функции vkResetFences, которая описывалась ранее.

Пример ожидания взведения и сброс барьера текущего кадра:

vkWaitForFences(logicalDevice, 1, &inWorkFences[currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(logicalDevice, 1, &inWorkFences[currentFrame]);

Добавим переменную под индекс изображения из списка показа, которая заполняется функцией vkAcquireNextImageKHR, которая в качестве аргументов принимает дескрипторы логического устройства и списка показа, таймаут ожидания (в заметке используется UINT64_MAX), семафор доступности изображения текущего кадра, барьер текущего кадра и адрес переменной под индекс изображения.

Пример получения индекса изображения цепочки показа:

uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(logicalDevice, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

if (result != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to acquire swap chain image");
}

Теперь, когда кадр готов к рендеру можно записать командный буфер, отвечающий за рендер, но перед этим его необходимо сбросить с помощью функции vkResetCommandBuffer, которая принимает в качестве аргументов дескриптор командного буфера и флаги сброса (в заметке не используется = 0)

Запись команд в командный буфер рассматривалась в 5 заметке.

Пример записи команд (комментариями обозначены основные этапы):

vkResetCommandBuffer(commandBuffers[currentFrame], 0);
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;

if (vkBeginCommandBuffer(commandBuffers[currentFrame], &beginInfo) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to begin recording command buffer");
}

// Запись команд прохода рендера
  // Подключение конвейера
  // Подключение буферов данных
  // Рисование из буферов данных

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

Как описано в примере, имеются несколько этапов создания конвейера:

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

Рассмотрим этапы подробнее.

Для записи команд прохода рендера служат функции vkCmdBeginRenderPass и vkCmdEndRenderPass, обе принимают дескриптор командного буфера, но для функции vkCmdBeginRenderPass требуется дополнительно передать адрес объекта структуры VkRenderPassBeginInfo и флаг, определяющий информацию о проходе рендера.

Для записи прохода рендера служат функции vkCmdBeginRenderPass и vkCmdEndRenderPass, обе принимают дескриптор командного буфера, но для функции vkCmdBeginRenderPass требуется дополнительно передать адрес объекта структуры VkRenderPassBeginInfo и флаг, определяющий информацию о проходе рендера задается перечислением (enum) VkSubpassContents:

  • VK_SUBPASS_CONTENTS_INLINE — команды будут вписаны в первичный буфер команд без запуска вторичных буферов;
  • VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS — команды будут запущены из вторичных буферов команд.

Примечание: в заметке используется флаг VK_SUBPASS_CONTENTS_INLINE.

Структура VkRenderPassBeginInfo определяет информацию о начале этапа рендеринга:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • renderPass — дескриптор используемого прохода рендера;
  • framebuffer — дескриптор буфера кадра;
  • renderArea — область рендера, на которую влияет проход рендера;
  • clearValueCount — размер массива, содержащего данные о начальных значениях буферов данных (цвет, глубина);
  • pClearValues — адрес массива, содержащего данные о начальных значениях буферов данных (цвет, глубина).

Примечание: массив pClearValues задается объединением (union) и для задания начальных значений цвета и глубины потребуется создать массив из двух элементов.

Пример записи команд прохода рендера:

VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};

VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = surface.selectedExtent;
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;

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

//...

vkCmdEndRenderPass(commandBuffers[currentFrame]);

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

Флаг типа конвейера определяется перечислением (enum) VkPipelineBindPoint:

  • VK_PIPELINE_BIND_POINT_GRAPHICS — графический конвейер;
  • VK_PIPELINE_BIND_POINT_COMPUTE — вычислительный конвейер;
  • VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR — конвейер трассировки лучей.

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

Для подключения буфера индексов используется функция vkCmdBindIndexBuffer, которая принимает в качестве аргументов дескриптор командного буфера, дескриптор индексного буфера, смещение и тип данных индексов (в заметке используется VK_INDEX_TYPE_UINT16).

VkBuffer vertexBuffers[] = {vertexBuffer};
VkDeviceSize offsets[] = {0};
vkCmdBindVertexBuffers(commandBuffers[currentFrame], 0, 1, vertexBuffers, offsets);

vkCmdBindIndexBuffer(commandBuffers[currentFrame], indexBuffer, 0, VK_INDEX_TYPE_UINT16);

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

  • vkCmdDraw — рисование без использования буфера индексов;
  • vkCmdDrawIndexed — рисование с использованием буфера индексов.

Обе функции принимают в качестве аргументов дескриптор командного буфера, количество вершин/индексов, количество экземпляров (instances), смещение индексов, идентификатор первого экземпляра для рисования. Для vkCmdDrawIndexed добавляется ещё один параметр перед последним, определяющий добавочное значение к индексу из буфера.

Примечание: экземпляры используются для instanced rendering, который не используется в заметках (количество = 1, идентификатор = 0).

Пример вызова функций (в заметке используется vkCmdDrawIndexed):

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

vkCmdDrawIndexed(commandBuffers[currentFrame], 6, 1, 0, 0, 0);

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

Теперь необходимо поместить командный буфер в очередь для выполнения. Для этого служит функция vkQueueSubmit, которая принимает в качестве аргументов дескриптор очереди, размер и адрес массива объектов структуры VkSubmitInfo и дескриптор барьера, который сработает после выполнения задачи.

Рассмотрим структуру VkSubmitInfo, которая содержит информацию:

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_SUBMIT_INFO;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • waitSemaphoreCount — размер массива семафоров pWaitSemaphores;
  • pWaitSemaphores — адрес массива с дескрипторами семафоров, которые необходимо ожидать перед тем, как выполнять буферы команд;
  • pWaitDstStageMask — адрес массива битовых масок флагов, определяющих этапы конвейера, на которых будет происходить ожидание соответствующего по индексу семафора;
  • commandBufferCount — размер массива с дескрипторами командных буферов;
  • pCommandBuffers — адрес массива командных буферов;
  • signalSemaphoreCount — размер массива семафоров pSignalSemaphores;
  • pSignalSemaphores — адрес массива с дескрипторами семафоров, которые будут сигнализированы, когда буферы команд завершат выполнение.

Рассмотрим подробнее содержимое массива VkSubmitInfo::pWaitDstStageMask, которое определяется перечислением (enum) VkPipelineStageFlagBits. Рассмотрим некоторые из них:

  • VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT — ожидание перед началом рендера;
  • VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT — ожидание перед этапом конвейера, где используются структуры данных VkDrawIndirect, VkDispatchIndirect, VkTraceRaysIndirect;
  • VK_PIPELINE_STAGE_VERTEX_INPUT_BIT — ожидание на этапе где используются буферы вершин и индексов;
  • VK_PIPELINE_STAGE_VERTEX_SHADER_BIT — ожидание перед вершинным шейдером;
  • VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT — ожидание перед шейдером управления тесселяцией;
  • VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT — ожидание перед шейдером вычисления тесселяции;
  • VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT — ожидание перед геометрическим шейдером;
  • VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT — ожидание перед этапом фрагментного шейдера;
  • VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT — ожидание перед переносом цвета в буфер кадра.

Пример размещения командного буфера в очереди:

VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};

VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;

if (vkQueueSubmit(queue.descriptor, 1, &submitInfo, inWorkFences[currentFrame]) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to submit draw command buffer");
}

Увеличим переменную currentFrame на 1, ограничив максимальное значение с помощью переменной surface.imageCount:

currentFrame = (currentFrame + 1) % surface.imageCount;

Во время работы будет выдана ошибка на слои отладки (validation layers) потому что кадры берутся в работу, но не выводятся.

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

Вывод кадра на экран

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

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

  • sType — определяет тип структуры, должно быть VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
  • pNext — указатель на расширение структуры (не используется в заметках);
  • waitSemaphoreCount — размер массива семафоров pWaitSemaphores;
  • pWaitSemaphores — адрес массива с дескрипторами семафоров, которые необходимо ожидать перед тем, как выполнять показ;
  • swapchainCount — размер массива списков показа;
  • pSwapchains — адрес массива с дескрипторами списков показа;
  • pImageIndices — адрес массива с индексами изображений;
  • pResults — адрес массива для записи результатов показа (может быть NULL).

Примечание: VkPresentInfoKHR::pResults не используется и по умолчанию равно NULL.

Пример добавления задачи вывода в очередь:

VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = &swapChain;
presentInfo.pImageIndices = &imageIndex;

if (vkQueuePresentKHR(queue.descriptor, &presentInfo) != VK_SUCCESS) 
{
    throw std::runtime_error("Unable to present swap chain image");
}

В результате программа должна вывести прямоугольник как на рисунке 1.

Рисунок 1 — Результат рисования прямоугольника

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

Вывод

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

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

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

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

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