圖形管線之旅 Part3

原文:《A trip through the Graphics Pipeline 2011》
翻譯:往昔之劍
 
轉載請註明出處
 
此時,咱們一路上經過多個驅動層和命令處理器將draw call從應用程序發送過來。最後終於要作圖形處理了。最後一部分,來看一下頂點管線。不過在開始以前…
 
一些名詞
 
咱們如今所在的3D管線依次由若干階段構成,每一個階段都有特殊功能。來給這些將要談到的階段命下名——基本上是按照D3D10/11的命名結構——加上相應的縮寫。咱們將在旅程的最終部分看到他們,可是還須要一些時間才能所有看到——我寫了一個大綱,用一句話總結了每一個階段都作了什麼。
 
  • IA——輸入組合器(Input Assembler)。讀索引和頂點數據。
  • VS——頂點着色器(Vertex Shader)。獲取輸入的頂點數據,寫入下一個階段用到的頂點數據。
  • PA——圖元裝配(Primitive Assimbly)。讀取頂點數據,組裝成圖元繼續傳遞。
  • HS——外殼着色器(Hull Shader)。接收補丁圖元,將變換過的(或者沒變換的)修補控制點寫入域着色器(Domain Shader),加上驅動細分的額外數據。
  • TS——細分階段(Tessellator Stage)。創造頂點並連通細分的直線或三角面。
  • DS——域着色器(Domain Shader)。取出已着色着色的控制點,HS中的額外數據,以及TS中的細分位置,並把他們再次變換成頂點。
  • GS——幾何着色器(Geometry Shader)。輸入圖元,選擇鄰接信息,而後輸出成不一樣的圖元。
  • SO——輸出流(Stream-Out)。將GS輸出的(如變換後的圖元)寫入內存緩衝。
  • RS——光柵器(Rasterizer)。光柵化圖形。
  • PS——像素着色器(Pixel Shader)。將通過插值的頂點數據輸出像素顏色。還能寫入UAV(無序訪問視圖 Unordered Access View)。
  • OM——輸出混合器(Output Merger)。從PS獲得着色後的像素,作半透混合處理並把它們這回後緩衝區。
  • CS——計算着色器(Compute Shader)。本身有一套獨立的管線。輸入只有常量緩衝和線程ID。能夠寫入緩衝和UAV。
 
如今全部的都交待完了,這裏列出了多種數據流程,我來按順序說明一下(這裏不講IA、PA、RS、OM階段,他們和主題無關,他們不對數據作任何處理,僅僅重組數據——就像是粘合劑)
 
  1. VS→PS:歷史悠久的管線。在D3D9時代,這就是所有管線了。目前爲止仍然是常規渲染的重要流程。我從頭到位走一遍,而後再回頭換一種更豐富的流程。
  2. VS→GS→PS:幾何着色(D3D10中新增)
  3. VS→HS→TS→DS→PS,VS→HS→TS→DS→GS→PS:曲面細分(D3D11中新增)
  4. VS→SO,VS→GS→SO,VS→HS→TS→DS→GS→SO:輸出流(有/無 曲面細分)
  5. CS:計算(D3D11中新增)
 
如今你知道接下來要發生什麼了,讓咱們從Vertex Shader開始吧。
 
輸入組合器階段(Input Assembler Stage)
 
這裏發生的第一件事是從Index Buffer中載入索引——若是它是個包含索引的渲染批次。若是不是的話,就當成是序號一致的Index Buffer(0 1 2 3 4……)做爲索引來使用。若是有Index Buffer,它的內容可不是從內存讀取的,IA階段一般經過一個數據緩存來訪問Index/Vertex Buffer。還要注意,讀取到的Index Buffer(實際上,在D3D10以上的全部資源訪問都是這樣)是作了邊界檢查的;若是你引用了原始Index Buffer以外的元素(例如,在只有5個索引的Index Buffer中,執行DrawIndexed函數,IndexCount參數設爲6)全部的越界讀取都將返回0。這麼作(這種特殊狀況)雖然徹底沒用,可是包含了必定意義。一樣,你能夠用一個NULL Index Buffer集合調用DrawIndexed——若是你的Index Buffer長度設爲0,這麼作也是同樣的,全部讀取都算越界,因此也返回0。在D3D10以上,你須要對未定義的東東多作一些處理:)
 
