Introduction to 3D Game Programming with Directx12系列小结

  Introduction to 3D Game Programming with DirectX系列书箱对DirectX技术的学习推广起到了非常大的作用,从原理到实践、从数学到物理、从渲染管线到常见技术思路都进行了比较深入的讲解,是难得的DirectX学习资料。DirectX12是Directx系列API的最新迭代,其出现代表了微软在图形硬件处理上的最新成果,也在一定程度上代表了未来图形处理的方向。
  本系列是以《Introduction to 3D Game Programming with DirectX12》作为学习蓝本,也是以《Introduction to 3D Game Programming with DirectX12》课后练习题作为主线的系统归纳技术资料。也是国内第一次公开对DirectX12进行讨论的系列资料。在写作本系列资料过程中,国内中文有价值的参考资料几乎没有,国外对Directx12的讨论也是刚刚起步萌芽,由于DirectX12体系架构的巨大变化,在碰到问题时可供参考的资料、代码极其有限,很多问题都是在摸索中前进,在前进中跌倒,在跌倒中爬起,在爬起后想办法解决,整个过程还是略为艰辛。

一、DirectX12的性能优先设计思路

  在学习完本书后,我们再反过头来看DirectX12,一种豁然开朗的感觉,似乎突然明白了微软的良苦用心。之前我们也提到过,Directx12最重要的变化就是其更接近底层硬件,Directx12在贴近硬件的路上走得很深入,因此能更加有效的发挥GPU、CPU的性能,原生的命令队列设计可以更加有效的利用多核CPU,资源绑定方式的变化带来了校验方式的变革,GPU与CPU的异步设计能更多的释放双方的性能潜力。因此来说,Directx12的设计确实是符合了多CPU核心时代发展的要求,能显著利用多核心的能力。
  现在可以看到,在整个DirectX12的设计过程中,“性能优先”是贯穿了整个DirectX12的研发过程,很一处的变动、第一处的改进都把这一理念体现的淋漓尽致,没有多余的设计,没有化繁为简的操作,一切均是向性能看齐。

(一)、D3D12 命令队列/列表

  D3D12 命令队列负责缓冲渲染命令,然后这些内置到由驱动程序已知且最后由命令队列执行的硬件命令。由于每个命令队列都可以在没有任何中间锁保护的情况下独立填写渲染命令,所以它的运行速度要比 D3D11 中的对应物快得多。

  命令队列可反复重新使用。当应用要重新提交相同的命令队列来执行 GPU 时,它必须等待 GPU 上的命令队列完成执行;否则,其行为是未定义的。无论命令队列是否仍然在 GPU 上执行,应用在关闭后还可以重置命令队列。已重置的命令队列不再保留任何以往的渲染状态,所以需要再次设置,如 PSO、视区、ScissorRect、RTV 和 DSV。

  在一般情况下,为了避免每个帧同步等待命令队在前一帧中执行,我们可以准备很多备用命令队列,然后在每帧结束时查看之前待定的命令队列。如果最近的命令队列已执行,则表示之前的命令队列也已执行,这是因为命令队列严格按顺序执行,相当于一个 FIFO 队列。为了确定某个命令队列是否已执行,我们需要使用围栏(Fence)的对象。当命令队列调用 ExecuteCommandList 函数时,通过将传递给信号函数的预期值设置成围栏对象,信号函数允许系统立即通知围栏对象何时执行了命令队列。因此在正常情况下,我们将各帧的累计帧号用作预期值,并将它传递给信号函数。查询命令队列命令队列是否完成的方式决定了 GetCompletedValue 的返回值是等于还是大于预期值。

  命令队列(Command Queue)是线程独立的,一个命令队列可以由多个线程执行,命令列表(Command List)是线程不独立的,一个命令列表只由一个线程执行。一个命令队列中可以放置多个命令列表,换句话说,这种设计的意义在于从源头上可以把任务分配到不同的GPU和CPU上,这是对之前DirectX的一个巨大的改进。

