android - 最小化 Android GLSurfaceView 延迟

标签 android surfaceview lag

继 Stack Overflow 上的其他一些问题之后,我从这里阅读了 Android Surfaces、SurfaceViews 等的内部指南:

https://source.android.com/devices/graphics/architecture.html

该指南让我对 Android 上的所有不同部分如何组合在一起有了更深入的了解。它介绍了 eglSwapBuffers 如何将渲染的帧推送到队列中,稍后 SurfaceFlinger 在准备下一帧以供显示时将使用该队列。如果队列已满,则它将等待,直到缓冲区可用于下一帧才返回。上面的文档将其描述为“填充队列”并依靠交换缓冲区的“背压”将渲染限制为显示器的 vsync。这就是使用 GLSurfaceView 的默认连续渲染模式时发生的情况。

如果您的渲染很简单并且在比帧周期短得多的时间内完成,则其负面影响是由 BufferQueue 引起的额外延迟,因为在队列已满之前不会发生对 SwapBuffers 的等待,因此我们的帧重新渲染总是注定在队列的后面,因此不会在下一个 vsync 上立即显示,因为队列中可能有缓冲区。

相比之下,按需渲染的发生频率通常远低于显示更新率,因此这些 View 的 BufferQueue 通常为空,因此任何推送到这些队列中的更新都将在下一个 vsync 上被 SurfaceFlinger 抓取。

所以这里的问题是:如何设置连续渲染器,但延迟最小?目标是缓冲区队列在每个 vsync 开始时为空,我在 16 毫秒内呈现我的内容,将其推送到队列(缓冲区计数 = 1),然后在下一个 vsync(缓冲区计数)上被 SurfaceFlinger 消耗= 0),重复。队列中的缓冲区数量可以在 systrace 中看到,所以目标是让这个在 0 和 1 之间交替。

我上面提到的文档介绍了 Choreographer 作为在每个 vsync 上获取回调的一种方式。但是,我不相信这足以实现我所追求的最小滞后行为。我已经测试过在 vsync 回调上使用非常小的 onDrawFrame() 执行 requestRender() 并且它确实表现出 0/1 缓冲区计数行为。但是,如果 SurfaceFlinger 无法在单个帧周期内完成其所有工作(可能弹出通知或其他什么)怎么办?在这种情况下,我希望我的渲染器会很高兴地为每个 vsync 生成 1 帧,但是该 BufferQueue 的消费者端已经丢弃了一个帧。结果:我们现在在队列中的 1 和 2 个缓冲区之间交替,并且我们在进行渲染和查看帧之间获得了一个延迟帧。

该文档似乎建议查看报告的 vsync 时间与运行回调之间的时间偏移。如果您的回调由于布局传递或其他原因导致主线程延迟交付,我可以看到这有什么帮助。但是,我认为这不会允许检测 SurfaceFlinger 跳过节拍并且未能消耗帧。应用程序是否可以通过任何方式确定 SurfaceFlinger 掉帧?似乎无法告诉队列的长度也打破了使用 vsync 时间进行游戏状态更新的想法,因为在实际显示您渲染的帧之前,队列中的帧数未知。

减少队列的最大长度并依靠背压将是实现这一目标的一种方法,但我认为没有 API 可以设置 GLSurfaceView BufferQueue 中的最大缓冲区数?

最佳答案

很好的问题。

快速的背景介绍 对于其他阅读本文的人:

这里的目标是最小化显示延迟,即应用程序渲染帧和显示面板点亮像素之间的时间。如果您只是将内容扔到屏幕上,那没关系,因为用户无法区分。但是,如果您对触摸输入做出响应,那么每一帧延迟都会让您的应用感觉响应速度变慢。

该问题类似于 A/V 同步,当视频帧显示在屏幕上时,您需要与帧相关联的音频从扬声器中传出。在这种情况下,整体延迟并不重要,只要它在音频和视频输出上始终相等即可。但是,这面临着非常相似的问题,因为如果 SurfaceFlinger 停顿并且您的视频始终在一帧后显示,您将失去同步。

SurfaceFlinger 以较高的优先级运行,并且执行的工作相对较少,因此它自己不太可能错过节拍……但它可能会发生。此外,它正在合成来自多个来源的帧,其中一些使用围栏来表示异步完成。如果一个准时的视频帧是用 OpenGL 输出合成的,并且在截止日期到来时 GLES 渲染还没有完成,整个合成将被推迟到下一个 VSYNC。

最小化延迟的愿望非常强烈,以至于 Android KitKat (4.4) 版本在 SurfaceFlinger 中引入了“DispSync”功能,这将通常的两帧延迟减少了半帧延迟。 (这在图形架构文档中简要提及,但并未广泛使用。)

所以情况就是这样。 在过去,这对视频来说不是问题,因为 30fps 视频每隔一帧更新一次。打嗝自然会自行解决,因为我们不会试图让队列保持满员。不过,我们开始看到 48Hz 和 60Hz 的视频,所以这更重要。

问题是,我们如何检测我们发送给 SurfaceFlinger 的帧是否正在尽快显示,或者是否在我们之前发送的缓冲区后面花费了额外的帧等待?

答案的第一部分是:你不能。 SurfaceFlinger 上没有状态查询或回调会告诉你它的状态是什么。理论上,您可以查询 BufferQueue 本身,但这不一定会告诉您需要了解的内容。

查询和回调的问题在于它们无法告诉您状态是什么,只能告诉您状态是什么。当应用收到信息并采取行动时,情况可能完全不同。该应用程序将以正常优先级运行,因此可能会出现延迟。

