Skip to main content

Chapter 93: Explicit Pipeline Management

显式管线管理要解决的问题,是把一帧里反复变化的材质、shader、资源布局、render target 格式和固定功能状态,提前整理成可查询、可复用、可预热的 pipeline state。读完本章后,读者应能定位一次 draw call 需要哪组 pipeline 条件,判断哪些条件会生成新的 Pipeline State Object(PSO),追踪 pipeline cache 命中失败的原因,并设计一套减少运行时卡顿的构建路径。

本章使用一个贯穿场景:一个延迟渲染器先把角色和场景写入 G-buffer,再执行 lighting pass。G-buffer pass 需要 vertex shader、fragment shader、顶点输入布局、descriptor set layout、颜色附件格式、深度格式、blend/depth/raster 状态共同成立;lighting pass 使用全屏三角形和不同的 render target 合约。显式 API 把这些条件提前组合进 pipeline 对象,命令录制阶段只绑定已经创建好的对象。这个设计把 draw 时校验开销换成创建期约束,也把开发者推到一个更早的阶段处理 shader 变体、cache key、预编译、fallback 和调试证据。

Vulkan specification 的 Pipelines 章节把 pipeline 描述为由 shader stages 和相关 fixed-function stages 的描述创建出的对象,并说明绑定 pipeline 后,动态状态需要由动态状态命令单独设置。这个判断适用于本章主线:PSO 覆盖 shader、draw call 附属状态和 render target 条件,共同形成 draw call 能否进入管线的完整契约。Metal 中的 MTLRenderPipelineDescriptorMTLRenderPipelineState 也体现同一类工程分工;应用先描述 vertex function、fragment function、color attachment、depth/stencil 相关条件,再创建可绑定的 pipeline state。

本章的核心结论是:显式 pipeline 管理的难点集中在维护“哪些状态属于 pipeline key、哪些状态适合动态设置、哪些 pipeline 必须在加载阶段预热、哪些失败可以回退”。如果这些边界混乱,画面问题会表现为 shader 输入错位、attachment format 不匹配、depth/blend 状态错误;性能问题会表现为切场景时卡顿、运行时同步等待、pipeline cache 命中率低和后台编译无法覆盖热路径。

下面这条路径描述本章贯穿的 G-buffer draw 如何从材质请求进入 GPU 命令流。图中只保留 pipeline 管理相关节点;descriptor 分配、资源同步和内存布局会在后续章节展开。

这条路径给出本章的可复用判断顺序:先确定 draw call 的输出合约,再确定 shader 与资源布局,再判断固定功能状态,再决定动态状态范围,最后用 cache、日志和 GPU capture 复查命中情况。显式 pipeline 管理的工程质量,取决于这条路径能否稳定、可观测、可回退。

93.1 Pipeline State Object Precombines Shader Layout and Render Target Contract

Pipeline State Object 是把 shader、资源布局、固定功能状态和 render target 合约预组合后的 GPU 执行对象。这里的“合约”指 draw call 进入管线前必须同时满足的一组条件:shader 输入输出接口匹配,descriptor set layout 或 argument layout 与 shader 访问一致,vertex input layout 能提供 vertex shader 读取的属性,color/depth attachment 的格式和 sample count 与 pipeline 创建描述一致,blend、depth、raster 状态与目标 pass 的视觉结果一致。

在贯穿场景中,G-buffer pass 至少输出 albedo、normal、material 和 depth。角色材质的 fragment shader 会把法线写入一个高精度 attachment,把 base color 写入 sRGB 或线性格式约定下的 color attachment,把 roughness/metallic 写入打包通道。只要 attachment format、sample count、fragment output location 或 blend 状态发生会改变硬件配置的变化,原来的 PSO 就不再覆盖这次 draw。此时强行复用旧 pipeline,常见结果是 validation 报错、输出 attachment 错位、深度测试行为偏离预期,或者在某些后端被迫重新创建底层对象。

