First frame submission fails with unrecorded command buffer and unsignaled semaphore in multithreaded pipeline [closed]

1 day ago 1
ARTICLE AD BOX

I’m implementing a Vulkan rendering loop with multiple frames in flight using a producer‑consumer pattern and a thread pool.

The main loop logic is:

PreRender on the main thread — wait on fences, acquire swapchain image.

Submit PrepareFrame and RenderFrame to a thread pool.

Wait (Join) for thread pool tasks to finish.

PostRender on the main thread — present the image.

Repeat.

However, on the very first frame, Vulkan validation reports errors that the command buffer is unrecorded and the semaphore used for waiting has no way to be signaled.


❓ What I’m struggling with

On the first iteration:

PrepareFrame records commands (color clear pass).

But RenderFrame — which submits the command buffer — runs with an incorrect frame index, submitting a command buffer that was never recorded.

Validation layers report that the wait semaphore was never signaled.

Here’s the relevant call chain:

void View::OnUpdate(double deltaTime) { if (m_RenderSurface) { m_RenderSurface->PreRender(); m_ThreadsUpdate.Submit([&]() { m_RenderQueue->SetDeltaTime(static_cast<float>(deltaTime)); m_RenderSurface->PrepareFrame(m_RenderQueue); }); m_ThreadsUpdate.Submit([&]() { m_RenderSurface->RenderFrame(); }); m_ThreadsUpdate.Join(); m_RenderSurface->PostRender(); } }

Code snippets (frames / Vulkan)

Frame data struct:

struct FrameData { VkFence fence = VK_NULL_HANDLE; VkSemaphore acquireSemaphore = VK_NULL_HANDLE; VkSemaphore renderSemaphore = VK_NULL_HANDLE; VkCommandBuffer cmdBuffer = VK_NULL_HANDLE; uint32_t imageIndex = 0; bool inFlight = false; }; std::array<FrameData, FRAMES_IN_FLIGHT> m_Frames;

Index rotation:

void IGAPI::EndRender() { m_IndexFrameRender = (m_IndexFrameRender + 1) % BACK_BUFFER_COUNT; m_IndexFramePrepare = (m_IndexFrameRender + 1) % BACK_BUFFER_COUNT; }

code:

#pragma region Rendering void SurfView_VK::PreRender() { static zU32 stepCounter = 0; stepCounter++; VkDevice device = m_VulkanAPI->GetDevice(); zU32 indexPrepare = m_VulkanAPI->GetIndexFramePrepare(); auto& frame = m_Frames[indexPrepare]; VkResult vr = vkWaitForFences(device, 1, &frame.fence, VK_TRUE, UINT64_MAX); if (vr != VK_SUCCESS) throw_runtime_error("Failed to vkWaitForFences()."); uint32_t imageIndex; vr = vkAcquireNextImageKHR(device, m_Swapchain, UINT64_MAX, frame.acquireSemaphore, VK_NULL_HANDLE, &imageIndex); if (vr == VK_ERROR_OUT_OF_DATE_KHR) { if (!RecreateSwapchain()) throw_runtime_error("Failed to RecreateSwapchain()."); return; } else if (vr != VK_SUCCESS && vr != VK_SUBOPTIMAL_KHR) { throw_runtime_error("Failed to vkAcquireNextImageKHR()."); } DebugOutputLite(L">>>>> #0 PreRender(). stepCounter: {}, indexPrepare: {}, imageIndex: {}.", stepCounter, indexPrepare, imageIndex); frame.imageIndex = imageIndex; } void SurfView_VK::PrepareFrame(const std::shared_ptr<RenderQueue> renderQueue) { static zU32 stepCounter = 0; stepCounter++; VkDevice device = m_VulkanAPI->GetDevice(); zU32 indexPrepare = m_VulkanAPI->GetIndexFramePrepare(); auto& frame = m_Frames[indexPrepare]; DebugOutputLite(L">>>>> PrepareFrame(). stepCounter: {}, indexPrepare: {}, frame.imageIndex: {}.", stepCounter, indexPrepare, frame.imageIndex); VkResult vr = vkResetCommandBuffer(frame.cmdBuffer, 0); if (vr != VK_SUCCESS) throw_runtime_error("Failed to vkResetCommandBuffer()."); VkCommandBufferBeginInfo beginInfo = {}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vr = vkBeginCommandBuffer(frame.cmdBuffer, &beginInfo); if (vr != VK_SUCCESS) throw_runtime_error("Failed to vkBeginCommandBuffer()."); // TODO: команды рендера // Начало render pass VkRenderPassBeginInfo renderPassInfo = {}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; renderPassInfo.renderPass = m_RenderPass; renderPassInfo.framebuffer = m_Framebuffers[frame.imageIndex]; renderPassInfo.renderArea.offset = { 0, 0 }; renderPassInfo.renderArea.extent = m_SwapchainExtent; VkClearValue clearValues[2]; clearValues[0].color = { {0.3f, 0.3f, 0.6f, 1.0f} }; clearValues[1].depthStencil = { 1.0f, 0 }; renderPassInfo.clearValueCount = 2; renderPassInfo.pClearValues = clearValues; vkCmdBeginRenderPass(frame.cmdBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); // Здесь ваши команды рендеринга (draw calls) vkCmdEndRenderPass(frame.cmdBuffer); vr = vkEndCommandBuffer(frame.cmdBuffer); if (vr != VK_SUCCESS) throw_runtime_error("Failed to vkEndCommandBuffer()."); frame.inFlight = true; } void SurfView_VK::RenderFrame() { static zU32 stepCounter = 0; stepCounter++; VkDevice device = m_VulkanAPI->GetDevice(); zU32 indexRender = m_VulkanAPI->GetIndexFrameRender(); //zU32 indexRender = m_VulkanAPI->GetIndexFramePrepare(); auto& frame = m_Frames[indexRender]; DebugOutputLite(L">>>>> RenderFrame(). stepCounter: {}, indexRender: {}.", stepCounter, indexRender); VkResult vr = vkResetFences(device, 1, &frame.fence); if (vr != VK_SUCCESS) throw_runtime_error("Failed to vkResetFences()."); VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;// VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.waitSemaphoreCount = 1; submitInfo.pWaitSemaphores = &frame.acquireSemaphore; submitInfo.pWaitDstStageMask = &waitStage; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &frame.cmdBuffer; submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &frame.renderSemaphore; vr = vkQueueSubmit(m_VulkanAPI->GetGraphicsQueue(), 1, &submitInfo, frame.fence); if (vr != VK_SUCCESS) throw_runtime_error("Failed to vkQueueSubmit()."); } void SurfView_VK::PostRender() { static zU32 stepCounter = 0; stepCounter++; VkDevice device = m_VulkanAPI->GetDevice(); zU32 indexRender = m_VulkanAPI->GetIndexFrameRender(); //zU32 indexRender = m_VulkanAPI->GetIndexFramePrepare(); auto& frame = m_Frames[indexRender]; DebugOutputLite(L">>>>> PostRender(). stepCounter: {}, indexRender: {}.", stepCounter, indexRender); VkPresentInfoKHR presentInfo = {}; presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; presentInfo.waitSemaphoreCount = 1; presentInfo.pWaitSemaphores = &frame.renderSemaphore; presentInfo.swapchainCount = 1; presentInfo.pSwapchains = &m_Swapchain; presentInfo.pImageIndices = &frame.imageIndex; std::lock_guard<std::mutex> lock(m_SwapchainMutex); VkResult vr = vkQueuePresentKHR(m_VulkanAPI->GetPresentQueue(), &presentInfo); if (vr != VK_SUCCESS) throw_runtime_error("Failed to vkQueuePresentKHR()."); frame.inFlight = false; } #pragma endregion

