什么是Compute Shader,它有何用处?

  • Compute Shader是一个能够利用通用的内存访问(即输入和输出),来进行任意运算的可编程shader
  • 它的优点是独立于渲染管线之外,可以利用它实现大量且并行的GPGPU(利用GPU进行非图形计算任务)算法,来加速游戏(也就是说可以用于优化)

语法介绍

因为在dx12部分已经介绍过compute shader了,所以这里不会深入原理

在开始前,还是简单介绍一下compute shader的语法

  • CS模型

    对于compute shader,在计算单元中以work group(线程组)作为单位;每个work group又包含多个work item,这些work item会填充SIMD单元;work domain划分多个work group,它指定了线程的工作区域,这些划分后的work group独立运行,work group之间可以互相通信

    更简单地,以Nvidia为例, 一个线程组含有n个线程,而硬件会将这些线程划分构成多个warp(SIMD,每个warp32个线程),每个warp都有相应的warp编排器负责warp的调度,多处理器会以SIMD32的方式处理warp,其中一个Core处理一个线程。在D3D中为了更优的性能,应将线程组的大小设置为warp的整数倍,若非如此会有warp被掺入无事可做的线程,造成浪费

  • 线程组

    • 线程组按三个维度进行分布(因为存在三维纹理,方便采样,可以理解为一个线程对应一个像素)

    每个线程组中的线程数最大个数为1024

    对于AMD,线程组大小设为64的倍数(WaveFront架构)

    对于Nvidia,线程组大小设为32的倍数(SIMD32(warp)架构)

    同一线程组中的线程可以同步,不同的不行

    • 分派线程组

    在dx12中,Dispatch函数定义如下

    void Dispatch(
      [in] UINT ThreadGroupCountX,  // x轴n个线程组
      [in] UINT ThreadGroupCountY,  // y轴n个线程组
      [in] UINT ThreadGroupCountZ   // z轴n个线程组
    );
    

    在Unity的URP管线下,Dispatch函数定义如下

    public void DispatchCompute(
          ComputeShader computeShader,  // 使用的computeShader
          int kernelIndex,  // 定义的kernel(computeShader)的Handle(句柄)
          int threadGroupsX,
          int threadGroupsY,
          int threadGroupsZ
    )
    {
        this.Internal_DispatchCompute(computeShader, kernelIndex, threadGroupsX, threadGroupsY, threadGroupsZ);
    }
    
  • 线程
    • 分派了线程组,还需要指定每个线程组中的线程数,同样也是三维

  • 语义
    • SV_GroupID:分派线程组时,系统会为每个线程组都分配一个ID,即线程组ID

    • SV_GroupThreadID:在线程组中,每个线程都会被指定一个组内(局部)的唯一ID(三维),即组内线程ID

    • SV_GroupIndex:与SV_GroupThreadID类似,不同在于它是一维

    计算公式:
    groupIndex = groupThreadID.z * ThreadGroupSize.x * ThreadGroupSize.y + groupThreadID.y * ThreadGroupSize.x + groupThreadID.x

    • SV_DispatchThreadID:Dispatch()会分派一个线程组网格(多个线程组),而同时每个线程组内的每个线程都会生成全局的唯一标识(标识它在work domain中的位置),即调度线程ID

