3.5 渲染状态与PSO——从散装开关到整体出厂


3.5 渲染状态与 PSO——从散装开关到整体出厂

本节摘要:渲染状态决定管线"怎么画":混合怎么做、深度怎么裁、三角形怎么光栅化。D3D 11 用几十个散装 Set 调用拼状态,D3D 12 把它们一次性打包进管线状态对象(PSO)。本节讲清三大状态块的职责与 PSO 化的性能账。

一个闪烁的画面引出的话题

测试报告:角色站在条纹地面上,接缝处不停闪烁。排查几小时后定位:深度缓冲精度不足导致两层面片深度值轮流胜出(Z-fighting),而修复的关键旋钮藏在深度模板状态里——调整深度比较规则与多边形偏移。这个案例说明状态管理的两个特点:旋钮多、影响隐蔽。渲染状态不改变"画什么",只改变"怎么画",而正是这些"怎么"决定画面正确性与性能上限。本节把状态世界分成三大块讲清,再看 D3D 12 如何把它们装进一只盒子。

三大状态块

光栅化状态管辖几何到像素的转换规则:填充模式(实心或线框)、剔除模式(背面剔除省一半像素着色器工作量)、多边形偏移(治 Z-fighting 的常用药)。它作用于 3.1 节的光栅化车间,是性能与正确性的交叉地带——背面剔除开与不开,像素着色器负载可能相差近一倍。

深度模板状态是三维正确性的守门员:深度比较规则(小于等于才通过是常见选择)、深度写入开关(画天空盒时关写入是标配技巧)、模板运算(平面阴影、镜面效果的经典实现手段)。上面闪烁案例的偏移修复,就落在这一块。

混合状态处理"新片元与已有像素怎么合":不透明物体关闭混合直写;半透明物体按 alpha 混合;叠加发光用加法混合。混合顺序敏感是它的天然属性——这直接催生了渲染队列按"不透明在前、半透明按深度排序在后"的行业惯例,第六章优化与第七章的排序无关话题都绕不开它。

图1 三大状态块与管线的对应

图1 三大状态块与管线的对应

PSO:状态管理的代际跃迁

D3D 11 时代的写法是散装旋钮:OMSetBlendStateOMSetDepthStencilStateRSSetState 各设各的,驱动要在每次绘制前把当前状态组合翻译成硬件可执行的形态——翻译本身有成本,且驱动无法预知你会怎么组合。D3D 12 改为出厂整机:把着色器、三大状态、根签名、输出格式一次性写进 PSO,创建时整体校验并让驱动预编译内部表示;运行时 SetPipelineObject 一次到位,没有逐项重组。

代价与收益都来自同一个性质。代价:PSO 创建昂贵(驱动要做大量准备工作),且不可中途修改——想换混合模式就得另建一个 PSO。收益:运行时绑定极快、行为完全可预测、驱动不再做投机猜测。工程上的对应对策是PSO 缓存:启动或加载期把会用到的 PSO 组合预先建好、按需取用,游戏项目还会把缓存序列化到磁盘以加速二次启动。

D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc{}; psoDesc.InputLayout = { layout, layoutCount }; psoDesc.pRootSignature = rootSig.Get(); psoDesc.VS = { vsBytecode->GetBufferPointer(), vsBytecode->GetBufferSize() }; psoDesc.PS = { psBytecode->GetBufferPointer(), psBytecode->GetBufferSize() }; psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT); // 混合:默认不透明 psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT); // 深度:默认小于通过 psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT); // 光栅化:背面剔除 psoDesc.SampleMask = UINT_MAX; psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE; psoDesc.NumRenderTargets = 1; psoDesc.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM; // 必须与交换链一致 psoDesc.DSVFormat = DXGI_FORMAT_D32_FLOAT; device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(pso.GetAddressOf()));

注意 RTVFormats 必须与实际渲染目标格式一致——格式不符创建会失败或行为诡异,这是 PSO "整机校验"性质的直接体现:它连输出长什么样都要提前知道。

实战守则与案例收尾

三条守则值得刻进习惯。预建缓存:把 PSO 创建集中在加载期,运行时只查表取用;带 PSO 缓存序列化的项目,二次启动可省下可观的初始化时间。组合收敛:状态组合要有纪律地规划(透明一个、不透明一个、线框调试一个……),放任各处临时拼组合会让 PSO 数量失控。格式先行:渲染目标格式定了再建 PSO,中途改格式等于全部重建。

回到开头闪烁案例的完整解法:调多边形偏移只是应急,正解是给重叠面片做深度分离设计,并顺带把该项目的状态管理迁到 PSO 缓存架构——后来调试线框渲染也只是多建了一个线框 PSO 而已。状态的最终形态不是"会调旋钮",而是"让旋钮在出厂时就被规划好"——这也是第六章优化章的前置心法。再补一条排错联动:PSO 创建失败时调试层会点名具体矛盾(混合状态要求的格式与渲染目标不符、根签名与着色器错位),读创建失败的报错要从"哪个字段与谁矛盾"入手,别急着怀疑创建函数本身——它只是个验货员,货是你在描述结构体里填的。

常见疑问:为什么透明物体必须最后画

三大状态块里最"讲究次序"的是混合。不透明物体的深度测试是裁决式的——谁离相机近谁留下,画两遍换个次序结果一样;透明物体的混合是算术式的——半透明片元与背景按比例调和,次序一换,调和的结果就不同。三块玻璃叠一块有色玻璃,A 先 B 后与 B 先 A 后,人眼看到的透色并不一样。所以行业惯例是:不透明物体先画(深度缓冲把遮挡关系立好),透明物体按离相机由远到近排序后画,且透明物体通常关闭深度写入、只读深度——否则一块透明玻璃会在深度缓冲里立起一堵墙,把它身后的粒子、贴花全部挡掉。这套次序纪律与 PSO 组合收敛直接相关:透明一个 PSO、不透明一个 PSO,两套混合状态各守各的,渲染循环按两遍 pass 推进。

本节要点回顾

  • 三大状态块:光栅化管性能与剔除、深度模板管正确性裁决、混合管合成观感——各守一段管线。
  • PSO 的性质:创建贵、运行快、不可变,一切实践(缓存、预建、组合收敛)都由此推导。
  • 格式一致性:PSO 必须提前知道渲染目标格式,改格式即重建。
  • 散装开关到整体出厂,正是全书"自动挡到手动挡"主调在状态维度上的落点。

第三章引擎全程走完。下一章转入油路:常量缓冲与描述符堆——手动挡世界最硬核的一块实验场。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U