代码工程地址:
https://github.com/jiabaodan/Direct12BookReadingNotes
GPU已经被优化为处理单个地址或者连续地址(流操作)的大量内存数据;这和CPU的随机内存访问形成鲜明对比。因为顶点和像素可以独立处理,所以GPU被架构为大量的并行运算;比如NVIDIA的“Fermi”架构支持16个拥有32个CUDA cores的流多处理器(streaming multiprocessors),总共可以由512个CUDA cores。
使用GPU计算非图形的应用称之为普通目的的GPU编程(general purpose GPU (GPGPU) programming)。
学习目标
- 学习如何编写计算着色器程序;
- 对硬件如何与线程组和线程处理有一个基本的高级理解;
- 学习哪些D3D资源可以作为CS的输入,哪些可以作为输出;
- 理解线程ID变量和他们的用途;
- 学习共享内存,已经它们如何用来优化性能;
- 查找更多有关GPGPU编程的资料。
1 线程(THREADS)和线程组(THREAD GROUPS)
在GPU编程中,多个用以处理的线程会划分为一个格子的线程组,一个线程组在单个处理器上执行。所以如果你的GPU有16个多处理器,那么你至少要把你的需求划分为16个线程组,这样你所有的多处理器都可以同时计算。为了有更好的性能,你应该为每个多处理器划分2个线程组,这样就可以切换线程组([Fung10])。
每个线程组获取的共享内存,可以让所有线程组内的线程访问;线程不能访问其他线程组的共享内存。
一个线程组包含n个线程。硬件把这些线程划分为warps(32个线程为一个warp),然后warps被多处理器以SIMD32来处理。每个CUDA core处理一个线程并且回顾“Fermi”多处理器,有32个CUDA cores。在D3D中你可以用一个不是32的倍数的值指定一个线程组的大小,但是出于性能考虑,最好还是指定为warp大小的倍数([Fung10])。
对于不同的硬件,设置线程组为256看起来是一个好的开始,然后再尝试其他尺寸。
NVIDIA使用warp尺寸(32线程);ATI使用wavefront尺寸(64线程),并且建议线程组尺寸要一直是wavefront的倍数。当然,warp和wavefront在将来的硬件中可能会改变。
在D3D,线程组有下面的函数开始:
void ID3D12GraphicsCommandList::Dispatch(
UINT ThreadGroupCountX,
UINT ThreadGroupCountY,
UINT ThreadGroupCountZ);
本书只关心2维。下面的例子表示x方向有3个线程组,y方向有2个线程组,所以总共6个:
2 一个简单的计算着色器
下面是一个简单的计算着色器,对两个相同尺寸的纹理相加:
cbuffer cbSettings
{
// Compute shader can access values in constant buffers.
};
// Data sources and outputs.
Texture2D gInputA;
Texture2D gInputB;
RWTexture2D<float4> gOutput;
// The number of threads in the thread group. The threads in a group can
// be arranged in a 1D, 2D, or 3D grid layout.
[numthreads(16, 16, 1)]
void CS(int3 dispatchThreadID : SV_DispatchThreadID) // Thread ID
{
// Sum the xyth texels and store the result in the xyth texel of
// gOutput.
gOutput[dispatchThreadID.xy] = gInputA[dispatchThreadID.xy] + gInputB[dispatchThreadID.xy];
}
一个计算着色器包含下面的组件:
- 一个全局变量用来访问常量缓冲;
- 输入和输出资源,下节介绍;
- [numthreads(X, Y, Z)]属性,指定在线程组中线程的数量;
- 着色器主体执行代码;
- 线程识别系统参数;
观察上面的代码,线程组中的线程可以有不同的线程拓扑结构,主要根据你的问题需求来选择不同的拓扑结构。尺寸最好是wavefront的倍数(因为同时也是warp的倍数),这样就可以同时兼容两种显卡。
2.1 计算PSO
为了开启计算着色器,我们使用一个特殊的“计算渲染状态描述”。它的属性要比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"])));
根签名描述了哪些输入参数。下面是编译CS代码的例子:
mShaders["wavesUpdateCS"] = d3dUtil::CompileShader(
L"Shaders\\WaveSim.hlsl", nullptr,
"UpdateWavesCS", "cs_5_0");
3 输入和输出资源
CS支持2种类型的资源:缓冲和纹理。
3.1 纹理的输入
在上一章的例子中,定义了2个纹理输入:
Texture2D gInputA;
Texture2D gInputB;
它们通过创建(SRVs)来传递:
cmdList->SetComputeRootDescriptorTable(1, mSrvA);
cmdList->SetComputeRootDescriptorTable(2, mSrvB);
这个和像素着色器的绑定是一样的(SRVs是只读的)。
3.2 纹理的输出和无序访问视图(UAVs)
之前的代码中创建了一个输出资源:
RWTexture2D<float4> gOutput;
输出资源比较特殊,并有一个特殊的前缀“RW”表示可以读写(read-write)。相比之下gInputA和gInputB是只读的。并且需要指定类型和维度。比如如果我们需要输出2D的整形类型DXGI_FORMAT_R8G8_SINT,那么需要这样写:
RWTexture2D<int2> gOutput;
绑定输出资源到CS,需要新的视图类型unordered access view (UAV),它在代码中通过描述句柄和D3D12_UNORDERED_ACCESS_VIEW_DESC描述来表示。它与SRV的创建类似,下面是创建UAV的例子:
D3D12_RESOURCE_DESC texDesc;
ZeroMemory(&texDesc, sizeof(D3D12_RESOURCE_DESC));
texDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
texDesc.Alignment = 0;
texDesc.Width = mWidth;
texDesc.Height = mHeight;
texDesc.DepthOrArraySize = 1;
texDesc.MipLevels = 1;
texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
texDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&texDesc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(&mBlurMap0)));
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = mFormat;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = 1;
D3D12_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
uavDesc.Format = mFormat;
uavDesc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D;
uavDesc.Texture2D.MipSlice = 0;
md3dDevice->CreateShaderResourceView(mBlurMap0.Get(),
&srvDesc, mBlur0CpuSrv);
md3dDevice->CreateUnorderedAccessView(mBlurMap0.Get(),
nullptr, &uavDesc, mBlur0CpuUav);
如果一个纹理要绑定为UAV,它必须要通过flag值为D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS来创建。
回顾描述堆的类型D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV可以混合它们到同一个堆上。当放它们到堆上的时候,我们只需要针对分派调用(dispatch call)通过传递描述句柄到根参数上来绑定资源到流水线。下面是针对CS的根签名代码:
void BlurApp::BuildPostProcessRootSignature()
{
CD3DX12_DESCRIPTOR_RANGE srvTable;
srvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
CD3DX12_DESCRIPTOR_RANGE uavTable;
uavTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_UAV, 1, 0);
// Root parameter can be a table, root descriptor or root constants.
CD3DX12_ROOT_PARAMETER slotRootParameter[3];
// Perfomance TIP: Order from most frequent to least frequent.
slotRootParameter[0].InitAsConstants(12, 0);
slotRootParameter[1].InitAsDescriptorTable(1, &srvTable);
slotRootParameter[2].InitAsDescriptorTable(1, &uavTable);
// A root signature is an array of root parameters.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(3,
slotRootParameter,
0, nullptr,
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_// create a root signature with a single slot which points to a
// descriptor range consisting of a single constant buffer
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc,
D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(),
errorBlob.GetAddressOf());
if(errorBlob != nullptr)
{
::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
}
ThrowIfFailed(hr);
ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),
serializedRootSig->GetBufferSize(),
IID_PPV_ARGS(mPostProcessRootSignature.GetAddressOf())));
}
在分派调用前,我们绑定常量和描述:
cmdList->SetComputeRootSignature(rootSig);
cmdList->SetComputeRoot32BitConstants(0, 1, &blurRadius, 0);
cmdList->SetComputeRoot32BitConstants(0, (UINT)weights.size(), weights.data(), 1);
cmdList->SetComputeRootDescriptorTable(1, mBlur0GpuSrv);
cmdList->SetComputeRootDescriptorTable(2, mBlur1GpuUav);
UINT numGroupsX