扫描,撕裂和垂直同步 - VSync 技术实现

我们来上一节历史课,回顾显示技术的发展史。故事要从人类最早的显示技术:CRT 说起。

CRT

CRT: Cathode Ray Tube,阴极射线管,更常见的称呼即是「显像管」,常见于「大屁股电视/显示器」。

黑白电视

CRT 的原理看起来很简单:

  1. 阴极射线管不断的发射电子
  2. 电子击中屏幕上的荧光材料,被击中的位置即发亮
  3. 由偏转线圈施加的磁场控制击中哪一个位置

CRT 扫描
图片来自 Captain Disillusion 视频: CD / Interlacing,版权系原作者所有

就这样,线圈不断控制电子偏转方向,从左到右,完成一排;从上到下,完成一张。从而绘出图像。而连续不断的扫描图像就会让人感觉到动态效果。这也是为什么视频/电影也被称为 moving pictures(移动的图片),每一张「图片」都被称为一「帧(Frame)」。PAL 标准采用 25Hz 的刷新率,后来也产生了 30Hz, 60Hz, 100Hz 刷新率的 CRT 显示器。

彩色电视

把黑白电视的荧光发光点从单个切换成三个,每个对应三原色:红、绿、蓝,即可实现。

彩色实现
图片来自 Captain Disillusion 视频: CD / Interlacing,版权系原作者所有

现代显示器

刷新率

CRT 的「扫描」思路仍然适用于现代显示器,不论是 LCD 还是 OLED,亦或是其他技术,都是「逐行扫描」,每秒扫描若干个周期。而这个计数,就是「刷新率(Refresh Rate)」,单位为「赫兹(Hz),1Hz = 一次/秒」。

比如某台显示器,每秒扫描 30 次,那么它的刷新率就是 30Hz. 如今常见的显示器通常都是 60Hz 的刷新率,即每秒扫描 60 帧。

通常显示器的刷新率是恒定的,如 60Hz 刷新率的显示器始终是以 60 帧/秒的速度更新屏幕上显示的内容。

帧率

显示器需要信号源,让它知道应该显示什么,才能显示内容。在 PC 上,信号源就是显卡(GPU)。「帧率(Frame Rate)」是显卡绘制/生成帧的速度,单位为「FPS(Frames Per Second, 帧每秒)」。

帧缓冲区

通常,显示器是一台独立工作的设备,状态与显卡无关,会以恒定不变的频率从某个「池」里面读取画面,以保证稳定的图像输出。

与此同时,显卡也会往这个「池」里写画面,以供显示器读取。

这个「池」叫「Framebuffer(帧缓冲区)」。一张显卡通常有 2 个帧缓冲区:主/副(Primary/Secondary)缓冲区(也称前/后缓冲区(Front/Back)),由数据选择器(Multiplexer)选择连接到显示器的缓冲区。

连接到显示器的缓冲区总是主缓冲区,显示器从中读取图像内容;
未连接到显示器的缓冲区总是副缓冲区,显卡向其中写入渲染好的内容。

当副缓冲区渲染完成后,数据选择器即切换连接,把最新的画面提供给显示器。显卡总是会尝试以最快的速度渲染内容,每完成一帧渲染即切换主/副缓冲区,与显示器的工作状态完全无关。

从显卡到屏幕

有了这三个概念,我们就可以构造出每一帧从显卡到屏幕的流程图了。

假设我们有一张强劲的显卡,或是很轻的渲染负载,显示器的刷新率为 50Hz,显卡渲染的帧率是 200FPS,那么渲染流程就像这样:

输出流程图

注意:这是 WDDM 1.3 以前的简化设计,在更新版本里,设计可能有变化。

数学题:显示器完全绘制一帧需要多长时间?1 秒 = 1000 毫秒,每秒绘制 50 帧,绘制一帧需要 1000/50 = 20 毫秒。那么显卡完全渲染一帧需要多长时间?5 毫秒。

我们把注意力放在显示器绘制 1 帧的这 20 毫秒里。

在这 20 毫秒里,显示器从连接到的帧缓冲区里逐行读取像素,显示到屏幕中。然而显卡渲染的速度超过了显示器绘制的速度,就像这样:

25% - 显示器刚渲染了 5 毫秒,显卡即在副缓冲区完成了渲染,切换了连接到显示器的帧缓冲区。
50% - 5 毫秒以后,显卡又一次切换了帧缓冲区。
75% - 显卡再次切换帧缓冲区。

最后,20 毫秒过去了,显示器终于完成了整个屏幕的绘制,但这张画面却来自于 4 个不同的帧缓冲区,即由 4 个不同的帧组成,这就造成了「撕裂(Tearing)」。画面移动越快(帧与帧间差别越大),撕裂现象越明显。

撕裂现象模拟

应对撕裂问题

撕裂问题有若干解决方案:垂直同步和 AMD FreeSync/NVIDIA G-Sync 等等。

垂直同步

垂直同步(Vertical Synchronization, VSync),顾名思义,是为了同步显卡和显示器读取/写入帧缓冲区步伐的技术,以使画面「同步」起来,避免撕裂现象。

