有没有更好的方法来同步计算着色器的所有调用?

时间:2014-06-07 06:33:02

标签: opengl synchronization glsl

我在计算着色器中实现了一个算法

  1. 对于图像中的每个像素
    • 计算某些内容并将其存储到临时图像
  2. 对于图像中的每个像素
    • 等到所有8个附近像素都完成了第1步
    • 从与其附近的8个像素对应的临时图像中读取数据
    • 使用它们来计算结果
  3. 我的工作组尺寸设置为layout (local_size_x = 256) in;glDispatchCompute(1, 256, 1);
    在步骤2中读取临时图像之前,每个像素都要求其所有8个邻居都已完成步骤1.所以我在步骤1和步骤2之间放置了一个memoryBarrier(),因为OpenGL Programming Guide, 8th Editionmemory barrier functions apply globally,而不仅仅是同一个地方工作组 但这并不像预期的那样有效。

    为了证明结果,请考虑一个简化但类似的问题,

    1. 在白色图像上绘制黑色矩形
    2. 对于图像中的每个像素
      • 如果它是黑色,则将1存储到临时图像
      • 否则将0存储到临时图像
    3. 对于图像中的每个像素
      • 如果它的黑色或其附近的8个像素中的至少一个是黑色,则将其自身设置为黑色
    4. 这会导致黑色矩形变得越来越大。 但结果是,矩形在变大时会变形。

      那么,memoryBarrier()真的要等到同一个glDispatchCompute调用触发的所有调用完成内存访问吗?

      在第2步和第3步之间实施锁 后,结果按预期工作。(但后来我发现有时会因为超出Windows时间而导致程序崩溃 - 限制!http://nvidia.custhelp.com/app/answers/detail/a_id/3007
      (p是当前位置,p + e [i]是它附近的8个像素&#39;位置。我使用着色器存储缓冲区对象代替图像变量,所以我添加了一个函数posi()来将ivec2转换为数组索引)< / p>

      bool finished;
      do
      {
          finished = true;
          for(int i = 1; i < 9; i++)
          {
              if(!outOfBound(p+e[i]) && lock[posi(p+e[i])] != 1)
              {
                  finished = false;
              }
          }
      }while(!finished);
      

      如果我错误地使用了memoryBarrier()并且它无法执行我想要的操作,是否有更好的方法来同步计算着色器的调用?

      更新以添加计算着色器代码

      这是我上面描述的黑色矩形示例的计算着色器代码:
      实际上标记是用于判断像素的颜色是黑色还是白色的图像,它在白色背景上初始化为一个小的黑色矩形。 在运行此计算着色器之前, temp 设置为零。 评论的代码是关于上面描述的锁。使用此锁定,着色器将提供所需的输出。

      #version 430 core
      
      layout (local_size_x = 256) in;
      
      const ivec2 e[9] = {
          ivec2(0,0),
          ivec2(1,0), ivec2(0,1), ivec2(-1,0), ivec2(0,-1), 
          ivec2(1,1), ivec2(-1,1), ivec2(-1,-1), ivec2(1,-1)
      };
      
      layout(std430, binding = 14) coherent buffer tag_buff
      {
          int tag[];
      };
      layout(std430, binding = 15) coherent buffer temp_buff
      {
          int temp[];
      };
      layout(std430, binding = 16) coherent buffer lock_buff
      {
          int lock[];
      };
      
      int posi(ivec2 point)
      {
          return point.y * 256 + point.x;
      }
      
      bool outOfBound(ivec2 p)
      {
          return p.x < 0 || p.x >= 256
              || p.y < 0 || p.y >= 256;
      }
      
      void main()
      {
          ivec2 p = ivec2(gl_GlobalInvocationID.xy);
      
          int x = tag[posi(p)];
          temp[posi(p)] = x;
          //lock[posi(p)] = 1;
      
          memoryBarrier();
      
          //bool finished;
          //do
          //{
          //    finished = true;
          //    for(int i = 1; i < 9; i++)
          //    {
          //        if(!outOfBound(p+e[i]) && lock[posi(p+e[i])] != 1)
          //        {
          //            finished = false;
          //        }
          //    }
          //}while(!finished);
      
          // if it's black or at least one of its 8 nearby pixel is black
          // set itself to black
          for(int i = 0; i < 9; i++)
          {
              if(!outOfBound(p+e[i]) && temp[posi(p+e[i])] == 1)
              {
                  tag[posi(p)] = 1;
              }
          }
      }
      

      后来我尝试将lock设置为另一个ssbo,然后将其元素设置为1并调用memoryBarrier(),然后在片段着色器中加载新的ssbo并将其打印到屏幕上,我从中发现了一些lock的元素尚未设置为1。 我还在片段着色器或计算着色器中使用图像变量而不是ssbo,只是为了找到memoryBarrier而且相干无法改变任何东西。看起来memoryBarrier或连贯似乎不起作用。

      在阅读了几篇文章后,似乎我知道这里有什么快乐,我在下面发表我的理解。如果不是,请纠正我。

      memoryBarrier无法通过同步内存访问来同步调用。更具体地说,memoryBarrier究竟在做什么只是等待已经在调用中发生的所有内存访问 。即使它在源代码中的memoryBarrier之前,它也不会等待内存访问代码完成但尚未执行。 Opengl编程指南说When memoryBarrier() is called, it ensures that any writes to memory that have been performed by the shader invocation have been committed to memory rather than lingering in caches or being scheduled after the call to memoryBarrier()。这意味着,例如,假设有三个调用,如果调用A和B都为coherent图像变量运行了imageStore(),那么A或B的后续memoryBarrier将保证此imageStore()已更改主内存中的数据,而不仅仅是缓存。但是如果调用C在A或B调用memoryBarrier时没有运行imageStore(),则此memoryBarrier调用将不会等待C运行其imageStore()。因此memoryBarrier无法帮助我实施该算法。

1 个答案:

答案 0 :(得分:3)

我遇到了类似的问题。我不是专家,但我相信我找到了一个很好的解决方案。

您已正确识别memoryBarrier以确保先前写入的可见性。

但是,它自己的memoryBarrier几乎没用,因为它不能确保执行顺序。因此,尽管您有一个memoryBarrier,但在其他人开始运行之前,可能会有完全完成的调用。 memoryBarrier无法显示尚未发生的写入。

我们有barrier来解决这个问题:

  

对于计算着色器中任何给定的静态屏障实例,所有在单个工作组中的调用必须先输入它,然后才允许任何调用继续超出它。

请注意重点:barrier 帮助您在一个glDispatchCompute调用中同步工作组,它只在工作组内同步。

显然,barrier对您的问题没有帮助, 所以你介绍了自己的障碍,它有缺点:

  1. 编译器/驱动程序/调度程序不知道它是一个障碍因此无法进行优化。
  2. 你的屏障使用旋转锁 占用处理器。这会延长看门狗定时器触发之前的运行时间。
  3. 如果驾驶员知道障碍物,它可以安排那些尚未到达障碍的调用。在您的解决方案中,驱动程序盲目地调度所有调用,在已经等待的调用上浪费资源,而不是运行那些尚未到达障碍的调用。

    该怎么做?

    解决方案

    要在所有调用中实现屏障,只需将多个glDispatchCompute与适当的glMemoryBarrier电话交错。

    分成多个glDispatchCompute电话会在它们之间形成障碍。 glMemoryBarrier使以前调用的写入对后来的可见。