OpenGL 计算着色器中的简单原子计数器测试问题

Issue with simple atomic counter test in OpenGL compute shader

我一直试图通过尝试一些简单的例子来解决内存同步和一致性问题。

在此,我将调度一个具有 8x8x1 大小工作组的计算着色器。工作组数量足以覆盖屏幕,即720x480。

计算着色器代码:

#version 450 core

layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in;

layout (binding = 0, rgba8) uniform image2D u_fboImg;

layout (binding = 0, offset = 0) uniform atomic_uint u_counters[100];

void main() {
    ivec2 texCoord = ivec2(gl_GlobalInvocationID.xy);

    // Only use shader invocations within first 100x500 pixels
    if (texCoord.x >= 100 || texCoord.y >= 500) {
        return;
    }

    // Each counter should be incremented 400 times
    atomicCounterIncrement(u_counters[texCoord.x]);

    memoryBarrier();

    // Use only "bottom row" of invocations to draw results
    // Draw a white column as high as the counter at given x
    if (texCoord.y == 0) {
        int c = int(atomicCounter(u_counters[texCoord.x]));
        for (int y = 0; y < c; ++y) {
            imageStore(u_fboImg, ivec2(texCoord.x, y), vec4(1.0f));
        }
    }
}

这是我得到的:(锯齿状条的高度每次都不同,但平均约为该高度)

这是我所期望的,并且是将 for 循环硬编码到 400 的结果。

奇怪的是,如果我减少调度中的工作组数量,比如将 x 值减半(现在只会覆盖一半的屏幕),条形图会变大:

最后证明没有其他废话,这里我只是根据本地调用id着色:

*编辑:忘了提到调度后紧接着 glMemoryBarrier(GL_ALL_BARRIER_BITS);

除非另有说明,否则特定着色器阶段的所有着色器调用,包括计算着色器阶段,都独立彼此执行,顺序未定义。并且调用 memoryBarrier 不会改变这个事实。这意味着,当 memoryBarrier 之后的内容被调用时,无法保证来自原子计数器的值已被最终将这样做的所有着色器调用递增。

因此,您所看到的正是人们所期望看到的:调用会写入一些随机值,具体取决于调用恰好在其中执行的依赖于实现的顺序。

您想要做的是为所有调用执行所有原子增量,然后读取这些值并根据您读取的内容绘制内容。您编写的代码无法做到这一点。

虽然计算着色器确实有 some ability to manipulate the order of execution of invocations,但这仅适用于同一工作组内的调用(这实际上是工作组存在的原因)。也就是说,您可以在工作组中按一定程度排序调用,但不能在工作组之间排序。

解决这个问题的简单方法是将它变成 2 个计算着色器分派操作。第一个执行所有递增。第二个将读取值并将结果写入图像。

更聪明的解决方案是采用工作组。也就是说,将您的工作分组,以便在同一工作组中执行增加相同原子计数器的任何内容。这样,您甚至不需要原子计数器;您只需使用共享变量(可以 perform atomic operations)。在完成共享变量的所有递增之后调用 barrier() ;这确保在任何调用继续超过该点之前,所有调用至少已执行那么远。所以所有的递增都完成了。