(Практический пример по этой статье: Синхронизация в GLFW)
Основная философия дизайна в Vulkan заключается в том, что синхронизация выполнения на GPU является явной. Порядок операций определяется нами с помощью различных примитивов синхронизации, которые сообщают драйверу порядок, в котором мы хотим, чтобы все выполнялось. Это означает, что многие вызовы API Vulkan, которые начинают выполнение работы на GPU, являются асинхронными, функции вернутся до завершения операции. И есть ряд событий, которые нам нужно упорядочить явно, поскольку они происходят на GPU, например:
Получение изображения из цепочки кадров
Выполнение команд, которые рисуют на полученном изображении
Отображение этого изображения на экране, возвращение его в цепочку кадров
Каждое из этих событий приводится в действие с помощью одного вызова функции, но все они выполняются асинхронно. Вызовы функций завершатся до фактического завершения операций, а порядок выполнения также не определен. И проблема здесь заключается в том, что каждая из операций зависит от завершения предыдущей. И для достижения желаемого порядка операций нам нужно использовать примитивы синхронизации: семафоры или барьеры
Семафор используется для упорядочивания выполнения операций очереди.
Барьер (fence) также используется для синхронизации выполнения, но предназначен для упорядочивания выполнения на CPU, также известном как хост. И если хосту нужно знать, когда GPU закончил какую-то работу, то применяется барьер.
Таким образом, основное отличие между этими двумя типами примитивов в том, что семафоры используются для указания порядка выполнения операций на графическом процессоре, а барьеры - для синхронизации центрального процессора и графического процессора друг с другом. Основные сценарии для применения синхронизации: операции цепочки кадров и ожидание завершения предыдущего кадра. Для операций цепочки кадров применяются семафоры, потому что эти операции выполняются на графическом процессоре. Для ожидания завершения предыдущего кадра применяются барьеры, потому что нам нужно, чтобы центральный процесс ожидал завершения кадра. Это нужно для того, чтобы мы не рисовали больше одного кадра за раз. Поскольку мы перезаписываем буфер команд каждый кадр, мы не можем записывать работу следующего кадра в буфер команд, пока текущий кадр не завершит выполнение, так как мы не хотим перезаписывать текущее содержимое буфера команд, пока графический процессор использует его.
В Vulkan API семафоры представлены типом VkSemaphore, а барьеры - типом VkFence. Оба типа представляют непрозрачные указатели (opaque handle).
Для описания семафоров применяется структура VkSemaphoreCreateInfo:
typedef struct VkSemaphoreCreateInfo {
VkStructureType sType;
const void* pNext;
VkSemaphoreCreateFlags flags;
} VkSemaphoreCreateInfo;
Фактически здесь нет никаких обязательных полей, кроме sType, которое должно иметь значение VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO. Поэтому создание структуры довольно просто:
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
А для создания семаформа применяется функция vkCreateSemaphore, в которую передается логическое устройство, структура VkSemaphoreCreateInfo, аллокатор памяти и указатель на создаваемый семафор:
VkResult vkCreateSemaphore(
VkDevice device,
const VkSemaphoreCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSemaphore* pSemaphore);
Пример создания семафора:
VkSemaphore mySemaphore;
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &mySemaphore) != VK_SUCCESS) {
throw std::runtime_error("Не удалось создать семафор!");
}
Для описания создания барьеров применяется структура VkFenceCreateInfo:
typedef struct VkFenceCreateInfo {
VkStructureType sType;
const void* pNext;
VkFenceCreateFlags flags;
} VkFenceCreateInfo;
Здесь аналогично можно указать только поле sType, которое должно иметь значение VK_STRUCTURE_TYPE_FENCE_CREATE_INFO. Пример определения структуры:
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
Также стоит отметить, что перечисление VkFenceCreateFlags имеет одно значение - VK_FENCE_CREATE_SIGNALED_BIT, которое переводит барьер в сигнальное состояние сразу
при создании (по умолчанию барьер в несигнальном состоянии):
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
А для создания барьера применяется функция vkCreateSemaphore, в которую передается логическое устройство, структура VkFenceCreateInfo, аллокатор памяти и указатель на создаваемый барьер:
VkResult vkCreateFence(
VkDevice device,
const VkFenceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkFence* pFence);
Пример создания барьера:
VkFence myFence;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
if (vkCreateFence(device, &fenceInfo, nullptr, &myFence) != VK_SUCCESS) {
throw std::runtime_error("Не удалось создать барьер!");
}
После завершения работы семафор и барьер надо удалить. Для удаления семаформа применяется функция vkDestroySemaphore():
void vkDestroySemaphore(
VkDevice device,
VkSemaphore semaphore,
const VkAllocationCallbacks* pAllocator);
А для удаления барьера применяется функция vkDestroyFence():
void vkDestroyFence(
VkDevice device,
VkFence fence,
const VkAllocationCallbacks* pAllocator);
Обе функции однотипны - меняется только второй параметр, только в первом случае передается удаляемый семафор, а во втором случае - удаляемый барьер.
(Практический пример по этой статье: Синхронизация в GLFW)