Vulkan 的 pipeline 创建信息把这些条件显式列出。VkGraphicsPipelineCreateInfo 会连接 shader stage、vertex input、input assembly、viewport、rasterization、multisample、depth/stencil、color blend、dynamic state、layout、render pass 或 dynamic rendering 信息。Metal 的 MTLRenderPipelineDescriptor 也要求设置 vertex function、fragment function、color attachment pixel format 等字段。两者的 API 形态不同,工程目标一致:把 draw 时的大量状态校验移到 pipeline 创建期。

G-buffer pipeline key 可以按“输出合约优先”的方式组织。先写入 render target 相关条件,再写入 shader 与资源布局,再写入固定功能状态。这个顺序来自可见故障的影响范围:attachment format 错误会直接破坏 framebuffer 写入;shader layout 错误会破坏资源读取;raster/depth/blend 错误会改变可见性和混合结果;viewport 这类状态在支持动态设置时可以从 key 中移出。

下面是简化 C++ 伪代码,目标是展示 key 中应包含的对象类别。真实工程需要为每个字段定义稳定序列化方式,直接对结构体内存做哈希会引入 padding、指针地址和平台字节序风险。

struct PipelineKey {
ShaderHash vertexShader;
ShaderHash fragmentShader;
LayoutHash pipelineLayout;
VertexInputHash vertexInput;

Format colorFormats[4];
Format depthFormat;
uint32_t colorAttachmentCount;
uint32_t sampleCount;

RasterState raster;
DepthState depth;
BlendState blend;

DynamicStateMask dynamicStates;
RenderPassClass passClass;
};

PipelineHandle requestPipeline(const PipelineKey& key) {
if (auto cached = pipelineCache.find(key)) {
return *cached;
}
PipelineHandle created = buildPipelineFromKey(key);
pipelineCache.insert(key, created);
return created;
}

这段伪代码说明一个关键边界:shader hash 只覆盖 shader 内容与编译宏,pipeline layout hash 覆盖 descriptor set layout、push constant range、argument binding 约定,render target 字段覆盖输出格式。把它们拆成独立字段,可以在日志和 capture 中定位缺失来源。例如同一材质在 forward pass 和 G-buffer pass 使用相同 fragment shader 名称,但输出附件数量不同,key 必须区分这两个 pipeline。

PSO 预组合带来的性能收益来自创建期和 draw 期的分工。创建期可以让驱动根据 shader IO、固定功能状态和 attachment 合约做优化;draw 期只需要绑定对象、设置动态状态和提交 draw。Vulkan 文档中提到,整体 pipeline 链接使实现可以基于 shader 输入输出优化,并减少 draw time state validation。工程上的含义是:越多关键状态在 draw 前稳定,运行时提交路径越短;越多状态在帧中临时变化,系统越依赖动态状态和缓存命中。

对当前章节而言,PSO 的检查顺序可以固定为五步。第一步检查输出合约:color/depth format、sample count、render pass 或 dynamic rendering 信息。第二步检查 shader 接口:入口函数、stage、specialization 常量、fragment output location。第三步检查资源布局:descriptor set layout、push constant、argument buffer 或 root signature 对应关系。第四步检查固定功能状态:depth test、cull mode、front face、blend、primitive topology。第五步检查动态状态:viewport、scissor、blend constants、stencil reference 等是否在录制命令时设置。这五步覆盖了 G-buffer draw 的主要失败点。

93.2 Vulkan Pipeline Cache 与预编译策略

Vulkan Pipeline Cache 的作用是复用 pipeline 构建过程中产生的实现内部表示。它提供复用机会,创建成本仍取决于驱动、shader、状态组合和 cache 兼容性;它回答的问题是:当应用再次创建相似或相同 pipeline 时,驱动是否能复用已有编译结果。Vulkan Guide 的 Pipeline Cache说明 pipeline creation 可能包含 shader 编译,cache 可以把 pipeline state 保存到文件并跨应用运行复用;Vulkan Samples 的 Pipeline Management进一步把 cache、创建成本和资源预热放到性能样例中解释。

