Vulkan: What's a command buffer and how to use it (pools, recording, reuse)

Almost nothing in Vulkan executes when you call it. vkCmdDraw, vkCmdCopyBuffer, vkCmdPipelineBarrier — all of them just append to a command buffer, a recording that the GPU consumes later at vkQueueSubmit. The command buffer is the unit of GPU work; the command pool is its allocator.

The lifecycle

example.txt
pool ──allocate──> cmdbuf ──begin──> record commands ──end──> submit
   ^                                                    |
   └──────────── reset (after fence) <──────────────────┘
  • VkCommandPool — allocates buffers for one queue family. Cheap to allocate from, and eResetCommandBuffer on the pool lets you reset buffers individually.
  • vkBeginCommandBuffer — starts recording. With eOneTimeSubmit you promise “recorded once, submitted once, then done” — drivers optimize for that.
  • vkQueueSubmit — hands it to the queue asynchronously. The function returns immediately; the fence signals later.

How to use it

  • One pool per thread. Pool operations are not externally synchronized — the standard pattern is pool-per-thread + per-frame buffers.
  • Per-frame buffers, not per-frame pools. Allocate 2–3 buffers (frames in flight), reset + re-record each frame — much cheaper than freeing pools.
  • Reset only after the fence. A buffer may be re-recorded only when the submit that used it has retired — that’s what the frames-in-flight fence is for.
  • eOneTimeSubmit for helpers: upload/transition helpers (like vkmini::oneTime) record → submit → waitIdle → free.

Primary vs secondary

Primary buffers are submitted to a queue; secondary buffers are called by a primary (vkCmdExecuteCommands) — that’s the mechanism for multi-threaded recording: N threads fill secondaries, one primary replays them in a render pass.

How NOT to use it

  • Don’t re-record a pending buffer. Classic corruption: frame 3 overwrites the cmd buffer still executing for frame 1 because no fence gated the slot.
  • Don’t submit the same command buffer twice while it’s in flight — same problem, different symptom.
  • Don’t create a pool per buffer — pools amortize allocation; 1000 pools ≈ 1000 mallocs of bookkeeping.
  • eSimultaneousUse isn’t a default — it exists for buffers submitted while a previous submission may still run; it has a cost. Prefer fence-gated reset.
  • Don’t forget begin/end — commands outside a begun buffer are silently dropped into nowhere (validation catches it, if you run validation layers).

Check out similar posts by category: C++ Vulkan