I'm trying to synchronize frames in Vulkan API, but I have some weird problems. I implemented synchronization like this:
void RenderSystem::OnUpdate(const float deltaTime)
{
uint32_t frameIndex{};
auto result = SwapChain->AcquireNextImageIndex(PresentationCompleteSemaphore.get(),
nullptr,
&frameIndex);
InFlightFences[frameIndex]->Wait();
InFlightFences[frameIndex]->Reset();
if (result == VK_ERROR_OUT_OF_DATE_KHR)
{
Recreate();
return;
}
else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR)
{
throw std::runtime_error("Error when acquiring next image...");
}
UpdateModelMatrix(deltaTime, frameIndex); // TODO: Remove this! For testing purposes only
VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
GraphicsMainQueue.Submit({ TriangleCommandBuffers[frameIndex].get() },
{ PresentationCompleteSemaphore.get() },
{ RenderCompleteSemaphore.get() },
InFlightFences[frameIndex].get(),
waitStages);
result = PresentationQueue.Present({ RenderCompleteSemaphore.get() },
{ SwapChain.get() },
&frameIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || MainWindow->HasBeenResized())
Recreate();
else if (result != VK_SUCCESS)
throw std::runtime_error("Failed to present result!");
}
And it works on Windows 10 like a charm. Unfortunately on Linux Mint, it doesn't work in some cases. First of all, moving window on Linux is very laggy and sometimes freezes the whole OS for a second, but it's not the biggest problem. Closing the window calls vkDeviceWaitIdle
and... it freezes the application. It will never start responding because it will wait for the device forever. The validation layer doesn't report any problem with my code.
I partly solved this problem by moving fences synchronization at the bottom of my function, but in my opinion, it's a suboptimal solution, because I wait for the frame to finish rendering, instead of preparing the next frame.
// ...
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || MainWindow->HasBeenResized())
Recreate();
else if (result != VK_SUCCESS)
throw std::runtime_error("Failed to present result!");
InFlightFences[frameIndex]->Wait();
InFlightFences[frameIndex]->Reset();
}
How can I properly synchronize frames not only on Windows but also on Linux? What am I doing wrong? What am I missing?
You have only one set of semaphores. That means access to those semaphores might be missynchronized.
Let's see the code without the distractors:
AcquireNextImageIndex( PresentationCompleteSemaphore, frameIndex );
InFlightFences[frameIndex].WaitAndReset();
QSubmit( PresentationCompleteSemaphore, RenderCompleteSemaphore, InFlightFences[frameIndex] );
Present( RenderCompleteSemaphore, frameIndex );
Now, how do we know we can reuse the PresentationCompleteSemaphore
on AcquireNextImageIndex()
?
The QSubmit
waits\unsignals it, and must then finish. We could infer it finished from the fence passed to QSubmit
, but the fence wait happens only after the AcquireNextImageIndex()
. So the semaphore still might be in use while AcquireNextImageIndex()
tries to reuse it. This is a possible program flow:
AcquireNextImageIndex( PresentationCompleteSemaphore ) -> frameIndex = 0;
InFlightFences[0].WaitAndReset();
QSubmit( PresentationCompleteSemaphore, RenderCompleteSemaphore, InFlightFences[0] );
// hazard; QSubmit still might be waiting on PresentationCompleteSemaphore
AcquireNextImageIndex( PresentationCompleteSemaphore ) -> frameIndex = 1;
InFlightFences[1].WaitAndReset();
How do we know we can reuse the RenderCompleteSemaphore
?
The QSubmit
can only use it when Present()
is already done with it. Only sane way historically to infer this is when AcquireNextImageIndex()
gives back the same swapchain image. This is a possible program flow:
AcquireNextImageIndex( PresentationCompleteSemaphore ) -> frameIndex = 0;
QSubmit( PresentationCompleteSemaphore, RenderCompleteSemaphore, InFlightFences[0] );
Present( RenderCompleteSemaphore, 0 );
AcquireNextImageIndex( PresentationCompleteSemaphore ) -> frameIndex = 1;
// hazard; RenderCompleteSemaphore might still be waited on by Present
// which presented image 0, but we acquired image 1, so it might be async
QSubmit( PresentationCompleteSemaphore, RenderCompleteSemaphore, InFlightFences[1] );
So one way to deal with this properly can be like so:
That translates to code something like this:
InFlightFences[frame].WaitAndReset();
// AcquiredSemaphore[frame] safe to (re)use from here on
AcquireNextImageIndex( AcquiredSemaphore[frame], imageIndex );
// We know that present should be done with swapchainImage[imageIndex]
// after AcquiredSemaphore[frame] is signaled,
// so RenderedSemaphore[imageIndex] should be safe to (re)use afterwards
QSubmit( AcquiredSemaphore[frame], RenderedSemaphore[imageIndex], InFlightFences[frame] );
Present( RenderedSemaphore[imageIndex], frame );
frame = (frame + 1) % 2;
Now there are also some new extension ways to deal with it. VK_KHR_present_wait
extension offers a way to determine if image was presented to the user, and VK_EXT_swapchain_maintenance1
adds a fence to vkQueuePresentKHR()
.