优化Unity渲染器

来源:互联网 发布:耽美网络剧美拍 编辑:程序博客网 时间:2024/04/30 12:49


我们成立了一个“突击小组”来优化unity的渲染在CPU方面的性能。我将用博客记录我进行的那部分(这种做法似乎被大多数人接受)。我不知道是从哪里兴起这样的,但这确实很有趣。

背景/忠告

在很多情况下,我要很严厉地说“这段代码很烂!”。当试图改进代码时,你显然会去改进那些不好的,这通常是被关注的焦点。并不是说代码库通常是不好的,或者说它不能做出好的东西。就在今年三月,我们的游戏 Pillarsof Eternity, Oriand the Blind Forest 以及Cities:Skylines 都栖身PC最受好评游戏榜。它们都是用Unity做的。这对“此款移动引擎只适合原型设计”来说,并不太糟糕。嗯!

事实是,在某种意义上,任何代码库经历了很长一段时间的成长,被很多人修改过,都很容易变“坏”。有些部分的代码很奇怪;有些地方没有人记得它是如何工作或者为什么要这么做;有些决策是很多年前做的,现在已经不太有意义了。没人有空去修复它们。在一个很大的代码库中,没有哪个人能知道关于它应该如何工作的全部细节,所以在某些微妙的地方,一些决策会和另一些相冲突。套用某人的一句话,“有的代码库很糟糕,有的代码库很没用”。

但是重要的是要努力改善代码库!不停的改善。在所有方面,我们都做了大量改善,但是坦白讲,在过去几年内渲染代码的改善带来了很大提升。没有人抱怨研究全部的代码很艰难,只是将改善它作为全职工作。这就是那时我们所做的!

好几次,我指着一段代码说“哈哈,这真是蠢”。而这正是我最初写的代码。没关系。也许我现在知道的更多了;或者这些代码只在那个时候合理;或者考虑了各种因素后这些代码就合理了(没有时间,等)。也许我当时就是愚蠢。说不定五年后我看现在代码的也会这样说。


总之…


愿望清单

下图就是我们想要达到的目标。一个高吞吐量的渲染系统,运行没有瓶颈。
0.gif 


现在(unity5.0)的渲染、着色器运行时(shader runtime)以及图形API的CPU代码不是很高效。它存在几个问题,我希望讲的尽可能详尽:

图形加速(Gfx)设备(渲染API的抽象):
  • 抽象主要根据DX9/部分DX10的概念设计。例如,常量/统一缓冲区,如今已经不适合了。
  • 这些年来混乱不断增长,有些地方需要清理。
  • 允许通过现代API(如控制台、DX12、Metal或Vulkan)创建并行的命令缓冲区。


渲染循环:
  • 很多小地方存在低效冗余设计;需要优化。
  • 循环中并行和/或jobify的运行部分。尽可能使用原生命令缓冲区创建的API。
  • 使代码更简洁,更统一;提取公共功能。提高代码可测试性。


着色器/材质运行时:
  • 数据在内存中的布局,呃…,“不太好”。
  • 令人费解的代码;需要清理。使它更可具有可测试性。
  • 在运行时中,不应该存在“固定功能着色器”概念。参见[Generating Fixed Function Shaders at Import Time]
  • 基于文本格式的着色器很愚蠢。参见 [Binary Shader     Serialization]



限制


不论我们做什么优化/清理代码,都应该尽可能保证功能可以继续工作。一些很少用到的功能或极端情况可能会被改变或废弃,但这只作为最后的手段。

还有一点需要考虑,如果有些代码看起很复杂,它可能是有原因的。其中一个是“有人写的代码太复杂”(好!简化他)。另一个可能是“有些历史原因导致它很复杂,但现在已经不同了”(好!简化它)。

但也可能代码本身就是在做复杂的事,例如,它需要处理一些棘手的问题。也许它可以被简化,也许不可以。“重新开始写”是一个诱人的开始,但是有些情况下,你新写的好代码可能变得和原来那份一样复杂,你让它做了旧代码做的所有事。

计划

一段CPU代码,有两个方向来优化性能:1)“只让它更快”。2)让它更并行。

我觉得在初期更愿意关注于“只让它更快”。部分原因是我也想简化代码,并记下各种必须做的棘手之事。简化数据,使数据流更清晰,使代码更简单,这样也会使第二步(使它更并行)更简单。

首先关注的是更高级的渲染逻辑(“渲染循环”)以及着色器/材质运行时。团队中的其他成员则关注简化、清理渲染API抽象,并尝试“使它更并行”的方法。

