Recording the Command Buffers#

Command buffers are the actual information sent to the GPU. They need to be appropriately recorded.

This is a fairly straightforward process that uses the Vulkan objects created in the initialisation stages. During initialisation, a number of command buffers equal to the number of Swapchain images were created. The application iterates through each of these command buffers and records the same set of commands into them, but each time rendering into a different Swapchain image. These commands tell the GPU what operations it needs to carry out in order to render a frame.

Different mutable objects and parts of memory are referenced for each swapchain image. Therefore, each swapchain image will be rendered slightly differently, representing the rotation of the triangle. These commands will not be executed by the GPU at the time they are recorded. Instead, they will start executing when the command buffer is submitted to the queue. We will discuss submission to the graphics queue in the next section.

A variety of GPU commands perform various operations, including:

  • Binding various objects, such as the vertex and index buffers, to the command buffer.

  • Setting any dynamic state for the pipeline.

  • Inserting pipeline barriers.

  • Updating push constants.

  • Transferring data between buffers and/or other memory objects.

  • Drawing primitives.

In the case of recordCommandBuffer(), the following commands are recorded into all command buffers:

  1. Set the viewport.

    • Both the viewport and scissor were created earlier during initialisation.

    • This is the area of the framebuffer that will be rendered to. Viewports act as transformations that define how the swapchain image’s co-ordinates map to the framebuffer pixel co-ordinates.

  2. Set the scissor.

    • This is a subsection of the viewport where rendering is allowed to occur. Pixels outside the scissor will not be processed and written out to the framebuffer.

  3. Record the render pass and begin operations.

    • A render pass describes the specifications of the set of attachments, such as colour, depth, and stencil buffers, used to produce a rendered image.

    • A render pass is started in a command buffer, on a specific framebuffer instance that is compatible with it – that is, its attachments follow the specifications of the render pass. The framebuffer object must be compatible with the render pass, so usually will have been created using that render pass instance as a blueprint. The render area that the render pass will affect is also defined.

    • A render pass contains at least one subpass which defines a phase of rendering and uses a subset of these attachments. The single subpass used in this example has the following set of commands associated with it:

      1. Bind the Pipeline to the command buffer. This means that the Pipeline previously created in the initialisation stage will handle any draw commands from this command buffer.

      2. Bind the descriptor sets. These link shader variables to actual resources. In this example, descriptor sets are used to allow the vertex shader to access the Uniform Buffer containing the transformation matrices and the fragment shader to access the texture image data. The transformation matrix is updated on each frame based on a new rotation value.

      3. Bind the vertex buffer. This buffer contains data about the three vertices of the triangle that will be rendered. This includes their position in 3D space and the 2D texture coordinates used to map the texture onto the triangle.

      4. Draw (in this case a single primitive). This command starts the rendering operations.

  4. End render pass.

    • Writing to the framebuffer is now complete. Note that in Vulkan, clearly marking render passes with begin and end is important because there are command buffer operations that are only allowed inside a render pass and others that are only allowed outside of one.

Each command does not serve a direct purpose in the dataflow diagram, but it is important to understand how these commands fit within the overall picture. View in context.

