我一直在尝试一些琐碎的示例,以期围绕内存同步和一致性问题。
在这种情况下,我将分派具有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);
答案 0 :(得分:2)
除非另有说明,否则特定着色器阶段(包括计算着色器阶段)的所有着色器调用将以不确定的顺序彼此独立地执行 。调用memoryBarrier
不会改变这一事实。这意味着,当调用memoryBarrier
之后的内容时,不能保证原子计数器的值已经被最终将进行的所有着色器调用增加。
因此,您所看到的正是人们所希望看到的:调用写一些随机值,具体取决于调用恰好在其中执行的依赖于实现的顺序。
您要执行的是对所有调用执行所有原子增量,然后读取这些值并根据读取的内容绘制内容。您编写的代码无法做到这一点。
尽管计算着色器确实具有some ability to manipulate the order of execution of invocations,但这仅适用于同一工作组内的调用(实际上这就是存在工作组的原因)。也就是说,您可以在工作组中将调用按顺序排序,但不能在工作组之间进行排序。
最简单的解决方法是将其转换为2个计算着色器调度操作。首先进行所有增量。第二个将读取值并将结果写入图像。
更聪明的解决方案将涉及雇用工作组。就是说,对您的工作进行分组,以便使增加了相同原子计数器的所有内容都可以在同一工作组中执行。这样,您甚至不需要原子计数器。您只需使用共享变量(可以使用perform atomic operations)。完成共享变量的所有增量后,调用barrier()
;这样可以确保所有调用至少在执行任何操作之前就已经执行了。这样就完成了所有的增量操作。