为了测试渲染性能,我们需要一些实际的内容来测试。我挑选了几个游戏和示例工程,使它们尽量只受CPU的限制(通过,减少GPU加载——在低分辨率下渲染;降低多边形数量这在渲染中占很大比重;降低阴影贴图的分辨率;减少或去除后期处理;降低纹理分辨率)。为了增加CPU的负载,我复制了场景中的某些部分,使场景要渲染的物体更多。

引用:
“嗨,我有100000个立方体”,这种情况下很容易测试渲染的性能。但是实际中我们并不会这样做。“只有大量使用完全相同材质的物体”和“上千个参数不同的材质、上百个不同的着色器、几十个不同的渲染目标、阴影贴图和常规渲染、alpha混合对象、动态生成几何体等”,这两种情况在渲染场景时差别很大。
另一方面,测试“完整的游戏”可能太繁琐,尤其如果它需要和其他模块交互、全部加载很耗时或者不是开始提到的那种受CPU限制的情况。



测试CPU性能时,在多个设备上测试更有帮助。我通常Windows的开发电脑上(目前是酷睿i7 5820K),Mac的笔记本上(2013 rMBP)和我所有的iOS设备(现在是iPhone6)上测试。控制台测试会很好,我一直听说它们有很棒的性能分析工具,或多或少的固定时钟和相对较弱的CPU——但是我身边没有devkits。也许我该去弄一个。

注意事项

接着,我运行了一个基准工程,并查看了性能分析数据(Unity系能分析器和第三方性能分析器,如Sleepy/Instruments),同时查看了代码所做的事。当我发现有些事很奇怪的时候,将它记录下来作为后续的研究内容:
1.png 
引用:
SetPassWithShader有时是避免PPtr deref所做的优化。现在看来它总是执行PPtr deref,然后调用SetPass(它会再执行一次deref)。
材质显示列表经常被>1的逐像素光源重建。每个材质需要缓存不止一个列表(周期性的Kaspar的5bce615bca6b)
GetTextureDecodeValues被调用了多次(设置像素光属性),最后做了一些无用的对“1”的伽马线性转换。
未赋值的全局纹理属性(_Cube,没有设置任何物体)导致的蝴蝶效应,经常使材质显示列表被重建。应该弄清为什么在任何全局属性丢失的时候,我们都没有记录。
GpuProgramParameters::MakeReady实际做了什么?它为什么要排序?
        为什么在属性表中使用了STL容器map?
                 仅使用健壮的简单的数据层。
                 相关的奇怪问题
                         为什么从PropertySheet中分离出TexEnv(但是两者还被关联在一起)
                         SetRectTextureID——为什么,是什么
                         纹理的纹理尺寸/高动态光照渲染(TexelSize/HDR )解码了内置设备状态的属性部分。
                         NotifyMipBiasChanged 听起来像是大量复杂操作,且目的不明。
IsPassSuitable 被渲染循环重复调用。可能是为每个渲染循环建立了一个直接pass指针的表?
一次性应用所有纹理,而不是通过SetTexture一次一个的设置。
在内存中重排属性表,使其和实际常量缓存布局一致。对不同的关键字变量可能需要不同的布局。
TextureID转换为长整型(在mac/linux 64位机上占64bit!),来自针对PS4平台提交的3cbd28d4d6cd
                看起来这是一个仅针对PS4的优化,它直接在TextureID中存储了一个指针。考虑如果可能的话再在各处执行该操作,或者只在PS4上将它转为长整型(intptr_t 更好)
为什么ChannelAssigns总是被传递?这似乎没什么用。
ChannelAssigns和VertexComponent尤其诡异(太多可能的VertexComponent实体)。
渲染循环排序成本太高。使用基于哈希表的排序(渲染循环排序)并启用它!


上述有些问题可能有合理的原因,这种情况下我会继续下去并添加代码注释来解释它们。有些可能曾经有原因,但现在不是了。上述两种情况中,版本控制的日志/注释功能会很有用,然后去问问这些代码的作者为什么会这样写。上面列表中的有一半的情况很可能是我好几年前是这样写的,这意味着我必须记住这些原因,甚至是那些“当时看起来很不错的主意”。


以上就是介绍,下次,我会从上面的“WAT”列表中挑选一些,并对它们做些什么。

扫描下方二维码关注游戏蛮牛官方微信~每日都有精选干货与你分享呦~

原文链接:http://aras-p.info/blog/2015/04/01/optimizing-unity-renderer-1-intro/
原文作者:Aras Pranckevičius