实现后处理

  • 这里为了更方便地展示compute shader的写法,因此使用的例子很简单,就是调整亮度饱和度对比度

  • Shader

    #pragma kernel CSMain // 定义compute shader
    
    RWTexture2D<float4> _Result;  // 可读可写的RW结构化缓冲区
    Texture2D<float4> _Sour;  // 只可读
    
    float _Brightness;
    float _Saturation;
    float _Contrast;
    
    [numthreads(8,8,1)]   // 定义每个线程组中的线程数
    void CSMain (uint3 id : SV_DispatchThreadID)
    {
      _Result[id.xy] = _Sour[id.xy];
      _Result[id.xy] *= _Brightness;  // 明度
      float gray = _Result[id.xy].x * 0.21f + _Result[id.xy].y * 0.71f + _Result[id.xy].z * 0.08f;    // 灰度
      _Result[id.xy] = lerp(float4(gray, gray, gray, 1.f), _Result[id.xy], _Saturation);    // 饱和度
      _Result[id.xy] = lerp(float4(0.5f, 0.5f, 0.5f, 1.f), _Result[id.xy], _Contrast);    // 对比度
    }
    
    
  • RenderFeature
    using System;
    using UnityEngine;
    using UnityEngine.Rendering;
    using UnityEngine.Rendering.Universal;
    
    public class BSCPassFeature : ScriptableRendererFeature
    {
      // render feature 显示内容
      [System.Serializable]
      public class PassSetting
      {
          // 安插位置
          public RenderPassEvent m_passEvent = RenderPassEvent.AfterRenderingTransparents;
    
          // 指定compute shader
          public ComputeShader CS = null;
    
          // 明度控制
          [Range(0, 3)] 
          public float m_Brightness = 1;
    
          // 饱和度控制
          [Range(0, 3)]
          public float m_Saturation = 1;
    
          // 对比度控制
          [Range(0, 3)]
          public float m_Contrast = 1;
      }
    
      class BSCRenderPass : ScriptableRenderPass
      {
          // profiler tag will show up in frame debugger
          private const string m_ProfilerTag = "BSC Pass";
    
          // 用于存储pass setting
          private BSCPassFeature.PassSetting m_passSetting;
    
          private ComputeShader m_CS;
          private int kernal;   // compute shader中的kernal Handle
    
          private RenderTargetIdentifier m_SourRT, m_TargetRT;
    
          struct ShaderID
          {
              // int 相较于 string可以获得更好的性能,因为这是预处理的
              public static readonly int m_BrightnessID = Shader.PropertyToID("_Brightness");
              public static readonly int m_SaturationID = Shader.PropertyToID("_Saturation");
              public static readonly int m_ContrastID = Shader.PropertyToID("_Contrast");
              public static readonly int m_TargetRTID = Shader.PropertyToID("_BufferRT1");
          }
    
          // 线程组个数
          struct Dispatch
          {
              public static int ThreadGroupCountX;
              public static int ThreadGroupCountY;
              public static int ThreadGroupCountZ;
          }
    
          public BSCRenderPass(BSCPassFeature.PassSetting passSetting) 
          {
              this.m_passSetting = passSetting;
    
              renderPassEvent = m_passSetting.m_passEvent;
    
              this.m_CS = m_passSetting.CS;
    
              // 查找kernal handle
              kernal = m_CS.FindKernel("CSMain");
          }
    
          public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
          {
              m_SourRT = renderingData.cameraData.renderer.cameraColorTarget;
    
              RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
              descriptor.depthBufferBits = 0;
              descriptor.enableRandomWrite = true;    // 用于D3D的UAV(无序视图)
    
              // 因为是对屏幕进行处理,所以需要使得一个线程对应一个pixel
              Dispatch.ThreadGroupCountX = (int)descriptor.width / 8;
              Dispatch.ThreadGroupCountY = (int)descriptor.height / 8;
              Dispatch.ThreadGroupCountZ = 1;
    
              cmd.GetTemporaryRT(ShaderID.m_TargetRTID, descriptor, FilterMode.Bilinear);
              m_TargetRT = new RenderTargetIdentifier(ShaderID.m_TargetRTID);
          }
    
          public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
          {
              // Grab a command buffer. We put the actual execution of the pass inside of a profiling scope
              CommandBuffer cmd = CommandBufferPool.Get();
    
              using (new ProfilingScope(cmd, new ProfilingSampler(m_ProfilerTag)))
              {
                  // compute shader定义数据的方式和PS的不同
                  cmd.SetComputeFloatParam(this.m_CS, ShaderID.m_BrightnessID, m_passSetting.m_Brightness);
                  cmd.SetComputeFloatParam(this.m_CS, ShaderID.m_SaturationID, m_passSetting.m_Saturation);
                  cmd.SetComputeFloatParam(this.m_CS, ShaderID.m_ContrastID, m_passSetting.m_Contrast);
                  cmd.SetComputeTextureParam(this.m_CS, kernal, "_Result", m_TargetRT);
                  cmd.SetComputeTextureParam(this.m_CS, kernal, "_Sour", m_SourRT);
                  // 分派线程组并执行compute shader
                  cmd.DispatchCompute(this.m_CS, kernal, Dispatch.ThreadGroupCountX, Dispatch.ThreadGroupCountY, Dispatch.ThreadGroupCountZ);
    
                  cmd.Blit(m_TargetRT, m_SourRT);
              }
    
              context.ExecuteCommandBuffer(cmd);
              CommandBufferPool.Release(cmd);
          }
    
          public override void OnCameraCleanup(CommandBuffer cmd)
          {
              if(cmd == null) throw new ArgumentNullException("cmd");
    
              cmd.ReleaseTemporaryRT(ShaderID.m_TargetRTID);
          }
      }
    
      public PassSetting m_Setting = new PassSetting();
      BSCRenderPass m_BSCPass;
    
      /// <inheritdoc/>
      public override void Create()
      {
          m_BSCPass = new BSCRenderPass(m_Setting);
      }
    
      public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
      {
          // can queue up multiple passes after each other
          renderer.EnqueuePass(m_BSCPass);
      }
    }
    

Group Shared Memory

  • compute shader与同样运行在GPU上的fragment shader不同之处在于,compute shader的Thread Group中的每个Thread,都可以很快的访问对应的Group Shared Memory中的数据,这个效率比采样一张贴图来的高。因此在进行需要大量贴图采样的计算时(如高斯模糊),先将贴图数据缓存到Group Shared Memory中,再多次访问Group Shared Memory,这样运行的效率会高得多
  • 在后面的篇章笔者会介绍如何使用Group Shared Memory来加速一些模糊算法

他们曾如此骄傲的活过,贯彻始终