Vulkan Command Buffer Lifecycle: Create, Record, Submit, and Synchronize
Step‑by‑step guide to allocate command buffers from a pool, record graphics commands, submit them with semaphores and fences, and verify correct execution.
17 Sept 2025, 03:42 UTC

Desired Outcome
Successfully create a Vulkan command pool, allocate primary command buffers, record a graphics pipeline draw call, submit the buffers to a graphics queue with semaphore‑fence synchronization, and verify that the GPU completes the work before the next frame begins.
Prerequisites
- A Vulkan instance and physical device selected.
- A logical device created with a queue family that supports
VK_QUEUE_GRAPHICS_BIT. - The Vulkan validation layer
VK_LAYER_KHRONOS_validationenabled for debugging. - Basic understanding of Vulkan handles (
VkDevice,VkQueue,VkCommandPool, etc.).
Procedure
-
Create a command pool
Command pools allocate memory for command buffers and are tied to a specific queue family.
VkCommandPoolCreateInfo poolInfo = {} poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; poolInfo.queueFamilyIndex = graphicsQueueFamilyIndex; // obtained from vkGetPhysicalDeviceQueueFamilyProperties poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; // allow individual buffer resets VkCommandPool commandPool; VkResult res = vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool); if (res != VK_SUCCESS) { /* handle error */ } -
Allocate command buffers
Primary buffers record rendering commands directly; secondary buffers are optional for reusable work.
VkCommandBufferAllocateInfo allocInfo = {} allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.commandPool = commandPool; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandBufferCount = 1; VkCommandBuffer commandBuffer; res = vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); if (res != VK_SUCCESS) { /* handle error */ } -
Begin recording
Recording starts with
vkBeginCommandBuffer. No GPU work occurs until submission.VkCommandBufferBeginInfo beginInfo = {} beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = 0; // optional: VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT res = vkBeginCommandBuffer(commandBuffer, &beginInfo); if (res != VK_SUCCESS) { /* handle error */ } -
Record graphics commands
Bind pipeline, descriptor sets, vertex buffers, and issue a draw call. Example assumes a render pass is already active.
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSet, 0, nullptr); vkCmdBindVertexBuffers(commandBuffer, 0, 1, &vertexBuffer, offsets); vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0); -
End recording
Finish recording with
vkEndCommandBuffer.res = vkEndCommandBuffer(commandBuffer); if (res != VK_SUCCESS) { /* handle error */ } -
Create synchronization objects
Use a binary semaphore to coordinate with swapchain presentation and a fence for CPU‑GPU sync.
VkSemaphoreCreateInfo semInfo = {} semInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; VkSemaphore renderFinishedSemaphore; vkCreateSemaphore(device, &semInfo, nullptr, &renderFinishedSemaphore); VkFenceCreateInfo fenceInfo = {} fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; // start signaled so first wait returns immediately VkFence inFlightFence; vkCreateFence(device, &fenceInfo, nullptr, &inFlightFence); -
Submit command buffer
Submit the recorded buffer to the graphics queue, signalling the semaphore when finished and associating the fence for host‑side wait.
VkSubmitInfo submitInfo = {} submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; VkSemaphore waitSemaphores[] = { imageAvailableSemaphore }; // from swapchain acquire VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }; submitInfo.waitSemaphoreCount = 1; submitInfo.pWaitSemaphores = waitSemaphores; submitInfo.pWaitDstStageMask = waitStages; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffer; submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = &renderFinishedSemaphore; // Reset fence to unsignaled before waiting vkResetFences(device, 1, &inFlightFence); res = vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence); if (res != VK_SUCCESS) { /* handle error */ } -
Wait for GPU completion
Host waits on the fence; a timeout indicates a synchronization problem.
res = vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX); if (res != VK_SUCCESS) { /* handle timeout or error */ } -
Optional: Reset command buffer for reuse
After the fence signals, the buffer can be reset and re‑recorded for the next frame.
vkResetCommandBuffer(commandBuffer, 0); // returns to initial state
Expected Checks
- Validation layer emits no
VK_ERROR_VALIDATION_FAILED_EXTwarnings during creation, recording, or submission. vkGetFenceStatusreturnsVK_SUCCESSaftervkWaitForFences, confirming GPU completed all commands.- Semaphore values observed via
vkGetSemaphoreCounterValue(if using timeline semaphores) increase by exactly one per frame.
Recovery Options
- If
vkWaitForFencestimes out, reset the fence (vkResetFences) and resubmit the same command buffer after ensuring no overlapping use. - If validation reports \"command buffer is already in recording state\", call
vkResetCommandBuffer(or allocate a fresh buffer) before beginning a new recording pass. - For secondary buffer errors (e.g., attempting to draw directly), ensure they are only executed via
vkCmdExecuteCommandsfrom a primary buffer.
Version Notes
The procedure works with Vulkan 1.0. Starting with Vulkan 1.1, vkQueueSubmit2 offers advanced semaphore signaling; the same submission pattern applies, replacing VkSubmitInfo with VkSubmitInfo2 and providing VkSemaphoreSignalInfo structures. Always check vkGetInstanceProcAddr for the function pointer if targeting multiple versions.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.