Validation output on first iteration

>>>> #0 PreRender(). stepCounter: 1, indexPrepare: 0, imageIndex: 0. >>>>> PrepareFrame(). stepCounter: 1, indexPrepare: 0, frame.imageIndex: 0. >>>>> RenderFrame(). stepCounter: 1, indexRender: 1. >>>>> -= DebugOutput =- Message: [Vulkan Validation] ERROR | VALIDATION: vkQueueSubmit(): pSubmits[0].pCommandBuffers[0] VkCommandBuffer 0x1c2556b58f0 is unrecorded and contains no commands. The Vulkan spec states: Each element of the pCommandBuffers member of each element of pSubmits must be in the pending or executable state (https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html#VUID-vkQueueSubmit-pCommandBuffers-00070) Source: [void __cdecl zzz::IGAPI::LogGPUDebugMessage(const class std::basic_string<wchar_t,struct std::char_traits<wchar_t>,class std::allocator<wchar_t> > &)] line: 114, file: C:\Workspaces\zzz\Zzz\engineFoundation\engineCore\Source\Interfaces\IGAPI\IGAPI.cppm >>>>> -= DebugOutput =- Message: [Vulkan Validation] ERROR | VALIDATION: vkQueueSubmit(): pSubmits[0].pWaitSemaphores[0] queue (VkQueue 0x1c25308d560) is waiting on semaphore (VkSemaphore 0x170000000017) that has no way to be signaled. The Vulkan spec states: All elements of the pWaitSemaphores member of all elements of pSubmits created with a VkSemaphoreType of VK_SEMAPHORE_TYPE_BINARY must reference a semaphore signal operation that has been submitted for execution and any semaphore signal operations on which it depends must have also been submitted for execution (https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html#VUID-vkQueueSubmit-pWaitSemaphores-03238) Source: [void __cdecl zzz::IGAPI::LogGPUDebugMessage(const class std::basic_string<wchar_t,struct std::char_traits<wchar_t>,class std::allocator<wchar_t> > &)] line: 114, file: C:\Workspaces\zzz\Zzz\engineFoundation\engineCore\Source\Interfaces\IGAPI\IGAPI.cppm >>>>> PostRender(). stepCounter: 1, indexRender: 1. >>>>> -= DebugOutput =- Message: [Vulkan Validation] ERROR | VALIDATION: vkQueuePresentKHR(): pPresentInfo->pImageIndices[0] was acquired with a semaphore VkSemaphore 0x140000000014 that has not since been waited on Source: [void __cdecl zzz::IGAPI::LogGPUDebugMessage(const class std::basic_string<wchar_t,struct std::char_traits<wchar_t>,class std::allocator<wchar_t> > &)] line: 114, file: C:\Workspaces\zzz\Zzz\engineFoundation\engineCore\Source\Interfaces\IGAPI\IGAPI.cppm

Expected behavior

I expect that:

The first frame records commands in PrepareFrame and submits them in RenderFrame.

The present semaphore is properly signaled and waited on in vkQueuePresentKHR.


Code repository

Full code and branch with this Vulkan implementation is available here:

https://github.com/k119-55524/Zzz
Branch: origin/clear-VK-api


Question

How should the first frame be correctly recorded and submitted in this frames‑in‑flight Vulkan pipeline to avoid “unrecorded command buffer” and “unsignaled semaphore” errors?

Is there a recommended pattern to initialize and submit the very first frame when using multiple frames in flight and a thread pool?

Alternatively, please provide links to tutorials or guides that explain how to implement a correct producer‑consumer rendering pipeline in Vulkan with multiple frames in flight and synchronization.

Read Entire Article