Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader)

本文是《3D Game Programming with DirectX 12》学习笔记,主要讲解了计算着色器(Compute Shader)的基础知识,包括线程和线程组、输入输出资源、纹理的处理以及模糊Demo的实现。文章详细介绍了如何创建和使用计算着色器,以及如何利用GPU的并行计算能力进行高效处理。通过实例演示了如何进行水平和垂直模糊,以及如何优化纹理采样以提升性能。
摘要由CSDN通过智能技术生成

代码工程地址:

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)。



学习目标

  1. 学习如何编写计算着色器程序;
  2. 对硬件如何与线程组和线程处理有一个基本的高级理解;
  3. 学习哪些D3D资源可以作为CS的输入,哪些可以作为输出;
  4. 理解线程ID变量和他们的用途;
  5. 学习共享内存,已经它们如何用来优化性能;
  6. 查找更多有关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];
}

一个计算着色器包含下面的组件:

  1. 一个全局变量用来访问常量缓冲;
  2. 输入和输出资源,下节介绍;
  3. [numthreads(X, Y, Z)]属性,指定在线程组中线程的数量;
  4. 着色器主体执行代码;
  5. 线程识别系统参数;

观察上面的代码,线程组中的线程可以有不同的线程拓扑结构,主要根据你的问题需求来选择不同的拓扑结构。尺寸最好是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 
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值