在贯穿场景中,第一次进入角色展示场景时,系统可能需要创建角色皮肤材质、金属武器材质、透明头发材质和 terrain 材质的 G-buffer pipeline。每个 pipeline 由 shader variant、layout、G-buffer format 和状态组合确定。如果这些 pipeline 在第一帧渲染前才创建,CPU 帧时间会出现尖峰。更稳定的策略是在资源加载或关卡切换阶段收集将要使用的材质组合,提前请求 pipeline,等待创建完成后再进入可交互帧。

Pipeline Cache 分两层理解。第一层是 API 层的 VkPipelineCache:它由设备创建,可以传入 vkCreateGraphicsPipelinesvkCreateComputePipelines 或相关 pipeline 创建命令。Vulkan specification 的 pipeline cache 部分说明,cache 对象允许 pipeline construction 的结果在多个 pipeline 之间和应用多次运行之间复用;跨运行复用需要取出 cache 数据并在下次运行时作为初始数据。第二层是引擎层的 resource cache:它根据 pipeline key 返回已经创建的 VkPipeline,并记录哪些 key 需要预热、哪些 key 构建失败、哪些 key 只能使用降级 shader。

这两层 cache 的边界必须清晰。VkPipelineCache 的二进制数据由实现管理,适合交给驱动复用内部编译结果;引擎自己的 key-value cache 由应用管理,适合保证同一个 draw 请求不会重复创建 VkPipeline。如果应用只保存驱动 cache 数据,却没有保存 pipeline key 和预热列表,下一次运行仍然需要等游戏逻辑触发同样材质后才知道要创建哪组 pipeline。此时驱动 cache 可能降低创建成本,但无法消除首次触发时机带来的卡顿。

预编译策略应从可观察场景出发。对 G-buffer pass,可以在加载角色 prefab、材质实例和场景 lightmap 后,生成一份本关卡 pipeline manifest。manifest 不需要包含所有理论组合,只包含实际会出现的 shader variant、render target format、MSAA 状态、skinning 开关和 alpha test 开关。这样做的依据是 pipeline 组合数量通常来自变体笛卡尔积,实际场景只使用其中一小部分。

下面是一个简化的预热流程。它不依赖某个具体引擎,表达的是对象关系。

这条流程的关键在于提前暴露热路径,并用实际场景约束预热集合。角色选择界面只需要角色、UI、后处理的 pipeline;开放世界场景切换需要 terrain、foliage、建筑、角色和天空 pipeline;编辑器热重载需要保留正在观察 viewport 的 pipeline 和 debug pipeline。不同场景的预热集合不同,manifest 应按 scene、quality level、platform profile 和 render path 组织。

磁盘 cache 还需要兼容性检查。Vulkan pipeline cache header 包含 vendor ID、device ID 和 pipelineCacheUUID 等字段,规范也说明无效 pipeline cache 数据会被忽略。工程上应把这些字段、驱动版本、应用 shader package 版本和 pipeline key schema 版本写入应用自己的 cache 元数据。任何一个字段变化,都应让系统重新生成或分区保存 cache。这样处理可以减少旧 cache 与新 shader 包混用造成的命中率异常。

预编译的失败回退也要进入设计。创建 pipeline 可能因为 shader 编译错误、layout 不兼容、内存不足、设备功能缺失或扩展不可用失败。G-buffer pass 的常见回退顺序是:先切到同材质的低质量 shader variant,再切到 debug magenta 或 flat material,再禁用可选效果并保留深度写入。这个顺序保证后续 pass 仍能获得基本 G-buffer 或至少获得可定位的错误画面。

93.3 高效构建与管理 Pipeline State

高效管理 Pipeline State 的核心是把 pipeline 生命周期拆成可审计阶段:声明、归一化、哈希、创建、绑定、热更新、销毁。声明阶段来自材质系统和 pass 系统;归一化阶段把不同上层对象折叠成稳定字段;哈希阶段生成 key;创建阶段调用底层 API;绑定阶段出现在 command buffer 录制;热更新阶段响应 shader 或 render path 变化;销毁阶段跟随 device、swapchain、render target profile 或 shader package 生命周期。

