作者 Wolfgang Engel
Microsoft* DirectCompute 将显卡硬件的计算功能视为新的着色器类型:计算着色器。计算着色器与顶点、几何或像素着色器类似,可提供编程接口,让显卡硬件的大量并行计算性能可用于普通基于光栅的显卡管线(借助 Microsoft* Direct3D* 或 OpenGL* 展现)之外的任务。
注: 尽管通过 Microsoft* DirectX* 11 推出了 DirectCompute,但仍可以在 Microsoft* DirectX* 10、10.1 和 11 类硬件上运行计算着色器。
与同样提供了计算解决方案的应用程序编程接口 (API) 相比,DirectCompute 具有诸多优势。首先,它能够很好地与 Direct3D* 集成,这意味着它不仅能有效地与纹理和缓冲区等 Direct3D* 资源进行互操作,而且许多概念和语言语法都是 Direct3D* 编程人员所熟知的。此外,与其他计算 API 相比,接口更为通用和简单。和 Direct3D* 相似,DirectCompute 确保不同硬件可得到一致的结果。
DirectCompute 的应用程序是不会映射到数据和处理或线程之间的固定映射中的算法,顶点或像素着色器都需要用到该算法,并且无需涉及光栅器。典型使用案例包括:
基于寄存器的内存
DirectCompute 使用相同的寄存器组作为 Direct3D* 管线(DirectCompute 寄存器)的其他可编程阶段。由于只能用高级别着色器语言 (HLSL) 对着色器编程,您将不能访问那些寄存器。但是,编译器可生成中间程序集。很难说清程序集代码的价值有多大,因为它只是中间性的,并且可通过基本硬件的驱动程序更改。
最令人关注的基于寄存器的内存要数临时寄存器。总共有 4096 个临时和索引零时寄存器(阵列式)可用。常规临时寄存器命名为 r#,而索引临时阵列寄存器命名为 x#[n]。在处理核心中或数个处理核心之间,可在运行状态下在所有线程之间共享这些寄存器。驱动程序编译器会自动选择和分配这些寄存器,不受着色器程序员的任何直接影响。着色器越复杂就越可能消耗更多临时寄存器,从而使临时寄存器的硬件处于“饥饿”状态。一个良好的显卡硬件性能评测工具将指出硬件用完临时寄存器的实际情况。然后您可尝试覆盖部分着色器以减少所用临时寄存器的数目。
设备内存
尽管临时寄存器中的数据仅在执行着色器程序期间存在,但仍需要保存时间更长的数据并提供更多存储。DirectCompute 可存储一般 Direct3D* 资源(例如缓冲区和纹理)中的数据。可使用读/写缓冲区和纹理、结构化缓冲区以及字节地址(原始)缓冲区。要对纹理和缓冲区进行读写操作,可使用所谓的内存视图。DirectX* 11 引入了无序访问视图 (UAV),可实现分散的写入和集中读取。为了在着色器中读取内存,DirectX* 10 引入了着色器资源视图 (SRV)。一种特殊类别的设备内存是常量缓冲区,适用于进行 16 次连续内存读取的访问模式。该访问模式的一个示例是从常量内存读取 4×4 矩阵。
读/写缓冲区和纹理
预期只读纹理内存适用于在空间上彼此靠近的内存访问模式。例如,双线性过滤需要访问彼此靠近的 texel。DirectX* 11 新增了一组读和写纹理:
结构化缓冲区
A 结构化缓冲区结构化缓冲区是一个包含结构元素的缓冲区。下面是一个简单的示例:
字节地址缓冲区
字节地址缓冲区或原始缓冲区都是特殊的缓冲区类型,以偏移缓冲区起始点一个字节的方式编址。字节偏移必须为 4 的倍数,以使字对齐。
原始缓冲区的类型始终为 32 位无符号整型。其他数据类型需要换算为无符号整型。原始缓冲区可用于通过 DirectCompute 生成几何,因为可将它们限定为顶点和索引缓冲区。在 HLSL 中,按如下对其进行声明:
常量缓冲区
常量缓冲区提供数据的只读访问权限,预期以将数据作为 16 个连续浮点值的方式访问数据。只要按顺序对其进行访问,开销就与只读取一个值相似。
着色器可访问 4096 32 位四分量常量:64 KB。尽管 DirectX* 10.x 和 DirectX* 11 将此定义为常量缓冲区大小的上限,但 DirectX* 11.1 可让您在常量缓冲区中存储更多常量并访问着色器中此缓冲区的子范围:
着色器资源视图和无序访问视图
与 DirectX* 管线中的其他着色器阶段相似,在 DirectCompute 中支持 SRV 以允许着色器读取资源内存。如果为结构化缓冲区,可如下创建 SRV:
结构化缓冲区的 UAV 代码形式与下面类似:
提供 append(T) 方法;ConsumeStructuredBuffer 提供 T .consume() 方法。
线程组共享内存
线程组共享内存 (TGSM) 位于片上内存上。可将其视为高速缓存以最小化片外带宽使用量。线程组中的所有线程访问本内存。换言之,TGSM 允许给定组中的线程以协作和共享数据。与全局缓冲区负载和存储相比对共享内存的读写更快 – 接近寄存器读写速度。
一种常用的编程模式是让组内的线程以协作方式加载数据块到共享内存,处理数据,然后将结果写入可写入缓冲区。一个常见的示例是为后处理管线存储水平或垂直模糊内核的所有相邻像素。
TGSM 不会一直停留在分派调用之间。因此需要将一个分派调用的结果存储在其他位置。使用 groupshared 类型限定词在 HLSL 着色器中指示 TGSM:
DirectCompute 使用不同的线程模型。具备 DirectCompute 的设备可运行数千个线程,可灵活地将线程映射至数据元素,同时同一着色器或程序执行所有这些线程,这个过程称为并行内核。
内核处理
在执行计算着色器时,将其视为一个处理内核。会对每个线程将内核实例化并应用至一组数据。该数据通过绑定至 DirectCompute 阶段的 Direct3D* 资源提供。也就是说,可对每个硬件线程分派执行单独调用内核的任务,该内核对于分派调用中的所有线程相同。
这意味着您可将 DirectCompute 应用程序的典型数据分拆为足够小的数据,从而可以分离方式处理数据。典型的 DirectCompute 应用程序需要由大量结构相似的数据段(显卡硬件的典型域)组成的数据。
分派内核
执行计算着色器也称为分派内核。DirectX* 11.x 中有两个函数可分派内核:
第二个方法 DispatchIndirect() 间接进行分派调用,方法是允许它使用 Dispatch() 调用预期的参数填充缓冲区,然后根据缓冲区数据分派新作业。以下代码段说明了如何调用 Dispatch() 并在线程组中提供线程数的典型示例:
DirectX* 11.x 支持 3D 组和线程。将 DirectX* 11 中线程组中的线程视为 3D 阵列。每个线程“阵列”作为 2D 或 3D 阵列都是线程组的一部分。通过使用保留线程和线程组维度的寄存器将线程组中的线程编址。
DirectCompute 运行时将存储在寄存器中的系统值提供给内核。四个寄存器保留该数据:
内存壁垒
在 DirectX* 11.x 中,六个不同的 HLSL 内部函数称为内存壁垒,可同步线程执行和内存写入:
有用于 TGSM、设备内存和同时用于这两种内存的 *内存壁垒。*MemoryBarrierWithGroupSync 在未完成的内存操作(这些操作在调用时处于活动状态)完成后并且组内的所有线程均接收到指令后才运行。在以下代码中示出了使用内存壁垒的典型示例:
原子函数
DirectX* 11.x 在计算和像素着色器中支持原子函数或互锁函数。它们能确保自动运行,换句话讲,它们能确保以设定的顺序运行。下面是原子函数的一览表:
请注意所有这些整数原子期望 Add、Min 和 Max 在 asInteger() 中传递时按原样在浮点数上起作用。但是只要所有浮点数为正数,Min 和 Max 都按原样在 asInteger() 浮点数上起作用。

