103.ComputeShader常用API

103.性能优化-GPU-着色器优化-ComputeShader-常用API


103.1 知识点

ComputeShader 常用 API 指的是什么

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

C# 常用 API

以下 API 均通过 ComputeShaderComputeBuffer 对象调用。

FindKernel — 查找入口函数索引

  • 作用: 根据内核函数名获取其在 Compute Shader 中的索引,后续 DispatchSetBufferSetTexture 等 API 都需要该索引。
  • 参数: string name — Compute Shader 中 #pragma kernel 声明的函数名
  • 返回值: int — 内核索引(找不到会抛出异常)
int index = computeShader.FindKernel("CSMain");

Dispatch — 按线程组数量启用 GPU 执行

  • 作用: 向 GPU 提交计算指令,启动指定数量的线程组执行目标内核。调用后 GPU 异步执行,不阻塞 CPU。
  • 参数:
    • int kernelIndex — 内核索引(由 FindKernel 获取)
    • int x, int y, int z — x / y / z 三个维度的线程组数量(均 >= 1)
  • 返回值:
computeShader.Dispatch(index, 64, 64, 1);

设置参数相关 API

所有 Set 方法都需要在 Dispatch 之前调用,变量名必须和 Compute Shader 中声明一致。

SetFloat — 设置 float 参数

  • 参数:string name 变量名,float value
  • 返回值:无
  • 对应 HLSL 类型:float

SetInt — 设置 int 参数

  • 参数:string name 变量名,int value
  • 返回值:无
  • 对应 HLSL 类型:int / uint

SetBool — 设置 bool 参数

  • 参数:string name 变量名,bool value
  • 返回值:无
  • 对应 HLSL 类型:bool

SetVector — 设置向量参数

  • 参数:string name 变量名,Vector4 value 四维向量
  • 返回值:无
  • 对应 HLSL 类型:float2 / float3 / float4(低维度时多余分量被忽略)

