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

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

Автор:

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

Есть приложение для экспериментов с синхронизациями на DX12.
https://github.com/TheRealMJP/OverlappedExecution
Проверяем будет ли распараллелено рисование и вычисление в пределах одной очереди.

single-queue | Синхронизации в Vulkan

Проверям будут ли распараллелены вычисления в отдельной очереди.

multi-queue | Синхронизации в Vulkan


В графической очереди рисуем в renderTarget, потом передаем в другую очередь для асинхронных вычислений (глава 6.7.4 queue transfer release).
Предыдущие данные текстуры не важны, поэтому image layout не определен. На предыдущем кадре renderTarget использовался в другой очереди, но опять же предыдущие данные нам не нужны, поэтому явное перемещение между очередями не производится. Параметр srcStageMask зависит от pWaitDstStageMask при ожидании семафора, здесь просто используется VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, srcAccessMask не важен, так как выполнение начинается после ожидания семафора и все операции записи завершатся до сигнала семафора.

// Это можно было сделать и в описании рендер пасса.
VkImageMemoryBarrier barrier = {};
barrier.image = renderTarget;
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
vkCmdPipelineBarrier( VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
                      VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, ..., 1, &barrier );

// рисуем в renderTarget
vkCmdBeginRenderPass( ... );
vkCmdDraw( ... );
vkCmdEndRenderPass( ... );

// передаем renderTarget в другую очередь (release operation),
// без этого барьера содержимое текстуры не будет видимо для другой очереди.
VkImageMemoryBarrier barrier = {};
barrier.image = renderTarget;
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
// dstAccessMask не важен, так как используется семафор
barrier.dstAccessMask = 0;
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// обязательно заполняем srcQueueFamilyIndex и dstQueueFamilyIndex
barrier.srcQueueFamilyIndex = graphicsQueueFamilyIndex;
barrier.dstQueueFamilyIndex = computeQueueFamilyIndex;
// dstStageMask может быть любым, так как сигнал семафора происходит
// после выполнения всех этапов, лучше всего указать последний этап (bottom of pipe),
// чтобы не мешать выполнению других команд.
vkCmdPipelineBarrier( VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
                      VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, ..., 1, &barrier );

VkSubmitInfo  submit = {};
submit.pSignalSemaphores = { semRenderingComplete };
vkQueueSubmit( graphicsQueue, 1, &submit );

В другой очереди получаем renderTarget и копируем его в свопчейн. (глава 6.7.4 queue transfer acquire)

// Получаем renderTarget (acquire operation).
// Поля oldLayout, newLayout, srcQueueFamilyIndex, dstQueueFamilyIndex
// должны быть такими же как и при передаче ИЗ очереди.
VkImageMemoryBarrier barrier = {};
barrier.image = renderTarget;
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcQueueFamilyIndex = graphicsQueueFamilyIndex;
barrier.dstQueueFamilyIndex = computeQueueFamilyIndex;
// srcStageMask и pWaitDstStageMask семафора должны совпадать
vkCmdPipelineBarrier( VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
                      VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, ..., 1, &barrier );

 // получаем swapchainImage
vkAcquireNextImageKHR( ... semImageAvailable, ... );

// перед использованием swapchainImage в вычислительном шейдере необходимо поменять leyout
VkImageMemoryBarrier barrier = {};
barrier.image = swapchainImage;
barrier.srcAccessMask = 0;  // не важен, так как ожидаем семафор
barrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
// данные будут полностью перезаписаны
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
// srcStageMask и pWaitDstStageMask семафора должны совпадать
vkCmdPipelineBarrier( VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
                      VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, ..., 1, &barrier );

// читаем из renderTarget, накладываем постпроцессы и пишем в swapchainImage
vkCmdDispatch( ... );

// перед выводом на экран необходимо поменять layout
VkImageMemoryBarrier barrier = {};
barrier.image = swapchainImage;
barrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
barrier.dstAccessMask = 0;  // не имеет значения
barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
// в документации сказано, что dstStageMask должен быть bottom of pipe
vkCmdPipelineBarrier( VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
                      VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, ..., 1, &barrier );

VkSubmitInfo  submit = {};
submit.pSignalSemaphores = { semComputeComplete };
submit.pWaitSemaphores = { semRenderingComplete, semImageAvailable };
submit.pWaitDstStageMask = { VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
                      VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT };
vkQueueSubmit( computeQueue, 1, &submit );

// выводим изображение на экран из очереди для вычислений
VkPresentInfoKHR present = {};
present.pWaitSemaphores = { semComputeComplete  };
vkQueuePresentKHR( computeQueue, &present );
vulkan-async-comp1 | Синхронизации в Vulkan


Если вывод изображения на экран доступен только для графической очереди, то придется передавать swapchainImage в графическую очередь и оттуда уже выводить на экран.
vulkan-async-comp2 | Синхронизации в Vulkan
Другой вариант - создать свопчейн с imageSharingMode = VK_SHARING_MODE_CONCURRENT, тогда не придется явно задавать srcQueueFamilyIndex и dstQueueFamilyIndex у барьеров, достаточно задать зависимость между очередями через семафор.
vulkan-async-comp3 | Синхронизации в Vulkan


Также в этом примере есть состояние гонки (data race): renderTarget используется каждый кадр, а синхронизация между чтением на предыдущем кадре и записью на следующем отсутствует.
vulkan-data-race1 | Синхронизации в Vulkan

Проблема решается добавлением семафора между чтением и записью, но тогда мы теряем все преимущество от асинхронных вычислений, поэтому лучше использовать двойную буферизацию - создаем две текстуры и чередум каждый кадр.
vulkan-data-race2 | Синхронизации в Vulkan


Использование ресурсов в разных очередях.
Если буфер или текстура созданы с флагом VK_SHARING_MODE_EXCLUSIVE, то они могут использоваться только в одном типе очереди (queue family), чтобы начать использовать ресурс в другом типе очереди и сохранить их содержимое нужно явно их передать между очередями вызвав vkCmdPipelineBarrier с соответствующими srcQueueFamilyIndex и dstQueueFamilyIndex, как написано выше. Если этого не сделать, то ресурсы захватятся очередью неявно и их содержимое может быть утеряно.
Проблема возникает, когда нужно читать из одного ресурса сразу в нескольких очередях в этом случае придется использовать VK_SHARING_MODE_CONCURRENT. Также этот режим упростит работу с ресурсами, доступными для записи из нескольких очередей, необходимо только синхронизировать доступ через семафоры.


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

// передаем ресурс в любую очередь (release operation)
barrier.srcQueueFamilyIndex = currentQueueFamilyIndex;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;

// получаем ресурс в любой очереди (acquire operation)
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_EXTERNAL;
barrier.dstQueueFamilyIndex = currentQueueFamilyIndex;

Страницы: 1 2 3 4 5

#Vulkan

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

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