Contents

ComputerShader

Compute Shader 详解

概述

计算着色器(Compute Shader)是一种在GPU上执行通用计算的程序。它不直接参与渲染管线,而是用于并行处理大量数据。本文将详细介绍计算着色器的核心概念和使用方法。

线程组结构

线程组参数

1
2
[numthreads(4, 4, 1)]  // 定义单个线程组大小
Dispatch(2, 2, 1)      // 定义要派发的线程组数量

numthreads 参数说明

  • 定义单个线程组的大小
  • 示例中每组包含:
    • X方向:4个线程
    • Y方向:4个线程
    • Z方向:1个线程
  • 总线程数:4 * 4 * 1 = 16个线程/组

Dispatch 参数说明

  • 定义要创建的线程组数量
  • 示例中创建:
    • X方向:2个线程组
    • Y方向:2个线程组
    • Z方向:1个线程组
  • 总组数:2 * 2 * 1 = 4个线程组

线程组布局可视化

整体调度布局(2x2个组)

1
2
3
4
5
┌─────────┬─────────┐
│ 组(0,0) │ 组(1,0) │
├─────────┼─────────┤
│ 组(0,1) │ 组(1,1) │
└─────────┴─────────┘

单个组内线程布局(4x4个线程)

1
2
3
4
5
6
7
8
9
┌───┬───┬───┬───┐
│0,0│1,0│2,0│3,0│
├───┼───┼───┼───┤
│0,1│1,1│2,1│3,1│
├───┼───┼───┼───┤
│0,2│1,2│2,2│3,2│
├───┼───┼───┼───┤
│0,3│1,3│2,3│3,3│
└───┴───┴───┴───┘

线程标识符

SV_DispatchThreadID

  • 表示整个调度范围内的全局线程ID
  • 范围:[0,0,0] 到 [7,7,0](在上述示例中)
  • 主要用于:访问全局资源
  • 计算方式:GroupID * numthreads + GroupThreadID

SV_GroupID

  • 表示线程组的ID
  • 范围:[0,0,0] 到 [1,1,0](在上述示例中)
  • 与Dispatch参数直接相关
1
2
3
4
5
6
// Dispatch(2,2,1) 调用示例
void ComputeShader(uint3 GroupID : SV_GroupID) {
    // GroupID.x 范围:0-1
    // GroupID.y 范围:0-1
    // GroupID.z 值为 0
}

SV_GroupThreadID

  • 表示线程在组内的局部ID
  • 范围:[0,0,0] 到 [3,3,0](在上述示例中)
  • 主要用于:访问局部资源和组内计算

SV_GroupIndex

  • 表示线程在组内的一维索引
  • 范围:[0] 到 [15](在上述示例中)
  • 计算方式:
1
2
3
GroupIndex = GroupThreadID.z * (numthreads.x * numthreads.y) +
             GroupThreadID.y * numthreads.x +
             GroupThreadID.x

常见应用场景示例

2D图像处理

1
2
3
4
5
6
7
8
9
[numthreads(16, 16, 1)]  // 256个线程,适合处理2D图像
void ImageProcess(uint3 DTid : SV_DispatchThreadID) {
    int2 pixel = int2(DTid.x, DTid.y);
    
    // 边界检查
    if(pixel.x < imageWidth && pixel.y < imageHeight) {
        output[pixel] = ProcessPixel(input[pixel]);
    }
}

3D体素处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[numthreads(8, 8, 8)]  // 512个线程,适合处理3D数据
void VoxelProcess(uint3 DTid : SV_DispatchThreadID) {
    int3 voxel = int3(DTid.x, DTid.y, DTid.z);
    
    // 边界检查
    if(voxel.x < volumeWidth && 
       voxel.y < volumeHeight && 
       voxel.z < volumeDepth) {
        ProcessVoxel(voxel);
    }
}

一维数组处理

1
2
3
4
5
6
7
8
9
[numthreads(256, 1, 1)]  // 256个线程,适合处理一维数组
void ArrayProcess(uint3 DTid : SV_DispatchThreadID) {
    uint index = DTid.x;
    
    // 边界检查
    if(index < arraySize) {
        output[index] = ProcessData(input[index]);
    }
}

最佳实践

  1. 线程组大小选择

    • 2D处理:通常选择 8x8 或 16x16
    • 3D处理:通常选择 4x4x4 或 8x8x8
    • 1D处理:通常选择 256 或 512 个线程
  2. 边界检查

    • 始终进行数组边界检查
    • 考虑输入数据尺寸不是线程组大小整数倍的情况
  3. 性能优化

    • 尽量使用组共享内存减少全局内存访问
    • 注意内存访问模式,避免bank冲突
    • 考虑硬件限制,如最大线程组大小
  4. 调试技巧

    • 使用原子操作进行调试
    • 输出中间结果到缓冲区
    • 使用条件编译进行调试代码管理