帧速率调整库   属于 Android 游戏开发套件 的一部分。

Android 帧速率调整库,也称为 Swappy,是 AGDK 库 的一部分。它帮助 OpenGL 和 Vulkan 游戏在 Android 上实现流畅的渲染和正确的帧速率调整。本文档定义了帧速率调整,描述了需要帧速率调整的情况,并展示了该库如何解决这些情况。如果您想直接跳转到在游戏中实现帧速率调整,请参见 下一步

背景

帧速率调整是将游戏的逻辑和渲染循环与操作系统的显示子系统和底层显示硬件同步。Android 显示子系统旨在避免在显示硬件在更新过程中切换到新帧中途时可能出现的视觉伪像(称为撕裂)。为了避免这些伪像,显示子系统执行以下操作:

  • 内部缓冲过去的帧
  • 检测延迟的帧提交
  • 检测到延迟的帧时重复显示过去的帧

游戏通知 SurfaceFlinger(显示子系统中的合成器),它已提交完成一帧所需的所有绘图调用(通过调用eglSwapBuffersvkQueuePresentKHR)。SurfaceFlinger 使用锁存器向显示硬件发出帧可用信号。然后显示硬件显示给定的帧。显示硬件以恒定速率运行,例如 60 Hz,如果硬件需要一帧时没有新帧,硬件会再次显示上一帧。

当游戏渲染循环的渲染速率与原生显示硬件不同时,通常会出现帧时间不一致的情况。如果以 30 FPS 运行的游戏尝试在原生支持 60 FPS 的设备上渲染,游戏渲染循环不会意识到重复的帧会在屏幕上额外停留 16 毫秒。这种脱节通常会导致帧时间出现相当大的不一致,例如:49 毫秒、16 毫秒、33 毫秒。过于复杂的场景会进一步加剧这个问题,因为它们会导致出现丢帧的情况。

非最优解

过去,游戏采用了以下帧速率调整解决方案,这些解决方案通常会导致帧时间不一致和输入延迟增加。

尽快提交渲染 API 允许的帧

这种方法将游戏与可变的 SurfaceFlinger 活动绑定在一起,并引入额外的帧延迟。显示管道包含一个帧队列,通常大小为 2,如果游戏尝试过快地呈现帧,该队列就会填满。队列中没有更多空间时,游戏循环(或至少渲染线程)会被 OpenGL 或 Vulkan 调用阻塞。然后游戏被迫等待显示硬件显示一帧,这种反压会同步这两个组件。这种情况称为缓冲填充队列填充。渲染器进程没有意识到发生了什么,因此帧率不一致会变得更糟。如果游戏在帧之前采样输入,输入延迟会变得更糟。

仅使用 Android Choreographer

游戏还使用 Android Choreographer 进行同步。此组件可在 API 16 中的 Java 和 API 24 中的 C++ 中使用,它以与显示子系统相同的频率提供定期滴答。关于此滴答相对于实际硬件 VSYNC 的交付时间,仍然存在一些细微之处,这些偏移因设备而异。对于长帧,仍然可能发生缓冲填充。

帧速率调整库的优势

帧速率调整库使用 Android 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 表示“无缓冲区”存在,并且重复使用之前的缓冲区)。

Ideal frame pacing at 30 Hz on a 60 Hz device

图 1. 在 60 Hz 设备上以 30 Hz 进行理想的帧速率调整

短游戏帧会导致卡顿

在大多数现代设备上,游戏引擎依赖于平台编排器提供滴答来驱动帧的提交。但是,由于短帧,仍然可能存在帧速率调整不佳的情况,如图 2 所示。短帧后跟长帧会被玩家感知为卡顿。

Short game frames

图 2. 短游戏帧 C 导致帧 B 只呈现一帧,然后是多个 C 帧

帧速率调整库通过使用呈现时间戳来解决此问题。该库使用呈现时间戳扩展 EGL_ANDROID_presentation_timeVK_GOOGLE_display_timing,这样帧就不会过早呈现,如图 3 所示。

Presentation timestamps

图 3. 游戏帧 B 呈现两次以获得更流畅的显示

长帧会导致卡顿和延迟

当显示负载时间超过应用程序负载时间时,额外的帧会被添加到队列中。这再次导致卡顿,也可能由于缓冲区填充而导致额外一帧的延迟(参见图 4)。该库同时消除了卡顿和额外的一帧延迟。

Long game frames

图 4. 长帧 B 对 2 帧(A 和 B)的节奏控制不准确

该库通过使用同步栅栏(EGL_KHR_fence_syncVkFence)向应用程序注入等待来解决这个问题,这使得显示管线能够赶上,而不是允许反压累积。帧 A 仍然呈现额外的一帧,但帧 B 现在正确呈现,如图 5 所示。

Waits added into application layer

图 5. 帧 C 和 D 等待呈现

支持的操作模式

您可以将帧速率控制库配置为以下三种模式之一运行

  • 自动模式关闭 + 管线
  • 自动模式开启 + 管线
  • 自动模式开启 + 自动管线模式(管线/非管线)

您可以尝试自动模式和管线模式,但是您可以先将它们关闭,并在初始化 Swappy 后添加以下内容:

  swappyAutoSwapInterval(false);
  swappyAutoPipelineMode(false);
  swappyEnableStats(false);
  swappySwapIntervalNS(1000000000L/yourPreferredFrameRateInHz);

管线模式

为了协调引擎工作负载,库通常使用管道模型,该模型跨 VSYNC 边界分离 CPU 和 GPU 工作负载。

Pipeline mode

图 6. 管线模式

非管线模式

通常,这种方法会导致更低、更可预测的输入屏幕延迟。在游戏帧时间非常短的情况下,CPU 和 GPU 工作负载都可能适合单个交换间隔。在这种情况下,非管道方法实际上会提供更低的输入屏幕延迟。

Non-pipeline mode

图 7. 非管线模式

自动模式

大多数游戏都不知道如何选择交换间隔,即每一帧呈现的持续时间(例如,30 Hz 为 33.3 毫秒)。在某些设备上,游戏可以以 60 FPS 渲染,而在其他设备上则可能需要降至较低的值。自动模式测量 CPU 和 GPU 时间以执行以下操作:

  • 自动选择交换间隔:在某些场景中以 30 Hz 呈现,在其他场景中以 60 Hz 呈现的游戏允许库动态调整此间隔。
  • 停用超高速帧的管道处理:在所有情况下都能提供最佳的输入屏幕延迟。

多个刷新率

支持多个刷新率的设备在选择看起来流畅的交换间隔方面提供了更高的灵活性

  • 在 60 Hz 设备上:60 FPS / 30 FPS / 20 FPS
  • 在 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 帧速率控制库集成到您的游戏中: