Chapter 87: Pipeline Mapping
Direct3D 12 的渲染管线映射,是把一帧画面拆成一组可检查的绑定关系:顶点数据如何进入 Input Assembler,shader 如何通过 root signature 和 descriptor 读取资源,固定功能状态如何解释 shader 输出,render target 和 depth stencil 如何接收结果,command list 如何把这些关系提交给 GPU。读完本章后,读者应能拿到一个 draw call,沿着资源、状态、shader、PSO、barrier 和 PIX 证据追踪它为什么生成当前像素。
本章使用一个贯穿材料:一个 D3D12 frame 中的 indexed draw。CPU 先准备 vertex buffer、index buffer、constant buffer 和纹理;随后创建 root signature、descriptor heap、pipeline state object;每帧把 swap chain back buffer 从 present 状态转到 render target 状态,设置 viewport、scissor、RTV、DSV、descriptor table 和 vertex/index buffer,最后执行 DrawIndexedInstanced。这个材料足够小,可以定位一帧三角形、模型或 UI quad;它也足够完整,可以覆盖 Direct3D 管线映射中的关键边界。
Microsoft 的 D3D12 文档把 graphics pipeline 描述为从输入到输出图像的顺序数据流,其中既有 programmable shaders,也有 fixed function operations;文档还指出 D3D12 使用 PSO 将 input assembler、rasterizer、pixel shader、output merger 等状态打包成不可变对象。这里的重点在于工程映射:每一类状态都要回答“它影响哪个管线阶段、由谁设置、何时生效、失败时能从哪里观察”。相关背景可对照 Microsoft Learn 的 Pipelines and Shaders with Direct3D 12 与 Managing Graphics Pipeline State in Direct3D 12。
本章的判断框架很直接:先确认 draw call 所属 pass,再确认 PSO 和 root signature 是否匹配 shader;接着检查 IA 输入、descriptor 资源、viewport/scissor、RTV/DSV、resource state;最后用 PIX 或 debug layer 观察 barrier、descriptor、PSO 和 GPU timing 的证据。管线映射的价值体现在排查顺序上:黑屏、错纹理、深度异常、blend 错误、PSO 创建失败、frame time 上升,都能回到这条路径上定位。
下面的图只描述本章贯穿材料中的 graphics draw 路径。Compute、ray tracing、mesh shader、multi-queue 和复杂 frame graph 属于后续扩展,不放入本章主线。
这条路径有两个并行维度。第一条是数据流:buffer、texture、constant 从 CPU 侧准备,经 descriptor 和 root signature 被 shader 读取,最后写入 render target。第二条是状态流:PSO、viewport、scissor、RTV/DSV、barrier 决定 GPU 如何解释数据。排查时要把这两条路径合在同一个 draw call 中观察。
87.1 Direct3D 渲染管线各阶段的映射逻辑
Direct3D 渲染管线映射的第一步,是把 draw call 拆成阶段输入和阶段输出。一个 indexed draw 在 D3D12 中通常从 IASetVertexBuffers、IASetIndexBuffer、IASetPrimitiveTopology 开始,随后由 PSO 中的 input layout 和 vertex shader bytecode 解释顶点;光栅化阶段根据 rasterizer state、viewport 和 scissor 产生 fragment;pixel shader 读取 descriptor 指向的资源并输出颜色;output merger 根据 depth/stencil、blend、RTV、DSV 和 render target format 写入最终 framebuffer。
Input Assembler 的工作定义是“把应用提供的 buffer 和拓扑组合成 GPU 可消费的 primitive 输入”。它读取 vertex buffer view、index buffer view、primitive topology 和 PSO 内的 input layout。这里的 input layout 是 CPU 端对顶点内存布局的声明,vertex shader 的 input semantic 是 shader 端对属性的接收契约。二者共同决定 POSITION、NORMAL、TEXCOORD 等字段是否能按正确偏移进入 vertex shader。
贯穿材料中的顶点输入可以压缩成四个对象:一个 vertex buffer、一个 index buffer、一个 input layout、一个 primitive topology。它们必须在同一个 draw call 前形成一致关系。vertex buffer 的 stride 决定每个顶点间隔;input layout 的 AlignedByteOffset 决定字段起点;index buffer 的 format 决定索引宽度;primitive topology 决定这些索引如何组成三角形。任意一项错误,PIX 中仍可能看到 draw call 已执行,但顶点位置、三角形连接或属性值会异常。
一个最小映射示例如下。代码只展示关键绑定顺序,省略 device 创建、资源上传和错误处理。
commandList->SetPipelineState(meshPso.Get());
commandList->SetGraphicsRootSignature(meshRootSignature.Get());
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->IASetIndexBuffer(&indexBufferView);
commandList->SetGraphicsRootDescriptorTable(0, frameCbvHandle);
commandList->SetGraphicsRootDescriptorTable(1, materialSrvHandle);
commandList->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);
这段代码说明 D3D12 的 draw call 不是单个函数完成全部绑定。DrawIndexedInstanced 只是触发已记录状态下的绘制。真正决定 GPU 行为的是它之前记录在 command list 中的状态集合:PSO、root signature、descriptor table、IA buffer、RTV/DSV、viewport、scissor 和 resource barrier。排查 draw call 时,应从 draw 前最后一次设置这些状态的位置开始,而非只看 draw 函数参数。
Vertex Shader 的阶段输出通常包括裁剪空间位置和一组插值变量。SV_Position 是光栅化阶段必须使用的输出;UV、normal、color 等 varyings 会经过插值后进入 Pixel Shader。映射关系的关键在于:vertex shader 读取的是 IA 提供的 attributes 和 root signature 绑定的 constants;pixel shader 读取的是插值结果和 descriptor 指向的纹理或 buffer。顶点错位优先检查 IA 和 VS,纹理错乱优先检查 PS 输入、SRV descriptor 和 sampler。
Rasterizer 的工作定义是“把裁剪后的 primitive 转成像素覆盖和插值输入”。它使用 PSO 内的 rasterizer state,并结合 command list 中设置的 viewport 和 scissor。viewport 决定 NDC 到屏幕空间的映射,scissor 决定允许写入的矩形区域。若 viewport 尺寸为零、scissor 过小或 back buffer 尺寸变化后未更新,这一阶段会直接把可见图元压缩、裁掉或映射到错误区域。
Output Merger 的工作定义是“把 pixel shader 输出和已有 render target/depth stencil 内容合并”。它读取 PSO 内的 blend state、depth stencil state、render target format、DSV format,也读取 command list 中设置的 RTV/DSV handle。颜色已经从 pixel shader 输出后,仍可能被 depth test、stencil test、blend 或 render target write mask 改写结果。黑屏排查中,若 PS 输出颜色正常,应继续检查 depth 写入、RTV format、blend 因子和 render target 状态。
完整阶段映射可以用下面的检查顺序压缩:IA 解释内存,VS 生成裁剪空间,Rasterizer 生成片元,PS 计算颜色,OM 执行 depth/blend/write。每一阶段都要同时看“数据输入”和“状态解释”。只看 shader 代码会漏掉 input layout、resource state、RTV/DSV 和 PSO 组合错误;只看 API 调用会漏掉 semantic、插值和 shader 输出语义。
87.2 固定功能与可编程阶段的协调
固定功能阶段和可编程阶段的协调,是 Direct3D 管线映射中最容易被混写的部分。可编程阶段负责用户写下的 shader 逻辑,固定功能阶段负责输入装配、裁剪、光栅化、深度模板、混合和目标写入。二者通过明确的输入输出语义连接:shader 输出给固定功能阶段的值必须满足固定功能阶段的解释规则,固定功能阶段提供给 shader 的输入也必须来自前一阶段的合法状态。
贯穿材料中的第一组协作发生在 IA 与 Vertex Shader。IA 不理解模型空间、法线空间或 UV 语义的数学含义,它只按 input layout 从 buffer 取字节。Vertex Shader 才把这些字节解释为 float3 position、float3 normal、float2 uv。因此,input layout 与 shader signature 的匹配是协议层问题,模型矩阵和投影矩阵的正确性是数学层问题。排查时先确认属性能被正确读到,再检查矩阵计算。
第二组协作发生在 Vertex Shader 与 Rasterizer。Vertex Shader 输出的 SV_Position 必须处在裁剪空间,随后由硬件完成裁剪、perspective divide 和 viewport transform。若 shader 把屏幕空间坐标误写到 SV_Position,或投影矩阵使用了另一套深度范围约定,rasterizer 会按 D3D 的规则继续执行,结果表现为图元消失、深度反转或视口外绘制。这里的错误属于 shader 输出与固定功能期望不一致。
第三组协作发生在 Rasterizer 与 Pixel Shader。Rasterizer 根据覆盖规则产生 pixel shader invocation,并插值 VS 输出的 varyings。Pixel Shader 只看到已经插值后的值,通常看不到三角形原始顶点顺序。若法线方向错误、UV 接缝明显或透视变形异常,原因可能在顶点属性、插值语义、顶点顺序、front face 约定或 rasterizer cull mode 中。排查时要把 shader 输入值和 rasterizer state 放到同一个 capture 里检查。
第四组协作发生在 Pixel Shader 与 Output Merger。Pixel Shader 输出颜色和可选深度,OM 根据 depth/stencil state 和 blend state 决定写入。透明物体排序、alpha blend、depth write、stencil mask、MRT format 都在这一层汇合。若 shader 输出 alpha 为 0,但 blend state 按 alpha 混合,画面会接近透明;若 depth write 仍开启,透明物体还可能挡住后续物体。这类问题不能从单独的 HLSL 片段得出结论,必须同时查看 OM 状态。
下面的表把固定功能与可编程阶段的接口压缩为同一组观察维度。表格中的“证据入口”既可以来自 PIX,也可以来自 RenderDoc 的 D3D12 capture 或 debug layer 输出。
| 协作点 | 可编程侧 | 固定功能侧 | 常见症状 | 证据入口 |
|---|---|---|---|---|
| IA → VS | VS input signature | input layout、vertex/index buffer | 顶点飞散、属性错位 | draw call 的 IA state 与 VS input |
| VS → Rasterizer | SV_Position、varyings | viewport、scissor、cull、fill mode | 图元消失、比例异常、背面被剔除 | VS output、rasterizer state |
| Rasterizer → PS | 插值输入 | sample coverage、front face、MSAA | UV 错、法线跳变、边缘异常 | PS input、primitive state |
| PS → OM | SV_Target、alpha、depth | blend、depth/stencil、RTV/DSV format | 黑屏、透明错误、深度覆盖 | PS output、OM state、render target view |
这张表的作用是减少排查跳跃。看到纹理颜色异常时,先判断 PS 是否采样到正确 SRV,再看 sampler 和 UV;看到物体被裁掉时,先看 SV_Position 和 viewport/scissor,再看 cull mode;看到颜色输出正常但画面没有变化,先看 depth/stencil、blend 和 RTV。固定功能阶段给 shader 提供边界,shader 输出又成为固定功能阶段的输入,二者必须合并判断。
D3D12 中有一部分状态属于 PSO,有一部分状态记录在 command list 中。Microsoft Learn 的 pipeline state 文档列出 PSO 覆盖 shader bytecode、input vertex format、primitive topology type、blend state、rasterizer state、depth stencil state、RTV/DSV format、multisampling 参数和 root signature;同时 viewport、scissor、descriptor heap、RTV/DSV、primitive topology order、blend factor、stencil ref 等状态通过 command list 方法设置。这个分界解释了一个常见现象:同一份 shader 代码在两个 draw 中表现不同,原因可能来自同一 PSO 外部的动态状态。
协调固定功能与可编程阶段的稳定做法,是把 draw call 前的状态分成三层记录。第一层是 PSO 层,包含 shader bytecode、input layout、blend/depth/raster state 和 render target format。第二层是 root/binding 层,包含 root signature、descriptor heap、descriptor table、CBV/SRV/UAV/sampler。第三层是 command dynamic 层,包含 viewport、scissor、RTV/DSV、vertex/index buffer、barrier 和 draw 参数。排查时按层切换,能快速发现哪一层发生漂移。
87.3 构建完整渲染管线与命令列表
构建完整渲染管线的核心,是把不可变对象、每帧资源和每个 draw 的动态绑定按生命周期分开。D3D12 没有 D3D11 那种 immediate context 驱动实时接收并隐藏大量状态修正的模型;Microsoft Learn 的 work submission 文档说明 D3D12 通过 command list 记录 draw 和 resource management 调用,再提交到 command queue 执行。这个模型要求应用明确管理 command allocator、command list、resource state、descriptor heap 和 fence。
贯穿材料中的初始化阶段应完成三件事。第一,创建 root signature,定义 shader 能看到哪些 CBV、SRV、UAV、sampler 或 root constants。第二,创建 descriptor heap 和 descriptor,将 constant buffer、texture SRV、sampler 等资源放到 shader 可见的绑定位置。第三,创建 graphics PSO,把 shader bytecode、input layout、rasterizer、blend、depth/stencil、RTV/DSV format 和 root signature 打包。Microsoft 的 root signature 文档把 root signature 定义为 graphics pipeline 绑定资源类型的契约,并说明它把 command list 与 shader 需要的资源连接起来,可对照 Root Signatures。
每帧阶段围绕 command allocator 和 command list 展开。command allocator 存储记录命令所需的内存,command list 记录本帧要提交给 GPU 的指令。通常每个 in-flight frame 有独立 allocator,CPU 在 fence 确认 GPU 已完成对应帧之后再 reset。这个边界很关键:如果 allocator 被过早 reset,命令内存可能仍被 GPU 使用;如果 command list 反复创建销毁,CPU 开销会上升并破坏 frame pacing。
一个最小 frame draw 的命令顺序如下。代码仍是简化形式,只保留管线映射相关调用。
commandAllocator->Reset();
commandList->Reset(commandAllocator.Get(), meshPso.Get());
commandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
backBuffer.Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET));
commandList->RSSetViewports(1, &viewport);
commandList->RSSetScissorRects(1, &scissorRect);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, &dsvHandle);
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
commandList->ClearDepthStencilView(dsvHandle, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
ID3D12DescriptorHeap* heaps[] = { cbvSrvUavHeap.Get() };
commandList->SetDescriptorHeaps(1, heaps);
commandList->SetGraphicsRootSignature(meshRootSignature.Get());
commandList->SetGraphicsRootDescriptorTable(0, frameCbvHandle);
commandList->SetGraphicsRootDescriptorTable(1, materialSrvHandle);
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->IASetIndexBuffer(&indexBufferView);
commandList->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);
commandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
backBuffer.Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT));
commandList->Close();
commandQueue->ExecuteCommandLists(1, commandLists);
swapChain->Present(1, 0);
这段顺序体现了三条依赖链。第一条是 render target 状态链:back buffer 先从 present 转为 render target,绘制完成后再转回 present。Microsoft 的 resource barrier 文档说明 D3D12 将 per-resource state 管理责任交给应用,并通过 ResourceBarrier 通知驱动潜在同步和状态转换;同一资源的 before/after 状态需要在连续 barrier 中保持一致。可对照 Using Resource Barriers to Synchronize Resource States in Direct3D 12。
第二条是 binding 链:descriptor heap 必须先设置到 command list,root signature 定义 root 参数布局,descriptor table handle 指向具体资源范围,shader 根据 register space 和 binding slot 访问资源。若 descriptor heap 没有设置、root parameter index 错误、descriptor 被覆盖或 shader register 与 root signature 不匹配,draw call 仍可能执行,但 shader 读取到的 constant 或 texture 会异常。
第三条是 geometry 链:PSO 内的 input layout 定义顶点格式,command list 绑定 vertex/index buffer view,primitive topology 决定索引组合,draw 参数决定索引数量和偏移。索引数量错、base vertex 错、index format 错和 stride 错会产生相似的“顶点飞散”症状。稳定排查方法是先在 PIX 中检查 draw 的 IA state,再打开 mesh view 或 VS input 观察属性值。
Command list 构建还要处理 resource lifetime。默认 heap 中的 vertex buffer、index buffer 和 texture 通常由 upload heap 上传后进入 GPU 读取状态;constant buffer 可以使用 upload heap ring buffer 按帧更新;back buffer 来自 swap chain 并跟随 frame index 轮换。每类资源的状态和更新频率不同,命令记录时应把它们分层管理。把 per-frame constant、per-material texture、per-mesh buffer 混在同一个更新函数里,会让 descriptor 覆盖和同步等待更难定位。
命令列表的提交边界决定 CPU 与 GPU 的并行度。一个简单 frame 可以用一个 direct command list 完成;更复杂的引擎会把 shadow pass、gbuffer pass、lighting pass、post process 分成多个 command list 或多个 worker thread 录制。无论录制方式如何变化,单个 draw 的映射关系仍保持不变:PSO 定义大部分 pipeline 状态,root signature 定义 shader 可见资源结构,command list 提供每帧和每 draw 的实际绑定,barrier 提供资源状态转换证据。
87.4 状态管理与流水线状态对象(PSO)
PSO 是 D3D12 管线映射的中心对象。它把多个硬件阶段之间存在依赖的状态预先组合成一个不可变对象,使运行时通过 SetPipelineState 快速切换。Microsoft Learn 明确说明 PSO 包含 input assembler、rasterizer、pixel shader、output merger 等组件状态,并且创建后不可变;硬件和驱动可以把 PSO 预处理为更接近硬件寄存器的状态组合。工程上的结论是:PSO 创建属于初始化或缓存路径,PSO 切换属于 draw 排序和材质系统路径。
贯穿材料中的 mesh PSO 至少包含这些字段:root signature、VS bytecode、PS bytecode、input layout、rasterizer state、blend state、depth stencil state、primitive topology type、RTV format、DSV format、sample count。每个字段都影响 draw call 的合法性和图像结果。shader bytecode 与 root signature 不兼容会导致创建或验证失败;RTV format 与当前 render target 不一致会导致输出解释错误或 debug layer 报告;depth state 与 DSV format 不匹配会让深度测试失效或无法创建 PSO。
下面是一个 PSO 描述的关键字段示例。它展示的是字段之间的关系,不是完整引擎封装。
D3D12_GRAPHICS_PIPELINE_STATE_DESC desc = {};
desc.pRootSignature = meshRootSignature.Get();
desc.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get());
desc.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get());
desc.InputLayout = { inputElements.data(), static_cast<UINT>(inputElements.size()) };
desc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
desc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
desc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
desc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
desc.NumRenderTargets = 1;
desc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.DSVFormat = DXGI_FORMAT_D32_FLOAT;
desc.SampleDesc.Count = 1;
ThrowIfFailed(device->CreateGraphicsPipelineState(&desc, IID_PPV_ARGS(&meshPso)));
PSO 的不可变性要求引擎提前设计 cache key。一个常见 key 至少包含 shader variant、root signature id、input layout id、blend/depth/raster state、RTV/DSV format、sample count、primitive topology type。若 key 过粗,不同状态会错误复用同一 PSO;若 key 过细,同一状态会创建大量等价 PSO。前者导致画面错误,后者导致 CPU 创建开销、内存占用和运行时卡顿。
PSO 与 root signature 的关系需要单独处理。root signature 是 shader 资源绑定契约,PSO 引用一个 root signature。D3D12 允许 command list 单独设置 graphics root signature,但 draw 时 active PSO、active root signature 和 shader 资源访问必须形成一致关系。稳定做法是在材质或 pipeline asset 中把 PSO 与 root signature 作为同一层资源管理,并在 debug 构建中记录它们的兼容性哈希。
PSO 外仍有动态状态。viewport、scissor、descriptor heaps、root arguments、RTV/DSV、vertex/index buffer、primitive topology order、blend factor、stencil ref 等通过 command list 设置。这个分界会影响 draw sorting。按 PSO 排序可以降低硬件状态切换;按 descriptor 或 material 排序可以降低绑定更新;按 depth 排序可以减少 overdraw。实际排序应依据当前瓶颈选择:CPU 提交瓶颈优先减少 PSO 和 descriptor 更新,GPU fragment 瓶颈优先考虑深度预排序和 early-z 条件。
状态管理的失败模式可以分成四类。第一类是创建期失败,例如 shader signature 与 input layout 不匹配、RTV format 未设置、root signature 不兼容。第二类是绑定期失败,例如 command list 中忘记设置 descriptor heap 或 root descriptor table。第三类是状态漂移,例如 viewport 沿用上一窗口尺寸、stencil ref 沿用上一 draw、blend factor 未复位。第四类是缓存污染,例如 PSO cache key 缺少 sample count 或 render target format。四类问题的证据位置不同,不能用同一种日志判断。
一个可维护的 D3D12 管线层,应把状态分成 immutable pipeline、frame binding、draw binding 和 transient dynamic state。immutable pipeline 由 PSO 和 root signature 代表;frame binding 包含 per-frame CBV、swap chain RTV、global descriptor heap;draw binding 包含 mesh buffer、material texture、object constant;transient dynamic state 包含 viewport、scissor、blend factor、stencil ref。这样划分后,黑屏和性能问题可以快速归因到某一层,而不会在全局状态中无序搜索。
87.5 PIX Evidence for Barrier Descriptor and PSO Bottlenecks
PIX 证据的价值在于把抽象的 pipeline mapping 转成可观察的 draw call 状态、资源历史和 timing 数据。Microsoft 的 PIX on Windows 页面把 PIX 定位为 Windows 上 DirectX 12 游戏的性能调优和调试工具;本章只使用它作为证据入口,不要求读者依赖某个特定 GPU counter。可选替代入口包括 RenderDoc、D3D12 debug layer、GPU markers 和自建日志,但证据问题保持一致:barrier 是否正确,descriptor 是否指向正确资源,PSO 是否匹配 draw,时间是否集中在某个 pass 或状态切换路径。
Barrier 证据要回答资源是否处在合法状态。贯穿材料中的 back buffer 在 clear 和 draw 前应处于 render target 状态,在 present 前应回到 present/common 状态。纹理上传后应从 copy destination 转为 pixel shader resource;vertex buffer 上传后应进入 vertex and constant buffer 读取状态;depth buffer 应处于 depth write 或 depth read。PIX 的 resource history 或 event list 能帮助确认这些转换是否发生在正确 draw 前。debug layer 能报告部分 before/after 不一致,但它的跟踪保守且覆盖有限,最终仍要按资源生命周期建立自己的状态表。
Descriptor 证据要回答 shader 读取的句柄是否指向预期资源。D3D12 resource binding 文档把 binding 定义为把资源对象链接到 graphics pipeline 的 shader;descriptor、descriptor table、descriptor heap 和 root signature 是这个模型的核心。贯穿材料中,frame CBV 应指向当前帧 constant buffer,material SRV 应指向当前材质 texture,sampler 应与纹理采样策略匹配。PIX 中需要查看 root parameters、descriptor table range、GPU descriptor handle、绑定资源名称和资源内容预览。若纹理错位,应先确认 descriptor table 起始句柄和 offset,再检查 descriptor heap 是否在同一 command list 中被替换。
PSO 证据要回答 draw 使用了哪一组不可变状态。PIX 的 draw call 视图通常能看到 active pipeline state、shader、input layout、rasterizer、blend、depth/stencil、RTV format 和 DSV format。若同一材质在不同 pass 中表现不同,应比较两个 draw 的 PSO 字段,而非只比较 shader 源码。PSO cache 问题常见于运行时变体系统:材质切换了 alpha blend,但 cache key 没有包含 blend state;MSAA render target 启用后,key 没有包含 sample count;HDR pass 切换 format 后,key 没有包含 RTV format。
GPU timing 证据要回答瓶颈集中在哪个阶段或 pass。一个 draw 的时间上升可能来自 shader ALU、texture sampling、overdraw、barrier stall、descriptor pressure、PSO creation hitch 或 CPU recording 开销。PIX timing capture、GPU markers 和 timestamp query 可以把 frame 拆成 pass 和 draw 区间。若某个 pass 前出现长时间空洞,优先检查 barrier、queue sync 和 resource state transition;若大量小 draw 造成 CPU 时间上升,优先检查 command recording、PSO 切换和 descriptor 更新;若 GPU 时间集中在 pixel shader draw,优先检查 overdraw、texture sampling 和 blend。
下面的表把三类问题和可观察证据对齐。它不是工具菜单清单,而是排查时的判断顺序。
| 问题类型 | 先看对象 | 关键证据 | 常见修正方向 |
|---|---|---|---|
| Barrier 错误 | resource history、event list | before/after state、subresource、pass 顺序 | 建立 per-resource state 表,合并同类 transition,减少多余 common 往返 |
| Descriptor 错误 | root parameters、descriptor heap | table 起点、range、资源名称、SRV/CBV/UAV 描述 | 固定 root parameter index,按 frame/material/object 分配 descriptor 区间 |
| PSO 错误 | active PSO、shader、formats | shader bytecode、input layout、RTV/DSV format、sample count | 完整 cache key,初始化期预创建,debug 名称覆盖所有变体 |
| Timing 异常 | GPU markers、timing capture | pass 时间、draw 时间、GPU 空洞、CPU recording 时间 | 按瓶颈分层处理:同步、shader、bandwidth、draw count、state switching |
Barrier 性能排查需要结合资源语义。D3D12 支持 common state promotion 和 decay,文档说明部分资源从 common 到可提升状态的转换可以不产生额外 GPU 同步;过度插入 barrier 可能触发 cache flush、layout change 或同步等待。工程上不应把“每次使用前都 transition”当作稳定策略。更好的做法是建立资源状态追踪:只在访问类型发生实际冲突时插入 barrier,把多个 transition 合并到一个调用中,并在 pass 边界用 marker 标记转换原因。
Descriptor pressure 排查需要结合绑定频率。per-frame constant、per-pass texture、per-material texture、per-object constant 的更新频率不同。把每个 draw 都重写大量 descriptor,会增加 CPU 维护和 heap 管理成本;把所有资源都放到一个巨大无约束表中,又会增加越界和生命周期错误的排查成本。当前章节只要求建立映射判断:descriptor heap 是物理存放位置,descriptor table 是 shader 可见范围,root signature 是访问契约,shader register 是 HLSL 访问入口。
PSO 瓶颈排查需要区分创建期和切换期。PSO 创建可能触发 shader bytecode 验证和驱动预处理,适合放在加载、热更新或异步编译缓存中;PSO 切换本身比临时组合状态更直接,但大量无序切换仍会增加提交和硬件状态更新压力。材质系统应把 PSO 变体数量和 draw sorting 联合管理:透明、深度写入、MSAA、HDR format、skinning、instancing、debug view 都可能改变 PSO key。
最终的 PIX 排查顺序可以固定成五步。先用 frame marker 找到目标 pass 和 draw;再检查 active PSO 与 root signature;接着检查 IA buffer、descriptor table、RTV/DSV 和 dynamic state;随后打开 resource history 检查 barrier 和 subresource 状态;最后看 timing capture 判断问题是否已经从正确性转为性能。这个顺序的好处是把视觉症状、API 状态、GPU 资源和工具证据连接在同一条路径上。
最小自检任务
给定一个 D3D12 frame:窗口背景被成功清空为蓝色,但一个使用纹理材质的 indexed mesh 没有显示。PIX 中能看到 DrawIndexedInstanced 已记录并执行,VS output 的 SV_Position 位于可见区域,PS 输出预览显示颜色接近黑色。请按本章的 pipeline mapping,给出排查顺序,并说明每一步要验证的对象、可能结论和下一步动作。
答案要点
先确认目标 draw 所属 pass 和 active PSO。背景能清空说明 RTV 至少能被写入,但 mesh draw 仍要检查 PSO 中的 input layout、VS/PS bytecode、rasterizer、depth stencil、blend、RTV/DSV format 和 sample count。VS output 可见,说明 IA 到 VS 的主路径大概率成立;仍应抽查 vertex/index buffer view、primitive topology 和 input layout,确认没有只显示到屏幕外或被错误剔除。
第二步检查固定功能到 OM 的状态。查看 viewport、scissor、cull mode、depth test、depth write、stencil state、blend state 和 render target write mask。若 depth test 失败或 stencil mask 屏蔽,PS 可以执行预览但最终没有写入目标;若 blend state 使用 alpha 且 PS alpha 为 0,颜色会被混合掉;若 render target write mask 关闭,PS 输出无法进入 RTV。
第三步检查 descriptor 和 shader 资源绑定。PS 输出接近黑色时,应查看 root signature、root parameter index、descriptor heap、descriptor table 起点、SRV 指向的 texture、sampler 和 material constant。若 SRV 指向默认黑纹理、descriptor offset 错位、texture 还在 copy destination 状态或 material constant 中 base color 为 0,PS 会稳定输出黑色。这个阶段的证据来自 PIX 的 root parameters、descriptor view 和资源内容预览。
第四步检查 resource barrier 和 subresource 状态。纹理上传后应转到 pixel shader resource,back buffer draw 前应处于 render target,depth buffer 应匹配 depth write 或 depth read。若 resource history 显示纹理仍处于 copy destination,或 back buffer 状态转换缺失,修正资源状态表和 pass 边界 barrier。若状态正确,再使用 timing capture 判断是否存在过度 barrier、PSO 切换或 descriptor 更新造成的性能问题。
最终结论应按证据收束。当前症状中 VS output 可见、PS 颜色接近黑色,所以首要怀疑是 PS 资源绑定、材质常量、sampler、texture state 或 OM blend/depth。只有在这些证据排除后,才回到 shader 逻辑或资产内容本身。这个顺序覆盖了数据输入、shader 执行、固定功能状态、资源状态和工具证据。
本章知识点总结
- 阶段映射:Direct3D draw call 应拆成 IA、VS、Rasterizer、PS、OM 的输入、状态和输出逐段检查。
- 贯穿材料:一个 indexed draw 足以覆盖 vertex/index buffer、root signature、descriptor、PSO、barrier 和 RTV/DSV 的核心关系。
- IA 契约:input layout、vertex buffer stride、index format、primitive topology 和 VS input signature 共同决定顶点属性能否正确进入 shader。
- VS 输出:
SV_Position是 VS 交给固定功能裁剪、透视除法和 viewport transform 的关键接口。 - 光栅状态:viewport、scissor、cull mode、fill mode 和 sample coverage 会影响图元是否产生 pixel shader invocation。
- OM 合并:depth/stencil、blend、RTV/DSV format 和 write mask 决定 PS 输出能否写入目标。
- PSO 边界:PSO 打包 shader bytecode、input layout、blend/depth/raster state、RTV/DSV format、sample count 和 root signature。
- 动态状态:descriptor heap、root arguments、viewport、scissor、RTV/DSV、vertex/index buffer、blend factor 和 stencil ref 由 command list 设置。
- 命令列表:command list 记录 draw 与资源管理调用,command queue 提交执行,allocator 复用必须受 fence 保护。
- Barrier 证据:resource barrier 说明资源访问状态转换,排查时要结合 before/after state、subresource 和 pass 顺序。
- Descriptor 证据:descriptor heap 存放描述符,descriptor table 暴露 shader 可见范围,root signature 定义访问契约。
- PSO 缓存:PSO cache key 应覆盖 shader variant、root signature、input layout、blend/depth/raster state、formats 和 sample count。
- PIX 顺序:先定位 pass 和 draw,再查 PSO/root signature,随后查 IA、descriptor、RTV/DSV、barrier 和 timing。
- 性能归因:barrier stall、descriptor pressure、PSO 切换、shader cost、overdraw 和 CPU recording 要按证据分别处理。