计算着色器虽然是一种可编程的着色器,但Direct3D并没有将它直接归为渲染流水线中的一部分。虽然如此,但位于流水线之外的计算着色器却可以读写GPU资源。从本质上来说,计算着 色器能够使我们访问GPU来实现数据并行算法,而不必渲染出任何图形。由于计算着色器是Direct3D的组成部分,也可以读写Direct3D资源, 由此我们就可以将其输出的数据直接绑定到渲染流水线上。
线程与线程组
在GPU编程的过程中,根据程序具体的执行需求,可将线程划分为由线程组(thread group )构成 的网格(grid )o 一个线程组运行于一个多处理器之上。因此,对于拥有16个多处理器的GPU来说,我 们至少应将任务分解为16个线程组,以此令每个多处理器都充分地运转起来。但是,要获得更佳的性能, 我们还应当令每个多处理器至少拥有两个线程组,使它能够切换到不同的线程组进行处理,以连续不停地 工作[FunglO](线程组在运行的过程中可能会发生停顿,例如,着色器在继续执行下一个指令之前会等待 纹理的处理结果,此时即可切换至另一个线程组)。
每个线程组中都有一块共享内存,供组内的线程访问。但是,线程并不能访问其他组中的共享内存。 同理,同组内的线程间能够进行同步操作,不同组的线程间却不能实现这一点。事实上,我们也无法控 制不同线程组间的处理W序,因为这些线程组可能正运行在不同的多处理器上。
一个线程组中含有n个线程。硬件实际上会将这些线程分为多个warp (每个warp中有32个线程), 而且多处理器会以SIMD32的方式(即32个线程同时执行相同的指令序列)来处理warpo每个CUDA核 心都可处理一个线程,前面也提到了,“Fermi”架构中的每个多处理器都具有32个CUDA核心(因此, CUDA核匚、就像一条专设的SIMD “计算通道” (lane))o在Direct3D中,我们能够以非32的倍数值来指 定线程组的大小。但是出于性能的原因,我们应当总是将线程组的大小设置为warp尺寸的整数倍[FunglO]。
对于各种型号的图形硬件来说,线程数为256的线程组是一种普遍适于工作的初始设置。我们可以 以此值为基础,再根据具体需求尝试将其调整为其他大小。值得注意的是,修改每个线程组中的线程数 量也会对线程组的分派(dispatch,调度)次数产生影响。
NVIDIA公司生产的图形硬件所用的warp单位共有32个线程。而ATI公司(已被AMD公司收购)采用的 "wavefront”单位则具有64个线程,且建议为其分配的线程组大小应总为wavefront尺寸 的整数倍[Bilodeau 10]。另夕卜,值得一提的是,不管是warp还是wavefront,它们的大小在 未来几代中都有可能发生改变。
在Direct3D中可通过调用下列方法来启动线程组:
void ID3D12GraphicsCommandList::Dispatch(
UINT ThreadGroupCountX,
UINT ThreadGroupCountY,
UINT ThreadGroupCountZ);
此方法可开启一个由线程组构成的3D网格,但是我们在本书中仅关注线程组2D网格。下面的调用 示例会分派一个在x方向上为3、y方向上为2,即总数为3x2 = 6个线程组的网格(见图13.3)。
cmdList->Dispatch(3, 2, 1);
一个简单的计算着色器
以下是将两个纹理进行简单累加的计算着色器示例,假设所有的纹理都具有相同的大小。虽然该着 色器有点索然无味,却五脏俱全,能详细地展示出计算着色器的基本套路语法。
cbuffer cbSettings
{
//计算着色器能访问的常量缓冲区数据
);
//数据源及着色器的输出
Texture2D glnputA;
Texture2D glnputB;
RWTexture2D<float4> gOutput;
//线程组中的线程数。组中的线程可以被设置为ID、2D或3D的网格布局
[numthreads(16, 16, 1)]
void CS(int3 dispatchThreadID : SV_DispatchThreadID) // 线程 ID
{
//对两种源像素中横纵坐标分别为x、y处的纹素进行求和,并将结果保存到相应的gOutput纹素中
gOutput[dispatchThreadID.xy]=glnputA[dispatchThreadID.xy] + glnputB[dispatchThreadlD.xy];
}
可见,一个计算着色器由下列要素构成:
1.通过常量缓冲区访问的全局变量。
2. 输入与输出资源。
3. [numthreads (X, Y, Z)]属性,指定3D线程网格中的线程数量。
4. 每个线程都要执行的着色器指令。
5. 线程ID系统值参数。
不难看出,我们能够根据需求定义岀不同的线程组布局。例如,可以定义一个具有X个线程的单行 线程组[numthreads (X, 1, 1)]或内含Y个线程的单列线程组[numthreads (1 ,Y, 1)]。抑或通 过将维度z设为1来定义规模为X x Y的2D线程组,形如[numthreads (X, Y, 1) ] 。我们应结合所遇到的具体问题来选择适当的线程组布局。如同前一节中提到的那样:针对NVIDIA品牌的显卡来说, 线程组中的总线程数应为warp大小(32 )的整数倍,而ATI公司生产的显卡应为wavefront尺寸(64 ) 的整数倍庁又因wavefront大小的倍数(64x„)必为warp尺寸的倍数(32xm),因此,以前者的线程数 为基础进行设置对两种显卡都适用。
计算流水线状态对象
为了开启计算着色器,我们还需使用其特定的“计算流水线状态描述”。此描述中的字段远少于 D3D12_GRAPHICS_PIPELINE_STATE_DESC结构体。这是因为计算着色器位列图形流水线之外,因 此所有的图形流水线状态都不适用于计算着色器,也就无须以此对它进行设置。下面给岀一个创建计算流水线状态对象的示例:
D3D12_COMPUTE_PIPELINE_STATE_DESC wavesUpdatePSO = {};
wavesUpdatePSO.pRootSignature = mWavesRootSignature.Get();
wavesUpdatePSO.CS =
{
reinterpret_cast<BYTE*>(mShaders["wavesUpdateCS"]->GetBufferPointer()),
mShaders["wavesUpdateCS"]->GetBufferSize()
};
wavesUpdatePSO.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateComputePipelineState(&wavesUpdatePSO, IID_PPV_ARGS(&mPSOs["wavesUpdate"])));
根签名定义了什么参数才是着色器所期望的输入(CBV、SRV等)。而cs (即compute shader的缩 写)字段就是所指定的计算着色器。下列代码展示了一个将着色器编译为字节码的示例:
mShaders["wavesUpdateCS"] = d3dUtil::CompileShader(L"Shaders\\WaveSim.hlsl", nullptr, "UpdateWavesCS", "cs_5_0");
mShaders["wavesDisturbCS"] = d3dUtil::CompileShader(L"Shaders\\WaveSim.hlsl", nullptr, "DisturbWavesCS", "cs_5_0");