什么是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来加速一些模糊算法
Comments | NOTHING