在 G-buffer 场景中,材质系统可能有 SKINNINGALPHA_TESTNORMAL_MAPCLEARCOAT 等开关。最直接的做法是把所有开关组合成 shader variant。这个做法在小 demo 中可行,在真实项目中会快速扩大 pipeline 数量。更稳定的做法是先区分“影响 shader 编译的条件”和“影响 uniform 或 texture 数据的条件”。例如 SKINNING 会改变 vertex shader 输入和骨骼矩阵读取,适合作为变体;roughness 数值只改变材质 buffer 内容,应留在材质 buffer;是否写 velocity buffer 会改变 render target 合约和 fragment output,也应进入 key。

Pipeline layout 是另一条常见边界。Vulkan 的 VkPipelineLayout 连接 descriptor set layout 和 push constant range,shader 通过 set/binding 读取资源。两个 shader variant 即使源码相近,只要 descriptor set layout 或 push constant range 不一致,就需要不同的 layout。Metal 的 argument buffer、buffer index 和 function argument 也有类似约束。管理系统应把 layout 视为独立资产,按 frame、pass、material、object 等频率组织,减少材质变体对 layout 的扰动。

一个可维护的 pipeline key 需要满足三个条件。第一,字段来自 API 会校验或会影响底层编译的状态。第二,字段顺序稳定,序列化结果跨进程一致。第三,日志能把 key 反解成人可读描述。只有 hash 值没有诊断价值;g_buffer/skinned/opaque/msaa4/rgba16f+rgba8+d24s8 这类标签可以在日志、RenderDoc、Xcode GPU capture 或引擎 overlay 中直接定位问题。

下面的简化表格给出 pipeline 管理中常见字段的归属。表格的目的,是帮助读者判断某个变化应该进入 pipeline key、descriptor 更新还是动态状态命令。

变化对象推荐归属判断依据
vertex shader / fragment shader 字节码Pipeline key改变编译输入和 stage 接口。
descriptor set layout / push constant rangePipeline key改变 shader 资源访问契约。
color attachment format / depth format / sample countPipeline key改变 fragment output 和 framebuffer 合约。
depth test enable / blend enable / cull modePipeline key 或扩展动态状态默认属于固定功能状态;支持动态状态时可移出部分字段。
viewport / scissor动态状态一帧内高频变化,通常适合命令录制期设置。
material texture / uniform buffer offsetDescriptor 或 bind group 更新改变资源实例,不改变 pipeline 编译输入。
roughness / color / tiling 参数Uniform 或 push constant改变 shader 数据,不改变 pipeline 对象。
render path 切换到 G-buffer 或 forwardPipeline key改变 pass class、输出格式和 shader 输出语义。

构建策略要区分同步请求和异步请求。同步请求适合启动阶段、加载阶段、工具命令和测试;异步请求适合编辑器热重载、运行时材质切换和后台预热。对实时游戏或交互式工具,render thread 发现 pipeline 缺失时,应返回 fallback pipeline,记录缺失 key,把创建任务提交给后台队列,创建完成后在安全帧边界替换。

热更新需要保留旧 pipeline 到安全点。shader 修改后,新 pipeline 创建成功前,旧 pipeline 仍应服务当前 viewport;新 pipeline 创建失败时,旧 pipeline 继续使用并显示编译错误。这样可以把开发体验从“画面中断”改成“错误可见且场景可继续观察”。在 Vulkan 中,销毁旧 VkPipeline 前需要保证没有未完成的 command buffer 仍引用它;通常用 frame-in-flight 资源回收队列或 fence 追踪延迟销毁。在 Metal 中,也应让旧 MTLRenderPipelineState 跟随 command buffer 生命周期自然释放或引用计数释放。

对大量 pipeline 的工程,还可以引入 pipeline library 或分段编译。Vulkan Samples 的 Graphics pipeline libraries说明 VK_EXT_graphics_pipeline_library 可以把 monolithic graphics pipeline 拆成 vertex input interface、pre-rasterization shaders、fragment shader、fragment output interface 等部分,并复用公共部分。它适合 runtime pipeline 创建较多的应用,但需要处理扩展支持、独立 descriptor set 约束和链接期优化成本。工程判断是:当 shader fragment 变化频繁、vertex input 和输出接口高度复用时,pipeline library 更有价值;当 pipeline 组合数量很小,普通预热和 cache 已经覆盖热路径时,额外复杂度收益有限。

