4.2 图形管线状态对象 本节摘要:图形管线是把着色器阶段与全部固定功能状态固化成的不可变对象——创建贵、绑定便宜。本节逐段拆解 VkGraphicsPipelineCreateInfo 的装配结构,讲清每个子状态的职责,并交付管线缓存与衍生管线两件摊薄创建成本的工具。 管线状态对象(业内口语 PSO)可以定义成:一次绘制的全部「规矩」的冻结快照——怎么读顶点、怎么拼三角形、怎么光栅化、怎么测深度、怎么混色,全部在创建时声明并经驱动优化,运行期不可再改。这个定义里有双重性格:规矩的集合庞大(创建信息里十来个子结构体),但冻结后每次绑定只换一个句柄。理解这重取舍,是理解本章乃至整个 Vulkan 性能模型的支点。
本节摘要:图形管线是把着色器阶段与全部固定功能状态固化成的不可变对象——创建贵、绑定便宜。本节逐段拆解 VkGraphicsPipelineCreateInfo 的装配结构,讲清每个子状态的职责,并交付管线缓存与衍生管线两件摊薄创建成本的工具。
管线状态对象(业内口语 PSO)可以定义成:一次绘制的全部「规矩」的冻结快照——怎么读顶点、怎么拼三角形、怎么光栅化、怎么测深度、怎么混色,全部在创建时声明并经驱动优化,运行期不可再改。这个定义里有双重性格:规矩的集合庞大(创建信息里十来个子结构体),但冻结后每次绑定只换一个句柄。理解这重取舍,是理解本章乃至整个 Vulkan 性能模型的支点。
VkGraphicsPipelineCreateInfo 本身字段不多,重头在它引用的十来个子状态。下面这张装配图先立骨架,代码随后逐段落实。

每个子状态的要点压缩成一张速查表:
| 子状态 | 决定什么 | 高频坑 |
|---|---|---|
| VertexInput | 每个顶点属性的位置、格式、跨距、偏移 | 与顶点着色器 location 漂移 |
| InputAssembly | 图元拓扑与邻接方式 | 改画线忘建新管线 |
| Tessellation | 补丁控制点数 | 不用细分则整段留空即可 |
| ViewportState | 视口与剪裁框数量及初值 | 设为动态状态后初值被忽略 |
| Rasterization | 多边形模式、剔除面、正面方向、深度偏移 | 负高度视口的翻转规则 |
| Multisample | 采样数、覆盖掩码、采样着色率 | 与附件样本数不一致直接退件 |
| DepthStencil | 比较算符、写开关、模板正反面 | 深度测试关了写还开着 |
| ColorBlend | 逐附件混合方程与写掩码 | attachmentCount 与通道不符 |
| DynamicState | 从冻结清单里豁免的项 | 声明了动态却没逐帧设置 |
把装配台落到代码。工程里通常把各子状态封装成可配置结构,这里给出教学骨架(省略字段沿用零值即禁用的默认语义):
VkPipelineVertexInputStateCreateInfo vin = {VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; vin.vertexBindingDescriptionCount = 1; vin.pVertexBindingDescriptions = &bindingDesc; // 跨距 32 字节之类 vin.vertexAttributeDescriptionCount = 3; // 位置 uv 法线 vin.pVertexAttributeDescriptions = attrDescs; VkPipelineInputAssemblyStateCreateInfo ia = {VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; VkPipelineRasterizationStateCreateInfo rs = {VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; rs.polygonMode = VK_POLYGON_MODE_FILL; rs.cullMode = VK_CULL_MODE_BACK_BIT; rs.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; rs.lineWidth = 1.0f; VkPipelineDepthStencilStateCreateInfo ds = {VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; ds.depthTestEnable = VK_TRUE; ds.depthWriteEnable = VK_TRUE; ds.depthCompareOp = VK_COMPARE_OP_LESS; VkPipelineColorBlendAttachmentState blendAtt = {}; blendAtt.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; VkPipelineColorBlendStateCreateInfo cb = {VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; cb.attachmentCount = 1; cb.pAttachments = &blendAtt; VkDynamicState dynStates[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR }; VkPipelineDynamicStateCreateInfo dyn = {VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; dyn.dynamicStateCount = 2; dyn.pDynamicStates = dynStates; VkGraphicsPipelineCreateInfo pci = {VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; pci.stageCount = 2; pci.pStages = stages; // 4.1 的顶点与片段阶段 pci.pVertexInputState = &vin; pci.pInputAssemblyState = &ia; pci.pRasterizationState = &rs; pci.pDepthStencilState = &ds; pci.pColorBlendState = &cb; pci.pDynamicState = &dyn; pci.layout = pipelineLayout; pci.renderPass = renderPass; // 4.3 展开 pci.subpass = 0; VkPipeline pipeline; vkCreateGraphicsPipelines(device, pipelineCache, 1, &pci, NULL, &pipeline);
注意:视口状态结构体在设了动态状态后可以不填数据(数量声明仍在)。多数字段零值即「关闭」,这是 Vulkan 少有的「默认」——但别依赖它,显式填全才是可读的装配。
管线创建的驱动开销可观(解析字节码、生成微码、分配硬件状态块),单次几毫秒很常见。两件工具负责摊薄。管线缓存(VkPipelineCache):创建时传入,驱动把生成结果存档;更实用的是磁盘持久化——vkGetPipelineCacheData 导出字节、下次启动回灌,冷启动装配提速明显。缓存头里带驱动与设备指纹,跨驱动复用会被拒绝,导出数据要做版本管理。衍生管线(basePipelineHandle):声明新管线与某条已建管线高度相似,驱动复用其已验证的部分做增量构建;代价是基底必须已存在,适合运行期衍生少量变体。
背景:某引擎支持五种混合模式、三种剔除组合、两档深度写、两种拓扑,组合即六十种管线,冷启动装配耗时逼近一分钟,显存里管线元数据可观。
操作:治理分三刀。第一刀砍需求:逐一核对矩阵,发现一半组合业务上不存在,砍掉;第二刀转动态:拓扑、剔除、深度写改走 DynamicState 与扩展动态状态(4.3 节展开),六个维度的组合塌缩成一条管线;第三刀上缓存:构建期预装配常用变体并持久化缓存进安装包,运行期命中缓存直接取。另外用管线创建反馈扩展读出各条管线的构建耗时,把剩余的大户点名优化。
结果:管线数从六十降到七,冷启动装配从分钟级回到秒级。变体矩阵从此作为「生成式清单」维护——代码里只写维度,管线清单由构建脚本展开并入库。
解读:变体失控的根源是「把可枚举的维度组合当成了状态机」。治理思路永远是三步:删伪需求、转动态状态、剩下的真变体交给缓存。这三步的顺序别颠倒——先缓存后删需求,等于缓存一堆没人用的管线。
变式:用 vkCreateGraphicsPipelines 批量接口一次申报全部变体,驱动有机会合并重复子表达式,比逐条创建更快也更利于缓存命中——装配清单式管理里这是标配写法。