9

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

一开始一切都很好。放大左侧部分,我们可以看到绘图在每个 vsync 上开始,并在剩余时间结束时完成,并等待下一个 vsync。由于它是三重缓冲的,它应该被绘制到一个缓冲区中,该缓冲区将在完成后发布到以下 vsync 上。

在放大的屏幕截图中的第 4 个 vsync 上,应用程序做了一些工作,并且绘制操作没有及时完成下一个 vsync。但是,我们不会丢弃任何帧,因为之前的抽签是在前一帧进行的。

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

放大右侧部分,该应用程序做了更多的工作并且错过了另一个垂直同步。因为我们没有在前面画一个框架,所以实际上这里丢掉了一个框架。在此之后,它会回到前面绘制一帧。

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


跟进问题

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

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

4

1 回答 1

10

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

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

有关背景,请参阅这篇文章这篇文章

更新添加的问题...

另一篇文章中的细节几乎没有触及表面。我们必须更深入。

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

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

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

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

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

并非所有设备都按上述方式工作。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 第 112 行)。旧接口不将栅栏作为“out”参数,因此调用必须调用waitForever()自身。较新的接口只是将栅栏返回给 GL 驱动程序,它会在需要时进行等待(可能不会立即等待)。

更新 3:现在可以在此处获得更长的解释。

于 2014-04-30T14:59:18.027 回答