C# GLSL自旋锁永久阻塞
我正在尝试在GLSL中实现自旋锁。它将用于体素圆锥体跟踪。我尝试将存储锁定状态的信息移动到一个单独的3D纹理,该纹理允许原子操作。为了不浪费内存,我不使用完整的整数来存储锁状态,而只使用一个位。问题在于,如果不限制最大迭代次数,循环永远不会终止。我在C#中实现了完全相同的机制,创建了许多在共享资源上工作的任务,并且在那里工作得非常好。 EooPar 2017:并行处理页面274(可以在谷歌上找到)提到在SIMT设备上使用锁时可能的警告。我认为代码应该绕过这些警告 有问题的GLSL代码:C# GLSL自旋锁永久阻塞,c#,opengl,glsl,C#,Opengl,Glsl,我正在尝试在GLSL中实现自旋锁。它将用于体素圆锥体跟踪。我尝试将存储锁定状态的信息移动到一个单独的3D纹理,该纹理允许原子操作。为了不浪费内存,我不使用完整的整数来存储锁状态,而只使用一个位。问题在于,如果不限制最大迭代次数,循环永远不会终止。我在C#中实现了完全相同的机制,创建了许多在共享资源上工作的任务,并且在那里工作得非常好。 EooPar 2017:并行处理页面274(可以在谷歌上找到)提到在SIMT设备上使用锁时可能的警告。我认为代码应该绕过这些警告 有问题的GLSL代码: void
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;
}
}
void imageAtomicRGBA8Avg(布局(RGBA8)volatile image3D图像,布局(r32ui)volatile uimage3D锁图像,
ivec3坐标,vec4值)
{
ivec3锁定坐标=坐标;
uint bit=1如果我理解得很好,您可以使用lockImage
作为线程锁:在确定坐标处的确定值表示“只有此着色器实例可以执行下一个操作”(在该坐标处更改其他图像中的数据)。对。
键是imageAtomicCompSwap
。我们知道它完成了任务,因为它能够存储确定的值(比如0
表示“自由”,而1
表示“锁定”)。我们知道它是因为返回值(原始值)是“自由”(即发生了交换操作):
我认为你的代码会永远循环,因为你用bit
var使你的生活复杂化,并且错误地使用oldVale
和newValue
编辑:
如果lockImage
的“z”是32的倍数(只是一个理解提示,不需要精确的倍数),则尝试将32个体素锁打包成一个整数。我们将此整数称为32C
。
着色器实例(“SI”)可能需要更改其在32C
中的位、锁定或解锁。因此,必须(A)获取当前值,(B)仅更改位
其他SIs正在尝试改变他们的位。一些SIs使用相同的位,另一些SIs使用不同的位
在一个SI中对imageAtomicCompSwap
的两次调用之间,另一个SI可能没有更改您的位(它被锁定,没有?),而是更改了相同32C
值中的其他位。您不知道哪个是当前值,您只知道您的位。因此您没有任何内容(或旧的错误值)与imageAtomicCompSwap
调用中的进行比较。它可能无法设置新值。几个SIs失败会导致“死锁”,while循环永远不会结束
通过oldValue=result
并使用imageAtomicCompSwap
重试,您试图避免使用旧的错误值。这是我之前编写的(A)-(B)。但是在(A)和(B)之间,其他SI可能更改了result=32C
值,破坏了您的想法
创意:
您可以使用我的简单方法(仅使用lockImage
中的0
或1
值),没有位
东西。结果是锁图像
更小。但是所有试图更新与锁图像
中的32C
值相关的32图像
坐标的着色器实例将等待锁定该值的人释放该值
使用另一个lockImage2
来锁定解锁32C
值以进行一点更新,这似乎太令人费解了。我写了一篇关于如何在片段着色器中与代码一起实现每像素互斥的文章。我想你可以参考一下。你正在做的事情与我在这里解释的非常类似。现在我们开始:
超越画图计数和每像素互斥
透支数是多少?
主要是在嵌入式硬件上,性能下降的主要原因可能是过度绘制。基本上,由于我们正在绘制的几何体或场景的性质,屏幕上的一个像素会被GPU多次着色,这被称为过度绘制。有许多工具可以可视化过度绘制计数
透支的细节?
当我们绘制一些顶点时,这些顶点将被转换为剪辑空间,然后转换为窗口坐标。光栅化器然后将这些坐标映射到像素/片段。然后,对于像素/片段,GPU调用像素着色器。当我们绘制多个几何体实例并混合它们时,可能会出现这种情况。因此,这将在同一个多像素上进行绘制多次。这将导致透支,并可能降低性能
避免透支的策略?
考虑截头体消隐在CPU上进行截头体消隐,以便不会渲染摄影机视野之外的对象
基于z对对象进行排序-以这种方式从前到后绘制对象,以便以后的对象z测试将失败,并且不会写入片段
启用背面剔除-使用此选项,我们可以避免渲染面向摄影机的背面
如果你观察点2,我们正以相反的顺序渲染混合。我们需要从后面渲染到前面。我们需要这样做,因为在Z测试之后混合发生了。如果对于任何片段都失败了Z测试,那么尽管它在后面,我们仍然应该认为它是混合的,但是,这个片段将被完全忽略。ce我们需要从后到前维持订单。因此,当启用混合时,我们会得到更多的透支计数
为什么我们需要每像素互斥?
GPU本质上是并行的,所以像素着色可以并行进行。因此,一次运行多个像素着色器实例。这些实例可能会着色相同的像素,因此访问相同的像素。这可能会导致一些同步问题。这可能会产生一些不必要的效果。在这个应用程序中,我会维护overdraw count在图像缓冲区中初始化为0。我执行的操作顺序如下
读取i像素的计数
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;
}
}
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;
}
}
#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));
}