基础概念
什么是 Compute Shader?
Compute Shader(计算着色器)是运行在 GPU 上、独立于传统渲染管线之外的通用计算程序。它可以充分利用 GPU 的大规模并行架构执行 GPGPU(通用 GPU 计算)算法,也可以加速渲染管线中的特定阶段。
单次 Dispatch 可以启动数十万个线程
不依赖顶点/片元着色器,直接读写资源
通过 RWTexture / RWStructuredBuffer 直接写入 GPU 资源
计算结果可直接被渲染着色器消费,无需回读 CPU
API 生态概览
| API | 平台 | 着色语言 | Unity 支持 |
|---|---|---|---|
| DirectCompute | Windows / DX11+ | HLSL | ✅ 完整 |
| OpenGL Compute | 跨平台 | GLSL | ✅ 完整 |
| Metal | iOS / macOS | Metal SL | ✅ 完整 |
| Vulkan | Android / PC | SPIR-V / GLSL | ✅ 完整 |
| OpenGL ES 3.1 | Android | GLSL ES | ⚠️ 受限 |
| CUDA / OpenCL | NVIDIA / AMD | CUDA C / OpenCL C | ❌ 不支持 |
典型应用场景
线程模型 · Thread Hierarchy
三层层级结构
Compute Shader 的执行单元分为三层,从上到下依次为:Dispatch → Thread Group → Thread。
由
[numthreads(tX, tY, tZ)] 声明可访问 Group Shared Memory
总线程数:
$$N_{total} = (g_X \times g_Y \times g_Z) \times (t_X \times t_Y \times t_Z)$$系统值语义(System Value Semantics)
| 语义 | 类型 | 含义 | 计算公式 |
|---|---|---|---|
SV_GroupID |
uint3 | 当前线程所在线程组 ID,范围 (0,0,0) → (gX-1, gY-1, gZ-1) |
— |
SV_GroupThreadID |
uint3 | 线程在组内的 ID,范围 (0,0,0) → (tX-1, tY-1, tZ-1) |
— |
SV_DispatchThreadID |
uint3 | 线程在全局所有线程中的 ID | $SV\_GroupID \times (t_X,t_Y,t_Z) + SV\_GroupThreadID$ |
SV_GroupIndex |
uint | 线程在组内的扁平化索引,范围 0 → tX·tY·tZ-1 |
$k \cdot t_X \cdot t_Y + j \cdot t_X + i$ |
#pragma kernel CSMain
RWTexture2D<float4> Result;
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
// id.xy 直接映射到纹理像素坐标
float2 uv = id.xy / float2(1024.0, 1024.0);
Result[id.xy] = float4(uv, 0.0, 1.0);
}
numthreads 选择策略
[numthreads(8, 8, 1)]
每组 64 线程,适合纹理像素映射,X/Y 对应 UV
[numthreads(64, 1, 1)]
单维度处理,粒子、顶点数组的标准选择
[numthreads(4, 4, 4)]
处理体积云、3D 噪声等三维数据结构
总线程数 = 32 的倍数
NVIDIA GPU 每 Warp 32 线程,AMD 每 Wave 64 线程,保持对齐避免浪费
Dispatch 组数计算(覆盖整张纹理):
$$dispatchX = \lceil \frac{texWidth}{t_X} \rceil, \quad dispatchY = \lceil \frac{texHeight}{t_Y} \rceil$$Unity C#: Mathf.CeilToInt(width / 8f)
硬件执行架构 · Warp / Wavefront
GPU 并非逐线程调度,而是以 Warp(NVIDIA)/ Wavefront(AMD)/ SIMD Group(Apple) 为最小执行单位,将一批线程捆绑在一起同步执行。理解这一机制是写出高效 Compute Shader 的关键。
GPU 内存层级(速度从快到慢)
groupshared,组内所有线程复用这份缓存,最后一次性写回 Global,是减少显存带宽消耗的标准模式。缓冲区与资源类型
资源类型总览
enableRandomWrite = true,是 GPU 输出的核心载体。.Append() 实现 GPU 端动态列表,用于 Indirect 过滤模式。.Consume() 从 Append Buffer 中弹出元素。RWByteAddressBuffer 支持原子操作,用于计数器和间接参数。GroupMemoryBarrierWithGroupSync() 同步。ComputeBuffer 创建与管理(Unity)
// 创建:指定元素数量和每个元素的字节大小
// sizeof(float3) = 12 bytes
ComputeBuffer buffer = new ComputeBuffer(count, 12);
// 上传数据 CPU → GPU
buffer.SetData(myArray);
// 绑定到 Compute Shader
computeShader.SetBuffer(kernelIndex, "particleBuffer", buffer);
// 绑定到普通 Shader(只读,作为 StructuredBuffer)
material.SetBuffer("particleBuffer", buffer);
// 读回数据 GPU → CPU(同步,会造成 GPU stall!)
buffer.GetData(myArray);
// 释放(必须手动释放,否则内存泄漏)
buffer.Release();
// 默认结构化 Buffer
new ComputeBuffer(count, stride, ComputeBufferType.Default);
// Append/Consume 类型(动态列表)
new ComputeBuffer(count, stride, ComputeBufferType.Append);
// Indirect Dispatch 参数 Buffer(4个 uint)
new ComputeBuffer(1, 16, ComputeBufferType.IndirectArguments);
// 原子计数器
new ComputeBuffer(1, sizeof(uint), ComputeBufferType.Counter);
// 复制 Append Buffer 的计数值到另一个 Buffer(用于 Indirect)
ComputeBuffer.CopyCount(appendBuffer, argsBuffer, dstOffsetBytes);
RWTexture2D 创建(Unity)
RenderTexture rt = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat);
rt.enableRandomWrite = true; // ← 关键:开启 UAV 绑定
rt.Create();
// 绑定到 Compute Shader
computeShader.SetTexture(kernel, "Result", rt);
// 绑定到 Material(直接用于渲染)
material.SetTexture("_MainTex", rt);
同步机制 · Synchronization
为什么需要同步?
GPU 线程是高度并行的,同一个线程组内的线程执行顺序不保证。当多个线程读写同一内存位置(共享内存或 UAV)时,若没有屏障保护,会产生 Race Condition(数据竞争)。
groupshared 数组,线程 B 已开始读取 → 未定义行为,结果不可预测。
HLSL 同步函数
| 函数 | 同步范围 | 内存屏障范围 | 典型用途 |
|---|---|---|---|
GroupMemoryBarrier() |
— (仅屏障) | GroupShared Memory | 保证组内共享内存写入对其他线程可见 |
GroupMemoryBarrierWithGroupSync() |
组内所有线程 | GroupShared Memory | 组内规约(reduce)、前缀和等算法的核心 |
DeviceMemoryBarrier() |
— (仅屏障) | UAV(全局内存) | 保证 UAV 写入对其他线程可见 |
DeviceMemoryBarrierWithGroupSync() |
组内所有线程 | UAV(全局内存) | 跨线程组数据依赖(较少见) |
AllMemoryBarrier() |
— (仅屏障) | 全部内存(SM + UAV) | 全内存可见性屏障 |
AllMemoryBarrierWithGroupSync() |
组内所有线程 | 全部内存(SM + UAV) | 最重量级屏障,确保所有内存操作完成 |
WithGroupSync 的函数相当于:内存屏障 + 线程栅栏(Barrier)。组内所有线程必须到达该语句后,才能继续向后执行。GroupShared 内存 · 正确用法
#pragma kernel ReduceSum
#define GROUP_SIZE 64
RWStructuredBuffer<float> Input;
RWStructuredBuffer<float> Output;
groupshared float sharedData[GROUP_SIZE];
[numthreads(GROUP_SIZE, 1, 1)]
void ReduceSum(uint3 id : SV_DispatchThreadID,
uint lid : SV_GroupIndex,
uint3 gid : SV_GroupID)
{
// 第一步:每个线程将数据载入共享内存
sharedData[lid] = Input[id.x];
// ★ 等待所有线程完成写入
GroupMemoryBarrierWithGroupSync();
// 第二步:二分规约
[unroll]
for (uint stride = GROUP_SIZE / 2; stride > 0; stride >>= 1)
{
if (lid < stride)
sharedData[lid] += sharedData[lid + stride];
// ★ 每轮规约后必须同步
GroupMemoryBarrierWithGroupSync();
}
// 仅 lid==0 的线程写出结果
if (lid == 0)
Output[gid.x] = sharedData[0];
}
二分规约示意(GROUP_SIZE = 8)
原子操作(Interlocked)
当多个线程需要修改同一个内存位置时,必须使用原子操作,而不是普通读-改-写。
RWStructuredBuffer<uint> counter;
RWStructuredBuffer<uint> histogram;
[numthreads(64, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
// 原子加法(返回操作前的旧值)
uint oldVal;
InterlockedAdd(counter[0], 1, oldVal);
// 其他原子操作
InterlockedMin(histogram[0], id.x); // 原子取最小
InterlockedMax(histogram[1], id.x); // 原子取最大
InterlockedAnd(histogram[2], 0xFF); // 原子与
InterlockedOr(histogram[3], 0x01); // 原子或
InterlockedXor(histogram[4], 0x01); // 原子异或
InterlockedExchange(counter[0], 0, oldVal); // 原子交换(置0并返回旧值)
// 比较并交换(CAS)
uint compareVal = 0, newVal = 1;
InterlockedCompareExchange(counter[0], compareVal, newVal, oldVal);
}
int / uint,不支持 float。浮点原子加法需要通过 CAS 循环或 RWByteAddressBuffer 变通实现。CPU ↔ GPU 同步(Unity)
| 方式 | 特性 | 性能影响 |
|---|---|---|
ComputeBuffer.GetData() |
同步回读,阻塞 CPU 直到 GPU 完成 | ⛔ GPU Stall,性能开销大 |
AsyncGPUReadback.Request() |
异步回读,非阻塞,下帧检查 request.done |
✅ 推荐,1-2 帧延迟 |
| CommandBuffer.DispatchCompute | 将 Dispatch 录入命令缓冲区,由渲染线程统一提交 | ✅ 配合 SRP 使用 |
using UnityEngine;
using UnityEngine.Rendering;
using Unity.Collections;
public class AsyncReadbackExample : MonoBehaviour
{
public ComputeShader cs;
private ComputeBuffer buffer;
private AsyncGPUReadbackRequest request;
private bool requestPending = false;
void Start()
{
// 检查平台支持(OpenGL 不支持)
if (!SystemInfo.supportsAsyncGPUReadback) return;
buffer = new ComputeBuffer(1024, sizeof(float) * 3);
cs.SetBuffer(0, "particleBuffer", buffer);
// 发起第一次异步请求
RequestReadback();
}
void RequestReadback()
{
// 方式一:直接回调
AsyncGPUReadback.Request(buffer, OnReadbackComplete);
// 方式二:手动检查(见 Update)
request = AsyncGPUReadback.Request(buffer);
requestPending = true;
}
void OnReadbackComplete(AsyncGPUReadbackRequest req)
{
if (req.hasError) return;
NativeArray<Vector3> data = req.GetData<Vector3>();
// 使用 data...
}
void Update()
{
// Dispatch 计算
cs.Dispatch(0, 32, 1, 1);
// 检查异步请求是否完成
if (requestPending && request.done)
{
if (!request.hasError)
{
var data = request.GetData<Vector3>();
// 处理数据...
}
RequestReadback(); // 再次请求
requestPending = false;
}
}
void OnDestroy() => buffer?.Release();
}
应用范例
RWTexture2D · 最简示例
#pragma kernel CSMain
RWTexture2D<float4> Result;
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
float4 baseColor = float4(
id.x & id.y,
(id.x & 15) / 15.0,
(id.y & 15) / 15.0,
1.0
);
float4 color = Result[id.xy];
// 每帧逐渐偏移颜色
color.r += 0.01f;
color.g += 0.005f;
color.b += 0.008f;
color = frac(color); // 环绕 [0,1]
Result[id.xy] = color + baseColor * 0.5f;
}
public class ComputeUAVTexture : MonoBehaviour
{
public ComputeShader shader;
public Material mat;
private int size = 128;
private int kernel;
void Start()
{
kernel = shader.FindKernel("CSMain");
RenderTexture tex = new RenderTexture(size, size, 0);
tex.enableRandomWrite = true;
tex.Create();
mat.SetTexture("_MainTex", tex);
shader.SetTexture(kernel, "Result", tex);
}
void Update()
{
int groups = Mathf.CeilToInt(size / 8f);
shader.Dispatch(kernel, groups, groups, 1);
}
}
RWStructuredBuffer · 粒子位置更新
#pragma kernel main
struct Particle { float3 position; };
RWStructuredBuffer<Particle> particleBuffer;
[numthreads(32, 1, 1)]
void main(uint3 id : SV_DispatchThreadID)
{
float spacingFactor = 0.5f;
particleBuffer[id.x].position.x =
particleBuffer[0].position.x + id.x * spacingFactor;
float y = particleBuffer[id.x].position.y;
y += sin(particleBuffer[id.x].position.x + y) * 0.005f;
particleBuffer[id.x].position.y = y;
}
struct Particle { public Vector3 position; }
const int warpSize = 32;
int warpCount = 5;
ComputeBuffer cBuffer;
Particle[] particleArray;
void Start()
{
int count = warpCount * warpSize;
particleArray = new Particle[count];
for (int i = 0; i < count; i++)
particleArray[i].position = transform.position;
// stride = 3 * 4 bytes = sizeof(Particle)
cBuffer = new ComputeBuffer(count, 12);
cBuffer.SetData(particleArray);
computeShader.SetBuffer(0, "particleBuffer", cBuffer);
}
void Update()
{
computeShader.Dispatch(0, warpCount, 1, 1);
// ⚠️ 同步回读(性能开销大,建议改用 AsyncGPUReadback)
cBuffer.GetData(particleArray);
for (int i = 0; i < particleArray.Length; i++)
objects[i].transform.position = particleArray[i].position;
}
void OnDestroy() => cBuffer.Release();
图像去色处理
演示 Texture2D(只读输入)+ RWTexture2D(可写输出)的经典搭配。
#pragma kernel CSMain
Texture2D<float4> _MainTex; // 只读输入
RWTexture2D<float4> _ResultTex; // 可写输出
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
uint width, height;
_MainTex.GetDimensions(width, height);
if (id.x >= width || id.y >= height) return;
float4 color = _MainTex[id.xy];
// ITU-R BT.601 亮度权重
// $L = 0.299R + 0.587G + 0.114B$
float gray = dot(color.rgb, float3(0.299, 0.587, 0.114));
_ResultTex[id.xy] = float4(gray, gray, gray, color.a);
}
ITU-R BT.601 亮度感知加权公式
Compute Mesh · GPU 顶点动画
Compute Shader 直接读写顶点 Buffer,结果由顶点着色器通过 StructuredBuffer 消费,绕过传统 MeshRenderer 的 CPU 瓶颈。
struct VertexData
{
uint id;
float4 pos, opos, col;
float3 nor, velocity;
float2 uv;
};
RWStructuredBuffer<VertexData> vertexBuffer;
float _Time;
uint _VertexCount;
[numthreads(1, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
uint rid = vertexBuffer[id.x].id;
// 彩虹颜色:仅首顶点设置,其余继承
if (rid == 0)
{
float3 c;
c.r = frac(sin(_Time + vertexBuffer[rid].velocity.x));
c.g = frac(sin(_Time + vertexBuffer[rid].velocity.y));
c.b = frac(sin(_Time + vertexBuffer[rid].velocity.z));
vertexBuffer[rid].col = float4(c, 1);
}
else
{
vertexBuffer[rid].col = vertexBuffer[rid - 1].col;
}
// 顶点波浪动画
vertexBuffer[rid].pos.xz =
vertexBuffer[rid].opos.xz *
(1 + sin(_Time + vertexBuffer[rid].opos.y * 3.0f) * 0.3f);
}
StructuredBuffer<VertexData> vertexBuffer;
v2f vert(uint id : SV_VertexID)
{
v2f o;
uint realid = vertexBuffer[id].id;
o.vertex = UnityObjectToClipPos(vertexBuffer[realid].pos);
o.uv = TRANSFORM_TEX(vertexBuffer[realid].uv, _MainTex);
o.color = vertexBuffer[realid].col;
return o;
}
数据流模式 · Data Traffic Patterns
五种核心流量模式
最基础模式。Compute 写入 RT,Material 直接引用该 RT 渲染。零回读开销。
CPU 每帧更新数据 → GPU 读取渲染(如实例化矩阵、颜色数组)。
物理模拟结果回读 CPU、碰撞检测结果等。建议使用 Async 模式避免 GPU Stall。
数据无需回读 CPU。GPU 先做过滤(写 Append Buffer),再通过 DispatchIndirect 只对过滤后的数据执行计算。最高效的 GPU-Driven 模式,适合 GPU Culling、粒子过滤等。
// 重置计数
cbPoints.SetCounterValue(0);
// Pass 1: 过滤(往 Append Buffer 写入通过的数据)
shader.Dispatch(kernelDirect, maxCount, 1, 1);
// 将 Append Buffer 的实际计数复制到 Args Buffer
ComputeBuffer.CopyCount(cbPoints, cbDrawArgs, 0);
// Pass 2: 只对过滤后的数量执行 Indirect Dispatch
shader.DispatchIndirect(kernelIndirect, cbDrawArgs, 0);
不仅 Compute Shader 可写 Buffer,普通片元着色器通过 Graphics.SetRandomWriteTarget 也可写入,用于 Shader Debug Print 等调试场景。
CPU ↔ GPU 完整交互流程图
从 CPU 申请资源到 GPU 执行完毕并将结果送回渲染管线的完整生命周期。蓝色节点在 CPU 主线程执行,紫色节点在 GPU 执行,绿色节点为可选的结果消费路径。
平台差异 · Platform Differences
各平台支持矩阵
| 平台 / API | CS 支持 | RWStructuredBuffer | AsyncGPUReadback | Indirect Dispatch | Warp Size | 备注 |
|---|---|---|---|---|---|---|
| Windows · DX11 | ✅ 完整 | ✅ | ✅ | ✅ | 32 (NVIDIA) 64 (AMD) |
最完整参考平台 |
| Windows · DX12 | ✅ 完整 | ✅ | ✅ | ✅ | 32 / 64 | 支持显式资源管理 |
| macOS / iOS · Metal | ✅ 完整 | ✅ | ✅ | ✅ | 32 (Apple Silicon) | AsyncReadback 颜色空间需注意 |
| Android · Vulkan | ✅ 完整 | ✅ | ✅ | ✅ | 32 / 64 | 推荐移动端首选 API |
| Android · OpenGL ES 3.1 | ⚠️ 受限 | ⚠️ 仅 Fragment 可访问 (部分设备) |
❌ 不支持 | ⚠️ 部分支持 | — | 兼容性差异大 |
| Android · OpenGL ES 3.0 及以下 | ❌ 不支持 | ❌ | ❌ | ❌ | — | 需降级处理 |
| WebGL | ❌ 不支持 | ❌ | ❌ | ❌ | — | WebGPU (实验性) 支持 |
Unity 平台检测
void Start()
{
// 检测 Compute Shader 支持(OpenGL ES 3.1+ / Metal / Vulkan)
if (!SystemInfo.supportsComputeShaders)
{
Debug.LogWarning("当前平台不支持 Compute Shader,切换到降级路径");
FallbackToCPU();
return;
}
// 检测异步回读支持(OpenGL 系列不支持)
if (!SystemInfo.supportsAsyncGPUReadback)
{
Debug.LogWarning("不支持 AsyncGPUReadback,使用同步 GetData");
useSyncReadback = true;
}
// 检测 Compute Shader Model(普通 Shader 内使用 CS 资源需要 SM 4.5)
// #pragma target 4.5
// 检测最大线程组尺寸
int maxThreadGroupSize = SystemInfo.maxComputeWorkGroupSize;
// 获取各维度最大值
int maxX = SystemInfo.maxComputeWorkGroupSizeX; // 通常 1024
int maxY = SystemInfo.maxComputeWorkGroupSizeY; // 通常 1024
int maxZ = SystemInfo.maxComputeWorkGroupSizeZ; // 通常 64
Debug.Log($"Max WorkGroup: ({maxX}, {maxY}, {maxZ})");
}
- 部分 OpenGL ES 3.1 设备只允许在 Fragment Shader 内访问
StructuredBuffer,Vertex/Compute 阶段访问会崩溃 - 普通 Shader 中使用 CS 资源需声明
#pragma target 4.5(Shader Model 最低要求) - AsyncGPUReadback 在 OpenGL 系列(包括 OpenGL ES)上完全不支持
- 某些 Mali GPU 的 Warp 执行策略与 NVIDIA/AMD 不同,
groupshared访问模式需特别注意 Bank Conflict
Warp / Wavefront 尺寸差异
GPU 内部的最小调度单位(Warp / Wave / Wavefront)在不同硬件上大小不同,这直接影响 numthreads 的最优选择:
| 厂商 / API | 最小调度单位 | 推荐 numthreads 策略 |
|---|---|---|
| NVIDIA · CUDA / DX | Warp = 32 | 总线程数为 32 的倍数(32, 64, 128, 256) |
| AMD · GCN / RDNA | Wavefront = 64 | 总线程数为 64 的倍数(64, 128, 256) |
| Apple · Metal (M 系列) | SIMD Group = 32 | 32 或 32 的倍数 |
| ARM · Mali | Quad = 4 | 线程数越多越好,但总量受限 |
| Qualcomm · Adreno | Fiber = 64~128 | 64 倍数较优 |
shader.GetKernelThreadGroupSizes(kernel, out x, out y, out z) 获取 Kernel 声明的大小,但无法运行时查询硬件 Warp 尺寸。性能优化 · Performance
内存访问优化
同一 Warp 内的线程应访问连续内存地址。buffer[id.x] 是连续的(好),buffer[id.x * stride] 是跨步的(差,触发多次内存事务)。
L1 级 GroupShared 访问速度远快于全局 Buffer(~100倍)。将热数据先载入 groupshared,计算完后再写回,是性能优化的核心手段。
GroupShared 内存分为若干 Bank,同一 Warp 内多个线程访问同一 Bank 的不同地址会串行化(Bank Conflict)。确保访问 sharedData[lid] 而非 sharedData[lid * 2]。
UAV 写入(非本地缓存)开销很大。尽量在 GroupShared 中完成计算,最终结果写一次 UAV。避免在分支中写 UAV(不同线程写不同位置是安全的,但性能取决于硬件)。
Dispatch 策略优化
// 从 Kernel 获取声明的 numthreads
uint threadX, threadY, threadZ;
shader.GetKernelThreadGroupSizes(kernel, out threadX, out threadY, out threadZ);
// 计算覆盖全部元素所需的线程组数(向上取整)
int groupsX = Mathf.CeilToInt((float)dataCount / threadX);
shader.Dispatch(kernel, groupsX, 1, 1);
RWStructuredBuffer<float4> Output;
uint _DataCount;
[numthreads(64, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
// 关键:线程数可能超过实际数据量
if (id.x >= _DataCount) return;
Output[id.x] = float4(id.x, 0, 0, 1);
}
CommandBuffer 异步提交
using UnityEngine;
using UnityEngine.Rendering;
public class ComputeViaCommandBuffer : MonoBehaviour
{
public ComputeShader cs;
private CommandBuffer cmd;
void OnEnable()
{
cmd = new CommandBuffer { name = "My Compute Pass" };
Camera.main.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, cmd);
}
void Update()
{
cmd.Clear();
// 设置参数
cmd.SetComputeIntParam(cs, "_FrameCount", Time.frameCount);
// 录入 Dispatch(不立即执行,等渲染线程调度)
int kernel = cs.FindKernel("CSMain");
cmd.DispatchCompute(cs, kernel, 128, 1, 1);
}
void OnDisable()
{
Camera.main.RemoveCommandBuffer(CameraEvent.BeforeForwardOpaque, cmd);
cmd.Dispose();
}
}
分支发散优化 · Branch Divergence
同一 Warp 内的线程如果走不同的 if-else 分支,GPU 必须串行执行两个分支(被 mask 的线程空转),导致吞吐量减半。
// Warp 内一半 id.x < 500,一半 >= 500
// → 两个分支串行执行
if (id.x < 500)
doExpensiveA();
else
doExpensiveB();
// 方案一:用 select / lerp 消除分支
float result = lerp(valB, valA, (float)(id.x < 500));
// 方案二:保证分支按 Warp 边界对齐
// (Warp 内 32 线程要么全走 A,要么全走 B)
// → 按 32 对齐组织数据
| 情形 | Warp 内行为 | 周期开销 | 建议 |
|---|---|---|---|
| 无分支 / 统一分支 | 全部线程执行相同路径 | × 1 | 最优 |
| 分支但 Warp 内统一 | 所有线程选相同路径(即使有 if) | × 1 | 数据布局使分支对齐到 Warp |
| Warp 内 50% 分叉 | if 路径 + else 路径各跑一次 | × 2 | 改用 lerp / select 消分支 |
| N 路发散分支 | 最坏情况 N 条路径串行 | × N | 将数据预先按分支结果排序 |
寄存器压力与 GPU Occupancy
Occupancy(占用率)指同一时刻 SM 上活跃 Warp 数量占硬件最大容量的比例。高 Occupancy 能掩盖内存延迟(当一个 Warp 等待内存时,SM 调度另一个 Warp 执行)。
| 降低寄存器压力的手段 | 说明 |
|---|---|
| 减少局部变量数量 | 临时变量复用,避免同时持有大量中间值 |
| 拆分 Kernel | 将计算量大的 Kernel 分拆成多个简单 Pass,每 Pass 寄存器用量少 |
#pragma unroll 谨慎使用 |
展开循环会增大寄存器用量;对寄存器已满的 Kernel 可能反而降低 Occupancy |
| 使用 Profiler 查看 | NVIDIA NSight、RenderDoc Compute 可直接显示 Register Usage 和 Occupancy |
GPU-Driven Indirect 完整流程图
GPU-Driven Rendering 的核心:CPU 只提交一次最大工作量的资源,GPU 自己做剔除 → 写入 Indirect 参数 → 自驱动渲染,整个过程无需 CPU 参与。
// ── 初始化(只做一次)────────────────────────────────────
// 所有物体数据上传一次
allObjectsBuffer.SetData(allObjectData);
// AppendBuffer:存储通过剔除的物体(每帧重置计数)
AppendStructuredBuffer visibleBuffer // type: Append
visibleBuffer.SetCounterValue(0);
// ArgsBuffer:4×uint:[instanceCount, 1, 1, 0](Dispatch 参数 / Draw 参数)
ComputeBuffer argsBuffer = new ComputeBuffer(4, 4, ComputeBufferType.IndirectArguments);
// ── 每帧执行 ──────────────────────────────────────────────
// 1. 重置 Append 计数
visibleBuffer.SetCounterValue(0);
// 2. Culling Pass
cullCS.SetBuffer(kernelCull, "_AllObjects", allObjectsBuffer);
cullCS.SetBuffer(kernelCull, "_VisibleObjects", visibleBuffer);
cullCS.Dispatch(kernelCull, Mathf.CeilToInt(totalCount / 64f), 1, 1);
// 3. 将可见数量复制到 ArgsBuffer(GPU 写 → GPU 读,不回到 CPU)
ComputeBuffer.CopyCount(visibleBuffer, argsBuffer, 0);
// 4. Indirect Dispatch(仅对可见物体执行后续 Pass)
processCS.SetBuffer(kernelProcess, "_VisibleObjects", visibleBuffer);
processCS.DispatchIndirect(kernelProcess, argsBuffer, 0);
// 5. DrawMeshInstancedIndirect(由 ArgsBuffer 决定实例数)
Graphics.DrawMeshInstancedIndirect(mesh, 0, material,
new Bounds(Vector3.zero, Vector3.one * 10000),
argsBuffer, 0);
Unity Compute Shader API 速查
ComputeShader 常用方法
| 方法 | 说明 |
|---|---|
FindKernel(name) | 查找 #pragma kernel 声明的 Kernel 索引 |
GetKernelThreadGroupSizes(k, x, y, z) | 获取 Kernel 的 numthreads 声明 |
SetBuffer(k, name, buf) | 绑定 ComputeBuffer 到 Kernel |
SetTexture(k, name, tex) | 绑定 Texture / RenderTexture 到 Kernel |
SetFloat / SetInt / SetVector / SetMatrix | 设置常量参数 |
SetFloatArray / SetVectorArray | 设置数组常量 |
Dispatch(k, gX, gY, gZ) | 同步执行 Compute(CPU 端阻塞直到提交) |
DispatchIndirect(k, argsBuffer, offset) | 从 Buffer 读取 Dispatch 参数,GPU-Driven |
HasKernel(name) | 检测 Kernel 是否存在(跨平台条件编译时有用) |
IsSupported(k) | 检测当前平台是否支持该 Kernel |
Live Playground · WebGPU 在线调试
在浏览器中运行 WGSL Compute Shader
下方嵌入了完整的 ComputeToy WebGPU 调试器。你可以直接编辑 WGSL 代码、调整 Uniform 参数、查看实时输出,也可以点击笔记中各章节代码块旁的 ▶ Playground 按钮,将对应示例一键注入到此处运行。