我正在尝试在GLSL中实现一个Spinlock。它将用于Voxel Cone Tracing的上下文中。我尝试将存储锁定状态的信息移动到允许原子操作的单独3D纹理。为了不浪费内存,我不使用完整的整数来存储锁定状态,而只是一个位。问题是在不限制最大迭代次数的情况下,循环永远不会终止。我在C#中实现了完全相同的机制,创建了许多处理共享资源的任务,并且它完美地运行。 当我在SIMT设备上使用锁定时,可以提到可能存在的一些注意事项。书籍Euro Part 2017:Parallel Processing Page 274(可在Google上找到)。我认为代码应该绕过这些警告。
有问题的GLSL代码:
void imageAtomicRGBA8Avg(layout(RGBA8) volatile image3D image, layout(r32ui) volatile uimage3D lockImage,
ivec3 coords, vec4 value)
{
ivec3 lockCoords = coords;
uint bit = 1<<(lockCoords.z & (4)); //1<<(coord.z % 32)
lockCoords.z = lockCoords.z >> 5; //Division by 32
uint oldValue = 0;
//int counter=0;
bool goOn = true;
while (goOn /*&& counter < 10000*/)
//while(true)
{
uint newValue = oldValue | bit;
uint result = imageAtomicCompSwap(lockImage, lockCoords, oldValue, newValue);
//Writing is allowed if could write our value and if the bit indicating the lock is not already set
if (result == oldValue && (result & bit) == 0)
{
vec4 rval = imageLoad(image, coords);
rval.rgb = (rval.rgb * rval.a); // Denormalize
vec4 curValF = rval + value; // Add
curValF.rgb /= curValF.a; // Renormalize
imageStore(image, coords, curValF);
//Release the lock and set the flag such that the loops terminate
bit = ~bit;
oldValue = 0;
while (goOn)
{
newValue = oldValue & bit;
result = imageAtomicCompSwap(lockImage, lockCoords, oldValue, newValue);
if (result == oldValue)
goOn = false; //break;
oldValue = result;
}
//break;
}
oldValue = result;
//++counter;
}
}
使用相同功能的C#代码
public static void Test()
{
int buffer = 0;
int[] resource = new int[2];
Action testA = delegate ()
{
for (int i = 0; i < 100000; ++i)
imageAtomicRGBA8Avg(ref buffer, 1, resource);
};
Action testB = delegate ()
{
for (int i = 0; i < 100000; ++i)
imageAtomicRGBA8Avg(ref buffer, 2, resource);
};
Task[] tA = new Task[100];
Task[] tB = new Task[100];
for (int i = 0; i < tA.Length; ++i)
{
tA[i] = new Task(testA);
tA[i].Start();
tB[i] = new Task(testB);
tB[i].Start();
}
for (int i = 0; i < tA.Length; ++i)
tA[i].Wait();
for (int i = 0; i < tB.Length; ++i)
tB[i].Wait();
}
public static void imageAtomicRGBA8Avg(ref int lockImage, int bit, int[] resource)
{
int oldValue = 0;
int counter = 0;
bool goOn = true;
while (goOn /*&& counter < 10000*/)
{
int newValue = oldValue | bit;
int result = Interlocked.CompareExchange(ref lockImage, newValue, oldValue); //imageAtomicCompSwap(lockImage, lockCoords, oldValue, newValue);
if (result == oldValue && (result & bit) == 0)
{
//Now we hold the lock and can write safely
resource[bit - 1]++;
bit = ~bit;
oldValue = 0;
while (goOn)
{
newValue = oldValue & bit;
result = Interlocked.CompareExchange(ref lockImage, newValue, oldValue); //imageAtomicCompSwap(lockImage, lockCoords, oldValue, newValue);
if (result == oldValue)
goOn = false; //break;
oldValue = result;
}
//break;
}
oldValue = result;
++counter;
}
}
锁定机制应该与Cyril Crassin和Simon Green使用GPU硬件光栅化器在OpenGL Insigts第22章基于八叉树的稀疏体素化中描述的机制完全相同。他们只是使用整数纹理来存储我想要避免的每个体素的颜色,因为这会使Mip Mapping和其他事情变得复杂。 我希望这篇文章是可以理解的,我觉得它已经变得太长了......
为什么GLSL实现不会终止?
答案 0 :(得分:1)
如果我理解你,你可以使用lockImage
作为线程锁定:确定的coords处的确定值意味着“只有这个着色器实例可以执行下一个操作”(更改该坐标系中其他图像中的数据)。对。
关键是imageAtomicCompSwap
。我们知道它完成了这项工作,因为它能够存储确定的值(假设0
表示“免费”而1
表示“已锁定”)。我们知道它,因为返回值(原始值)是“免费”(即交换操作发生):
bool goOn = true;
unit oldValue = 0; //free
uint newValue = 1; //locked
//Wait for other shader instance to free the simulated lock
while ( goON )
{
uint result = imageAtomicCompSwap(lockImage, lockCoords, oldValue, newValue);
if ( result == oldValue ) //it was free, now it's locked
{
//Just this shader instance executes next lines now.
//Other instances will find a "locked" value in 'lockImage' and will wait
...
//release our simulated lock
imageAtomicCompSwap(lockImage, lockCoords, newValue, oldValue);
goOn = false;
}
}
我认为您的代码会永远循环,因为您使用bit
var使您的生活变得复杂并错误地使用了oldVale
和newValue
修改强>
如果lockImage
的'z'是32的倍数(只是一个理解的提示,不需要精确的倍数),你试图在一个整数中打包32个体素锁。我们称这个整数为32C
。
着色器实例(“SI”)可能想要更改32C
中的位,锁定或解锁。所以你必须(A)得到当前值,(B)只改变你的位。
其他SI正试图改变他们的位。一些具有相同的位,另一些具有不同的位。
在一个SI中对imageAtomicCompSwap
的两次调用之间,其他SI可能没有改变你的位(它被锁定,没有?)但是同一个32C
中的其他位值。你不知道哪个是当前值,你只知道你的位。因此,您在imageAtomicCompSwap
调用中没有任何内容(或旧的错误值)可供比较。它可能无法设置新值。几个SI失败导致“死锁”,而while循环永远不会结束。
您尝试避免在oldValue = result
之前使用旧的错误值并再次使用imageAtomicCompSwap
。这是我之前写过的(A) - (B)。但是在(A)和(B)之间,其他SI可能已经改变了result= 32C
值,破坏了你的想法。
<强> IDEA:强>
您可以使用我的简单方法(仅0
中的1
或lockImage
值),而不使用bits
。结果是lockImage
更小。但是,所有着色器实例尝试更新与image
中的32C
值相关的32个lockImage
坐标中的任何将等到锁定该值的人释放它
使用另一个lockImage2
来锁定解锁32C
值以进行位更新,似乎太过分了。
答案 1 :(得分:-1)
我写过关于如何在片段着色器中实现每像素互斥的文章以及代码。我想你可以参考一下。你做的很类似我在那里解释过的。我们走了:
主要是在嵌入式硬件上,性能下降的主要问题可能是透支。基本上,由于我们正在绘制的几何或场景的性质,屏幕上的一个像素被GPU多次着色,这称为过度绘制。有许多工具可以显示透支计数。
当我们绘制一些顶点时,这些顶点将被转换为剪辑空间,然后转换为窗口坐标。然后,光栅化器将此坐标映射到像素/片段。然后,对于像素/片段,GPU调用像素着色器。可能存在我们绘制多个几何实例并将它们混合的情况。因此,这将在同一像素上多次绘制。这将导致过度绘制并可能降低性能。
考虑Frustum剔除 - 对CPU进行视锥体剔除,以便不会渲染摄像机视野范围内的物体。
基于z排序对象 - 以这种方式从前到后绘制对象以供后续对象使用z测试失败并且片段不会被写入。
启用背面剔除 - 使用此功能,我们可以避免渲染背面朝向相机的脸部。
如果观察第2点,我们将以完全相反的顺序渲染混合。我们从后向前渲染。我们需要这样做,因为混合发生在z测试之后。如果任何片段失败z测试然后虽然它在后面我们应该仍然认为它是混合打开但是,该片段将被完全忽略给予artifacts.Hence我们需要从后到前维护顺序。因此,当启用混合时,我们会获得更多的透支计数。
本质上GPU是平行的,因此像素的阴影可以并行完成。因此,有很多像素着色器实例一次运行。这个实例可能是阴影相同的像素,因此访问相同的像素。这可能会导致一些同步问题。这可能会产生一些不必要的影响。在这个应用程序中,我维护初始化为0的图像缓冲区中的透支计数。我所做的操作按以下顺序进行。
正如我告诉你的那样,像素着色器的多个实例可能在同一个像素上工作,这可能会导致计数器变量的损坏。由于这些算法步骤不是原子的。我可以使用内置函数 imageAtomicAdd()。我想展示我们如何实现每像素互斥,所以我没有使用内置函数imageAtomicAdd()
。
#version 430
layout(binding = 0,r32ui) uniform uimage2D overdraw_count;
layout(binding = 1,r32ui) uniform uimage2D image_lock;
void mutex_lock(ivec2 pos) {
uint lock_available;
do {
lock_available = imageAtomicCompSwap(image_lock, pos, 0, 1);
} while (lock_available == 0);
}
void mutex_unlock(ivec2 pos) {
imageStore(image_lock, pos, uvec4(0));
}
out vec4 color;
void main() {
mutex_lock(ivec2(gl_FragCoord.xy));
uint count = imageLoad(overdraw_count, ivec2(gl_FragCoord.xy)).x + 1;
imageStore(overdraw_count, ivec2(gl_FragCoord.xy), uvec4(count));
mutex_unlock(ivec2(gl_FragCoord.xy));
}
Fragment_Shader.fs
在演示视频中,您可以看到我们正在渲染许多茶壶,并且正在进行混合。因此,强度更高的像素显示过度抽取计数很高。
注意:在Android上,您可以在调试GPU选项中看到此过度抽取计数。