(二)、资源绑定

  在了解资源绑定之前,我们必须先了解一个核心概念,即根签名。就资源绑定模型而言,DirectX12 和 DirectX11 之间存在巨大的差异。DirectX11中的资源绑定是固定的。运行时间为每个着色器安排一定量的资源槽孔,并且该应用只需调用相应的接口,以便能够将资源绑定到着色器。在DirectX12中,资源绑定过程非常灵活,并且不限制绑定资源的方式以及所绑定资源的数量,可以随意设置资源绑定风格。绑定资源最常用的方法是描述符表和根描述符。描述符表方法有点复杂,这是因为它提前将一组资源的描述符放在一个描述符堆中,这样,当绘制调用需要引用这些资源时,需对其进行初步处理即可。着色器会根据此项处理发现所有后续的描述符。这是一种指针类数组,这意味着着色器需要做第二个寻址以定位最终的资源。而根描述符的优势在于,不是提前将描述符放在描述符堆中,而是将资源的 GPU 地址设置到命令列表中,这就相当于在命令列表中动态地构建一个描述符,这样,只需一个寻址操作,着色器就可以定位资源。然而,根描述符消耗的参数空间是描述符表的两倍。由于根签名的最大尺寸有限,所以根描述符和描述符表之间的合理比例安排非常重要。

  在正常情况下,我们把 SRV 和 UAV 放在描述符表中(而采样器只可存在于描述符表中),但将 CBV 放在根描述符中。因为 CBV 消耗的大部分资源是动态的,它的地址变化频繁,所以使用描述符表会导致组合激增。不仅内存的占用量会急剧增加,而且管理起来也很麻烦。相比之下,采样器、SRV 和 UAV 组合的变化也比 CBV 少得多,尤其是采样器。只要上层渲染逻辑设计正确,采样器组合的数量可以小于 128。因此,直接将其放到描述符堆中更合适。此处,为了重复利用描述符堆中的描述符组合,我们必须使用 PSO 类对象管理技术,以便首先对每个采样器、SRV 和 UAV 编号,然后根据着色器的需求,将其放在一起,并生成用于创建和索引描述符堆中的描述符组合的唯一散列值。由于在着色器中使用的采样器的最大数量是 16,所以,每个采样器组合可被放置在每单元 16 的跨度。SRV 和 UAV 也可以使用采样器的方法进行管理,所以,对其进行引用的着色器的上限最好也是 16。当然,可变组合跨度单元也是一种选择,但是不方便跨框架对其重复利用,这是因为由 SRV 指向的纹理被释放时,它的序号将被该应用回收,并且所有引用它的描述符组合将被标记为“删除”。此时,如果描述符堆中的组合块在大小方面出现变化并且不连续,则它们会难以重新分配到内存池碎片,除非花费大量的时间进行反碎片努力。出于这个原因,对于描述符组合,折衷的解决方法是使用固定长度的跨度。

  在命令列表中最多只能设置两个描述符堆,每类描述符堆分别一个。“采样器”以及“SRV / UAV / CBV”属于两类不同的描述符堆,并且不能进行混合。

  当我们需要重写描述符堆的描述符时,我们可以先在 CPU 可见的描述符堆中完成更新,然后通过 CopyDescriptors* 命令将堆的内容复制到 GPU 可见的描述符堆。这种资源绑定方式比DirectX11要高效得多,特别是使用根描述符,在编译阶段就可以对资源绑定情况进行检查,这样在运行时可以不用再频繁的进行资源检查,得以大辐提升性能。

(三)、资源屏障

  这是一个新概念。在 DirectX12 之前,资源状态管理由驱动程序完成。现在,DirectX12 将其从驱动程序层剥离出来,并让应用控制何时以及如何处理它。

  存在三类不同的资源屏障。最常见的类型是转换,主要用来切换资源的状态。当资源的应用场景发生变化时,我们将先放置相应的资源壁垒,然后再使用该资源。

  在实践中,非常常见的转换屏障是在 ShaderResourceView 和 RenderTargetView 之间来回切换的资源。因此,我们需要在资源的包装类中添加一个成员变量来记录当前的资源状态。当上层逻辑调用 OMSetRenderTargets 时,我们应该首先确认当前的状态是否为 RenderTargetView,如果不是则放置一个屏障。其 StateBefore 填充了存储在成员变量中的状态值,而其 StateAfter 则填充了 RenderTargetView 状态。如果渲染逻辑调用 XXSetShaderResources,那么,我们继续按照上述流程执行类似的操作,除了 StateAfter 应填入 ShaderResourceView 状态。

  资源屏障的引入,最大的优势在于其由应用去控制资源而不是由驱动来管理资源,因为由驱动来管理资源,驱动需要跟踪资源的状态,从而带来不必要的同步,因为它会阻止后续命令的执行,这会极大的消耗性能,而现在由应用来管理,应用知道管理的状态,也知道何时转换状态,这对硬件性能利用上起来一个非常有效的帮助。

