gl_WorkGroupSize是一个用来存储本地工作组大小的常数。它已在着色器的布局限定符中有local_size_x,local_size_y和local_size_z声明。之所以拷贝这些信息,主要是为了两个目的:首先,它使得工作组的大小可以在着色器中被访问很多次而不需要依赖于预处理;其次,它使得以多维形式表示的工作组大小可以直接按向量处理,而无需显式地构造。
gl_NumWorkGroups是一个向量,它包含传给glDispatchCompute()的参数(num_groups_x,num_groups_y和 num_groups_z)。这使得着色器知道它所属的全局工作组的大小。除了比手动给uniform显式赋值要方便外,一部分OpenGL硬件对于这些常数的设定也提供了高效的方法。
gl_LocalInvocationID 表示当前执行单元在本地工作组中的位置。它的范围从uvec3(0)到gl_WorkGroupSize – uvec3(1)
gl_WorkGroupID 表示当前本地工作组在更大的全局工作组中的位置。该变量的范围在uvec3(0)和gl_NumWorkGroups – uvec3(1)之间。
gl_GlobalInvocationID 由gl_LocalInvocationID、gl_WorkGroupSize和gl_WorkGroupID派生而来。它的准确值是gl_WorkGroupID * gl_WorkGroupSize + gl_LocalInvocationID,所以它是当前执行单元在全局工作组中的位置的一种有效的3维索引。
gl_LocalInvocationIndex是gl_LocalInvocationID的一种扁平化形式。其值等于gl_LocalInvocationID.z*gl_WorkGroupSize.x*gl_WorkGroupSize.y+gl_LocalInvocationID.y * gl_WorkGroupSize.x + gl_LocalInvocationID.x. 它可以用1维的索引来代表2维或3维的数据。
假设已经知道自己在本地工作组和全局工作组中的位置,则可以利用信息来操作数据。如例12.5所示,加入一个图像变量使得我们能够将数据写入由当前执行单元坐标决定的图像位置中去,并且可以在计算着色器中更新。
例12.5 数据的操作
例12.5中的着色器把执行单元在本地工作组中的坐标按本地工作组大小进行归一化, 然后将该结果写入由全局请求ID确定的图像位置上去。 图像结果表达了全局和本地的请求ID的关系,并且展示在计算着色器中定义的矩形的工作组。(本例有32*16个执行单元,图像如12.2所示)
为了生成如图12-2的图像, 在计算着色器写完数据后,只需简单地将纹理渲染至一个全屏的三角条带上即可。
通信与同步当调用glDispatchCompute()(或者glDispatchComputeIndirect())的时候,图形处理器的内部将执行大量的工作。图形处理器会尽可能采取并行的工作方式,并且每个计算着色器的请求都被看作是一个执行某项任务的小队。我们必然要通过通信来加强团队之间的合作,所以即使OpenGL并没有定义执行顺序和并行等级的信息,我们还是可以在请求之间建立某种程度的合作关系,以实现变量的共享。此外,我们还可以对一个本地工作组的所有请求进行同步,让它们在同一时刻同时抵达着色器的某个位置。
图12-2 全局和本地的请求ID的关系
通信我们可以使用shared关键字来声明着色器中的变量,其格式与其它的关键字,例如uniform、in、out等类似。例12.6给出了一个使用shared关键字来进行声明的示例。
例12.6 声明共享变量的示例
如果一个变量被声明为shared,那么它将被保存到特定的位置,从而对同一个本地工作组内的所有计算着色器请求可见。如果某个计算着色器请求对共享变量进行写入,那么这个数据的修改信息将最终通知给同一个本地工作组的所有着色器请求。在这里我们用了“最终”这个词,这是因为各个着色器请求的执行顺序并没有定义,就算是同一个本地工作组内也是如此。因此,某个着色器请求写入共享shared变量的时刻可能与另一个请求读取该变量的时刻相隔甚远,无论先写入后读取还是先读取后写入。为了确保能够获得期望的结果,我们需要在代码中使用某种同步的方法。下一个小节详细介绍这一问题。
通常访问共享shared变量的性能会远远好于访问图像或者着色器存储缓存(例如主内存)的性能。因为着色器处理器会将共享内存作为局部量处理,并且可以在设备中进行拷贝,所以访问共享变量可能比使用缓冲区的方法更迅速。因此我们建议,如果你的着色器需要对一处内存进行大量的访问,尤其是可能需要多个着色器请求访问同一处内存地址的时候,不妨先将内存拷贝到着色器的共享变量中,然后通过这种方法进行操作,如果有必要,再把结果写回到主内存中。