手动深度渲染:尽管使用了原子操作,但结果仍然是随机的
Manual depth rendering: Random results despite using atomic operations
我正在使用计算着色器将单像素点渲染到 uint32 纹理中。纹理是 3d 纹理,x 和 y 是视口坐标,z 在坐标 0 上有深度信息,在 1 上有附加属性。所以如果你愿意的话,有两个手动构建的渲染目标。代码如下所示:
layout (r32ui, binding = 0) coherent volatile uniform uimage3D renderBuffer;
layout (rgba32f, binding = 1) restrict readonly uniform imageBuffer pointBuffer;
for(int j = 0; j < numPoints / gl_WorkGroupSize.x + 1; j++)
{
vec4 point = imageLoad(pointBuffer, ...)
// ... transform point ...
uint originalDepth = imageAtomicMin(renderBuffer, ivec3(imageCoords, 0), point.depth);
if (originalDepth >= point.depth)
{
// write happened, store the attributes
imageStore(renderBuffer, ivec3(imageCoords, 1), point.attributes);
}
}
虽然深度值正确,但我有几个像素的属性在两个值之间闪烁。
pointBuffer 中点的顺序是随机的(但我已经验证了所有点的集合总是相同的),所以我的第一个想法是两个相等的深度值可能会改变输出,这取决于哪个先来。所以我做到了,if originalDepth == point.depth
它使用 imageAtomicMax
来始终写入相同的两个替代属性,但没有任何改变。
我把 barrier()
和 memoryBarrier()
散落到各处,但这并没有改变什么。我还为此删除了所有发散的控制流,什么也没改变。
将本地工作大小减少到 32 可以消除 90% 的闪烁,但仍有一些闪烁。
如有任何想法,我们将不胜感激。
编辑:在你问我为什么手动做这些东西而不是使用正常的光栅化和片段着色器之前,原因是性能。光栅化器无济于事,因为我正在渲染单像素点,共享内存大大加快了速度,并且我多次渲染每个点,这需要我使用速度很慢的几何着色器。
问题是这样的:您在写入 renderBuffer
时遇到竞争条件。如果两个不同的 CS 调用映射到同一个像素,并且它们都决定写入该值,那么您的 imageStore
调用就会发生竞争。一个可能会覆盖另一个,可能是部分覆盖,也可能是完全覆盖。但无论如何,它不能保证有效。
这最好通过执行光栅化器所做的事情来解决:将过程分解为两个独立的阶段。第一阶段执行 ... transform point ...
部分,将数据写入缓冲区。第二阶段然后遍历这些点并将它们写入最终图像。
在第 2 阶段,每个 CS 调用对特定输出像素执行 所有 处理。这样,就没有竞争条件。当然,这要求阶段 1 以可以按像素排序的方式生成数据。
有几种方法可以解决后者。您可以使用链表,每个像素都有一个列表。或者您可以为每个工作组使用一个列表,其中一个工作组代表像素 space 的某个 X/Y 区域。在这种情况下,您将使用本地共享内存作为本地深度缓冲区,所有 CS 调用都会读取 from/writing 到该区域。在它们都处理完像素后,您将其写出到实际内存中。基本上,您将手动实施基于图块的渲染。
事实上,如果您有 很多 个这些点,基于图块的解决方案将允许您合并流水线,这样您就不必等到所有第 1 阶段在开始第 2 阶段的某些部分之前完成。您可以将第 1 阶段分解为多个块。您开始几个阶段 1 块,然后是从第一个阶段 1 读取的阶段 2 块,然后是另一个阶段 1,依此类推。
具有事件系统的 Vulkan 拥有比 OpenGL 更好的工具来构建如此高效的依赖链。
我正在使用计算着色器将单像素点渲染到 uint32 纹理中。纹理是 3d 纹理,x 和 y 是视口坐标,z 在坐标 0 上有深度信息,在 1 上有附加属性。所以如果你愿意的话,有两个手动构建的渲染目标。代码如下所示:
layout (r32ui, binding = 0) coherent volatile uniform uimage3D renderBuffer;
layout (rgba32f, binding = 1) restrict readonly uniform imageBuffer pointBuffer;
for(int j = 0; j < numPoints / gl_WorkGroupSize.x + 1; j++)
{
vec4 point = imageLoad(pointBuffer, ...)
// ... transform point ...
uint originalDepth = imageAtomicMin(renderBuffer, ivec3(imageCoords, 0), point.depth);
if (originalDepth >= point.depth)
{
// write happened, store the attributes
imageStore(renderBuffer, ivec3(imageCoords, 1), point.attributes);
}
}
虽然深度值正确,但我有几个像素的属性在两个值之间闪烁。
pointBuffer 中点的顺序是随机的(但我已经验证了所有点的集合总是相同的),所以我的第一个想法是两个相等的深度值可能会改变输出,这取决于哪个先来。所以我做到了,if originalDepth == point.depth
它使用 imageAtomicMax
来始终写入相同的两个替代属性,但没有任何改变。
我把 barrier()
和 memoryBarrier()
散落到各处,但这并没有改变什么。我还为此删除了所有发散的控制流,什么也没改变。
将本地工作大小减少到 32 可以消除 90% 的闪烁,但仍有一些闪烁。
如有任何想法,我们将不胜感激。
编辑:在你问我为什么手动做这些东西而不是使用正常的光栅化和片段着色器之前,原因是性能。光栅化器无济于事,因为我正在渲染单像素点,共享内存大大加快了速度,并且我多次渲染每个点,这需要我使用速度很慢的几何着色器。
问题是这样的:您在写入 renderBuffer
时遇到竞争条件。如果两个不同的 CS 调用映射到同一个像素,并且它们都决定写入该值,那么您的 imageStore
调用就会发生竞争。一个可能会覆盖另一个,可能是部分覆盖,也可能是完全覆盖。但无论如何,它不能保证有效。
这最好通过执行光栅化器所做的事情来解决:将过程分解为两个独立的阶段。第一阶段执行 ... transform point ...
部分,将数据写入缓冲区。第二阶段然后遍历这些点并将它们写入最终图像。
在第 2 阶段,每个 CS 调用对特定输出像素执行 所有 处理。这样,就没有竞争条件。当然,这要求阶段 1 以可以按像素排序的方式生成数据。
有几种方法可以解决后者。您可以使用链表,每个像素都有一个列表。或者您可以为每个工作组使用一个列表,其中一个工作组代表像素 space 的某个 X/Y 区域。在这种情况下,您将使用本地共享内存作为本地深度缓冲区,所有 CS 调用都会读取 from/writing 到该区域。在它们都处理完像素后,您将其写出到实际内存中。基本上,您将手动实施基于图块的渲染。
事实上,如果您有 很多 个这些点,基于图块的解决方案将允许您合并流水线,这样您就不必等到所有第 1 阶段在开始第 2 阶段的某些部分之前完成。您可以将第 1 阶段分解为多个块。您开始几个阶段 1 块,然后是从第一个阶段 1 读取的阶段 2 块,然后是另一个阶段 1,依此类推。
具有事件系统的 Vulkan 拥有比 OpenGL 更好的工具来构建如此高效的依赖链。