对Android游戏画面抖动现象的研究

对Android游戏画面抖动现象的研究

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


一、前言

近期笔者一直在研究一个专题:在Android平台下,游戏以30帧运行时,即便整体性能稳定,仍普遍存在画面抖动现象。即使是知名厂商的游戏也不例外。例如笔者测试《星穹轨道》时发现:


角色跑动时留意那盆绿植会比较明显

可能是出于功耗的考虑,一进来就默认推荐了30帧,玩起来也有持续性的画面抖动,不像是偶发的卡顿,我的手机性能是较好的,不至于连30帧都跑不流畅。

通过一段时间的研究,对这个问题的原理和解决方案有了一定的见解。

二、原因分析

1. 显示机制
Android的显示机制是一个典型的生产者-消费者模型。应用端负责生产画面,通过调用SwapBuffer将内容写入BufferQueue,系统端(SurfaceFlinger进程)则从BufferQueue中取出内容,执行后续的合成与显示流程。

SurfaceFlinger取BufferQueue的节奏是根据系统Vsync-sf信号调节的,一般跟屏幕刷新率走,比如60hz那么就是16.6ms为一个周期定期触发,当游戏为30帧时,平均是33.3ms生产一个Buffer,生产的速率远远慢于消耗的速率,当Vsync-sf信号来临但BufferQueue里面为空时,系统会重复显示上一帧的内容。

理想情况下,在60Hz屏幕下运行30帧,SurfaceFlinger应该每两个周期消耗一帧内容。然而通过分析发现,实际的消耗周期并不均匀:有时一个周期消耗,有时甚至三个周期消耗。导致每帧画面在屏幕上的停留时间不一致,从而在视觉上产生抖动感。

2. 量化工具
接下来使用Perfetto来看一下这个游戏,Perfetto是Android上系统级别的Trace工具,可以使用它来观察具体的BufferQueue、Vsync情况。

Perfetto有一些配置,针对画面抖动问题需要确认开启ATrace的gfx、view模块:

下图中标注了每一次Buffer被消耗时,距离上一次的周期数:

这里筛选出了Trace数据中相关的轨道,从BufferQueue里面可以看到大多数情况是2个周期消耗一次,偶尔会有1、3个周期出现。

Perfetto生成的Trace文件可以通过SQL进行统计。笔者统计了每次BufferQueue减少时的时间点,进一步计算了这些时间间隔的标准差。该指标可用于量化画面抖动的严重程度,为后续方案对比与优化提供了数据基础。


完整SQL内容在附录中提供

三、目前的解决方案

1. Swappy方案
Swappy是Google GameSDK的一部分,这里是它的官方介绍:
https://developer.android.com/games/sdk/frame-pacing?hl=zh-cn
*上述网址需要使用VPN打开

它是大多数游戏引擎内置的方案,以Unity引擎为例直接设置一下生效就行。

此方案核心是通过两个EGL拓展来达成它的目的:

  1. 通过setPresentationTime,为Buffer设置一个时间戳,避免此Buffer被过快消耗掉。

回到之前我们的例子,如果这里设置了一个允许被消耗的时间戳,那么这里不会存在1个周期就消耗掉的情况:

  1. 利用EGL_KHR_fence_sync,可以追踪上一帧GPU处理完毕再将当前帧Swap进去。由于实际游戏运行过程中不会是平稳地每次33.3ms Swap一次,会有波动,此策略可以避免局部生产过快导致的BufferQueue堆积。

Hook了Swappy中的一些函数,将其标注到了Perfetto中以观察其运行过程:

当然,笔者在实际使用过程中,也是遇到了不少问题:

  • 掉帧,常见于开60帧的情况,因为要等上一帧GPU结束,不同手机追踪到的GPU时间差异大,有的要10几ms,再加上Swappy是以周期为单位进行等待的,很容易延后的时间太多,最终导致帧率下降严重,所以建议60帧时不启用Swappy机制。
  • 延迟明显,一方面是渲染线程进行Swap的平均时间是要延后很多的,另一个方面是主线程是先做逻辑Update,然后通过WaitForPendingPresent等待上一帧渲染线程结束后,再继续,从Update到最终呈现的链路多了一个流程,30帧时预估延迟会增加30~40ms左右,体感明显。
  • 不稳定,不同机型效果差异很大,观察了一下发现有些机型setPresentationTime调用后,实际生效的时间并不精确,还有些机型60hz的屏幕刷新率但程序返回的值是59、58这些奇怪的值,可能会导致功能失效、锁帧之类的问题。

