UPL9-17-g——Compute Shader 常用 API

Compute Shader 常用 API

我们目前已经掌握了 Compute Shader 的核心知识点,已经能够使用 Compute Shader 来帮助我们来完成计算需求
这里整理 Compute Shader 中的常用 API (大部分学习过,补充部分新 API)

C# 常用 API

通过 ComputeShader 对象调用

  1. 查找入口函数索引

    1
    ComputeShader.FindKernel(string name)

    示例可见:UPL9-17——Compute Shader

  2. 按线程组数量启用GPU执行

    1
    ComputeShader.Dispatch(int kernelIndex, int x, int y, int z)

    示例可见:UPL9-17——Compute Shader

  3. 设置参数相关API

    1
    2
    3
    4
    5
    6
    7
    8
    ComputeShader.SetFloat(string name, float value)                                // 设置单个 float 参数
    ComputeShader.SetInt(string name, int value) // 设置 int 参数
    ComputeShader.SetBool(string name, bool value) // 设置 bool 参数
    ComputeShader.SetVector(string name, Vector4 value) // 设置 float2/float3/float4 类型
    ComputeShader.SetFloats(string name, params float[] values) // 设置 float2/float3 类型 或 数组
    ComputeShader.SetMatrix(string name, Matrix4x4 value) // 设置矩阵类型(float3x3/float4x4)
    ComputeShader.SetBuffer(int kernelIndex, string name, ComputeBuffer buffer) // 绑定结构化缓冲区
    ComputeShader.SetTexture(int kernelIndex, string name, Texture tex) // 绑定纹理

    相关内容可见:UPL9-17-c——数据输入规则

    设置常量缓冲区

    1
    ComputeShader.SetConstantBuffer(string name, ComputeBuffer buffer, int offset, int size)

    获取 [numthreads(x,y,z)] 的设置值

    1
    ComputeShader.GetKernelThreadGroupSizes(int kernelIndex, out uint x, out uint y, out uint z)
  4. ComputeBuffer 相关API

    1
    2
    3
    4
    5
    new ComputeBuffer(count, stride)                // 创建一个 GPU 缓冲区
    ComputeBuffer.SetData(T[] data) // 向 GPU 写入数据
    ComputeBuffer.GetData(T[] data) // 从 GPU 读取数据(同步操作)
    ComputeBuffer.Release() // 释放 GPU 缓冲区资源
    ComputeBuffer.CopyCount(buffer1,buffer2, 0) // 统计 GPU 动态生成的数据数量

    主要配合 AppendStructuredBuffer​(动态追加元素)和 ConsumeStructuredBuffer(动态消耗元素)使用

    相关内容可见:UPL9-17-c——数据输入规则,UPL9-17-f——获取 GPU 数据

    注意:新版本中有一个 GraphicsBuffer​ 类,它的用法和 ComputeBuffer 类似,它可以和 SRP 等管线更好地配合

  5. 异步获取

    1
    AsyncGPUReadback.Request(buffer, callback)

    相关内容可见:UPL9-17-f——获取 GPU 数据

  6. 判断设备是否支持 ComputeShader

    1
    SystemInfo.supportsComputeShaders
  7. 动态启用/关闭编译关键字(可用于多功能 ComputeShader

    1
    2
    ComputeShader.enableKeyword
    ComputeShader.DisableKeyword

    相关内容可参考:US5L3——Shader变体和关键字

Compoute Shader 内常用 API

  1. 入口函数参数的三个常用语义

    1
    2
    3
    SV_DispatchThreadID        // 调度线程 ID,每个线程的全局索引(系统自动传入)
    SV_GroupThreadID // 组内线程 ID,当前线程在组内的局部索引
    SV_GroupID // 线程组 ID,当前线程组在整个 Dispatch 中的索引

    相关内容可见:UPL9-17-e——参数的系统语义

  2. HLSL 内置函数

    1
    2
    3
    4
    5
    6
    7
    8
    lerp(a,b,t)                 // 线性插值
    step(edge,x) // 阶跃函数
    saturate(x) // 将值限制在 [0,1]
    smoothstep(min, max, x) // 平滑插值
    dot(a,b) // 向量点积
    cross(a,b) // 向量叉积
    length(x) // 向量长度
    normalize(x) // 向量单位化

    等等,具体可以参考:US2S3L12——CG内置函数

  3. 组内共享内存 - groupshared 关键字

    它可以让同一线程组内的所有线程共享一块高速、低延迟的片上内存,以便线程之间协作计算、减少显存访问
    groupshared​ 修饰的变量会被放入组内共享内存,组内所有线程都能访问、修改同一份数据
    每个组有自己的那一份;不同组之间互不干扰,比如:

    1
    groupshared float test[64];

    假设一个线程组 [numthreads(8,8,1)],每组64个线程
    那么每组线程都会有自己的一块共享内存

  4. 同步与内存屏障函数

    先解释一些概念:

    1. GPU 的 三层内存层级

      1. 寄存器 —— 每个线程独有,比如普通局部变量
      2. 组内共享内存 —— 每个线程组独立拥有,比如 groupshared 变量
      3. 全局内存 —— 所有线程组共享,比如可读写数据流、纹理等
    2. 内存同步

      让之前写入的内存在后续访问时保证最新可见,不被缓存或乱序,举例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      groupshared int data;

      [numthreads(2, 1, 1)]
      void CSMain(uint3 tid : SV_GroupThreadID)
      {
      if (tid.x == 0)
      data = 123;
      // 线程 1 可能此时还没看到 data=123
      // 因此我们可以在这等待内存同步完成(添加一些内存同步API)
      int v = data;
      }
    3. 线程同步

      让所有线程等一等,等大家都执行到同一位置后再继续执行

    同步函数:

    • GroupMemoryBarrierWithGroupSync()

      线程同步 + 内存同步(组内共享内存,groupshared​、当前组写的 RWBuffer​ / Texture​等,不跨线程组)
      让同一个线程组内部的所有线程在某个时刻进行汇合同步
      最常用的同步函数,一般所有涉及 groupshared​ 的协作计算都要用它
      假设一个线程组 [numthreads(8,8,1)]​,每组有 64 个线程,这些线程是并行执行的
      但是在某些时刻,我们可能希望该组所有线程都完成了当前阶段后,再一起进行下一阶段
      这时候我们就可以调用 GroupMemoryBarrierWithGroupSync() 函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      groupshared int data;

      [numthreads(2, 1, 1)]
      void CSMain(uint3 tid : SV_GroupThreadID)
      {
      if (tid.x == 0)
      data = 123;
      // 让所有线程都执行到这一步时,再执行继续并行执行后边的逻辑,并且此时内存也同步了
      GroupMemoryBarrierWithGroupSync();
      int v = data;
      }
    • GroupMemoryBarrier()

      内存同步(组内共享内存,groupshared​、当前组写的 RWBuffer​ / Texture等,不跨线程组)

    • DeviceMemoryBarrier()

      内存同步(全局内存同步,比如可读写的纹理和数据流,跨线程组),跨组的全局内存同步

    • AllMemoryBarrier()

      内存同步(组内共享内存 + 全局内存 都得同步,跨线程组),同步所有层级内存

    • AllMemoryBarrierWithGroupSync()

      最强版,线程同步 + 所有内存同步,等待组内所有线程汇合,确保所有类型内存(共享内存 + 全局内存)写入完成

  5. 原子操作

    原子操作用于并行计算时防止多个线程同时修改同一份数据造成的竞态条件
    指一个操作在 GPU 上执行时,不会被其他线程中断或同时修改同一内存地址

    举例:

    1
    2
    3
    4
    5
    6
    7
    RWStructuredBuffer<int> Counter;

    [numthreads(64, 1, 1)]
    void CSMain(uint3 id : SV_DispatchThreadID)
    {
    Counter[0] += 1; // 64个线程同时执行时,结果可能 Counter[0] < 64 !
    }
    1. dest 执行原子加法,并返回加前的值

      1
      InterlockedAdd(dest, value, out originalValue)

      示例:

      1
      2
      3
      4
      5
      6
      7
      8
      RWStructuredBuffer<int> Counter;

      [numthreads(64, 1, 1)]
      void CSMain(uint3 id : SV_DispatchThreadID)
      {
      int i;
      InterlockedAdd(Counter[0], 1, out i)
      }
    2. 原子地将 dest 设为新值,并返回加前的值

      1
      InterlockedExchange(dest, value, out originalValue)
    3. 比较后再交换 如果 dest == compareValue​,则写入 value

      1
      InterlockedCompareExchange(dest, compareValue, value, out originalValue)
    4. 原子地取最小值

      1
      InterlockedMin(dest, value, out originalValue)
    5. 原子地取最大值

      1
      InterlockedMax(dest, value, out originalValue)
    6. 原子按位与

      1
      InterlockedAnd(dest, value, out originalValue)
    7. 原子按位或

      1
      InterlockedOr(dest, value, out originalValue)
    8. 原子按位异或

      1
      InterlockedXor(dest, value, out originalValue)

    原子操作让对共享变量的 “读-改-写” 过程变成一个不可被打断的整体

    注意:原子操作会破坏 GPU 的并行性,影响 GPU 性能,需要谨慎使用