我们成立了一个“突击小组”来优化unity的渲染在CPU方面的性能。我将用博客记录我进行的那部分(这种做法似乎被大多数人接受)。我不知道是从哪里兴起这样的,但这确实很有趣。

背景/忠告

在很多情况下,我要很严厉地说“这段代码很烂!”。当试图改进代码时,你显然会去改进那些不好的,这通常是被关注的焦点。并不是说代码库通常是不好的,或者说它不能做出好的东西。就在今年三月,我们的游戏 Pillarsof Eternity, Oriand the Blind Forest 以及Cities:Skylines 都栖身PC最受好评游戏榜。它们都是用Unity做的。这对“此款移动引擎只适合原型设计”来说,并不太糟糕。嗯!

事实是,在某种意义上,任何代码库经历了很长一段时间的成长,被很多人修改过,都很容易变“坏”。有些部分的代码很奇怪;有些地方没有人记得它是如何工作或者为什么要这么做;有些决策是很多年前做的,现在已经不太有意义了。没人有空去修复它们。在一个很大的代码库中,没有哪个人能知道关于它应该如何工作的全部细节,所以在某些微妙的地方,一些决策会和另一些相冲突。套用某人的一句话,“有的代码库很糟糕,有的代码库很没用”。

但是重要的是要努力改善代码库!不停的改善。在所有方面,我们都做了大量改善,但是坦白讲,在过去几年内渲染代码的改善带来了很大提升。没有人抱怨研究全部的代码很艰难,只是将改善它作为全职工作。这就是那时我们所做的!

好几次,我指着一段代码说“哈哈,这真是蠢”。而这正是我最初写的代码。没关系。也许我现在知道的更多了;或者这些代码只在那个时候合理;或者考虑了各种因素后这些代码就合理了(没有时间,等)。也许我当时就是愚蠢。说不定五年后我看现在代码的也会这样说。


总之…


愿望清单

下图就是我们想要达到的目标。一个高吞吐量的渲染系统,运行没有瓶颈。
0.gif 


现在(unity5.0)的渲染、着色器运行时(shader runtime)以及图形API的CPU代码不是很高效。它存在几个问题,我希望讲的尽可能详尽:

图形加速(Gfx)设备(渲染API的抽象):
  • 抽象主要根据DX9/部分DX10的概念设计。例如,常量/统一缓冲区,如今已经不适合了。
  • 这些年来混乱不断增长,有些地方需要清理。
  • 允许通过现代API(如控制台、DX12、Metal或Vulkan)创建并行的命令缓冲区。


渲染循环:
  • 很多小地方存在低效冗余设计;需要优化。
  • 循环中并行和/或jobify的运行部分。尽可能使用原生命令缓冲区创建的API。
  • 使代码更简洁,更统一;提取公共功能。提高代码可测试性。


着色器/材质运行时:
  • 数据在内存中的布局,呃…,“不太好”。
  • 令人费解的代码;需要清理。使它更可具有可测试性。
  • 在运行时中,不应该存在“固定功能着色器”概念。参见[Generating Fixed Function Shaders at Import Time]
  • 基于文本格式的着色器很愚蠢。参见 [Binary Shader     Serialization]



限制


不论我们做什么优化/清理代码,都应该尽可能保证功能可以继续工作。一些很少用到的功能或极端情况可能会被改变或废弃,但这只作为最后的手段。

还有一点需要考虑,如果有些代码看起很复杂,它可能是有原因的。其中一个是“有人写的代码太复杂”(好!简化他)。另一个可能是“有些历史原因导致它很复杂,但现在已经不同了”(好!简化它)。

但也可能代码本身就是在做复杂的事,例如,它需要处理一些棘手的问题。也许它可以被简化,也许不可以。“重新开始写”是一个诱人的开始,但是有些情况下,你新写的好代码可能变得和原来那份一样复杂,你让它做了旧代码做的所有事。

计划

一段CPU代码,有两个方向来优化性能:1)“只让它更快”。2)让它更并行。

我觉得在初期更愿意关注于“只让它更快”。部分原因是我也想简化代码,并记下各种必须做的棘手之事。简化数据,使数据流更清晰,使代码更简单,这样也会使第二步(使它更并行)更简单。

首先关注的是更高级的渲染逻辑(“渲染循环”)以及着色器/材质运行时。团队中的其他成员则关注简化、清理渲染API抽象,并尝试“使它更并行”的方法。

