帧速率控制库   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 / 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 帧速率匹配库集成到您的游戏中