Vulkan. Руководство разработчика. Рендеринг и отображение на экране

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Добрый день. Я занимаюсь переводом уроков к Vulkan API с английского языка на русский (ссылка на оригинальный тьюториал — vulkan-tutorial.com).

Сегодня я бы хотела поделиться с вами переводом заключительной главы в разделе Drawing — «Rendering and presentation».

Содержание
1. Вступление

2. Краткий обзор

3. Настройка окружения

4. Рисуем треугольник

  1. Подготовка к работе
    • Базовый код
    • Экземпляр (instance)
    • Слои валидации
    • Физические устройства и семейства очередей
    • Логическое устройство и очереди
  2. Отображение на экране
    • Window surface
    • Swap chain
    • Image views
  3. Графический конвейер (pipeline)
    • Вступление
    • Шейдерные модули
    • Непрограммируемые этапы
    • Проходы рендера
    • Заключение
  4. Отрисовка
    • Фреймбуферы
    • Буферы команд
    • Рендеринг и отображение на экране
  5. Повторное создание цепочки показа

5. Буферы вершин

  1. Описание
  2. Создание буфера вершин
  3. Staging буфер
  4. Буфер индексов

6. Uniform-буферы

  1. Дескриптор layout и буфера
  2. Дескриптор пула и sets

7. Текстурирование

  1. Изображения
  2. Image view и image sampler
  3. Комбинированный image sampler

8. Буфер глубины

9. Загрузка моделей

10. Создание мип-карт

11. Multisampling

FAQ

Политика конфиденциальности


Рендеринг и отображение на экране


  • Подготовка
  • Синхронизация
  • Семафоры
  • Получение image из swap chain
  • Отправка буфера команд
  • Зависимости подпроходов
  • Отображение на экране
  • Кадры в конвейере (Frames in flight)
  • Выводы


Подготовка


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

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

...

void drawFrame() {

}

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


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

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

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

Есть два способа синхронизировать операции в swap chain: с помощью барьеров (fences) и семафоров (semaphores). Эти объекты используются для координации операций: пока одна операция выполняется, следующая ожидает, когда синхронизатор перейдет из состояния unsignaled в signaled.

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

Семафоры


Мы будем использовать два семафора. Первый семафор сообщит нам о том, что image получен и готов к рендерингу, а второй сообщит об окончании рендеринга и о том, что image можно выводить на экран. Создадим два члена класса VkSemaphore:

VkSemaphore imageAvailableSemaphore;
VkSemaphore renderFinishedSemaphore;

Чтобы создать семафоры, добавим функцию createSemaphores:

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    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;
}

Создадим семафоры уже знакомым нам способом — с помощью вызова функции vkCreateSemaphore:

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

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

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

void cleanup() {
    vkDestroySemaphore(device, renderFinishedSemaphore, nullptr);
    vkDestroySemaphore(device, imageAvailableSemaphore, nullptr);


Получение image из swap chain


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

void drawFrame() {
    uint32_t imageIndex;
    vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);
}

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

Следующие два параметра — это синхронизаторы, которые переходят в состояние signaled, когда presentation engine закончит работу с image, после чего мы можем начать отрисовку в него. Здесь можно указать семафор, барьер или и то, и другое. Мы используем imageAvailableSemaphore.

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

Отправка буфера команд


Отправка в очередь и синхронизация настраиваются в структуре VkSubmitInfo.

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;

Первые три параметра указывают, какие семафоры необходимо дождаться перед началом выполнения и на каком этапе (или этапах) графического конвейера происходит ожидание. Прежде чем начать запись цвета в image, нам нужно дождаться, когда image станет доступен, поэтому укажем этап графического конвейера, который выполняет запись в цветовой буфер. Это значит, что теоретически выполнение нашего вершинного шейдера может начаться тогда, когда image еще не доступен. Каждый элемент в массиве waitStages соответствует семафору с тем же индексом в pWaitSemaphores.

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

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

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

Параметры signalSemaphoreCount и pSignalSemaphores указывают, какие семафоры сообщат об окончании выполнения буферов команд. Мы используем renderFinishedSemaphore.

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

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

Зависимости подпроходов


Помните, что image layout автоматически передается между подпроходами в проходе рендера. Зависимости памяти и порядка выполнения между подпроходами контролируются зависимостями подпроходов. Хоть мы используем только один подпроход, операции непосредственно до него и сразу после него также считаются неявными «подпроходами».

Есть две встроенные зависимости, которые отвечают за передачу данных в начале и в конце прохода рендера, но первая передача не происходит в нужное время. Предполагается, что передача данных должна происходить в начале конвейера, но на этом этапе image еще не получен! Есть два пути решения этой проблемы. Можно изменить waitStages для imageAvailableSemaphore и использовать VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, чтобы не допустить начало прохода рендера до тех пор, пока image не станет доступен. Другой способ — заставить проход рендера ждать VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT. Я решил выбрать второй способ, поскольку он позволит лучше рассмотреть зависимости подпроходов и то, как они работают.

