最近开始在研究GPU Marching Cube相关的Compute Shader逻辑, 其中存在将SV_DispatchThreadID类型的数据转化为Cube数量的运算, 由于目前对Compute Shader中的各种内置语义值的工作原理了解得不够, 故对该转化始终未能有一个清晰的理解. 恰巧也发现一篇不错的关于Compute Shader中Dispatch IDs的英文博客, 特以本文翻译学习一波~
参考材料
1. Dispatch IDs and you
2. Compute Shader 参数含义总结

可以在各种变量SV_*的Win32文档页面上找到这张图片, 例如此页面.
$\\$ 如果不熟悉的话, 很容易混淆何时该使用哪个语义值. 最重要的是记住SV_GroupID和SV_GroupIndex的命名存在误导性(或至少是命名不一致的问题). 可用的Compute Shader语义如本文结尾处的总结表格所示.
1. Compute Shader语义
我们需要考虑三个ID:
$\\$ $\cdot$ SV_DispatchThreadID
$\\$ $\cdot$ SV_GroupThreadID
$\\$ $\cdot$ SV_GroupID
$\\$ 回想一下, 计算调度的结构分为两个层级(暂时忽略WaveSize的概念). 首先, 线程会被组织成线程组. 每个线程组的大小由附加在Compute Shader入口点上的numthreads属性的三个参数指定. 这些线程组本身则通过调用Dispatch在主机端启用, 这构成了层级结构的上层. 线程组内的线程数量与调度的线程组数量是相互独立的.
$\\$ 因此, 如果入口函数声明如下所示:
[numthreads(5, 5, 1)] void main();
那么线程组的总大小将是25个线程, 分布在$5 \times 5 \times 1$的概念性三维空间中, 该空间中的每个单元格都对应一个独立的线程或调用实例. 线程组内线程的地址由SV_GroupThreadID值指定, 该值是一个uint3类型. 关键的是, SV_GroupThreadID与特定调度中存在的线程组数量完全无关.
$\\$ 相比之下, SV_DispatchThreadID也是一个线程ID, 因此我们期望它的值能以某种方式标识一个线程. 这里的前缀Dispatch给了我们一个提示: SV_GroupThreadID标识的是线程组作用域内的线程, 而SV_DispatchThreadID标识的是整个调度作用域内的线程(即全局唯一的线程标识).
$\\$ 例如, 如果我们以上述入口函数调用Dispatch(3, 2, 4), 总共会调度$3 \times 2 \times 4 = 24$个线程组, 对应$24 \times 25 = 600$个总线程. 此时SV_DispatchThreadID会在Shader执行期间取到600个唯一值. 与线程组内的线程类似, 调度中的线程组同样是通过三维坐标定位的. 因此, 将线程组地址(组ID) 与组内线程地址(组线程ID) 组合起来, 就能理解为何SV_DispatchThreadID同样是uint3类型(与SV_GroupThreadID一致).
$\\$ 这让我们来到SV_GroupID的定义. 顾名思义, 与用于标识特定作用域内线程的SV_*ThreadID不同, SV_GroupID标识的是线程组本身. 这里的作用域没有明确说明, 因为实际上只有一种可能的作用域: SV_GroupID标识的是调度中的线程组. 例如, 在调用Dispatch(3, 2, 4)的情况下, SV_GroupID会取值于24个线程组(每个组内包含$5 \times 5 \times 1$的线程) 所构成的三维坐标范围(uint3类型). 个人认为这个命名存在缺陷的原因是: 尽管该ID的作用域是明确的(毕竟它必须属于一次调度), 但它的命名与之前的SV_GroupThreadID和SV_DispatchThreadID不一致. 如果笔者是设计者, 笔者会更倾向于使用SV_DispatchGroupID这样的命名, 以保持与其它两个值命名规则的统一性. 如果总是记混这些概念, 可以尝试将SV_GroupID理解为SV_DispatchGroupID, 这样能恢复与其它术语之间的逻辑一致性.
$\\$ 现在我们已经理清了前面的术语, 接下来要记住最后一个值: SV_GroupIndex. 与SV_GroupID(其值映射到线程组) 不同, SV_GroupIndex的值映射到线程. 具体来说, SV_GroupIndex的值对应的是组内的线程. 以之前带有[numthreads(5, 5, 1)]属性的入口函数为例, SV_GroupIndex的取值范围是0到24(包含两端). 这个命名尤其令人费解: 尽管它描述的是线程, 但名称中却完全没有出现Thread这个词.
$\\$ 需要注意的是, DirectX并未提供SV_DispatchIndex这样的语义值—— 理论上它应该是一个能标识整个调度中唯一线程展平后的索引(即全局唯一的线性索引). 在Compute Shader中, 这个值有时非常有用, 但通常需要开发者自行通过展平SV_GroupThreadID来计算. 例如, 若线程的三维坐标为$(x, y, z)$, 且调度的总维度为$(dimX, dimY, dimZ)$, 则可以通过公式:$$index = x + y \times dimX + z \times dimX \times dimY$$将三维坐标转换为一维索引. 这种方式能帮助实现类似SV_DispatchIndex的功能.
2. 总结
以下表格展示了各语义值的原始名称(左列), 功能描述(中列) 以及建议的更名(右列). 这些更名主要作为记忆辅助工具, 在某些情况下(如命名已清晰时), 原始名称仍可接受.
