1. 前言
D3D12伴随DirectX12自2014年正式发布以来已经近3年多时间了。遗憾的是我最近才有时间仔细研究D3D12接口及编程方面的内容。D3D12给我总体的感觉用一句话来概括就是——D3D12是一个“显卡操作系统!”。
得益于我对Windows内核编程的深入了解和掌握,突然发现掌握起D3D12多线程渲染时居然可以无障碍学习,看来并不是学过的东西都会过时,这也是让我暗自窃喜的地方。正所谓“众里寻他千百度,蓦然回首,那人却在灯火阑珊处!”
对比自D3D11中添加了对多线程渲染的支持以后,D3D12更是将多线程渲染支持发挥到了极致。如果说D3D11中对多线程渲染的支持还只是停留在“小荷才露尖尖角”的话,那么D3D12中的多线程渲染则是一番“接天莲叶无穷碧,映日荷花别样红”的盛况了。
本文就将从概念、编程原理、编程框架方面解刨一下D3D的多线程渲染技术。因本人知识水平有限,同时期待您的阅读与批评指正!
2. D3D11多线程渲染基本原理
在D3D11中,对于多线程渲染的支持主要可以概括为几个“小点”:
首先利用ID3D11Device::CreateDeferredContext方法创建一个(或多个)延迟渲染(Deferred Device Context)的设备上下文接口ID3D11DeviceContext;
接着利用这个Deferred Device Context接口调用诸如:IASetPrimitiveTopology、IASetInputLayout、RSSetState、OMSetBlendState 、OMSetDepthStencilState、DrawIndexed、DrawIndexedInstanced(最后两个为Draw Call)等等方法(可以统称为命令)像往常一样进行渲染调用;
所有的渲染调用都结束后,接着调用ID3D11DeviceContext::FinishCommandList方法,得到一个ID3D11CommandList接口;
最终通过即时设备接口(Immediate Device Context)的ID3D11DeviceContext::ExecuteCommandList方法执行这个Command List;
当所有的Command List都Execute完成之后,就可以调用IDXGISwapChain::Present方法呈现最终渲染画面了。
其中最(ling)最(ren)吸(jing)引(tan)人的就是Deferred Device Context的ID3D11DeviceContext接口可以有多个,并且每一个可以在不同的CPU线程(Windows线程)中分别记录命令(Command),然后提交给Immediate Device Context所对应的CPU线程(Windows线程)进行Execute(MSDN中称之为Queues commands,这不是巧合!),然后也在同样的线程中调用Present即可。这也就是D3D11多线程渲染的全部核心奥秘了。
当然这不是全貌,只是一个示意性的核心原理说明,但至少说明使用D3D11加入多线程渲染在编程原理和具体实现上其实并不复杂。在D3D11中命令列表中的命令(其实主要是CPU发送给GPU执行的命令)是被快速记录下来,而不是立即执行的(包括那些可怕的被称之为Draw Call的性能杀手,当然一定要记得升级你的显卡驱动程序),直到你调用ExecuteCommandList方法(调用即返回,不等待)才被GPU真正的执行,此时那些使用延迟渲染设备接口的CPU线程以及主渲染线程(不一定是进程的主线程,此处是指调用Immediate Device Context::ExecuteCommandList方法的线程)又可以去干别的事情了,比如继续下一帧的输入变换、碰撞检测、物理变换、动画矩阵调色板准备、光照准备等等,从而为记录形成新的命令列表做准备。而此时GPU就忙碌的开始执行渲染命令了。这也就是延迟渲染设备名字的真正含义。最终这就形成了CPU和GPU同时都在忙碌的高效渲染效果。
而在拥有D3D11多线程渲染之前,CPU和GPU的工作就好像两个人打台球一样,一个击球时,另一个只能在旁边观望(CPU线程在Draw Call上等待GPU完成渲染)。如果你对游戏编程了解深刻的话,你就会明白,让任何硬件设备闲置等待另一个设备工作完成都是严重的犯罪(我们不怕费电)!而D3D11引入的多线程渲染,不但让CPU和GPU可以同时处于忙碌状态,更让现代多核CPU及多GPU(前提是你要很有钱!)并行执行任务的能力得到根本上的解放。由此也可以看出来多线程渲染也是为解放生产力而生!
3. D3D12多线程渲染基本原理
而在D3D12中多线程渲染与D3D11中是异曲同工的:
首先在D3D12中利用命令队列(Command Queue 接口:ID3D12CommandQueue)代替了ID3D11DeviceContext接口,更准确的说是代替了Immediate Device Context的ID3D11DeviceContext接口(名字差别好大的样子…注意前面的小暗示),在D3D12中不论什么队列(Queues)都需要自己创建,而不是像D3D11中Immediate Device Context伴随ID3D11Device一同被创建。
在D3D12中Command Queue被进一步细分为D3D12_COMMAND_LIST_TYPE_DIRECT(直接命令队列), D3D12_COMMAND_LIST_TYPE_BUNDLE(捆绑包), D3D12_COMMAND_LIST_TYPE_COMPUTE(计算命令队列),D3D12_COMMAND_LIST_TYPE_COPY(复制命令队列), D3D12_COMMAND_LIST_TYPE_VIDEO_DECODE(视频解码命令队列), D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS(视频处理命令队列)。
不论什么命令队列,它们本质上就是用来执行命令列表的,相当于D3D11中的Immediate Device Context,同样也是调用ExecuteCommandLists方法来执行命令列表。因此从其丰富的种类就可以感受到D3D12中命令队列本身就已经开始大大扩展了。
在D3D12中也有与D3D11中相类似的命令列表的概念,具体是用ID3D12GraphicsCommandList接口来表达,它就相当于D3D11中的Deferred Device Context。当然其内涵比在D3D11中要丰富的多,在D3D11中记录命令列表是由Deferred Device Context代俎越庖的,也就是我们按逻辑调用一堆ID3D11DeviceContext的方法,结束的时候使用FinishCommandList方法得到一个命令队列的接口ID3D11CommandList,而这个接口几乎没什么方法,仅仅是个“概念标志物”,或者直白的说就是仅仅代表GPU上的一个命令队列而已,其自身并没有什么方法可供调用。而在D3D12中ID3D12GraphicsCommandList却包含了几乎所有的可供放入命令队列中的命令方法(几乎全部是渲染相关的方法),并且在D3D12中命令队列本身是被创建的,使用的是ID3D12Device::CreateCommandList方法,有了这个接口以后你就可以在对应的其它CPU线程中按照渲染逻辑调用ID3D12GraphicsCommandList接口的方法生成一个命令列表(Command List)了,通常这个过程被称作“记录(或录制)”一个命令列表。同样录制只是说记录了你调用的顺序和使用的资源(CPU从内存传入显存),而不是立即执行这些方法,这些方法都会快速返回,因为是CPU调用这些方法,这个过程可以想象为你到餐馆去点菜,并不是你点一个菜,就做一个菜上一个菜再点下一个,而是生成一个菜单(Command Lists),统一提交到厨房(Queues Commands & Execute)。对应于不同的命令队列,命令列表也分为很多种类,基本上就是有多少种命令队列,就有多少种命令列表。
最后在命令列表(Command Lists)记录完成后,与D3D11中相同,提交到对应的命令队列(Command Queues)上去执行(ExecuteCommandLists)即可。
在命令队列执行命令列表的对应关系上,D3D12中基本的原则就是直接命令队列几乎可以执行所有种类的命令列表,而其它的命令队列只能执行对应种类的命令列表,如复制命令队列原则上只执行复制命令列表。
最终当所有的命令列表都执行结束后,主渲染线程(这时往往指的就是运行直接命令队列的CPU线程了)再调用Present方法将最终画面呈现出来即可。
需要注意的就是在D3D12中最终的Execute Command Lists操作也是立即返回,此时CPU线程可以进行其它的操作,而GPU就同时忙着执行各种渲染命令了,只有到所有都结束后才调用Present。当然这很好理解,在所有渲染没有完成之前,后台缓冲区里还不是我们想要的最终画面。
由此可以看出,至少从原理上来说D3D12与D3D11多线程渲染框架基本是一致的。都是通过在不同的CPU线程中录制命令列表(Command Lists),最后再统一执行(Execute)的方式完成多线程渲染。并且都从根本上屏蔽了令人发指的Draw Call同步调用,而改为CPU和GPU完全异步执行的方式(并行!),从而在整体渲染效率和性能上获得巨大的提升。
当然如果你看到这里就觉得原来D3D12与D3D11在多线程渲染上没多大区别,或者D3D12相对于D3D11没多大改进的话,甚至认为我说D3D12是一个显卡操作系统有些太夸张了的话,那么我只能说你图样图森破了!当然你能理解到这一步,也已经非常不错了,至少说明你不但明白了D3D11多线程渲染是怎么回事,同时对D3D12的多线程渲染框架也有了一个初步的认识。
接下来就让我们更详细的了解下D3D12多线程渲染的一些更深入的内容(注意还不是细节,本文中我不打算写过多细节,那样这篇文章会被写成一本书的,考虑到我还要养家糊口,所以你懂的!)
暂无评论内容