以下GLSL计算着色器只需将inImage
复制到outImage
。它源于更复杂的后处理过程。
在main()
的前几行中,单个线程将64像素的数据加载到共享数组中。然后,在同步之后,64个线程中的每一个将一个像素写入输出图像。
根据我的同步方式,我会得到不同的结果。我原本认为memoryBarrierShared()
是正确的调用,但它会产生以下结果:
与没有同步或使用memoryBarrier()
的结果相同。
如果我使用barrier()
,我会得到以下(所需)结果:
条带宽度为32像素,如果我将工作组大小更改为小于或等于32的任何值,我会得到正确的结果。
这里发生了什么?我误解了memoryBarrierShared()
的目的吗?为什么barrier()
有效?
#version 430
#define SIZE 64
layout (local_size_x = SIZE, local_size_y = 1, local_size_z = 1) in;
layout(rgba32f) uniform readonly image2D inImage;
uniform writeonly image2D outImage;
shared vec4 shared_data[SIZE];
void main() {
ivec2 base = ivec2(gl_WorkGroupID.xy * gl_WorkGroupSize.xy);
ivec2 my_index = base + ivec2(gl_LocalInvocationID.x,0);
if (gl_LocalInvocationID.x == 0) {
for (int i = 0; i < SIZE; i++) {
shared_data[i] = imageLoad(inImage, base + ivec2(i,0));
}
}
// with no synchronization: stripes
// memoryBarrier(); // stripes
// memoryBarrierShared(); // stripes
// barrier(); // works
imageStore(outImage, my_index, shared_data[gl_LocalInvocationID.x]);
}
答案 0 :(得分:22)
图像加载存储和朋友的问题是,实现不再能确定着色器仅更改其专用输出值的数据(例如片段着色器之后的帧缓冲)。这更适用于计算着色器,它没有专用输出,只能通过将数据写入可写存储(如图像,存储缓冲区或原子计数器)来输出内容。这可能需要在各个传递之间进行手动同步,否则尝试访问纹理的片段着色器可能没有将最新数据写入该纹理,并通过前一遍传递图像存储操作,如计算着色器。
因此,可能是您的计算着色器工作正常,但它是与以下显示(或任何)传递(需要以某种方式读取此图像数据)的失败同步。为此目的,存在glMemoryBarrier
函数。根据您在显示过程中读取图像数据的方式(或者更准确地说是在计算着色器通过后读取图像的过程),您需要为此函数指定不同的标志。如果您使用纹理阅读它,请使用GL_TEXTURE_FETCH_BARRIER_BIT
,如果您再次使用图片加载,请使用GL_SHADER_IMAGE_ACCESS_BARRIER_BIT
,如果使用glBlitFramebuffer
进行显示,请使用GL_FRAMEBUFFER_BARRIER_BIT
... < / p>
虽然我对图像加载/存储和手动内存同步没有太多经验,但这只是我理论上提出的。因此,如果有人知道更好或您已经使用了正确的glMemoryBarrier
,那么请随时纠正我。同样,这不是您唯一的错误(如果有的话)。但链接的Wiki文章中的最后两点实际上解决了您的用例,并且恕我直言明确表示您需要某种glMemoryBarrier
:
在一个渲染过程中写入图像变量并在稍后的过程中由着色器读取的数据不需要使用
coherent
变量或memoryBarrier()
。用{调用glMemoryBarrier
传球之间设置障碍的SHADER_IMAGE_ACCESS_BARRIER_BIT
是 必要的。着色器在一个渲染过程中写入的数据,并在稍后的传递中由另一个机制(例如,顶点或索引缓冲区拉动)读取 不要使用
coherent
个变量或memoryBarrier()
。调用glMemoryBarrier
在两者之间设置了适当的位 通行证是必要的。
编辑:实际上Wiki article on compute shaders说
共享变量访问使用非相干内存访问规则。 这意味着用户必须按顺序执行某些同步 确保共享变量可见。
共享变量都是隐式声明的
coherent
,所以你没有 需要(并且不能使用)该限定符。但是,你仍然需要 提供适当的记忆障碍。通常的一组内存屏障可用于计算着色器,但是 他们也可以访问
memoryBarrierShared();
这个障碍 专门用于共享变量排序。groupMemoryBarrier()
像memoryBarrier()
这样的行为,为所有类型的内存写入命令 变量,但它只为当前工作组命令读/写。虽然工作组中的所有调用都被称为“并行”执行,但这并不意味着您可以假设它们都是 在锁步中执行。如果您需要确保调用具有 写入某个变量,以便您可以阅读它,您需要 将执行与调用同步,而不仅仅是发出内存 障碍(你仍然需要记忆障碍)。
要在工作组内的调用之间同步读取和写入,必须使用
barrier()
函数。这迫使了 工作组中所有调用之间的显式同步。 工作组内的执行将不会继续进行 调用已经到了这个障碍。一旦超过barrier()
,全部 先前在。中的所有调用中写入的共享变量 小组将可见。
所以这实际上听起来你需要barrier
而memoryBarrierShared
是不够的(尽管你不需要两者,正如最后一句所说)。内存屏障只是同步内存,但它不会阻止线程的执行跨越它。因此,线程将不会从共享内存中读取任何旧的缓存数据如果第一个线程已经写了一些东西,但是它们很可能在之前读取第一个线程试图写任何东西。
这实际上完全符合以下事实:对于32块及以下的块尺寸,它可以工作,前32个像素可以工作。至少在NVIDIA硬件32上是扭曲尺寸,因此是完美锁定步骤中操作的线程数。所以前32个线程(好吧,每个32个线程的块)总是完全并行工作(好吧,从概念上讲),因此它们不会引入任何竞争条件。如果您知道自己在单个warp(一种常见的优化)中工作,那么实际上并不需要任何同步的情况也是如此。