Для проведения рендера на основании графического конвейера с использованием буферов данных из заметки 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.
Текущий вариант доступен на теге v0.4 репозитория 06.
Вывод
В рамках данной заметки были рассмотрены объекты синхронизации, созданы командный буфер для рендера кадра и задания в очереди для выполнения буфера команд и представления результата на экране.
Проект доступен в публичном репозитории: 06
Библиотеки: dependencies