F896c00ae945868abda42e6547788e4a
Metal【12】—— Triple Buffering

前段时间忙自己的事情,挺久没有更新了,见谅~

之前群里,有朋友在问 “什么是 Triple Buffer”,我相信,不少朋友同样存在这个疑惑。

那么今天,我们就具体来聊聊 Triple Buffer。

PS:

订阅后的朋友,可以加我微信:wxidlongze,拉你进群。交流,扯淡,学习资源分享~

最后,源码在文末~

那么,开始吧~


1. 预备知识

我们简单看下上图,它描述了主流图形系统 (Graphics APIs,e.g. Metal,OpenGL..)的大致处理过程。

其中,CPU 上执行的 encodes commands 操作,我们一般称为 encoding。这个阶段,主要是 CPU 准备指令相关的数据。比如上图中的 CPU writes to vertex buffer。

GPU 上执行的 executes commands 操作,我们一般称为 rendering。这个阶段,主要是GPU 执行相关指令。比如上图中的 GPU reads from vertex buffer。

一次 CPU encoding 操作开始,到 GPU rendering 操作结束,这个过程,我们用 in-flight 来描述。

在上图的渲染过程中,任意一个 frame 的 encoding 要等前一个 frame 的 rendering 结束后才能开始,这也是正常情况下的处理方式。

所以任意时刻最多存在一个 in-flight,这样 CPU 和 GPU 都无法避免空闲等待的状态,即上图中的 idle 过程。

而 Triple Buffer 的引入,就是要尽可能的减少空闲时间,使得 CPU 和 GPU 最大程度的被利用起来,从而更快的完成任务、实现更流畅的效果。

下面,我们看看 Triple Buffer 是怎么做到的。


2. Double Buffer

在讲 Triple Buffer 之前,我相信大多数朋友都听过 Double Buffer,或者双重缓存(缓冲)区这个概念。

在 OpenGL ES 或者 iOS 中,我们一直看到这样的描述:iOS 设备会始终使用双缓存,并开启垂直同步

所以,Triple Buffer 和 Double Buffer 之间,是不是仅仅多了一个 Buffer 而已?

实际上并不是,他们的定义、具体实现,是有本质区别的。

  • Double Buffer 里面的 Buffer,指的是 frame buffer,即存储 rendering 结果的区域。
  • Triple Buffer 里面的 Buffer,则是指 parameter buffer,即存储数据的区域(比如 Metal 里的 MTLBuffer)。

先划重点:

Triple Buffer 涉及 frame buffer,parameter buffer 以及基于 semaphore 的 CPU-GPU 同步方式。它能实现真正意义上的 Triple in-flight,减少空闲时间。

而 Double Buffer 则无法严格减少等待时间。

那么,他们的区别是什么呢?

我们从最初的问题说起。

我们知道,屏幕中需要显示的内容都会被放在一个称为显示缓存的地方,我们一般称为前端缓存(Front Frame Buffer),它是一块能直接映射到显示器的内存区域。

通常情况下,我们只有一个这样的缓冲区,也就是单缓冲,在单缓冲中任何绘图的过程都会被显示在屏幕中。

这种模式下,我们会经常遇到这么两个问题:场景的闪烁撕裂

闪烁主要是由于多重绘制操作造成的,假如我们一个最终的效果,需要很多绘制步骤才能完成,但都是在同一个 framebuffer 上进行,导致这些绘制过程,直接呈现在了屏幕上,所以会产生画面的闪烁变化。

至于撕裂的话,静态的图片一般来说没什么问题,但如果是高频运动的物体,问题就来了。

因为只有一个缓存,显示器在刷新的时候也在读取这块缓存,用户更新画面的时候也在写入这块缓存,一个读一个写,并且读写都需要一些时间,这样就会出现显示错误。

我们举个具体的例子:

有个小球,从左向右做高速的运动。

在这个例子里,GPU 当前已经完成了写入数据到这个 framebuffer(左边的完整图形),这时候 CRT 的电子枪开始从上至下扫描,假如当前已经扫描了一半,屏幕上就会显示出上半部分的内容,如下图所示:

但是,如果 GPU 的刷新频率大于 CRT 的,即这时候这个 framebuffer 的数据已经被变更,如下图左边所示,向右偏移了。这时候 CRT 自身不知道,它继续向下扫描绘制,这时候撕裂就产生了。如下所示:

针对上面两个问题,引入了双缓存的概念。

简单来说,就是在内存中另外创建一块区域,其格式和大小都与前端缓存完全相同,并在其上进行所有的绘制操作,这一块区域就是后备缓存(Front Frame Buffer)

由于是在一个离屏(off-screen)的缓存上进行绘制,因而在绘制的时候屏幕上不会发现变化,故而能避免闪烁问题,也能很大程度规避撕裂问题。

注意这里是很大程度,而不是完全。

正常情况下,GPU 会预先渲染好一帧放入一个缓存内,当显示器要显示画面的时候,视频控制器就会读取它。当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓存。

但是当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓存并把两个缓存进行交换后(即,显示器的刷新和前后缓存的拷贝交换同时进行),视频控制器就会把新的一帧数据的下半段显示到屏幕上,同样不可避免会产生撕裂现象。

为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟(掉帧)。

这篇文章想必大家都不陌生,还没看过的强烈建议看看哈。

iOS 保持界面流畅的技巧

至此,我们提到了为什么需要 Double Buffer 以及和 Double Buffer 相关的一些技术实现。

讲到这里,其实不难发现,这其实是一个利用空间和时间,来保证数据完整性的一个过程。

双缓存机制必须要求有比单缓存更多的显示内存和消耗时间,因为后备缓存需要显示内存,而复制操作和等待同步需要时间。

所以,关于我们最初提到的那个问题,减少空闲时间,使得 CPU 和 GPU 最大程度的被利用起来,其实它的作用是非常局限的。

下面,我们看看 Triple Buffer 是怎么做到的。


3. Triple Buffer

为了减少空闲时间,我们很容易想到,让 CPU 和 GPU 持续工作,如下:

但是在单个 vertex buffer 的情况下,就会导致读写冲突,即 CPU 在写数据到 vertex buffer 的过程中,GPU 也从这个 vertex buffer 中拿数据,导致发生不可控的错误。

为了避免这一情况,我们可以引入 multiple buffer,即 GPU 在读一个 vertex buffer 数据的时候,CPU 同时向另一个 vertex buffer 中写入数据,使得 CPU 和 GPU 之间能并行工作,打破彼此的相互依赖、等待,如下所示:

这种工作模式,就是 Triple Buffer 的核心思想。

总结来说,就是:

top Created with Sketch.