01

基础概念

什么是 Compute Shader?

Compute Shader(计算着色器)是运行在 GPU 上、独立于传统渲染管线之外的通用计算程序。它可以充分利用 GPU 的大规模并行架构执行 GPGPU(通用 GPU 计算)算法,也可以加速渲染管线中的特定阶段。

大规模并行
单次 Dispatch 可以启动数十万个线程
🔗
管线外执行
不依赖顶点/片元着色器,直接读写资源
🗄️
UAV 读写
通过 RWTexture / RWStructuredBuffer 直接写入 GPU 资源
🔄
数据复用
计算结果可直接被渲染着色器消费,无需回读 CPU

API 生态概览

API平台着色语言Unity 支持
DirectComputeWindows / DX11+HLSL✅ 完整
OpenGL Compute跨平台GLSL✅ 完整
MetaliOS / macOSMetal SL✅ 完整
VulkanAndroid / PCSPIR-V / GLSL✅ 完整
OpenGL ES 3.1AndroidGLSL ES⚠️ 受限
CUDA / OpenCLNVIDIA / AMDCUDA C / OpenCL C❌ 不支持

典型应用场景

🌊粒子系统 / 流体模拟
🌄GPU 地形 LOD / 剔除
🌥️体积云 / 大气散射
🔬后处理特效(模糊、SSAO)
🗺️Runtime Virtual Texturing
🧠神经网络推理加速
💡光照探针烘焙
📐物理碰撞预计算
02

线程模型 · Thread Hierarchy

三层层级结构

Compute Shader 的执行单元分为三层,从上到下依次为:Dispatch → Thread Group → Thread

Dispatch(gX, gY, gZ)
CPU 调用一次 Dispatch,启动 gX × gY × gZ 个线程组
Thread Group
每个线程组包含 tX × tY × tZ 个线程
[numthreads(tX, tY, tZ)] 声明
Thread(线程)
GPU 上实际执行的最小单元
可访问 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 线程在组内的扁平化索引,范围 0tX·tY·tZ-1 $k \cdot t_X \cdot t_Y + j \cdot t_X + i$
HLSL · 线程 ID 使用示例
#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 选择策略

🖼️ 2D 图像处理
[numthreads(8, 8, 1)]

每组 64 线程,适合纹理像素映射,X/Y 对应 UV

📋 1D 数组处理
[numthreads(64, 1, 1)]

单维度处理,粒子、顶点数组的标准选择

📦 3D 体积数据
[numthreads(4, 4, 4)]

处理体积云、3D 噪声等三维数据结构

⚠️ Warp 对齐
总线程数 = 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 的关键。

WARP 执行模型(SIMT — Single Instruction, Multiple Threads) SM(流式多处理器) Warp #0 — 32 线程,执行同一条指令(收敛) × 4 行 = 32 线程 全部执行相同指令 ✓ ⚡ 最优:带宽 100% 利用率,时钟周期 ×1 Warp #1 — 分支发散(Branch Divergence) ← 被 mask ⛔ 串行执行:if 分支与 else 分支各跑一遍,时钟周期 ×2 例:if (id.x < 16) { A; } else { B; } ← 同 Warp 内各半走不同路

GPU 内存层级(速度从快到慢)

Registers(寄存器) 延迟:~1 cycle 线程私有 · 容量极小 · 最快 L0 GroupShared / LDS 延迟:~5–10 cycles 组内共享 · 32~64 KB · 极快 L1 L1 Cache / Texture Cache 延迟:~30–80 cycles SM 级别 · 只读优化 L2 Global Memory(VRAM / 显存) 延迟:~200–800 cycles 全部线程可见 · GB 级 · 慢 带宽瓶颈所在(GDDR6~1TB/s) 速度 ↑ 快 容量 ↑ 大
💡
优化核心原则:尽量将热数据(频繁读写的数据)提升到更高级的内存层。先将 Global Memory 数据载入 groupshared,组内所有线程复用这份缓存,最后一次性写回 Global,是减少显存带宽消耗的标准模式。
03

缓冲区与资源类型

资源类型总览

