推荐去个人网站或者原文观阅文章,因为文章中有嵌入式视频:

https://www.lfzxb.top/srp-batcher-speed-up-your-rendering/

本文翻译自:SRP Batcher: Speed up your rendering!https://docs.unity3d.com/Manual/SRPBatcher.html

在2018,我们介绍了一个被称为SRP的高度自定义的渲染技术,一个被称为SRP Batcher新的引擎底层Loop就是这个技术的一部分,SRP Batcher可以在渲染时加速你的CPU 1.2到4倍(这取决于你的场景)。让我们看看如何这个功能的最佳实践吧!

上面那个视频显示了Unity渲染时最坏的一个场景:每个对象都是动态的,并且使用不同的材质(材质的颜色或者贴图等属性不同),所以尽管非常多相同的Mesh,但是在渲染时每个Mesh都会被当成不同的Mesh进行渲染(所以无法使用GPU Instance优化技术)。这个场景使用SRP Batcher在PS4平台上可以加速4倍(这个视频是PC的,渲染API是DX11)

注意:当我们谈及4倍的加速时,我们说的是CPU端的渲染相关代码(例如在Profiler中显示的“RenderLoop.Draw”h和“ShadowLoop.Draw”标签),而不是总的渲染帧率。

Unity和Materials

Unity是一个非常灵活的渲染引擎,你可以在一帧的的任何时间点修改任何Material属性。此外,Unity历史版本的渲染管线并不是面向Constant Buffer进行开发的,这是为了支持DX9这样的图形API。但是,这样漂亮的功能有一些弊端,比如当一个DrawCall要求使用一个新的Material的时候,需要有大量的工作去处理,所以,你在一个场景中的Material越多,为了设置GPU端的数据而占用的CPU资源就越多。解决此问题的传统方法是减少DrawCall的数量以优化CPU渲染消耗,因为Unity在发出DrawCall之前必须进行很多设置。而且实际的CPU消耗就来自该设置,而不是来自GPU DrawCall本身,而这只是Unity需要将其推送到GPU命令缓冲区的一些字节而已。

image-20210314185722632

在渲染的循环中,当一个新的Material被发现了,CPU会收集所有的属性并且在GPU的内存中设置不同的Constant Buffers。GPU缓冲区的数量就取决于Shader是如何声明其CBUFFERs的。

译者PS:上文提到的Constant Buffer对应于GPU上的一种常量内存缓存,速度仅次于流处理器族中的共享内存和寄存器,下图来自《全局光照技术》(手机拍照不好看,就pdf截图了)

image-20210314191119196

image-20210314191131547

SPR是如何工作的

当我们开发SRP的时候,我们不得不重写一些引擎的底层部分。这同样是一个好机会,我们可以原生地集成一些新的规范,例如GPU数据持久化。我们旨在加快场景使用大量不同材质(但Shader变体很少)的这种常见情况。

现在,底层的渲染Loop可以把material的数据持久化在GPU的内存。如果material的内容没有改变,我们就不需要设置,上传buffer到GPU。此外,我们使用特定的代码路径来在一个大的GPU buffer中快速的更新引擎的内置属性。现在这个新的流程图就像这样:

image-20210314191849638

这样,CPU就只需要处理引擎内置的属性(例如标记为对象的变换的属性)。所有的Materials都在GPU内存上有一块持久化的CBUFFERs。综上所述,SRP Batcher加速来自于两个不同的方面:

  • 每个材质的内容持久化在GPU的memory上
  • 一个专用的代码路径来管理一个大的“per object”的GPU CBUFFER

如何启用SRP Batcher

你的工程必须使用URP,HDRP或者你的自定义SRP。然后就可以在SRP的Asset Inspector面板上进行激活

image-20210314192555568

如果你想要在运行时使用代码来开关SRP Batcher:

1
GraphicsSettings.useScriptableRenderPipelineBatching = true;

SRP Batcher 兼容性

