Skip to content

门泊吴船亦已谋

Vulkan 画三角形 第九章 渲染路径

我们需要配置渲染时需要使用的帧缓冲区附着的相关信息。我们需要指定有多少个颜色和深度缓冲区,它们的采样数以及通过渲染操作应当如何处理它们的内容。所有的这些信息都会被封装到渲染路径对象中,于是我们添加一个成员变量来保存它

VkRenderPass renderPass;

添加一个成员函数,并在 initVulkan 中调用它

void initVulkan() {
    createInstance();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapchain();
    createImageViews();
    createRenderPass();
}

void createRenderPass() {

}

接下来我们开始填充这个函数

附着描述

我们只需要单独的一个由交换链中的一张图像代表的颜色附着

VkAttachmentDescription colorAttachment = {};
colorAttachment.format = swapchainImageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;

format 应当与交换链图像的格式相匹配

我们不需要多重采样,因此使用 VK_SAMPLE_COUNT_1_BIT

loadOp 和 storeOp 决定了渲染前与渲染后应当对附着中的数据如何处理。loadOp 有以下几种选择

  • VK_ATTACHMENT_LOAD_OP_LOAD
    保留附着中原有的内容
  • VK_ATTACHMENT_LOAD_OP_CLEAR
    清除附着中的值,设为最开始的常量
  • VK_ATTACHMENT_LOAD_OP_DONT_CARE
    附着中的原有内容是未定义的,不关心它

而我们在绘制新的一帧的时候应当清除帧缓冲区的内容,恢复成黑色

storeOp 有以下几种选择

  • VK_ATTACHMENT_STORE_OP_STORE
    渲染好的内容会被保留在内存中,以供后续的读取
  • VK_ATTACHMENT_STORE_OP_DONT_CARE
    渲染后帧缓冲区的内容是未定义的

当然我们需要在屏幕上看到这个三角形,所以选择第一个

loadOp 和 storeOp 应用与颜色和深度数据,而 stencilLoadOp 和 stencilStoreOp 应用于模板数据。我们的应用不需要模板缓冲区,因此它们无关紧要

Vulkan 中纹理和帧缓冲区是由指定了某种像素格式的 VkImage 对象代表的,然而像素在内存中的布局可以根据你的需求进行改变,最常见的几种布局有这几种

  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
    用作颜色附着的图像
  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
    在交换链中用于展示的图像
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
    用作内存拷贝操作的终点的图像

我们需要知道的是图像在即将用于某种操作之前需要先转换为与之适配的特定的布局

initialLayout 指定了渲染路径开始之前图像的布局,finalLayout 指定了经过渲染路径之后图像需要自动转换到的布局。把 initialLayout 设为 VK_IMAGE_LAYOUT_UNDEFINED 表示我们不关心图像之前是什么类型的布局。需要注意,选择这个值意味着图像中的内容不保证会被保留,不过现在这个问题不重要因为我们本来就会清除它。我们希望在渲染后这个图像就能够用交换链展示到屏幕上,于是我们把 finalLayout 设为 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR

子路径和附着引用

一个渲染路径会由多个子路径组成。子路径就是一系列有序的渲染操作,它依赖于上一个路径处理后的帧缓冲区中的内容,例如一系列一个接一个的后处理特效。如果你把这些渲染操作分组到同一个渲染路径,Vulkan 就能重排这些操作,节约内存带宽来尽可能提高性能。不过对于我们的第一个三角形,我们使用一条子路径就行了

每一个子路径需要引用一个或多个附着,也就是我们上面提到的附着描述结构体。不过我们不是使用指针,而是使用 VkAttachmentReference 结构体来描述引用关系

VkAttachmentReference colorAttachmentRef = {};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

