pc8888888 发表于 2022-12-18 11:17

理解unity的批处理

2021.7.15更新,看到鬼泣的分享,发现之前对SRP batcher的理解不够深入,学到了新东西,来更新一下。其实没有源码,想了解的深一点还挺难的,看来下次的工作目标应该是去个买了源码的公司~。主要参考了这里王江荣:【Unity】SRP底层渲染流程及原理,再加上之前的理解。
unity的优化中,一个很重要的优化就是批处理,但是又没有太详细的解释,只给了开关,以及文档几句简单的说明,然而使用时,却发现并不是简单的开启了选项就能得到很好的效果,所以查找了一些资料,做个总结。
为什么要用批处理

首先CPU和GPU交互,靠的是一个命令缓冲区。命令缓冲区包含了一个命令队列,由CPU向其中添加指令,而由GPU从中读取指令。那很明显可以知道,添加和读取指令都是需要时间的,如果添加和读取的速度不同,必然出现一方在等待的情况,也就是说高效交互的目标,就是双方处理速度相同,并达到一个比较大的值。
命令包含两种,一是设置渲染状态,也就是通知GPU取读取一些数据,另一个就是渲染。
GPU渲染能力很强,一个批次数据量多些GPU依然可以正常处理,但是CPU每次提交都有一定性能消耗,所以一次提交更多的数据,可以充分利用GPU的处理能力。
对显卡来说,渲染一个物体需要两个指令,首先是设置渲染状态,就是要渲染的贴图等数据,对应unity的setpass,然后是drawcall,发送指令。设置渲染状态要比处理drawcall慢的多,所以一个优化的方向是减少渲染状态的改变。
批次还有个要注意的地方是带宽的限制,比如手机上延迟渲染支持的不好的原因就是延迟渲染需要的各个缓冲区占用带宽太多。带宽能做的优化基本上是资源上的,不在本文谈论范围之内。
<hr/>unity对批次的处理方式

分为两部分,setpass和batch。在profiler查看的就是这两个值。一般来说,setpass和batch如果很接近,那就有比较大的优化空间了。
SetPassCall

[*]如果一个batch和另一个batch使用的不是同种材质或者同一个材质的不同pass,那么就要触发一次set pass call来重新设定渲染状态。
[*]复用图片以及用图集可减少。
batch

[*]作用就是把要渲染的数据提交给GPU。
[*]提交vbo,提交ibo,提交shader,设置好硬件渲染状态,设置光源等
drawcall

[*]实际unity用batch封装了一次drawcall,之前看资料说unity可以把多个drawcall合并成一个batch渲染,还疑惑了一下,unity是怎么做到的,后来想想,batch就是各种合批方法执行后的结果,最终还是调用图形接口的drawcall方法渲染的。
除了setpass和batch,另一个重要的概念是buffer,GPU instancing和SRP Batcher都是针对buffer提升的性能。
<hr/>SRP batcher

只有在SRP管线支持的一种批处理方法,针对的是setpass的消耗,而不减少drawcall。
这个的优先级没查到明确的说明,在工程中,开启静态和动态批处理后,能看到SRP batcher,而静态的看不到了,看这效果应该是最高优先级。
最核心的部分是把batch里面每个drawcall里小的CBUFFER组织成一个大的CBUFFER,然后统一上传到GPU。组合的前提是drawcall使用的shader相同,不支持不同变体。
        这样做的好处是一个设置渲染状态的指令,发送了更多数据,而且这些数据会在GPU有缓存,不改变时可以复用,不用每帧都发。
        unity底层降低了setpass的要求,只要shader的feature一样,贴图不同也不影响。
        SRP batcher后的drawcall,就变成了先判断cbuffer有没有改变,如果有改变,重新填充数据,然后调用多次drawcall,依次渲染使用这个shader的各种物体。
        要注意的是,CBUFFER保存的只是数据,因为在API底层,比如vulkan,支持的资源是缓冲区和贴图两种,unity也不能把贴图放到CBUFFER里。
