A trip through the graphics pipeline 2011 Part 10(翻譯)

minggoddess發表於2015-06-02

之前的幾篇翻譯都爛尾了,這篇希望。。。。能好些,恩,還有往昔呢。

-------------------------------------------------------------

primitive 圖元

tris   三角形

quad  柵格

 

------------------------------------------------------------

第十部分

譯:minggoddess

 

歡迎回來。上一次,我們一頭扎進了畫素管線的最底端。這次,切換到管線的中間位置看一下伴隨D3D10而來的大概是最引人注目擴充套件:幾何著色器(Geometry Shaders)。

但首先呢,我會先講一下我在本系列中如何分解圖形管線,這與你從APIs角度看到的景象是多麼的不同。

 

多重管線/對管線階段的剖析

儘管第三部分已經提及過,因為它特別重要,所以我還要再重複一次:如果你去看比如,D3D10的文件,你就會發現D3D10管線的文件。你會發現有一份D3D10管線的示意圖

包含了可能涉及到的各個階段。這個D3D10管線裡會包含幾何著色器,,即使你沒有設定過幾何著色器,同樣對輸出流(Stream-Out)也是如此。在純功能型的D3D10模型中,

幾何著色器階段總是在那裡的;如果你沒有設定幾何著色器,它就會很簡單(和沒意思):資料僅僅從這裡以不被修改的方式傳遞到下面的階段(光柵化階段/輸出流)。

 

這是使用API的正確方法,但是卻不是我們在本系列中瞭解這件事的好方法。我們更關心的是硬體是如何實現這種功能模型的呢。那麼,到目前為止,我們看到的這兩個著色器階段

(VS和PS)是怎樣的呢?對於頂點著色器(VS), 我們瞭解了輸入裝配器(IA--Input Assembler)。IA為著色準備了頂點資料塊,然後把這一批次分發到一個著色單元。

著色單元處理了一會兒之後,我們得到了返回結果並把結果寫到緩衝區(供面片裝配用)。確保它們按照正確的順序進行裝配,然後把它們發到下一個管線階段(剔除/裁剪等等)。

對於畫素著色器(PS),我們接受到從光柵化階段發過來的,準備被著色的柵格,把它們批量快取起來,直到ps著色器單元空閒下來,可以接收新的批次時,把這一批次的資料分發

到一個著色單元。同樣在著色器單元處理一會兒之後,我們的到處理結果並把結果寫到緩衝區(供ROPs使用),確保它們是正確的順序,然後做混合和延遲深度剔除(late Z)然後把結果

發到記憶體,聽起來有點熟悉,是嗎?

 

實際上,如果我們想讓著色器單元幫我們做事情,就總是這樣的情況:我們先是需要一個緩衝區,然後一些分發邏輯(實際上這部分邏輯很普遍,所以可以被所有著色器型別

所共享),然後我們擴充套件這一行為到一束著色器上進行並行運算。最終我們需要另一個緩衝區和有排序功能的單元來處理前面的運算結果。從著色器出來的運算結果有潛在亂序

的可能,需要把它轉回API順序。

 

我們已經瞭解了著色器單元(和著色器執行),並且瞭解了分發機制;實際上,我們已經瞭解了畫素著色器(Pixel Shaders),它具有例如衍生計算,輔助畫素,捨棄輸出和屬性插值

等特性。接下來就是計算著色器,在這之間我們不會遇到什麼著色器單元的額外功能了。計算著色器有它們專門的緩衝和核心。所以在接下來的部分中,我不會涉及著色器單元,因為

不同著色器型別僅僅是進出資料的結構和解釋不同。著色器部分不處理輸入輸出(算術,紋理取樣)的地方是一樣的,所以我也不講相同的這部分了。

 

著色片元(Tris)的形狀

我們一起看下幾何著色器的輸入輸出緩衝。讓我們從輸入開始 。額。。,理論上這很合理--是我們寫入頂點著色器的資料。或者,額。。並不完全是;幾何著色器處理的是片後設資料,

而不是分立的頂點,所以我們真正需要的是片元組裝(Primitive Assemble -PA)的輸出。注意,有多種方法可以處理這些。PA可以擴充套件片元輸出(如果頂點被引用多次就複製頂點),

或者只給我們一個塊頂點資料(我繼續使用之前用過的32個頂點),把它們用小的索引緩衝關聯起來(因為需要索引的資料塊有32個頂點,所以我們每個索引只需要5位[譯註:2的5次冪])

