Adreno OpenGL ES 3.1 介绍(1)
4. 使用OpenGL ES 3.1与Adreno
4.1 OpenGL ES 3.1中的新特性
本节简要介绍了OpenGL ES 3.1中引入的一些重要特性。详情请参见以下内容:
- OpenGL ES 3.1规范- http://www.khronos.org/registry/gles/specs/3.1/es_spec_3.1.pdf
- OpenGL ES着色语言3.10规范- http://www.khronos.org/registry/gles/specs/3.1/GLSL_ES_Specification_3.10.pdf
- OpenGL ES 3.1参考页面- http://www.khronos.org/opengles/sdk/docs/man31/
4.1.1 原子计数器
OpenGL ES 3.1提供了一个新的无符号整型变量类,称为原子计数器。这些计数器可以通过着色器使用不同的原子操作来访问。这些操作的原子性质意味着当多个着色器调用试图同时访问一个原子计数器时,这些访问将被序列化,这样就不会发生线程竞争。
要在着色器中定义一个原子计数器,使用新的ES着色语言类型atomic_uint。该类型是不透明的,所以从着色器中访问或操作计数器值的唯一方法是使用一个新的访问函数,包括:
- atomicCounter - 返回计数器的值
- atomicCounterDecrement - 减少计数器的值并返回新的值
- atomicCounterIncrement — 使计数器的值递增,并在递增操作之前返回该值
原子计数器必须得到缓冲区对象存储空间的支持。使用新的索引缓冲区对象绑定点GL_ATOMIC_COUNTER_BUFFER,将原子计数器与缓冲区对象的区域关联起来。允许多个原子计数器使用一个缓冲区对象,只要它们的存储空间不重叠或相交。
要将原子计数器与唯一的缓冲区对象区域关联起来,在声明原子计数器时需要使用两个新的布局限定符:
- Binding - 指定缓冲区对象绑定点的索引,例如,它决定将使用哪个缓冲区对象,并且必须始终指定
- Offset - 指定缓冲区对象内原子计数器的偏移量(以字节为单位)
有关使用这些限定符的更多细节,请参阅OpenGL ES 3.1着色语言规范http://www.khronos.org/registry/gles/specs/3.1/GLSL_ES_Specification_3.10.pdf
例如,假设任务是找出传入输入数据集的值中哪些是素数。可以通过实现一个计算着色器来解决这个问题。
单个计算着色器调用将获取唯一的工作组和工作项ID,将此信息转换为唯一的条目索引,并使用该索引来寻址输入数据集以检索候选编号。一旦调用知道要处理哪个值,它就可以继续执行必要的检查,以确定该值是否是素数。如果输入值确实是一个素数,那么计算着色器调用可以将它存储在外部存储器中。但是原子计数器是如何在这里出现的呢?
使用计数器作为获取索引值的一种方法,这些索引值将确保在已计划运行的所有计算着色器调用中是唯一的。使用OpenGL ES着色器语言指令递增原子计数器,在递增之前返回计数器的值。如果多个着色器调用同时尝试递增计数器,则可以保证这些请求将被序列化。一旦计算着色器调用获得了唯一的计数器值,它就可以使用它来存储在着色器存储缓冲区或图像中找到的素数,其偏移量不会被任何其他计算着色器调用覆盖。最后,一旦所有计算着色器调用完成执行,计数器值将用作找到的素数的计数。然后,应用程序可以将缓冲区对象区域或用于结果存储的纹理mipmap中的素数值传回进程空间。
注意:在核心opengles3.1中,只保证在计算着色器阶段支持原子计数器。
在其他着色器阶段也可能支持它们,但情况并非如此。
4.1.2 计算着色器
OpenGL ES 3.1引入了一种称为计算着色器的全新类型的着色器。这些不构成正常渲染管道的一部分,可以使用GPU执行数据处理任务。
计算着色器的孤立特性会产生许多后果:
- 不支持输入属性或输出变量
- 不会像顶点着色器那样每个顶点调用一次
- 执行之前或之后没有其他着色器阶段
但是,它们与常规着色器类似,因为它们可以访问原子计数器、图像变量、着色器存储缓冲区、纹理、uniforms和uniform blocks。
例如,计算着色器如何与外部通信?毕竟,它不能使用输入属性或输出变量。答案是它可以使用上面提到的对象类型。其中,只有原子计数器、图像变量和着色器存储缓冲区可以在着色器运行时写入。当计算着色器想要存储其工作结果时,它可以使用一个新的原子计数器函数,更新图像的内容,或者写入缓冲区变量。
由于计算着色器在正常渲染管道之外操作,因此它们不会通过绘图调用来调用。取而代之,新的glDispatchCompute函数用于启动操作。
在开始计算之前,必须把要做的工作分成几个工作单元。
每个单元由一个工作组处理。单个工作组由多个调用组成,这些调用可能并行处理该工作单元。使用的调用数由着色器定义。根据着色器的需要,在1D、2D或3D网格中安排调用。网格的尺寸定义了本地工作组的大小。对于本地工作组的最大规模,存在特定于实现的限制。
这些约束就是为什么工作通常必须被分割成多个单元的原因。glDispatchCompute接受一组参数,定义X、Y和Z维度中的工作组计数。这允许使用单个API调用启动对工作单元的三维数组的处理。回想一下,每个工作单元都可以由一个三维的着色器调用数组组成。
单个工作组内的调用可以使用新的ES SL函数groupMemoryBarrier同步它们的执行。它们可以使用共享变量相互通信。它们还可以使用可写对象(如图像)交换信息,前提是它们可以使用新的ES SL函数之一:barrier、memoryBarrier*或groupMemoryBarrier来同步访问。
关键点
重要的工作组可以以任何顺序执行,并且不保证并行执行。这意味着在不同工作组之间同步执行流或资源访问的任何尝试都可能导致死锁。不能在多个工作组之间通信或同步执行流,只能在同一工作组内的不同调用之间通信或同步。
计算着色器允许在工作的组织方式中有更多的灵活性,这要感谢共享变量,这在计算着色器中可用,但在其他着色器类型中没有。因此,它们越来越多地被用于人工智能、物理或后期处理效果。
提示
不要交叉使用计算着色器和图形着色器。在Adreno架构下,在这两种管道类型之间切换是昂贵的,应该避免。相反,批处理draw调用和dispatch调用以减少驱动程序必须切换的次数。
4.1.3 ES着色语言的一些增强功能
OpenGL ES 3.1着色语言引入了以下增强功能:
- 现在绑定布局限定符可用于指定与uniform block或uniform采样器关联的初始绑定点
- 将浮点数拆分为有效位和指数的函数(frexp)
- 从有效位和指数构建浮点数的函数(ldexp)
- 使用进位或借位执行32位无符号整数加减操作的函数(uaddCarry, usubBorrow)
- 执行32位有符号和无符号乘法的函数,32位输入和64位结果跨越两个32位输出(imulExtended、umulExtended)
- 执行位字段提取、插入和反转的函数(bitfieldExtract、bitfieldInsert、bitfieldReverse)
- 现在可以在ES SL代码中定义多维数组
- 现在可以通过一次调用确定整数值中设置为1的比特数(bitCount)
- 现在可以通过一个函数调用确定将最高或最低有效位设置为1的位置(finddlsb, findMSB)
- ES着色语言(textureGather,textureGatherOffset)中现已提供纹理收集函数;这些函数可用于检索2x2占位面积,用于在纹理查找操作中进行线性过滤
- 将4个8位整数打包为32位无符号整数,并将32位无符号整数解包为4个8位整数的函数(packUnorm4x8, packSnorm4x8, unpackUnorm4x8, unpackSnorm4x8)
- 通过使用位置布局限定符,可以直接在ES着色语言中预先配置默认uniform block中的uniform的位置
本章其他章节中涉及的ES阴影语言特性未在上述列表中体现。