Vulkan API — урок 29. Рендеринг и представление (Hello world из 884 строк)(+листинг)

Ну что соберем все вместе. Напишем функцию drawFrame, вызываемую из mainLoop, для вывода треугольника на экран.

void mainLoop() {
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
        drawFrame();
    }
}

...

void drawFrame() {

}

Синхронизация

Функция drawFrame будет выполнять следующие операции:

  • Получение изображения из swap chain
  • Выполнение буфера команд с этим изображением в качестве вложения фреймбуфера
  • Возвращение изображения в swap chain для представления

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

Имеется два способа синхронизации: барьер (аналог блокировки) и семафор. Оба объекта могут быть использованы для синхронизаций по средствам сигнала и операции ожидания для перехода от состоянии unsignaled в signaled.

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

Семафоры

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

VDeleter<VkSemaphore> imageAvailableSemaphore{device, vkDestroySemaphore};
VDeleter<VkSemaphore> renderFinishedSemaphore{device, vkDestroySemaphore};

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

mainLoop, для вывода треугольника на экран.

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

...

void createSemaphores() {

}

Создание семафоров потребует заполнения структуры VkSemaphoreCreateInfo, но в текущей версии API в ней можно сказать нет обязательных полей, только sType:

void createSemaphores() {
    VkSemaphoreCreateInfo semaphoreInfo = {};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
}

В будущих версиях Vulkan’а могут добавить функциональные возможности flags и pNext, как это делается для других структур. Создание семафоров происходит по знакомой схеме:

if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, imageAvailableSemaphore.replace()) != VK_SUCCESS ||
    vkCreateSemaphore(device, &semaphoreInfo, nullptr, renderFinishedSemaphore.replace()) != VK_SUCCESS) {

    throw std::runtime_error("failed to create semaphores!");
}

Получение изображения из swap chain

Как упоминалось ранее, сначала получим изображение из swap chain. Напомню, swap chain является расширением, потому нужно использовать функцию с именем вида vk*KHR:

void drawFrame() {
    uint32_t imageIndex;
    vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(), imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
}

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

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

Последний параметр определяет переменную для вывода индекса изображения swap chain, которое стало доступным. Индекс ссылается на VkImage в массиве swapChainImages. Мы используем этот индекс для выбора правильного буфера команд.

Подача буфера команд

Для настройки и синхронизации очереди используем структуру VkSubmitInfo. Первые три параметра определяет какие семафоры нужно обождать перед началом выполнения и в каком шаге/шагах конвейера ожидать. Нам нужно подождать с записью цветов в изображение, пока оно доступно, поэтому указываем шаг графического конвейера, которые пишет в color attachment. Это означает, что, теоретически, реализация может уже приступить к выполнению нашего vertex shader, например, пока изображение еще не доступно. Каждая запись в массиве waitStages соответствует семафору с тем же индексом в  pWaitSemaphores.

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

VkSemaphore waitSemaphores[] = {imageAvailableSemaphore};
VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;

Следующие два параметра определяют, какие буферы команд нужно подавать на выполнение. Как упоминалось ранее, мы должны подать буфер команд, соответствующий изображению swap chain, что мы только что использовали как color attachment.

submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[imageIndex];

Параметры signalSemaphoreCount и pSignalSemaphores определяют какие семафоры подадут сигнал после того, как буфер(ы) команд закончил выполнение. В нашем случае используется renderFinishedSemaphore:

VkSemaphore signalSemaphores[] = {renderFinishedSemaphore};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;

Теперь можно подать буфер команд в графическую очередь используя vkQueueSubmit. Функция принимает массив структур VkSubmitInfo как аргумент для повышения эффективности. Последний параметр опционален и ссылается на барьер, который сигнализирует, когда буфер команд закончит выполнение. Но у нас семафоры, так что используем VK_NULL_HANDLE:

if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
    throw std::runtime_error("failed to submit draw command buffer!");
}

Зависимости подпроходов (Subpass dependencies)

Следует помнить, что подпроходы автоматически заботятся о переходах layout изображения. Эти переходы контролируются зависимостями подпроходов (subpass dependencies), которые управляют памятью и зависимостями между подпроходами. В нашем случае есть только один подпроход, но операции непосредственно перед и после подпрохода так же считаются неявными «подпроходами».

Есть две встроенных зависимости, которые заботятся о переходах в начале и в конце render pass, но нужное действие не происходит в нужное время. Сейчас система предполагает, что переход происходит в начало конвейера, но мы еще не получили изображение. Изображение не готово до стадии VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT. Поэтому нам необходимо переопределить эту зависимость.