总的来说Swappy对画面抖动是有一定效果的,上述问题有些也好处理,只是此方案虽然开启简单,但决策前仍然要权衡、多机型实测评估。

2. 渲染线程同步方案
这是我在《王者荣耀》上发现的,他们使用了一种不同的方案来解决这个问题。


王者开30帧时的表现

此方案是通过让应用端画面生产节奏保持绝对的均匀,来间接让SurfaceFlinger消耗的节奏均匀,需要改一下引擎实现:

  • 渲染线程每次Swap调用前留一个时间戳,如果下一次Swap前的间隔时间小于33ms,则Sleep补全时间后再Swap。另外主线程WaitForTargetFPS的逻辑要去掉,依赖渲染线程控制输出节奏。
  • 还有个小细节,我通过逆向分析发现王者主线程等待上一个渲染线程结束的Sync点调整到了逻辑Update前,这样虽然降低了主线程、渲染线程的并行程度,但延迟上有优势,因为从逻辑Update到Swap中间没有WaitForPendingPresent。

这个办法简单易行,笔者实测是有效的,当然延迟还是不可避免会增加,30帧时预估延迟会增加16ms左右,仍然比Swappy少很多,算是一个平衡的方案,值得参考。

四、后续探索

很惭愧,目前对于这一问题,笔者尚未找到一个完全理想的解决方案。虽然“渲染线程同步”方案在实际测试中确实能够改善画面抖动,但仍存在不可忽视的局限:一方面,这一方案本质上是通过延长时间来达到输出节奏的均匀,导致整体延迟有所增加,另一方面,对于抖动的缓解效果其实也是有限的。

同时,Swappy库作为Google官方提供的开源方案,内部实现中有许多值得借鉴的点,它能推算出Vsync-sf信号点时间。所以笔者计划在现有“渲染线程同步”方案上进行进一步优化,结合推算出的Vsync-sf信号点时间信息来减少渲染线程Sleep的量,降低延迟,提升精准度。希望通过不断的迭代,能逐步逼近一个更加完美、兼顾效果与延迟的方案。

五、附录:Perfetto中量化统计画面抖动的SQL

WITH ValueChange AS (
    SELECT
        c.ts,
        t.name,
        c.value,
        LAG(c.value) OVER (PARTITION BY c.track_id ORDER BY c.ts) AS previous_value,
        process.name
    FROM
        counter AS c
    JOIN process_counter_track AS t ON c.track_id = t.id
    JOIN process ON t.upid = process.upid
    WHERE
        t.name LIKE "%SurfaceView%" and process.name LIKE "%SurfaceFlinger%"
),
ValueChange2 AS (
    SELECT
        ts,
        name,
        value
    FROM
        ValueChange
    WHERE
        value < previous_value
),
intervals AS (
    SELECT
        ts - LAG(ts) OVER (ORDER BY ts) AS interval_ns
    FROM ValueChange2
),
intervals_ms AS (
    SELECT
        interval_ns / 1000000.0 AS interval_ms  -- 将纳秒转换为毫秒
    FROM intervals
    WHERE interval_ns IS NOT NULL and interval_ns < 100000000
),
stats AS (
    SELECT
        COUNT(interval_ms) AS count_interval,
        AVG(interval_ms) AS mean_interval,
        AVG(interval_ms * interval_ms) AS mean_square_interval
    FROM intervals_ms
)
SELECT
    mean_interval AS avg_consume_interval_ms,
    SQRT(mean_square_interval - mean_interval * mean_interval) AS stddev_consume_interval_ms
FROM stats;

这是侑虎科技第1894篇文章,感谢作者其乐陶陶供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/jun-yan-76-80

再次感谢其乐陶陶的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)