内部实现方式是PerObjectBuffer
        当一个shader确定的时候,这个shader使用了哪些feature就已经确定了,没有使用的feature不会填充PerObjectBuffer,也就是shader的feature越少,一次能合批的数量就会越多。
        填充完PerObjectBuffer后,就会把他们组成一个大的CBUFFER(PerObjectLargeBuffer),然后统一传到GPU做渲染。
提速效果源于二个方面:一是各种属性值都会一直保留在GPU内存中,省去了上传和读取的消耗。二是专用代码会管理大型“per object” GPU CBUFFER,不过怎样管理的并没有详细说明。
在Frame Debug可以看到一个batcher处理了多少drawcall,数量越多越高效。
就像缓存都会遇到的那个问题一样,内存是有限的,如果大量使用,也可能显存不足,所以还是要有些选择,比如大量相似的小物体还是可以用GPU instance。
关于打断


条件大多是关于shader的,shader变体会导致重新设置渲染状态,所以官方有个建议是少用shader变体,而在CBuffer增加更多属性,增加属性并没有多少性能消耗。
还有一个要注意的是MaterialBufferOverride,应该是超过了GPU可读取的贴图上限,这是硬件限制的。一般一个shader也不用采样太多贴图,消耗也比较大,可以合并一些通道。
<hr/>静态批处理

这个使用起来很简单,完全交给美术做场景时选择,效果也很好,就是内存消耗比较大,还有个缺点是加载场景会变慢,因为在合mesh。
原理是将静态物体集合成一个大的vbo提交(不考虑material是否相同),但只对要渲染的物体提交其ibo。代价是顶点数据结构按最复杂的存,组合这个vbo对CPU和显存有额外开销。
针每个物体可以被单独剔除,通过设置ibo,也就是实际渲染时只会渲染可见物体,但是vbo是一直在内存中的,这也有个好处,就是打断drawcall的物体,也不需要上传vbo,少了上传消耗。
<hr/>GPU Instancing

优先级比静态低,比动态高。对于一些大型多人的游戏,或是slg这种,还是种很实用的技术。
原理理解

[*]针对的是mesh
[*]主要想法是GPU在一次遍历的时候会被告知多次来渲染相同的网格,所以网格只上传一份,每个物体引用这个网格,设置自己的矩阵,不限于小网格,不能合并不同的网格。
[*]只提交一个mesh,但是将多个使用同种mesh和material的物体的差异化信息组合成一个pio(per instanced attribute)提交。
[*]差异化信息保存位置、缩放、旋转、shader上的参数,不包括纹理
[*]在GPU,通过读取每个物体的pio数据,对同一个mesh进行各种变换后绘制
主要限制是缓冲区,缓冲区大小决定了一个渲染批次中可以容纳多少个实例。
<hr/>动态批处理

比静态批处理省内存,同时有一定的计算消耗。同时限制比较多,具体可以查看FrameDebug,针对问题进行优化。
原理是将物体动态组装成一个个稍大的vbo+ibo提交,mesh不必相同,material必须相同。
<hr/>简单总结一下

SRP batcher可能是最高效的,但是要占显存。静态批处理同样要占显存,不过不知道内存中合并后的mesh和原mesh会不会在什么时机释放。GPU instancing技术是针对网格处理的,结合GPU skin可以实现大量模型的高效渲染。动态批处理比较方便,但是限制也很多。
实际项目往往会根据需要综合使用,而不是只用一个,每个技术都有适合的地方。
最后还有个疑问,profiler里能看到drawcall和batch值,并且还不完全相同,unity应该都合成batch渲染的,这里的drawcall是啥意思,谁给答个疑~~

mastertravels77 发表于 2022-12-18 11:25

写得很好呀 。学到了

DomDomm 发表于 2022-12-18 11:28

drawcall 顾名思义就是渲染DrawIndex的API调用,一个批次可能有好几个drawcall的调用

zt3ff3n 发表于 2022-12-18 11:32

静态合批应该需求Material相同

BlaXuan 发表于 2022-12-18 11:33

大佬,请问kSRPBatch的那张图是在哪里截的呀
页: [1]
查看完整版本: 理解unity的批处理