Vulkan. Developer's guide. Rendering

I am engaged in technical translations at the Izhevsk IT company CG Tribe and continue to publish the translation of the Vulkan Tutorial lessons into Russian. The original text of the guide can be found here .



My post today is about the first two articles in the Drawing section, Framebuffers and Command buffers.



Content
1.



2.



3.



4.







  1. (pipeline)



5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ









Framebuffers



In the last chapters, we talked a lot about framebuffers and set up a render pass for one framebuffer with the same format as the image from the swap chain. However, the framebuffer itself has not yet been created.



The attachments we referenced when creating the render pass need to be wrapped in a VkFramebuffer . It points to all the VkImageView objects that we will use as targets. In our case, there is only one buffer: the color buffer. However, the swap chain contains many images, and we must use exactly the one that we got from the swap chain for drawing. In other words, we need to create framebuffers for each image from the swap chain and use the framebuffer to which the image of interest is attached.



To do this, add one more class member:



std::vector<VkFramebuffer> swapChainFramebuffers;
      
      





Let's add a function createFramebuffers



and call it right after creating the graphics pipeline. Inside this method, we will create objects for our array:



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
}

...

void createFramebuffers() {

}
      
      





First, let's allocate the necessary space in the container for storing framebuffers:



void createFramebuffers() {
    swapChainFramebuffers.resize(swapChainImageViews.size());
}
      
      





Then we go through all the image views and create framebuffers from them:



for (size_t i = 0; i < swapChainImageViews.size(); i++) {
    VkImageView attachments[] = {
        swapChainImageViews[i]
    };

    VkFramebufferCreateInfo framebufferInfo{};
    framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
    framebufferInfo.renderPass = renderPass;
    framebufferInfo.attachmentCount = 1;
    framebufferInfo.pAttachments = attachments;
    framebufferInfo.width = swapChainExtent.width;
    framebufferInfo.height = swapChainExtent.height;
    framebufferInfo.layers = 1;

    if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) {
        throw std::runtime_error("failed to create framebuffer!");
    }
}
      
      





As you can see, it is not difficult to create a framebuffer. First you need to specify which one renderPass



it should be compatible with. Framebuffers can only be used with compatible render passes, that is, they use the same number and type of buffers.



The parameters attachmentCount



and pAttachments



point to the VkImageView objects that should match the description pAttachments



used when creating the render pass.



Parameters width



and height



do not cause problems. The field layers



specifies the number of layers for images. Our images only have one layer, so layers = 1



.



The framebuffers need to be removed before the image views and the render pass, but only after rendering is complete:



void cleanup() {
    for (auto framebuffer : swapChainFramebuffers) {
        vkDestroyFramebuffer(device, framebuffer, nullptr);
    }

    ...
}
      
      





We now have all the objects we need to render. In the next chapter, we will already be able to write the first drawing commands.



C ++ Code / Vertex Shader / Fragment Shader





Command buffers





Commands in Vulkan, such as drawing and moving in memory, are not directly executed when the corresponding function is called. All necessary operations must be written to the buffer. This is convenient because the complex process of configuring commands can be done in advance and in multiple threads. In the main loop, all you have to do is tell Vulkan to run commands.



Team pool



Before moving on to creating the command buffer, we must create the command pool. The command pool manages the memory that is used to store buffers.



Let's add a new member of the VkCommandPool class .



VkCommandPool commandPool;
      
      





Now let's create a new function createCommandPool



and call it from initVulkan



after creating the framebuffers.



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
}

...

void createCommandPool() {

}
      
      





You only need two parameters to create a command pool:



QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);

VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
poolInfo.flags = 0; // Optional
      
      





To run command buffers, they must be sent to a queue, such as a graphics or display queue. The command pool can only allocate command buffers for one queue family. We need to write the drawing commands, so we are using a family of graphics-enabled queues.



There are two possible flags for the command pool:



  • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT



    : a hint telling Vulkan that buffers from the pool will be flushed and allocated frequently (may change the behavior of the pool when allocating memory)
  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT



    : allows you to overwrite buffers independently; if the flag is not set, it will be possible to reset the buffers only all and at the same time


We will write command buffers only at the beginning of the program, and then we will repeatedly run them in the main loop, so none of these flags suits us.



if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
    throw std::runtime_error("failed to create command pool!");
}
      
      





Let's complete the creation of the command pool with the vkCreateCommandPool function . There are no special parameters in it. The commands will be used throughout the entire life cycle of the program, so the pool must be destroyed at the very end:



void cleanup() {
    vkDestroyCommandPool(device, commandPool, nullptr);

    ...
}
      
      







Allocating memory for command buffers



Now we can allocate memory for command buffers and write drawing commands to them. Since each of the drawing commands is bound to a corresponding VkFrameBuffer , we must write a command buffer for each image in the swap chain. To do this, let's create a list of VkCommandBuffer objects as a class member. Once the command pool is destroyed, the command buffers are automatically freed, so we don't need to explicitly flush it.



