上一篇中我们讨论了GearVR设备的特点还有创建高效的GearVR游戏的方法。这一篇,我将聚焦于调试在这些设备上性能不够好的Unity程序的方法。
性能调试
即使你的游戏场景经过良好的设计,并且设置了合理的节流值,你也会发现你的游戏在GearVR设备上不会一直以固定的60FPS运行。下一步就来看看这三个工具怎么使用:Unity’s internal profiler log, Unity’s Profiler , 以及Oculus Remote Monitor.
在调试Unity性能时,第一个要做的就是在Player Settings打开Enable Internal Profiler选项。开启之后,每几秒钟都会输出许多重要的帧率统计信息到logcat控制台上。这会让你清楚的知道你的每帧时间都干嘛去了。
为了说明调试性能一般步骤,我们来看一下从一个相当复杂场景中得到的采样数据,该游戏运行在Note 4 GearVR设备上。
Android Unity internal profiler stats:
cpu-player> min: 8.8 max: 44.3 avg: 16.3
cpu-ogles-drv> min: 5.1 max: 6.0 avg: 5.6
cpu-present> min: 0.0 max: 0.3 avg: 0.1
frametime> min: 14.6 max: 49.8 avg: 22.0
draw-call #> min: 171 max: 177 avg: 174 | batched: 12
tris #> min: 153294 max: 153386 avg: 153326 | batched: 2362
verts #> min: 203346 max: 203530 avg: 203411 | batched: 3096
player-detail> physx: 0.1 animation: 0.1 culling 0.0 skinning: 0.0
batching: 0.4 render: 11.6 fixed-update-count: 1 .. 1
mono-scripts> update: 0.9 fixedUpdate: 0.0 coroutines: 0.0
mono-memory> used heap: 3043328 allocated heap: 3796992
max number of collections: 0 collection total duration: 0.0
由于GearVR设备被卡在头盔中,为了获得这个数据,你需要使用ADB over TCPIP工具,通过WIFI连接手机,然后运行adb logcat命令。(更多信息,请查看Mobile SDk文档中“Android Debugging部分”)
上面的数据告诉我们,平均每帧耗时是22ms,大约是45FPS,比我们60FPS的目标还差得远。还可以看出,这个场景对CPU来说负担很重——22ms中有16.3ms花费在CPU上。我们花费了5ms在驱动上(“cpu-ogles-drv”),这意味着我们以较慢的方式在使用驱动。潜在的问题就很清楚了:每一帧有174Draw Calls,严重超标(我们的目标是100)。除此之外,我们使用了比预期更多的多边形。这个视图并没有告诉我们GPU上发生了什么,但是他告诉我们,只看CPU的话45FPS需要提高,并且应该专注于降低draw call。
这个数据还显示,在帧耗时上有规则的峰值(最长帧耗时为49.8ms)。为了理解这些峰值是哪来的,下一步要连接到Unity Profile,看下它的输出。
不出所料,上面的图显示了规则的峰值。在非峰值期间,我们的渲染时间和上面报告的数值相近,并且对最终的帧耗时影响不大。
Profiler中显示,峰值的时候,Gfx.WaitForPresent这个函数耗时最多。奇怪的是,我们实际的渲染时间再峰值的那一帧并没有显著增加。究竟是发生了什么?
Wait for present
看起来是因为WaitForPresent(及其兄弟版本,Overhead)重复出现,从而破坏了我们的帧率。实际上,它并不执行什么神秘的工作。相反,WaitForPresent记录了渲染管线停止的时间数量。
要理解渲染管线,一个好的方法是想象一个火车站。火车以固定的时间离开——每16.6毫秒。我们说火车在一个时刻只有一辆,现在有很多人排队准备上车。只要队伍中的每个人在火车离开之前都能上得去,那么每16毫秒都可以出发把他们运往目的地。但是,如果有一个人动作太慢没上去(可能被他自己的鞋带绊倒了),他就错过了这趟火车,他将坐在那里等待另一个16毫秒下一趟火车过来。哪怕他仅仅慢了1毫秒,错过这趟车都意味着他不得不等待下一次机会。
在渲染管线中,火车就相当于是前缓冲区(正在显示的内容)与后缓冲区(要显示的下一帧内容)交换。这通常发生在前一帧完成扫描显示结束的时候。假设,GPU可以在一段合理的时间内,执行所有缓冲的渲染指令,这里是每16毫秒前后缓冲交换一次。要维持在60FPS的刷新率,程序就必须在在16毫秒之内结束所有的工作。当CPU花费太长的时间来完成一帧,即使是只晚了1ms,就错过了缓冲区的交换时间,下一帧的扫描输出就开始使用上一阵的数据,然后CPU不得不停下来,等到下一次交换结束。用我们上面的比喻来说,火车就是缓冲区,CPU发送的每帧的指令就是这些乘客。
在这个例子中,我们清楚的看到,由于我们的帧率不够平稳,所以不能使得渲染管线更加稳定。解决WaitForPresent的办法就是在Profiler中忽略它,然后集中在优化其他每个地方。在这个例子中,就是降低场景中的Draw Calls。
其他Profiler信息
Unity Profiler对于挖掘运行时的各类信息非常有用,包括内存使用,Draw Calls,纹理分配以及音频开销。对于雅阁的性能调试来说,在Player Preferences里面关掉多线程渲染是个好主意。这将会使得渲染变慢很多,但是能让你清楚的知道每帧的时间都去哪了。当你优化完成的时候,记得再把多线程渲染打开。
除了Draw call Batching,其他常见的性能开销包括overdraw(通常是大型透明对象或者遮挡剔除不足导致的),蒙皮动画,物理开销,以及垃圾回收(通常是内存泄露或者反复回收引起的)。在你优化场景的时候要注意这些方面。同样要记住,显示最终的VR输出,包括warping和TimeWarp开销,每帧都会消耗大约2ms。
Oculus Remote Monitor
OVRMonitor是Oculus Mobile SDK中最近发布的一个新工具。它帮助开发者理解渲染管线工作的方式并且识别管线停止。它还可以利用无线从GearVR设备上把低分辨率无压缩的视频以流的形式获取回来,这点对可用性测试很有用。
OVRMonitor目前正在开发中,但是这个早期版本仍然可以用来可视化GearVR程序的图形管线。下面是一张截图,是使用该工具审查上述场景时的截图。
黄色条代表的是VSync间隔,指的是一帧的输出已经结束了。窗口上方的图片是渲染好的该帧截图。图片中间红色的条显示的是TimeWarp线程,可以看到它和实际的游戏是平行运行的。下面的蓝条指的是CPU和GPU的负载,看起来是不变的(这个例子中,4个CPU同时运行)。
这个截图实际上显示了Unity Profiler中WaitForPresent峰值中的一个。时间线中间的那一帧开始的太晚,以至于到下一个竖线的地方还没结束,结果造成CPU阻塞了完整的一帧(缺乏场景截图的那一帧,WarpSwapInternal线程占据了16.25ms)。
OVRMonitor可以帮助我们搞清楚渲染管线从一帧到另一帧发生了什么。它可以用于使用最新SDK编译的任何GearVR程序。更多信息请参考SdkRoot/Tools/OVRMonitor中的文档。更多文档和特性很快就来。
Tips and Tricks
这里有一些我们使用过的或者从其他开发者那里听过的性能技巧。这些并不能保证对所有VR应用有效,但是它们可能给你一些关于潜在解决方案的想法。
最后渲染大型的不透明Mesh。尝试把天空盒放到几何体+1渲染队列,让它在所有不透明物体渲染之后绘制。这可能导致被天空盒遮挡的许多像素被深度测试丢弃,因此节省你的时间。这个技巧也适用于地面和其他静态不透明物体,它们占据很多像素并且很可能大部分被遮挡了。
动态改变CPU/GPU节流限制。你可以在任何时候改变节流限制。如果游戏中大部分都可以以相对较低的设置运行,只有一两个特殊场景不行的话,考虑只在这些场景中加快CPU/GPU。对于简单的场景来说,也可以降低一个或多个处理器的速度,延长电池寿命并减少发热。例如,场景加载的时候可以把GPU设置成0.
不经常的更新渲染目标。如果你把场景渲染到辅助的纹理上,可以尝试使用比主场景较低频率渲染。例如,一个立体渲染的镜子每帧只对一个眼睛刷新一次。这有效的把镜子的帧率降低到30FPS,但是因为每帧都有新数据,对于轻微的移动看起来也是可以的。
降低渲染分辨率。通过降低一点视觉质量,通常可以显著提高游戏性能,例如稍微的减小每只眼睛渲染目标的大小。OVRManager.virtualTextureScale是0.0到1.0之间的数值,控制了渲染输出的尺寸。在老旧设备上稍微降低分辨率是支持较慢硬件的简单办法。
压缩纹理。所有GearVr设备支持ASTC压缩格式,这种格式能明显的减小纹理尺寸。需要注意的是,在本文写作时,Unity4.6期望ASTC压缩是和OpenGL ES3.0一起使用的。如果你使用在OpenGL ES2.0的情况下使用ASTC,你的纹理将在加载时解压,这可能延长程序启动时间。对于OpenGL ES2.0来说ETC1是一个低质量但是通用的方案。
使用纹理图集。如上所述,可以被许多Mesh共享访问的大纹理将会更高效。要避免过多小的单个纹理。
原文