103.性能优化-GPU-着色器优化-ComputeShader-常用API
103.1 知识点
ComputeShader 常用 API 指的是什么
目前已经掌握了 Compute Shader 的核心知识点,已经能够使用 Compute Shader 来帮助我们完成计算需求。这节课来整理下 Compute Shader 中的常用 API(大部分学习过,补充部分新 API)。
C# 常用 API
以下 API 均通过 ComputeShader 或 ComputeBuffer 对象调用。
FindKernel — 查找入口函数索引
- 作用: 根据内核函数名获取其在 Compute Shader 中的索引,后续
Dispatch、SetBuffer、SetTexture等 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 value4×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)— 指定缓冲区类型(如Raw、Append)
SetData(T[] data)
- 作用:将 CPU 端数组数据上传到 GPU 缓冲区
- 参数:
T[] data或List<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 buffer或Texture 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 的三层内存层级:
- 寄存器 —— 每个线程独有,比如普通局部变量
- 组内共享内存 —— 每个线程组独立拥有,比如
groupshared变量 - 全局内存 —— 所有线程组共享,比如可读写数据流、纹理等
内存同步: 让之前写入的内存在后续访问时保证最新可见,不被缓存或乱序。
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