1. 程式人生 > >D3D11和D3D12多執行緒渲染框架的比較(一)

D3D11和D3D12多執行緒渲染框架的比較(一)

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多執行緒渲染的一些更深入的內容(注意還不是細節,本文中我不打算寫過多細節,那樣這篇文章會被寫成一本書的,考慮到我還要養家餬口,所以你懂的!)