Зависимости подпроходов определяются в структуре VkSubpassDependency. Добавим её (структуру) в функцию createRenderPass. Первые два поля определят индексы на зависимость и зависимый подпроход. Значение VK_SUBPASS_EXTERNAL относится к неявному подпроходу до или после render pass, зависит от того, записано в srcSubpass или dstSubpass соответственно. Индекс 0 ссылаентся на наш подпроход, первый и единственный. Параметр dstSubpass всегда должен быть больше, чем srcSubpass, что бы избежать циклы в зависимостях:

VkSubpassDependency dependency = {};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;

Следующие два поля  определяют операции ожидания и этапы, в которых эти оперции происходят. Нам нужно ожидать пока swap chain окончит чтение изображения перед тем, как мы получим к нему доступ. Это можно достичь ожиданием на выводе самого color attachment:

dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;

Операции, которые нужно ожидать на шаге color attachment и включают чтение и запись color attachment. Эти настройки не допустят переход от действия, пока это действительно необходимо (и разрешено): когда мы хотим начать записывать цвет в него:

dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

Структура VkRenderPassCreateInfo имеет два поля для того, что бы задать массив зависимостей.

renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;

Представление

Последниц шаг рисования кадра – это подача результат назад в swap chain, для вывода на экран. Представление настраивается при помощи структуры VkPresentInfoKHR в конце функции drawFrame, первые два параметра которой определяют какие семафоры будут ожидать пока произойдет представление, прям как VkSubmitInfo:

VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;

presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;

Следующие два параметра определяют swap chains для представления изображения и индекс изображения для swap chain.

VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;

Последний опциональный параметр зовется pResults. Он позволяет определить массив значений VkResult, для проверки каждой отдельной swap chain, прошло ли представление успешно. Если используется только одна swap chain, то в этом нет необходимости.

presentInfo.pResults = nullptr; // Optional

Функция vkQueuePresentKHR подает запрос на представление изображения в swap chain. Добавим обработку ошибок для vkAcquireNextImageKHR и vkQueuePresentKHR чуть позже, о том в следующем уроке.

И после этого всего вы должны увидеть на экране это чудо:

Если сейчас закрыть окно, то вы увидите в терминале ошибку:

semaphore_in_use

Как упоминалось ранее – все операции в drawFrame асинхронны. Это означает, что отрисовка и вывод продолжается при выходе из основного цикла. Очистка ресурсов пока все это происходит – плохая идея.

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

void mainLoop() {
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
        drawFrame();
    }

    vkDeviceWaitIdle(device);
}

Это один из вариантов решения проблемы. Можно так же ожидать операций в определенной очереди команд, которые будут закончены при помощи vkQueueWaitIdle. Но это не суть. Теперь все работает хорошо.

Послесловие

Ну что же, «Hello world» мы дописали, и занял они 884 строки. Вот только он включает в себя столь много, все основные моменты, описывает тонкое управление большинством нюансов, большая часть того, что нужно знать, уже озвучена.

Моментов, про которые «еще нужно упомянуть» накопилось так же достаточно, на них уйдет еще от двух до трех недель и тогда с уроками по как таковому Vulkan’у можно будет заканчивать, и переходить к чемулибонибудь еще.

 Хочу так же отметить пункт «Зависимости подпроходов (Subpass dependencies)». Не смотря на все старания его перевод получился весьма… своеобразным, хоть и потрачено было на него 2/3 времени от всего урока, даже вспомнился ролик, на который недавно наткнулся https://youtu.be/s3IuPzL7si4?t=49.

И листинг.

Main Admin

2 Comments

  1. «Параметр dstSubpass всегда должен быть больше, чем srcSubpass, что бы избежать циклы в зависимостях:» — может меньше, а не больше? Точно нет ошибки?

    • Точно. У нас есть ряд подпроходов: 0,1,2,3 . В самом начале и самом конце указывается VK_SUBPASS_EXTERNAL
      Соответственно пары будут:

      VK_SUBPASS_EXTERNAL;
      0;
      0;
      1;
      1;
      2;
      2;
      3;

      Здесь можно прочитать следующее:

      srcSubpass is the subpass index of the first subpass in the dependency (индекс первого подпрохода в зависимости), or VK_SUBPASS_EXTERNAL.
      dstSubpass is the subpass index of the second subpass in the dependency (индекс второго подпрохода в зависимости), or VK_SUBPASS_EXTERNAL.

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

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