最小化Android GLSurfaceView滞后

时间:2014-10-11 17:09:10

标签: android surfaceview lag

关于Stack Overflow的其他一些问题,我已经从这里阅读了Android Surfaces,SurfaceViews等内部指南:

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

该指南让我更加了解所有不同的部分如何在Android上融合在一起。它介绍了eglSwapBuffers如何将渲染帧推送到队列中,当它准备下一帧显示时,SurfaceFlinger将使用该队列。如果队列已满,那么它将等到缓冲区在返回之前可用于下一帧。上面的文档将此描述为"填充队列"并依靠"背压"交换缓冲区限制渲染到显示的vsync。这是使用GLSurfaceView的默认连续渲染模式发生的事情。

如果渲染很简单并且在比帧周期少得多的情况下完成,那么这是由BufferQueue引起的额外延迟,因为SwapBuffers上的等待直到队列满了才发生,并且因此,我们渲染的帧始终位于队列的后面,因此不会立即显示在下一个vsync上,因为队列中可能存在缓冲区。

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

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

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

该文档似乎建议查看报告的vsync时间与回调运行时间之间的时间偏差。我可以看到,如果你的回调由于你的主线程由于布局传递或其他东西而延迟传递,那会有什么帮助。但是我不认为这会允许检测到SurfaceFlinger跳过一个节拍并且没有消耗帧。应用程序有什么方法可以解决SurfaceFlinger丢帧的问题吗?似乎无法告诉队列的长度打破了使用vsync时间进行游戏状态更新的想法,因为在你渲染的队列之前队列中有未知数量的帧实际上是显示。

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

1 个答案:

答案 0 :(得分:24)

很棒的问题。

快速背景让其他人阅读此内容:

此处的目标是最小化显示延迟,即应用渲染帧与显示面板亮起像素之间的时间。如果您只是在屏幕上投放内容,那并不重要,因为用户无法区分。但是,如果您对触摸输入做出响应,那么每一段延迟都会让您的应用感觉响应性稍差。

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

SurfaceFlinger以更高的优先级运行,并且相对较少的工作,所以不可能单独错过一个节拍......但它可能会发生。此外,它是从多个源合成帧,其中一些使用栅栏来发出异步完成信号。如果准时视频帧由OpenGL输出组成,并且在截止日期到来时GLES渲染尚未完成,则整个构图将被推迟到下一个VSYNC。

最小化延迟的愿望足够强大,以至于Android KitKat(4.4)版本引入了" DispSync" SurfaceFlinger中的功能,可以减少通常的两帧延迟的半帧延迟。 (这在图形架构文档中有简要提及,但它并未得到广泛使用。)

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

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

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

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

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

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同步,您需要恒定的延迟,而不是最小延迟。如果你看看格拉菲卡" scheduled swap"活动中,您可以找到一些使用eglPresentationTimeANDROID()的代码,其方式与视频播放器类似。 (在目前的状态下,它只是一个"音调发生器"用于创建systrace输出,但基本的部分就在那里。)那里的策略是提前几帧,所以SurfaceFlinger永远不会干涸,但这对你的应用来说是完全错误的。

然而,表示时机制确实提供了一种删除帧而不是让它们备份的方法。如果你碰巧知道Choreographer报告的时间和你的帧可以显示的时间之间有两个延迟帧,你可以使用这个功能来确保帧被丢弃而不是排队,如果它们太远了过去。 Grafika活动允许您设置帧速率和请求的延迟,然后在systrace中查看结果。

应用程序知道SurfaceFlinger实际上有多少帧延迟会有所帮助,但是没有查询。 (无论如何,这有点尴尬,因为"智能面板"可以改变模式,从而改变显示延迟;但除非您正在进行A / V同步,否则您真正关心的是最小化SurfaceFlinger延迟。)在4.3+上假设两帧是合理安全的。如果它不是两个帧,那么你的表现可能不是很理想,但如果你没有设置演示时间,那么净效果将不会比你想要的更差。

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

我不记得添加eglPresentationTimeANDROID()功能的时间,但在旧版本中,它应该是无操作。

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

更新:避免排队帧的一种方法是致电eglSwapInterval(0)。如果您将输出直接发送到显示器,则呼叫将禁用与VSYNC的同步,取消限制应用程序的帧速率。当通过SurfaceFlinger进行渲染时,这会将BufferQueue置于"异步模式",如果它们提交的速度超过了系统可以显示的速度,则会丢弃帧。

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