Зависимости подпроходов указываются через VkSubpassDependency. Добавьте в функцию createRenderPass следующий код:

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

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

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

В следующих двух полях указаны операции, которые нужно ожидать, и этапы, на которых эти операции выполняются. Нам нужно дождаться, когда swap chain закончит считывание image, прежде чем мы сможем получить к нему доступ. Этого можно добиться, дождавшись output stage у записи в цветовой буфер.

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

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

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

В структуре VkRenderPassCreateInfo есть два поля для указания массива зависимостей.

Отображение на экране


Последнее действие, которое осталось выполнить для отрисовки кадра — отправить результат обратно в swap chain, чтобы вывести на экран. Отображение на экране настраивается в структуре VkPresentInfoKHR в конце функции drawFrame.

VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;

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

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

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

Следующие два параметра указывают swap chains для представления images и индекс image для каждого swap chain. Почти всегда будет использоваться только один swap chain.

presentInfo.pResults = nullptr; // Optional

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

vkQueuePresentKHR(presentQueue, &presentInfo);

Функция vkQueuePresentKHR отправляет запрос на представление image в swap chain. Позже мы добавим обработку ошибок для vkAcquireNextImageKHR и vkQueuePresentKHR, поскольку их ошибки необязательно приводят к закрытию программы, в отличие от функций, встречавшихся ранее.

Если вы все сделали правильно, при запуске программы должно отобразиться что-то подобное:



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

Ура! Получилось! К сожалению, при включенных слоях валидации программа будет падать при каждом закрытии. Сообщения, выводимые в терминал из debugCallback, укажут причину:



Все операции в drawFrame выполняются асинхронно. Это значит, что при выходе из mainLoop операции рисования и отображения могут продолжаться. Освобождать ресурсы в такой момент — не самая лучшая идея.

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

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

    vkDeviceWaitIdle(device);
}

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

Кадры в конвейере (Frames in flight)


Если запустить программу с включенными слоями валидации, можно либо получить ошибки, либо заметить, что использование памяти постепенно растет. Дело в том, что программа сразу отправляет работу в функцию drawFrame и не проверяет, завершилась ли какая-нибудь ее часть. Если CPU отправляет работу быстрее, чем GPU успевает ее выполнять, очередь будет медленно заполняться работой. Но хуже то, что мы повторно используем семафоры imageAvailableSemaphore и renderFinishedSemaphore вместе с буферами команд одновременно для нескольких кадров!

Самый простой способ решить эту проблему — дождаться завершения работы сразу после отправки, например, с помощью vkQueueWaitIdle:

void drawFrame() {
    ...

    vkQueuePresentKHR(presentQueue, &presentInfo);

    vkQueueWaitIdle(presentQueue);
}

Однако в этом случае графический конвейер используется неоптимально, т.к. за раз только один кадр может пройти через конвейер. Этапы, через которые прошел кадр, освобождаются и уже могут использоваться для следующего кадра. Расширим нашу программу, чтобы каждый освободившийся этап начинал обрабатывать следующий кадр.

Сначала добавим константу, указывающую, сколько кадров должно обрабатываться одновременно:

const int MAX_FRAMES_IN_FLIGHT = 2;

Для каждого кадра должен быть свой набор семафоров:

std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;

Изменим функцию createSemaphores, чтобы их создать:

void createSemaphores() {
    imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);

    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
        if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
            vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS) {

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

Также все семафоры должны быть очищены:

void cleanup() {
    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
        vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
        vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
    }

    ...
}

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

size_t currentFrame = 0;

Теперь изменим функцию drawFrame, чтобы использовать соответствующие семафоры:

void drawFrame() {
    vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

    ...

    VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};

    ...

    VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};

    ...
}

Не забывайте каждый раз переходить к следующему кадру:

void drawFrame() {
    ...

    currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}

Использование деления по модулю (%) гарантирует, что индекс кадра закольцовывается после каждого MAX_FRAMES_IN_FLIGHT кадра в очереди.

Несмотря на то, что мы настроили необходимые объекты для облегчения обработки нескольких кадров одновременно, мы все еще не препятствуем отправке более MAX_FRAMES_IN_FLIGHT кадров. Сейчас используется только синхронизация GPU-GPU без синхронизации CPU-GPU. Мы можем использовать объекты кадра #0, в то время как сам кадр #0 все еще находится в конвейере (in flight)!

Для выполнения синхронизации CPU-GPU Vulkan предлагает использовать барьеры (fences). Барьеры похожи на семафоры тем, что они используются для ожидания и уведомляют об окончании операций, но на этот раз мы пропишем их ожидание в нашем коде. Сначала создадим барьер для каждого кадра:

std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
std::vector<VkFence> inFlightFences;
size_t currentFrame = 0;

Я решил создать барьеры вместе с семафорами, поэтому переименовал createSemaphores в createSyncObjects:

void createSyncObjects() {
    imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);

    VkSemaphoreCreateInfo semaphoreInfo{};
    semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

    VkFenceCreateInfo fenceInfo{};
    fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;

    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
        if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
            vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS ||
            vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) {

            throw std::runtime_error("failed to create synchronization objects for a frame!");
        }
    }
}

Создание барьеров (VkFence) очень похоже на создание семафоров. Также не забудьте выполнить очистку:

void cleanup() {
    for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
        vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
        vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
        vkDestroyFence(device, inFlightFences[i], nullptr);
    }

    ...
}

Теперь изменим функцию drawFrame, чтобы использовать барьеры. Вызов vkQueueSubmit содержит опциональный параметр для передачи барьера, который переходит в состояние signaled по окончании выполнения буфера команд. Мы можем использовать барьер для уведомления о создании кадра.

void drawFrame() {
    ...

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

Теперь осталось изменить начало функции drawFrame, чтобы дождаться создания кадра:

void drawFrame() {
    vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
    vkResetFences(device, 1, &inFlightFences[currentFrame]);

    ...
}

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

Если запустить программу сейчас, можно заметить, что что-то не так. Кажется, будто ничего не рендерится. Если у вас были включены слои валидации, появится следующее сообщение:



Это значит, что ожидаемый барьер не был отправлен. Проблема в том, что по умолчанию барьеры создаются в состоянии unsignaled. Поэтому, если барьер не использовался ранее, vkWaitForFences будет ждать бесконечно. Для решения этой проблемы мы можем изменить создание барьера, чтобы инициализировать его в состоянии signaled:

void createSyncObjects() {
    ...

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

    ...
}

Утечка памяти устранена, но программа работает не совсем корректно. Если число MAX_FRAMES_IN_FLIGHT больше, чем количество images в swap chain или vkAcquireNextImageKHR возвращает images не по порядку, есть вероятность, что мы можем начать рендеринг в image, который уже находится в конвейере (in flight). Чтобы этого избежать, нужно отслеживать каждый image из swap chain и проверять, не использует ли его в текущий момент кадр в конвейере.

Для этого добавим новый список imagesInFlight:

std::vector<VkFence> inFlightFences;
std::vector<VkFence> imagesInFlight;
size_t currentFrame = 0;

Подготовим его в createSyncObjects:

void createSyncObjects() {
    imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
    inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
    imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE);

    ...
}

Изначально ни один кадр не использует image, поэтому мы явно инициализируем его как «без барьеров». Изменим функцию drawFrame, чтобы дождаться любого из предыдущих кадров, использующих image, который мы только что ассигновали новому кадру:

void drawFrame() {
    ...

    vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);

    // Check if a previous frame is using this image (i.e. there is its fence to wait on)
    if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) {
        vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX);
    }
    // Mark the image as now being in use by this frame
    imagesInFlight[imageIndex] = inFlightFences[currentFrame];

    ...
}

Поскольку вызовов vkWaitForFences стало больше, вызов vkResetFences следует переместить. Лучше всего вызвать его прямо перед использованием барьера:

void drawFrame() {
    ...

    vkResetFences(device, 1, &inFlightFences[currentFrame]);

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

    ...
}

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

Чтобы узнать о синхронизации больше, предлагаем ознакомиться с подробным обзором Khronos.

Выводы


Написав чуть более 900 строк кода, мы наконец-то видим результат нашей работы на экране! Безусловно, настройка программы с Vulkan занимает немало времени, но благодаря тому, что состояния в Vulkan должны описываться явно, вы можете контролировать многие процессы. Я рекомендую вам потратить чуть больше времени и еще раз перечитать код, чтобы понять назначение всех объектов Vulkan и то, как они соотносятся друг с другом.

В следующей главе мы рассмотрим еще одну деталь, необходимую для правильной работы программы с Vulkan.

C++ code / Vertex shader / Fragment shader
Источник: https://habr.com/ru/post/567754/


Интересные статьи

Интересные статьи

В этой статье я попыталась собрать несколько своих техник тестирования на Python. Не стоит воспринимать их как догму, поскольку, думаю, со временем я обновлю свои практик...
В этом уроке мы поговорим о серверном рендеринге (SSR), его преимуществах и подводных камнях. Затем мы создадим мини React проект и express сервер (Node.js), чтобы продем...
Цель этого руководства – помочь администраторам кластеров в планировании обновлений кластерного парка OpenShift и представить лучшие методики автоматизированной эксплуата...
Я переводчик ижевской компании CG Tribe и здесь я буду публиковать перевод руководства к Vulkan API. Ссылка на источник — vulkan-tutorial.com. Это моя вторая публикация, которая посвящена...
Предыстория Когда-то у меня возникла необходимость проверять наличие неотправленных сообщений в «1С-Битрикс: Управление сайтом» (далее Битрикс) и получать уведомления об этом. Пробле...