93.4 动态状态与灵活配置技巧

动态状态(dynamic state)把部分 pipeline 状态从创建期移到命令录制期。它解决的工程问题是减少 pipeline 组合数量,同时保留一帧内高频变化的灵活性。Vulkan Guide 的 Pipeline Dynamic State用 viewport 示例说明,当 pipeline 使用动态状态时,部分信息可在创建时省略,并在 command buffer 录制时通过 vkCmdSetViewport 等命令设置。

在 G-buffer 场景中,viewport 和 scissor 常随窗口尺寸、split view、shadow atlas 或 editor viewport 变化。如果 viewport 被写入 pipeline key,那么每次窗口 resize 都会生成新 pipeline;把 viewport 设为动态状态后,同一个 pipeline 可以服务多个 viewport 区域。这个选择的输出合约没有变化:fragment shader 仍写同样的 attachment,depth/blend/raster 主状态仍一致,只是屏幕映射范围变化。

动态状态的选择应按频率和风险判断。高频且对底层编译优化影响小的状态,适合动态设置;低频且会明显影响 shader IO、attachment、深度/混合硬件路径的状态,适合保留在 pipeline key。某些扩展提供更多动态状态,例如 cull mode、front face、primitive topology、vertex input、color write enable 等。扩展能降低组合数量,但也会把更多责任推到命令录制顺序上:每次绑定需要动态状态的 pipeline 前,相关 vkCmdSet* 命令必须已经设置到有效值。

动态状态的生命周期是常见 bug 来源。命令 buffer 中的动态状态是一组需要显式写入的录制期值。Vulkan Guide 给出若干 valid/invalid 示例,核心判断是:当绑定的 pipeline 声明某个状态是动态的,draw 前必须有对应动态状态命令提供有效值;绑定静态 pipeline 后再切换到动态 pipeline,静态值不会自动转换成动态值。工程上应在 command encoder 层统一写入动态状态,减少单个 draw path 遗漏设置。

下面的简化命令顺序展示一个稳定做法。它属于局部伪代码,只展示动态状态与 pipeline 绑定的相对位置。

void recordGBufferDraw(CommandBuffer cmd, const DrawItem& item) {
PipelineHandle pipeline = pipelineSystem.request(item.pipelineKey);

vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline.vkPipeline);
vkCmdSetViewport(cmd, 0, 1, &item.viewport);
vkCmdSetScissor(cmd, 0, 1, &item.scissor);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipeline.vkLayout, 0,
item.descriptorSetCount,
item.descriptorSets,
item.dynamicOffsetCount,
item.dynamicOffsets);
vkCmdDrawIndexed(cmd, item.indexCount, 1, item.firstIndex, item.vertexOffset, 0);
}

这段伪代码的要点是,命令录制层把动态状态写在 draw 前的固定位置。这样即使上层材质路径不同,viewport/scissor 设置也不会分散到各个材质分支中。对 editor、多 viewport、VR、多 render target atlas 这类场景,这种集中写入能明显降低遗漏风险。

动态状态也有成本边界。某些实现可能对特定动态状态有性能开销,或者失去部分创建期优化机会。Vulkan Guide 也指出,不同实现对动态状态的性能影响可能不同。工程上应把“可动态”继续放入性能和复杂度评估。判断顺序应是:先统计 pipeline 组合膨胀来自哪些字段,再确认这些字段是否支持动态状态,再通过 GPU capture 或帧时间记录比较创建数量、绑定数量、CPU 录制成本和 GPU 执行成本。

Metal 的状态拆分方式和 Vulkan 不完全相同。Metal render pipeline state 固定了 shader function 和 attachment pixel format 等核心条件,而 viewport、scissor、depth stencil state、cull mode 等通过 render command encoder 设置。对跨 API 引擎,抽象层应按引擎状态归属建模,再分别映射到各后端能力。更稳的做法是定义引擎自己的状态归属:PipelineImmutableStateEncoderDynamicStateResourceBindingState。每个后端再映射到自己的 API 对象和命令。

