DirectX 11 學習筆記-07【Computer Shader】

Computer Shader 

使用GPU進行非圖形應用被稱爲general purpose GPU (GPGPU) programming。並非所有的算法都適用於GPU計算,由於GPU的高並行構架,我們需要大量的數據元素使用相似操作並能對它們進行並行操作,向對像素片段着色器所做的那樣。粒子系統也可以作爲GPU計算的粒子,當粒子之間可以獨立計算時,使用GPU進行並行計算。 

對GPGPU編程,通常來說我們需要獲取GPU的計算結果並返回到CPU中。這需要拷貝顯存中的數據到系統內存。在01中我們討論了這方面的性能問題,通常來說這個拷貝操作是需要較多時間的,但如果和計算所花費的時間相比較能獲得較大的提升,那拷貝帶來的損失是完全可以接受的。 

對圖形學上的一些應用,我們可以直接將計算結果作爲管線的輸入,因此不需要GPU到CPU的拷貝。一個典型的例子是模糊的計算,通過compute shader計算模糊後的紋理,然後將紋理作爲管線的輸入顯示在屏幕中。 

Compute shader並不是Direct3D渲染管線的直接組成部分。它可以讓我們訪問GPU並實現數據並行的算法而不需要繪製任何東西。如上面所提到的,這對GPGPU很有用,但同樣有很多圖形學效果使用compute shader來實現。因此Compute shader對圖形學程序員來說也是關聯十分緊密的。 

Threads and Thread groups 

在GUP編程中,需要執行的線程被分割爲一系列線程組。每一個線程組在同一個微處理器中執行。也就是說,如果GPU有16個微處理器,那麼你應該把你的問題分成至少16個線程組,這樣每個微處理器都能有工作做。爲獲取更好的性能,你可以讓至少兩個線程在一個微處理器上執行,因爲微處理器可以在線程組中切換當某個線程停轉時(可能是在等待一個紋理操作)。 

每個線程組內的線程可以共享一塊共享內存。一個線程無法訪問其他線程組的共享內存。同一個組內的線程可以進行同步操作,但不同組內的線程無法同步。實際上,我們無法控制不同線程組之間的運行順序,因爲不同線程組運行在不同的微處理器上。 

一個線程組包含n個線程。實際上硬件會把這些線程分爲數個warps(每個warp包含32個線程),微處理器按照SIMD32(同時執行32個線程中的相同指令)方式處理warp。每個CUDA核心處理一個線程,一個“Fermi”構架的微處理器有32個CUDA核心。在D3D中,你可以指定非32倍數的n值,但考慮到性能,線程組的維度最好是warp尺寸的整數倍,256是一個比較常用的值。你可以試驗不同的值然後選擇最合適的值。 

COMPUTE SHADER的一般結構 

一個compute shader一般由以下幾個部分組成 

1.全局值例如constant buffers. 

2.輸入輸出資源 

3.[numthreads(X, Y, Z)] 屬性, 指明瞭一個線程組內的線程數 

4.在每個線程中要執行的着色器函數主體。 

5.線程Id 

數據輸入與輸出資源 

兩種類型的資源能作爲compute shader的輸入:緩衝區和紋理。 

compute shader的輸出比較特別,輸出類型有一個特殊的前綴“RW”,表示爲可讀且可寫。相反的,一般的紋理是隻讀的。另外需要通過模板參數指明輸出類型。例如: 

RWTexture2D<float4> gOutput; 

RWTexture2D<int2> gOutput; 

綁定一個資源到compute shader 的輸出需要使用一個新的視圖類型unordered access view (UAV),在C++中由ID3D11UnorderedAccessView表示。創建的過程與一般資源和資源視圖的創建過程類似,但需要用D3D11_BIND_UNORDERED_ACCESS指明這個資源會被用作一個UAV。一但創建完成,我們可以使用SetUnorderedAccessView來把資源視圖設置到shader中。 

