【问题标题】:GLSL per-pixel spinlock using imageAtomicCompSwap使用 imageAtomicCompSwap 的 GLSL 每像素自旋锁
【发布时间】:2019-10-06 04:34:18
【问题描述】:

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

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 规范?

【问题讨论】:

    标签: opengl glsl mutex spinlock


    【解决方案1】:

    如果执行顺序是问题,稍微重新排序代码可能会解决问题:

    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);
    
            if (lock_available == 0)
            {
                // do some operations protected by the lock
                do_something();
    
                // spinlock - release
                imageAtomicExchange(lock_image, pos, 0);
            }
    
        } while (lock_available != 0);
    }
    

    【讨论】:

      【解决方案2】:

      和这个例子一样,为什么我们不能使用原子操作imageAtomicCompSwap来保证不同PS调用之间的独占访问?

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

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

      这正是ARB_fragment_shader_interlock 存在的原因。


      话虽如此,即使有前进的保证,你的代码仍然会被破坏。

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

      另外,正如其他人指出的那样,如果原子比较/交换的返回值为零,则您需要继续旋转。记住:all 原子函数返回图像中的 original 值。因此,如果它以原子方式读取的原始值不是 0,那么它比较为 false 并且您没有锁。

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

      【讨论】:

      • @Nical Bolas,我将返回值更改为lock_available != 0。而如果imageStore改成imageAtomicExchange(lock_image, pos, 0),还是会导致APPRCASH。
      • @ZhiboShen:我认为“按照规范,你的代码仍然是 UB。”说了该说的话。
      【解决方案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 只为您提供其他调用组的绘制调用粒度。规范的这一部分也有以下脚注,以明确这一点:

      因为将调用划分为调用组是依赖于实现的 并且不可观察,应用程序通常需要假设抽奖中所有调用属于单个调用组的最坏情况

      因此,除了关于未定义的相对调用顺序的更强有力的声明外,该规范还涵盖了“同步”SIMD 处理,并明确表明您在图形管道中对其没有太多控制。

      【讨论】:

      • 是的,OpenGL 4.6 中添加了“调用组”,但之前的 GL 规范没有定义。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-10-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多