Direct X 12 – Compute Shader

GPU被优化为从单一内存地址或者连续的内存地址来处理大量的内存(其也被称为streaming operation);这正好与CPU相反,CPU被设计为读取随机的内存空间Introduction to Direct Compute。此外,由于顶点和像素能被独立处理,GPU大多为平行架构;例如,NVIDIA的“Fermi”架构支持拥有512个CUDA核心,且32个CUDA核心包含16个多处理器。

显然,GPU的架构就是为了渲染而生。然而,一些非渲染或者说非图形学的软件也能从GPU强大的计算能力中获益。而将GPU用于非渲染的应用则被称为GPGPU(general purpose GPU)编程。并不是所有的算法都适用于GPU;GPU需要数据并行(data-parallel)算法,因为这样才最适合GPU的架构。也就是说,我们需要大量的数据元素,且处理这些数据的操作都是相似的,这样才能由GPU进行并行处理。而渲染中的像素处理就是最好的例子,因为每一个像素点都需要通过像素着色器。另一个例子是之前章节中的波浪模拟器,我们计算了每一个格子的高度。而这部分运算也可由GPU完成,因为GPU能够并行处理每一个格子。而游戏中的particle粒子系统也一样,如果每个粒子都具有物理特性且不会互相干涉,那么GPU便能并行处理所有的粒子。

对于GPGPU编程,我们需要将GPU的运算结果返回到CPU端。而这个拷贝的过程(从显存到系统内存)是十分缓慢的,如下图所示,但是相较于GPU处理这些任务所节省的时间是微不足道的。对于渲染来说,我们只需要将GPU的计算结果作为渲染管线的输入即可,所以并不存在GPU至CPU的资源拷贝。例如,我们可以使用compute shader来使一张贴图模糊,之后将shader resource view/descriptor绑定至处理完的贴图,将其作为着色器的输入(root parameter)。

Compute shader是DX12暴露给我们的一个可编程的着色器,但其并不属于渲染管线。相反,compute shader位于渲染管线的“对面”,并且既可以读取GPU资源也能够写入GPU资源(如下图所示)。最重要的是,compute shader能够让GPU进行其特有的并行数据算法,但并不需要绘制任何东西。正如之前提到的,这就是GPGPU有用的地方,但是仍然有许多渲染方面的效果可以由compute shader来实现,所以compute shader对于rendering/graphics programmer来说是很有用的。由于compute shader是DX12的一部分,其能够对DX的资源(GPU资源)进行读写,也就是说我们能够将compute shader的计算结果直接绑定至渲染管线。

目标:

1. 学会如何编写compute shader。

2. 对于硬件的线程组以及线程有基本的理解。

3. 了解哪种DX12资源能够被设置为compute shader的输入资源,哪种DX12资源可以被设置为compute shader的输出资源。

4. 了解不同的线程id与其作用。

5. 理解共享内存空间,了解其如何被运用与性能优化。

6. 了解哪里能够学习到更具体的GPGPU编程信息。

13.1 线程与线程组

在GPU编程中,用于执行任务的多个线程将被划分为线程组。一个线程组将由一个多处理器执行。今次,如果你的GPU拥有16个多处理器,那么你就需要将你的任务划分为至少16个部分,这样每个多处理能够拥有自己可以处理的任务。为了更高的运行效率,每个多处理器至少拥有两个线程组,这样多处理就能在不同的线程组中切换至不同的线程以防止处理器闲置[GPU Optimizations and Performance](如果着色器需要等待一个贴图的处理结果,否则其不能处理接下去的指令,那么这是处理器就闲置了)。

每一个线程组都有各自的共享内存,该线程组中所有的线程都能访问该内存;而线程不能够访问其他线程组的共享内存。同一线程组中的线程可以进行同步,但是不同线程组中的线程不能同步。事实上,我们并不能控制不同线程组被处理的顺序。也就是说,线程组能运行于不同的多处理器。

一个线程组由n个线程组成。实际上,硬件将这些线程分配为warp(每一个warp有32个线程),而多处理器将以SIMD32来处理一个warp(例如,同时对32个线程执行相同的指令)。每一个CUDA核心处理一个线程,我们之前提到“Fermi”处理器拥有32个CUDA核心(所以,CUDA核心就像SIMD“车道”)。在DX12中,我们可以声明线程组的容量,其并不一定是32的整倍数,但是考虑到性能的因素,线程组的容量因该是warp中线程个数的整倍数[GPU Optimizations and Performance]。

容量为256的线程组能够适应不同的硬件。如果我们改变每个线程组中线程的个数,那么也会改变线程组的个数。

NVIDIA的warp由32个线程组成。ATI的“wavefront”由64个线程组成,所以线程组的容量应该永远是64的整倍数。当然,warp或者wavefront的容量在下一代硬件面世后也可能会改变。

在DX12中,通过以下函数进行线程组的分配:

void ID3D12GraphicsCommandList::Dispatch(
    UINT ThreadGroupCountX,
    UINT ThreadGroupCountY,
    UINT ThreadGroupCountZ);

该函数让我们能够构建一个类似于3D格子的线程组结构,但是,本书中我们只关心线程组的2D结构。下图中,我们在x方向与y方向分别分配了线程组,总共为3×2=6个线程组。

上图中,分配的线程组为3×2。每一个线程组拥有8×8个线程。

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据