這兩種方法都行;前者是用於PA之後的裁減剔除的自然輸入格式,但是後者在執行GS(幾何著色器)時需要少的多的緩衝空間,所以在這裡我們採用第二種模型。

 

你需要操心GS的快取空間的一個理由是它一下處理很大量的片元,因為它不僅支援純線和三角形(每個片元2/3個頂點),也支援有鄰接資訊的線和三角形(每個片元4/6個頂點)。

D3D11增加了體形更大的片元,但一個GS仍然可以消耗高達32個控制點的片作為輸入。拿複製頂點來講,

困死了不翻了明天中午再翻些

   其實好多天過去了,並且剛剛翻譯的一段mei儲存,消失了。。。。

一個有16個控制點的面片,可以擁有高達16個向量屬性(D3D11下32個)?這是嚴重的記憶體浪費。所以我假設沒有複製,而是採用索引頂點的方法。這樣VS的輸出,外加一個相對而言

比較小的索引快取就成為一個批次片元的輸入。

 

現在,每個片元上都有幾何著色器了。對頂點著色器而言,我們需要收集一個批次的頂點,並且我們用簡單的貪心演算法來決定批次的大小。貪心演算法在不把一個片元分到多個批次的情況下

包儘可能多的頂點進一個批次--足夠公平。至於畫素著色,我們從光柵化階段得到足夠多的柵格且把它們全部包進批次。幾何著色器就有些不方便了,輸入塊被保證至少包含一塊完整的

片元,很有可能是多個--但是除此之外,塊裡面片元的數量完全是取決於頂點緩衝的命中率的。如果命中率高並且我們使用三角形片元,我們大概可以得到40-43個;如果我們使用包含鄰接

資訊的三角形,不走運的話就只能得到5個片元了。

 

當然,我們在這裡可以從不同的輸入塊裡收集片元, 但那就有點尷尬了。現在我們需要為一個GS批次保留多個輸入塊和索引快取,如果一個批次涉及多個索引快取就意味著那個批次裡的每個

片元都要知道去哪裡找索引和頂點資料。這就需要更多儲存空間,更多管理,更多開銷。很噁心。並且即使你使用兩個輸入塊,如果頂點快取的命中低的話那利用率仍然很糟糕。你可以支援

更多的輸入塊,但那會吃掉記憶體--記住,你還需要空間用於幾何輸出(我待會兒講)。

 

所以這是我們遇到的第一個障礙:我們用VS可以大概選擇我們目標批次的大小,並且為了讓我們在PA(Primitive Assemble)階段好過一些(這裡是指GS,待會講到的HS也是這樣),

我們並不總是產生最大的批次。對於PS, 我們總是對柵格著色,即使特別小的三角形也總是會命中多個柵格,所以我們可以得到一個合適的柵格數量與三角形數量的比率。但是

對於GS,我們對於管線的任何一個終點都沒有完全的控制權(因為我們在中間階段!),並且我們需要每個圖元的多個輸入頂點(與之相反的是一個輸入三角形的多個柵格),所以

快取很多輸入開銷很大(在記憶體和管理方面的開銷)。

 

在這一階段,你基本上可以想合併多少輸入塊就得到多少來獲得幾何著色圖元的一個塊;由於記憶體的限制塊的數量會很低(超過4塊會讓我很吃驚的)。根據你認為GS有多重要,塊的數量

有可能只是選為1。例如,根本不合並輸入塊。對於三角形這並不好,對於那種有更多頂點的片元就真的很糟糕了,但是如果你用GS的目的主要是把點擴充套件成柵格(點精靈)或者是偶爾出現的

立方體陰影貼圖(用Viewport索引陣列合Rendertarget索引--我一會就會講到),這就不太稱得上一個問題了。

 

GS 輸出:那裡也沒有玫瑰園

在輸出那面是什麼情況?這比普通的VS資料流更為複雜。實際上是複雜的多;一個VS輸出一樣東西(著色後的頂點),頂點在著色前後的比率是1:1。一個GS輸出大量頂點,(可以達到編譯時

確定的最大值),至於在D3D11,可以有多輸出流--然而,輸出流的最大值可以在管線接下來的階段送出,這就是我現在在談的路徑。GS資料的其它目的地將在接下來的章節裡談及。

 

