主页(http://www.zhonghuagame.com):APM 实现 GPU 硬件层加速优化 Android 系统的游戏流畅度
作为一款 VR 实时操作游戏 App,我们需要根据重力感应系统,实时监控手机的角度,并渲染出相应位置的 VR 图像,因此在不同 Android 设备之间,由于使用的芯片组和不同架构的 GPU,游戏性能会因此受到影响。举例来说:游戏在 Galaxy S20+ 上可能以 60fps 的速度渲染,但它在 HUAWEI P50 Pro 上的表现可能与前者大相径庭。 由于新版本的手机具有良好的配置,而游戏需要考虑基于底层硬件的运行情况。
如果玩家遇到帧速率下降或加载时间变慢,他们很快就会对游戏失去兴趣。如果游戏耗尽电池电量或设备过热,我们也会流失处于长途旅行中的游戏玩家。如果提前预渲染不必要的游戏素材,会大大增加游戏的启动时间,导致玩家失去耐心。如果帧率和手机不能适配,在运行时会由于手机自我保护机制造成闪退,带来极差的游戏体验。
基于此,我们需要对代码进行优化以适配市场上不同手机的不同帧率运行。
所遇到的挑战
首先我们使用 Streamline 获取在 Android 设备上运行的游戏的配置文件,在运行测试场景时将 CPU 和 GPU 性能计数器活动可视化,以准确了解设备处理 CPU 和 GPU 工作负载,从而去定位帧速率下降的主要问题。
以下的帧率分析图表显示了应用程序如何随时间运行。
数学公式 : $ 每帧 GPU 成本 = GPU 最高频率 / 目标帧率 $
CPU 到 GPU 的调度存在一定的约束,由于调度上存在限制所以我们无法达到目标帧率。另外,由于 CPU-GPU 接口上的工作负载序列化,渲染过程是异步进行的。CPU 将新的渲染工作放入队列,稍后由 GPU 处理。
数据资源问题
CPU 控制渲染过程并且实时提供最新的数据,例如每一帧的变换和灯光位置。然而,GPU 处理是异步的。这意味着数据资源会被排队的命令引用,并在命令流中停留一段时间。而程序中的 OpenGL ES 需要渲染以反映进行绘制调用时资源的状态,因此在引用它们的 GPU 工作负载完成之前无法修改资源。
我们曾做出尝试,对引用资源进行代码上的编辑优化,然而当我们尝试修改这部分内容时,会触发该部分的新副本的创建。这将能够一定程度上实现我们的目标,但是会产生大量的 CPU 开销。
于是我们使用 Streamline 查明高 CPU 负载的实例。在图形驱动程序内部 libGLES_Mali.so 路径函数 , 视图中看到极高的占用时间。
基于前文的分析,我们首先尝试从缓冲区入手进行优化。单缓冲区方案 使用 glMapBufferRange 和 GL_MAP_UNSYNCHRONIZED. 然后使用单个缓冲区内的子区域构建旋转。这避免了对多个缓冲区的需求,但是这一方案仍然存在一些问题,我们仍需要处理管理子区域依赖项,这一部分的代码给我们带来了额外的工作量。多缓冲区方案 我们尝试在系统中创建多个缓冲区,并以循环方式使用缓冲区。通过计算我们得到了适合的缓冲区的数目,在之后的帧中,代码可以去重新使用这些循环缓冲区。由于我们使用了大量的循环缓冲区,那么大量的日志记录和数据库写入是非常有必要的。但是有几个因素会导致此处的性能不佳:1. 产生了额外的内存使用和 GC 压力2. Android 操作系统实际上是将日志消息写入日志而并非文件,这需要额外的时间。3. 如果只有一次调用,那么这里的性能消耗微乎其微。但是由于使用了循环缓冲区,所以这里需要用到多次调用。我们会在基于 c# 中的 Mono 分析器中启用内存分配跟踪函数用于定位问题:
$ adb shell setprop debug.mono.profile log:calls,alloc
我们可以看到该方法在每次调用时都花费时间:
Method call summary Total ( ms ) Self ( ms ) Calls Method name 782 5 100 MyApp.MainActivity:Log ( string,object [ ] ) 775 3 100 Android.Util.Log:Debug ( string,string,object [ ] ) 634 10 100 Android.Util.Log:Debug ( string,string )
在这里定位到我们的日志记录花费了大量时间,我们的下一步方向可能需要改进单个调用,或者寻求全新的解决方案。
log:alloc 还让我们看到内存分配;日志调用直接导致了大量的不合理内存分配:
Allocation summary Bytes Count Average Type name 41784 839 49 System.String 4280 144 29 System.Object [ ]
硬件加速
最后尝试引入硬件加速,获得了一个新的绘图模型来将应用程序渲染到屏幕上。它引入了 DisplayList 结构并且记录视图的绘图命令以加快渲染速度。
同时,可以将 View 渲染到屏幕外缓冲区并随心所欲地修改它而不用担心被引用的问题。此功能主要适用于动画,非常适合解决我们的帧率问题 , 可以更快地为复杂的视图设置动画。
如果没有图层,在更改动画属性后,动画视图将使其无效。对于复杂的视图,这种失效会传播到所有的子视图,它们反过来会重绘自己。
在使用由硬件支持的视图层后,GPU 会为视图创建纹理。因此我们可以在我们的屏幕上为复杂的视图设置动画,并且使动画更加流畅。
代码示例: