双缓冲在PC和嵌入式屏幕上的区别与设计取舍

相关阅读:任务链、TE信号与双缓冲:GUI渲染同步的三层保障。

PC的双缓冲区模型

前缓冲区和后缓冲区都在显卡的显存里,GPU生成数据到一个缓冲区的同时,显示控制器会通过视频线(HDMI、DP等)实时把另一个缓冲区的像素流发送出去。

有的SoC带LCD控制器,它们的工作机制和PC是完全一样的: 帧缓冲在SRAM或者SDRAM里,显示控制器持续扫描输出。双缓冲就是在vsync中断里改一下帧缓冲地址寄存器。

屏幕驱动芯片自带显存的双缓冲区模型

这是部分嵌入式设备的模型。

这种情况下,屏幕驱动芯片会自己从显存(GRAM)往屏幕上扫描刷新,我们只需要负责往GRAM里写数据。

区别在于:

  1. GRAM通常只有一块:它实际上就是前缓冲,屏幕一直在读取。往里写的同时屏幕也在扫描读取,所以会撕裂。GRAM这一侧做不了双缓冲,因此不利用TE的话,撕裂无法避免。

  2. 交换变成拷贝:在上一种情况里,交换是瞬间完成的指针操作,在这里就要用DMA复制,速度会慢很多。 当DMA正在读一块缓冲区往屏幕发送的时候,这块缓冲区不能被修改,相当于前缓冲区,且DMA充当了显示控制器的角色。

  3. 同步靠TE:屏幕通过TE(Tearing Effect)信号告诉你它扫描到哪儿了。常见的做法是TE到来时开始写,并保证写入速度不被扫描线追上,或者反过来紧跟在扫描线后面写。

在一些RAM紧张的MCU上,常见的折中是不放整帧缓冲,而是用两块部分缓冲区(比如十分之一屏幕的缓冲区)轮流渲染和发送。LVGL就支持这种做法。

双缓冲的本质与交换

双缓冲的本质是让读数据的一方永远只看到一份完整的、不会再变的数据,不会读到正在写到一半的数据。

双缓冲的代价是要多一份内存占用,画面也会多一帧延迟。

交换操作是双缓冲区的关键一步,因为交换的时候需要停止对缓冲区的一切读写,所以我们希望交换尽可能快。 最常见的方法是交换缓冲区的指针或引用,但如果想复用缓冲区中已有的数据,就需要考虑到这些数据是2帧前的。 另一种做法是在缓冲区之间拷贝数据,适用于比如要求帧缓冲位于固定地址的系统上。这种做法交换更加耗时,不过数据是1帧以前的。

如果很多对象都持有一块数据,为了交换,需要遍历对象集合并交换,这可能导致交换操作更慢。一个更好的实现思路是实现两个函数 current() 和 next(),函数返回对应的缓冲区下标,只需要调用一个 swap(),就能修改函数的返回值,能规避耗时的遍历和大量交换。