本系列前面的五篇已经让引擎可以在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类型。
Comments