编辑注:这篇文章的早期版本出现在 Mason Chang 的个人博客上。
在过去的几个月里,我一直在致力于丝绸项目,该项目旨在提高整个浏览器的流畅度。 就像Android 的黄油计划一样,其中一部分终于在 Firefox OS 上上线了。 丝绸项目主要做三件事
- 将绘制与硬件垂直同步对齐
- 根据硬件垂直同步对触摸输入事件进行重采样
- 将合成与硬件垂直同步对齐
什么是垂直同步,为什么要使用垂直同步,以及它为什么重要?
垂直同步 (vsync) 发生在硬件显示器在屏幕上显示新帧时。 这个速率由特定硬件设置,但在美国的多数显示器以每秒 60 次的速度运行,即每 16.6 毫秒 (毫秒) 一次。 这就是你听到每秒 60 帧的说法,硬件显示器刷新一次就有一帧。 这在现实中意味着,无论软件中产生了多少帧,硬件显示器每秒最多只能显示 60 帧。
目前,Firefox 模拟每秒 60 帧,并通过一个软件计时器来实现垂直同步,该计时器每 16.6 毫秒调度一次渲染。 然而,软件调度器有两个问题:(a)它很嘈杂,(b)它相对于垂直同步可能在不合适的时机进行调度。
关于噪音,软件计时器比硬件计时器要嘈杂得多。 这会造成微型卡顿,原因有很多。 首先,许多动画都是基于软件调度器生成的用于更新动画位置的时间戳。 如果你曾经使用过requestAnimationFrame,你就会从软件计时器那里获得一个时间戳。 如果你想要平滑的动画,提供给 requestAnimationFrame 的时间戳应该是一致的。 不一致的时间戳会导致不一致和卡顿的动画。 下面是一个图表,显示了软件与硬件垂直同步计时器的均匀性
哇! 使用硬件计时器后,改进很大。 我们得到了更均匀,因此更平滑的时间戳,可以用来键控动画。 这样就解决了问题 (a),软件与硬件中的计时器噪音问题。
对于问题 (b),软件计时器可能会在相对于垂直同步的错误时机进行调度。 无论软件做什么,硬件显示器都会按照自己的时钟刷新。 如果我们的渲染管道在下一个垂直同步之前完成了帧的制作,显示器就会用新内容进行更新。 如果我们没有在下一个垂直同步之前完成帧的制作,就会显示前一帧,从而导致卡顿。 有些渲染函数可能在接近垂直同步时执行,并溢出到下一个时间间隔。 因此,我们实际上引入了更多的潜在延迟,因为帧在下一个垂直同步之前不会显示在屏幕上。 让我们通过图形来观察一下
在时间 0,我们开始制作帧。 例如,假设所有帧的制作时间都是 10 毫秒。 我们的帧预算为 16.6 毫秒,因为我们只需要在下一个硬件垂直同步发生之前完成帧的制作。 由于帧 1 在下一个垂直同步之前 6 毫秒完成(时间 t=16 毫秒),所以一切都顺利,生活很美好。 帧及时制作,硬件显示器会用更新的内容进行刷新。
现在让我们看一下帧 2。 由于软件计时器很嘈杂,我们在下一个垂直同步之前的 9 毫秒开始制作帧(时间 t=32)。 由于我们的帧制作需要 10 毫秒,所以我们实际上在下一个垂直同步之后的 1 毫秒完成了帧的制作。 这意味着在垂直同步编号 2(t=32)时,没有新帧可供显示,所以显示器仍然显示前一帧。 此外,刚刚制作的帧直到垂直同步 3(t=48)才会显示,因为那是硬件更新自身的时候。 这会造成卡顿,因为现在显示器会跳过一帧,并尝试在接下来的帧中进行弥补。 这还会造成一帧额外的延迟,这对游戏来说是不可取的。
垂直同步解决了这两个问题,因为我们得到了更均匀的计时器和更长的帧预算时间来制作新帧。 现在我们知道了什么是垂直同步,我们终于可以继续探讨丝绸项目是什么以及它如何帮助在 Firefox 中创建流畅的体验。
渲染管道
用非常简单的术语来说,Gecko 的渲染管道主要做三件事
- 在主线程上绘制/渲染新帧。
- 通过 LayerTransaction 将更新的内容发送到合成器。
- 合成新内容。
在理想情况下,我们可以在 16.6 毫秒内完成这三个步骤,但这在大多数情况下是不可能的。 步骤 (1) 和 (3) 都在独立的软件计时器上执行。 因此,这三个步骤之间没有真正的同步时钟,它们都是独立的。 它们也与垂直同步无关,因此管道的时间安排与显示器实际用内容更新屏幕的时间无关。 在丝绸项目中,我们将这两个独立的软件计时器替换成了硬件垂直同步计时器。 就我们而言,(2) 不会真正影响结果,但为了完整起见,这里也提到了它。
将绘制与硬件垂直同步对齐
将用于驱动刷新计时器与垂直同步对齐,可以通过两种方式创造流畅度。 首先,许多动画仍然是在主线程上完成的,这意味着任何使用时间戳设置动画位置的动画都应该更加平滑。 这也包括 requestAnimationFrame 动画! 另一个好处是,我们现在对渲染启动时间有了一个非常严格的顺序。 我们不再是在独立的同步偏移量上执行 (1) 和 (3),而是在特定时间启动渲染。
根据垂直同步对触摸输入事件进行重采样
在丝绸项目中,我们可以启用触摸重采样,这可以在跟踪你的手指时改善流畅度。 由于我已经在博客中介绍过触摸重采样,所以我在这里只说几句。 在丝绸项目中,我们终于可以启用它!
将合成与硬件垂直同步对齐
最后,丝绸项目的最后一部分是关于将合成与硬件垂直同步对齐。 合成器将所有绘制的内容合并在一起,形成你在显示器上看到的单幅图像。 在丝绸项目中,所有合成都在硬件垂直同步发生后立即开始。 这实际上带来了一项非常好的额外好处——这里显示的合成时间缩短了
在火焰设备的设备驱动程序中,有一个全局锁,在接近垂直同步间隔时会被获取。 这个锁可能需要 5-6 毫秒才能获取,这会大大增加合成时间。 然而,当我们在垂直同步后立即启动合成时,获取锁的争用很小。 因此,我们可以节省等待时间,从而大大减少合成时间。 我们不仅获得了更流畅的动画,还获得了更短的合成时间,从而延长了电池寿命。 这真是双赢!
有了这三个部分,我们现在对渲染管道有了很好的严格顺序。 我们在 16.6 毫秒内绘制并向合成器发送更新的内容。 在下一个垂直同步时,我们将合成更新的内容。 在接下来的垂直同步时,帧应该已经完成了渲染管道,并将显示在屏幕上。 保持这种顺序可以减少卡顿,因为我们减少了计时器在错误时机调度每个步骤的可能性。 在没有丝绸项目的当前实现的最佳情况下,一帧可以在单个 16.6 毫秒帧内完成绘制和合成。 这是很棒的。 然而,如果下一帧需要 2 帧才能完成,我们就造成了额外的卡顿,即使管道的任何阶段都没有真正变慢。 将整个管道对齐以创建事件的严格顺序,可以降低错误调度帧的可能性。
这是一张没有丝绸项目的渲染管道图。 我们在该配置文件的底部看到了合成器 (3)。 我们在中间看到了绘制 (1),其中可以看到样式、重排、显示列表和光栅化。 我们在顶部看到了垂直同步,用那些橙色的方块表示。 最后,我们在底部看到了层事务 (2)。 最初,当我们启动时,合成器和绘制是不对齐的,因此动画的位置会根据它们是在主线程还是合成器线程上而有所不同。 其次,我们看到合成器时间很长,因为合成器正在等待设备驱动程序中的全局锁。 最后,很难解读任何顺序,或者看到是否存在问题,除非你对事物为何/何时发生有深入的了解。
这是一张包含丝绸项目的相同管道图。 合成器时间短了一点,整个管道只在垂直同步间隔启动。 合成时间缩短了,因为我们在垂直同步间隔的精确时刻启动合成器。 现在清楚地表明了事物应该发生的顺序。 合成器和绘制都以相同的时间戳为键,确保动画更流畅。 最后,有一个明确的指示,只要所有操作都在下一个垂直同步之前完成,一切都会很流畅。
最终,丝绸项目的目的是在 Firefox 和 Web 上创造更流畅的体验。 许多人对该项目做出了贡献。 感谢 Jerry Shih、Boris Chou、Jeff Hwang、Mike Lee、Kartikaya Gupta、Benoit Girard、Michael Wu、Ben Turner 和 Milan Sreckovic 帮助实现丝绸项目。
23 条评论