对于 A/V 同步,它稍微复杂一些,因为应用程序无法知道显示特性。例如,某些显示器具有内置内存的“智能面板”。 (如果屏幕上的内容不经常更新,您可以通过不让面板以每秒 60 次的速度扫描内存总线上的像素来节省大量电量。)这会增加额外的延迟帧,必须加以考虑。

Android 针对 A/V 同步的解决方案是让应用程序告诉 SurfaceFlinger 何时需要显示帧。如果 SurfaceFlinger 错过了最后期限,则会丢弃该帧。这是在 4.4 中以实验方式添加的,尽管它并没有真正打算在下一个版本之前使用(它应该在“L 预览版”中运行良好,但我不知道这是否包括完全使用它所需的所有部分) .

应用程序使用它的方式是调用 eglPresentationTimeANDROID()前扩展 eglSwapBuffers() .该函数的参数是所需的演示时间,以纳秒为单位,使用与 Choreographer 相同的时基(特别是 Linux CLOCK_MONOTONIC)。因此,对于每一帧,您使用从 Choreographer 获得的时间戳,添加所需的帧数乘以近似刷新率(您可以通过查询 Display 对象获得 - 请参阅 MiscUtils#getDisplayRefreshNsec() ),并将其传递给 EGL .当您交换缓冲区时,所需的演示时间与缓冲区一起传递。

回想一下,SurfaceFlinger 每个 VSYNC 唤醒一次,查看待处理缓冲区的集合,并通过 Hardware Composer 向显示硬件提供一组。如果您请求在时间 T 显示,并且 SurfaceFlinger 认为传递给显示硬件的帧将在时间 T-1 或更早的时间显示,则该帧将被保留(并重新显示前一帧)。如果帧将在时间 T 出现,它将被发送到显示器。如果该帧将在时间 T+1 或之后出现(即它将错过其最后期限),并且队列中还有另一个帧排在稍后的时间(例如,该帧用于时间 T+1),则用于时间 T 的帧将被丢弃。

该解决方案并不完全适合您的问题。对于 A/V 同步,您需要恒定延迟,而不是最小延迟。如果您查看 Grafika 的“scheduled swap” Activity ,您会发现一些使用 eglPresentationTimeANDROID() 的代码。以类似于视频播放器的方式。 (在目前的状态下,它只不过是一个用于创建 systrace 输出的“音调发生器”,但基本部分就在那里。)那里的策略是提前几帧渲染,所以 SurfaceFlinger 永远不会干涸,但这对你来说是完全错误的应用程序。

然而,呈现时间机制确实提供了一种丢帧而不是让它们备份的方法。如果您碰巧知道 Choreographer 报告的时间与您的帧可以显示的时间之间存在两帧延迟,则可以使用此功能确保帧在距离太远时将被丢弃而不是排队。过去的。 Grafika Activity 允许您设置帧速率和请求的延迟,然后在 systrace 中查看结果。

应用程序知道 SurfaceFlinger 实际有多少帧延迟会很有帮助,但没有查询。 (无论如何,这有点难以处理,因为“智能面板”可以改变模式,从而改变显示延迟;但除非您正在处理 A/V 同步,否则您真正关心的只是最小化 SurfaceFlinger 延迟。)它是在 4.3+ 上假设两帧相当安全。如果不是两帧,你的性能可能不是最优的,但净效果不会比你根本不设置演示时间时差。

您可以尝试将所需的演示时间设置为 Choreographer 时间戳;最近的时间戳意味着“尽快显示”。这确保了最小的延迟,但可能会适得其反。 SurfaceFlinger 具有两帧延迟,因为它为系统中的所有内容提供了足够的时间来完成工作。如果您的工作负载不均匀,您将在单帧和双帧延迟之间摇摆不定,并且输出在转换时看起来会很卡顿。 (这是 DispSync 的一个问题,它将总时间减少到 1.5 帧。)

我不记得是什么时候 eglPresentationTimeANDROID()功能已添加,但在旧版本中它应该是一个空操作。

底线 :对于“L”,以及在某种程度上 4.4,您应该能够使用具有两帧延迟的 EGL 扩展来获得您想要的行为。在早期版本中,系统没有任何帮助。如果您想确保没有缓冲区妨碍您,您可以故意每隔一段时间丢弃一帧以让缓冲区队列耗尽。

更新 :避免排队帧的一种方法是调用 eglSwapInterval(0) .如果您将输出直接发送到显示器,该调用将禁用与 VSYNC 的同步,从而取消应用程序的帧速率上限。当通过 SurfaceFlinger 渲染时,这会将 BufferQueue 置于“异步模式”,如果它们的提交速度比系统可以显示它们的速度快,则会导致它丢帧。

请注意,您仍然是三重缓冲:一个缓冲区正在显示,一个由 SurfaceFlinger 保存以在下一次翻转时显示,另一个正在由应用程序绘制。

关于android - 最小化 Android GLSurfaceView 延迟,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26317132/

相关文章:

android - 在 WebView 中自动播放视频时如何隐藏 "Start Video Playback"按钮

android:webView 处理方向变化

android - 将屏幕分为SurfaceView和xml布局

android - Listview 上的通用图像加载器很慢

java - 我在菜单项上膨胀了自定义布局以显示计数

java - 如何停止 Android 中的 SurfaceView?

android - 使用 Google Map V2 并打开虚拟键盘时 Android EditText 上的黑色矩形

javascript - 动画:jQuery VS CSS:jQuery 滞后,为什么? - jsFiddle 比较/示例

r - `dplyr::group_by` 中的因素是否有限制?

android - 从 BroadcastReceiver/AsyncTask 连接到 Google Drive