Command Buffers
Commands in Vulkan, like drawing operations and memory transfers, are not executed directly using function calls. You have to record all the operations you want to perform in command buffer objects. The advantage of this is that when we are ready to tell Vulkan what we want to do, all the commands are submitted together. Vulkan can more efficiently process the commands since all of them are available together. In addition, this allows command recording to happen in multiple threads if so desired.
Command pools
We have to create a command pool before we can create command buffers.
Command pools manage the memory that is used to store the buffers and command buffers are allocated from them.
Add a new class member to store a VkCommandPool
:
vk::raii::CommandPool commandPool = nullptr;
Then create a new function createCommandPool
and call it from initVulkan
after the graphics pipeline was created.
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createGraphicsPipeline();
createCommandPool();
}
...
void createCommandPool() {
}
Command pool creation only takes two parameters:
vk::CommandPoolCreateInfo poolInfo{ .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = graphicsIndex };
There are two possible flags for command pools:
-
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT
: Hint that command buffers are rerecorded with new commands very often (may change memory allocation behavior) -
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
: Allow command buffers to be rerecorded individually, without this flag they all have to be reset together
We will be recording a command buffer every frame, so we want to be able to reset and rerecord over it.
Thus, we need to set the VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT
flag bit for our command pool.
Command buffers are executed by submitting them on one of the device queues, like the graphics and presentation queues we retrieved. Each command pool can only allocate command buffers that are submitted on a single type of queue. We’re going to record commands for drawing, which is why we’ve chosen the graphics queue family.
commandPool = vk::raii::CommandPool(device, poolInfo);
Finish creating the command pool using the vkCreateCommandPool
function.
It doesn’t have any special parameters.
Commands will be used throughout the program to draw things on the screen.
Command buffer allocation
We can now start allocating command buffers.
Create a VkCommandBuffer
object as a class member.
Command buffers will be automatically freed when their command pool is destroyed, so we don’t need explicit cleanup.
vk::raii::CommandBuffer commandBuffer = nullptr;
We’ll now start working on a createCommandBuffer
function to allocate a single command buffer from the command pool.
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
createImageViews();
createGraphicsPipeline();
createCommandPool();
createCommandBuffer();
}
...
void createCommandBuffer() {
}
Command buffers are allocated with the vkAllocateCommandBuffers
function, which takes a VkCommandBufferAllocateInfo
struct as parameter that specifies the command pool and number of buffers to allocate:
vk::CommandBufferAllocateInfo allocInfo{ .commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 };
commandBuffer = std::move(vk::raii::CommandBuffers(device, allocInfo).front());
The level
parameter specifies if the allocated command buffers are primary or secondary command buffers.
-
VK_COMMAND_BUFFER_LEVEL_PRIMARY
: Can be submitted to a queue for execution, but cannot be called from other command buffers. -
VK_COMMAND_BUFFER_LEVEL_SECONDARY
: Cannot be submitted directly, but can be called from primary command buffers.
We won’t make use of the secondary command buffer functionality here, but you can imagine that it’s helpful to reuse common operations from primary command buffers.
Since we are only allocating one command buffer, the commandBufferCount
parameter is just one.
Command buffer recording
We’ll now start working on the recordCommandBuffer
function that writes the commands we want to execute into a command buffer.
The VkCommandBuffer
used will be passed in as a parameter, as well as the index of the current swapchain image we want to write to.
void recordCommandBuffer(uint32_t imageIndex) {
}
We always begin recording a command buffer by calling vkBeginCommandBuffer
with a small VkCommandBufferBeginInfo
structure as argument that specifies some details about the usage of this specific command buffer.
commandBuffer->begin( {} );
The flags
parameter specifies how we’re going to use the command buffer.
The following values are available:
-
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT
: The command buffer will be rerecorded right after executing it once. -
VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT
: This is a secondary command buffer that will be entirely within a single render pass. -
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT
: The command buffer can be resubmitted while it is also already pending execution.
None of these flags are applicable for us right now.
The pInheritanceInfo
parameter is only relevant for secondary command buffers.
It specifies which state to inherit from the calling primary command buffers.
If the command buffer was already recorded once, then a call to vkBeginCommandBuffer
will implicitly reset it.
It’s not possible to append commands to a buffer at a later time.
Image layout transitions
Before we can start rendering to an image, we need to transition its layout to one that is suitable for rendering. In Vulkan, images can be in different layouts that are optimized for different operations. For example, an image can be in a layout that is optimal for presenting to the screen, or in a layout that is optimal for being used as a color attachment.
We’ll use a pipeline barrier to transition the image layout from VK_IMAGE_LAYOUT_UNDEFINED
to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
:
void transition_image_layout(
uint32_t imageIndex,
vk::ImageLayout oldLayout,
vk::ImageLayout newLayout,
vk::AccessFlags2 srcAccessMask,
vk::AccessFlags2 dstAccessMask,
vk::PipelineStageFlags2 srcStageMask,
vk::PipelineStageFlags2 dstStageMask
) {
vk::ImageMemoryBarrier2 barrier = {
.srcStageMask = srcStageMask,
.srcAccessMask = srcAccessMask,
.dstStageMask = dstStageMask,
.dstAccessMask = dstAccessMask,
.oldLayout = oldLayout,
.newLayout = newLayout,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = swapChainImages[imageIndex],
.subresourceRange = {
.aspectMask = vk::ImageAspectFlagBits::eColor,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1
}
};
vk::DependencyInfo dependencyInfo = {
.dependencyFlags = {},
.imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &barrier
};
commandBuffer.pipelineBarrier2(dependencyInfo);
}
This function will be used to transition the image layout before and after rendering.
Starting dynamic rendering
With dynamic rendering, we don’t need to create a render pass or framebuffers. Instead, we specify the attachments directly when we begin rendering:
// Before starting rendering, transition the swapchain image to COLOR_ATTACHMENT_OPTIMAL
transition_image_layout(
imageIndex,
vk::ImageLayout::eUndefined,
vk::ImageLayout::eColorAttachmentOptimal,
{}, // srcAccessMask (no need to wait for previous operations)
vk::AccessFlagBits2::eColorAttachmentWrite, // dstAccessMask
vk::PipelineStageFlagBits2::eTopOfPipe, // srcStage
vk::PipelineStageFlagBits2::eColorAttachmentOutput // dstStage
);
First, we transition the image layout to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
. Then, we set up the color attachment:
vk::ClearValue clearColor = vk::ClearColorValue(0.0f, 0.0f, 0.0f, 1.0f);
vk::RenderingAttachmentInfo attachmentInfo = {
.imageView = swapChainImageViews[imageIndex],
.imageLayout = vk::ImageLayout::eColorAttachmentOptimal,
.loadOp = vk::AttachmentLoadOp::eClear,
.storeOp = vk::AttachmentStoreOp::eStore,
.clearValue = clearColor
};
The imageView
parameter specifies which image view to render to. The imageLayout
parameter specifies the layout the image will be in during rendering. The loadOp
parameter specifies what to do with the image before rendering, and the storeOp
parameter specifies what to do with the image after rendering. We’re using VK_ATTACHMENT_LOAD_OP_CLEAR
to clear the image to black before rendering, and VK_ATTACHMENT_STORE_OP_STORE
to store the rendered image for later use.
Next, we set up the rendering info:
vk::RenderingInfo renderingInfo = {
.renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent },
.layerCount = 1,
.colorAttachmentCount = 1,
.pColorAttachments = &attachmentInfo
};
The renderArea
parameter defines the size of the render area, similar to the render area in a render pass. The layerCount
parameter specifies the number of layers to render to, which is 1 for a non-layered image. The colorAttachmentCount
and pColorAttachments
parameters specify the color attachments to render to.
Now we can begin rendering:
commandBuffer.beginRendering(renderingInfo);
All the functions that record commands can be recognized by their vkCmd
prefix. They all return void
, so there will be no error handling until we’ve finished recording.
The parameter for the beginRendering
command is the rendering info we just set up, which specifies the attachments to render to and the render area.
Basic drawing commands
We can now bind the graphics pipeline:
commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline);
The second parameter specifies if the pipeline object is a graphics or compute pipeline. We’ve now told Vulkan which operations to execute in the graphics pipeline and which attachment to use in the fragment shader.
As noted in the fixed functions chapter, we did specify viewport and scissor state for this pipeline to be dynamic. So we need to set them in the command buffer before issuing our draw command:
commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, static_cast<float>(swapChainExtent.width), static_cast<float>(swapChainExtent.height), 0.0f, 1.0f));
commandBuffer.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent));
Now we are ready to issue the draw command for the triangle:
commandBuffer.draw(3, 1, 0, 0);
The actual vkCmdDraw
function is a bit anticlimactic, but it’s so simple because of all the information we specified in advance.
It has the following parameters, aside from the command buffer:
-
vertexCount
: Even though we don’t have a vertex buffer, we technically still have 3 vertices to draw. -
instanceCount
: Used for instanced rendering, use1
if you’re not doing that. -
firstVertex
: Used as an offset into the vertex buffer, defines the lowest value ofSV_VertexId
. -
firstInstance
: Used as an offset for instanced rendering, defines the lowest value ofSV_InstanceID
.
Finishing up
The rendering can now be ended:
commandBuffer.endRendering();
After rendering, we need to transition the image layout back to VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
so it can be presented to the screen:
// After rendering, transition the swapchain image to PRESENT_SRC
transition_image_layout(
imageIndex,
vk::ImageLayout::eColorAttachmentOptimal,
vk::ImageLayout::ePresentSrcKHR,
vk::AccessFlagBits2::eColorAttachmentWrite, // srcAccessMask
{}, // dstAccessMask
vk::PipelineStageFlagBits2::eColorAttachmentOutput, // srcStage
vk::PipelineStageFlagBits2::eBottomOfPipe // dstStage
);
And we’ve finished recording the command buffer:
commandBuffer.end();
In the next chapter we’ll write the code for the main loop, which will acquire an image from the swap chain, record and execute a command buffer, then return the finished image to the swap chain.