对于一个通过SRP Batcher代码路径渲染的对象,有三个要求

  1. 对象必须是Mesh或者Skinned Mesh,不能是粒子特效
  2. Shader必须与SRP Batcher兼容,所有的在URP或者HDRP中的Lit和Unlit Shader都满足这个需求(除了这些shader的粒子特效版本)
  3. 渲染的对象不使用MaterialPropertyBlocks

对于一个与SRP Batcher兼容的Shader来说:

  1. 必须声明所有引擎内置的属性(例如unity_ObjectToWorld,unity_SHAr等)在一个名称为“UnityPerDraw”的CBUFFER中。
  2. 必须声明所有的Material属性在一个名为“UnityPerMaterial”的CBUFFER中

可以在一个Shader的Inspector面板查看其是否兼容SRP Batcher

image-20210314193932708

在一个给定的Scene中,一些对象时SRP Batcher兼容的,而另一些不是,这个场景仍然可以正常渲染。兼容SRP Batcher的对象使用SRP Batcher的代码路径,另一些将会使用标准的SRP渲染代码路径

image-20210314194218239

性能监测的艺术

SRPBatcherProfiler.cs

如果你想在特定场景测试SRP Batcher带来的性能提升,可以使用SRPBatcherProfiler.cs这一C#脚本(这是链接)。

只需要把这个脚本加入你的场景,当游戏运行时,你就能看到一个UI面板,上面记录了一些有用的信息。你还可以使用F9来开关SRP Batcher

image-20210314194548062

在这里,所有时间均以毫秒(ms)为单位。这些时间测量结果显示了在Unity SRP渲染循环中CPU消耗。

注意:计时表示在一帧期间调用的所有“RenderLoop.Draw”和“Shadows.Draw”标记的累计时间(所有线程)。例如“1.31ms SRP Batcher code path”,也许只有0.31ms花费在主线程,1ms的是所有的Graphic JobSystem的消耗。

名称 描述
(SRP batcher ON) / (SRP batcher OFF) 表示是否开启SRP Batcher,按F9开关
CPU Rendering time 表示SRP循环在CPU中花费的总的时间,无论使用哪种多线程模式,例如单线程,client/worker或Graphic Job。在这里,您可以最大程度地看到SRP Batcher的效果。要查看批处理程序的优化,请尝试将其打开和关闭以查看CPU耗时的差异。此示例总计为2.11ms。**(incl RT idle):**表示SRP在渲染线程空闲状态耗时。这可能意味着该应用程序处于client/worker模式,没有任何图形工作,这是在渲染线程必须等待主线程上的渲染命令时发生的。在此示例中,渲染线程空闲了0.36毫秒。
SRP Batcher code path (flushes) 表示您的游戏或应用程序花费在SRP Batcher代码路径中的时间。这可分为游戏渲染所有对象(shadow passes除外)所花费的时间(1.18ms),以及Shadows(1.13ms)。如果Shadows耗时很高,请尝试减少“场景”中的阴影投射灯数量,或者在“Render Pipeline Asset”中选择较低的Shadow Cascades数量。在这个例子中,它是1.31ms。**(flush(s))**数量表示Unity刷新场景的次数,因为它遇到了新的Shader Variant(在此示例中:89)。较少的flush总是更好的,因为这意味着一个渲染帧中的Shader Variants数量较少。
Standard code path (flushes) 表示Unity花费在渲染与SRP Batcher不兼容的对象(例如粒子)上的时间。在此示例中,SRP Batcher在0.80ms内flush了81个对象:shadow passes为0.09毫秒,所有其他pass为0.71毫秒。
Global Main Loop: (FPS) 表示全局主循环时间(以毫秒为单位),可以得知每秒帧数(FPS)。注意:“FPS”不是线性的,因此,如果您看到FPS增加20,这并不一定意味着您已经优化了场景。要查看SRP Batcher是否优化了场景渲染,请将其切换为“开”和“关”,然后在CPU Rendering time下比较数字。

