前面介紹了opengl中的uniform對象,今天繼續介紹一種新的對象---Shader Storage Block,我們簡稱為SSB對象,相比之前的uniform block,這種新的資料類型最大的變化在于,shader可以對SSB對象進行讀寫操作,而uniform block對象在shader中是隻讀的。衆所周知,opengl相對于cpu最大的優點是快,而快的基礎是其強大的并行特性,同一份shader代碼可能同時在幾千幾萬甚至更多的gpu unit上同時執行,如果隻有隻讀的uniform對象,這是沒有問題的,但如果引入了SSB對象,問題就來了,怎麼樣才能保證資料的正确性,這和cpu的多線程其實是一樣的,都需要一些同步機制來保證,下面我們将詳細介紹。
SSB對象
首先,我們來看看怎麼在一個shader中定義一個SSB對象,如下
layout (binding = 0,std430) buffer my_storage_block
{
vec4 foo;
vec3 bar;
}
如上代碼,SSB對象通過關鍵字“buffer”來指定,SSB對象和uniform block一樣,也和一個buffer object綁定,讀取和寫入實際上都是指向buffer的相應位置。定義中的binding指定buffer所在的binding point,std430為一種新的布局方式,相對于之前見到的std410,這種布局方式更為緊湊。430或者之前的410這些數字代表的是opengl的版本号,像std430是4.3版本引入的,是以才會叫這個名字。理論上,uniform block可以做的事,SSB對象都可以完成,以前的vbo對象做的事也都可以通過SSB對象完成,因為SSB更為通用,但這種通用性帶來的問題是效率不如uniform block高,因為後者的隻讀性可以有很多的優化空間。因為SSB對象綁定的是一個buffer object對象,可通過之前的glMapBufferRange或glBufferSubData等方法對SSB對象進行讀取和寫入操作。
SSB對象還有一個比較好的用途是變長數組,即在shader中指定數組但不指定大小,如下所示,vertices變量是一個數組,但大小沒有指定,隻要綁定的buffer夠大,數組大小可以非常大。需要特别注意的是每一個SSB對象隻能有一個變成數組成員變量,而且必須放在block的最後面。
layout (binding = 0,std430) buffer my_block
{
vec4 foo;
float a[3];
vec4 vertices[]
}
Atomic Memory Operations
這一部分我們介紹原子操作,這主要是為了解決同步問題,防止出現邏輯錯誤。比如兩個shader同時執行m = m +1,兩次執行都從m的位址讀取到舊的value,然後對value +1,再寫入到m的位址,如果不能正确協調順序,兩次執行後,m的值可能為m+1,也可能為m+2.shader的情況更為複雜,因為并行執行的shader非常多,而原子操作能保證多個并行執行的shader對同一記憶體同時操作時有一個正确的時序,一個原子操作在執行完成前是不允許中斷的,在執行的時候也不允許别的shader對同一個記憶體區域進行通路,這樣就保證了最終結果的正确性,這些常用的方法定義如下:
atomicAdd(mem,data) //mem = mem + data
atomicAnd(mem,data) // mem = mem & data
atomicOr(mem,data) // mem = mem | data
atomicXor(mem,data) // mem = mem ^ data
atomicMin(men,data) // mem = min(mem,data)
.......
大部分的函數的含義都是顯而易見的,需要注意的是函數傳回的是之前的值,而不是修改後的值。函數隻有int 和uint兩個版本,不支援其他資料類型。
Memory Barrier
接下來我們介紹openglk的記憶體同步機制,這種機制和原子操作并無太多關系,原子操作保證的是讀寫結果的正确性,而Memory Barrier機制保證的是代碼間的順序,確定如下幾種問題都能被有效避免。就拿上面的原子操作。舉個例子,shader對SSB對象通過原子操作進行寫入,比如對SSB對象的某個成員變量加1,而後再進行讀操作,期望的結果是所有寫操作完成後再進行讀操作,雖然原子操作保證了各個shader間能有序寫入進而保證結果的正确性,但由于opengl的并行特性,所有shader是并行執行的,某個shader讀取時并不能保證其他shader都已經完成寫入操作,這時候就需要用到Memory Barrier機制了。介紹之前,首先總結下可能遇到幾種問題類型,如下
read-after-write,由于指令順序可能會被優化時改變,可能導緻讀取的是write之前的值
write-after-write,可能導緻兩次寫入順序錯亂,進而寫入的最終結果錯誤
write-after-read,可能導緻讀取到write後的新值
由于opengl固有的并行行,記憶體同步機制非常重要,而其中最重要的一個就是memory barrier,這個機制的實作隻有一句代碼即可,這行代碼的含義直白點說就是保證opengl優化和改變代碼順序時,保證該行之前的所有代碼塊都在該行之後的代碼塊之前執行。函數定義如下:
void glMemoryBarrier(GLbitfield barriers);
參數barriers是一個位操作符參數,具體含義不在這詳述,下面介紹幾個目前常用的,其他的等以後再進行補充:
GL_SHADER_STORAGE_BARRIER_BIT, 確定該行代碼之前對SSB對象讀取或寫入的操作先完成,然後才運作該行代碼之後的shader繼續對SSB對象進行存取。
GL_UNIFORM_BARRIER_BIT,說明該行代碼執行前的一些shader對某些buffer object對象進行了寫入,而該行代碼後的一些shadre會用這個buffer object作為uniform object,設定該屬性位保證之前的代碼寫入完成後才可能執行後面的讀取操作。
GL_VERTEX_ATTRIB_ARRAY_BUFFER_BIT,說明該行代碼執行前的一些shader對某些buffer object 對象進行了寫入,而該行代碼後的一些shadre 會用這個buffer object作為頂點輸入的vbo對象,設定該屬性位保證之前的代碼寫入後才會執行後面的讀取操作。
GL_ATOMIC_COUNTER_BARRIER_BIT,說明該行代碼執行前的一些shader 對某些buffer object對象進行了寫入,而該行代碼之後的一些shader 将此buffer 作為Atomic Counter 對象的綁定buffer,可參加下節内容。
... ... 其他屬性後,等以後用到再補充。
需要特别注意的是,glMemoryBarrier函數是cpu調用執行的,但協調的是gpu shader的寫入和讀取順序。shadre中也有類似的同步機制,shader 函數定義如下:
void memeoryBarrier()
在shader 中執行此函數,該函數會讓gpu進入等待狀态,隻到shader 之前的讀取和寫入操作都完成後才繼續往後執行,該函數針對的是opengl中的所有記憶體對象,類似的還有BarrierBuffer()等針對某些特殊記憶體對象的保證機制。
Atomic Counters
最後介紹一種opengl的計數機制Atomic Counters,這是一種特殊的uniform對象,需要綁定特定的buffer object到GL_ATOMIC_COUNTER_BUFFER這個Target上,對該對象的讀寫操作都是原子操作,進而保證計數的正确性,具體定義如下:
layout (binding = 3,offset = 8) uniform atomic_uint my_variable;
其中,binging = 3代表綁定GL_ATOMIC_COUNTER_BUFFER的第三個binding point 上的對象,offset=8代表存儲位置位于該buffer 的offset為8的位址,除了在shader中設定好binding point和offset,還需要用戶端代碼把buffer 綁定到對應的bingding point上,代碼如下:
GLUint buf ;
glGenBuffers(1,&buf);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER,buf);
glBufferData(GL_ATOMIC_COUNTER_BUFFER,16*sizeof(GLuint),NULL,GL_DYNAMIC_COPY);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER,3,buf);
設定好Atomic Counter對象綁定的緩沖區後,就可以通過shader進行相應的操作了,具體操作如下:
uint atomicCounterIncrement(atomic_uint c) //加1操作
uint atomicCounterDecrement(atomic_uint c) //減1操作
uint atomicCounter(atomic_uint c) //擷取couter 數目
每一個Atomic Counter 對象都有一個綁定的buffer object對象,也會遇到上面的記憶體同步問題,是以,也需要用glMemoryBarrier (GL_ATOMIC_COUNTER_BARRIER_BIT)函數來確定代碼的執行順序,特别需要注意的是,這行代碼保證的是先寫入buffer object然後将buffer object 作為Atomic Counter的綁定buffer 的情況,如果先通過上面的原子操作寫入buffer,而後對該buffer 進行讀取操作,并不一定要用GL_ATOMIC_COUNTER_BARRIER_BIT來保證,需要根據該buffer 的實際用途來指定對應的bit。
到此,關于SSB對象及OpenGL的一些記憶體保護機制都已講完,希望對大家有所幫助。下一篇,我将介紹下opengl裡用于存儲圖像的texture對象,歡迎大家繼續閱讀。