../../_images/record-command-buffer.png
void VulkanHelloAPI::recordCommandBuffer()
{
    // State the clear values for rendering.
    // This is the colour value that the framebuffer is cleared to at the start of the render pass.
    // The framebuffer is cleared because, during render pass creation, the loadOp parameter was set to VK_LOAD_OP_CLEAR. Remember
    // that this is crucial as it can reduce system memory bandwidth and reduce power consumption, particularly on PowerVR platforms.
    VkClearValue clearColor = { 0.00f, 0.70f, 0.67f, 1.0f };

    // This is a constant offset which specifies where the vertex data starts in the vertex
    // buffer. In this case the data just starts at the beginning of the buffer.
    const VkDeviceSize vertexOffsets[1] = { 0 };

    // Iterate through each created command buffer to record to it.
    for (size_t i = 0; i < appManager.cmdBuffers.size(); ++i)
    {
      // Reset the buffer to its initial state.
      debugAssertFunctionResult(vk::ResetCommandBuffer(appManager.cmdBuffers[i], 0), "Command Buffer Reset");

      // Begin the command buffer.
      VkCommandBufferBeginInfo cmd_begin_info = {};
      cmd_begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
      cmd_begin_info.pNext = nullptr;
      cmd_begin_info.flags = 0;
      cmd_begin_info.pInheritanceInfo = nullptr;

      debugAssertFunctionResult(vk::BeginCommandBuffer(appManager.cmdBuffers[i], &cmd_begin_info), "Command Buffer Recording Started.");

      // Start recording commands.
      // In Vulkan, commands are recorded by calling vkCmd... functions.
      // Set the viewport and scissor to previously defined values.
      vk::CmdSetViewport(appManager.cmdBuffers[i], 0, 1, &appManager.viewport);

      vk::CmdSetScissor(appManager.cmdBuffers[i], 0, 1, &appManager.scissor);

      // Begin the render pass.
      // The render pass and framebuffer instances are passed here, along with the clear colour value and the extents of
      // the rendering area. VK_SUBPASS_CONTENTS_INLINE means that the subpass commands will be recorded here. The alternative is to
      // record them in isolation in a secondary command buffer and then record them here with vkCmdExecuteCommands.
      VkRenderPassBeginInfo renderPassInfo = {};
      renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
      renderPassInfo.pNext = nullptr;
      renderPassInfo.renderPass = appManager.renderPass;
      renderPassInfo.framebuffer = appManager.frameBuffers[i];
      renderPassInfo.clearValueCount = 1;
      renderPassInfo.pClearValues = &clearColor;
      renderPassInfo.renderArea.extent = appManager.swapchainExtent;
      renderPassInfo.renderArea.offset.x = 0;
      renderPassInfo.renderArea.offset.y = 0;

      vk::CmdBeginRenderPass(appManager.cmdBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

      // Bind the pipeline to the command buffer.
      vk::CmdBindPipeline(appManager.cmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, appManager.pipeline);

      // A single large uniform buffer object is being used to hold all of the transformation matrices
      // associated with the swapchain images. It is for this reason that only a single descriptor set is
      // required for all of the frames.
      const VkDescriptorSet descriptorSet[] = { appManager.staticDescSet, appManager.dynamicDescSet };

      // An offset is used to select each slice of the uniform buffer object that contains the transformation
      // matrix related to each swapchain image.
      // Calculate the offset into the uniform buffer object for the current slice.
      uint32_t offset = static_cast<uint32_t>(appManager.dynamicUniformBufferData.bufferInfo.range * i);

      // Bind the descriptor sets. The &offset parameter is the offset into the dynamic uniform buffer which is
      // contained within the dynamic descriptor set.
      vk::CmdBindDescriptorSets(appManager.cmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, appManager.pipelineLayout, 0, NUM_DESCRIPTOR_SETS, descriptorSet, 1, &offset);

      // Bind the vertex buffer.
      vk::CmdBindVertexBuffers(appManager.cmdBuffers[i], 0, 1, &appManager.vertexBuffer.buffer, vertexOffsets);

      // Draw three vertices.
      vk::CmdDraw(appManager.cmdBuffers[i], 3, 1, 0, 0);

      // End the render pass.
      vk::CmdEndRenderPass(appManager.cmdBuffers[i]);

      // End the command buffer recording process.
      debugAssertFunctionResult(vk::EndCommandBuffer(appManager.cmdBuffers[i]), "Command Buffer Recording Ended.");

      // At this point the command buffer is ready to be submitted to a queue with all of the recorded operations executed
      // asynchronously after that. A command buffer can, and if possible should, be executed multiple times, unless
      // it is allocated with the VK_COMMAND_BUFFER_ONE_TIME_USE bit.
      // The command buffers recorded here will be reused across the lifetime of the application.
    }
}