SetFloats — 设置浮点数组或低维向量

  • 参数:string name 变量名,params float[] values 可变长浮点参数
  • 返回值:无
  • 对应 HLSL 类型:float2 / float3 / float[] 数组(建议非 4 维向量用此 API 替代 SetVector

SetMatrix — 设置矩阵参数

  • 参数:string name 变量名,Matrix4x4 value 4×4 矩阵
  • 返回值:无
  • 对应 HLSL 类型:float4x4(小于 4×4 的矩阵需在 HLSL 侧截取转换)

SetBuffer — 绑定结构化缓冲区

  • 参数:int kernelIndex 内核索引,string name 缓冲区变量名,ComputeBuffer buffer 缓冲区对象
  • 返回值:无
  • 对应 HLSL 类型:StructuredBuffer<T> / RWStructuredBuffer<T>

SetTexture — 绑定纹理

  • 参数:int kernelIndex 内核索引,string name 纹理变量名,Texture tex 纹理对象
  • 返回值:无
  • 对应 HLSL 类型:Texture2D / RWTexture2D 等(可写纹理需 enableRandomWrite = true

SetConstantBuffer — 设置常量缓冲区

  • 参数:string name 常量缓冲区名,ComputeBuffer buffer 数据源,int offset 字节偏移,int size 字节大小
  • 返回值:无
  • 对应 HLSL 类型:cbuffer

GetKernelThreadGroupSizes — 获取线程组大小

  • 作用:查询指定内核的 [numthreads(x,y,z)] 设置值,可在 C# 侧动态计算 Dispatch 参数
  • 参数:int kernelIndex 内核索引,out uint x, out uint y, out uint z 输出的线程组维度
  • 返回值:无(通过 out 参数返回)
computeShader.GetKernelThreadGroupSizes(index, out uint threadX, out uint threadY, out uint threadZ);
int groupsX = Mathf.CeilToInt(width / (float)threadX);

ComputeBuffer 相关 API

构造函数 new ComputeBuffer(count, stride)

  • 作用:在 GPU 显存中分配一块缓冲区
  • 参数:int count 元素数量,int stride 单个元素字节数(必须与 GPU 侧结构体大小一致)
  • 可选重载:new ComputeBuffer(count, stride, ComputeBufferType type) — 指定缓冲区类型(如 RawAppend

SetData(T[] data)

  • 作用:将 CPU 端数组数据上传到 GPU 缓冲区
  • 参数:T[] dataList<T> data(类型和大小必须与构造时匹配)
  • 返回值:无

GetData(T[] data)

  • 作用:从 GPU 缓冲区同步读取数据到 CPU 端数组(会阻塞主线程)
  • 参数:T[] data 接收数据的数组
  • 返回值:无

Release()

  • 作用:释放 GPU 缓冲区占用的显存资源,避免内存泄漏。使用完毕后必须调用
  • 参数:无
  • 返回值:无

ComputeBuffer.CopyCount(src, dst, dstOffsetBytes)

  • 作用:将 AppendStructuredBuffer / ConsumeStructuredBuffer 的内部计数器值拷贝到目标缓冲区,用于统计 GPU 动态生成的数据数量
  • 参数:ComputeBuffer src 带计数器的源缓冲区,ComputeBuffer dst 目标缓冲区,int dstOffsetBytes 目标缓冲区中的字节偏移
  • 返回值:无

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

AsyncGPUReadback — 异步获取 GPU 数据

  • 作用: 异步读取 GPU 缓冲区或纹理数据,不阻塞主线程,读取完成后通过回调返回结果(最快下一帧返回)。
  • 参数:
    • ComputeBuffer bufferTexture src — 要读取的 GPU 资源
    • Action<AsyncGPUReadbackRequest> callback — 完成后的回调函数
  • 返回值: AsyncGPUReadbackRequest(可用于查询状态)
AsyncGPUReadback.Request(computeBuffer, (request) =>
{
    if (!request.hasError)
    {
        NativeArray<int> data = request.GetData<int>();
    }
});

SystemInfo.supportsComputeShaders — 判断设备支持

  • 作用: 检测当前运行设备是否支持 Compute Shader,建议在初始化时检测,不支持则降级处理。
  • 类型: bool(只读属性)
if (SystemInfo.supportsComputeShaders)
{
    // 使用 ComputeShader
}

EnableKeyword / DisableKeyword — 动态编译关键字

  • 作用: 动态启用或关闭 Compute Shader 中的编译关键字,可用于在同一个 Compute Shader 文件中实现多功能切换(类似 Shader 的 #pragma multi_compile)。
  • 参数: string keyword — 关键字名称
  • 返回值:
computeShader.EnableKeyword("USE_FEATURE_A");
computeShader.DisableKeyword("USE_FEATURE_A");

Compute Shader 内常用 API

参数三兄弟(系统语义)

语义 类型 说明
SV_DispatchThreadID uint3 全局线程 ID,每个线程在整个 Dispatch 中的唯一索引,可直接映射到数据下标
SV_GroupThreadID uint3 组内线程 ID,当前线程在所属线程组内的局部索引
SV_GroupID uint3 线程组 ID,当前线程组在整个 Dispatch 网格中的位置索引
[numthreads(8,8,1)]
void CSMain(uint3 id : SV_DispatchThreadID,
            uint3 groupId : SV_GroupID,
            uint3 groupThreadId : SV_GroupThreadID)
{
    // id = groupId * numthreads + groupThreadId
}

HLSL 内置数学函数

函数 参数 返回值 说明
lerp(a, b, t) a, b: 起止值;t: 插值因子 [0,1] 与 a 同类型 线性插值,返回 a + t * (b - a)
step(edge, x) edge: 阈值;x: 输入值 float 阶跃函数,x >= edge 返回 1,否则返回 0
saturate(x) x: 输入值 float 将值钳制到 [0, 1] 范围
smoothstep(min, max, x) min, max: 范围边界;x: 输入值 float Hermite 平滑插值,x < min 返回 0,x > max 返回 1,之间平滑过渡
dot(a, b) a, b: 同维向量 float 向量点积(标量积)
cross(a, b) a, b: float3 向量 float3 向量叉积,返回垂直于 a、b 的向量
length(x) x: 向量 float 返回向量的模(欧几里得长度)
normalize(x) x: 向量 与 x 同类型 返回单位向量(方向不变,长度归一)
abs(x) x: 标量或向量 与 x 同类型 返回绝对值
clamp(x, min, max) x: 输入值;min, max: 范围 与 x 同类型 将 x 钳制到 [min, max] 范围
pow(base, exp) base: 底数;exp: 指数 float 返回 base 的 exp 次幂
floor(x) / ceil(x) x: 浮点值 float 向下/向上取整

组内共享内存 — groupshared 关键字

  • 作用: 让同一线程组内的所有线程共享一块高速、低延迟的片上内存,以便线程之间协作计算、减少全局显存访问。
  • 特性:
    • groupshared 修饰的变量会被放入组内共享内存
    • 组内所有线程都能访问、修改同一份数据
    • 每个组有自己的那一份,不同组之间互不干扰
  • 限制: 共享内存总量有限(通常 16KB~32KB/组),不宜声明过大数组
groupshared float test[64];
// 假设一个线程组 [numthreads(8,8,1)],每组 64 个线程
// 那么每组线程都会有自己的一块共享内存

同步与内存屏障函数

先理解几个概念:

GPU 的三层内存层级:

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

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

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;
}

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

同步函数一览:

GroupMemoryBarrierWithGroupSync() — 最常用

  • 类型:线程同步 + 内存同步
  • 同步范围:组内共享内存(groupshared、当前组写的 RWBuffer/Texture 等,不跨线程组)
  • 作用:让同一个线程组内部的所有线程在此处汇合,确保组内共享内存写入完成后再继续。一般所有涉及 groupshared 的协作计算都要用它
  • 参数:无
  • 返回值:无

GroupMemoryBarrier()

  • 类型:仅内存同步(不等待线程汇合)
  • 同步范围:组内共享内存(groupshared、当前组写的 RWBuffer/Texture 等,不跨线程组)
  • 参数:无
  • 返回值:无

DeviceMemoryBarrier()

  • 类型:仅内存同步
  • 同步范围:全局内存(可读写的纹理和数据流,跨线程组)
  • 参数:无
  • 返回值:无

AllMemoryBarrier()

  • 类型:仅内存同步
  • 同步范围:组内共享内存 + 全局内存,跨线程组
  • 参数:无
  • 返回值:无

AllMemoryBarrierWithGroupSync() — 最强版

  • 类型:线程同步 + 所有内存同步
  • 同步范围:组内共享内存 + 全局内存,跨线程组
  • 作用:等待组内所有线程汇合,确保所有类型内存(共享内存 + 全局内存)写入完成
  • 参数:无
  • 返回值:无

原子操作

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

RWStructuredBuffer<int> Counter;
[numthreads(64, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    Counter[0] += 1;   // 64 个线程同时执行时,结果可能 < 64!
}

常用原子操作 API:

InterlockedAdd(dest, value, out originalValue)

  • 作用:对 dest 执行原子加法
  • 参数:dest 目标内存地址,value 要加的值,originalValue 操作前的原始值
  • 返回值:通过 out 参数返回操作前的值

InterlockedExchange(dest, value, out originalValue)

  • 作用:原子地将 dest 设为新值
  • 参数:dest 目标内存地址,value 新值,originalValue 操作前的原始值
  • 返回值:通过 out 参数返回操作前的值

InterlockedCompareExchange(dest, compareValue, value, out originalValue)

  • 作用:比较后再交换(CAS 操作),如果 dest == compareValue 则写入 value,否则不修改
  • 参数:dest 目标内存地址,compareValue 比较值,value 待写入值,originalValue 操作前的原始值
  • 返回值:通过 out 参数返回操作前的值

InterlockedMin(dest, value, out originalValue)

  • 作用:原子地将 dest 更新为 min(dest, value)
  • 参数:dest 目标内存地址,value 比较值,originalValue 操作前的原始值

InterlockedMax(dest, value, out originalValue)

  • 作用:原子地将 dest 更新为 max(dest, value)
  • 参数:dest 目标内存地址,value 比较值,originalValue 操作前的原始值

InterlockedAnd(dest, value, out originalValue)

  • 作用:原子按位与,dest = dest & value
  • 参数:dest 目标内存地址,value 按位与值,originalValue 操作前的原始值

InterlockedOr(dest, value, out originalValue)

  • 作用:原子按位或,dest = dest | value
  • 参数:dest 目标内存地址,value 按位或值,originalValue 操作前的原始值

InterlockedXor(dest, value, out originalValue)

  • 作用:原子按位异或,dest = dest ^ value
  • 参数:dest 目标内存地址,value 按位异或值,originalValue 操作前的原始值

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


103.2 知识点代码

Lesson103_性能优化_GPU_着色器优化_ComputeShader_常用API.cs

public class Lesson103_性能优化_GPU_着色器优化_ComputeShader_常用API
{
    void Start()
    {
        #region 知识点一 ComputeShader 常用 API 指的是什么

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

        #endregion

        #region 知识点二 C# 常用 API

        // 通过 ComputeShader 对象调用

        // 1. 查找入口函数索引
        //    FindKernel(string name)

        // 2. 按线程组数量启用 GPU 执行
        //    Dispatch(int kernelIndex, int x, int y, int z)

        // 3. 设置参数相关 API
        //    SetFloat(string name, float value) 设置单个 float 参数
        //    SetInt(string name, int value) 设置 int 参数
        //    SetBool(string name, bool value) 设置 bool 参数
        //    SetVector(string name, Vector4 value) 设置 float2/float3/float4 类型
        //    SetFloats(string name, params float[] values) 设置 float2/float3 类型或数组
        //    SetMatrix(string name, Matrix4x4 value) 设置矩阵类型(float3x3/float4x4)
        //    SetBuffer(int kernelIndex, string name, ComputeBuffer buffer) 绑定结构化缓冲区
        //    SetTexture(int kernelIndex, string name, Texture tex) 绑定纹理
        //    设置常量缓冲区
        //    SetConstantBuffer(string name, ComputeBuffer buffer, int offset, int size)
        //    获取 [numthreads(x,y,z)] 的设置值
        //    GetKernelThreadGroupSizes(int kernelIndex, out uint x, out uint y, out uint z)

        // 4. ComputeBuffer 相关 API
        //    new ComputeBuffer(count, stride) 创建一个 GPU 缓冲区
        //    SetData(T[] data) 向 GPU 写入数据
        //    GetData(T[] data) 从 GPU 读取数据(同步操作)
        //    Release() 释放 GPU 缓冲区资源
        //    CopyCount(buffer1, buffer2, 0) 统计 GPU 动态生成的数据数量
        //    主要配合 AppendStructuredBuffer(动态追加元素)和 ConsumeStructuredBuffer(动态消耗元素)使用
        //    注意:
        //    新版本中有一个 GraphicsBuffer 类,它的用法和 ComputeBuffer 类似
        //    它可以和 SRP 等更好地配合

        // 5. 异步获取
        //    AsyncGPUReadback.Request(buffer, callback)

        // 6. 判断设备是否支持 ComputeShader
        //    SystemInfo.supportsComputeShaders

        // 7. 动态启用/关闭编译关键字(可用于多功能 ComputeShader)
        //    ComputeShader.EnableKeyword / DisableKeyword

        #endregion

        #region 知识点三 Compute Shader 内常用 API

        // 1. 参数三兄弟
        //    SV_DispatchThreadID、SV_GroupThreadID、SV_GroupID

        // 2. HLSL 内置函数
        //    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)             向量单位化
        //    等等

        // 3. 组内共享内存 - groupshared 关键字
        //    它可以让同一线程组内的所有线程共享一块高速、低延迟的片上内存
        //    以便线程之间协作计算、减少显存访问
        //    用 groupshared 修饰的变量会被放入组内共享内存,组内所有线程都能访问、修改同一份数据
        //    每个组有自己的那一份;不同组之间互不干扰
        //    比如:
        //    groupshared float test[64];
        //    假设一个线程组 [numthreads(8,8,1)],每组 64 个线程
        //    那么每组线程都会有自己的一块共享内存

        // 4. 同步与内存屏障函数
        //    几个概念
        //    4-1. GPU 的三层内存层级
        //         1. 寄存器 —— 每个线程独有,比如普通局部变量
        //         2. 组内共享内存 —— 每个线程组独立拥有,比如 groupshared 变量
        //         3. 全局内存 —— 所有线程组共享,比如可读写数据流、纹理等
        //    4-2. 内存同步:
        //         让之前写入的内存在后续访问时保证最新可见,不被缓存或乱序
        //         举例:
        //         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;
        //         }
        //    4-3. 线程同步:
        //         让所有线程等一等,等大家都执行到同一位置后再继续执行

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

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

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

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

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

        // 5. 原子操作
        //    并行计算时防止多个线程同时修改同一份数据造成的竞态条件
        //    指一个操作在 GPU 上执行时,不会被其他线程中断或同时修改同一内存地址
        //    举例:
        //    RWStructuredBuffer<int> Counter;
        //    [numthreads(64, 1, 1)]
        //    void CSMain(uint3 id : SV_DispatchThreadID)
        //    {
        //        Counter[0] += 1;   // 64 个线程同时执行时,结果可能 < 64!
        //    }
        //    常用 API
        //    5-1. 对 dest 执行原子加法,并返回加前的值
        //         InterlockedAdd(dest, value, out originalValue)
        //    5-2. 原子地将 dest 设为新值,并返回加前的值
        //         InterlockedExchange(dest, value, out originalValue)
        //    5-3. 比较后再交换,如果 dest==compareValue 则写入 value
        //         InterlockedCompareExchange(dest, compareValue, value, out originalValue)
        //    5-4. 原子地取最小值
        //         InterlockedMin(dest, value, out originalValue)
        //    5-5. 原子地取最大值
        //         InterlockedMax(dest, value, out originalValue)
        //    5-6. 原子按位与
        //         InterlockedAnd(dest, value, out originalValue)
        //    5-7. 原子按位或
        //         InterlockedOr(dest, value, out originalValue)
        //    5-8. 原子按位异或
        //         InterlockedXor(dest, value, out originalValue)
        //    原子操作让对共享变量的"读-改-写"过程变成一个不可被打断的整体

        #endregion
    }
}

Lesson103_ComputeShader.compute

// 声明计算着色器的内核(Kernel),指定要编译执行的函数为 CSMain
#pragma kernel CSMain

// 可读写的 2D 纹理(RWTexture2D),用于存储计算输出的颜色结果
// 需在 C# 脚本中绑定启用了 enableRandomWrite 标志的 RenderTexture
RWTexture2D<float4> Result;

// 定义线程组大小:每个线程组包含 8×8×1 = 64 个线程,沿 x、y 轴二维排列
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // id:当前线程的全局坐标 ID(SV_DispatchThreadID 语义标识,对应线程在所有线程组中的全局位置)
    // 根据线程的全局 x、y 坐标生成渐变颜色:
    // 红色通道 = x/512.0(从左到右渐变),绿色通道 = y/512.0(从上到下渐变)
    // 蓝色通道 = 0,Alpha 通道 = 1(完全不透明)
    Result[id.xy] = float4(id.x / 512.0, id.y / 512.0, 0, 1);

    // 组内内存屏障与同步:
    // 确保同一线程组内的所有线程都执行到此处,且组内共享内存同步后,再继续执行后续逻辑
    GroupMemoryBarrierWithGroupSync();

    // 示例:将所有像素值覆盖为指定颜色
    Result[id.xy] = 123123;
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