在上面我们知道了,撕裂的原因在于:显卡在显示器还来不及完成渲染的时候,切换了帧缓冲区。要解决这个问题,就要压制显卡的渲染速率,使显卡的帧缓冲区切换行为与显示器的帧绘制保持同步。

当垂直同步启用的时候,显卡并不会按照「越快越好」的渲染策略进行渲染,而是等待显示器的信号。当显示器请求刷新时,再切换帧缓冲区,使显示器能安心完成每一帧的绘制。

我们回到刚刚那 20 毫秒:

25% - 显示器渲染了 5 毫秒,显卡已经完成副缓冲区的渲染,进入闲置状态。
100% - 显示器绘制完成,请求显卡切换帧缓冲区。
此时,显卡才切换帧缓冲区,并开始进行下一帧的渲染。

垂直同步的局限 - 输入延迟

由于显卡不能及时将最新的画面呈现出来,而是需要等待显示器请求刷新。如果画面变动的速度很快,就不能及时呈现出来。

比如 CS:GO 这种竞技类游戏,本身产生的负载就不高,中高端显卡通常能达到 200FPS 以上的渲染帧率(5ms/帧),而假设是 50Hz 的显示器,刷新一次需要 20ms,那么就有 3 个帧的信息被完全抛弃了,玩家需要等待 20ms 才能观察到动作的反馈。

而在垂直同步关的情况下,这些丢失的帧能被第一时间显示出来,即使是组成撕裂的图像,玩家也能第一时间得到输入反馈。

垂直同步的局限 - 低帧率损失

我们刚刚一直在讨论显卡渲染速度大于显示器绘制速度的情况,那么如果突然负载增大,显卡渲染速度反而更慢了呢?

我们仍然利用那 20 毫秒的例子,但这次,显卡渲染的帧率是 40FPS,即每帧渲染耗时 25ms。

当垂直同步关闭:

100% - 显示器完成绘制初始帧。
0 - 25% - 这 5ms 内,显卡未切换帧缓冲区,显示器仍然在渲染上一帧。
25% - 显卡完成第二帧渲染(距离开始已过 25ms),切换帧缓冲区。

显卡在显示器绘制过程中切换了帧缓冲区 → 画面撕裂。

但如果我们开启垂直同步呢?

同样的,显卡在切换帧缓冲区的时候需要等待显示器的信号:

100% - 显示器完成绘制初始帧,请求切换帧,但显卡并没有完成下一帧渲染 —— 重复绘制同一帧。
25% - 显卡完成第二帧渲染,进入闲置状态,等待显示器请求。
200% - 显示器完成第二帧渲染,请求切换帧。
此时,显卡才切换帧缓冲区,并开始进行下一帧但渲染。

发现了吗,在显卡帧率比显示器刷新率低时,这种压制仍然存在。

我们假设:
显卡渲染速度为 40FPS (25ms/frame)
显示器刷新速度为 50Hz (20ms/frame)

渲染时间轴

在图例中,实际帧率被限制为了显示器刷新率的一半。

克服垂直同步问题 - 自适应垂直同步

NVIDIA 推出了「自适应垂直同步(Adaptive VSync)」功能,原理非常简单:

  1. 帧率大于显示器刷新率时,启用垂直同步。
  2. 帧率小于显示器刷新率时,禁用垂直同步。

这样做,虽然低帧率时画面撕裂问题会重现,但不会发生帧率额外损失问题。属于帧率重于画面撕裂问题的方法。

克服垂直同步问题 - 三重缓冲

我们刚刚发现,低帧率的时候,显示器将遇到显卡无法提供新图像的情况,导致帧率在原本的基础上进一步降低。

而某些情况下,大部分时间里,渲染帧率都能稳定在较高水平,但会偶尔出现突然的短暂帧率下跌。

三重缓冲试图用「未雨绸缪」的方法解决这个问题:增加一个额外的帧缓冲区,当显卡输出突然跟不上显示器时,将有一个额外的帧可以提供给显示器以及时更新画面,此后再跟上显示器刷新的步伐。

终极方案:软硬兼施

到这时你可能发现,无论如何,显示器就像大爷一样,它的刷新率永远不变,无论如何都要显卡去伺候它,但是……

也许……要是让显示器的刷新率和显卡的帧率在任何时候都保持同步呢?

这就是 AMD FreeSync / NVIDIA G-Sync 想做的事情:可变刷新率(Variable Refresh Rate, VRR),从根源上消灭撕裂问题。它们让显示器的刷新率和显卡的帧率随时保持同步,不仅消灭了撕裂问题,而且不会引入传统垂直同步带来的输入延迟和帧率损失问题。

唯一的问题是:

AMD 秉持着一贯的良心:开放 FreeSync 标准,任何人都可以做自己的 FreeSync 实现。但 G-Sync 则也是老黄传统艺能:封闭标准。这也是为什么市面上不少显示器支持 FreeSync 但不支持 G-Sync.

最后,当然是:

AMD YES!
NVIDIA, F* YOU!