Search code examples
synchronizationvulkan

Vulkan Multiple Submissions Waiting on Acquire Image


While learning Vulkan, I frequently run into code like this:

vk::raii::Semaphore imageAvailable = device.createSemaphore(...);
vk::raii::Semaphore renderingFinished = device.createSemaphore(...);

while (shouldRun) {
    // Platform loop

    auto [result, imageIndex] = swapchain.acquireNextImage(UINT64_MAX, *imageAvailable, {});
    queue.submit(
        vk::SubmitInfo(*imageAvailable, waitDstStage, *commandBuffers[imageIndex], 
    *renderingFinished), {});
    queue.presentKHR(vk::PresentInfoKHR(*renderingFinished, *swapchain, imageIndex));
}

In this example, we ask the presentation engine to provide us with the index of the next available image and signal imageAvailable when we the image comes available for us to use. We then submit our pre-recorded command buffer which draws to its corresponding image (for simplicity, I record one command buffer per swapchain image at initialization and draw the same thing every frame) and require that imageAvailable become signaled before execution (reaches waitDstStage), and ask that renderingFinished be signaled when execution completes. Finally, we hand the image back to the presentation engine and ask it to present the image once renderingFinished becomes signaled.

Does this potentially exhibit undefined behavior due to improper synchronization?

If I understand the spec, I can imagine a scenario in which two submit calls are simultaneously waiting on imageAvailable: None of the calls in the main loop above is required to block at all... and so it could potentially execute two calls to vkQueueSubmit, both waiting on imageAvailable, before imageAvailable has been signalled. In my head, when it does become signaled, both batches would execute, and one would potentially access an image before the presentation engine has released it to us. However, in theory, this can't happen:

If 'semaphore' is not 'VK_NULL_HANDLE', the semaphore must be unsignaled, with no signal or wait operations pending. It will become signaled when the application can use the image.

After the first call to vkQueueSubmit, the semaphore could have a wait operation pending. At the second call, the wait operation could still be pending. This would result in undefined behavior, and my Vulkan program is at fault for any application misbehavior.

Firstly, am I to understand that I may treat vkQueuePresentKHR as a "command" (or at least executing commands) within a queue, and I can apply synchronization rules to it? Secondly, am I correct in assuming that I require additional synchronization in the above code to avoid potentially invoking undefined behavior?


Solution

  • You do not require "additional synchronization;" you require additional semaphores.

    You can potentially have outstanding synchronizations for each image in your swapchain. As such, you should have one set of semaphores for each image. You don't have to number them by index or anything; it's fine to just put them in a circular buffer. But the main thing is that you cannot acquire with a semaphore (or do any other signal operations with it) until that semaphore has been unsignaled by a prior queue submission operation.

    With a circular buffer of semaphores, you won't run into this problem. Since your rendering operation will typically need to be multiply buffered as well (so that you aren't writing to scratch memory that's still being read from), this is just one more thing that needs to be buffered.

    And since any multiply-buffered system has limits on the maximum number of buffers in flight, you would naturally guard it with fences. To reuse a semaphore (or any other buffer for that frame) requires checking the fence associated with the waiting semaphore queue operation/consuming operation for the frame. This fence will be several frames old, so it should almost certainly be set. And if it isn't, then the CPU has gotten ahead of the GPU and needs to slow down if it wants to keep drawing stuff.