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

上一篇讲了tone mapping的改进。作为引擎的一个长期议题,优化是不可缺少的。本篇就讲讲在4.10中引入的新优化。

CPU端

在profiler里看到的占据CPU耗用第一名的一直是驱动。原先一直没在意这个,前一阵自己看了一下,发现前几位的好几个都是在SceneManager里,而且都和渲染队列相关。具体情况是,在每一帧确定渲染队列的时候,会执行一遍这样的步骤:

  1. 扫描一遍场景里的所有SceneObject,根据它的Renderable的类型建立一个从Renderable到SceneObject列表的unordered_map,每个物体作为那个Renderable的instance
  2. 把unordered_map中的Renderable建立一个队列
  3. 渲染这个队列
  4. 销毁unordered_map

所以其实这里的unordered_map只是个临时的对象,会频繁的建立和销毁。与此相关的大量内存申请、释放、排序,其实都是临时使用一下而已。而存成unordered_map的原因,也只是为了快速找到Renderable。实际上在每一个Renderable里,已经有一个vector,用来保存instance信息。所以在新版本里,我就改成了把SceneObject直接放到那个列表里。这样一来不需要建立临时的结构,而来可以直接从Renderable找到SceneObject,不需要搜索map。修改之后的步骤就变成:

  1. 扫描一遍场景里的所有SceneObject,把它放入对应的Renderable的instance中,同时记录一个Renderable的队列
  2. 渲染这个队列

没了这个临时结构,CPU部分每帧的开销降低了10%左右。

CPU和GPU之间的通信

由于每帧有大量的buffer用来从CPU到GPU传递数据,而这些buffer之前都是通过Map/Unmap来存入数据的。在D3D11中,Map/Unmap是同步的,这会造成一定的等待时间。而UpdateSubresource是异步的,等待时间大大少于Map/Unmap。改成尽可能用UpdateSubresource之后,整体性能提升了1-5%。虽然不多,但也比没有好。

以前要用Map/Unmap的原因是,不管AMD还是NV,在GDC上都推荐用Map/Unmap,说那个更快。实际测试的结果是,对于buffer来说,UpdateSubresource总是比Map/Unmap快。对于texture来说,在2015年之后的驱动上,UpdateSubresource绝大部分时候比Map/Unmap快。所以换成UpdateSubresource是有好处的。

GPU端

自从KlayGE 4.5增加了基于CS的TBDR之后,整个TBDR就是写在一个shader里。整个shader分为两个阶段。第一个是tiling,把每个tile的frustum和光源求交。如果存在相交,就把光源放到一个列表里。第二个阶段是shading,每个tile里的每个像素跟列表里的光源做shading计算。这个光源列表放在groupmemory,在同一个group里的thread可以共享。

实际上这两个阶段对于内存和thread的使用差别很大。Tiling阶段是per-tile的,shading阶段是per-pixel的。Tiling阶段主要读写groupmemory,shading阶段主要读写texture。放在一起并不是特别有利于GPU。但因为在实现TBDR的时候,其实就已经打算在某个时候切换到cluster shading(现在确定了4.11就要完成这个切换),一直没仔细优化这个地方。现在突然发现在GTX960上,CS的TBDR速度还不如PS的LIDR,而且切换到cluster shading也需要把这两个过程分开,所以在这个版本里就干脆优化一下。

结果就是把这两个阶段放到两个shader,之间通过texture做数据交换。经过一定的thread数调整之后,终于,让性能有了10%-50%的提升。还是蛮明显的,尤其是光源数量多的情况下。

D3D12

虽然去年就写好了D3D12的插件,但一直以来都只是能用,并非好用。速度只有D3D11的1%。这个版本里,profile的结果表明一大问题出在buffer的建立。之前因为偷懒,对于WRITEDISCARD类型的buffer,实现方法是每次都建立一个新的,删掉旧的。几乎所有时间都消耗在这里了。现在改成每个buffer保存一个D3D12Resource的列表,需要的时候找个空闲的使用,用完了标记成空闲,放回这个列表。这么一来,建立和删除buffer的次数远远下降了。D3D12插件的速度提升了10倍。

不过现在对于空闲buffer的检查还是每帧一次,不是每个drawcall一次。以至于buffer的数量还是不少。以后需要更精细地通过fence控制,降低内存消耗。

总结

KlayGE 4.10里还有一些改进,比如volumetric post process、blitter、asyn swap chain等,时间有限就不在这里写出来了。很快4.10就会发布,并迅速进入4.11的开发中。届时会有更多的改进会加入。