一個GS產生大小可變的輸出,但是它需要執行在有限的記憶體需求上(除其它事項外,可用的記憶體緩衝區數量決定了GS可以並行處理的圖元數量),這就是為什麼輸出頂點數量的最大值在編譯時確定。

這個最大值(和寫入的輸出屬性的數量)決定了需要分配多少緩衝空間,從而間接決定了呼叫的並行GS數量;如果數量太小,延遲不能被完全隱藏,GS就會以一定時間比例暫停。

 

還需要注意的是GS的輸入的是圖元(例如,點,線,三角形和麵片,可選的鄰接資訊),但是輸出的是頂點--即使我們把圖元發到下一階段做光柵化!如果輸出圖元的型別是點,這是微不足道的。

但是對於線和三角形來說,我們需要重新組裝頂點回圖元。這是通過使輸出頂點形成一條線或者三角形帶來分別進行處理的。這種方式很好地處理了三個大概是最重要的案例:單一的線,三角形,

或者四邊形。如果GS試著做一些實際的擠壓或者是產生某些比較複雜的幾何圖形就不那麼方便了,這就需要幾個“重啟帶”標誌(這可以歸結為每個頂點上的一個位來決定當前帶繼續還是新帶的開始)

那麼為什麼會有限制?在API層面,看起來是非常武斷的--為什麼GS不能只輸出一個頂點列表外加一個小的頂點索引?

 

答案可以歸結為兩個字(在漢語裡是四個。。。):圖元裝配。這就是我們在這裡做的--拿一些頂點把它們裝配滿一個圖元然後送到管線下個階段。但我們已經在這條資料通路上使用了這個功能塊了,

就在GS前面。所以對GS來說,我們需要第二次圖元裝配階段,我們希望這階段可以很簡單。裝配三角形確實很簡單:三角形總是按照輸出快取的裡面排列的順序三個三個的排下去。換句話說,

採用帶並沒有比按理說最簡單的圖元(沒有索引的線和三角形)產生更為顯著的複雜度增加,但他們仍然為像四邊形這樣典型的圖元節省了輸出緩衝空間(因此提供了更高潛在並行的可能性)。

 

再談API順序

 但是,這裡有幾個問題:在常規頂點著色的時候,我們很明確的知道一個批次裡有多少圖元,並且它們在哪,即使在被著色的頂點到達PA快取之前--這些在我們設定被著色的批次時就得到確定了。

如果我們,比如有多個單元來進行揀選/裁剪/三角形組裝,它們都可以並行進行;它們知道去哪裡得到頂點資料,並且它們可以提前知道三角形要有的序號,這樣它們就可以被有序放置了。

 

對於GS, 在我們得到輸出之前,我們一般不知道我們將會產生多少圖元--實際上,我們可能一個也不生成!但是我們仍然需要注意API順序:首先所有的圖元從GS的0號呼叫生成,然後所有的圖元

產生於1號呼叫,這樣,貫穿到批次的末尾(當然批次們需要被和VS一樣按序處理)。所以對GS來說,我們得到返回結果,我們首先需要掃描輸出資料來決定整個圖元從哪裡開始。只有在這以後,

我們才可以開始揀選,裁剪和三角形安裝(可能並行)更多額外的工作!

 

VPAI 和RTAI

 這是兩個和GS一起加進來的功能,它們實際上不會影響GS的執行,但是確實會對接下來的處理流產生影響,所以我認為我們需要在這裡提一下:ViewPort索引陣列(VPAI作為簡稱)和

RenderTarget索引陣列(RTAI)。先講RTAI,因為它解釋起來更容易一些:正如你希望知道的那樣,D3D11增加了對紋理陣列的支援。嗯哼,RTAI給與了渲染道紋理陣列的支援:你可以把

紋理陣列作為渲染目標(RenderTarget),然後在GS裡,你可以為每個圖元選擇應該去哪個索引。注意因為GS是寫到頂點但不是圖元上的,,我們需要對一個單獨的頂點選擇對每個圖元的RTAI

(VPAI也是如此);這總是個“領頭的頂點”,例如第一個屬於圖元的那個頂點。使用RTAI的一個例子是在一個通道里渲染立方體貼圖:GS決定立方體的每個面應該被送到哪個片元(可能其中幾個),

VPAI是一個正交功能,允許你設定多個viewport和裁剪矩形(高達15個),然後來決定每個圖元用哪個viewport。這可以被用來在一個通道渲染CSM(Cascaded Shadow Map)的多個級聯,

