[翻譯]A trip through the Graphics Pipeline 2011, part 3
來自專欄 Dirty Game Engine
先解釋一些名詞:
IA-Input Assembler 讀取索引和頂點數據
VS-Vertex Shader 獲取頂點數據,處理完之後交給下一個stage
PA-Primitive Assembly 獲取組成一個面元的頂點
HS-Hull Shader 接收patch面元,計算變換過後的patch控制點,作為domain shader的輸入,外加一些控制tessellation的信息
TS-Tessellation Stage 創建頂點以及細分後的面元的連通性
DS-Domain Shader 獲取HS輸出的控制頂點、TS輸出的細分位置,把它們再次變為頂點
GS-Geometry Shader 輸入面元(也許還有鄰接信息),輸出更多面元,而且也可以直接輸出到buffer
SO-Stream-Out 寫出GS到buffer
RS-Rasterizer 光柵化
PS-Pixel Shader 獲取插值後的頂點數據,輸出像素顏色。也可以直接輸出都UAV
OM-Output Merger 獲取PS輸出的顏色信息,做alpha blend並寫回到RT
CS-Compute Shader 自己獨自的計算管線,輸入時constant buffer+thread id,輸出到buffer或者UAV
數據流:
VS->PS 老的D3D9渲染管線
VS->GS->PS D3D10渲染管線
VS->HS->TS->DS->PS,VS->HS->TS->DS->GS->PS D3D11細分管線
VS->SO,VS->GS->SO,VS->HS->TS->DS->GS->SO D3D11(w/o細分)Stream-out
CS D3D11 GPGPU Compute
IA Stage
首先是從ib中load索引數據,如果渲染的mesh是沒有ib的數據,那麼會使用統一的索引分配(0 1 2 3 4 …)。如果有ib,那麼會通過cache來讀取索引以及頂點。另外,ib的讀取會做bound check,比如你調用DrawIndexed使用了IndexCount=6的參數,但實際上index buffer只有5個elements,那麼超出範圍的索引返回值0。所以當你DrawIndexed並設置ib為NULL時,所有返回的索引都是0,這在D3D10之前是UB的。
一旦有了index,我們就需要讀取vertex和instance的數據了(instanceID就是一個很直接的counter)簡單來說就是根據declaration layout,從cache/memory中讀取並unpack到shader input需要的float格式。但是實際上並沒有直接去讀取,而是判斷這個index索引的頂點是否已經處理好在cache中,因為相同的頂點可能會被多個三角形索引,沒必要一遍又一遍的運行vs計算。
Vertex Caching and Shading
注意,這段基本上都是作者根據已有的結果進行的猜測,不保證一定正確。
在SM3.0時代的GPU,VS和PS都是獨立的硬體單元,所以vertex cache很簡單,就是大概十幾二十個左右頂點容量的FIFO隊列。之後統一的架構出現了,因為兩套shader的用途不同,所以設計折中了許多。一方面vs大概每幀會處理100w個頂點數據,另一方面ps每幀至少填充1900*1200(230w)個像素,那麼哪個會是壓垮cache的最後一根稻草?
老的vs單元一次處理一個vertex,新的統一架構需要更高的吞吐量而不在意延遲,所以vertex都需要batch在一起進行大規模處理(大概一個batch有16-64個vertices)。那麼在處理一批次vs之前會有16-64個cache miss,而且FIFO隊列對於這種批處理來說也不適合。舉個例子,比如在隊尾加入一個batch(我們假設一個batch有32個vertices),那麼這也意味著隊首的32個vertices需要出隊了-但是這32個出隊的vertices有可能包含我們正要進行vs處理的頂點(意味著此頂點vs的結果有cache hit),當我們根據hit去查詢vertex的結果時,它已經出隊了,這就行不通了。所以我們需要多大的FIFO隊列?如果每批次處理32個頂點,那麼最少隊列長度32個,但是這樣我們就沒法復用之前計算完成的32個頂點了(它們會被擠出隊列),所以每次處理新的批次時,隊列總是空的。所以需要更大的隊列,比如64個entries?這太大了,因為vertex cache lookup需要對比FIFO中所有的tag標記,這是並行的又特別費電,需要好好設計一個全相連的cache映射機制。另外,在vs處理頂點時,我們又不想什麼都不做只是傻等,所以這時再並行的load一個batch?這就需要FIFO至少有64個entries。而且我們還有這麼多shader cores可以並行處理vs,根據Amdahl定律FIFO很明顯的成為了並行系統中的串列瓶頸。
整套FIFO隊列真的不能適應新的環境了,所以丟棄掉吧。我們想要一個可以批處理頂點的方法,而且盡量減少vs的重複進入。解決方法很簡單:保留給32個vertices(1 batch)buffer足夠的空間,同樣保存32個cache tag array的entries。開始時候cache都是空的,對於每個index buffer中primitive的index,在cache中查一遍,如果有命中那說明能省一個vs計算,如果沒有命中那麼把vertex加入batch並把ib的索引加入cache tag array。等batch的空間都滿了,dispatch這個batch進行vs計算,並保存cache tag array(為了計算完畢查索引)。然後開始新的一個batch,並使用空的cache,保證每個batch都是相互獨立沒有依賴。
每個batch會使shader保持一段時間的忙碌(也許就是幾百個cycles),因為我們有一堆shader unit,所以可以對每個batch使用不同的shader unit,最終我們會得到結果,這是我們可以根據保存的cache tags以及ib數據來組裝primitive(這就是primitive assembly部分,下次涉及)
Shader Unit internals
這一塊資料很多,可以具體的看NVIDIA或者AMD的文檔。ALU的基礎是FMAC(Floating Multiply-Accumulate, 浮點乘加)單元,以及一些為了高吞吐率設計的倒數,倒數開平方,log2,exp2,sin,cos的硬體計算單元,它運行大量的線程來抵消延遲,每個線程只擁有少量的register(因為運行的線程實在太多了),善於運行直線式的代碼,對於branch不是太在行(尤其是它們之間沒有一致性)
比較有趣的是每個shader stage之間的不同,它們之間的不同只有很少的部分,所有的算術和邏輯指令在不同stage中都一樣,只有一些指令(比如ps的求導、插值的屬性)在特定的stage才有,大部分的不同只有input和ouput的數據類型。Texture sampling是一個很大的主題值得我們下次討論。
推薦閱讀:
※[DOD Series][GCAP09] Pitfalls of Object Oriented Programming
※從零開始手敲次世代遊戲引擎(四十九)
※[DOD Series][OGRE] Pitfalls and Design proposal for Ogre 2.0
※Dirty Game Engine
※[翻譯]DOOM(2016) - Graphic Study
TAG:遊戲引擎 |