Fix Vulkan synchronization between CPU and GPU to prevent running over frames in flight

This commit is contained in:
2026-01-28 20:43:01 +01:00
parent 45306ca20e
commit 43b337e163
7 changed files with 57 additions and 12 deletions
@@ -20,6 +20,15 @@ void CmdBufferVulkan::AddWaitSemaphore(VkPipelineStageFlags waitFlags, Semaphore
_waitSemaphores.Add(waitSemaphore);
}
void CmdBufferVulkan::Wait(float timeInSecondsToWait)
{
if (!IsSubmitted())
return;
const bool failed = _device->FenceManager.WaitForFence(_fence, (uint64)(timeInSecondsToWait * 1e9));
ASSERT(!failed);
RefreshFenceStatus();
}
void CmdBufferVulkan::Begin()
{
PROFILE_CPU();
@@ -186,9 +195,8 @@ CmdBufferVulkan::~CmdBufferVulkan()
auto& fenceManager = _device->FenceManager;
if (_state == State::Submitted)
{
// Wait 60ms
const uint64 waitForCmdBufferInNanoSeconds = 60 * 1000 * 1000LL;
fenceManager.WaitAndReleaseFence(_fence, waitForCmdBufferInNanoSeconds);
// Wait
fenceManager.WaitAndReleaseFence(_fence);
}
else
{
@@ -300,7 +308,6 @@ void CmdBufferManagerVulkan::SubmitActiveCmdBuffer(SemaphoreVulkan* signalSemaph
void CmdBufferManagerVulkan::WaitForCmdBuffer(CmdBufferVulkan* cmdBuffer, float timeInSecondsToWait)
{
PROFILE_CPU();
ASSERT(cmdBuffer->IsSubmitted());
const bool failed = _device->FenceManager.WaitForFence(cmdBuffer->GetFence(), (uint64)(timeInSecondsToWait * 1e9));
ASSERT(!failed);
@@ -119,6 +119,7 @@ public:
public:
void AddWaitSemaphore(VkPipelineStageFlags waitFlags, SemaphoreVulkan* waitSemaphore);
void Wait(float timeInSecondsToWait = 1.0f);
void Begin();
void End();
@@ -2251,8 +2251,13 @@ FenceVulkan* FenceManagerVulkan::AllocateFence(bool createSignaled)
bool FenceManagerVulkan::WaitForFence(FenceVulkan* fence, uint64 timeInNanoseconds) const
{
if (fence->IsSignaled)
return false;
PROFILE_CPU();
ZoneColor(TracyWaitZoneColor);
ASSERT(_usedFences.Contains(fence));
ASSERT(!fence->IsSignaled);
if (timeInNanoseconds)
timeInNanoseconds = 1000ll * 1000ll * 1000LL; // 1s
const VkResult result = vkWaitForFences(_device->Device, 1, &fence->Handle, true, timeInNanoseconds);
LOG_VULKAN_RESULT(result);
if (result == VK_SUCCESS)
@@ -106,7 +106,7 @@ public:
}
// Returns true if waiting timed out or failed, false otherwise.
bool WaitForFence(FenceVulkan* fence, uint64 timeInNanoseconds) const;
bool WaitForFence(FenceVulkan* fence, uint64 timeInNanoseconds = 0) const;
void ResetFence(FenceVulkan* fence) const;
@@ -114,7 +114,7 @@ public:
void ReleaseFence(FenceVulkan*& fence);
// Sets the fence handle to null
void WaitAndReleaseFence(FenceVulkan*& fence, uint64 timeInNanoseconds);
void WaitAndReleaseFence(FenceVulkan*& fence, uint64 timeInNanoseconds = 0);
private:
// Returns true if fence was signaled, otherwise false.
@@ -20,7 +20,7 @@ void BackBufferVulkan::Setup(GPUSwapChainVulkan* window, VkImage backbuffer, Pix
initResource(VK_IMAGE_LAYOUT_UNDEFINED);
Device = window->GetDevice();
Handle.Init(window->GetDevice(), this, backbuffer, 1, format, MSAALevel::None, extent, VK_IMAGE_VIEW_TYPE_2D);
Handle.Init(Device, this, backbuffer, 1, format, MSAALevel::None, extent, VK_IMAGE_VIEW_TYPE_2D);
RenderingDoneSemaphore = New<SemaphoreVulkan>(Device);
ImageAcquiredSemaphore = New<SemaphoreVulkan>(Device);
}
@@ -30,6 +30,12 @@ void BackBufferVulkan::Release()
Handle.Release();
Delete(RenderingDoneSemaphore);
Delete(ImageAcquiredSemaphore);
if (SubmitCmdBuffer)
{
SubmitCmdBuffer->Wait();
SubmitCmdBuffer = nullptr;
}
Device = nullptr;
}
GPUSwapChainVulkan::GPUSwapChainVulkan(GPUDeviceVulkan* device, Window* window)
@@ -133,6 +139,22 @@ GPUTextureView* GPUSwapChainVulkan::GetBackBufferView()
return &_backBuffers[_acquiredImageIndex].Handle;
}
void GPUSwapChainVulkan::Begin(RenderTask* task)
{
GPUSwapChain::Begin(task);
// Wait for the backbuffer to be available
if (_currentImageIndex != -1)
{
auto& backBuffer = _backBuffers[_currentImageIndex];
if (backBuffer.SubmitCmdBuffer)
{
backBuffer.SubmitCmdBuffer->Wait();
backBuffer.SubmitCmdBuffer = nullptr;
}
}
}
bool GPUSwapChainVulkan::Resize(int32 width, int32 height)
{
// Check if size won't change
@@ -540,6 +562,11 @@ void GPUSwapChainVulkan::Present(bool vsync)
context->AddImageBarrier(backBuffer, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
context->FlushBarriers();
// Cache a command buffer to wait on its fence before drawing to this backbuffer again
auto& acquiredBackBuffer = _backBuffers[_acquiredImageIndex];
ASSERT(acquiredBackBuffer.SubmitCmdBuffer == nullptr);
acquiredBackBuffer.SubmitCmdBuffer = context->GetCmdBufferManager()->GetActiveCmdBuffer();
context->GetCmdBufferManager()->SubmitActiveCmdBuffer(_backBuffers[_acquiredImageIndex].RenderingDoneSemaphore);
// Present the back buffer to the viewport window
@@ -20,15 +20,20 @@ public:
GPUDeviceVulkan* Device;
/// <summary>
/// The image acquired semaphore handle.
/// The semaphore used to signal backbuffer image being ready to be used again for the commands rendering.
/// </summary>
SemaphoreVulkan* ImageAcquiredSemaphore;
/// <summary>
/// The rendering done semaphore handle.
/// The semaphore used to signal the last command buffer rendering to be completed before presenting the image.
/// </summary>
SemaphoreVulkan* RenderingDoneSemaphore;
/// <summary>
/// The last command buffer used to submit commands for rendering to that backbuffer. Used to wait by CPU for the rendering completion before starting recording new commands (ensure that frame is not in flight).
/// </summary>
CmdBufferVulkan* SubmitCmdBuffer = nullptr;
/// <summary>
/// The render target surface handle.
/// </summary>
@@ -109,6 +114,7 @@ public:
bool IsFullscreen() override;
void SetFullscreen(bool isFullscreen) override;
GPUTextureView* GetBackBufferView() override;
void Begin(RenderTask* task) override;
void Present(bool vsync) override;
bool Resize(int32 width, int32 height) override;
void CopyBackbuffer(GPUContext* context, GPUTexture* dst) override;
@@ -62,8 +62,7 @@ void QueueVulkan::Submit(CmdBufferVulkan* cmdBuffer, uint32 signalSemaphoresCoun
const bool WaitForIdleOnSubmit = false;
if (WaitForIdleOnSubmit)
{
// Use 200ms timeout
bool success = _device->FenceManager.WaitForFence(fence, 200 * 1000 * 1000);
bool success = _device->FenceManager.WaitForFence(fence);
ASSERT(success);
ASSERT(_device->FenceManager.IsFenceSignaled(fence));
cmdBuffer->GetOwner()->RefreshFenceStatus();