DirectX-Graphics-Samples 中的 D3D12 动态索引示例:Shader Model 5.1 动态索引与无界描述符表实战解析

发布时间:2026/10/6 7:24:28
DirectX-Graphics-Samples 中的 D3D12 动态索引示例:Shader Model 5.1 动态索引与无界描述符表实战解析
示例工程【免费下载链接】DirectX-Graphics-SamplesThis repo contains the DirectX Graphics samples that demonstrate how to build graphics intensive applications on Windows.项目地址https://gitcode.com/gh_mirrors/di/DirectX-Graphics-Samples点击查看免费下载本篇文章以 Samples/Desktop/D3D12DynamicIndexing/readme.md 为骨架结合仓库内该示例的完整 C 与 HLSL 源码深入讲解 Direct3D 12 中 HLSL Shader Model 5.1 的两大新特性——动态索引dynamic indexing与无界描述符表unbounded descriptor tables的实际用法并剖析其配套的 Root Signature 1.1 静态性声明优化。读完本文你将掌握如何让着色器在编译期未知索引的情况下安全地索引资源数组、如何把大型描述符表绑定到着色器以及如何在 Windows 10 Anniversary Update SDK 下利用 Root Signature 1.1 为驱动提供优化机会。示例概览一个由 120 座城市构成的演示场景该示例构建了一个非常直观的演示场景一个 15×8 的网格共120 座迷你城市每座城市都是独立的模型网格实例并拥有不同颜色的材质。场景画面取自 D3D12DynamicIndexing.cpp其关键常量定义在 D3D12DynamicIndexing.h 中static const UINT FrameCount 3; static const UINT CityRowCount 15; static const UINT CityColumnCount 8; static const UINT CityMaterialCount CityRowCount * CityColumnCount; // 120 static const UINT CityMaterialTextureWidth 64; static const UINT CityMaterialTextureHeight 64; static const UINT CityMaterialTextureChannelCount 4; static const bool UseBundles true; static const float CitySpacingInterval; // 定义于 .cpp值为 16.0f场景的核心机制是120 座城市共用同一份网格几何体顶点/索引缓冲与同一张漫反射贴图但每一座城市通过一个材质索引从 120 张程序化生成的材质纹理数组中选取不同的颜色材质。这个材质索引正是动态索引的用武之地——它不是在着色器编译期写死的而是在 CPU 侧逐城市通过 root constant 动态设置。材质纹理在 D3D12DynamicIndexing.cpp 中程序化生成沿 HSL 色相彩虹均匀取 120 个颜色梯度materialGradStep 1.0f / CityMaterialCount使得每座城市获得视觉上截然不同的材质直观展示动态索引的效果。核心技术一Shader Model 5.1 的动态索引与无界数组原文档指出该示例演示了 HLSL Shader Model 5.1 的新特性核心是着色器可以在编译时不知道索引值的情况下对数组进行索引dynamic indexing结合无界数组unbounded arrays为着色器作者和美术管线增加了又一层间接性和灵活性。像素着色器 shader_mesh_dynamic_indexing_pixel.hlsl 完整展示了这套用法struct MaterialConstants { uint matIndex; // Dynamically set index for looking up from g_txMats[]. }; ConstantBufferMaterialConstants materialConstants : register(b0, space0); Texture2D g_txDiffuse : register(t0); Texture2D g_txMats[] : register(t1); SamplerState g_sampler : register(s0); float4 PSMain(PSInput input) : SV_TARGET { float3 diffuse g_txDiffuse.Sample(g_sampler, input.uv).rgb; float3 mat g_txMats[materialConstants.matIndex].Sample(g_sampler, input.uv).rgb; return float4(diffuse * mat, 1.0f); }要点逐一拆解无界纹理数组Texture2D g_txMats[] : register(t1)声明了一个不带数组大小的纹理数组。Shader Model 5.1 允许这种无界资源数组数组的实际大小由绑定到该寄存器区域的描述符表在运行时决定着色器无需在编译期知道确切个数。动态索引g_txMats[materialConstants.matIndex]的索引来自一个根常量root constant即 CPU 每帧、每个绘制调用动态写入的uint。这与 Shader Model 5.0 之前索引必须是编译期常量或循环不变式的约束完全不同着色器第一次可以在真正运行期用任意运行时值索引资源数组。漫反射贴图与材质数组分离示例把烘焙了环境光照的g_txDiffuse来自occcity.bin资产与纯色材质数组分开采样两者相乘得到最终颜色。这种基础贴图 × 动态材质表的架构正是文档提到的对美术管线更灵活的典型应用——美术团队可以在不改着色器、不改网格的情况下仅通过调整索引来更换任意实例的材质表现。配套的顶点着色器 shader_mesh_simple_vert.hlsl 则保持传统风格只做 MVP 变换与 UV 传递说明动态索引能力完全集中在像素阶段两种风格的着色器可以在同一管线中共存。核心技术二Root Signature 1.1 与描述符静态性声明原文档的 Optional features 一节强调该示例已更新为基于Windows 10 Anniversary Update SDK构建。该 SDK 为 Direct3D 12 应用提供了新版本的根签名——Root Signature 1.1。它允许应用显式声明描述符堆中的描述符不会改变或描述符指向的数据不会改变从而让驱动有机会针对这些在一段时间内保持静态的描述符及其内存做出优化。示例在 D3D12DynamicIndexing.cpp 中完整实现了从检测到回退的严谨流程D3D12_FEATURE_DATA_ROOT_SIGNATURE featureData {}; // 这是示例支持的最高版本。若 CheckFeatureSupport 成功 // 返回的 HighestVersion 不会高于此值。 featureData.HighestVersion D3D_ROOT_SIGNATURE_VERSION_1_1; if (FAILED(m_device-CheckFeatureSupport(D3D12_FEATURE_ROOT_SIGNATURE, featureData, sizeof(featureData)))) { featureData.HighestVersion D3D_ROOT_SIGNATURE_VERSION_1_0; // 回退到 1.0 }随后构造三个描述符范围descriptor range其中两个带有D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC标志CD3DX12_DESCRIPTOR_RANGE1 ranges[3]; ranges[0].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1 CityMaterialCount, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC); // 漫反射贴图 材质数组 ranges[1].Init(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0); ranges[2].Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0, 0, D3D12_DESCRIPTOR_RANGE_FLAG_DATA_STATIC);DATA_STATIC的含义即原文档所述描述符指向的数据在描述符可见的整个时间内不会改变。示例之所以可以放心地这样声明是因为这 120 张材质纹理在初始化后永不修改城市不移动、材质不上传更新从源码结构看CPU 侧只需在LoadAssets()阶段做一次性上传UpdateSubresources后将资源过渡到D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE此后数据完全静态。相应地根参数布局如下CD3DX12_ROOT_PARAMETER1 rootParameters[4]; rootParameters[0].InitAsDescriptorTable(1, ranges[0], D3D12_SHADER_VISIBILITY_PIXEL); // SRV 表diffuse 120 张材质 rootParameters[1].InitAsDescriptorTable(1, ranges[1], D3D12_SHADER_VISIBILITY_PIXEL); // 采样器 rootParameters[2].InitAsDescriptorTable(1, ranges[2], D3D12_SHADER_VISIBILITY_VERTEX); // CBV rootParameters[3].InitAsConstants(1, 0, 0, D3D12_SHADER_VISIBILITY_PIXEL); // 材质索引 root constant最后通过CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC::Init_1_1(...)创建版本化描述并用D3DX12SerializeVersionedRootSignature序列化序列化时使用featureData.HighestVersion从而在 1.1 不可用时自动退化为 1.0 描述。从Init_1_1的调用可以看出Root Signature 1.1 相比 1.0 的核心差异就是CD3DX12_DESCRIPTOR_RANGE1/CD3DX12_ROOT_PARAMETER1中携带的静态性标志——这正是文档所说允许驱动做优化的具体落地方式。源码级实现解析描述符堆布局与动态索引的驱动链路描述符堆的精心排布CBV/SRV 堆在 D3D12DynamicIndexing.cpp 中按如下公式分配描述符数量cbvSrvHeapDesc.NumDescriptors FrameCount * CityRowCount * CityColumnCount // 3 帧 × 120 360 个 CBV CityMaterialCount 1; // 120 个材质 SRV 1 个漫反射 SRV布局分两段SRV 段堆起始处依次创建 1 个漫反射贴图 SRV 120 个材质纹理 SRV共 121 个CBV 段紧接其后为 3 个帧资源FrameCount各创建 120 个 CBV每座城市一个因此偏移计算为(CityMaterialCount 1) frameResourceIndex * CityRowCount * CityColumnCount。这个偏移计算出现在 FrameResource.cpp 的PopulateCommandList中配合m_cbvSrvDescriptorSizeGetDescriptorHandleIncrementSize获取逐描述符推进句柄——这也是 D3D12 下处理大描述符堆的标准手法。动态索引的 CPU 侧驱动链路每帧的绘制循环在FrameResource::PopulateCommandList中完成索引注入for (UINT i 0; i m_cityRowCount; i) { for (UINT j 0; j m_cityColumnCount; j) { // 将该城市的材质索引写入 root constant根参数 3 pCommandList-SetGraphicsRoot32BitConstant(3, (i * m_cityColumnCount) j, 0); // 将该城市的 CBV 表绑定到根参数 2并移动到下一个描述符 pCommandList-SetGraphicsRootDescriptorTable(2, cbvSrvHandle); cbvSrvHandle.Offset(cbvSrvDescriptorSize); pCommandList-DrawIndexedInstanced(numIndices, 1, 0, 0, 0); } }也就是说每个城市对应一次SetGraphicsRoot32BitConstantSetGraphicsRootDescriptorTable 一次绘制调用root constant 里写的就是着色器中materialConstants.matIndex的值着色器据此在g_txMats[]上做动态索引CBV 表则指向该城市独立的 MVP 常量缓冲。像素阶段与顶点阶段各自使用独立的根参数可见性D3D12_SHADER_VISIBILITY_PIXEL/VERTEX互不干扰。帧资源与 Bundle 加速FrameResource类FrameResource.h、FrameResource.cpp按帧封装了命令分配器直接命令 bundle 命令各一、120 个 CBV 的上传堆以及每座城市的模型矩阵。城市位置在构造时一次性计算完毕SetCityPositions间距 16.0f并以0.02f * 索引微调 Y 轴以避免 z-fightingMVPs 仅在相机移动时通过UpdateConstantBuffers更新。示例默认开启UseBundles true120 次设置索引 绑定表 绘制的记录工作被预录制进 bundleInitBundle中创建并Close()主命令列表每帧只需一次ExecuteBundle见 D3D12DynamicIndexing.cpp 的PopulateCommandList。这也是该示例顺带演示的另一个性能技巧——预录制重复绘制命令减少每帧 CPU 记录开销。若将UseBundles改为false则退化为每帧直接记录命令列表便于对比两种路径的行为差异。构建与运行工程配置与 Shader 编译细节示例位于 Samples/Desktop/D3D12DynamicIndexing/包含D3D12DynamicIndexing.slnx解决方案与D3D12DynamicIndexing.vcxproj工程支持Debug/Release × x64/ARM64四种配置。工程通过 NuGet 包提供依赖见 packages.configMicrosoft.Direct3D.D3D121.618.3Direct3D 12 头文件与库含 Root Signature 1.1 相关 APIMicrosoft.Direct3D.DXC1.8.2505.32HLSL 编译器 dxcWinPixEventRuntime1.0.240308001PIX 事件标记用于调试捕获两个 HLSL 着色器在 D3D12DynamicIndexing.vcxproj 中通过自定义构建步骤调用 dxc 编译为 .csodxc.exe -nologo -Tps_6_0 -EPSMain -Zi -Qembed_debug -Fo$(OutDir)%(Filename).cso %(FullPath) dxc.exe -nologo -Tvs_6_0 -EVSMain -Zi -Qembed_debug -Fo$(OutDir)%(Filename).cso %(FullPath)注意虽然原文档介绍的特性源于 Shader Model 5.1但工程实际以Shader Model 6.0ps_6_0/vs_6_0DXIL 目标编译并开启-Zi -Qembed_debug嵌入调试信息便于在 PIX 中逐帧分析。.cso文件在运行时由ReadDataFromFile加载D3D12DynamicIndexing.cpp。运行前提Windows 10 Anniversary Update1607或更高版本的 SDK 与系统以支持 Root Signature 1.1支持 Direct3D 12 的 GPU无硬件光追等额外要求示例是纯光栅化场景示例入口 Main.cpp 以 1280×720 窗口启动并支持调试模式下启用 D3D12 调试层EnableDebugLayer需系统图形工具可选功能以及-warp参数切换到 WARP 软件适配器源码中m_useWarpDevice分支。总结动态索引 无界表 静态性声明三位一体从整个示例可以提炼出 Shader Model 5.1 时代 D3D12 资源绑定的完整实践范式着色器侧声明无界资源数组g_txMats[]用任意运行时值root constant动态索引摆脱编译期常量限制CPU 侧通过描述符表把大型资源数组整体暴露给着色器逐实例写入索引 root constant 即可实现一网格、多材质驱动侧当资源数据确认不变时用 Root Signature 1.1 的DATA_STATIC等标志显式声明换取驱动优化机会同时通过CheckFeatureSupport优雅回退到 Root Signature 1.0保持对旧环境兼容。这套模式对需要大量实例差异化表现如批量建筑、植被、程序化生成场景的渲染管线尤其具有参考价值——它把美术资产切换从换绑定/换管线降低为改一个索引正是原文档所述为着色器作者和美术管线增加的灵活性与间接性的工程化诠释。赞分享示例工程【免费下载链接】DirectX-Graphics-SamplesThis repo contains the DirectX Graphics samples that demonstrate how to build graphics intensive applications on Windows.项目地址https://gitcode.com/gh_mirrors/di/DirectX-Graphics-Samples点击查看免费下载相关推荐MiniEngine深度探索DirectX-Graphics-Samples中的游戏引擎框架MiniEngine深度探索DirectX Graphics Samples中的游戏引擎框架 想要学习现代游戏引擎开发MiniEngine作为DirectX示例工程Modin 的 pd.DataFrame API 支持矩阵全解析Y/N/P/D 标记、分布式实现边界与 pandas 回退机制Modin 的 pd.DataFrame API 支持矩阵全解析Y/N/P/D 标记、分布式实现边界与 pandas 回退机制 Modin 以改一行导入代码示例工程DirectX-Graphics-Samples HDR渲染完全指南高动态范围图形技术DirectX Graphics Samples HDR渲染完全指南高动态范围图形技术 在当今视觉计算领域 HDR渲染技术 正成为游戏开发和图形应用的核心竞示例工程上一篇图像转ASCII艺术教程下一篇Komorebi 开源项目教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考