作者:Confetti 首席执行官 Wolfgang Engel
随着 Windows* 10 在 7 月 29 日的发布以及第 6 代英特尔® 酷睿™ 处理器产品家族(代号 Skylake)的发布,我们现在可以更详细地探讨一下专门针对英特尔® 平台的资源绑定。
之前的文章 “Microsoft DirectX* 12 中的资源绑定简介” 介绍了 DirectX 12 中的新资源绑定方法并得出了如下结论:凭借所有这些选择,面临的挑战在于如何为目标 GPU、资源类型及其更新频率选择最理想的绑定机制。
文本介绍了如何选择不同的资源绑定机制,从而在特定的英特尔 GPU 上高效运行应用。
商业工具
若要使用 DirectX 12 开发游戏,您需要以下工具:
- Windows 10
- Visual Studio* 2013 或更高版本
- Visual Studio 附带的 DirectX 12 SDK
- 支持 DirectX 12 的 GPU 和驱动程序
概述
描述符是一个通过 GPU 特定的不透明格式描述 GPU 对象的数据块。 DirectX 12 提供了以下描述符,之前在 DirectX 11 中名为“资源视图”:
- 常量缓冲视图 (CBV)
- 着色器资源视图 (SRV)
- 无序访问视图 (UAV)
- 取样器视图 (SV)
- 渲染目标视图 (RTV)
- 深度模板视图 (DSV)
- 和其他
这些描述符或资源视图可被视为 PGU 前端使用的结构(也称为块)。 描述符的大小约为 32 到 64 字节,并保存纹理尺寸、格式和布局等信息。
描述符存储在描述符堆中,后者代表内存中的结构序列。
描述符表将偏移保存到这一描述符堆中。 它将一个连续的描述符范围映射到着色器插槽,通过一个根签名使其可用。 这个根签名还可以保存根常量、根描述符和静态采样。
图 1. 描述符、描述符堆、描述符表、根签名
图 1 显示了描述符、描述符堆、描述符表和根签名之间的关系。
图 1 描述的代码如下:
// the init function sets the shader registers // parameters: type of descriptor, num of descriptors, base shader register // the first descriptor table entry in the root signature in // image 1 sets shader registers t1, b1, t4, t5 // performance: order from most frequent to least frequent used D3D12_DESCRIPTOR_RANGE Param0Ranges[3]; Param0Ranges[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 1); // t1 Param0Ranges[1].Init(D3D12_DESCRIPTOR_RANGE_CBV, 1, 1); // b1 Param0Ranges[2].Init(D3D12_DESCRIPTOR_RANGE_SRV, 2, 4); // t4-t5 // the second descriptor table entry in the root signature // in image 1 sets shader registers u0 and b2 D3D12_DESCRIPTOR_RANGE Param1Ranges[2]; Param1Ranges[0].Init(D3D12_DESCRIPTOR_RANGE_UAV, 1, 0); // u0 Param1Ranges[1].Init(D3D12_DESCRIPTOR_RANGE_CBV, 1, 2); // b2 // set the descriptor tables in the root signature // parameters: number of descriptor ranges, descriptor ranges, visibility // visibility to all stages allows sharing binding tables // with all types of shaders D3D12_ROOT_PARAMETER Param[4]; Param[0].InitAsDescriptorTable(3, Param0Ranges, D3D12_SHADER_VISIBILITY_ALL); Param[1].InitAsDescriptorTable(2, Param1Ranges, D3D12_SHADER_VISIBILITY_ALL); // root descriptor Param[2].InitAsShaderResourceView(1, 0); // t0 // root constants Param[3].InitAsConstants(4, 0); // b0 (4x32-bit constants) // writing into the command list cmdList->SetGraphicsRootDescriptorTable(0, [srvGPUHandle]); cmdList->SetGraphicsRootDescriptorTable(1, [uavGPUHandle]); cmdList->SetGraphicsRootConstantBufferView(2, [srvCPUHandle]); cmdList->SetGraphicsRoot32BitConstants(3, {1,3,3,7}, 0, 4);
上面的源代码设置了一个有两个描述符表、一个根描述符和一个根常量的根签名。 代码还表明,根常量没有间接级别,通过 SetGraphicsRoot32bitConstants 调用直接提供。 它们被直接发送到着色器寄存器中;没有实际的常量缓冲、常量缓冲描述或绑定。 根描述符只有一个间接级别,因为它们存储指向内存的指针(描述符->内存),描述符表有两个间接级别(描述符表 -> 描述符-> 内存)。
根据其类型(如 SV 和 CBV/SRV/UAV),描述符驻留在不同的描述符堆中。 这是因为,不同硬件平台上的描述符类型尺寸非常不一致。 对于每种类型的描述符堆,也应该只有一个分配的描述符堆,因为更改描述符堆成本很高。
一般而言,DirectX 12 会预先提供 100 多万个描述符的分配,这对于整个游戏级别足够了。 先前版本的 DirectX 根据自己的情况在驱动程序中处理分配,而在 DirectX 12 中,用户可以避免在运行时进行任何分配。 这意味着,一个描述符的任何初始分配都可以从性能“方程”中取出。
注:凭借第三代英特尔® 酷睿™ 处理器(代号为 Ivy Bridge)/第四代英特尔® 酷睿™ 处理器产品家族(代码为 Haswell)以及 DirectX 11 和 Windows 显示驱动程序模型 (WDDM) 1.x 版本,资源可通过一个页表映射操作,根据命令缓冲区中引用的资源动态映射到内存之中。 这样可避免复制数据。 这种动态映射很重要,因为这些架构只为 PGU 提供 2GB 内存(英特尔® 至强® 处理器 E3-1200 v4 产品家族(代号为 Broadwell)提供更大内存)。
借助 DirectX 12 和 WDDM 2.x 版本,在必要时将资源重新映射到 GPU 虚拟地址空间已经不再可能了,因为资源在创建时必须分配一个静态虚拟地址,因此资源的虚拟地址在创建后不能更改。 即使从 GPU 内存中删除资源,它也会保留其虚拟地址,以便以后再次驻留。
因此,Ivy Bridge/Haswell 中 2 GB 的总可用内存可能会成为一个限制因素。
正如以前的文章中指出的,对于一款应用来说,完全合理的结果可能是所有绑定类型的组合:根常量、根描述符、在发布绘制调用时动态收集的描述符的描述符表,以及大型描述符表的动态索引。
不同的硬件架构会在使用根常量集和根描述符集与使用描述符表之间做出不同的性能取舍。 因此,可能需要根据硬件目标平台调整根参数和描述符表之间的比率。
预期变化模式
要了解哪些类型的变化会产生额外费用,我们必须先分析游戏引擎通常如何更改数据、描述符、描述符表和根签名。
我们从常量数据开始。 大部分游戏引擎通常在“系统内存”中存储所有常量数据。 游戏引擎将更改 CPU 访问内存中的数据,而后在帧中,整块的常量数据将复制/映射到 GPU 内存中,然后通过常量缓冲视图或根描述符由 GPU 读取。
如果常量数据通过 SetGraphicsRoot32BitConstants() 作为根常量提供,根描述符中的条目不会更改,但数据可能更改。 如果常量数据先通过 CBV == 描述符,后通过描述符表提供,则描述符不会更改,但数据可能更改。
如果我们需要多个常量缓冲视图,比如对于双重或三重缓冲渲染,CBV 或描述符可能会针对根签名中的每个帧更改。
对于纹理数据,预计纹理在启动过程中在 GPU 内存中分配。 然后将创建 SV == 描述符,存储在描述符表或静态取样器中,并在根描述符中引用。 数据和描述符或静态样品不会在这之后更改。
对于更改纹理或缓冲数据等动态数据(例如含渲染本地化文本的纹理,动画顶点缓冲或程序生成的网格),我们会分配一个渲染目标或缓冲区,提供一个 RTV 或 UAV(描述符),这些描述符此后可能不会更改。 渲染目标或缓冲区中的数据可能会更改。
如果我们需要多个渲染目标或缓冲区,比如对于双重或三重缓冲渲染,描述符可能会针对根签名中的每个帧更改。
对于下面的讨论,如果进行了以下操作,那么更改对于绑定资源至关重要:
- 更改/替换描述符表中的描述符,例如 CBV、RTV 或 上面描述的 UAV
- 更改根签名的任何条目
Haswell/Broadwell 描述符表中的描述符
在基于 Haswell/Broadwell 的平台上,更改根签名中的一个描述符表的成本相当于更改所有描述符表。 更改一个参数意味着硬件必须复制所有当前参数。 根签名中根参数的数量是任何子集变化时硬件必须进行版本控制的数据量。
注:DirectX 12 中所有其他类型的内存,如描述符堆、缓冲资源等,不由硬件进行版本控制。
换句话说,更改所有参数的成本与更改一个参数的成本大致相同(参见 [Lauritzen] 和 [MSDN])。 不更改仍然是最省钱的方法,但没有多大作用。
注:其他硬件,比如在快/慢(溢出)根参数存储有分隔的硬件,只需对参数更改的内存区域(无论是快速区还是溢出区)进行版本控制。
在 Haswell/Broadwell 上,更改描述符表的额外成本可能来自于硬件中绑定表格的尺寸限制。
这些硬件平台上的描述符表使用“绑定表”硬件。 每个绑定表项是一个 DWORD,它可被视为描述符堆内的偏移。 64 KB 环可存储 16,384 个绑定表项。
换句话说,每个绘制调用所用的内存量取决于描述符表中索引并通过根签名引用的描述符的总数。
如果绑定表条目的 64 KB 内存用完,驱动程序将再分配 64KB 的绑定表。 这些表格之间的切换会导致管线停滞,如图 2 所示。
图 2. 管线停滞(由 Andrew Lauritzen 提供)
例如,根签名在描述符表中引用 64 个描述符。 每 256(16,384 / 64)次绘制调用发生一次停滞。
由于更改根签名被视为一种经济的方法,因此拥有描述符表中描述符数量较少的多个根签名,比拥有描述符表中描述符数量较多的根签名要好。
因此,在 Haswell/Broadwell 上,描述符表中引用的描述符数量越少越好。
这对渲染器设计意味着什么? 如果使用描述符较少的更多描述符表,更多的根签名应增加管线状态对象 (PSO) 的数量,因为随着根签名的数量增加,PSO 的数量也需要增加,这两者之间是一对一的关系。
拥有更多的管线状态对象可能导致更多数量的着色器(在这种情况下)更加专业化,而不是提供更多功能的较长着色器(经常作为推荐)。
Haswell/Broadwell 上的根常量/描述符
之前我们提到,更改一个描述符表的成本和更改所有描述符表相同,此处也是如此:更改一个根常量或根描述符的成本相当于更改所有根常量或根描述符(参见 [Lauritzen])。
根常量通过“推送常量“实施,后者是一个硬件用来预填充执行单元 (EU) 寄存器的缓冲区。 由于 EU 线程启动时数值立即可用,将常量数据存储为根常量可实现性能优势,而不要通过描述符表存储它们。
根描述符也作为”推送常量“实施。 它们只是作为常量传送到着色器的指针,通过一般的内容路径读取数据。
Haswell/Broadwell 上的描述符表与根常量/描述符
我们已经介绍了描述符表、根常量和描述符的实施方式,我们便可以回答本文的主要问题:是否一个比另一个要好呢? 由于硬件中绑定表格的尺寸有限以及跨越这一限制所造成的潜在停滞,预计在 Haswell/Broadwell 硬件上更改根常量和根描述符更经济一些,因为它们不使用绑定表硬件。 对于根描述符和根常量,特别推荐使用 Haswell/Broadwell 硬件,以免数据更改每个绘制调用。
Haswell/Broadwell 上的静态取样器
正如先前文章中介绍的,可以通过 HLSL 根签名语言,在根签名或直接在着色器中定义取样器。 这些被称为静态取样器。
在 Haswell/Broadwell 硬件上,驱动程序将会将静态取样器放置在常规的取样器堆中。 这相当于手动将它们放在描述符中。 其他硬件在着色器寄存器中实施了取样器,因此静态取样器可直接编译到着色器中。
一般而言,静态取样器在很多平台上都应该具有优势,所以使用它们没有缺点。 在 Haswell/Broadwell 硬件上,通过增加描述符表中的描述符数量,我们更容易出现管线停滞,因为描述符表硬件只有 16,384 个插槽。
这是 HLSL 中的静态取样器的语法:
StaticSampler( sReg, [ filter = FILTER_ANISOTROPIC, addressU = TEXTURE_ADDRESS_WRAP, addressV = TEXTURE_ADDRESS_WRAP, addressW = TEXTURE_ADDRESS_WRAP, mipLODBias = 0.f, maxAnisotropy = 16, comparisonFunc = COMPARISON_LESS_EQUAL, borderColor = STATIC_BORDER_COLOR_OPAQUE_WHITE, minLOD = 0.f, maxLOD = 3.402823466e+38f, space = 0, visibility = SHADER_VISIBILITY_ALL ])
大部分参数都是一目了然的,因为它们类似于 C++ 级别用途。 主要区别在于边框颜色:在 C++ 级别上,它提供一个完整的色彩范围,而 HLSL 级别仅限于不透明的白色/黑色和透明的黑色。 静态着色器的示例为:
StaticSampler(s4, filter=FILTER_MIN_MAG_MIP_LINEAR)
Skylake
Skylake 支持在一个描述符表中对整个描述符堆(约 100 万资源)进行动态索引。 这意味着,一个描述符表就足以索引所有可用的描述符堆内存。
相比以往的架构,没有必要在根签名中经常更改描述符表条目。 这也意味着,根签名的数量可以减少。 显然,不同材料将需要不同的着色器和不同的 PSO。 但这些 PSO 可以引用相同的根签名。
由于现代渲染引擎利用的着色器数量比 DirectX 9 和 11 少以便消除更改着色器和附加状态的成本,因此减少根签名和 PSO 的数量是有好处的,应该能够在任何硬件平台上实现性能优势。
结论
专注于 Haswell/Broadwell 和 Skylake,开发高性能 DirectX 12 应用的建议都依赖于底层平台。 对于 Haswell/Broadwell,描述符表中的描述符数量应保持低水平,而对于 Skylake,建议保持较高数量并减少描述符表的数量。
为了实现最佳性能,应用编程人员可以在启动期间检查硬件类型,然后选择最有效的资源绑定模式。 (有一个 GPU 检测示例,展示如何检测不同的英特尔硬件架构:https://software.intel.com/zh-cn/articles/gpu-detect-sample/) 资源绑定模式的选择将影响到系统的着色器如何编写。
关于作者
Wolfgang 是 Confetti 的首席执行官。 Confetti 是电子游戏和电影行业的先进实时图形研究与服务提供商的智囊团。 在与他人合作创立 Confetti 之前,Wolfgang 曾在 Rockstar 的核心技术组 RAGE 任职首席图形程序员超过 4 年。 他是 ShaderX和 GPU Pro系列书籍的创始人和编辑、微软最有价值专家、实时渲染相关书籍和文章的作者,以及多家网站的定期撰稿人和全球会议的定期参与者。 他参与编辑的一本书籍《ShaderX4》于 2006 年赢得了游戏开发前线名人堂奖项。 Wolfgang 还是整个行业中许多咨询委员会的成员;其中一个是微软面向 DirectX 12 的图形顾问委员会。 他还积极参与制定推动游戏行业的多项未来标准。 您还可以在 Twitter 上了解他的更多信息,帐户名为 wolfgangengel。 Confetti 的网站为 www.conffx.com
致谢
在此感谢本文的审核人员:
- Andrew Lauritzen
- Robin Green
- Michal Valient
- Dean Calver
- Juul Joosten
- Michal Drobot
参考资料和相关链接
- [Lauritzen] Andrew Lauritzen 等人, “借助英特尔显卡上的 DirectX 12 进行高效渲染”, GDC 2015
- [MSDN] MSDN, “描述符表的高级使用,”https://msdn.microsoft.com/zh-cn/library/windows/desktop/dn859250(v=vs.85).aspx
- Microsoft DirectX 博客:http://blogs.msdn.com/b/directx/
- Twitter 上的 DirectX 12: @DirectX12https://twitter.com/DirectX12
- Direct3D* 12 - 电脑上的控制台 API 效率和性能 (https://software.intel.com/zh-cn/articles/console-api-efficiency-performance-on-pcs)
- Microsoft DirectX 12 显卡培训(YouTube 频道): https://www.youtube.com/channel/UCiaX2B8XiXR70jaN7NK-FpA
** 在性能检测过程中涉及的软件及其性能只有在英特尔微处理器的架构下才能得到优化。 诸如 SYSmark* 和 MobileMark* 等测试均系基于特定计算机系统、硬件、软件、操作系统及功能, 上述任何要素的变动都有可能导致测试结果的变化。 请参考其他信息及性能测试(包括结合其他产品使用时的运行性能)以对目标产品进行全面评估。