示例:(译者PS:对于flushes数据变动,个人认为是在SRP Batcher关闭的时候,遇到一个不同材质的就会flush一次,而开启SRP Batcher,flushes值就取决于Shader变体了)

image2

支持的平台

SRP几乎支持所有的平台,下面是一张表,包含了最低Unity版本信息

Platform Minimum Unity version required
Windows DirectX 11 2018.2
PlayStation 4 2018.2
Vulkan 2018.3
OSX Metal 2018.3
iOS Metal 2018.3
Nintendo Switch 2018.3
Xbox One DirectX 11 2019.2
OpenGL 4.2 and higher 2019.1
OpenGL ES 3.1 and higher 2019.1
Xbox One DirectX 12 2019.1
Windows DirectX 12 2019.1

VR那点事

SRP Batcher支持VR,但仅限于“SinglePassInstanced”模式,开启VR模式并不会增加任何CPU消耗

常见问题

我怎么知道我当前用法是否是SRP Batcher的最佳实践呢?

使用SRPBatcherProfiler.cs,首先检查SRP Batcher是否开启,然后查看“Standard code path”消耗,应该尽可能接近0。当然了,如果你的场景使用了一些skinned meshes或者粒子特效,“Standard code path”会不可避免的有一些消耗。

无论是否开启SRP Batcher,SRPBatcherProfiler耗时显示都差不多是为啥?

首先,您应该检查几乎所有渲染耗时都使用了SRP Batcher的代码路径(请参见上文)。如果是这样,数字仍然相似,则查看“flush”数字。当SRP Batcher打开时,“flush”数量应减少很多。根据经验,减少10倍非常好了,减少2倍也可以接受。如果flush计数没有减少很多,则意味着您仍然有很多Shader变体。尝试减少Shader变体的数量。如果您做了很多不同的着色器,请尝试使用更多参数制作一个“Uber”着色器。这样就无需拥有大量不同的Material参数。

当我开启SRP Batcher的时候并没有显著变化是为啥?

先看下上面的两个问题。如果SRPBatcherProfiler显示“CPU Rendering time”快两倍,并且FPS不变,则CPU渲染部分不是您的瓶颈。这并不意味着您不是CPU bound,而是因为您可能使用了太多的C# Gameplay或太多的物理元素。无论如何,如果“CPU Rendering time”快两倍,那还是不错的。您可能已经在顶部视频中注意到,即使以3.5倍的速度进行加速,场景仍然保持60FPS的速度。那是因为我们已将VSYNC打开。SRP Batcher确实在CPU端节省了6.8ms。这些毫秒可以用于其他任务。它还可以节省移动设备的电池寿命。

如何高效的检查SRP Batcher

理解“batcher”在SRP Batcher上下文中含义是很重要的。

传统做法,人们倾向于减少DrawCall的数量以优化CPU渲染消耗。这样做的真正原因是引擎在发出DrawCall之前必须进行很多设置。而且实际的CPU消耗来自该设置,而不是来自GPU DrawCall本身(这只是要放入GPU命令缓冲区的一些字节)。SRP Batcher不会减少DrawCalls的数量。它只是减少了DrawCall之间的GPU设置成本。

您可以在以下工作流程中看到这一点:

image-20210314203746906

左侧是标准SRP渲染管线循环。右边是SRP Batcher循环。在SRP Batcher程序上下文中,“batch”只是“Bind”,“Draw”,“Bind”,“Draw”…这样的一个GPU命令的序列。

在标准SRP中,对于每种新material都调用慢的一批的SetShaderPass。在SRP Batcher上下文中,将为每个新的Shader变体调用SetShaderPass。

为了获得最佳性能,您需要将这些batches保持尽可能大。因此,您需要避免任何Shader变体更改,但是如果它们使用相同的Shader,则可以使用任意数量的不同materials。

