安卓三重缓冲 - 预期的行为是什么?

9
我正在调查我的应用程序的性能,因为我注意到在滚动时会掉帧。我在运行4.3系统的Nexus 4上运行了systrace,并注意到输出中有一个有趣的部分
一开始一切都很好。放大左侧部分, 我们可以看到每个vsync都开始绘制,完成后还有时间,并等待下一个vsync。由于它是三倍缓冲,所以应该绘制到一个缓冲区中,在完成后将在以下vsync上发布。
在缩放截图的第四个vsync中,应用程序执行了一些操作,绘制操作没有及时完成下一个vsync。但是,我们没有丢帧,因为之前的绘制已经提前一帧工作。

在这种情况下,绘制操作无法弥补错过的垂直同步。相反,每个垂直同步只启动一次绘制操作,现在它们不再提前绘制一帧了。

放大右侧部分, 应用程序会执行更多的工作并错过另一个垂直同步。由于我们没有提前绘制一帧,因此实际上会丢失一帧。之后,它会回到提前绘制一帧的状态。

这是预期的行为吗?我的理解是三倍缓冲允许您在错过垂直同步时进行恢复,但这种行为似乎会在错过两个垂直同步时丢失一帧。


跟进问题
  1. 这张截图的右侧,应用程序实际上正在比显示更快地渲染缓冲区。在执行遍历#1(在截图中标记)期间,假设正在显示缓冲区A并且正在渲染缓冲区B。 #1在vsync之前很长时间完成,并将缓冲区B放入队列中。此时,应用程序是否能够立即开始渲染缓冲区C?相反,执行遍历#2直到下一个vsync才开始,浪费了宝贵的时间。

  2. 同样地,我有点困惑为什么需要在这里左侧waitForever。假设正在显示缓冲区A,缓冲区B在队列中,缓冲区C正在被渲染。当缓冲区C完成渲染时,为什么不立即将其添加到队列中?相反,它会一直等待,直到从队列中删除缓冲区B,然后才添加缓冲区C,这就是为什么队列似乎始终保持大小为1,无论应用程序渲染缓冲区多快。

1个回答

10
提供的缓冲量只有在保持缓冲区充满的情况下才有意义。这意味着要比显示器消耗它们的速度更快地进行渲染。
标签不会出现在您的图像中,但我猜测上面绿色垂直同步行的紫色行是BufferQueue状态。您可以看到它通常同时具有0或1个完整的缓冲区。在“左侧放大”的图片的最左边,您可以看到它有两个缓冲区,但之后它只有一个,在屏幕的3/4处,您会看到一个非常短的紫色条,表示它刚好勉强及时渲染了帧。
请参阅此帖子此帖子以获取背景信息。
为了回答新增的问题,更新如下: 另一篇文章中的详细信息仅仅触及到表面。我们必须更深入地探讨。
在systrace中显示的BufferQueue计数是排队缓冲区的数量,即包含内容的缓冲区数量。当SurfaceFlinger获取用于显示的缓冲区时,它会立即释放该缓冲区,并将其状态更改为“free”。当缓冲区在叠加层上显示时,这特别令人兴奋,因为显示器直接从缓冲区渲染(而不是合成到临时缓冲区并显示)。
让我再说一遍:正在主动读取数据以在屏幕上显示的缓冲区在BufferQueue中被标记为“free”。该缓冲区有一个关联的fence,最初为“active”。在其处于活动状态时,任何人都不允许修改缓冲区内容。当显示器不再需要缓冲区时,它会发出信号。
因此,左侧跟踪代码中的waitForever()之所以如此,是因为它正在等待fence发出信号。当VSYNC触发时,显示器切换到另一个缓冲区,发出信号,您的应用程序可以立即开始使用该缓冲区。这消除了必须等待SurfaceFlinger唤醒、查看缓冲区是否不再使用、通过BufferQueue发送IPC来释放缓冲区等待时间引起的延迟。
请注意,当您没有落后(跟踪的左侧和右侧)时,对waitForever()的调用才会出现。我不确定为什么在队列只有1个完整缓冲区时它会发生,因为它应该将最旧的缓冲区出列,这个缓冲区应该已经发出信号。
最重要的是,对于三倍缓冲,您永远不会看到BufferQueue超过两个。
并非所有设备都像上面描述的那样工作。 Nexus 7(2012)不使用“显式同步”机制,而且ICS之前的设备根本没有BufferQueues。
回到您的编号截图,是的,在“1”和“2”之间有足够的时间让您的应用程序运行performTraversals()。如果不知道您的应用程序正在做什么,很难确定,但我猜您有一个由Choreographer驱动的动画循环,每次VSYNC唤醒并执行任务。它不会更频繁地运行。
如果您使用Android Breakout进行系统跟踪,您可以看到当您尽可能快地渲染时(“队列填充”),并依靠BufferQueue反向压力来调节游戏速度时的外观。
特别有趣的是将运行4.3的N4与运行4.4的N4进行比较。在4.3上,跟踪类似于您的跟踪,队列大部分时间都在1左右,定期下降到0,偶尔会升高到2。在4.4上,队列几乎总是在2,并偶尔下降到1。在两种情况下,它都在eglSwapBuffers()中睡眠;在4.3中,跟踪通常显示下面的waitForever(),而在4.4中则显示dequeueBuffer()。(我不知道这个原因。)

更新2: 4.3和4.4之间的差异原因似乎是Nexus 4驱动程序的更改。 4.3驱动程序使用旧的dequeueBuffer调用,它转换为dequeueBuffer_DEPRECATED()Surface.cpp第112行)。 旧接口不将围栏作为“out”参数,因此调用必须自己调用waitForever()。 新接口只是将围栏返回给GL驱动程序,需要等待时才执行等待(可能不会立即执行)。

更新3: 现在可以在此处找到更长的解释。


谢谢回答!链接的帖子非常有用,知道systrace显示缓冲队列也很好。不过我还有几个问题(已添加在上面)。 - peterb_sm
感谢您提供的所有信息!您说得对,systrace片段是在ListView fling期间生成的,并且从阅读源代码来看,AbsListView在flings期间使用postOnAnimation()来刷新自身。我仍然需要弄清楚解决我的问题并赶上落后的最佳方法是什么,但知道原因真的很有帮助。再次感谢! - peterb_sm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接