在 Android 上进行高刷新率渲染
作者 / Ady Abraham, Software Engineer
长久以来,手机屏幕刷新率都是 60Hz。应用和游戏开发者也习惯了假定刷新率为 60Hz,也就是每 16.6ms 生成一帧,而且这样开发出来的应用和游戏都会正常进行。但现在的情况已经不同了。最新的旗舰级设备往往会搭载刷新率更高的屏幕,可以带来更流畅的动画效果、更低的延迟,从而获得更好的整体用户体验。还有一些设备支持可变刷新率,比如 Pixel 4,它支持 60Hz 和 90Hz 两种刷新率。
60Hz 的屏幕每 16.6ms 刷新一次显示内容。这意味着图像显示的时间是 16.6ms 的倍数 (16.6ms、33.3ms、50ms 等)。支持多种刷新率的屏幕则带来了更多的选择,这些屏幕能以不同的速度进行渲染,并且不会出现抖动。例如,一个无法维持 60fps 渲染的游戏,在 60Hz 的屏幕上必须一路降到 30fps 才能确保流畅无抖动 (因为显示器只能以 16.6ms 的倍数周期呈现图像,所以 60Hz 的下一档可用帧速是每 33.3ms 显示一帧,即 30fps)。而在 90Hz 设备上,同样的游戏只需要下降到 45fps (每帧 22.2ms) 即可,这就为用户带来了更流畅的体验。而同时支持 90Hz 和 120Hz 的设备,则可以用每秒 120、90、60 (120/2)、45 (90/2)、40 (120/3)、30 (90/3)、24 (120/5) 等帧率流畅地呈现内容。
高频率渲染
渲染频率越高,就越难维持帧率,因为只有更少的时间完成相同的工作量。要在 90Hz 下进行渲染,应用需要在 11.1ms 内生成一帧,与此相比,在 60Hz 时则有 16.6ms 来生成一帧。
应用的 UI 线程处理输入事件,调用应用的回调,并更新视图 (View) 层次结构中记录的绘图命令列表; 应用的 RenderThread 将记录的命令发送到 GPU ; GPU 绘制这一帧; SurfaceFlinger 是负责在屏幕上显示不同应用窗口的系统服务,它会组合出屏幕应该最终显示出的内容,并将画面提交给屏幕的硬件抽象层 (HAL); 屏幕最终呈现该帧的内容。
利用可变刷新率
如上所述,可变刷新率允许我们使用更多样的渲染频率。对于可以控制渲染速度的游戏,以及需要以特定速率呈现内容的视频播放器来说,这一点尤其有用。例如,要在 60Hz 的显示器上播放 24fps 的视频,我们需要使用 3:2 pulldown 算法,这就会产生抖动。但是,如果设备的屏幕可以原生显示 24fps 的内容 (24/48/72/120Hz),就无需使用 pulldown 算法,自然也就不会出现抖动了。
3:2 pulldown 算法
https://en.wikipedia.org/wiki/Three-two_pull_down
为此,应用可能需要知道当前设备的刷新率。可以通过以下方法来实现:
SDK 通过 DisplayManager.DisplayListener 注册一个显示监听器,并通过 Display.getRefreshRate 查询刷新率。 NDK 使用 AChoreographer_registerRefreshRateCallback 注册回调 (API 级别30)。
DisplayManager.DisplayListener https://developer.android.google.cn/reference/android/hardware/display/DisplayManager#registerDisplayListener(android.hardware.display.DisplayManager.DisplayListener,%20android.os.Handler) Display.getRefreshRate https://developer.android.google.cn/reference/android/view/Display#getRefreshRate() AChoreographer_registerRefreshRateCallback https://developer.android.google.cn/ndk/reference/group/choreographer#achoreographer_registerrefreshratecallback
应用可以通过在其 Window 或 Surface 上设置帧率来影响设备刷新率。这是 Android 11 中引入的一个新功能,允许平台了解应用的渲染需求。应用可以调用以下方法之一:
SDK
Surface.setFrameRate
https://developer.android.google.cn/reference/android/view/Surface#setFrameRate(float,%20int)
SurfaceControl.Transaction.setFrameRate
https://developer.android.google.cn/reference/android/view/SurfaceControl.Transaction.html#setFrameRate(android.view.SurfaceControl,%20float,%20int)
NDK
ANativeWindow_setRrameRate https://developer.android.google.cn/ndk/reference/group/a-native-window#anativewindow_setframerate ASurfaceTransaction_setFrameRate https://developer.android.google.cn/ndk/reference/group/native-activity#asurfacetransaction_setframerate
帧率指南 https://developer.android.google.cn/guide/topics/media/frame-rate
WindowManager.LayoutParams.preferredDisplayModeId https://developer.android.google.cn/reference/android/view/WindowManager.LayoutParams#preferredDisplayModeId Display.getSupportedModes https://developer.android.google.cn/reference/android/view/Display#getSupportedModes()
总结
刷新率并不总是恒定的——如果您想了解实际的刷新率,就需要注册一个回调来知晓刷新率的变动,并相应地更新您应用内部的数据。
如果您没有使用 Android UI 工具包,而使用自定义的渲染器,请考虑根据当前的刷新率来改变您的渲染流水线。通过使用 OpenGL 上的 eglPresentationTimeANDROID 或 Vulkan 上的 VkPresentationTimesInfoGOOGLE 设置一个呈现时间戳,即可深化流水线。设置呈现时间戳可以向 SurfaceFlinger 指示何时呈现图像。如果设置为未来的几帧,它就会按照设置的帧数加深流水线。前文例子中的 Android UI 将呈现时间设置成了 frameTimeNanos + 2 * vsyncPeriod。
注: frameTimeNanos 从 Choreographer 获取;vsyncPeriod 从 Display.getRefreshRate() 获取。
eglPresentationTimeANDROID https://www.khronos.org/registry/EGL/extensions/ANDROID/EGL_ANDROID_presentation_time.txt VkPresentationTimesInfoGOOGLE https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkPresentTimesInfoGOOGLE.html
实现适当的帧同步 https://developer.android.google.cn/games/sdk/frame-pacing
推荐阅读