一旦有了索引,咱們就有了須要從頂點數據流中讀取的預處理頂點(pre-vertex)和預處理實例(pre-instance)數據(當前這個階段的實例ID僅僅是一個簡單的計數器)。這很簡單——咱們已經聲明瞭數據佈局(data layout);從緩存/內存中讀取它,而且解包成浮點格式做爲shader內核的輸入數據。然而,讀取不是當即完成的;硬件使用了一個着色頂點的緩存,以致於頂點能夠被多個三角形引用(在常規的閉包mesh中,每一個頂點都被6個三角形引用),就不用每次都重複渲染同一個頂點了——咱們僅引用已經着色後的數據。
 
頂點緩存和着色
 
注意:本段部份內容包含了猜想,都是依據專家給出的關於現代GPU的評論。可是僅告訴了我是什麼,卻沒解釋緣由,全部這塊有一些是推斷的。還有,我僅是猜想了一些細節。就是說,我不知道的就不會在這裏徹底闡述了——我描述的東西都是我認爲靠譜可信的,我還不能保證明際在硬件裏確實這麼實現的,沒準會漏過一些技巧和細節。
 
長久以來(直到shader model 3.0的時代),vertex & pixel shader都使用不一樣的處理單元實現,它們有各自不一樣性能權衡和頂點緩存,是很簡單的東西:通常只是 個包含少許頂點的FIFO,對於最糟糕的輸出屬性也保留了足夠空間,各自標記了頂點和索引。很簡單吧。
 
以後,Unified Shader出現了。若是在兩種類型的Shader中統一處理不一樣事物,這種設計必然要作出妥協讓步。換句話說,Vertex Shader一般一幀可能達到1百萬個頂點,而Pixel Shader在1920x1200分辨率上填充全屏 一幀至少 須要二百三十萬個像素——還會有更多的渲染內容。那麼猜一下那個處理單元會拖後腿?
 
有一個解決辦法:用大量的統一着色單元(unified shader unit)替換掉每次只渲染若干個頂點的舊vertex shader uint來最大化吞吐量,避免延遲,於是就能處理大量批次的渲染工做(有多大?目前這個數貌似是一個批次 處理16~64個着色頂點)。
 
若是不想下降渲染效率,在你執行一次頂點着色負載(vertex shading load)以前,會有16~64次頂點cache miss。可是整個FIFO實際上並非按照這個想法批處理頂點cache miss,且一口氣渲染完他們。由於問題在於:若是你一次性渲染整個批次的頂點,就只能在頂點着色以後才能開始組裝三角形。而此時,你剛剛纔添加了一整個頂點批次(好比這裏是32個)到FIFO的隊尾,就意味着如今有32箇舊的頂點被擠出隊列了——可是這32個頂點中的每一個頂點,可能已經命中了,在當前批次裏咱們正在組裝的三角形的頂點緩存。哦!那就行不通了。很明顯,咱們實際上不能在FIFO裏統計32箇舊的頂點做爲頂點緩存命中(vertex cache hits),由於正在引用的頂點已經不在了!那咱們該須要多大的FIFO?若是咱們正在渲染的一個批次裏有32個頂點,就至少 須要32個條目大的空間,可是由於咱們不能使用32箇舊的條目(由於咱們要移出他們),意味着實際上每一個批次都是用的一個空的FIFO。那就讓它大一點,64個條目呢?至關大了吧。注意,每次頂點緩存查找要涉及到比較全部FIFO中的標記(頂點序號)——這徹底是並行的,但也很耗電;咱們在這裏用一個徹底關聯緩存來高效率實現它。還有,在派發執行32個頂點着色負載和收到結果之間裏作什麼呢——只能是等待嗎?着色要花費幾百個cycle,等待可不是個好主意!或許應該同事有兩個着色負載,並行執行?可是如今咱們的FIFO須要至少64個條目長度,而且咱們不能統計上次的64個條目做爲頂點命中,由於當咱們收到結果的時候,他們都將被移出隊列。而且,一個FIFO對應大量的shader內核嗎?Amdahl定律——將一系列徹底串行化的份量 (不能並行化)加入到管線裏,必然會產生性能瓶頸。
 