並且它可以和RTAI結合。

 

至於說,這兩個功能都沒有對GS的處理產生顯著影響--它們只是被附加到圖元上之後被使用的額外資料:VPAI在視口變換時被使用,而RTAI在之後畫素著色器一路在用。

 

到目前為止的總結

好,所以在輸入端有些麻煩--我們不能全部選擇輸入資料的格式,所以需要用於輸入資料的額外緩衝,

感覺我有大半個月沒動這篇翻譯了。。

    沒想到之前翻譯了那麼多,原來就剩兩節了,早知道我就早點接著翻譯了。。。

 這樣,就會產生可變數量的輸入圖元,雖然我們並不需要把這些圖元分成合適的大批次。在輸出端呢,我們又在組裝可變數量的圖元,不必知道GS會產生多少圖元

(儘管對於有些GS來說,我們可以從編譯程式碼靜態地來確定這一點,例如因為所有頂點要麼在控制流外面產生,要麼在一個已知迭代數量並且沒有提前輸出的迴圈之內)

這就不得不在送往三角形裝配階段之前,花點時間來解析輸出。

 

如果這聽起來比只有VS的狀況複雜,那是因為確實如此。這就是為什麼我上面要提到:認為GS一直在執行是錯誤的--即使只有一個非常簡單的GS除了在兩個緩衝階段傳資料什麼也不做,

還會有額外的一輪圖元裝配可能以極低的利用率在著色單元上執行。所有這些都有代價,並且代價趨於累積:我在D3D硬體非常新的時候檢查過這一點,還有AMD和NVidia硬體上。一個

純粹傳遞作用的GS比沒有GS慢了3到7倍(這是在一個幾何受限的情景下)。我沒有在更新的硬體上重新測了;我假設現在情況好轉一些(那是實現GS的第一代,在實現它們的第一代GPU上

功能的表現通常不是很好),但是觀點仍然成立:即便只是用GS傳遞資料,那裡什麼也不做,都會產生可觀的開銷。

 

讓GS按順序產生帶狀圖元也不會有幫助; 對一個頂點著色器來說,每個頂點會呼叫它一次來讀和寫一個頂點(很好)。對一個GS來說,

程式碼寫完了,遇到寫TDD的任務,感覺還是翻譯這個難度低些。。continue

雖然我們最終可能只有一個批次11個GS執行(因為在輸入快取沒有足夠的圖元),他們中的每一個都會執行足夠長的時間,併產生大概八個頂點輸出。在低利用率的情況下,這需要很長時間!

(記住我們在每個批次需要介於16到64之間的某個數量的獨立任務分配到著色器單元)。如果GS主要由迴圈構成,這就很煩人了--例如,我在RTAI提及的渲染到立方體貼圖的例子,我們

在一個立方體裡為六個面迴圈6次,檢查一個三角形在那個面是否可見,如果是這樣的話就輸出這個三角形。對這六個面的計算是相當獨立的,如果可能的話,我們希望可以平行計算。

 

附贈:GS例項化

嗯哼,涉及GS例項化就需要提到D3D11的另一個新特性,可惜相關文件很少(我不確定在SDK裡面有沒有好點的例子)。雖然,解釋起來是很簡單的:對每個輸入圖元,GS並不是只執行一次

而是很多次(這是一個在編譯階段可以決定的靜態數值)。基本上這相當於把整個著色器包在下面這迴圈裡

for(int i = 0; i < N; i++)

{

  //...大括號分行 歐耶

}

 每個輸入圖元產生多個GS呼叫,在著色器外處理這迴圈。這種方式可以幫我們產生更大的批次因而提升利用率。這裡的i作為一個系統產生的值被送到著色器(在D3D11,由SV_GSInstanceID定義)。

所以如果你有一個那樣的GS,就去掉外面的迴圈,宣告一個[instances(N)]用合適的語義宣告i。這樣就會快很多--對大規模並行機分配獨立工作的魔法!

 

不管怎麼說,這就是幾何著色器.我略過了輸出流,但這個階段已經很長了,何況輸出流是個很大的話題(足以獨立於GS!)。下個階段會更精細,到時你就知道了!

----------------------------------------------------------------------------------------

我終於翻譯完了,這要感謝往昔翻譯其它部分一直很快,這種鞭策。。。

 

 

 

 

 

 

 

 

 

 

 

, 

相關文章