为了测试渲染性能,我们需要一些实际的内容来测试。我挑选了几个游戏和示例工程,使它们尽量只受CPU的限制(通过,减少GPU加载——在低分辨率下渲染;降低多边形数量这在渲染中占很大比重;降低阴影贴图的分辨率;减少或去除后期处理;降低纹理分辨率)。为了增加CPU的负载,我复制了场景中的某些部分,使场景要渲染的物体更多。

引用:
“嗨,我有100000个立方体”,这种情况下很容易测试渲染的性能。但是实际中我们并不会这样做。“只有大量使用完全相同材质的物体”和“上千个参数不同的材质、上百个不同的着色器、几十个不同的渲染目标、阴影贴图和常规渲染、alpha混合对象、动态生成几何体等”,这两种情况在渲染场景时差别很大。
另一方面,测试“完整的游戏”可能太繁琐,尤其如果它需要和其他模块交互、全部加载很耗时或者不是开始提到的那种受CPU限制的情况。



测试CPU性能时,在多个设备上测试更有帮助。我通常Windows的开发电脑上(目前是酷睿i7 5820K),Mac的笔记本上(2013 rMBP)和我所有的iOS设备(现在是iPhone6)上测试。控制台测试会很好,我一直听说它们有很棒的性能分析工具,或多或少的固定时钟和相对较弱的CPU——但是我身边没有devkits。也许我该去弄一个。

注意事项

接着,我运行了一个基准工程,并查看了性能分析数据(Unity系能分析器和第三方性能分析器,如Sleepy/Instruments),同时查看了代码所做的事。当我发现有些事很奇怪的时候,将它记录下来作为后续的研究内容:
1.png 
引用:
SetPassWithShader有时是避免PPtr deref所做的优化。现在看来它总是执行PPtr deref,然后调用SetPass(它会再执行一次deref)。
材质显示列表经常被>1的逐像素光源重建。每个材质需要缓存不止一个列表(周期性的Kaspar的5bce615bca6b)
GetTextureDecodeValues被调用了多次(设置像素光属性),最后做了一些无用的对“1”的伽马线性转换。
未赋值的全局纹理属性(_Cube,没有设置任何物体)导致的蝴蝶效应,经常使材质显示列表被重建。应该弄清为什么在任何全局属性丢失的时候,我们都没有记录。
GpuProgramParameters::MakeReady实际做了什么?它为什么要排序?
        为什么在属性表中使用了STL容器map?
                 仅使用健壮的简单的数据层。
                 相关的奇怪问题
                         为什么从PropertySheet中分离出TexEnv(但是两者还被关联在一起)
                         SetRectTextureID——为什么,是什么
                         纹理的纹理尺寸/高动态光照渲染(TexelSize/HDR )解码了内置设备状态的属性部分。
                         NotifyMipBiasChanged 听起来像是大量复杂操作,且目的不明。
IsPassSuitable 被渲染循环重复调用。可能是为每个渲染循环建立了一个直接pass指针的表?
一次性应用所有纹理,而不是通过SetTexture一次一个的设置。
在内存中重排属性表,使其和实际常量缓存布局一致。对不同的关键字变量可能需要不同的布局。
TextureID转换为长整型(在mac/linux 64位机上占64bit!),来自针对PS4平台提交的3cbd28d4d6cd
                看起来这是一个仅针对PS4的优化,它直接在TextureID中存储了一个指针。考虑如果可能的话再在各处执行该操作,或者只在PS4上将它转为长整型(intptr_t 更好)
为什么ChannelAssigns总是被传递?这似乎没什么用。
ChannelAssigns和VertexComponent尤其诡异(太多可能的VertexComponent实体)。
渲染循环排序成本太高。使用基于哈希表的排序(渲染循环排序)并启用它!


上述有些问题可能有合理的原因,这种情况下我会继续下去并添加代码注释来解释它们。有些可能曾经有原因,但现在不是了。上述两种情况中,版本控制的日志/注释功能会很有用,然后去问问这些代码的作者为什么会这样写。上面列表中的有一半的情况很可能是我好几年前是这样写的,这意味着我必须记住这些原因,甚至是那些“当时看起来很不错的主意”。


以上就是介绍,下次,我会从上面的“WAT”列表中挑选一些,并对它们做些什么。

扫描下方二维码关注游戏蛮牛官方微信~每日都有精选干货与你分享呦~

原文链接:http://aras-p.info/blog/2015/04/01/optimizing-unity-renderer-1-intro/
原文作者:Aras Pranckevičius

0 0