整個FIFO真的不適合這個環境,因此,好吧,咱們只能拋棄他了。回到畫板上來。咱們實際想要作什麼?拿到一個大小合適的頂點批次來渲染,而且不渲染沒必要要的頂點。
 
那麼,好吧,簡單點:爲32個頂點(1個批次)保留足夠大的緩存空間,而且一樣留出32個條目的標記的緩存空間。從一個空的緩衝開始,例如全部條目都是非法的。對於index buffer中的每一個圖元,從全部的頂點中查找一次;若是他命中緩存,那最好了。若是沒命中,在當前的批次裏分配一個插槽而且添加一個新的索引到 緩存標記數組(the cache tag array)裏。當咱們沒有足夠剩餘空間再添加新的圖元時,派發所有的頂點着色批次,保存緩存標記數組(例如剛剛着色過得32個頂點索引),而且再次從一個空的緩存開始設置下一個批次——確保渲染批次都是徹底獨立的。
 
每一個批次都將佔用shader unit一段時間(可能至少幾百cycles!)。可是這不會有問題,由於咱們有足夠的shader uint——僅須要選擇一個不一樣的shader unit來執行每一個批次!咱們最終可以高效並行的獲得返回結果。在這點上咱們可使用保存的緩存標記和袁術index buffer數據來組裝圖元,發送到管線裏(這就是我後面部分要講到的「圖元組裝」的概念)。
 
順便說下,我剛纔說的「獲得返回結果」,是什麼意思呢?他們在哪結束的? 主要有兩個選項:1. 特定的緩存裏 或 2. 一些通用的緩存/臨時內存。過去通常都用選項1,在 頂點數據周圍用一個固定組織結構設計的緩存(每一個頂點有16個float4向量的屬性空間,之類的等等),可是後來GPU開始朝着選項2發展,僅僅是內存。這很靈活,一個重要的好處是你 能夠在別的shader階段也使用這個內存,然而舉例來講,特定的頂點緩存對於像素着色或者計算管理是沒什麼用的。
 
目前爲止所描述的頂點着色數據流程 
 
Shader Unit內部
 
簡而言之:這就是你想要看到的HLSL編譯器的 反彙編輸出結果(fxc/dumpbin)。它只是個擅長執行這種代碼的處理器,在硬件中負責將 某些shader代碼編譯 成近似的shader字節碼。與我以前談論的東西不一樣,這塊內容有豐富的資料——若是你感興趣的話,能夠從AMD和NVidia找到一些會議演示文檔,或者閱讀一下CUDA/Stream SDK的文檔。
 
概括一下:高速ALU主要佈置在FMAC(浮點乘法累加 Floating Multiply-Accumulate)單元周圍,某些硬件支持倒數,倒數平方根,log2,exp2,sin,cos運算,高吞吐量和高密度無延遲的優化,運行大量的線程來下降上述延遲,每一個線程有不多的寄存器(由於正在運行的線程太多了!),很是適合執行直接式代碼(無循環的),不適合運行有分支的(特別是不連貫的代碼)。
 
上述的一般就是全部的實現。還有一些區別;AMD的硬件一般直接用4-位寬的SIMD表示HLSL/GLSL以及shader自節碼(儘管它們後來不用了),而NVidia不久以前打算將4-通路SIMD轉變成標量指令(scalar instruction)。再次提醒,全部的這些在web上都有資料。
 
尾註
 
我再次免責聲明「頂點緩存與着色」一節:其中有一部分是個人猜想,因此講的有點不清楚。
 
我也不打算講如何寫緩存的細節,這部分是託管的;緩存大小取決於處理批次的大小和想要輸出的頂點屬性。緩存大小和管理對於性能是很重要的,但我不在這裏詳細解釋,我也不想解釋;雖然頗有趣,可是這部份內容與談論的硬件很是特殊,不用深刻了解。
相關文章
相關標籤/搜索