您可以使用Unity Frame Debugger查看SRP Batcher的“batches”长度。每个批次都是帧调试器中称为“ SRP Batch”的事件,如您在此处看到的:

image-20210314205007279

查看左侧的SRP Batch事件。另外查看batch的大小,即Drawcall的数量(此处为109)。这是一个非常有效的batch。您还将看到上一个batch被破坏的原因(“Node use different shader keywords”)。这意味着用于batch的Shader关键字不同于先前batch中的关键字。这意味着shader变体已更改,我们必须中断该batch。

在某些场景中,某些batch大小可能会非常低,例如以下示例:

image-20210314205338410

batch大小仅为2。这可能意味着您有太多不同的shader变体。如果要创建自己的SRP,请尝试使用最少的关键字编写通用的“Uber Shader”。您不必担心在“property”部分中输入了多少material参数。

注意:Frame Debugger中的SRP Batcher信息需要Unity 2018.3或更高版本。

编写你自己的SRP Batcher兼容的Shader

注意:此部分是专门给高级用户看的,他们会编写他们自己的SRP和Shader库。如果使用我们提供的管线(URP,HDRP)的用户可以跳过,我们提供的Shader是SRP Batcher兼容的。

如果你正在编写自己的渲染循环,你的shader需要遵循以下几条规则来受益于SRP Batcher

“Per Material”变量

首先,所有的“per material”数据都需要声明在一个名为“UnityPerMaterial”的CBUFFER中。什么是“per material”数据呢?其实就是你定义在“shader property”中所有的变量。这些变量可以显示在material的Inspector面板上。举个例子:

1
2
3
4
5
6
7
8
Properties
{
_Color1 ("Color 1", Color) = (1,1,1,1)
_Color2 ("Color 2", Color) = (1,1,1,1)
}

float4 _Color1;
float4 _Color2;

如果你编译这个Shader,这个Shader的Inspector面板将会这样显示:

image-20210314210139097

为了修复这个问题,仅仅需要这样声明即可

1
2
3
4
5
6
CBUFFER_START(UnityPerMaterial)

float4 _Color1;
float4 _Color2;

CBUFFER_END

“Per Object”变量

SRP Batcher同样需要一个名为“UnityPerDraw”的CBUFFER。这个CBUFFER应该包含所有的Unity引擎 built-In的变量

变量声明顺序同 “UnityPerDraw” CBUFFER一样重要。所有变量都应遵循我们称为“Block Feature”的布局。例如,“Space Position block feature”应按以下顺序包含所有这些变量:

1
2
3
4
5
6
7
8
CBUFFER_START(UnityPerDraw)

float4x4 unity_ObjectToWorld;
float4x4 unity_WorldToObject;
float4 unity_LODFade;
float4 unity_WorldTransformParams;

CBUFFER_END

如果不需要它们,则不必声明其中的某些block features。“UnityPerDraw”中的所有引擎内置变量应为float4或float4x4。在移动设备上,人们可能想使用real4(16位编码的浮点值)来节省一些GPU带宽。并非所有UnityPerDraw变量都可以使用“real4”。请参考“Could be real4”一栏。

下表描述了可以在“UnityPerDraw” CBUFFER中使用的所有可能的block features:

Screen-Shot-2019-02-27-at-3.50.52-PM

注意:如果将一个feature block的变量之一声明为real4(half),则该feature block的所有其他潜在变量也应声明为real4。

提示1:始终在Inpsector中检查新shader的兼容性状态。我们检查了几个潜在的错误(UnityPerDraw布局声明等),并显示为什么不兼容。

提示2:在编写自己的SRP Shader时,可以参考LWRP或HDRP包以查看其UnityPerDraw CBUFFER声明以获取灵感。

参考

https://blogs.unity3d.com/2019/02/28/srp-batcher-speed-up-your-rendering/

https://docs.unity3d.com/Manual/SRPBatcher.html