基于英特尔® Ivy Bridge 处理器显卡的 Microsoft* DirectCompute

分类:
作者 Wolfgang Engel

下载文章

下载 基于英特尔® 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 新增了一组读和写纹理:

  • RWBuffer
  • RWTexture1D, RWTexture1DArray
  • RWTexture2D, RWTexture2DArray
  • RWTexture3D
下面是如何在着色器中定义 RWTexture2D 纹理的示例:
// 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 中,按如下对其进行声明:
ByteAddressBuffer
RWByteAddressBuffer
DirectX* 11 将原始缓冲区调整为 16 位。


常量缓冲区

常量缓冲区提供数据的只读访问权限,预期以将数据作为 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:
// 
// 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 可同时绑定八个 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);
在 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 = 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);
在 HLSL 中,AppendStructuredBuffer 提供 append(T) 方法;ConsumeStructuredBuffer 提供 T .consume() 方法。


线程组共享内存

线程组共享内存 (TGSM) 位于片上内存上。可将其视为高速缓存以最小化片外带宽使用量。线程组中的所有线程访问本内存。换言之,TGSM 允许给定组中的线程以协作和共享数据。与全局缓冲区负载和存储相比对共享内存的读写更快 – 接近寄存器读写速度。

一种常用的编程模式是让组内的线程以协作方式加载数据块到共享内存,处理数据,然后将结果写入可写入缓冲区。一个常见的示例是为后处理管线存储水平或垂直模糊内核的所有相邻像素。

TGSM 不会一直停留在分派调用之间。因此需要将一个分派调用的结果存储在其他位置。使用 groupshared 类型限定词在 HLSL 着色器中指示 TGSM:
groupshared float sharedmem[256];
 

DirectCompute 线程模型

在基于传统 CPU 的算法中使用的典型多线程范例使用分离的处理器核心和线程来执行,并采用共享内存空间和手动同步。诸如英特尔® Ivy Bridge 的高端 CPU 有两个核心,最多支持两个线程。

DirectCompute 使用不同的线程模型。具备 DirectCompute 的设备可运行数千个线程,可灵活地将线程映射至数据元素,同时同一着色器或程序执行所有这些线程,这个过程称为并行内核


内核处理
在执行计算着色器时,将其视为一个处理内核。会对每个线程将内核实例化并应用至一组数据。该数据通过绑定至 DirectCompute 阶段的 Direct3D* 资源提供。也就是说,可对每个硬件线程分派执行单独调用内核的任务,该内核对于分派调用中的所有线程相同。

这意味着您可将 DirectCompute 应用程序的典型数据分拆为足够小的数据,从而可以分离方式处理数据。典型的 DirectCompute 应用程序需要由大量结构相似的数据段(显卡硬件的典型域)组成的数据。


分派内核
执行计算着色器也称为分派内核。DirectX* 11.x 中有两个函数可分派内核:
Dispatch(UINT ThreadGroupCountX, UINT ThreadGroupCountY, UINT ThreadGroupCountX);

DispatchIndirect(ID3D11Buffer *pBufferForArgs, UINT AlignedOffsetForArgs);
第一个方法需要三个值,这些值代表在三个维度中应该分派的共享组数。例如,如果应用程序使用 4、8 和 2 调用 Dispatch() 方法,则总共会启动 64 个线程组。每一个线程组中拥有的线程数在计算着色器中指定。

第二个方法 DispatchIndirect() 间接进行分派调用,方法是允许它使用 Dispatch() 调用预期的参数填充缓冲区,然后根据缓冲区数据分派新作业。以下代码段说明了如何调用 Dispatch() 并在线程组中提供线程数的典型示例:
// 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 )
...
在 x 的线程组情况中,窗口的宽度除以应该在线程组中的线程数。线程数在 HLSL 着色器代码中定义。在每个线程组中有 16 个线程以及窗口大小为 800 的情况下,应用程序将使用 50 个线程组,每个线程组由 16 个线程组成。对于 y 方向,如果窗口高度为 640,将有 20 个线程组,每个线程组由 32 个线程组成。该示例分派 1000 个线程组,每个具有 512 个线程。因此将有 512,000 个线程处于运行状态。

DirectX* 11.x 支持 3D 组和线程。将 DirectX* 11 中线程组中的线程视为 3D 阵列。每个线程“阵列”作为 2D 或 3D 阵列都是线程组的一部分。通过使用保留线程和线程组维度的寄存器将线程组中的线程编址。


