最小化Android GLSurfaceView滞后

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

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

该指南让我对Android上所有不同部分的组合方式有了更好的理解。 它介绍了eglSwapBuffers如何将渲染帧推送到队列中,该队列在准备下一帧进行显示时将由SurfaceFlinger使用。 如果队列已满,那么它将等到缓冲区在返回之前可用于下一帧。 上面的文档将其描述为“填充队列”并依赖交换缓冲区的“反压”来限制渲染到显示的vsync。 这是使用GLSurfaceView的默认连续渲染模式发生的情况。

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

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

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

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

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

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

好问题。

阅读此内容的其他人的快速背景知识

这里的目标是最小化显示延迟,即应用渲染帧和显示面板点亮像素之间的时间。 如果你只是在屏幕上投放内容,那没关系,因为用户无法区分。 但是,如果您正在响应触摸输入,那么每一段延迟都会让您的应用感觉响应性稍差。

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

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

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

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

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

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

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

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

Android正在向A / V同步迈进的解决方案是让应用程序在想要显示帧时告诉SurfaceFlinger。 如果SurfaceFlinger错过截止日期,则会丢弃框架。 这是在4.4中通过实验添加的,虽然它并不打算在下一个版本之前使用(它应该在“L预览”中运行得很好,但我不知道是否包含完全使用它所需的所有部分) 。

应用程序使用它的方式是在eglPresentationTimeANDROID()之前调用eglPresentationTimeANDROID()扩展。 该函数的参数是所需的呈现时间,以纳秒为单位,使用与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的“ 预定交换 ”活动,你可以find一些使用eglPresentationTimeANDROID()代码,其方式与video播放器类似。 (在目前的状态下,它只不过是用于创建systrace输出的“音调发生器”,但基本部分就在那里。)那里的策略是提前几帧,所以SurfaceFlinger永远不会干,但这对你来说是完全错误的应用程序。

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

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

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

我不记得是什么时候添加了eglPresentationTimeANDROID()函数,但是在旧版本中它应该是no-op。

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

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

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