帧同步库 属于 Android 游戏开发套件。
Android 帧同步库(也称为 Swappy)是 AGDK 库的一部分。它帮助 OpenGL 和 Vulkan 游戏在 Android 上实现流畅的渲染和正确的帧同步。本文档定义了帧同步,描述了需要帧同步的情况,并展示了该库如何解决这些情况。如果您想直接跳转到在游戏中实现帧同步,请参阅下一步。
背景
帧同步是指游戏的逻辑和渲染循环与操作系统的显示子系统和底层显示硬件的同步。Android 显示子系统旨在避免在显示硬件更新过程中切换到新帧时可能出现的视觉伪影(称为画面撕裂)。为避免这些伪影,显示子系统会执行以下操作:
- 内部缓冲过去帧
- 检测延迟的帧提交
- 当检测到延迟帧时,重复显示过去的帧
游戏通过调用 eglSwapBuffers
或 vkQueuePresentKHR
通知 SurfaceFlinger(显示子系统中的合成器),它已提交一帧所需的所有绘制调用。SurfaceFlinger 使用锁存器向显示硬件发出帧可用信号。然后显示硬件显示给定帧。显示硬件以恒定速率(例如 60 Hz)运行,如果硬件需要新帧时没有新帧,则硬件会再次显示前一帧。
当游戏渲染循环以与原生显示硬件不同的速率渲染时,通常会出现不一致的帧时间。如果一个以 30 FPS 运行的游戏尝试在原生支持 60 FPS 的设备上渲染,游戏渲染循环不会意识到重复帧会在屏幕上额外停留 16 毫秒。这种不一致通常会导致帧时间出现显著不一致,例如:49 毫秒、16 毫秒、33 毫秒。过于复杂的场景会进一步加剧这个问题,因为它们会导致帧丢失。
非最优解决方案
过去游戏中采用的以下帧同步解决方案通常会导致帧时间不一致和输入延迟增加。
尽可能快地提交帧(由渲染 API 允许)
这种方法将游戏与可变的 SurfaceFlinger 活动绑定,并引入额外的帧延迟。显示管道包含一个帧队列,通常大小为 2,如果游戏试图过快地呈现帧,该队列就会被填满。当队列中没有更多空间时,游戏循环(或至少是渲染线程)会被 OpenGL 或 Vulkan 调用阻塞。然后游戏被迫等待显示硬件显示一帧,这种反压同步了这两个组件。这种情况称为缓冲区填充或队列填充。渲染器进程没有意识到发生了什么,因此帧率不一致会变得更糟。如果游戏在帧之前采样输入,则输入延迟会变得更糟。
单独使用 Android Choreographer
游戏也使用 Android Choreographer 进行同步。此组件在 Java 中从 API 16 开始提供,在 C++ 中从 API 24 开始提供,它以与显示子系统相同的频率提供规律的“节拍”。关于此节拍相对于实际硬件 VSYNC 的传递时间,仍然存在一些细微之处,并且这些偏移因设备而异。对于长帧,仍然可能发生缓冲区填充。
帧同步库的优势
帧同步库使用 Android Choreographer 进行同步,并为您处理节拍传递中的可变性。它使用呈现时间戳确保帧在正确的时间呈现,并使用同步栅栏避免缓冲区填充。如果 NDK Choreographer 可用,则库使用 NDK Choreographer,否则回退到 Java Choreographer。
如果设备支持多种刷新率,该库会处理它们,这为游戏提供了更大的灵活性来呈现帧。例如,对于支持 60 Hz 和 90 Hz 刷新率的设备,无法每秒生成 60 帧的游戏可以降至 45 FPS 而不是 30 FPS 以保持流畅。该库检测预期的游戏帧率并相应地自动调整帧呈现时间。帧同步库还提高了电池续航,因为它避免了不必要的显示更新。例如,如果游戏以 60 FPS 渲染但显示器以 120 Hz 更新,则屏幕每帧更新两次。帧同步库通过将刷新率设置为设备支持的、最接近目标帧率的值来避免这种情况。
工作原理
以下部分展示了帧同步库如何处理长帧和短帧以实现正确的帧同步。
在 30 Hz 下实现正确的帧同步
当在 60 Hz 设备上以 30 Hz 渲染时,Android 上的理想情况如图 1 所示。SurfaceFlinger 会锁存新的图形缓冲区(如果存在)(图中的 NB 表示“无缓冲区”存在,并且重复前一个缓冲区)。
图 1. 在 60 Hz 设备上以 30 Hz 实现理想帧同步。
短游戏帧导致卡顿
在大多数现代设备上,游戏引擎依靠平台 Choreographer 提供节拍来驱动帧的提交。然而,由于短帧,仍然可能出现糟糕的帧同步,如图 2 所示。短帧后跟长帧被玩家认为是卡顿。
图 2. 短游戏帧 C 导致帧 B 仅呈现一帧,随后是多个 C 帧。
帧同步库通过使用呈现时间戳来解决此问题。该库使用呈现时间戳扩展 EGL_ANDROID_presentation_time
和 VK_GOOGLE_display_timing
,以确保帧不会过早呈现,如图 3 所示。
图 3. 游戏帧 B 呈现两次以实现更流畅的显示。
长帧导致卡顿和延迟
当显示工作负载所需时间超过应用工作负载时,额外的帧会被添加到队列中。这再次导致卡顿,并且由于缓冲区填充,还可能导致额外的帧延迟(参见图 4)。该库既消除了卡顿,也消除了额外的帧延迟。
图 4. 长帧 B 导致帧 A 和 B 的同步不正确
该库通过使用同步栅栏(EGL_KHR_fence_sync
和 VkFence
)向应用注入等待,从而允许显示管道追赶,而不是允许反压累积来解决此问题。帧 A 仍然呈现额外一帧,但帧 B 现在正确呈现,如图 5 所示。
图 5. 帧 C 和 D 等待呈现。
支持的运行模式
您可以将帧同步库配置为以下三种模式之一运行:
- 自动模式关闭 + 管道
- 自动模式开启 + 管道
- 自动模式开启 + 自动管道模式(管道/非管道)
推荐模式
您可以尝试自动模式和管道模式,但首先要关闭它们并在初始化 Swappy 后包含以下内容:
swappyAutoSwapInterval(false);
swappyAutoPipelineMode(false);
swappyEnableStats(false);
swappySwapIntervalNS(1000000000L/yourPreferredFrameRateInHz);
管道模式
为了协调引擎工作负载,该库通常使用流水线模型,该模型将 CPU 和 GPU 工作负载在 VSYNC 边界上分离。
图 6. 管道模式。
非管道模式
通常,这种方法会降低并提高输入-屏幕延迟的可预测性。在游戏帧时间非常低的情况下,CPU 和 GPU 工作负载可能都可以在单个交换间隔内完成。在这种情况下,非流水线方法实际上会提供更低的输入-屏幕延迟。
图 7. 非管道模式。
自动模式
大多数游戏不知道如何选择交换间隔,即每帧呈现的持续时间(例如,30 Hz 对应 33.3 毫秒)。在某些设备上,游戏可以以 60 FPS 渲染,而在另一些设备上可能需要降低到更低的值。自动模式测量 CPU 和 GPU 时间以执行以下操作:
- 自动选择交换间隔:某些场景以 30 Hz 呈现,另一些场景以 60 Hz 呈现的游戏可以允许库动态调整此间隔。
- 停用超快帧的流水线处理:在所有情况下提供最佳输入-屏幕延迟。
多刷新率
支持多刷新率的设备在选择看起来流畅的交换间隔方面提供了更高的灵活性:
- 在 60 Hz 设备上:60 FPS / 30 FPS / 20FPS
- 在 60 Hz + 90 Hz 设备上:90 FPS / 60 FPS / 45 FPS / 30 FPS
- 在 60 Hz + 90 Hz + 120 Hz 设备上:120 FPS / 90 FPS / 60 FPS / 45 FPS / 40 FPS / 30 FPS
该库选择最符合游戏帧实际渲染持续时间的刷新率,从而提供更好的视觉体验。
有关多刷新率帧同步的更多信息,请参阅《Android 上的高刷新率渲染》博客文章。
帧统计
帧同步库提供以下统计信息,用于调试和性能分析:
- 渲染完成后,帧在合成器队列中等待的屏幕刷新次数的直方图。
- 请求呈现时间与实际呈现时间之间经过的屏幕刷新次数的直方图。
- 两个连续帧之间经过的屏幕刷新次数的直方图。
- 此帧 CPU 工作开始时间与实际呈现时间之间经过的屏幕刷新次数的直方图。
下一步
请参阅以下任一指南,将 Android 帧同步库集成到您的游戏中:
额外资源
- 《奇迹:MU》通过使用 Swappy 提升渲染性能,将慢会话率从 40% 降低到 10%。