命令缓冲区

      +

      在 Vulkan 中,绘图操作和内存传输等命令不是直接使用函数调用执行的。您必须将要执行的所有操作记录在命令缓冲区对象中。这样做的优势在于,当我们准备告诉 Vulkan 我们想要做什么时,所有命令都一起提交。Vulkan 可以更有效地处理命令,因为所有命令都一起可用。此外,这允许命令记录在多个线程中进行(如果需要的话)。

      命令池

      在创建命令缓冲区之前,我们必须创建一个命令池。命令池管理用于存储缓冲区的内存,命令缓冲区从中分配。添加一个新的类成员来存储 VkCommandPool

      vk::raii::CommandPool commandPool = nullptr;

      然后创建一个新函数 createCommandPool 并在创建图形管线后从 initVulkan 调用它。

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

      命令池创建只需要两个参数:

      vk::CommandPoolCreateInfo poolInfo{ .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer, .queueFamilyIndex = graphicsIndex };

      命令池有两个可能的标志:

      • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:提示命令缓冲区经常用新命令重新记录(可能会改变内存分配行为)

      • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许单独重置命令缓冲区,没有此标志它们必须一起重置

      我们将在每一帧记录一个命令缓冲区,因此我们希望能够重置并重新记录它。因此,我们需要为命令池设置 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT 标志位。

      命令缓冲区通过提交到设备队列之一来执行,例如我们检索的图形和呈现队列。每个命令池只能分配提交到单一类型队列的命令缓冲区。我们将记录用于绘制的命令,这就是为什么我们选择了图形队列族。

      commandPool = vk::raii::CommandPool(device, poolInfo);

      使用 vkCreateCommandPool 函数完成命令池的创建。它没有任何特殊参数。命令将在整个程序中用于在屏幕上绘制东西。

      命令缓冲区分配

      我们现在可以开始分配命令缓冲区了。

      创建一个 VkCommandBuffer 对象作为类成员。当命令池被销毁时,命令缓冲区将自动释放,因此我们不需要显式清理。

      vk::raii::CommandBuffer commandBuffer = nullptr;

      我们现在将开始编写 createCommandBuffer 函数,从命令池中分配一个命令缓冲区。

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

      命令缓冲区使用 vkAllocateCommandBuffers 函数分配,该函数将 VkCommandBufferAllocateInfo 结构作为参数,指定命令池和要分配的缓冲区数量:

      vk::CommandBufferAllocateInfo allocInfo{ .commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 };
      
      commandBuffer = std::move(vk::raii::CommandBuffers(device, allocInfo).front());

      level 参数指定分配的命令缓冲区是主要命令缓冲区还是次要命令缓冲区。

      • VK_COMMAND_BUFFER_LEVEL_PRIMARY:可以提交到队列执行,但不能从其他命令缓冲区调用。

      • VK_COMMAND_BUFFER_LEVEL_SECONDARY:不能直接提交,但可以从主要命令缓冲区调用。

      我们不会在这里使用次要命令缓冲区功能,但您可以想象,从主要命令缓冲区重用常见操作是有帮助的。

      由于我们只分配一个命令缓冲区,commandBufferCount 参数只是 1。

      命令缓冲区记录

      我们现在将开始编写 recordCommandBuffer 函数,该函数将我们想要执行的命令写入命令缓冲区。使用的 VkCommandBuffer 将作为参数传入,以及我们想要写入的当前交换链图像的索引。

      void recordCommandBuffer(uint32_t imageIndex) {
      
      }

      我们总是通过调用 vkBeginCommandBuffer 开始记录命令缓冲区,该函数以一个小的 VkCommandBufferBeginInfo 结构作为参数,指定有关此特定命令缓冲区使用的一些详细信息。

      commandBuffer->begin( {} );

      flags 参数指定我们将如何使用命令缓冲区。以下值可用:

      • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:命令缓冲区将在执行一次后立即重新记录。

      • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:这是一个次要命令缓冲区,将完全在单个渲染通道内。

      • VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:命令缓冲区可以重新提交,同时它已经在等待执行。

      现在这些标志对我们都不适用。

      pInheritanceInfo 参数仅与次要命令缓冲区相关。它指定从调用主要命令缓冲区继承哪些状态。

      如果命令缓冲区已经记录过一次,那么调用 vkBeginCommandBuffer 将隐式重置它。不可能在稍后时间向缓冲区附加命令。

      图像布局转换

      在我们开始渲染到图像之前,我们需要将其布局转换为适合渲染的布局。在 Vulkan 中,图像可以处于不同的布局中,这些布局针对不同的操作进行了优化。例如,图像可以处于适合呈现到屏幕的布局中,或者处于适合用作颜色附件的布局中。

      我们将使用管线屏障将图像布局从 VK_IMAGE_LAYOUT_UNDEFINED 转换为 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);
      }

      这个函数将用于在渲染前后转换图像布局。

      开始动态渲染

      使用动态渲染,我们不需要创建渲染通道或帧缓冲区。相反,我们在开始渲染时直接指定附件:

      // 在开始渲染之前,将交换链图像转换为 COLOR_ATTACHMENT_OPTIMAL
      transition_image_layout(
          imageIndex,
          vk::ImageLayout::eUndefined,
          vk::ImageLayout::eColorAttachmentOptimal,
          {},                                                     // srcAccessMask(不需要等待之前的操作)
          vk::AccessFlagBits2::eColorAttachmentWrite,                // dstAccessMask
          vk::PipelineStageFlagBits2::eTopOfPipe,                   // srcStage
          vk::PipelineStageFlagBits2::eColorAttachmentOutput        // dstStage
      );

      首先,我们将图像布局转换为 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。然后,我们设置颜色附件:

      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
      };

      imageView 参数指定要渲染到哪个图像视图。imageLayout 参数指定图像在渲染期间将处于的布局。loadOp 参数指定在渲染之前对图像做什么,storeOp 参数指定在渲染之后对图像做什么。我们使用 VK_ATTACHMENT_LOAD_OP_CLEAR 在渲染之前将图像清除为黑色,使用 VK_ATTACHMENT_STORE_OP_STORE 存储渲染的图像以供以后使用。

      接下来,我们设置渲染信息:

      vk::RenderingInfo renderingInfo = {
          .renderArea = { .offset = { 0, 0 }, .extent = swapChainExtent },
          .layerCount = 1,
          .colorAttachmentCount = 1,
          .pColorAttachments = &attachmentInfo
      };

      renderArea 参数定义渲染区域的大小,类似于渲染通道中的渲染区域。layerCount 参数指定要渲染到的层数,对于非分层图像为 1。colorAttachmentCountpColorAttachments 参数指定要渲染到的颜色附件。

      现在我们可以开始渲染:

      commandBuffer.beginRendering(renderingInfo);

      所有记录命令的函数都可以通过其 vkCmd 前缀识别。它们都返回 void,因此在我们完成记录之前不会有错误处理。

      beginRendering 命令的参数是我们刚刚设置的渲染信息,它指定要渲染到的附件和渲染区域。

      基本绘制命令

      我们现在可以绑定图形管线:

      commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, graphicsPipeline);

      第二个参数指定管线对象是图形管线还是计算管线。我们现在已经告诉 Vulkan 在图形管线中执行哪些操作以及在片段着色器中使用哪个附件。

      固定功能章节 中所述,我们确实指定了此管线的视口和剪刀状态为动态。因此,我们需要在发出绘制命令之前在命令缓冲区中设置它们:

      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));

      现在我们准备发出三角形的绘制命令:

      commandBuffer.draw(3, 1, 0, 0);

      实际的 vkCmdDraw 函数有点平淡无奇,但它之所以如此简单,是因为我们提前指定了所有信息。除了命令缓冲区外,它还有以下参数:

      • vertexCount:尽管我们没有顶点缓冲区,但从技术上讲,我们仍然有 3 个顶点要绘制。

      • instanceCount:用于实例化渲染,如果不这样做,请使用 1

      • firstVertex:用作顶点缓冲区的偏移量,定义 SV_VertexId 的最低值。

      • firstInstance:用作实例化渲染的偏移量,定义 SV_InstanceID 的最低值。

      完成

      现在可以结束渲染:

      commandBuffer.endRendering();

      渲染后,我们需要将图像布局转换回 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,以便它可以呈现到屏幕上:

      // 渲染后,将交换链图像转换为 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
      );

      我们已经完成了命令缓冲区的记录:

      commandBuffer.end();

      下一章 中,我们将编写主循环的代码,该循环将从交换链获取图像,记录并执行命令缓冲区,然后将完成的图像返回到交换链。