狸村 Mystic Web · 狸村
目录

Vulkan 中的命令,如绘制操作和内存传输,并不是直接通过函数调用来执行的。 你必须将所有想要执行的操作记录到命令缓冲对象中。 这样做的优点是,当我们准备好告诉 Vulkan 我们想要做什么时,所有命令可以一起提交。由于所有命令都可以一起使用,Vulkan 可以更高效地处理这些命令。 此外,如果愿意的话,这也允许在多个线程中进行命令记录。

命令池

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

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

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

  • vk::CommandPoolCreateFlagBits::eTransient:提示命令缓冲会非常频繁地使用新命令重新记录(可能会改变内存分配行为)

  • vk::CommandPoolCreateFlagBits::eResetCommandBuffer:允许命令缓冲单独重新记录,没有此标志则必须一起重置

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

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

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

使用 vk::raii::CommandPool 构造函数完成命令池的创建。 它没有任何特殊参数。 命令将在整个程序中用于在屏幕上绘制内容。

命令缓冲分配

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

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

vk::raii::CommandBuffer commandBuffer = nullptr;

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

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

...

void createCommandBuffer()
{
}

命令缓冲使用 vk::raii::CommandBuffers 构造函数分配,该构造函数接受一个 vk::CommandBufferAllocateInfo 结构体作为参数,该结构体指定命令池和要分配的缓冲数量:

vk::CommandBufferAllocateInfo allocInfo{ .commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = 1 };

commandBuffer = std::move(vk::raii::CommandBuffers(device, allocInfo).front());

level 参数指定分配的命令缓冲是主命令缓冲还是辅助命令缓冲。

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

  • vk::CommandBufferLevel::eSecondary:不能直接提交,但可以从主命令缓冲中调用。

我们在这里不会使用辅助命令缓冲功能,但你可以想象,在主命令缓冲中重用常见操作会很有帮助。

由于我们只分配一个命令缓冲,commandBufferCount 参数就是 1。 vk::raii::CommandBuffers 构造函数会生成一个 std::vector<vk::raii::CommandBuffer>,但我们这里只需要一个,所以我们将其从这个向量中移出到一个单独的变量中。

命令缓冲记录

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

void recordCommandBuffer(uint32_t imageIndex)
{
}

我们总是通过调用 vk::raii::CommandBuffer::begin 并传入一个小的 vk::CommandBufferBeginInfo 结构体作为参数来开始记录命令缓冲,该结构体指定了有关此特定命令缓冲的用法的一些细节。

commandBuffer->begin({});

vk::CommandBufferBeginInfoflags 成员指定我们将如何使用命令缓冲。 以下值是可用的:

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

  • vk::CommandBufferUsageFlagBits::eRenderPassContinue:这是一个辅助命令缓冲,将完全位于单个渲染通道内。

  • vk::CommandBufferUsageFlagBits::eSimultaneousUse:命令缓冲可以在其已挂起执行的同时重新提交。

目前这些标志都不适用于我们。

vk::CommandBufferBeginInfopInheritanceInfo 成员仅与辅助命令缓冲相关。 它指定要从调用的主命令缓冲继承哪些状态。

如果命令缓冲已经被记录过一次,那么调用 vk::raii::CommandBuffer::begin 将隐式重置它。 不可能在稍后向缓冲追加命令。

图像布局转换

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

我们将使用管线屏障将图像布局从 vk::ImageLayout::eUndefined 转换为 vk::ImageLayout::eColorAttachmentOptimal

void transition_image_layout(
	    uint32_t                imageIndex,
	    vk::ImageLayout         old_layout,
	    vk::ImageLayout         new_layout,
	    vk::AccessFlags2        src_access_mask,
	    vk::AccessFlags2        dst_access_mask,
	    vk::PipelineStageFlags2 src_stage_mask,
	    vk::PipelineStageFlags2 dst_stage_mask)
{
		vk::ImageMemoryBarrier2 barrier = {
		    .srcStageMask        = src_stage_mask,
		    .srcAccessMask       = src_access_mask,
		    .dstStageMask        = dst_stage_mask,
		    .dstAccessMask       = dst_access_mask,
		    .oldLayout           = old_layout,
		    .newLayout           = new_layout,
		    .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 dependency_info = {
		    .dependencyFlags         = {},
		    .imageMemoryBarrierCount = 1,
		    .pImageMemoryBarriers    = &barrier};
    commandBuffer.pipelineBarrier2(dependency_info);
}

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

开始动态渲染

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

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

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

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::AttachmentStoreOp::eStore 存储渲染后的图像以供以后使用。

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

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

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

现在我们可以开始渲染了:

commandBuffer.beginRendering(renderingInfo);

所有记录命令的函数都返回 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);

实际的 vk::raii::CommandBuffer::draw 函数有点平淡无奇,但由于我们提前指定了所有信息,它变得如此简单。 它有以下参数:

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

  • instanceCount:用于实例化渲染,如果你不进行实例化则使用 1

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

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

完成

现在可以结束渲染了:

commandBuffer.endRendering();

渲染之后,我们需要将图像布局转换回 vk::ImageLayout::ePresentSrcKHR,以便将其呈现到屏幕:

// 在渲染之后,将交换链图像转换为 vk::ImageLayout::ePresentSrcKHR
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();

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

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。