从DX角度看SRPBatcher

Author Avatar
Kanglai Qian 5月 01, 2022

最近抽空研究了下Unity的SRPBatcher,根据官方文档说法这货能极大降低DrawCall代价,从而达到提升性能的目的,而且这个行为大多数情况下对于使用者是透明的。正好对这块比较感兴趣其实是最近在尝试优化这块,看看有什么思路可以白嫖,抓了帧稍微研究了下原理。

官方解释原理及应用

SRP Batcher: Speed up your rendering!里解释原理比较清楚: 早期Unity每次Draw之前需要大量准备工作

Unity historically was made for non-constant buffers, supporting Graphics APIs such as DirectX9. However, such nice features have some drawbacks. For example, there is a lot of work to do when a DrawCall is using a new Material. So basically, the more Materials you have in a Scene, the more CPU will be required to setup GPU data.

而使用SRP Batcher之后能尽可能的减少这些准备工作(不变的数据是不用每次从CPU刷到GPU的)

Now, low-level render loops can make material data persistent in the GPU memory. If the Material content does not change, there is no need to set up and upload the buffer to the GPU. Plus, we use a dedicated code path to quickly update Built-in engine properties in a large GPU buffer. Now the new flow chart looks like:

从SRPBatcher实现原理来说,其实就会比原来的方案多出一些限制(这个在后面抓帧的时候其实可以反向验证)

  • 不支持粒子,必须有mesh或者skinned mesh
  • 不支持MaterialPropertyBlocks
  • Shader必须保证所有的参数被抱在UnityPerDraw或者UnityPerMaterial

本地抓帧测试

官方提供了一个测试工程SRPBatcherBenchmark,就省的我自己构造测试场景了;在PC上简单测试了下,开SRPBatcher时 3.6ms左右,关SRPBatcher 17.9ms左右(仅考虑CPU Rendering time),雀实效果明显。

抓了两帧先简单对比一下: 从EID个数来看少了接近一半的DX API调用,而且比较有意思的是SRPBatcher里就算单个物体也是走的Instance Draw。

具体看下关闭和打开SRPBatcher情况下同一个物体绘制的对比(主要看左下角的API Inspector)

srpbatcher_off

srpbatcher_on

可以得到一些进一步的结论:

  • 对于不变的Texture/Sampler, 在关闭SRPBatcher下是每个Draw都暴力设置的(相当于大量的重复调用),在打开SRPBatacher时就不需要了
  • 在关闭SRPBatcher时情况下,cbuffer0/1/2每次都设置,同时cbuffer1/2每次都Map/Unmap更新数据; 在打开SRPBatcher时情况下,cbuffer0始终不变,cbuffer2一般不变,cbuffer1只变Range
    • 我后来打开shader debug信息看了下,cbuffer0对应ShaderVariablesGlobal,cbuffer1对应UnityPerDraw,cbuffer2对应UnityPerMaterial

以我之前的踩坑经验来说,优化redunant call的效果一般来说看不太出来(像XCode的Frame Capture里Insights专门会提示这些Issue,但是我之前改了一波发现FPS毫无变化orz); 但是对于buffer的优化雀实效果很好。这里其实也反过来验证了几个前面提到的点:

  • 如果Shader里的参数有不出现在UnityPerDrawUnityPerMaterial的,就没法走SRPBatcher了
  • ShaderVariablesGlobal一帧刷一次即可,UnityPerMaterial对于相同材质是完全不需要变的,UnityPerDraw其实就是一个4096大小的Transient Buffer反复使用(说起这个,我最早对这货的概念还是来自龚大的高效GPU Buffer管理之Transient Buffer)

当然文档也说了收集引擎属性的code path也做了特化提升,不过由于没有代码就不知道到底影响有多大了,略过不谈~

延展思考

重新反思下问题,降低CPU提交代价的常见套路不外乎几个:

  1. 尽量降低DrawCall数量,如LOD、Frustrum Culling、GPU Culling、PVS等手段
  2. 合并DrawCall,常见的就是Draw Instanced,但是限制点在于相同的Mesh及Material
    2.1 更进一步Indirect Draw,可以解开相同Mesh的限制,但是相同Material无法规避
  3. 如果DrawCall无法降低,那么就尽量降低其代价,这也是很多现代API演进的思路

SRPBatcher其实就是属于第三个范畴里的工作,不过说实话2019年才搞这个我是有点诧异的绝不是分析之前期待太高,以为是什么黑科技可以白嫖一波的缘故

后来和朋友闲聊到这个话题,准备有心情的时候再验证下Metal下SRPBatcher的行为。