转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=3420

本系列前面的五篇已经让引擎可以在D3D12下跑通所有的例子。但这并不代表功能已经齐全。其中很多例子能跑通的原因,是因为例子本身有fall back的代码,允许在没有各种高级功能的情况下运行。Compute shader就是其中的一个。本篇将会讲解如何加入compute shader,以及在此过程中遇到的一个大坑。

计算引擎

和以前的API不同的是,D3D12是个多引擎的API,可以让硬件的不同的独立部分异步执行,以提高效率。D3D12里的引擎有,渲染引擎、计算引擎和拷贝引擎。这三个引擎有不同的指令队列,可以在程序的控制下并行执行和互相同步。所以,在D3D12里,推荐的做法是,渲染和计算分开,放到不同引擎执行。所以虽然compute shader也可以在渲染引擎执行,但因为早晚要把KlayGE重构成支持多引擎的架构,还不如现在就在D3D12插件里把它分开,让计算引擎来执行compute shader。

其实compute shader并不难,基本和第四篇里的做法一样,都是设置heap、root signature、各种资源,然后调用Dispatch。因为只有cs,pso变得很简单。

D3D12_COMPUTE_PIPELINE_STATE_DESC pso_desc;
pso_desc.pRootSignature = so->RootSignature().get();
{
	auto const & blob = so->ShaderBlob(ShaderObject::ST_ComputeShader);
	if (blob && !blob->empty())
	{
		pso_desc.CS.pShaderBytecode = blob->data();
		pso_desc.CS.BytecodeLength = static_cast<UINT>(blob->size());
	}
	else
	{
		pso_desc.CS.pShaderBytecode = nullptr;
		pso_desc.CS.BytecodeLength = 0;
	}
}
pso_desc.NodeMask = 0;
pso_desc.CachedPSO.pCachedBlob = nullptr;
pso_desc.CachedPSO.CachedBlobSizeInBytes = 0;
pso_desc.Flags = D3D12_PIPELINE_STATE_FLAG_NONE;

坑!坑!坑!

这里遇到一个问题,程序能执行,没出错,结果有的能出来有的不能。究其原因,竟然是cbuffer读到的都是0!折腾了一段时间,排除掉其他的问题,最后就剩下一个可能:CBV不能放到heap里。在D3D12中,有两种设置CBV的方法。前一篇说的放入heap中,可以减少root signature的大小(所有CBV之占一个空位),从而提高速度。另一个方法是用Set*RootConstantBufferView,直接把CBV设置到root signature上。这么一来,理论上速度不如heap,但所有的例子都是这么来处理CS的CBV。

这么一改,果然成了,这真是个大坑!换句话说,渲染流水线的CBV可以放到heap,也可以直接设置给root signature。计算流水线的CBV只能直接设置给root signature。这么修改之后,CS基本正确了。

还有一个需要注意的地方,渲染和计算属于不同的command list,两者之间没有执行顺序的保证。需要在程序里显示控制两者的切换。可以用fence来精确控制,也可以简单地强制CPU/GPU同步。

总结

迈过了CS的大坑,D3D12插件的功能更加完整。下一篇将会将如何模拟D3D11资源的不同usage和map类型。