前面介绍了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对象,欢迎大家继续阅读。