线程编址系统

上面示例中 512,000 个线程中的每一个都执行一个内核或计算着色器的实例。每个内核如何得知哪个线程负责执行它?了解哪个线程正在执行内核对于在数据中编索引很重要,由此就可从 Direct3D* 资源读取数据。

DirectCompute 运行时将存储在寄存器中的系统值提供给内核。四个寄存器保留该数据:

  • vThreadID.xyz
  • vThreadGroupID.xyz
  • vThreadIDInGroup.xyz
  • vThreadIDInGroupFlattended
寄存器保留的值可在计算着色器中通过以下语义访问:

  • SV_DispatchThreadID - 每个维度中整个分派中的线程索引:x - 0..x - 1; y - 0..y - 1; z - 0..z - 1
  • SV_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 - 线程组中线程的索引
在以下源代码中示出了写入 2D 纹理的简单示例:
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/*WithGroupSync
  • DeviceMemoryBarrier/*WithGroupSync
  • GroupMemoryBarrier/*WithGroupSync
内存壁垒可发出这样的指令:“请等待内存操作完成。” 使用这种壁垒可确保当设备或 TGSM 中的线程彼此共享数据时,写入内存的所需值有机会在被其他线程读取前被写入。换言之,在执行写入指令的着色器核心和由 GPU 的内存系统实际执行并写入内存的指令之间有重大差别。根据基本硬件,在写入值和值实际在其内存目的地终止之间的时间量是可变的。

有用于 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 在计算和像素着色器中支持原子函数或互锁函数。它们能确保自动运行,换句话讲,它们能确保以设定的顺序运行。下面是原子函数的一览表:

  • InterlockedAdd
  • InterlockedMin
  • InterlockedMax
  • InterlockedOr
  • InterlockedAnd
  • InterlockedXor
  • InterlockedCompareStore
  • InterlockedCompareExchange
  • InterlockedExchange
除了 InterlockedExchange 以外,所有函数在 TGSM 中仅接受整数或无符号整数值。例如,如果计算着色器要保留遇到特定值的线程数计数,可调用 InterlockedAdd()。InterlockedCompareExchange() 会比较目的地的值和参考值,如果这两者匹配,则会将第三个自变量写入目的地 (Zink)。

请注意所有这些整数原子期望 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 功能级别。
   //
   // 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.0 功能级别。如果不支持,返回值将显示错误。

在创建该交换链后,可检索指向后台缓冲区的指针以及用于写入该后台缓冲区的渲染目标视图。然后将该后台缓冲区设置为主要渲染目标:
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 );
支持 DirectX*11 的硬件支持 RWTexture 并且可允许您将 UAV 应用到该纹理中。以上代码从交换链检索后台缓冲区并创建指向缓冲区的 UAV。这样就无需写入结构化缓冲区并稍后读取该缓冲区。


常量内存
只有需要为本示例分配的缓冲区为常量缓冲区。要利用该缓冲区提供的读取优化,缓冲区需要调整为 16 字节:
//
// 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);		
稍候在 Render() 函数中,通过将系统内存副本映射至 GPU 内存填充该常量缓冲区:
// 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 中的其他任何着色器一样:
//
// 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);
主要差别在于 cs_5_0 目标的指定。


分派内核
运行计算着色的应用程序代码十分紧凑:
// 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() 调用之前,会设置计算着色器和 UAV 以写入后台缓冲区,并会设置常量缓冲区,该缓冲区用于保留曼德布洛特算法的数据。


线程编址系统
上文所示的 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 和处理器显卡之间的负载。


参考资料

 

关于作者

Wolfgang Engel 是 Confetti* 的 CTO/CEO 和共同创始人
Confetti*, 是电子游戏和电影行业的先进实时图形研究领域的智囊。之前他作为首席图形程序员在 Rockstar 的核心技术团队工作了 4 年以上。在该网址可了解他参与制作的游戏 http://www.mobygames.com/developer/sheet/view/developerId,158706. 。他是 ShaderXGPU Pro 这两本书的编者,并著有其他书籍,而且是全球性会议中图形编程领域的重要发言人。自 2006 年 7 月起他就是 DirectX* MVP 的顶级专家,在业内的数个咨询委员会内皆很活跃。他还在圣地亚哥的加利福尼亚大学教授课程“GPU 编程”。您还可以在 Twitter 上了解他,帐户名为 @wolfgangengel。

如需更全面地了解编译器优化,请参阅优化注意事项.