93.5 错误处理与调试策略

Pipeline 管理错误应按“创建失败、绑定错误、输出错误、性能错误”四类观察。创建失败发生在 vkCreateGraphicsPipelines、Metal makeRenderPipelineState 或后台编译任务中;绑定错误发生在 command buffer 录制和 draw 前状态缺失;输出错误表现为 G-buffer 通道异常、深度全白、法线方向错、透明物体写入错误;性能错误表现为切场景卡顿、runtime pipeline miss、后台编译占用 CPU 或 pipeline cache 文件无效。

创建失败的第一证据是 API 返回值、编译日志和 validation message。Vulkan 创建多个 pipeline 时,规范说明可能只有部分 pipeline 创建成功,失败项会返回空句柄,应用需要清理成功创建的对象并记录失败原因。工程系统应把失败记录绑定到可读 pipeline key,并让日志同时包含 pass、shader variant、layout、color/depth format、sample count、dynamic state mask 和 fallback 结果。

绑定错误的第一证据来自 validation layer 或 GPU capture。Vulkan 的 Debugging 章节说明 debug utilities 可以给 Vulkan object 设置名称、给 command buffer 或 queue 添加标签、捕获 debug message。对 pipeline 系统,应给 VkPipelineVkPipelineLayoutVkPipelineCache、shader module 或 shader stage 设置可读名称。这样在 RenderDoc、Nsight Graphics 或 validation 输出中看到的应是 g_buffer/skinned/opaque/rgba16f 这类可读名称,十六进制句柄只作为底层补充信息。

输出错误需要回到贯穿的 G-buffer frame。法线通道全黑,先看 fragment shader 是否执行,再看 color attachment format 和 write mask,再看 blend 是否关闭,再看 render target load/store 和 layout。深度全白,先看 depth test/write 状态,再看 depth attachment format,再看 projection depth range,再看 early-z 和 clear value。材质颜色错位,先看 descriptor set layout 与绑定资源,再看 shader variant 宏,再看 vertex input layout。这个顺序把视觉结果映射回 pipeline、资源和 shader 输入三层。

性能错误需要记录 pipeline miss。每次 requestPipeline(key) 发现 key 缺失时,系统应记录当前 frame、pass、material、scene、是否在 render thread、创建耗时、是否使用磁盘 cache、是否 fallback。只有这些数据齐全,才能判断卡顿来自“预热列表缺失”“cache 文件失效”“key 设计过细”“shader variant 过多”还是“后台创建没有覆盖当前热路径”。

VK_KHR_pipeline_executable_properties 这类能力可以提供 pipeline executable 的统计和内部表示查询,但它属于设备功能和实现相关能力,适合作为增强诊断入口。更通用的观察入口是:API validation message、pipeline 创建耗时统计、pipeline key miss 日志、GPU capture 中的 pipeline state、shader 编译日志和磁盘 cache 元数据。工具只能回答某个层级的问题;系统需要把这些证据串回同一条 draw path。

下面是一个可复用排查顺序,用于定位 G-buffer pipeline 相关问题。

症状先查对象再查对象结论边界
切场景首帧卡顿pipeline miss 日志预热 manifest 与 cache 元数据卡顿来自创建时机或 cache 失效,再查 shader 编译耗时。
validation 报 pipeline/layout 不兼容pipeline layout hashdescriptor set layout 和 push constant range资源绑定契约与 shader 访问不一致。
G-buffer 某通道为空fragment output locationattachment format、write mask、blend state输出合约或 shader 输出路径不匹配。
resize 后 pipeline 数量暴涨key 中 viewport/scissor 字段dynamic state 设置路径可动态设置的屏幕状态进入了 key。
热重载后画面中断新 pipeline 创建日志旧 pipeline 延迟销毁和 fallback热更新缺少成功前保留旧对象的路径。

错误处理的最终目标,是让 pipeline 系统在失败时仍能给出可观察结果。调试材质可以写明显颜色,fallback pipeline 可以保留深度写入,日志可以把 hash 反解成字段,capture 可以显示对象名称。这样开发者看到的结论会从“某次 draw 失败”推进到“某个 pass 的某个 pipeline 合约没有成立”。

