Creating the Synchronisation Objects#

Vulkan’s efficiency comes from its ability to achieve higher levels of parallelism with a lower overhead. In in exchange, the API does less to track the order of operations, so most of the synchronisation is left to the developer. This section will outline how to utilise the synchronisation tools Vulkan offers.

Because Vulkan makes few guarantees about the order in which command buffers will be executed on the queue, synchronisation objects are needed to ensure that operations happen in a specific order. While allowing asynchronous execution enables Vulkan to achieve the highest parallelism possible (higher parallelism leads to higher performance) and many operations can overlap, some operations need a specific order with regards to each other – for example, acquiring an image from the swapchain must come before rendering that image, which must come before presenting the image to a surface. Vulkan provides four types of objects to do this: Barriers, Fences, Semaphores, and Events.

Barriers order operations within a command buffer, ensuring that operations recorded before them reach a specific stage of execution before operations submitted after them are allowed to progress to a specific state of execution.

Fences are GPU to CPU syncs, meaning that they ensure certain CPU operations only occur after specific GPU operations have finished. They are signalled by the GPU and can only be waited on by the CPU. This is done in code by calling vkWaitForFences().

Semaphores are GPU to GPU syncs, and can be used to insert dependencies or synchronise queue submissions (on the same or different queues). Again, they are signalled by the GPU and thus waited on by the GPU. They are reset after they are waited on.

Events are more fine-grained and share characteristics with both Fence, Semaphore, and Barrier. They can be signaled on the CPU or GPU, and waited on the GPU or the CPU. They can synchronise specific points during command buffer execution either with other points in GPU command buffer execution or the CPU. However, they are not necessary for this example, and so will not be used.

In the example code, two semaphores and a fence are created for each swapchain image. The semaphores are used to sync acquiring and presenting operations between different images.

  • The acquiring semaphore will signal that the image has been retrieved from the swapchain and is ready to be rendered to.

  • The present semaphore will signal when rendering has been completed and the image is ready to be presented.

These semaphores ensure that the GPU does not attempt to begin rendering before it has been acquired, and does not attempt to present an image before rendering is complete.

The fence is used to wait for the the command buffer of the previous frame to finish executing. The application will wait for the fence to become signalled before continuing with the rest of the drawFrame() function. Because of the way this example is structured, the fence is created with the flags parameter set to VK_FENCE_CREATE_SIGNALED_BIT. The fence is created signalled so that the application can immediately begin rendering. View in context.

void VulkanHelloAPI::initSemaphoreAndFence()
{
    // All of the objects created here are stored in std::vectors. The individual semaphores and fences
    // will be accessed later with an index relating to the frame that is currently being rendered.
    for (uint32_t i = 0; i < appManager.swapChainImages.size(); ++i)
    {
    VkSemaphore acquireSemaphore;
    VkSemaphore renderSemaphore;

    VkFence frameFence;

    VkSemaphoreCreateInfo acquireSemaphoreInfo = {};
    acquireSemaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    acquireSemaphoreInfo.pNext = nullptr;
    acquireSemaphoreInfo.flags = 0;

    debugAssertFunctionResult(vk::CreateSemaphore(appManager.device, &acquireSemaphoreInfo, nullptr, &acquireSemaphore), "Acquire Semaphore creation");

    appManager.acquireSemaphore.emplace_back(acquireSemaphore);

    VkSemaphoreCreateInfo renderSemaphoreInfo = {};
    renderSemaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
    renderSemaphoreInfo.pNext = nullptr;
    renderSemaphoreInfo.flags = 0;

    debugAssertFunctionResult(vk::CreateSemaphore(appManager.device, &renderSemaphoreInfo, nullptr, &renderSemaphore), "Render Semaphore creation");

    appManager.presentSemaphores.emplace_back(renderSemaphore);

    VkFenceCreateInfo FenceInfo;
    FenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
    FenceInfo.pNext = nullptr;
    FenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; // Start the fence as signaled.

    debugAssertFunctionResult(vk::CreateFence(appManager.device, &FenceInfo, nullptr, &frameFence), "Fence Creation");

    appManager.frameFences.emplace_back(frameFence);
    }
}