GLSL Spinlock永远封锁

时间:2017-10-08 14:35:00

标签: c# opengl glsl

我正在尝试在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实现不会终止?

2 个答案:

答案 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使您的生活变得复杂并错误地使用了oldValenewValue

修改

如果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中的1lockImage值),而不使用bits。结果是lockImage更小。但是,所有着色器实例尝试更新与image中的32C值相关的32个lockImage坐标中的任何将等到锁定该值的人释放它

使用另一个lockImage2来锁定解锁32C值以进行位更新,似乎太过分了。

答案 1 :(得分:-1)

我写过关于如何在片段着色器中实现每像素互斥的文章以及代码。我想你可以参考一下。你做的很类似我在那里解释过的。我们走了:

克服绘制计数和每像素互斥

什么是透支计数?

主要是在嵌入式硬件上,性能下降的主要问题可能是透支。基本上,由于我们正在绘制的几何或场景的性质,屏幕上的一个像素被GPU多次着色,这称为过度绘制。有许多工具可以显示透支计数。

有关透支的详细信息?

当我们绘制一些顶点时,这些顶点将被转换为剪辑空间,然后转换为窗口坐标。然后,光栅化器将此坐标映射到像素/片段。然后,对于像素/片段,GPU调用像素着色器。可能存在我们绘制多个几何实例并将它们混合的情况。因此,这将在同一像素上多次绘制。这将导致过度绘制并可能降低性能。

避免透支的策略?

  1. 考虑Frustum剔除 - 对CPU进行视锥体剔除,以便不会渲染摄像机视野范围内的物体。

  2. 基于z排序对象 - 以这种方式从前到后绘制对象以供后续对象使用z测试失败并且片段不会被写入。

  3. 启用背面剔除 - 使用此功能,我们可以避免渲染背面朝向相机的脸部。

  4. 如果观察第2点,我们将以完全相反的顺序渲染混合。我们从后向前渲染。我们需要这样做,因为混合发生在z测试之后。如果任何片段失败z测试然后虽然它在后面我们应该仍然认为它是混合打开但是,该片段将被完全忽略给予artifacts.Hence我们需要从后到前维护顺序。因此,当启用混合时,我们会获得更多的透支计数。

    为什么我们需要Per Pixel Mutex?

    本质上GPU是平行的,因此像素的阴影可以并行完成。因此,有很多像素着色器实例一次运行。这个实例可能是阴影相同的像素,因此访问相同的像素。这可能会导致一些同步问题。这可能会产生一些不必要的影响。在这个应用程序中,我维护初始化为0的图像缓冲区中的透支计数。我所做的操作按以下顺序进行。

    1. 从图像缓冲区读取i像素的计数(第一次为零)
    2. 将1添加到步骤1中的计数器读取值
    3. 将计数器的新值存储在图像缓冲区中的第i个位置像素
    4. 正如我告诉你的那样,像素着色器的多个实例可能在同一个像素上工作,这可能会导致计数器变量的损坏。由于这些算法步骤不是原子的。我可以使用内置函数 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

      关于演示。

      在演示视频中,您可以看到我们正在渲染许多茶壶,并且正在进行混合。因此,强度更高的像素显示过度抽取计数很高。

      on youtube

      注意:在Android上,您可以在调试GPU选项中看到此过度抽取计数。

      来源:Per Pixel Mutex