UAV
RWTexture2D<T>
可随机读写的 2D 纹理。需开启 enableRandomWrite = true,是 GPU 输出的核心载体。
UAV
RWStructuredBuffer<T>
结构化 GPU Buffer,支持自定义结构体。用于粒子、顶点数据的 GPU-GPU 传递。
SRV
StructuredBuffer<T>
只读结构化 Buffer,可在普通着色器中访问,CPU→GPU 单向传递。
SRV
Texture2D<T>
只读纹理输入,在 Compute Shader 中读取已有纹理数据(如去色示例中的 _MainTex)。
特殊
AppendStructuredBuffer<T>
只写追加 Buffer,配合 .Append() 实现 GPU 端动态列表,用于 Indirect 过滤模式。
特殊
ConsumeStructuredBuffer<T>
只读消费 Buffer,配合 .Consume() 从 Append Buffer 中弹出元素。
特殊
ByteAddressBuffer
原始字节地址 Buffer,RWByteAddressBuffer 支持原子操作,用于计数器和间接参数。
LDS
groupshared T[]
组内共享内存(LDS/TGSMEM),极快速访问,大小通常限制在 32KB 以内,需 GroupMemoryBarrierWithGroupSync() 同步。

ComputeBuffer 创建与管理(Unity)

C# · ComputeBuffer 生命周期
// 创建:指定元素数量和每个元素的字节大小
// 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();
C# · 特殊 Buffer 类型标志
// 默认结构化 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)

C# · RenderTexture 随机写入配置
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);
04

同步机制 · Synchronization

为什么需要同步?

GPU 线程是高度并行的,同一个线程组内的线程执行顺序不保证。当多个线程读写同一内存位置(共享内存或 UAV)时,若没有屏障保护,会产生 Race Condition(数据竞争)

⚠️
典型危险场景:线程 A 尚未写完 groupshared 数组,线程 B 已开始读取 → 未定义行为,结果不可预测。

HLSL 同步函数

函数同步范围内存屏障范围典型用途
GroupMemoryBarrier() — (仅屏障) GroupShared Memory 保证组内共享内存写入对其他线程可见
GroupMemoryBarrierWithGroupSync() 组内所有线程 GroupShared Memory 组内规约(reduce)、前缀和等算法的核心
DeviceMemoryBarrier() — (仅屏障) UAV(全局内存) 保证 UAV 写入对其他线程可见
DeviceMemoryBarrierWithGroupSync() 组内所有线程 UAV(全局内存) 跨线程组数据依赖(较少见)
AllMemoryBarrier() — (仅屏障) 全部内存(SM + UAV) 全内存可见性屏障
AllMemoryBarrierWithGroupSync() 组内所有线程 全部内存(SM + UAV) 最重量级屏障,确保所有内存操作完成
💡
WithGroupSync 的函数相当于:内存屏障 + 线程栅栏(Barrier)。组内所有线程必须到达该语句后,才能继续向后执行。

GroupShared 内存 · 正确用法

HLSL · 使用 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)

载入 stride =4 stride =2 stride =1 3 1 4 2 7 2 5 4 sharedData[0..7] GroupSync 10 3 9 6 - - - - GroupSync 19 9 - - GroupSync 28 - sharedData[0] = 28(总和) ← lid==0 写出 3+1+4+2+7+2+5+4 = 28 ✓

原子操作(Interlocked)

当多个线程需要修改同一个内存位置时,必须使用原子操作,而不是普通读-改-写。

HLSL · 原子操作函数
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 使用
C# · AsyncGPUReadback 异步回读模式
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();
}
05

应用范例

RWTexture2D · 最简示例

CPU
→ Dispatch →
GPU 写 Texture
→ 直接
Shader 渲染
HLSL · 彩虹纹理
#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;
}
C# · Unity MonoBehaviour
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 · 粒子位置更新

CPU 初始化
GPU 每帧更新位置
Shader 读取渲染
HLSL · 粒子位置更新
#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;
}
C# · 初始化与每帧 Dispatch
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(可写输出)的经典搭配。

HLSL · 亮度加权灰度化
#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);
}
$$L = 0.299 \cdot R + 0.587 \cdot G + 0.114 \cdot B$$

ITU-R BT.601 亮度感知加权公式

Compute Mesh · GPU 顶点动画