(四)、管线状态对象

  管线状态对象是 DirectX12 的核心概念。它包含 Shader、RasterizerState、BlendState、DepthStencilState、InputLayout 和其他数据。一旦 PSO 对象被传送到系统,这些与 PSO 相关联的状态将在同一时间进行设置。以前在 DirectX11 的接口层,这些渲染参数使用不同的 API 进行设置。为了完成适应阶段,我们必须使用可查询的运行时间容器对其进行管理。最常见的对象容器是 HashMap,它可用于避免多余的 PSO 和对应的 API 调用。在使用 HashMap 之前,我们必须先准备资源 ID,第一件事就是资源的内存地址。它在整个应用生命周期具有全局唯一性,但它存在一个缺点,就是占用了太多的内存空间:尤其是 64 位系统上的 8 个字节。实证分析表明,大多数应用不使用如此数额巨大的对象,因此,我们可以采用序号方式减少表示资源对象的空间,也就是说,使用一个单调递增的整数值来表示一个资源对象。相同的整数还可以表示不同的资源,只要这些资源是不同类型的。例如,RasterizerState 和 BlendState 可以使用不同的资源计数器。这种管理方法的一个重要优势是,它让资源的编码空间更紧凑且易于生成较短的散列值。否则,如果使用拼接内存地址后生成的散列值,则由散列值占用的内存字节数将很大,这不仅会影响 PSO 的存储,而且也会影响查询速度。需要在实践中找到为计数器定义的上限。不同的项目可能存在很大的差异,但我们可以先在测试中使用一个较大值,并在分配序号的位置添加断言。一旦它超过上限,则系统会触发警报。

  为了进一步减少 PSO 实例的数量,当生成 RasterizerState、BlendState 和 DepthStencilState 时,我们需要观察它们之间的状态依赖性。例如,当我们禁用 DepthStencilState 中的深度测试时,可以忽略 RasterizerState 中的深度偏移设置。为了避免产生多余的对象,在这些情况下,我们使用默认值。

  RTV 和 DSV 也与 PSO 相关。由于 DSV 可以控制是否读取和写入深度图中的“深度”或“模版”,所以当启用深度测试且禁用深度写入时,需要在系统中设置只读 DSV。DSV 存在三种只读模式:1) 深度只读 2) 模板只读 3) 深度及模板只读。此外,PSO 还需要 RTV 和 DSV 的格式信息,所以,最好将 OMSetRenderTargets 操作推迟到设置 PSO 的时间。

  ScissorEnable 属性已从 RasterizerState 删除。在硬件端,Scissor 测试将处于永远在线状态。因此,如果应用需要禁用 Scissor 测试,则应设置 ScissorRect 的宽度和高度,以匹配视区或设置为硬件所允许的最大分辨率,如 16K。primative主要拓扑类型需要在 PSO 中设置,其中包括点、线、三角形和补丁。当调用 IASetPrimitiveTopology 直接将其转换为上述主要拓扑类型时,我们可以使用预建的转换表。PSO 的 HashMap 也可根据主要拓扑类型进行分类,其中,每种拓扑类型对应一个 HashMap,这样,它就将使用数组下标直接定位。

  管线状态对象的一大优势是其可以在初始化阶段设置。一个管线状态对象设置完,则在同一时间载入到GPU中去以便通知GPU做好相应准备,而不是像以前DirectX11中使用的分离设置,GPU需要等待到所有状态设置完再进行状态更新。

   应该来说,DirectX12整个设计过程中无不贯穿这种“性能优先”的设计理念,包括GPU与CPU同步、架构的重建、资源绑定、根描述符、PSO使用、命令队列等等等,这种“性能优先”设计带来高大的改变就是GPU/CPU利用率的提高,这在当今CPU/GPU多核化,追求模型逼真,追求效果震撼的时代,这种设计无疑是正确而且是具有前瞻性的。i

二、Introduction to 3D Game Programming with Diectx12一书的成功之处

  Introduction to 3D Game Programming with Diectx12是luna写的DirectX学习资料系列的最新版本,就本书而言,很多章节还是在原章节上改的,但因为DirectX12与DirectX11的巨大差异,代码基本都进行了更新重写。应该说本书作者在对DirectX渲染管线的理解上还是很深入的,同时,作者语言表达能力也很好,能把比较深奥的内容用通俗易懂的方式加以描述而不是故弄玄虚用专业术语罗列。在本书的编排上,先进行基本的数学知识介绍,为方便以门,然后就技术点对DirectX12进行分块讨论,最后再对一些常用技能或应用进行讲解。符合学习的一般逻辑,也有很强的系统性。从原理到实践、从数学到物理、从渲染管线到常见技术思路都进行了比较深入的讲解,是难得的DirectX学习资料。建议希望了解图形渲染底层技术的读者学习了解一下。

三、对未来行业的影响

   从国内的行业特点来说,我们研究底层技术的真的不算太多,使用软件、依赖插件的思想真的很严重,不能说这个一定是坏事,但从长远来看,这种功利思想显得极为短视。这样做的结果是跟着跑但永远也跑不到别人前面去。
  近期,微软对DirectX12进行了一项大的更新DXR,DXR技术要在当前的GPU硬件上实时运行,则需要有性能非常良好的底层架构,这在之前可能是没法想象的。
  从目前现状来看,出现一个有意思的现象,目前绝大部分用户的电脑都采用了多核心CPU/GPU,但之前的图形硬件API不能充分利用多核心硬件资源,虽然用户计算资源充裕,但渲染性能却一直提不上去。因此,无论是AMD,NVIDIA,微软,OPENGL,大家都在想办法提升对多核资源的利用效能。从行业发展的角度来看,旧版Directx API已经不能堪当大任了,Directx12或者在DirectX12基础上发展起来的新型图形硬件API必将取代之,因为它代表着未来!

参考文献

1、将应用迁移到 DirectX12 链接

猜你喜欢

转载自blog.csdn.net/yolon3000/article/details/80878663