std::vector<VkCommandBuffer> commandBuffers;
      
      





Let's move on to the function createCommandBuffers



that allocates memory and writes commands for each image from the swap chain.



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
    createCommandBuffers();
}

...

void createCommandBuffers() {
    commandBuffers.resize(swapChainFramebuffers.size());
}
      
      





Command buffers are created using the vkAllocateCommandBuffers function , which takes a VkCommandBufferAllocateInfo structure as a parameter. The structure indicates the command pool and the number of buffers:



VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();

if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate command buffers!");
}
      
      





The parameter level



determines whether command buffers are primary or secondary.



  • VK_COMMAND_BUFFER_LEVEL_PRIMARY



    : Primary buffers can be queued, but cannot be called from other command buffers.
  • VK_COMMAND_BUFFER_LEVEL_SECONDARY



    : Secondary buffers are not sent directly to the queue, but can be called from the primary command buffers.


We will not use secondary command buffers, although it can sometimes be convenient to transfer some frequently used command sequences into them and reuse them in the primary buffer.



Write command buffer



Let's start recording the command buffer by calling vkBeginCommandBuffer . As an argument, we pass a small VkCommandBufferBeginInfo structure that contains information about the use of this buffer.



for (size_t i = 0; i < commandBuffers.size(); i++) {
    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = 0; // Optional
    beginInfo.pInheritanceInfo = nullptr; // Optional

    if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) {
        throw std::runtime_error("failed to begin recording command buffer!");
    }
}
      
      





The parameter flags



specifies how the command buffer will be used. The following values ​​are available:



  • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT



    : After each start, the command buffer is overwritten.
  • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT



    : indicates that this will be a secondary command buffer that is entirely within one render pass.
  • VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT



    : the buffer can be re-sent to the queue even if its previous call has not yet been completed and is in a pending state.


At the moment, none of the flags suits us.



This parameter is pInheritanceInfo



used only for command secondary buffers. It defines the state inherited from the primary buffers.



If the command buffer has already been written once, calling vkBeginCommandBuffer will implicitly flush it. You cannot add commands to an already full buffer.



Start render pass



Let's start drawing the triangle by starting the render pass with vkCmdBeginRenderPass . The render pass is configured using some parameters in the VkRenderPassBeginInfo structure .



VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[i];
      
      





The first parameters define the render pass and framebuffer. We have created a framebuffer for each image in the swap chain, which is used as a color buffer.



renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;
      
      





The next two parameters determine the size of the render area. The render area defines the portion of the framebuffer where shaders can save and load data. Pixels outside this area will be undefined. For best performance, the render area should match the size of the buffers.



VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
      
      





The last two parameters define the values ​​for flushing buffers when using the operation VK_ATTACHMENT_LOAD_OP_CLEAR



. Fill the framebuffer with black at 100% opacity.



Now let's start the render pass.



vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
      
      





Functions for writing commands to the buffer can be found by the vkCmd prefix . They all return void



, so error handling will be done only after the end of the recording.



The first parameter for each command is the command buffer into which the command is written. The second parameter is information about the render pass. The last parameter determines how the commands will be provided in the subpass. You can choose one of two values:



  • VK_SUBPASS_CONTENTS_INLINE



    : commands will be written to the primary command buffer without triggering secondary buffers.
  • VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS



    : commands will be run from command secondary buffers.


We will not be using command buffers, so we will choose the first option.



Basic drawing commands



Now we can connect the graphics pipeline:



vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
      
      





The second parameter indicates which pipeline is used - graphics or computational. It remains to draw a triangle:



vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);
      
      





The vkCmdDraw function looks very modest, but it is simple due to the fact that we have specified all the data in advance. In addition to the command buffer, the following parameters are passed to the function:



  • vertexCount



    : even though we are not using a vertex buffer, we technically have 3 vertices to draw the triangle.
  • instanceCount



    : used when it is necessary to render several instances of one object (instanced rendering); pass 1



    if not using this function.
  • firstVertex



    : used as an offset in the vertex buffer, specifies the smallest value gl_VertexIndex



    .
  • firstInstance



    : used as an offset when drawing multiple instances of the same object; determines the smallest value gl_InstanceIndex



    .


Completion



Now we can finish the render pass:



vkCmdEndRenderPass(commandBuffers[i]);
      
      





And finish writing the command buffer:



if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) {
    throw std::runtime_error("failed to record command buffer!");
}
      
      





In the next chapter, we will write the code for the main loop, where the image is extracted from the swap chain, after which the corresponding command buffer is launched, and then the finished image is returned to the swap chain for display.



C ++ Code / Vertex Shader / Fragment Shader



All Articles