Compute Shader 直接读写顶点 Buffer,结果由顶点着色器通过 StructuredBuffer 消费,绕过传统 MeshRenderer 的 CPU 瓶颈。

CPU 初始化 VertexData
CS 更新 pos/col
Vertex Shader 读 Buffer
HLSL · 顶点动画 CS
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);
}
HLSL · 顶点着色器消费 Buffer
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;
}
06

数据流模式 · Data Traffic Patterns

五种核心流量模式

T1
CPU → GPU 纹理写入 → Shader 渲染
CPU Dispatch GPU 写 RWTexture2D Fragment Shader 读取

最基础模式。Compute 写入 RT,Material 直接引用该 RT 渲染。零回读开销。

T2
CPU → GPU Buffer → Shader 渲染
CPU SetData RWStructuredBuffer VS/FS 消费

CPU 每帧更新数据 → GPU 读取渲染(如实例化矩阵、颜色数组)。

T3
CPU 指令 → GPU 计算 → CPU 读回
CPU Dispatch GPU 更新 Buffer AsyncGPUReadback CPU 处理结果

物理模拟结果回读 CPU、碰撞检测结果等。建议使用 Async 模式避免 GPU Stall。

T4
Indirect Dispatch · GPU 自驱动计算
CPU 设最大工作量 CS1 过滤 → AppendBuffer CopyCount DispatchIndirect

数据无需回读 CPU。GPU 先做过滤(写 Append Buffer),再通过 DispatchIndirect 只对过滤后的数据执行计算。最高效的 GPU-Driven 模式,适合 GPU Culling、粒子过滤等。

C# · Indirect 关键流程
// 重置计数
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);
T5
普通 Shader → UAV 写入 → CPU 读取(Shader to CPU)
Fragment Shader RWStructuredBuffer (register u6) GetData → CPU

不仅 Compute Shader 可写 Buffer,普通片元着色器通过 Graphics.SetRandomWriteTarget 也可写入,用于 Shader Debug Print 等调试场景。

CPU ↔ GPU 完整交互流程图

从 CPU 申请资源到 GPU 执行完毕并将结果送回渲染管线的完整生命周期。蓝色节点在 CPU 主线程执行,紫色节点在 GPU 执行,绿色节点为可选的结果消费路径。

CPU 主线程 GPU 执行 ① 创建资源 ComputeBuffer / RT ② 上传数据 buffer.SetData( ) ③ 绑定参数 SetBuffer / SetTexture ④ Dispatch shader.Dispatch(k,x,y,z) ⑦ CPU 继续 无需等待(异步) ⑧ 回读(可选) AsyncGPUReadback 提交 1~2 帧延迟(Async) ⑤ 命令队列 GPU Command Buffer ⑥ Kernel 执行 线程组调度 → Warp 执行 → 读写 Buffer/Texture ✓ 全程无 CPU 参与 → Shader 消费 RWTexture → Material → 下一 CS Pass 多 Pass Compute → GPU Buffer 结果 等待 Async 读回 CPU 不等待 CPU 操作 GPU 执行 结果消费 / 异步回读 可选路径
关键设计原则:尽可能让结果保留在 GPU 内部(路径 → Shader 消费→ 下一 CS Pass),避免读回 CPU。只有在游戏逻辑必须使用结果(如 AI 决策)时,才走异步回读路径,且延迟 1~2 帧是正常的。
07

平台差异 · 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 平台检测

C# · 运行时能力检测
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 / DXWarp = 32总线程数为 32 的倍数(32, 64, 128, 256)
AMD · GCN / RDNAWavefront = 64总线程数为 64 的倍数(64, 128, 256)
Apple · Metal (M 系列)SIMD Group = 3232 或 32 的倍数
ARM · MaliQuad = 4线程数越多越好,但总量受限
Qualcomm · AdrenoFiber = 64~12864 倍数较优
💡
DX12 / Vulkan / Metal 提供 API 查询实际 Warp 尺寸,Unity 可通过 shader.GetKernelThreadGroupSizes(kernel, out x, out y, out z) 获取 Kernel 声明的大小,但无法运行时查询硬件 Warp 尺寸。
08

性能优化 · Performance

内存访问优化

