4.3 渲染通道、帧缓冲与动态状态 本节摘要:渲染通道规定「这一遍加工用哪几张附件、按什么次序、依赖关系如何」;帧缓冲把附件具体化;动态状态则把视口、剪裁这类高频项从管线执照里摘出来按帧报备。本节覆盖传统路线与 Vulkan 1.3 动态渲染两条路径,并交付两者的落地代码。 绘制命令为什么必须发生在 begin/endRenderPass 之间?因为光栅化输出的去处必须先约定清楚——写哪几张图、之前的写入何时可见、结束后数据流向哪里。这套约定就是渲染通道(RenderPass)。而动态状态回答另一个问题:哪些参数每帧都在变,不该被焊死在管线里。本节把这两个互为补集的机制讲透,它们合起来决定了「绘制时上下文」的全部内容。 一、渲染通道的三层声明 渲染通道由三层声明组成。
本节摘要:渲染通道规定「这一遍加工用哪几张附件、按什么次序、依赖关系如何」;帧缓冲把附件具体化;动态状态则把视口、剪裁这类高频项从管线执照里摘出来按帧报备。本节覆盖传统路线与 Vulkan 1.3 动态渲染两条路径,并交付两者的落地代码。
绘制命令为什么必须发生在 begin/endRenderPass 之间?因为光栅化输出的去处必须先约定清楚——写哪几张图、之前的写入何时可见、结束后数据流向哪里。这套约定就是渲染通道(RenderPass)。而动态状态回答另一个问题:哪些参数每帧都在变,不该被焊死在管线里。本节把这两个互为补集的机制讲透,它们合起来决定了「绘制时上下文」的全部内容。
渲染通道由三层声明组成。附件(attachment):列出用到的图像资源及其格式、样本数、加载与存储动作(LOAD/CLEAR/DONT_CARE,STORE/DONT_CARE)——loadOp 与 storeOp 是 tile 架构 GPU 的性能开关:clear 与 dont_care 允许驱动跳过显存回读与回写,移动端收益显著。子通道(subpass):一次加工阶段读写哪些附件(颜色、深度、输入附件),多子通道可以在 tile 内串接多道后处理而不落显存。依赖(subpass dependency):声明子通道之间的执行与内存依赖——粗粒度的屏障,告诉驱动「上一通道的颜色写入对下一通道的采样可见」。
帧缓冲(VkFramebuffer)则把声明落到具体资源:为每个附件视图指定一张图像视图(3.2 的取景框)。注意尺寸约束——帧缓冲的宽高必须覆盖所有附件,交换链重建(2.4 节)时帧缓冲同批重建的原因就在此。
// 附件声明:一张颜色附件,清屏进入、落盘退出 VkAttachmentDescription color = {}; color.format = swapchainFormat; color.samples = VK_SAMPLE_COUNT_1_BIT; color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; // 旧内容不要了 color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; // 收尾直接可上台 VkAttachmentReference cref = { 0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; VkSubpassDescription sub = {}; sub.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; sub.colorAttachmentCount = 1; sub.pColorAttachments = &cref; // 依赖:外部写入对本子通道颜色输出阶段的保护 VkSubpassDependency dep = {}; dep.srcSubpass = VK_SUBPASS_EXTERNAL; dep.dstSubpass = 0; dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask = 0; dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; VkRenderPassCreateInfo rci = {VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; rci.attachmentCount = 1; rci.pAttachments = &color; rci.subpassCount = 1; rci.pSubpasses = ⊂ rci.dependencyCount = 1; rci.pDependencies = &dep; vkCreateRenderPass(device, &rci, NULL, &renderPass);
录制绘制时通道被打开与闭合,清屏值在此时给出:
VkClearValue clear = { .color = { {0.1f, 0.1f, 0.12f, 1.0f} } }; VkRenderPassBeginInfo bi = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; bi.renderPass = renderPass; bi.framebuffer = framebuffer; // 绑定具体附件 bi.renderArea = {{0, 0}, width, height}; bi.clearValueCount = 1; bi.pClearValues = &clear; vkCmdBeginRenderPass(cmd, &bi, VK_SUBPASS_CONTENTS_INLINE); vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); vkCmdDraw(cmd, 3, 1, 0, 0); vkCmdEndRenderPass(cmd);
视口与剪裁框是每帧甚至每物体都可能变的参数,把它们焊进管线(4.2)意味着变体爆炸。动态状态的机制:创建管线时在 DynamicState 清单里声明「这几项我运行期给」,之后在命令缓冲里逐次设置:
// 录制时按需报备(管线已声明这两项为动态) VkViewport vp = { 0, (float)height, (float)width, -(float)height, 0.0f, 1.0f }; VkRect2D sc = { {0, 0}, {width, height} }; vkCmdSetViewport(cmd, 0, 1, &vp); vkCmdSetScissor(cmd, 0, 1, &sc);
注意 vp 里那行负高度——Vulkan 的 NDC 坐标 Y 轴向下,与多数教材相反;用负高度视口配合 Y 翻转是最常见的对齐手法,剪裁框则保持正向。可声明为动态的项在核心里已有十来种(视口、剪裁、线宽、偏置、深度边界等),Vulkan 1.2 起陆续把剔除、图元拓扑、混合常数等也纳入扩展动态状态,4.2 节变体塌缩的弹药大多来自这里。
传统渲染通道的声明成本不低——附件、子通道、依赖全要提前写死,与管线还要做兼容性匹配。Vulkan 1.3 把 VK_KHR_dynamic_rendering 收进核心:不建 RenderPass 与 Framebuffer,直接在 vkCmdBeginRendering 里给出每个附件的视图与布局,管线创建时渲染通道字段留空、以 VK_PIPELINE_RENDERING_CREATE_INFO 声明格式。
// 1.3 路线:免建 RenderPass 与 Framebuffer VkRenderingAttachmentInfo colorAtt = {VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; colorAtt.imageView = colorView; colorAtt.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; colorAtt.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; colorAtt.storeOp = VK_ATTACHMENT_STORE_OP_STORE; colorAtt.clearValue = clear; VkRenderingInfo ri = {VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; ri.colorAttachmentCount = 1; ri.pColorAttachments = &colorAtt; ri.layerCount = 1; ri.renderArea = {{0, 0}, width, height}; vkCmdBeginRendering(cmd, &ri); // 绑定动态渲染管线、设置动态视口、绘制…… vkCmdEndRendering(cmd);
两条路线的工序差异值得画在一起对照看——声明期与录制期各自要办的手续一目了然。

