最小化Android GLSurfaceView滞后

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

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

该指南让我更加深入地了解了Android上所有不同的部分。 它涵盖了eglSwapBuffers如何将渲染帧推入队列,稍后在准备显示下一帧时由SurfaceFlinger使用。 如果队列已满,则会在返回之前等待下一帧的缓冲区可用。 上面的文档将其描述为“填充队列”,并依靠交换缓冲区的“背压”将渲染限制为显示的vsync。 这是使用GLSurfaceView的默认连续渲染模式发生的情况。

如果你的渲染比较简单,并且完成的时间远远less于帧周期,那么这个负面影响是由BufferQueue引起的额外的延迟,因为SwapBuffers的等待在队列满了之前不会发生,因此,重新渲染总是注定要在队列的后面,所以不会在下一个vsync中直接显示,因为在队列之前可能有缓冲区。

相比之下,按需渲染通常比显示更新频率less得多,所以通常这些视图的BufferQueues是空的,因此任何推入这些队列的更新都将被下一个vsync的SurfaceFlinger抓取。

所以这里有一个问题:我怎样才能build立一个连续的渲染器,但是延迟最小? 我们的目标是在每个vsync开始时缓冲区队列是空的,我在16ms内渲染我的内容,将它推到队列(缓冲区计数= 1),然后在下一个vsync(缓冲区计数)时被SurfaceFlinger使用= 0),重复。 队列中的缓冲区的数量可以在systrace中看到,所以目标是在0和1之间交替。

我上面提到的文档介绍了Choreographer作为获取每个vsync的callback的方法。 不过,我不相信这足以让我达到最小的滞后行为。 我已经testing了在一个非常小的onDrawFrame()的vsynccallback上执行requestRender(),它确实performance出0/1缓冲区计数的行为。 然而,如果SurfaceFlinger不能在单个帧周期内完成所有工作(可能是通知popup或者其他) 在这种情况下,我希望我的渲染器可以愉快地生成每帧vsync 1帧,但是BufferQueue的消费者结束了一帧。 结果:我们现在在队列中交替使用1和2个缓冲区,并且在渲染和查看帧之间获得了一个滞后帧。

该文档似乎build议查看报告的vsync时间与callback运行时间之间的时间偏移。 我可以看到,如果你的callback是由于你的主线程由于布局通过或者其他原因而被延迟的话,那么这可能会有帮助。 不过,我不认为这将允许检测SurfaceFlinger跳过节拍,并没有消耗一个框架。 有什么办法的应用程序可以解决SurfaceFlinger已经下降了一个框架? 似乎无法确定队列的长度是否会将使用vsync时间用于游戏状态更新的想法打破,因为在渲染渲染实际显示之前队列中存在未知数量的帧。

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

Solutions Collecting From Web of "最小化Android GLSurfaceView滞后"

伟大的问题。

对于读这个的其他人来说,一点点背景

这里的目标是最大限度地减less显示延迟,即应用渲染帧的时间和显示面板点亮像素之间的时间。 如果你只是在屏幕上扔内容,没关系,因为用户无法区分。 但是,如果您对触摸input做出响应,那么每一帧的延迟都会让您的应用程序感觉到反应迟钝。

问题类似于A / V同步,在屏幕上显示video帧时,您需要与帧相关联的audio才能从扬声器出来。 在这种情况下,只要在audio和video输出上始终保持相同的状态,整体延迟就不重要。 但是,这个问题面临着非常相似的问题,因为如果SurfaceFlinger停滞不前,并且您的video一直显示在一帧之后,就会失去同步。

SurfaceFlinger以高优先级运行,并且做的工作相对较less,所以不可能错过自己的节拍…但是可以发生。 此外,它合成来自多个来源的帧,其中一些使用栅栏来表示asynchronous完成。 如果一个准时的video帧是由OpenGL输出组成的,并且GLES渲染在截止时间到达时还没有完成,则整个合成将被推迟到下一个VSYNC。

最小化延迟的愿望足够强大,Android KitKat(4.4)版本在SurfaceFlinger中引入了“DispSync”function,可以减less半帧的延迟。 (这在graphics体系结构文档中有简要提及,但没有被广泛使用。)