🚀 合并内存访问 (Coalescing)

同一 Warp 内的线程应访问连续内存地址buffer[id.x] 是连续的(好),buffer[id.x * stride] 是跨步的(差,触发多次内存事务)。

💾 优先使用 GroupShared

L1 级 GroupShared 访问速度远快于全局 Buffer(~100倍)。将热数据先载入 groupshared,计算完后再写回,是性能优化的核心手段。

⚡ 避免 Bank Conflict

GroupShared 内存分为若干 Bank,同一 Warp 内多个线程访问同一 Bank 的不同地址会串行化(Bank Conflict)。确保访问 sharedData[lid] 而非 sharedData[lid * 2]

📦 减少 UAV 写入

UAV 写入(非本地缓存)开销很大。尽量在 GroupShared 中完成计算,最终结果写一次 UAV。避免在分支中写 UAV(不同线程写不同位置是安全的,但性能取决于硬件)。

Dispatch 策略优化

C# · 动态计算最优 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);
HLSL · 边界检测(防止越界写入)
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 异步提交

C# · 使用 CommandBuffer 调度 Compute
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 执行)。

SM · 最大支持 32 Warp(示意) 高占用率 (75%):每线程寄存器少,更多 Warp 活跃 低占用率 (25%):每线程寄存器多,大量 Slot 闲置 → 无法掩盖内存延迟 活跃 Warp 闲置 Slot(寄存器不足,无法调度更多 Warp)
降低寄存器压力的手段说明
减少局部变量数量 临时变量复用,避免同时持有大量中间值
拆分 Kernel 将计算量大的 Kernel 分拆成多个简单 Pass,每 Pass 寄存器用量少
#pragma unroll 谨慎使用 展开循环会增大寄存器用量;对寄存器已满的 Kernel 可能反而降低 Occupancy
使用 Profiler 查看 NVIDIA NSight、RenderDoc Compute 可直接显示 Register Usage 和 Occupancy
💡
注意:Occupancy 高并不等于性能好。计算密集型 Kernel(ALU-bound)对 Occupancy 不敏感;只有内存延迟主导(Memory-bound)的 Kernel 才真正受益于高 Occupancy 的延迟掩盖。用 Profiler 区分瓶颈类型再针对性优化。

GPU-Driven Indirect 完整流程图

GPU-Driven Rendering 的核心:CPU 只提交一次最大工作量的资源,GPU 自己做剔除 → 写入 Indirect 参数 → 自驱动渲染,整个过程无需 CPU 参与。

CPU(一次性) GPU(全程自驱动) ① 上传全量数据 所有待渲染物体 StructuredBuffer ② Culling Kernel Frustum / Hi-Z 剔除 通过 → .Append(data) AppendBuffer.SetCounterValue(0) ③ AppendBuffer 只存可见物体数据 ④ CopyCount → ArgsBuffer[0] GPU 写计数 ⑤ DispatchIndirect 从 ArgsBuffer 读参数 精确 Dispatch 可见数量 ⑥ DrawMeshInstanced Indirect → GPU 渲染 只渲染可见实例 [可选] LOD CS 计算每实例 LOD 等级 写入 IndirectArgs [可选] GPU Sort 按材质/距离排序 减少 DrawCall Overhead 注:全程 CPU 只需一次 SetData + 初始 Dispatch,后续由 GPU 自驱动,无 CPU↔GPU 往返。
C# · GPU-Driven Indirect 完整流程
// ── 初始化(只做一次)────────────────────────────────────
// 所有物体数据上传一次
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);
09

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
10

Live Playground · WebGPU 在线调试

在浏览器中运行 WGSL Compute Shader

下方嵌入了完整的 ComputeToy WebGPU 调试器。你可以直接编辑 WGSL 代码、调整 Uniform 参数、查看实时输出,也可以点击笔记中各章节代码块旁的 ▶ Playground 按钮,将对应示例一键注入到此处运行。

💡
「注:当前功能施工中,wgsl与hlsl语法不同」
Ctrl+Enter 编译运行
Ctrl+. 停止
Ctrl+S 保存代码
需要 Chrome 113+
切换内置示例:
ComputeToy · WebGPU Playground
Loading WebGPU Playground…

参考资源