接下来让我们更进一步,再来设想一个更复杂的场景,还是之前那个一个线程(CPU线程)+命令列表(GPU线程)渲染一个正方体,另一个线程+命令列表渲染一个球体的例子,当然我们还要加入一个线程+命令队列渲染一个平面,而球体和正方体都放在这个平面上(看起来有点像素描,原谅我很懒没有配任何图像),只不过在这里我们加入了阴影效果(求此时你看懂这个问题时的心理阴影面积!)。如果你是一个资深的3D开发人员的话,至少知道这个地方需要的就是我们通常所说的多趟渲染(Pass),第一趟我们需要关掉渲染目标,只记录深度缓冲和模板值,第二趟我们打开渲染目标,根据之前记录的深度缓冲和模板值来渲染整个场景,这样我们就可以得到带有阴影效果的场景值。
正如刚才所说,这三个命令队列是并行记录的,然后统一提交执行的,那么问题来了,如何保证这些命令队列之间的先后顺序呢?或者说怎样保证三个独立命令队列都先进行第一趟渲染得到阴影图,再去执行第二趟中带阴影的渲染呢?希望我将这个问题描述清楚了,当然更希望聪明的你也已经从我蹩脚的文字描述中理解了这个问题(就是在你的脑子中已经完成了类似GPU线程所做的渲染工作,并得到了最终图像)。
乍看起来,这个场景好像本身是没法多线程渲染的,复杂的遮挡关系就打破了数据并行计算的条件,因为计算某一个物体上一点处的像素颜色时,可能就需要知道有没有其它物体遮挡了它,从而使它在另一物体的阴影中,而这个遮挡它的物体完全可能在另一个线程中渲染。
要解决这个问题其实我们需要做的仅仅是将这个多趟渲染对应到多个命令列表上去(此例中是两趟渲染,two pass),在我们这个例子场景中,我们就为每个CPU线程设定两个命令列表,每一个CPU线程的第一个命令列表记录第一趟渲染阴影图的命令,并设置一个围栏值,然后先提交到对应的命令队列(GPU线程)上执行一下, CPU的主渲染线程就在这个围栏值上等待(因为是多个线程在渲染不同物体的阴影,所以需要同步等待多个围栏值的Event,希望你知道还有个WaitForMultipleObjects),然后各CPU线程再利用第二个命令列表录制第二趟渲染得到带阴影的整个场景图。当然在启动每个线程的第二趟渲染的命令列表之前CPU的主渲染线程需要等待所有的GPU线程(命令队列)都执行完了第一趟的渲染,再启动代表第二个趟渲染的命令列表的执行,这样就最终确保了所有的阴影图都正确的渲染显示出来。
这样也就是说多趟渲染也完全适用于多线程渲染框架,无非就是在第一趟渲染命令记录结束时,插入一个“围栏”,然后录制第二趟的命令列表,最终在执行时,CPU线程先命令GPU执行第一趟的命令列表,然后在围栏上等待,当所有的GPU线程说第一趟的命令列表执行完毕了(Signal),CPU线程再命令他们开始执行第二趟的命令列表。
我想如果你能够耐心的看到这里,并且大体明白了我所介绍的所有截止此处之前的所有D3D12多线程渲染的内容的话,那么请思考一下D3D12多线程渲染针对D3D11多线程渲染所扩展的内容究竟有些什么让我们惊奇的地方?其实细想过来并不多,无非就是增加了显存管理、丰富的命令队列(GPU线程)、以及围栏和资源屏障这类同步对象(其实D3D11中也有简化的对等物)。当然如果你都弄明白了的话,其实这并不令人惊叹,说白了这一切都是为了让那个令人发指的Draw Call命令变成异步的而已!
当然D3D12带来的改变不仅仅是这么简单而已,它最最让人可以惊叹一下的就是可以通过纯软件编程的方式来操作多个不同的独立GPU协同工作,也可以称之为多显卡渲染(至少我是这么命名这种能力的),甚至你都可以不需要STL排线或CrossFire排线了,也可以操控多个GPU(显卡),或者甚至是不同结构、不同GPU核的多个显卡。我觉得这种能力才是彻底的解放了硬件的生产力。
因为就拿我写这系列文章的笔记本电脑来说,它其中就有一个独立的GTX965M显卡和一个集成在CPU之内的HD530显卡。在拥有D3D12多显卡渲染之前,这俩货在我的电脑里同时只能有一个在工作,最常见的情况就是我在用Word写文档的时候,是HD530在工作,而GTX965M则休眠,而我在进行游戏的时候,则是GTX965M在工作,而HD530则处于休眠状态。虽然就渲染能力上来说HD530甚至连入门级显卡都比不上,但终究它还是能够执行Shader进行Shader Model5.0级别的渲染的。而遗憾的就是它们都只能各自独立的工作。
自从我安装了最新版的Windows10系统并升级了DirectX12运行时库,及对应的Windows SDK后,我就可以通过编程的方式让这两款显卡同时工作了。而让我感到非常兴奋的是,其中一块来自Nvidia,而另一块则来自Intel的集成显卡。这样的情况在拥有D3D12多显卡渲染以前是无论如何都没法轻易做到的。小小的遗憾就是目前还没有多少支持D3D12多显卡渲染的游戏,让我来切身感受一下这种多显卡加速所带来的效果增强。当然对于如我这样的程序员来说,这都不是事!自己写程序看效果就行了!正所谓“自己动手丰衣足食”!当然我想在我写完了这系列文章之后,D3D12多显卡多线程渲染的游戏应该会如同雨后春笋般诞生了!那时的情形用一句话来形容就是“待到山花烂漫时,它在丛中笑。”,当然但愿彼时我已经去研究和普及D3D13以及D3D14了!
从现实的编程角度来说,如果你认为在D3D12以前其实也可以利用在不同的显卡适配器上创建不同的Device接口,然后再利用多个Device分别渲染来实现的话,我只能提醒你注意一个问题,你的渲染目标究竟应该在哪个设备上呢?或者多个渲染目标(以及对应的深度缓冲和蜡板(Stencil他们叫模板,而我更喜欢叫蜡板,因为在我上小学初中的时候,老师们都是用蜡板刻卷子,并用油墨来印刷的,原理上跟我们使用的Stencil很相似的,并且Stencil本身也有蜡板的意思))应该如何去“合并”成最终的结果呢?所以没有D3D12之前,要做到多显卡渲染是没那么容易的。
在D3D12中,最终通过共享句柄的办法最终解决了多显卡之间的资源共享问题,通过复制命令队列的方式启用独立的GPU线程完成在不同显卡之间的资源复制的工作,另外可以通过ID3D12Device::CheckFeatureSupport方法返回的
D3D12_FEATURE_DATA_D3D12_OPTIONS. CrossAdapterRowMajorTextureSupported字段来判定显卡是否支持交叉型资源,其实就是一个资源横跨多个显卡之间的显存。这种能力本身就是为多显卡协同工作而生。因为现代的计算机体系结构都是冯诺依曼体系架构的,核心就是围绕内存(存储)的系统架构,因此如果能够共用内存(存储)则充分说明两个系统间可以进行几乎无缝的协同工作。
同时在D3D12中还有很多结构体都具有NodeMask参数,如果你懂的CPU线程的亲缘性的话,这个参数是很好理解的,它是一个32位的标志值,在一个有多个显卡的系统中,每一位对应一个序号的显卡,比如第一位1b(二进制)代表第一块显卡,10b(二进制)代表第二块显卡,100b(二进制)代表第三块显卡等,当然你可以组合11b表示同时使用第一第二块显卡。而在单显卡系统中只需要为此字段设置0值即可。
当然D3D12中的多显卡编程还是具有挑战性的,这里就不再多赘述了,之后的文章中我会更详细的以编程的方式介绍这块内容。最终大家只要知道一旦渲染目标可以得到统一,那么实际上多显卡支持就变成了可能,剩下的就是看你如何具体编程来控制多块显卡以何种方式协同工作了(可能的方式有每个显卡负责渲染一帧,交替进行,或者一个显卡负责资源复制、另一个负责渲染、或者一个负责执行计算命令队列和复制,另一个负责渲染等等),总之D3D12在多显卡协同渲染方面给了我们无限的可能!
时间马上就要到2018年了,在搞明白了D3D12中的多线程多显卡渲染架构之后,我是非常激动的。展望未来,我们可以想象将会有这样一款高性能高画质的实时渲染引擎——它拥有异构多显卡、多线程渲染支持,并且充分利用了系统提供的线程池,以及各种轻量级锁或Lock-Free原子锁,它几乎让系统中所有的硬件(除了CPU和GPU外还包括输入设备、声卡、网卡、硬盘、显示器等等)都在以最高性能的方式并行协同工作,为HDR及高分辨率显示器或者VR、AR设备提供着实时电影级镜头画质的逼真画面,人们完全沉浸在美好的幻想和现实超统一的世界中进行游戏,甚至进行着教学、学习、工作、设计、交流等等活动,获得着普通现实很难给予的满足感、成就感、幸福感、愉悦感等等,从而让灵魂得到前所未有的升华!我们甚至可以想象,在人类还没有真正登上火星之前,你已经可以通过VR设备真实体验火星的环境;在人类无法登陆的太阳表面,通过VR超近距离研究太阳黑子活动......
而这一切都需要每一个游戏引擎开发人员继续不懈的努力!
(全文完,谢谢您的阅读!)