GUI 渲染延迟

Posted on February 17, 2023

鼠标点击窗口标题栏, 然后快速拖动. 如果窗口和鼠标紧紧贴到一起, 那么恭喜你, 你的OS并没有GUI渲染延迟问题. 但是, 经过实验后, 各位想必都会发现, 往往是鼠标指针先到达, 然后窗口才会跟过去. 窗口的移动似乎总是延后于鼠标指针. 如果各位是从旧时代过来的人, 应该还记得, 早期的 WinXP, 窗口拖到是非常跟鼠标的.

没有渲染延迟问题的意思是, UI 的反应总是在下次屏幕刷新的时候出现. 也就是最多延迟为一帧, 最少可以恰好没延迟. 平均延迟时间为 0.5帧.

我们设想屏幕为班车, 如果设定刷新率为 60hz, 就是每 1/60 s 就发车. 而用户的输入, 就像随机到站的乘客.
如果到站后, 总是能乘坐下一趟车, 那么就可以认为 UI 没有延迟. 虽然总体上是平均延迟了 0.5帧, 但是本篇
不考虑总延迟, 只考虑 "错过" 班车的情况. 因此我以 "错过" 班车的数量来定义延迟. 因为鼠标指针的绘制,
是独立的路径, 因此鼠标指针永远不会错过班车. 但是, 窗口的内容绘制, 可能会错过班车. 所以可以以拖动窗口
来观察渲染延迟.

如今不跟了, 主要原因, 其实是现在普遍使用的桌面混成功能导致的. 在非混合时代, 窗口的内容是直接对应显卡上的屏幕输出缓冲区. 而在桌面混成时代, 所有的窗口都有一个独立的缓冲区. 由混成器 将这些图片合成到屏幕上. 也就是说, 要将大量的图片, 合成为一张图片.

而混成器, 为了避免屏幕撕裂, 必然会打开垂直同步. 可是, 打开垂直同步, 为何会导致错过班车呢? 这就不得不说到目前的 UI 库了. 目前 gui 程序使用的 UI 库本身, 也是打开了垂直同步的.

于是, GUI 程序更新窗口内容的时候, 会等待下一班车, 虽然他们绘制足够快的话, 看起来也总是能搭上最后一趟. 但是, 关键在于, 他们的班车, 也是 1/60 s 发车, 但是目的地却不是屏幕, 而是把人载到合成器的车站里. 等待合成器的下一趟班车. 由于 GUI 的班车和 屏幕的班车是同时发车的, 所有他们到达屏幕车站后, 必然只能等下一趟.

于是, 所有的 GUI 内容更新, 一定一定会错过一帧.

也就是, GUI 内容的更新, 平均延迟时间是 1.5帧. 这还是在 GUI 程序绘制时间为 0 的情况下. 如果绘制时间超过 1/60s , 则延迟时间还要上升到 2.5帧.

让问题更雪上加霜的是, 现在的显卡驱动, 即便你只要求打开 vsync ( 也就是使用双缓冲 ), 驱动都会在内部实现里, 偷偷给你搞成 三缓冲. 于是这混成器 和app两边各增加一帧延迟.

最终 GUI 程序的平均延迟时间就达到了恐怖的 3.5 帧了, 也就是 60hz 刷新率的条件下, 平均延迟 60ms. 当 8ms 延迟的鼠标指针喷上 60ms 延迟的窗口内容, 自然就能很明显的分辨出窗口的迟滞了.

这些延迟, 对所有开启混合功能的 OS 平台都是适用的.

其中, 早期, 各大平台的策略是对全屏显示的窗口关闭混合功能. 以消除混合功能带来的延迟对游戏的影响.

到了 wayland 时代, 由于混成功能是必需品, 所以 wayland 有更大的必要去解决混成导致的延迟. wayland 提出的方法则是 frame callbacks. 除了混成器需要等待 vblank 信号, 其他 app, 使用 frame callback 来确定什么时候上车. 这样保证 app 的车到站后, 恰好遇到 混成器发车. 于是解决错过班车导致的延迟问题.

但是很可惜, frame callback 机制下, 混成器给app发信号的时机点选择非常重要. 只有恰到好处的时间点, 才能同时兼顾流畅性和低延迟. KDE 直到 5.21 才解决好. gnome 则还在 Work In Progress 中.

不过 KDE 最新版本都到 5.27 啦, 所以其实 KDE 早就解决了. 嘿嘿.

Comments