使用imageAtomicCompSwap的GLSL每像素自旋锁

时间:2018-08-06 09:41:17

标签: opengl glsl mutex spinlock

OpenGL红皮书第9版(OpenGL 4.5)示例11.13是简单的每像素互斥体。它在imageAtomicCompSwap循环中使用do {} while()进行每像素锁定,以防止在同一像素坐标对应的像素着色器调用之间同时访问共享资源。

layout (binding = 0, r32ui) uniform volatile coherent uimage2D lock_image;

void main(void)
{
    ivec2 pos = ivec2(gl_FragCoord.xy);

    // spinlock - acquire
    uint lock_available;
    do {
        lock_available = imageAtomicCompSwap(lock_image, pos, 0, 1);
    } while (lock_available != 0);

    // do some operations protected by the lock
    do_something();

    // spinlock - release
    imageStore(lock_image, pos, uvec4(0));
}

此示例在Nvidia和AMD GPU上均产生APPCRASH。我知道在这两个平台上PS的​​职业无法彼此独立地进行-线程的子组在锁步中执行,共享控制流(Nvidia术语中的32个线程的“扭曲”)。因此可能会导致死锁。

但是,OpenGL规范在任何地方都没有提到“锁步执行的线程” 。它仅提到“未定义相同着色器类型的调用的相对顺序。” 。如本例所示,为什么我们不能使用原子操作imageAtomicCompSwap来确保不同PS调用之间的互斥访问?这是否意味着Nvidia和AMD GPU不符合OpenGL规范?

3 个答案:

答案 0 :(得分:5)

  

在此示例中,为什么我们不能使用原子操作imageAtomicCompSwap来确保不同PS调用之间的互斥访问?

如果您使用原子操作来锁定对像素的访问,那么您将依赖于相对顺序的一个方面:所有线程最终都会前进。也就是说,您假设在锁上旋转的任何线程都不会使拥有其执行资源锁的线程饿死。持有该锁的线程最终将前进并释放它。

但是,由于执行的相对顺序是 undefined ,因此不能保证其中的任何一个。因此,您的代码无法正常工作。任何依赖单个​​着色器阶段调用之间的顺序的任何方面的代码都无法工作(除非有特定的保证)。

这正是ARB_fragment_shader_interlock存在的原因。


话虽如此,即使可以保证前进的进度,您的代码仍然会被破坏。

您使用非原子操作来释放锁。您应该使用原子设置操作。

另外,正如其他人指出的那样,如果原子比较/交换的返回值为 not 零,则需要继续旋转。请记住: all 原子函数从图像返回原始值。因此,如果原子读取的原始值不为0,则比较为false,则您没有锁定。

现在,按照规范,您的代码仍将是UB。但这更有可能起作用。

答案 1 :(得分:3)

  

但是,OpenGL规范在任何地方都没有提到“ 以锁步方式执行的线程”。它只提到“ 未定义相同着色器类型的调用的相对顺序。”。

您这样说,好像GL规范的措词不能涵盖“锁步”情况。但是“ 相同着色器类型的相对调用顺序未定义。”实际上涵盖了这一点。给定两个着色器调用A和B,此语句意味着您不能假定以下的任何

  • A在B之前执行
  • B在A之前执行
  • A和B并行执行
  • A和B不能并行执行
  • A的部分在B的相同或其他部分之前执行
  • B的部分先于A的相同部分或其他部分执行
  • A和B的部分并行执行
  • A和B的部分不能并行执行
  • ...(可能更多)...

未定义的顺序意味着您不能永远等待另一个调用的结果,因为不能保证可以在等待之前执行另一个调用的结果,除外 >在GL规范做出某些额外保证的情况下,即:

  • 使用显式同步机制(例如barrier()
  • )时
  • 在不同的着色器阶段之间存在一些较弱的排序保证 (即,可以假设在为非常原始的 片段处理时,所有顶点着色器发票都已经发生。)

例如,GLSL Spec, Version 4.60在第8.18节中解释了“调用组”的概念:

  

OpenGL着色语言的实现可以选择将多个着色器分组   单个着色器阶段调用到单个SIMD调用组中,其中调用是   以未定义的实现相关方式分配给组。

及其随附的GL 4.6 core profie spec在第7.9节中将“调用组”定义为

  

计算着色器的调用组 [...]是一组   单个工作组中的调用。对于图形着色器,调用组是   给定的着色器调用集的与实现相关的子集   由单个绘图命令生成的着色器平台。对于MultiDraw*   drawcount大于1的命令,则分别绘制的调用是   在不同的调用组中。

因此,除了用于计算着色器之外,GL还为您提供了其他调用组的draw-call-granularity。规范的这一部分还具有以下脚注,以使这一点非常清楚:

  

因为将调用划分为多个调用组取决于实现   并且不是可观察到的,应用程序通常需要假设平局中所有调用的最坏情况属于一个调用组

因此,除了对未定义的相对调用顺序进行更强有力的说明之外,该规范还涵盖了“按步进行” SIMD处理,并且非常清楚地表明,您在图形管道中没有太多控制权。

答案 2 :(得分:0)

如果执行顺序有问题,则对代码重新排序可能会解决问题:

<section class=container id=st>
  <div>1</div>
  <div>2</div>
  <div>3</div>
</section>
<section class=container id=nd>
  <div>1</div>
  <div>2</div>
  <div>3</div>
</section>