最小自检任务

给定一个延迟渲染器,G-buffer pass 使用两个材质:CharacterOpaqueCharacterAlphaTest。两者使用相同的 vertex input、相同的 G-buffer attachment format、相同的 descriptor set layout;CharacterAlphaTest 的 fragment shader 多一个 ALPHA_TEST 编译宏,并且会使用 discard。窗口 resize 后,viewport 和 scissor 改变。请设计这两个材质的 pipeline key,并说明哪些字段进入 key,哪些字段通过动态状态或资源绑定更新。随后说明系统如何在关卡加载阶段预热这两个 pipeline,并给出 pipeline 创建失败时的回退路径。

答案要点

两个材质至少需要两个 pipeline key,因为 ALPHA_TEST 改变 fragment shader 编译输入和片元执行行为。key 中应包含 vertex shader hash、fragment shader hash、pipeline layout hash、vertex input hash、G-buffer color/depth format、sample count、raster/depth/blend 状态、pass class 和 dynamic state mask。由于题目说明 viewport 与 scissor 由窗口 resize 改变,它们应放入动态状态设置路径,不进入 key。材质纹理、uniform buffer、roughness、base color 等资源实例通过 descriptor 或 buffer 更新,不生成新 pipeline。

预热顺序是:加载关卡资产后收集两个材质和 G-buffer pass,生成两个 PipelineKey,读取与当前设备、驱动、shader package 和 key schema 匹配的磁盘 pipeline cache,创建 VkPipelineCache 或后端等价对象,批量创建两个 pipeline,把结果写入引擎 pipeline cache。进入第一帧前,render thread 只绑定已经创建好的 pipeline,并在 draw 前设置 viewport/scissor 动态状态。

失败回退应先记录完整 key 和 API 错误,再尝试低质量或通用 opaque G-buffer shader。CharacterAlphaTest 创建失败时,可以先回退到 alpha test debug shader;如果仍失败,使用 flat magenta G-buffer pipeline,并保留 depth write 以便后续 pass 仍能暴露遮挡关系。旧 pipeline 热重载场景下应保留到新 pipeline 创建成功和 GPU 不再引用旧对象之后再释放。

本章知识点总结

  • PSO 契约:Pipeline State Object 预组合 shader、layout、固定功能状态和 render target 合约,draw 阶段绑定的是这份可执行契约。
  • 输出优先:pipeline key 应先覆盖 attachment format、depth format、sample count 和 pass class,因为这些字段直接决定 framebuffer 写入路径。
  • 接口匹配:shader stage、vertex input、fragment output location、descriptor set layout 和 push constant range 必须共同匹配同一个 pipeline layout。
  • 缓存分层VkPipelineCache 复用驱动内部编译结果,引擎 pipeline cache 负责从可读 key 返回已创建 pipeline。
  • 预热清单:关卡或场景加载阶段应从实际材质、pass 和质量档位生成 pipeline manifest,提前覆盖热路径。
  • 兼容元数据:磁盘 cache 应记录设备、驱动、shader package 和 key schema 信息,版本变化时分区或重建。
  • 变体边界:影响 shader 编译、资源布局或输出合约的条件进入 pipeline key,普通材质参数留在 buffer、texture 或 push constant 中。
  • 动态状态:viewport、scissor 等高频屏幕状态适合命令录制期设置,可以减少 pipeline 组合数量。
  • 生命周期:热重载创建新 pipeline 成功前应保留旧 pipeline,销毁旧对象要等待 GPU 不再引用。
  • 分段编译:graphics pipeline library 适合公共 pipeline 部分高度复用且运行时创建频繁的场景。
  • 调试命名:pipeline、layout、cache 和 shader stage 应设置可读名称,让 validation、capture 和日志能回到具体 key。
  • 失败回退:pipeline 创建失败时应记录 key、错误和 fallback,优先保留可见 debug 结果和必要深度写入。
  • 排查顺序:先查输出合约,再查 shader 接口,再查资源布局,再查固定功能状态,最后查动态状态设置和 cache 命中。