OpenGL 编程指南 (10)
1、OpenGL提供了许多图像类型来表达未编码的图像数据,它们与纹理相比:(1)表达的是单一层级的纹理不带有mipmap;(2)不支持滤波等采样操作、深度比较。
layout (binding = 4, rgba32f) uniform image2D imageIns;//rgba32f为image的数据格式format,在图像不是只写入的情况下这个属性是必须的;需要与图像类型相对应,binding = 4 绑定到4纹理单元,不是必须的
2、每种shader单个使用图像的数量是有限的,通过GL_MAX_VERTEX_IMAGE_UNIFORMS(型如GL_MAX_*_IMAGE_UNIFORMS)进行查询单个着色器能够使用多少个图像的uniform,GL_MAX_COMBINED_IMAGE_UNIFORMS给定了所有shader中可用图像uniform的总和。因平台而已,使用图像是最好查询一下GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS看看时候有其它的限制。
3、图像对象绑定与纹理绑定差别有点大
void glBindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format)//unit纹理单元;texture图像id;layered为GL_TURE表示绑定一个数组同时忽略layer参数,GL_FALSE表示绑定单个图像单元;layer图像绑定到的层;access访问域(GL_READ_ONLY GL_WRITE_ONLY GL_READ_WRITE),定义的是shader对图像的访问方式。
4、图像的读取
gvec4 imageLoad(readonly gimage1D image, int P)
gvec4 imageLoad(readonly gimage2D image, ivec2 P)
gvec4 imageLoad(readonly gimage1D image, ivec3 P)
gvec4 imageLoad(readonly gimage2DRect image, ivec2 P)
gvec4 imageLoad(readonly gimageCube image, ivec3 P)
gvec4 imageLoad(readonly gimageBuffer image, int P)
......
图像数据的写入
gvec4 imageStore(writeonly gimage1D image, int P, gvec4 data)
gvec4 imageStore(writeonly gimage2D image, ivec2 P, gvec4 data)
......
图像数据大小获取
int imageSize(gimage1D image)
int imageSize(gimageBuffer image)
ivec2 imageSize(gimage2D image)
......
5、对于传统的帧缓冲光栅化来说,片元写入的位置是着色器运行之前的固定流程决定的,但使用图像存储方式就能够在着色器中写入位置信息;图像存储的方式没有限制的能够多次写入,因此能够写入大量的数据后用于帧缓冲或者其它的附件。总的来说,图像存储非常有利于大量的齐次数据处理。
6、对于大型结构化数据相较于图像存储方式,SSBO(shader storage buffer object) 是一个更好的选择,在接口前使用buffer关健字。
layout (std430, binding = 0) buffer BufferObject//binding=0表示这个快必须和索引为0的GL_SHADER_STORAGE_BUFFER关联
{
int diss;
int viewIndex;
vec4 points[];
};
对应于应用程序中需要使用如下配合使用
glGenBuffers(1, &ssbo);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
glBufferData(GL_SHADER_STORAGE_BUFFER, size, nullptr, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, bindindex, ssbo);
7、OpenGL不提供shader在同一个绘制命令中的执行顺序,甚至是两个绘制命令之间也不管,因此能够高度并行得到良好的性能。所以对于同时读写一块内存有时需要注意,如下
#version 420 core
layout (r32ui) uniform uimage2D overdrawCount;//unity scene有一个overdraw的显示模式,表示每个像素被画了多少次,与这个是一个意思
void main
{
uint count = imageLoad(overdarwCount, ivec2(gl_FrageCoord.xy));
count = count + 1;
iamgeStore(overdarwCount, ivec2(gl_FragCoord.xy), count);
}
上面这种做法会产生错误的结果,因为读写循环会被其它实例的相同shader中断执行。下面图例中,希望得到的是4但实际上得到的是3。
8、为了避免7中的问题,OpenGL提供了一系列原子(atomic)函数直接操作被共享的内存。它们拥有两个方面的属性能够保障原子性,其一保障在"单一"的时间内执行而不被中断,其二硬件方面能够保障对同一内存的多个操作序列地依次执行。需要注意的是,这一系列函数保障的是一个关键操作之间的连续性而不互相影响计算结果,而不是保障所有调用的执行顺序,并且只能用于整形哦。(Note that there is still no guarantee of order-just a guarantee that all invocations execute their operation without stepping on each other's results.)那么对于7中的例子,可修改如下:
#version 420 core
layout (r32ui) uniform uimage2D overdrawCount;
void main()
{
imageAtomicAdd(overdrawCount, ivec2(gl_FragCoord.xy), 1);
}
这一些列函数有加减法、逻辑操作、比较、交换等函数,返回值都是更新之前的值
uint imageAtomicAdd(IMAGE_PARAMS mem, uint data)//IMAGE_PARAMS 宏可以定义为所有图像类型
uint imageAtomicXor(IMAGE_PARAMS mem, uint data)
uint imageAtomicCompSwap(IMAGE_PARAMS mem, uint compare, uint data)
......
9、除了图像外缓存变量(buffer 关键字声明)也有一系列的原子函数
uint atomicAdd(inout uint mem, uint data)//inout 的关键字参数用于引用内存位置
uint atomicXor(inout uint mem, uint data)
......
10、OpenGL大部分情况下CPU与GPU都是异步交互,但在一些情况下需要采用同步的方式,因此产生了同步对象(sync object)的概念,也称栅栏(fence)。它本质上是一个命令流中的标记,可以在绘制或者状态变化命令过程中被发送。它的起始生命为无信号状态,在GPU执行过后变为有信号状态,任何时候它的状态都能够被应用程序查询。
GLsync glFenceSync(GLenum condition, GLbitfield flags)//创建一个删栏对象并插入到命令流中,condition必须是GL_SYNC_GPU_COMMANDS_COMPLETE,flag保留参数设0
void glGetSynciv(GLsync sync, GLenum pname, GLsizei bufSize, GLsizei* length, GLint* values)//length将被写入values的字节大小,pname为要查询的信息如GL_SYNC_STATUS
GLenum glClientWaitSync(GLsync sync, GLbitfields flags, GLuint64 timeout)//timeout是超时时间单位为纳秒(ns),这个函数能够强制应用程序等待到达删栏,它可能返回一下内容
1)GL_ALREADY_SIGNALED 调用这个函数的时刻就已经有信号了
2)GL_CONDITION_SATISFIED 调用时没有信号当在超时前有信号了
3)GL_TIMEOUT_EXPIRED 超时了
4)GL_WAIT_FAILED 调用失败,如参数错误
11、同步对象一个重要用途是判断GPU是否已经使用过映射缓冲中的数据,如果缓冲(或者一部分)使用glMapBufferRange函数带有GL_MAP_UNSYNCHRONIZED_BIT进行映射,就需要这种判断。
glBindVertexArry(vao);
glDrawArrays(GL_TRIANGLES, 0 , count);
GLsync sync = glFenceSync();
void* data = glMapBufferRange(GL_UNIFORM_BUFFER, 0, size, GL_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT);
......//do something
switch(glClientWaitSync(sync, 0, 1000000));
glDeleteSync(sync);
......//do something
glUnmapBuffer(GL_UNIFORM_BUFFER);
12、在多个环境中共享对象的一般做法是在源环境中创建删栏在目标环境中调用glWaitSync获取信号
void glWaitSync(GLsync sync, GLbitfield flags, GLuint64 timeout)//flags 必须设置为0,timeout必须设置为GL_TIMEOUT_IGNORED,这两个参数都由设备实现决定
13、几个关键字
1)volatile 禁优化关键字,可用于全局变量或者函数参数,但是变量一旦被声明为volatile的,那么不能作为一个参数不带这个关键字的函数参数
2)restrict 表示某个图像中的引用数据并不是其它图像的数据别名,都是自己的东西,可以进行存在破坏性优化。(默认,编译器会假设外部缓存可能存在别名替代的情形,因此不会使用对此有破坏性的优化方式,但是GLSL认为shader中不存在变量和参数的别名问题,所以优化功能全开)
3)readonly、writeonly 意思上很明显,需要注意的是原子操作是 读取-修改-写入 的流程,所以被修饰的变量时无法用于原子操作的
4)coherent 现代GPU框架都是多级缓存,上级缓存的修改不会立即写回下层,那么对于被共享的内容在不连续的情况下,内存的修改如何知会所有使用者是一个问题。两种思路,其一忽略所欲缓存写回后重新获取但代价高昂,其二是将其移到下一个层级的缓存保证连续性
14、内存屏障,用于解决编译器对内存操作进行从新排序可能导致意想不到的结果。
#version 420 core
layout (rgba32f) uniform coherent image2D imageIns;
void functionUsingBarriers(coherent uimageBuffer i)
{
uint val;
do { val = imageLoad(i, 0).x; }while (val != gl_PrimitiveID);
vec4 frag = imageLoad(imageIns, gl_FragCoord.xy)
......// do something to frag
imageStore(imageIns, gl_FragCoord.xy, frag);
memoryBarrier();//确保存储操作已经完成
imageStore(1, 0, gl_PrimitiveID + 1);
memoryBarrier();//确保存储结果进入内存
}
应用程序中也可以适应OpenGL的内存屏障
void glMemoryBarrier(GLbitfield flags);// 关于flags值得设置查阅 http://docs.gl/gl4/glMemoryBarrier
15、预先深度与模板测试
许多物体产生的片元会被其它的物体遮挡完全看不见,但是这些片元同样执行了fragment shader,这是一种浪费,因此OpenGL提供了一种将深度与模板测试提前的方法,使用early_fragment_tsets 修饰输入,这一特性需要4.2版本及以上或者支持ARB_shader_image_load_store扩展。但限制是在fragment shader中修改片元的gl_FragDepth时需要指明修改的值均不小于、或者不大于原来的值,否则该功能会被忽略。
layout (early_fragment_tests) in;