图 1. 曼德布洛特
图 2 示出了运行在英特尔® Ivy Bridge 上的演示应用程序截屏。

图 2. 曼德布洛特
该示例很好地说明了 DirectX* 11 实现,因为其使用了最小数目的 API 调用来设置 DirectX* 11 的 DirectCompute 应用程序并很好地展现了最低需求。总的说来,在编写该示例代码时未考虑生产品质,以便更易于阅读和更具有启发性。因此未检查所有返回的语句,并用了一些时间选取正确的设备,并且如果不重新编译将不能更改窗口大小。让我们根据 DirectX*11 调用的顺序了解实现。
设置设备
设置设备的最简单方法是使用默认值调用 D3D11CreateDeviceAndSwapChain()。这意味着第一个设备支持的所有功能集都会展示给应用程序。对于使用 DirectX* 11 功能级别运行的 DirectX* 11 应用程序,这意味着在创建设备时,需要对其进行验证,确定基本硬件是否支持 DirectX* 11 功能级别。
在创建该交换链后,可检索指向后台缓冲区的指针以及用于写入该后台缓冲区的渲染目标视图。然后将该后台缓冲区设置为主要渲染目标:
常量内存
只有需要为本示例分配的缓冲区为常量缓冲区。要利用该缓冲区提供的读取优化,缓冲区需要调整为 16 字节:
编译计算着色器的方式与编译 DirectX*11 中的其他任何着色器一样:
分派内核
运行计算着色的应用程序代码十分紧凑:
线程编址系统
上文所示的 Dispatch() 调用会创建网格,其由窗口的宽度和高度组成,每一个都除以 16 个线程组成的线程组。换句话讲,对于 640×480 窗口,网格由 x 方向上的 40 个线程和 y 方向上的 30 个线程以及 z 方向上的一个线程组成。由于计算着色器直接写入后台缓冲区,可将此视为基于图块的渲染,其中每个图块使用由 256 个线程组成的线程组渲染。
然后计算着色器使用关键字 numthreads 定义线程组大小为 16×16×1。
下载文章
下载 基于英特尔® Ivy Bridge 处理器显卡的 Microsoft* DirectCompute (Microsoft* DirectCompute on Intel® Ivy Bridge Processor Graphics) [PDF 762KB]Microsoft* DirectCompute 将显卡硬件的计算功能视为新的着色器类型:计算着色器。计算着色器与顶点、几何或像素着色器类似,可提供编程接口,让显卡硬件的大量并行计算性能可用于普通基于光栅的显卡管线(借助 Microsoft* Direct3D* 或 OpenGL* 展现)之外的任务。
注: 尽管通过 Microsoft* DirectX* 11 推出了 DirectCompute,但仍可以在 Microsoft* DirectX* 10、10.1 和 11 类硬件上运行计算着色器。
与同样提供了计算解决方案的应用程序编程接口 (API) 相比,DirectCompute 具有诸多优势。首先,它能够很好地与 Direct3D* 集成,这意味着它不仅能有效地与纹理和缓冲区等 Direct3D* 资源进行互操作,而且许多概念和语言语法都是 Direct3D* 编程人员所熟知的。此外,与其他计算 API 相比,接口更为通用和简单。和 Direct3D* 相似,DirectCompute 确保不同硬件可得到一致的结果。
DirectCompute 应用程序
DirectCompute 在自己正在处理的数据和正在进行处理的线程(例如顶点或像素着色器)之间没有固定的映射。一个线程可处理一个或多个数据元素,并且应用程序可直接控制将多少线程用于执行计算。DirectCompute 的应用程序是不会映射到数据和处理或线程之间的固定映射中的算法,顶点或像素着色器都需要用到该算法,并且无需涉及光栅器。典型使用案例包括:
- 游戏物理和人工智能;
- 在涉及所用数据以及应用在该数据上的线程之间的关系时,将算法应用在需要更为灵活的内核的图像空间中;
- 使用读/写功能的诸多高级渲染效果,例如与顺序无关的透明度、光线跟踪以及全局照明效应。
DirectCompute 内存模型
显卡硬件预期有不同内存类型,每个类型都有利于特定访问模式。在 DirectCompute 中,可区分 基于寄存器的内存、设备内存和组共享的内存。基于寄存器的内存
DirectCompute 使用相同的寄存器组作为 Direct3D* 管线(DirectCompute 寄存器)的其他可编程阶段。由于只能用高级别着色器语言 (HLSL) 对着色器编程,您将不能访问那些寄存器。但是,编译器可生成中间程序集。很难说清程序集代码的价值有多大,因为它只是中间性的,并且可通过基本硬件的驱动程序更改。
最令人关注的基于寄存器的内存要数临时寄存器。总共有 4096 个临时和索引零时寄存器(阵列式)可用。常规临时寄存器命名为 r#,而索引临时阵列寄存器命名为 x#[n]。在处理核心中或数个处理核心之间,可在运行状态下在所有线程之间共享这些寄存器。驱动程序编译器会自动选择和分配这些寄存器,不受着色器程序员的任何直接影响。着色器越复杂就越可能消耗更多临时寄存器,从而使临时寄存器的硬件处于“饥饿”状态。一个良好的显卡硬件性能评测工具将指出硬件用完临时寄存器的实际情况。然后您可尝试覆盖部分着色器以减少所用临时寄存器的数目。
设备内存
尽管临时寄存器中的数据仅在执行着色器程序期间存在,但仍需要保存时间更长的数据并提供更多存储。DirectCompute 可存储一般 Direct3D* 资源(例如缓冲区和纹理)中的数据。可使用读/写缓冲区和纹理、结构化缓冲区以及字节地址(原始)缓冲区。要对纹理和缓冲区进行读写操作,可使用所谓的内存视图。DirectX* 11 引入了无序访问视图 (UAV),可实现分散的写入和集中读取。为了在着色器中读取内存,DirectX* 10 引入了着色器资源视图 (SRV)。一种特殊类别的设备内存是常量缓冲区,适用于进行 16 次连续内存读取的访问模式。该访问模式的一个示例是从常量内存读取 4×4 矩阵。
读/写缓冲区和纹理
预期只读纹理内存适用于在空间上彼此靠近的内存访问模式。例如,双线性过滤需要访问彼此靠近的 texel。DirectX* 11 新增了一组读和写纹理:
RWBufferRWTexture1D, RWTexture1DArrayRWTexture2D, RWTexture2DArrayRWTexture3D
// Compute Shader code: RWTexture with Unordered Access View in u0 RWTexture2D<float4> output : register (u0);
结构化缓冲区
A 结构化缓冲区结构化缓冲区是一个包含结构元素的缓冲区。下面是一个简单的示例:
// Compute Shader code: structured buffer with Unordered Access View in u0
struct BufferStruct
{
float4 color;
};
RWStructuredBuffer<BufferStruct> output : register(u0);
要填充计算着色器中的结构化缓冲区,您可以按如下使用代码:以下代码可在应用程序级别创建结构化缓冲区:uint stride = WindowWidth; // buffer stride, assumes data stride = // data width (i.e. no padding) // DTid is the SV_DispatchThreadID uint idx = (DTid.x) + (DTid.y) * stride; output[idx].color = color;
//
// structured buffer
//
struct BufferStruct
{
float color[4];
};
D3D11_BUFFER_DESC sbDesc;
sbDesc.BindFlags = D3D11_BIND_UNORDERED_ACCESS | D3D11_BIND_SHADER_RESOURCE;
sbDesc.CPUAccessFlags = 0;
sbDesc.MiscFlags = D3D11_RESOURCE_MISC_BUFFER_STRUCTURED; sbDesc.StructureByteStride = sizeof(BufferStruct);
int Height = WindowHeight;
int Width = WindowWidth;
sbDesc.ByteWidth = sbDesc.StructureByteStride * Width * Height; sbDesc.Usage = D3D11_USAGE_DEFAULT;
pd3dDevice->CreateBuffer(&sbDesc, NULL, &pStructuredBuffer);
字节地址缓冲区
字节地址缓冲区或原始缓冲区都是特殊的缓冲区类型,以偏移缓冲区起始点一个字节的方式编址。字节偏移必须为 4 的倍数,以使字对齐。
原始缓冲区的类型始终为 32 位无符号整型。其他数据类型需要换算为无符号整型。原始缓冲区可用于通过 DirectCompute 生成几何,因为可将它们限定为顶点和索引缓冲区。在 HLSL 中,按如下对其进行声明:
DirectX* 11 将原始缓冲区调整为 16 位。ByteAddressBuffer RWByteAddressBuffer
常量缓冲区
常量缓冲区提供数据的只读访问权限,预期以将数据作为 16 个连续浮点值的方式访问数据。只要按顺序对其进行访问,开销就与只读取一个值相似。
着色器可访问 4096 32 位四分量常量:64 KB。尽管 DirectX* 10.x 和 DirectX* 11 将此定义为常量缓冲区大小的上限,但 DirectX* 11.1 可让您在常量缓冲区中存储更多常量并访问着色器中此缓冲区的子范围:
// Create constant buffer
typedef struct
{
float diffuse[4]; // diffuse shading color
float mu[4]; // quaternion julia parameter
float epsilon; // detail julia
int c_width; // view port size
int c_height;
int selfShadow; // selfshadowing on or off
float orientation[4*4]; // rotation matrix
float zoom;
} QJulia4DConstants;
D3D11_BUFFER_DESC Desc;
Desc.Usage = D3D11_USAGE_DYNAMIC;
Desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
Desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
Desc.MiscFlags = 0;
// must be multiple of 16 bytes
Desc.ByteWidth = ((sizeof( QJulia4DConstants ) + 15)/16)*16;
pd3dDevice->CreateBuffer(&Desc, NULL, &pcbFractal);
着色器资源视图和无序访问视图
与 DirectX* 管线中的其他着色器阶段相似,在 DirectCompute 中支持 SRV 以允许着色器读取资源内存。如果为结构化缓冲区,可如下创建 SRV:
UAV 可让您随机分散写入字节地址或原始缓冲区以及结构化缓冲区,然后在读取这些缓冲区时随机聚集。DirectX* 11 可同时绑定八个 UAV。// // shader resource view on structured buffer // D3D11_SHADER_RESOURCE_VIEW_DESC sbSRVDesc; ZeroMemory( &sbSRVDesc, sizeof( sbSRVDesc ) ); sbSRVDesc.Buffer.ElementOffset = 0; sbSRVDesc.Buffer.ElementWidth = sbDesc.StructureByteStride; sbSRVDesc.Buffer.FirstElement = sbUAVDesc.Buffer.FirstElement; sbSRVDesc.Buffer.NumElements = sbUAVDesc.Buffer.NumElements; sbSRVDesc.Format = DXGI_FORMAT_UNKNOWN; sbSRVDesc.ViewDimension = D3D11_SRV_DIMENSION_BUFFER; hr = pd3dDevice->CreateShaderResourceView((ID3D11Resource *) pStructuredBuffer, &sbSRVDesc, &pComputeShaderSRV);
结构化缓冲区的 UAV 代码形式与下面类似:
在 DirectX* 11.x 中,UAV 还允许使用名为“附加并使用”的新访问模式。该模式可构建和访问列表或堆栈形式的数据。附加并使用缓冲区是具有专门创建的 UAV 的结构化或原始缓冲区:// Unordered access view on structured buffer D3D11_UNORDERED_ACCESS_VIEW_DESC sbUAVDesc; ZeroMemory( &sbUAVDesc, sizeof(sbUAVDesc) ); sbUAVDesc.Buffer.FirstElement = 0; sbUAVDesc.Buffer.Flags = 0; sbUAVDesc.Buffer.NumElements = sbDesc.ByteWidth / sbDesc.StructureByteStride; sbUAVDesc.Format = DXGI_FORMAT_UNKNOWN; sbUAVDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; HRESULT hr = pd3dDevice>CreateUnorderedAccessView((ID3D11Resource *)pStructuredBuffer, &sbUAVDesc, &pComputeOutputUAV);
在 HLSL 中,AppendStructuredBuffer// Unordered access view on structured buffer D3D11_UNORDERED_ACCESS_VIEW_DESC sbUAVDesc; ZeroMemory( &sbUAVDesc, sizeof(sbUAVDesc) ); sbUAVDesc.Buffer.FirstElement = 0; sbUAVDesc.Buffer.Flags = D3D11_BUFFER_UAV_FLAG_APPEND; sbUAVDesc.Buffer.NumElements = sbDesc.ByteWidth / sbDesc.StructureByteStride; sbUAVDesc.Format = DXGI_FORMAT_UNKNOWN; sbUAVDesc.ViewDimension = D3D11_UAV_DIMENSION_BUFFER; HRESULT hr = pd3dDevice>CreateUnorderedAccessView((ID3D11Resource *)pStructuredBuffer, &sbUAVDesc, &pComputeOutputUAV);
线程组共享内存
线程组共享内存 (TGSM) 位于片上内存上。可将其视为高速缓存以最小化片外带宽使用量。线程组中的所有线程访问本内存。换言之,TGSM 允许给定组中的线程以协作和共享数据。与全局缓冲区负载和存储相比对共享内存的读写更快 – 接近寄存器读写速度。
一种常用的编程模式是让组内的线程以协作方式加载数据块到共享内存,处理数据,然后将结果写入可写入缓冲区。一个常见的示例是为后处理管线存储水平或垂直模糊内核的所有相邻像素。
TGSM 不会一直停留在分派调用之间。因此需要将一个分派调用的结果存储在其他位置。使用 groupshared 类型限定词在 HLSL 着色器中指示 TGSM:
groupshared float sharedmem[256];
DirectCompute 线程模型
在基于传统 CPU 的算法中使用的典型多线程范例使用分离的处理器核心和线程来执行,并采用共享内存空间和手动同步。诸如英特尔® Ivy Bridge 的高端 CPU 有两个核心,最多支持两个线程。DirectCompute 使用不同的线程模型。具备 DirectCompute 的设备可运行数千个线程,可灵活地将线程映射至数据元素,同时同一着色器或程序执行所有这些线程,这个过程称为并行内核。
内核处理
在执行计算着色器时,将其视为一个处理内核。会对每个线程将内核实例化并应用至一组数据。该数据通过绑定至 DirectCompute 阶段的 Direct3D* 资源提供。也就是说,可对每个硬件线程分派执行单独调用内核的任务,该内核对于分派调用中的所有线程相同。
这意味着您可将 DirectCompute 应用程序的典型数据分拆为足够小的数据,从而可以分离方式处理数据。典型的 DirectCompute 应用程序需要由大量结构相似的数据段(显卡硬件的典型域)组成的数据。
分派内核
执行计算着色器也称为分派内核。DirectX* 11.x 中有两个函数可分派内核:
第一个方法需要三个值,这些值代表在三个维度中应该分派的共享组数。例如,如果应用程序使用 4、8 和 2 调用 Dispatch() 方法,则总共会启动 64 个线程组。每一个线程组中拥有的线程数在计算着色器中指定。Dispatch(UINT ThreadGroupCountX, UINT ThreadGroupCountY, UINT ThreadGroupCountX); DispatchIndirect(ID3D11Buffer *pBufferForArgs, UINT AlignedOffsetForArgs);
第二个方法 DispatchIndirect() 间接进行分派调用,方法是允许它使用 Dispatch() 调用预期的参数填充缓冲区,然后根据缓冲区数据分派新作业。以下代码段说明了如何调用 Dispatch() 并在线程组中提供线程数的典型示例:
在 x 的线程组情况中,窗口的宽度除以应该在线程组中的线程数。线程数在 HLSL 着色器代码中定义。在每个线程组中有 16 个线程以及窗口大小为 800 的情况下,应用程序将使用 50 个线程组,每个线程组由 16 个线程组成。对于 y 方向,如果窗口高度为 640,将有 20 个线程组,每个线程组由 32 个线程组成。该示例分派 1000 个线程组,每个具有 512 个线程。因此将有 512,000 个线程处于运行状态。// C++ application code pImmediateContext->Dispatch(Width / THREADSX, Height / THREADSY, 1 ); // HLSL compute shader code [numthreads(THREADSX, THREADSY, 1)] void CS_QJulia4D( uint3 Gid : SV_GroupID, uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint GI : SV_GroupIndex ) ...
DirectX* 11.x 支持 3D 组和线程。将 DirectX* 11 中线程组中的线程视为 3D 阵列。每个线程“阵列”作为 2D 或 3D 阵列都是线程组的一部分。通过使用保留线程和线程组维度的寄存器将线程组中的线程编址。
线程编址系统
上面示例中 512,000 个线程中的每一个都执行一个内核或计算着色器的实例。每个内核如何得知哪个线程负责执行它?了解哪个线程正在执行内核对于在数据中编索引很重要,由此就可从 Direct3D* 资源读取数据。DirectCompute 运行时将存储在寄存器中的系统值提供给内核。四个寄存器保留该数据:
vThreadID.xyzvThreadGroupID.xyzvThreadIDInGroup.xyzvThreadIDInGroupFlattended
SV_DispatchThreadID- 每个维度中整个分派中的线程索引:x - 0..x - 1; y - 0..y - 1; z - 0..z - 1SV_GroupID- 分派中线程组的索引 - 例如调用 Dispatch(2,1,1) 会得到可能值 0,0,0 和 1,0,0,从 0 至 (numthreadsX * numthreadsY * numThreadsZ) – 1 变动SV_GroupThreadID- SV_GroupIndex 的 3D 版本 – 如果指定了 numthreads(3,2,1),SV_GroupThreadID 输入值的可能值将有一系列值 (0–2,0–1,0)SV_GroupIndex- 线程组中线程的索引
RWTexture2D<float4> output : register (u0);
void CS_QJulia4D( uint3 Gid : SV_GroupID, uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint GI : SV_GroupIndex )
{
...
output[DTid.xy] = color;
}
下面的代码说明了如何访问计算着色器中的 1D 结构化缓冲区: struct BufferStruct
{
float4 color;
};
RWStructuredBuffer&;lt;BufferStruct> output : register (u0); // UAV 0
void CS_QJulia4D( uint3 Gid : SV_GroupID, uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint GI : SV_GroupIndex )
{
...
uint stride = c_width;
uint idx = (DTid.x) + (DTid.y) * stride;
output[idx].color = color;
}
线程同步
对于传统多线程编程模型,众多线程可读写同一内存位置,因此可能因为写后读的危险造成内存损坏。要同步线程的内存访问,可使用内存壁垒和原子函数。内存壁垒
在 DirectX* 11.x 中,六个不同的 HLSL 内部函数称为内存壁垒,可同步线程执行和内存写入:
AllMemoryBarrier/*WithGroupSyncDeviceMemoryBarrier/*WithGroupSyncGroupMemoryBarrier/*WithGroupSync
有用于 TGSM、设备内存和同时用于这两种内存的 *内存壁垒。*MemoryBarrierWithGroupSync 在未完成的内存操作(这些操作在调用时处于活动状态)完成后并且组内的所有线程均接收到指令后才运行。在以下代码中示出了使用内存壁垒的典型示例:
for (uint tile = 0; tile < numTiles; tile++)
{
sharedPos[threadId] = particles[…];
GroupMemoryBarrierWithGroupSync();
// gravitation() uses sharedPos[] as input data
acceleration = gravitation(…);
GroupMemoryBarrierWithGroupSync();
}
壁垒停止未完成的内存操作的粒度为 4 字节。内存壁垒用于同步整组线程:它们并非只同步线程组中一少部分线程的适用解决方案。这种情况将可以使用原子函数。 原子函数
DirectX* 11.x 在计算和像素着色器中支持原子函数或互锁函数。它们能确保自动运行,换句话讲,它们能确保以设定的顺序运行。下面是原子函数的一览表:
InterlockedAddInterlockedMinInterlockedMaxInterlockedOrInterlockedAndInterlockedXorInterlockedCompareStoreInterlockedCompareExchangeInterlockedExchange
请注意所有这些整数原子期望 Add、Min 和 Max 在 asInteger() 中传递时按原样在浮点数上起作用。但是只要所有浮点数为正数,Min 和 Max 都按原样在 asInteger() 浮点数上起作用。
示例
示例程序展示了曼德布洛特集合。(在维基百科上清楚地说明了该算法。) Jan Vlietinck 的网站和其他网站展示了使用源代码的实现。图 1 显示了示例程序的截屏。
图 1. 曼德布洛特
图 2 示出了运行在英特尔® Ivy Bridge 上的演示应用程序截屏。

图 2. 曼德布洛特
该示例很好地说明了 DirectX* 11 实现,因为其使用了最小数目的 API 调用来设置 DirectX* 11 的 DirectCompute 应用程序并很好地展现了最低需求。总的说来,在编写该示例代码时未考虑生产品质,以便更易于阅读和更具有启发性。因此未检查所有返回的语句,并用了一些时间选取正确的设备,并且如果不重新编译将不能更改窗口大小。让我们根据 DirectX*11 调用的顺序了解实现。
设置设备
设置设备的最简单方法是使用默认值调用 D3D11CreateDeviceAndSwapChain()。这意味着第一个设备支持的所有功能集都会展示给应用程序。对于使用 DirectX* 11 功能级别运行的 DirectX* 11 应用程序,这意味着在创建设备时,需要对其进行验证,确定基本硬件是否支持 DirectX* 11 功能级别。
该代码询问硬件是否至少支持 DirectX* 11.0 功能级别。如果不支持,返回值将显示错误。// // Initialize Direct3D device and swap chain // DXGI_SWAP_CHAIN_DESC sd; // … // this qualifies the back buffer for being the target of compute shader writes sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_UNORDERED_ACCESS | DXGI_USAGE_SHADER_INPUT; // … // return value -> what the hardware supports D3D_FEATURE_LEVEL MaxFeatureLevel = D3D_FEATURE_LEVEL_11_0; // we are asking for DirectX 11 feature level support here D3D_FEATURE_LEVEL FeatureLevel = D3D_FEATURE_LEVEL_11_0; HRESULT hr = D3D11CreateDeviceAndSwapChain( NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, D3D11_CREATE_DEVICE_DEBUG, &FeatureLevel, 1, D3D11_SDK_VERSION, &sd, &pSwapChain, &pd3dDevice, &MaxFeatureLevel, &pImmediateContext);
在创建该交换链后,可检索指向后台缓冲区的指针以及用于写入该后台缓冲区的渲染目标视图。然后将该后台缓冲区设置为主要渲染目标:
支持 DirectX*11 的硬件支持 RWTexture 并且可允许您将 UAV 应用到该纹理中。以上代码从交换链检索后台缓冲区并创建指向缓冲区的 UAV。这样就无需写入结构化缓冲区并稍后读取该缓冲区。DXGI_SWAP_CHAIN_DESC sdtemp; pSwapChain->GetDesc(&sdtemp); // get access to the back buffer via a texture ID3D11Texture2D* pTexture; pSwapChain->GetBuffer(0, __uuidof( ID3D11Texture2D ), ( LPVOID* )&pTexture ); // create shader unordered access view on back buffer for compute shader to write into texture pd3dDevice->CreateUnorderedAccessView(pTexture, NULL, &pComputeOutput );
常量内存
只有需要为本示例分配的缓冲区为常量缓冲区。要利用该缓冲区提供的读取优化,缓冲区需要调整为 16 字节:
稍候在 Render() 函数中,通过将系统内存副本映射至 GPU 内存填充该常量缓冲区:// // Create constant buffer // D3D11_BUFFER_DESC Desc; Desc.Usage = D3D11_USAGE_DYNAMIC; Desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; Desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; Desc.MiscFlags = 0; Desc.ByteWidth = ((sizeof( MandelbrotConstants ) + 15)/16)*16; // must be multiple of 16 bytes pd3dDevice->CreateBuffer(&Desc, NULL, &pcbFractal);
编译计算着色器内核// Fill constant buffer D3D11_MAPPED_SUBRESOURCE msr; pImmediateContext->Map(pcbFractal, 0, D3D11_MAP_WRITE_DISCARD, 0, &msr); *(MandelbrotConstants *)msr.pData = MandelC; pImmediateContext->Unmap(pcbFractal,0);
编译计算着色器的方式与编译 DirectX*11 中的其他任何着色器一样:
主要差别在于 cs_5_0 目标的指定。// // compile the compute shader // if(D3DX11CompileFromFile(L"Mandelbrot.hlsl", NULL, NULL, "Mandelbrot", "cs_5_0", 0, 0, NULL, &pByteCodeBlob, &pErrorBlob, NULL)!= S_OK) MessageBoxA(NULL, (char *)pErrorBlob->GetBufferPointer(), "Error", MB_OK | MB_ICONERROR); if(pd3dDevice->CreateComputeShader(pByteCodeBlob->GetBufferPointer(), pByteCodeBlob->GetBufferSize(), NULL, &pCompiledComputeShader)!= S_OK) MessageBoxA(NULL, "CreateComputerShader() failed", "Error", MB_OK | MB_ICONERROR);
分派内核
运行计算着色的应用程序代码十分紧凑:
在调用计算着色器执行的 Dispatch() 调用之前,会设置计算着色器和 UAV 以写入后台缓冲区,并会设置常量缓冲区,该缓冲区用于保留曼德布洛特算法的数据。// Set compute shader pImmediateContext->CSSetShader(pCompiledComputeShader, NULL, 0 ); // UAV for CS output into back-buffer pImmediateContext->CSSetUnorderedAccessViews(0, 1, &pComputeOutput, NULL); // CS constant buffer pImmediateContext->CSSetConstantBuffers(0, 1, &pcbFractal ); // Run the CS pImmediateContext->Dispatch((gWidth) / 16, (gHeight) / 16, 1 ); // make it visible pSwapChain->Present( 0, 0 );
线程编址系统
上文所示的 Dispatch() 调用会创建网格,其由窗口的宽度和高度组成,每一个都除以 16 个线程组成的线程组。换句话讲,对于 640×480 窗口,网格由 x 方向上的 40 个线程和 y 方向上的 30 个线程以及 z 方向上的一个线程组成。由于计算着色器直接写入后台缓冲区,可将此视为基于图块的渲染,其中每个图块使用由 256 个线程组成的线程组渲染。
然后计算着色器使用关键字 numthreads 定义线程组大小为 16×16×1。
[numthreads(16, 16, 1)]
void Mandelbrot( uint3 Gid : SV_GroupID, uint3 DTid : SV_DispatchThreadID, uint3 GTid : SV_GroupThreadID, uint GI : SV_GroupIndex )
{
…
output[ DTid.xy ] = color;
}
SV_DispatchThreadID 系统值用于对正确的线程编址。该运行时生成的值在整个分派中提供线程的索引。对于曼德布洛特计算着色器,output 变量输出至作为后台缓冲区的 2D 纹理。它编有索引,从而通过计算着色器的不同实例可同时填充所有图块。 总结
DirectCompute 允许您在英特尔® Ivy Bridge 硬件上对计算着色器进行编程。从而可将需要线程和数据间关系更为宽松的操作或无需光栅器的操作应用到显卡硬件中。这样可以更加细致地平衡 CPU 和处理器显卡之间的负载。参考资料
- 有关曼德布洛特集合的维基百科,http://en.wikipedia.org/wiki/Mandelbrot_set
- cs_5_0 中使用的寄存器 http://msdn.microsoft.com/en-us/library/hh447206(v=VS.85).aspx
- Jan Vlietinck, http://users.skynet.be/fquake
- Jason Zink、Matt Pettineo、Jack Hoxley“借助 Direct3D 11 的实用渲染和计算”,CRC Press,2011 年;第 305 页。