所以就是这样的情况。 在过去,这对于video来说不是什么问题,因为30fps的video会更新每隔一帧的video。 打嗝自然而然地发生了,因为我们并没有试图保持排队。 我们开始看到48Hz和60Hz的video,所以这个更重要。

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

答案的第一部分是:你不能。 SurfaceFlinger没有状态查询或callback,它会告诉你它的状态是什么。 理论上你可以查询BufferQueue本身,但是这并不一定告诉你你需要知道什么。

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

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

Android正在向A / V同步方向发展的解决scheme是让应用程序在需要显示帧的时候告诉SurfaceFlinger。 如果SurfaceFlinger错过了最后期限,则会丢弃帧。 这是在4.4版本中添加的,尽pipe直到下一个版本才会被使用(在“L预览版”中它应该能够运行得很好,尽pipe我不知道是否包含了所有需要使用它的部分) 。

应用程序使用的方法是在eglPresentationTimeANDROID()之前调用eglPresentationTimeANDROID()扩展。 该函数的参数是所需的呈现时间,以纳秒为单位,使用与Choreographer相同的时基(特别是Linux CLOCK_MONOTONIC )。 因此,对于每一帧,您从编排器中获取时间戳,添加所需的帧数乘以大致的刷新率(您可以通过查询Display对象获得 – 参见MiscUtils#getDisplayRefreshNsec() ),并将其传递给到EGL。 当您交换缓冲区时,所需的显示时间将与缓冲区一起传递。

回想一下,SurfaceFlinger每个VSYNC唤醒一次,查看未决缓冲区的集合,并通过Hardware Composer将一组数据传送到显示硬件。 如果您在时间T请求显示,并且SurfaceFlinger相信在时间T-1或更早时间显示传递给显示硬件的帧,则帧将被保持(并重新显示前一帧)。 如果帧在时间T出现,它将被发送到显示器。 如果帧将在时间T + 1或更晚时间出现(即,它将错过其最后期限), 并且在稍后计划的队列中(例如,用于时间T + 1的帧)在其后面存在另一个帧,则用于时间T的框架将被丢弃。

该解决scheme并不完全适合您的问题。 对于A / V同步,您需要恒定的延迟,而不是最小的延迟。 如果你看看Grafika的“ 计划交换 ”活动,你可以find一些使用eglPresentationTimeANDROID()代码,类似于video播放器的做法。 (在目前的状态下,它仅仅是一个创buildsystrace输出的“音调发生器”,但基本的部分就在那里)。战略是提前几帧,所以SurfaceFlinger永远不会干燥,但是这对你的应用程序。

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

这将有助于一个应用程序知道有多less帧的延迟SurfaceFlinger实际上有,但没有一个查询。 (无论如何,这个处理起来有些尴尬,因为“智能面板”可以改变模式,从而改变显示延迟;但是除非你正在处理A / V同步,否则你真正关心的就是最小化SurfaceFlinger延迟。合理安全地假设4.3+上的两帧。 如果不是两帧的话,你的performance可能不是最理想的,但是如果你没有设置演示时间的话,那么净效果不会比你得到的差。

您可以尝试设置所需的演示时间等于编排时间戳; 最近的时间戳意味着“尽快显示”。 这确保了最小的等待时间,但是可以适应平稳。 SurfaceFlinger具有两帧延迟,因为它使系统中的所有内容都有足够的时间完成工作。 如果你的工作负载不平衡,你将在单帧和双帧的等待时间之间摇摆,输出在转换时看起来会很难受。 (这是DispSync的一个问题,将总时间减less到1.5帧。)

我不记得什么时候添加了eglPresentationTimeANDROID()函数,但是在较早的版本中,它应该是没有操作的。

底线 :对于'L',在某种程度上4.4,你应该能够得到你想要的行为使用EGL扩展与两帧的延迟。 在早期版本中,系统没有任何帮助。 如果你想确保你的方式没有缓冲区,你可以每隔一段时间故意删除一个帧,让缓冲区队列消失。

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

请注意,您仍然是三重缓冲区:正在显示一个缓冲区,SurfaceFlinger持有一个缓冲区以显示在下一个翻页中,一个正在被应用程序拖动。