Войти
ПрограммированиеСтатьиГрафика

Синхронизации в Vulkan (2 стр)

Автор:

Все примеры написаны псевдокодом, работающий код можно посмотреть здесь:
https://github.com/SaschaWillems/Vulkan
https://github.com/LunarG/VulkanSamples
https://github.com/azhirnov/FrameGraph/tree/dev/samples/vulkan


Переиспользуем буфер команд.

VkFence fences[2];
VkBuffer stagingBuffers[2];
VkDeviceMemory stagingBufferMemory[2];
void* stagingBufferMappedMemory[2];
VkCommandBuffer  cmdBuffers[2];
vkCreateFence(...); // создаем fences с флагом VK_FENCE_CREATE_SIGNALED_BIT
// создаем stagingBufferMemory и stagingBuffers,
// мапим память и записываем указатели в stagingBufferMappedMemory
...
// цикл рисования
for (unsigned frameIndex = 0;; ++frameIndex)
{
    unsigned i = frameIndex & 1;
    // ожидаем пока выполнятся команды на GPU
    vkWaitForFences( ... fences[i] );

    // получаем индекс изображения из свопчейна
    vkAcquireNextImageKHR( ... );

    // теперь переиспользуем буфер команд и host visible память
    memcpy( stagingBufferMappedMemory[i], ... );
    vkBeginCommandBuffer( cmdBuffers[i], ... );
    vkCmdCopyBuffer( stagingBuffers[i], ... );  //
    ...
    vkEndCommandBuffer( cmdBuffers[i] );

    // нужно сбросить сигнальное состояние до переиспользования
    vkResetFences( ..., fences[i] );

    // отправляем команды на GPU
    vkQueueSubmit( .., cmdBuffers[i], ..., fences[i] );

    // выводим результат на экран
    vkQueuePresentKHR( ... );
}
Здесь используется двойная буферизация и счетчик кадров frameIndex, это дает стабильное поочередное использование ресурсов. Для получения индекса кадра нельзя использовать индекс возвращаемый функцией vkAcquireNextImageKHR, потому что нет гарантий, что индекс будет стабильно чередоваться.
Пример, когда драйвер будет возвращать одно и то же значение в vkAcquireNextImageKHR.
// возвращает индекс 0
vkAcquireNextImageKHR( ... );

// возвращает индекс 1
vkAcquireNextImageKHR( ... );
// рисуем кадр и выводим изображение под индексом 1
vkQueuePresentKHR( ... );

// снова возвращает индекс 1, так как 0 мы захватили и не освободили
vkAcquireNextImageKHR( ... );
 

Зачем нужен pWaitDstStageMask
Сигнал семафора происходит сразу на всех этапах конвейера, но ожидание сигнала происходит только на этапах, указанных в pWaitDstStageMask.
vkAcquireNextImageKHR( ..., semImageAvailable );

// переводим image layout для рисования в текстуру
VkImageMemoryBarrier barrier = {};
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_COLOR_ATTACHMENT_WRITE_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
// ниже в pWaitDstStageMask указано VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
// поэтому здесь в srcStageMask тоже указан этот этап
vkCmdPipelineBarrier( VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                       VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,  ..., 1, &barrier );

vkCmdBeginRenderPass( ... );
vkCmdDraw( ... );
vkCmdEndRenderPass( ... );

// переводим image layout для вывода изображения на экран
VkImageMemoryBarrier barrier = {};
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.dstAccessMask = 0;
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// в srcStageMask указываем, что будем ждать завершения рисования в текстуру
// в dstStageMask можем указать любой этап, так как между рисованием и
// выводом на экран зависимость установлена через семафор
vkCmdPipelineBarrier( VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                      VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, ..., 1, &barrier );

VkSubmitInfo  submit = {};
submit.pSignalSemaphores = { semRenderComplete };
submit.pWaitSemaphores = { semImageAvailable };
submit.pWaitDstStageMask = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
vkQueueSubmit( ..., &submit );

VkPresentInfoKHR  present = {};
present.pWaitSemaphores = { semRenderComplete };
vkQueuePresentKHR( ..., &present );

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

Если в каждом батче обновляется буфер юниформ, то добавляем этапы VK_PIPELINE_STAGE_VERTEX_SHADER_BIT и VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT на которых идет чтение юниформ, добавляем этап VK_PIPELINE_STAGE_TRANSFER_BIT так как обновление буфера это операция копирования. Чтобы не ошибиться при перечислении этапов, можно указать сразу все через флаг VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, тогда гарантированно все команды начнут выполняться только после сигнала от семафора.


Многопоточность.
VkQueue  queue;
std::mutex  queueGuard;
VkSemaphore  semaphore;

void WorkerThread1 ()
{
    VkFence  fences[2];

    VkCommandPool  cmdPool;
    VkDescriptorPool  descPool;

    for (;;)
    {
        vkWaitForFences( ... );

        vkBeginCommandBuffer( ... );
        ...
        vkEndCommandBuffer( ... );

        VkSubmit  submit;
        submit.pWaitSemaphores = { semaphore };
        submit.pCommandBuffers = { ... };

        queueGuard.lock();
        vkQueueSubmit( queue, ... );
        queueGuard.unlock();
    }
}

void WorkerThread2 ()
{
    VkFence  fences[2];

    VkCommandPool  cmdPool;
    VkDescriptorPool  descPool;

    for (;;)
    {
        vkWaitForFences( ... );

        vkBeginCommandBuffer( ... );
        ...
        vkEndCommandBuffer( ... );

        VkSubmit  submit;
        submit.pSignalSemaphores = { semaphore };
        submit.pCommandBuffers = { ... };

        queueGuard.lock();
        vkQueueSubmit( queue, ... );
        queueGuard.unlock();
    }
}
Здесь два потока отправляют команды в одну очередь, поэтому вызов vkQueueSubmit должен быть защищен мютексом.  VkCommandPool и VkDescriptorPool предназначены для того, чтобы храниться локально внутри потока и таким образом избегать блокировок.
В этом примере есть одна ошибка - один батч ожидает семафор, а другой переводит его в сигнальное состояние, поэтому батчи должны быть отправлены строго в таком порядке - сначала сигнал, потом ожидание, в противном случае батч повиснет на бесконечном ожидании и через некоторое время драйвер упадет.
semaphore-deadlock | Синхронизации в Vulkan

Страницы: 1 2 3 4 5 Следующая »

#Vulkan

26 февраля 2019 (Обновление: 29 мар. 2019)

Комментарии [36]