两条路线怎么选?经验分界:需要输入附件或 tile 内多子通道(移动端延迟渲染、subpass 合并后处理)用传统通道,那是它独有的能力;其余场景优先动态渲染——声明少、重建交换链时只需换视图、与 4.2 的变体治理天然契合。两条路线在同一个命令缓冲里不可嵌套混用,框架要么分层选用,要么整体站队。
背景:某工具有亮部提取、模糊、合成三段后处理,传统实现为一个渲染通道三子通道加两条内部依赖,声明代码二百余行;每加一段效果都要动通道声明与兼容性,维护苦不堪言。
操作:迁移到动态渲染后,三段各自独立 begin/endRendering,段间依赖用普通管线屏障声明(第 6 章的语义),附件视图在重建交换链时重新指向。原本的内部依赖改写为「颜色写入对采样可见」的标准屏障模板;输入附件环节(亮部提取读取 HDR 图)本就不依赖子通道机制,直接以采样方式完成。
结果:声明代码缩减到三分之一,新增后处理段不再触碰任何通道结构体;移动端 tile 收益的损失实测可忽略——因为该链本就没把子通道用出 tile 合并的价值。
解读:这个案例的价值判断值得记住:动态渲染不是性能特性而是维护性特性,迁移的收益在「改动半径」。只有当你的移动端渲染真正吃到 tile 内合并(如移动延迟渲染)时,传统通道才有不可替代性。
变式:混合架构也可行——主场景用动态渲染,移动端专用的 tile 合并链单独走传统通道,两者以屏障交接。前提是团队有能力同时维护两套声明,否则统一路线更稳。