線程ID系統 

  1. Group ID 系統爲每個線程組分配一個唯一的group ID,使用語義SV_GroupID表示。線程組是一個三維數組,所以如果線程組數量爲Gx*Gy*Gz,則group ID範圍是從(0,0,0)到(Gx-1,Gy-1,Gz-1)。 
  2. 2.線程組內的每個線程同樣被分配了一個組內的ID,在shader中用SV_GroupThreadID。同樣如果一個組的尺寸是X*Y*Z那麼group thread ID的範圍是(0,0,0)到(X-1,Y-1,Z-1)。 
  3. 3.每一次Dispatch都會對所有的線程生成一個唯一ID稱爲dispatch thread ID。它使用系統語義SV_DispatchThreadID。可以由組ID和組內ID推出Dispatch ID,公式如下 
    dispatchThreadID.xyz = groupID.xyz * ThreadGroupSize.xyz + groupThreadID.xyz; 

     

  4. 此外還有一個線性的線程組索引SV_GroupIndex, 
groupIndex = groupThreadID.z*ThreadGroupSize.x*ThreadGroupSize.y +groupThreadID.y*ThreadGroupSize. 

線程ID是並行算法的重要工具,因爲我們在拆分問題之後需要把問題分配到各個線程中,使用線程ID來劃分每個線程的工作範圍,例如不同的線程ID對應處理圖片上不同的紋理像素,從而達到並行的效果。 

紋理索引和採樣 

通常我們可以使用sample函數來對紋理進行採樣,但在Compute Shader中,由於不是直接用於渲染圖形,採樣函數無法自動選擇最合適的mipmap,因此在Compute Shader中,我們應該使用SmapleLevel指定我們需要的mipmap等級並進行採樣。 

由於我們並行計算中使用的線程id不是標準的uv座標,所以在取樣前應該向把id換算成uv座標,即 

u=x/width, v=y/height. 

另外可以直接將紋理理解爲數組,不使用採樣而是直接通過座標方式來索引紋理數據。 

結構化buffer資源 

StructuredBuffer<T>可以在HLSL中定義自定義結構的資源緩衝。 

在C++端,需要創建緩衝資源。類似一般的緩衝區,不過這裏應該要指定StructureByteStride,即結構體的尺寸。同樣的,要將資源綁定到管線中時需要爲資源創建資源視圖。 

比較不同的是,創建資源視圖時使用的格式是DXGI_FORMAT_UNKNOWN因爲自定義結構體對Direct11是未知的。 

拷貝CS結果到Memory 

使用CopyResource將資源拷貝到一個staging的資源中,然後再使用map的方式來獲取staging資源中的結果。 

ConsumeStructuredBuffer和AppendStructuredBuffer

是可以被減少(consume)添加(append)的buffer。每個線程中consume的順序都是不同的,一個元素只能被一個線程consume。另外Appendbuffer的尺寸是不會自動增加的,所以要保證buffer的大小足夠大。 

共享內存和同步 

線程組能被分配一塊共享內存或者叫做線程本地內存。訪問這塊內存的速度是非常快的。在compute shader中。可以聲明爲 

groupshared float4 gCache[256]; 

尺寸可以是任意的,但組共享內存的最大尺寸是32kb。使用過多的共享內存會引起性能問題。假設一個微處理器支持32kb的共享內存,你的compute shader要求一個20kb的共享內存。這意味着只能有一個線程組能夠在微處理器中得到滿足因爲沒有足夠的內存空間給另一個線程組使用。這會影響到GPU的並行性,因爲這種情況下微處理器無法在線程組中切換來避免延遲。因此儘管硬件支持32kb共享內存,但爲性能考慮還是應該儘量減少共享內存尺寸。 

共享內存的一個常見應用是用來存儲需要使用的紋理值。例如模糊算法中,需要從紋理中多次獲取同一個圖素。紋理採樣是GPU中一個較慢的操作,因爲GPU的存儲帶寬和延遲並沒有提高很多。通過預先獲取需要的紋理元素並存放在共享內存數組中,線程組可以避免冗餘的紋理獲取操作。之後算法再從共享內存中查找紋理元素,而這個操作是非常快的。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章