Modernize and fix frame sync in QVulkanWindow

Follow 24d62ffd720b5bec5d07b07b8d2c9dda7635f3c0 for QVulkanWindow.
The Vulkan backend of QRhi has diverged from QVulkanWindow even
before that recent fix, having many changes over the years that
are not present in QVulkanWindow.

Introduce some of these now, to make things work with the newer
validation layer bundled with the Vulkan 1.4 SDKs.

Task-number: QTBUG-135645
Change-Id: Ic630e9a57c344843c171871b5a0386ed18027b22
Reviewed-by: Andy Nichols <andy.nichols@qt.io>
This commit is contained in:
Laszlo Agocs 2025-04-09 15:00:19 +02:00 committed by Laszlo Agocs
parent 53756dcfbc
commit 8fc3bc67df
2 changed files with 55 additions and 76 deletions

View File

@ -1196,13 +1196,6 @@ void QVulkanWindowPrivate::recreateSwapChain()
return; return;
} }
err = devFuncs->vkCreateFence(dev, &fenceInfo, nullptr, &image.cmdFence);
if (err != VK_SUCCESS) {
qWarning("QVulkanWindow: Failed to create command buffer fence: %d", err);
return;
}
image.cmdFenceWaitable = true; // fence was created in signaled state
VkImageView views[3] = { image.imageView, VkImageView views[3] = { image.imageView,
dsView, dsView,
msaa ? image.msaaImageView : VK_NULL_HANDLE }; msaa ? image.msaaImageView : VK_NULL_HANDLE };
@ -1270,13 +1263,17 @@ void QVulkanWindowPrivate::recreateSwapChain()
frame.imageAcquired = false; frame.imageAcquired = false;
frame.imageSemWaitable = false; frame.imageSemWaitable = false;
devFuncs->vkCreateFence(dev, &fenceInfo, nullptr, &frame.fence);
frame.fenceWaitable = true; // fence was created in signaled state
devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.imageSem); devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.imageSem);
devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.drawSem); devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.drawSem);
if (gfxQueueFamilyIdx != presQueueFamilyIdx) if (gfxQueueFamilyIdx != presQueueFamilyIdx)
devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.presTransSem); devFuncs->vkCreateSemaphore(dev, &semInfo, nullptr, &frame.presTransSem);
err = devFuncs->vkCreateFence(dev, &fenceInfo, nullptr, &frame.cmdFence);
if (err != VK_SUCCESS) {
qWarning("QVulkanWindow: Failed to create command buffer fence: %d", err);
return;
}
frame.cmdFenceWaitable = true; // fence was created in signaled state
} }
currentFrame = 0; currentFrame = 0;
@ -1432,12 +1429,9 @@ void QVulkanWindowPrivate::releaseSwapChain()
for (int i = 0; i < frameLag; ++i) { for (int i = 0; i < frameLag; ++i) {
FrameResources &frame(frameRes[i]); FrameResources &frame(frameRes[i]);
if (frame.fence) { if (frame.cmdBuf) {
if (frame.fenceWaitable) devFuncs->vkFreeCommandBuffers(dev, cmdPool, 1, &frame.cmdBuf);
devFuncs->vkWaitForFences(dev, 1, &frame.fence, VK_TRUE, UINT64_MAX); frame.cmdBuf = VK_NULL_HANDLE;
devFuncs->vkDestroyFence(dev, frame.fence, nullptr);
frame.fence = VK_NULL_HANDLE;
frame.fenceWaitable = false;
} }
if (frame.imageSem) { if (frame.imageSem) {
devFuncs->vkDestroySemaphore(dev, frame.imageSem, nullptr); devFuncs->vkDestroySemaphore(dev, frame.imageSem, nullptr);
@ -1451,17 +1445,17 @@ void QVulkanWindowPrivate::releaseSwapChain()
devFuncs->vkDestroySemaphore(dev, frame.presTransSem, nullptr); devFuncs->vkDestroySemaphore(dev, frame.presTransSem, nullptr);
frame.presTransSem = VK_NULL_HANDLE; frame.presTransSem = VK_NULL_HANDLE;
} }
if (frame.cmdFence) {
if (frame.cmdFenceWaitable)
devFuncs->vkWaitForFences(dev, 1, &frame.cmdFence, VK_TRUE, UINT64_MAX);
devFuncs->vkDestroyFence(dev, frame.cmdFence, nullptr);
frame.cmdFence = VK_NULL_HANDLE;
frame.cmdFenceWaitable = false;
}
} }
for (int i = 0; i < swapChainBufferCount; ++i) { for (int i = 0; i < swapChainBufferCount; ++i) {
ImageResources &image(imageRes[i]); ImageResources &image(imageRes[i]);
if (image.cmdFence) {
if (image.cmdFenceWaitable)
devFuncs->vkWaitForFences(dev, 1, &image.cmdFence, VK_TRUE, UINT64_MAX);
devFuncs->vkDestroyFence(dev, image.cmdFence, nullptr);
image.cmdFence = VK_NULL_HANDLE;
image.cmdFenceWaitable = false;
}
if (image.fb) { if (image.fb) {
devFuncs->vkDestroyFramebuffer(dev, image.fb, nullptr); devFuncs->vkDestroyFramebuffer(dev, image.fb, nullptr);
image.fb = VK_NULL_HANDLE; image.fb = VK_NULL_HANDLE;
@ -1470,10 +1464,6 @@ void QVulkanWindowPrivate::releaseSwapChain()
devFuncs->vkDestroyImageView(dev, image.imageView, nullptr); devFuncs->vkDestroyImageView(dev, image.imageView, nullptr);
image.imageView = VK_NULL_HANDLE; image.imageView = VK_NULL_HANDLE;
} }
if (image.cmdBuf) {
devFuncs->vkFreeCommandBuffers(dev, cmdPool, 1, &image.cmdBuf);
image.cmdBuf = VK_NULL_HANDLE;
}
if (image.presTransCmdBuf) { if (image.presTransCmdBuf) {
devFuncs->vkFreeCommandBuffers(dev, presCmdPool, 1, &image.presTransCmdBuf); devFuncs->vkFreeCommandBuffers(dev, presCmdPool, 1, &image.presTransCmdBuf);
image.presTransCmdBuf = VK_NULL_HANDLE; image.presTransCmdBuf = VK_NULL_HANDLE;
@ -1920,24 +1910,21 @@ void QVulkanWindowPrivate::beginFrame()
return; return;
} }
// wait if we are too far ahead
FrameResources &frame(frameRes[currentFrame]); FrameResources &frame(frameRes[currentFrame]);
if (frame.cmdFenceWaitable) {
if (!frame.imageAcquired) { devFuncs->vkWaitForFences(dev, 1, &frame.cmdFence, VK_TRUE, UINT64_MAX);
// Wait if we are too far ahead, i.e. the thread gets throttled based on the presentation rate devFuncs->vkResetFences(dev, 1, &frame.cmdFence);
// (note that we are using FIFO mode -> vsync) frame.cmdFenceWaitable = false;
if (frame.fenceWaitable) {
devFuncs->vkWaitForFences(dev, 1, &frame.fence, VK_TRUE, UINT64_MAX);
devFuncs->vkResetFences(dev, 1, &frame.fence);
frame.fenceWaitable = false;
} }
// move on to next swapchain image // move on to next swapchain image
if (!frame.imageAcquired) {
VkResult err = vkAcquireNextImageKHR(dev, swapChain, UINT64_MAX, VkResult err = vkAcquireNextImageKHR(dev, swapChain, UINT64_MAX,
frame.imageSem, frame.fence, &currentImage); frame.imageSem, VK_NULL_HANDLE, &currentImage);
if (err == VK_SUCCESS || err == VK_SUBOPTIMAL_KHR) { if (err == VK_SUCCESS || err == VK_SUBOPTIMAL_KHR) {
frame.imageSemWaitable = true; frame.imageSemWaitable = true;
frame.imageAcquired = true; frame.imageAcquired = true;
frame.fenceWaitable = true;
} else if (err == VK_ERROR_OUT_OF_DATE_KHR) { } else if (err == VK_ERROR_OUT_OF_DATE_KHR) {
recreateSwapChain(); recreateSwapChain();
q->requestUpdate(); q->requestUpdate();
@ -1950,23 +1937,15 @@ void QVulkanWindowPrivate::beginFrame()
} }
} }
// make sure the previous draw for the same image has finished
ImageResources &image(imageRes[currentImage]);
if (image.cmdFenceWaitable) {
devFuncs->vkWaitForFences(dev, 1, &image.cmdFence, VK_TRUE, UINT64_MAX);
devFuncs->vkResetFences(dev, 1, &image.cmdFence);
image.cmdFenceWaitable = false;
}
// build new draw command buffer // build new draw command buffer
if (image.cmdBuf) { if (frame.cmdBuf) {
devFuncs->vkFreeCommandBuffers(dev, cmdPool, 1, &image.cmdBuf); devFuncs->vkFreeCommandBuffers(dev, cmdPool, 1, &frame.cmdBuf);
image.cmdBuf = nullptr; frame.cmdBuf = nullptr;
} }
VkCommandBufferAllocateInfo cmdBufInfo = { VkCommandBufferAllocateInfo cmdBufInfo = {
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, nullptr, cmdPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1 }; VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, nullptr, cmdPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 1 };
VkResult err = devFuncs->vkAllocateCommandBuffers(dev, &cmdBufInfo, &image.cmdBuf); VkResult err = devFuncs->vkAllocateCommandBuffers(dev, &cmdBufInfo, &frame.cmdBuf);
if (err != VK_SUCCESS) { if (err != VK_SUCCESS) {
if (!checkDeviceLost(err)) if (!checkDeviceLost(err))
qWarning("QVulkanWindow: Failed to allocate frame command buffer: %d", err); qWarning("QVulkanWindow: Failed to allocate frame command buffer: %d", err);
@ -1975,7 +1954,7 @@ void QVulkanWindowPrivate::beginFrame()
VkCommandBufferBeginInfo cmdBufBeginInfo = { VkCommandBufferBeginInfo cmdBufBeginInfo = {
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, 0, nullptr }; VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, nullptr, 0, nullptr };
err = devFuncs->vkBeginCommandBuffer(image.cmdBuf, &cmdBufBeginInfo); err = devFuncs->vkBeginCommandBuffer(frame.cmdBuf, &cmdBufBeginInfo);
if (err != VK_SUCCESS) { if (err != VK_SUCCESS) {
if (!checkDeviceLost(err)) if (!checkDeviceLost(err))
qWarning("QVulkanWindow: Failed to begin frame command buffer: %d", err); qWarning("QVulkanWindow: Failed to begin frame command buffer: %d", err);
@ -1985,6 +1964,7 @@ void QVulkanWindowPrivate::beginFrame()
if (frameGrabbing) if (frameGrabbing)
frameGrabTargetImage = QImage(swapChainImageSize, QImage::Format_RGBA8888); // the format is as documented frameGrabTargetImage = QImage(swapChainImageSize, QImage::Format_RGBA8888); // the format is as documented
ImageResources &image(imageRes[currentImage]);
if (renderer) { if (renderer) {
framePending = true; framePending = true;
renderer->startNextFrame(); renderer->startNextFrame();
@ -2006,8 +1986,8 @@ void QVulkanWindowPrivate::beginFrame()
rpBeginInfo.renderArea.extent.height = swapChainImageSize.height(); rpBeginInfo.renderArea.extent.height = swapChainImageSize.height();
rpBeginInfo.clearValueCount = sampleCount > VK_SAMPLE_COUNT_1_BIT ? 3 : 2; rpBeginInfo.clearValueCount = sampleCount > VK_SAMPLE_COUNT_1_BIT ? 3 : 2;
rpBeginInfo.pClearValues = clearValues; rpBeginInfo.pClearValues = clearValues;
devFuncs->vkCmdBeginRenderPass(image.cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE); devFuncs->vkCmdBeginRenderPass(frame.cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
devFuncs->vkCmdEndRenderPass(image.cmdBuf); devFuncs->vkCmdEndRenderPass(frame.cmdBuf);
endFrame(); endFrame();
} }
@ -2033,7 +2013,7 @@ void QVulkanWindowPrivate::endFrame()
presTrans.image = image.image; presTrans.image = image.image;
presTrans.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; presTrans.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
presTrans.subresourceRange.levelCount = presTrans.subresourceRange.layerCount = 1; presTrans.subresourceRange.levelCount = presTrans.subresourceRange.layerCount = 1;
devFuncs->vkCmdPipelineBarrier(image.cmdBuf, devFuncs->vkCmdPipelineBarrier(frame.cmdBuf,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, 0, nullptr, 0, nullptr, 0, 0, nullptr, 0, nullptr,
@ -2044,7 +2024,7 @@ void QVulkanWindowPrivate::endFrame()
if (frameGrabbing) if (frameGrabbing)
addReadback(); addReadback();
VkResult err = devFuncs->vkEndCommandBuffer(image.cmdBuf); VkResult err = devFuncs->vkEndCommandBuffer(frame.cmdBuf);
if (err != VK_SUCCESS) { if (err != VK_SUCCESS) {
if (!checkDeviceLost(err)) if (!checkDeviceLost(err))
qWarning("QVulkanWindow: Failed to end frame command buffer: %d", err); qWarning("QVulkanWindow: Failed to end frame command buffer: %d", err);
@ -2056,7 +2036,7 @@ void QVulkanWindowPrivate::endFrame()
memset(&submitInfo, 0, sizeof(submitInfo)); memset(&submitInfo, 0, sizeof(submitInfo));
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1; submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &image.cmdBuf; submitInfo.pCommandBuffers = &frame.cmdBuf;
if (frame.imageSemWaitable) { if (frame.imageSemWaitable) {
submitInfo.waitSemaphoreCount = 1; submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &frame.imageSem; submitInfo.pWaitSemaphores = &frame.imageSem;
@ -2068,12 +2048,12 @@ void QVulkanWindowPrivate::endFrame()
VkPipelineStageFlags psf = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; VkPipelineStageFlags psf = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
submitInfo.pWaitDstStageMask = &psf; submitInfo.pWaitDstStageMask = &psf;
Q_ASSERT(!image.cmdFenceWaitable); Q_ASSERT(!frame.cmdFenceWaitable);
err = devFuncs->vkQueueSubmit(gfxQueue, 1, &submitInfo, image.cmdFence); err = devFuncs->vkQueueSubmit(gfxQueue, 1, &submitInfo, frame.cmdFence);
if (err == VK_SUCCESS) { if (err == VK_SUCCESS) {
frame.imageSemWaitable = false; frame.imageSemWaitable = false;
image.cmdFenceWaitable = true; frame.cmdFenceWaitable = true;
} else { } else {
if (!checkDeviceLost(err)) if (!checkDeviceLost(err))
qWarning("QVulkanWindow: Failed to submit to graphics queue: %d", err); qWarning("QVulkanWindow: Failed to submit to graphics queue: %d", err);
@ -2228,6 +2208,7 @@ void QVulkanWindowPrivate::addReadback()
return; return;
} }
FrameResources &frame(frameRes[currentFrame]);
ImageResources &image(imageRes[currentImage]); ImageResources &image(imageRes[currentImage]);
VkImageMemoryBarrier barrier; VkImageMemoryBarrier barrier;
@ -2242,7 +2223,7 @@ void QVulkanWindowPrivate::addReadback()
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
barrier.image = image.image; barrier.image = image.image;
devFuncs->vkCmdPipelineBarrier(image.cmdBuf, devFuncs->vkCmdPipelineBarrier(frame.cmdBuf,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 0, 0, nullptr, 0, nullptr,
@ -2254,7 +2235,7 @@ void QVulkanWindowPrivate::addReadback()
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.image = frameGrabImage; barrier.image = frameGrabImage;
devFuncs->vkCmdPipelineBarrier(image.cmdBuf, devFuncs->vkCmdPipelineBarrier(frame.cmdBuf,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 0, nullptr, 0, nullptr, 0, 0, nullptr, 0, nullptr,
@ -2268,7 +2249,7 @@ void QVulkanWindowPrivate::addReadback()
copyInfo.extent.height = frameGrabTargetImage.height(); copyInfo.extent.height = frameGrabTargetImage.height();
copyInfo.extent.depth = 1; copyInfo.extent.depth = 1;
devFuncs->vkCmdCopyImage(image.cmdBuf, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, devFuncs->vkCmdCopyImage(frame.cmdBuf, image.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
frameGrabImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copyInfo); frameGrabImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copyInfo);
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
@ -2277,7 +2258,7 @@ void QVulkanWindowPrivate::addReadback()
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT; barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
barrier.image = frameGrabImage; barrier.image = frameGrabImage;
devFuncs->vkCmdPipelineBarrier(image.cmdBuf, devFuncs->vkCmdPipelineBarrier(frame.cmdBuf,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 0, nullptr, 0, nullptr, 0, 0, nullptr, 0, nullptr,
@ -2286,14 +2267,14 @@ void QVulkanWindowPrivate::addReadback()
void QVulkanWindowPrivate::finishBlockingReadback() void QVulkanWindowPrivate::finishBlockingReadback()
{ {
ImageResources &image(imageRes[currentImage]);
// Block until the current frame is done. Normally this wait would only be // Block until the current frame is done. Normally this wait would only be
// done in current + concurrentFrameCount(). // done in current + concurrentFrameCount().
devFuncs->vkWaitForFences(dev, 1, &image.cmdFence, VK_TRUE, UINT64_MAX); FrameResources &frame(frameRes[currentFrame]);
devFuncs->vkResetFences(dev, 1, &image.cmdFence); if (frame.cmdFenceWaitable) {
// will reuse the same image for the next "real" frame, do not wait then devFuncs->vkWaitForFences(dev, 1, &frame.cmdFence, VK_TRUE, UINT64_MAX);
image.cmdFenceWaitable = false; devFuncs->vkResetFences(dev, 1, &frame.cmdFence);
frame.cmdFenceWaitable = false;
}
VkImageSubresource subres = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0 }; VkImageSubresource subres = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 0 };
VkSubresourceLayout layout; VkSubresourceLayout layout;
@ -2523,7 +2504,7 @@ QSize QVulkanWindow::swapChainImageSize() const
} }
/*! /*!
Returns The active command buffer for the current swap chain image. Returns The active command buffer for the current swap chain frame.
Implementations of QVulkanWindowRenderer::startNextFrame() are expected to Implementations of QVulkanWindowRenderer::startNextFrame() are expected to
add commands to this command buffer. add commands to this command buffer.
@ -2537,7 +2518,7 @@ VkCommandBuffer QVulkanWindow::currentCommandBuffer() const
qWarning("QVulkanWindow: Attempted to call currentCommandBuffer() without an active frame"); qWarning("QVulkanWindow: Attempted to call currentCommandBuffer() without an active frame");
return VK_NULL_HANDLE; return VK_NULL_HANDLE;
} }
return d->imageRes[d->currentImage].cmdBuf; return d->frameRes[d->currentFrame].cmdBuf;
} }
/*! /*!

View File

@ -109,9 +109,6 @@ public:
struct ImageResources { struct ImageResources {
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VkImageView imageView = VK_NULL_HANDLE; VkImageView imageView = VK_NULL_HANDLE;
VkCommandBuffer cmdBuf = VK_NULL_HANDLE;
VkFence cmdFence = VK_NULL_HANDLE;
bool cmdFenceWaitable = false;
VkFramebuffer fb = VK_NULL_HANDLE; VkFramebuffer fb = VK_NULL_HANDLE;
VkCommandBuffer presTransCmdBuf = VK_NULL_HANDLE; VkCommandBuffer presTransCmdBuf = VK_NULL_HANDLE;
VkImage msaaImage = VK_NULL_HANDLE; VkImage msaaImage = VK_NULL_HANDLE;
@ -123,13 +120,14 @@ public:
uint32_t currentImage; uint32_t currentImage;
struct FrameResources { struct FrameResources {
VkFence fence = VK_NULL_HANDLE;
bool fenceWaitable = false;
VkSemaphore imageSem = VK_NULL_HANDLE; VkSemaphore imageSem = VK_NULL_HANDLE;
VkSemaphore drawSem = VK_NULL_HANDLE; VkSemaphore drawSem = VK_NULL_HANDLE;
VkSemaphore presTransSem = VK_NULL_HANDLE; VkSemaphore presTransSem = VK_NULL_HANDLE;
VkFence cmdFence = VK_NULL_HANDLE;
VkCommandBuffer cmdBuf = VK_NULL_HANDLE;
bool imageAcquired = false; bool imageAcquired = false;
bool imageSemWaitable = false; bool imageSemWaitable = false;
bool cmdFenceWaitable = false;
} frameRes[MAX_FRAME_LAG]; } frameRes[MAX_FRAME_LAG];
uint32_t currentFrame; uint32_t currentFrame;