vk::raii::CommandPool commandPool = nullptr;
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::CommandBufferBeginInfo 的 flags 成员指定我们将如何使用命令缓冲。
以下值是可用的:
vk::CommandBufferUsageFlagBits::eOneTimeSubmit:命令缓冲将在执行一次后立即重新记录。
vk::CommandBufferUsageFlagBits::eRenderPassContinue:这是一个辅助命令缓冲,将完全位于单个渲染通道内。
vk::CommandBufferUsageFlagBits::eSimultaneousUse:命令缓冲可以在其已挂起执行的同时重新提交。
目前这些标志都不适用于我们。
vk::CommandBufferBeginInfo 的 pInheritanceInfo 成员仅与辅助命令缓冲相关。
它指定要从调用的主命令缓冲继承哪些状态。
如果命令缓冲已经被记录过一次,那么调用 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。colorAttachmentCount 和 pColorAttachments 参数指定要渲染到的颜色附件。
现在我们可以开始渲染了:
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();
在下一章中,我们将编写主循环的代码,它将从交换链获取一张图像,记录并执行命令缓冲,然后将完成的图像返回给交换链。
C++ 代码 / Slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。