attachment 参数指定了需要引用的附着在附着数组中的索引。我们只有一个 VkAttachmentDescription 结构体,也就是数组只有一个元素,所以它的索引是零。layout 参数指定了在子路径过程中这个附着应当使用哪种布局,Vulkan 会在子路径开始时自动地把附着转换为这种布局,我们希望把这个附着作为颜色附着来使用,因此选择 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 布局来最优化性能

一个子路径由一个 VkSubpassDescription 结构体来描述

VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;

Vulkan 将来可能会支持计算子路径,因此这里我们需要用 pipelineBindPoint 成员显式地指定为图形子路径

然后 pColorAttachments 指向一个数组,其中的附着将会与片元着色器中的输出根据数组中的索引一一对应。这里我们指向了一个结构体,也就是长度为一的数组,也就跟上一篇文章中的 layout**(location = 0)** out vec4 outColor 这行命令相对应

接下来几种类型的附着也同样能被子路径引用

  • pInputAttachments
    被着色器读取的附着
  • pResolveAttachments
    用于多重采样的颜色附着
  • pDepthStencilAttachment
    用于深度和模板数据的附着
  • pPreserveAttachments
    不用于当前子路径的附着,但是数据将会被保留

子路径依赖

之前提到,渲染路径中的子路径会自动处理图像布局的转换。这些转换需要由子路径依赖控制,子路径依赖指定了子路径之间的内存与执行的依赖关系。这里虽然我们只有一个子路径,但是这个子路径之前与之后的操作其实也可以认为是隐式的子路径

有两种内置的默认依赖关系分别用来处理渲染路径开始时与结束时的图像布局转换,但是前者并不合适。它假设转换发生在流水线的一开始,但是我们在那个时候还没有能够从交换链中拿到图像。于是我们可以让渲染路径等待 VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT 阶段

子路径的依赖关系由多个 VkSubpassDependency 结构体指定,当然我们这里只需要指定一个依赖关系,也就是渲染路径之前的操作(隐式的子路径)与我们之前定义的子路径之间的依赖关系

VkSubpassDependency dependency = {};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;

srcSubpass 和 dstSubpass 指定的是两个存在依赖关系的独立的子路径的索引,即索引为 dstSubpass 的子路径依赖于索引为 srcSubpass 的子路径,VK_SUBPASS_EXTERNAL 这个值其实用于指代渲染路径之前或之后的操作,是之前还是之后取决于它被用于 srcSubpass 还是 dstSubpass。然后零这个索引指的是我们之前定义的子路径,是第一个也就是我们唯一的一个子路径。dstSubpass 的值必须要比 srcSubpass 的值更大,以此避免循环依赖问题(除了 VK_SUBPASS_EXTERNAL)

而 srcStageMask 和 srcAccessMask 指定的是需要等待的源子路径中的操作以及操作所在的阶段。我们在进入子路径访问图像之前,需要等待交换链完成对图像的读取。我们可以通过等待颜色附着输出阶段本身来实现

接下来的 dstStageMask 和 dstAccessMask 指定的是目标子路径中的操作以及操作所处于的阶段,这里我们的操作涉及写入颜色缓冲区,且位于颜色附着阶段。这些设置可以防止转换的发生,直到它被允许时,这里我们希望在开始写入颜色时才发生转换

渲染路径

现在附着和引用它的子路径都已经讨论完了,我们就可以开始创建渲染路径了

VkRenderPassCreateInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;

if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS)
    throw std::runtime_error("failed to create render pass")

这些参数都非常显然,只是传入之前定义好的结构体

最后需要在 cleanup 中销毁它

void cleanup() {
    vkDestroyRenderPass(device, renderPass, nullptr);
    for (auto imageView : swapchainImageViews)
        vkDestroyImageView(device, imageView, nullptr);
    vkDestroySwapchainKHR(device, swapchain, nullptr);
    vkDestroyDevice(device, nullptr);
    vkDestroySurfaceKHR(instance, surface, nullptr);
    vkDestroyInstance(instance, nullptr);

    glfwDestroyWindow(window);
    glfwTerminate();
}