【问题标题】:Android triple buffering - expected behavior?Android 三重缓冲 - 预期的行为?
【发布时间】:2014-04-30 00:21:45
【问题描述】:

我正在调查我的应用的性能,因为我注意到它在滚动时丢了一些帧。我运行了 systrace(在运行 4.3 的 Nexus 4 上)并注意到输出中有一个 interesting section

一开始一切都很好。 Zooming in on the left section,我们可以看到绘图在每个 vsync 上开始,并在空闲时间结束,并等待到下一个 vsync。由于它是三重缓冲的,因此它应该被绘制到一个缓冲区中,该缓冲区将在完成后发布到以下 vsync 上。

在放大的屏幕截图中的第 4 次垂直同步中,应用执行了一些工作,但绘制操作没有及时完成下一次垂直同步。但是,我们不会丢弃任何帧,因为之前的抽签是在前一帧进行的。

发生这种情况后,绘制操作并不能弥补错过的垂直同步。相反,每个 vsync 只启动一个绘制操作,现在它们不再向前绘制一帧了。

Zooming in on the right section,该应用程序做了更多工作,但错过了另一个垂直同步。因为我们没有在前面画一个框架,所以实际上这里丢掉了一个框架。在此之后,它会返回到前面绘制一帧。

这是预期的行为吗?我的理解是,如果您错过了一次垂直同步,三重缓冲可以让您恢复,但这种行为看起来每错过两次垂直同步就会丢帧一次。


跟进问题

  1. this screenshot 的右侧,应用实际渲染缓冲区的速度比显示器消耗缓冲区的速度要快。在 performTraversals #1 期间(在屏幕截图中标记),假设正在显示缓冲区 A,正在渲染缓冲区 B。 #1 在 vsync 之前很久就完成了,并将缓冲区 B 放入队列中。此时,应用程序不应该能够立即开始渲染缓冲区C吗?相反,performTraversals #2 直到下一次 vsync 才开始,浪费了这之间的宝贵时间。

  2. 与此类似,我对 waitForever on the left side here 的需求感到有点困惑。假设缓冲区 A 正在显示,缓冲区 B 在队列中,缓冲区 C 正在渲染。当缓冲区 C 完成渲染时,为什么不立即将其添加到队列中?相反,它一直等待直到缓冲区 B 从队列中移除,此时它添加缓冲区 C,这就是为什么无论应用程序渲染缓冲区的速度有多快,队列似乎总是保持大小为 1。

    李>

【问题讨论】:

    标签: android performance systrace


    【解决方案1】:

    仅当您保持缓冲区满时,提供的缓冲量才有意义。这意味着渲染速度比显示器消耗它们的速度快。

    标签没有出现在您的图像中,但我猜绿色垂直同步行上方的紫色行是 BufferQueue 状态。您可以看到它通常随时都有 0 或 1 个完整缓冲区。在“左侧放大”图像的最左侧,您可以看到它有两个缓冲区,但之后它只有一个,在屏幕的 3/4 处您会看到一个非常短的紫色条表示它只是几乎及时渲染了帧。

    有关背景信息,请参阅 this postthis post

    更新添加的问题...

    the other post 中的细节几乎没有触及表面。我们必须更深入。

    systrace 中显示的 BufferQueue 计数是排队缓冲区的数量,即其中包含内容的缓冲区的数量。当 SurfaceFlinger 抓取一个缓冲区进行显示时,它会立即释放缓冲区,将其状态更改为“空闲”。当缓冲区显示在叠加层上时,这尤其令人兴奋,因为显示是直接从缓冲区渲染的(而不是合成到暂存缓冲区并显示它)。

    让我再说一遍:显示器正在从中读取数据以显示在屏幕上的缓冲区在 BufferQueue 中被标记为“空闲”。缓冲区有一个关联的栅栏,最初是“活动的”。当它处于活动状态时,不允许任何人修改缓冲区内容。当显示器不再需要缓冲区时,它会向围栏发出信号。

    所以跟踪左侧的代码位于waitForever() 中的原因是它正在等待围栏发出信号。当 VSYNC 命中时,显示切换到不同的缓冲区,向栅栏发出信号,您的应用程序可以立即开始使用缓冲区。这消除了如果您必须等待 SurfaceFlinger 唤醒、查看缓冲区不再使用、通过 BufferQueue 发送 IPC 以释放缓冲区等所产生的延迟。

    请注意,对waitForever() 的调用仅在您没有落后(跟踪的左侧和右侧)时才会显示。当队列只有 1 个完整缓冲区时,我不确定为什么会发生这种情况——它应该使最旧的缓冲区出队,这应该已经发出信号。

    最重要的是,您永远不会看到 BufferQueue 超过 2 来进行三重缓冲。

    并非所有设备都按上述方式工作。 Nexus 7 (2012) 不使用“显式同步”机制,并且 pre-ICS 设备根本没有 BufferQueues。

    回到您的编号屏幕截图,是的,在“1”和“2”之间有很多时间,您的应用可以运行 performTraversals()。如果不知道您的应用程序在做什么,很难确定,但我猜您有一个Choreographer 驱动的动画循环,它会唤醒每个 VSYNC 并且确实可以工作。它不会比这更频繁地运行。

    如果您 systrace 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 line 112)。旧接口不将栅栏作为“out”参数,因此调用必须调用waitForever() 本身。较新的接口只是将栅栏返回给 GL 驱动程序,它会在需要时等待(可能不会马上)。

    更新 3:现在可以使用更长的解释here

    【讨论】:

    • 感谢您的回答!链接的帖子非常有用,因为知道 systrace 显示缓冲区队列。我仍然有几个问题(在上面添加)。
    • 感谢您提供的所有信息!没错,systrace sn-p 是在 ListView 投掷期间,从阅读源代码来看,AbsListView 在投掷期间使用 postOnAnimation() 来刷新自身。我仍然需要弄清楚最好的方法是解决我落后后需要赶上的问题,但知道原因真的很有帮助。再次感谢!
    猜你喜欢
    • 2020-05-23
    • 1970-01-01
    • 1970-01-01
    • 2019-04-02
    • 1970-01-01
    • 2013-07-20
    • 2017-08-29
    • 1970-01-01
    相关资源
    最近更新 更多