3. 显示刷新原理

双缓冲这个名字,听起来好像挺唬人。其实说白了,就是准备两个画板。

一个画板给 GPU 或 CPU 往上面画图,另一个画板给屏幕去显示。两个画板轮流干活,互不干扰。我最早接触这个概念是在做一款工业手持设备的时候,那时候单缓冲刷屏,屏幕老是闪,客户投诉说眼睛都快晃瞎了。嗯,从那以后,双缓冲就成了我 GUI 项目里的标配。

3.1 前台缓冲与后台缓冲:两个画板的故事

先搞清楚两个缓冲的角色:

  • 前台缓冲(Front Buffer):屏幕正在读取并显示的那个缓冲区。它里面的内容,就是用户当前看到的画面。
  • 后台缓冲(Back Buffer):应用程序正在往里面绘制新内容的缓冲区。用户看不见它,它是个“幕后工作者”。

你想想看,如果没有后台缓冲,你直接在屏幕上画图,画到一半的时候,用户看到的就是半幅画面——上面一半是新图,下面一半还是旧图。这就是撕裂(Tearing)的根源。

核心原则:前台缓冲只读,后台缓冲只写。两者通过交换操作切换角色。

我在项目中遇到过一种情况:有些低端 MCU 没有硬件双缓冲支持,只能靠软件模拟。那时候我就用两个内存块,一个标记为“显示中”,一个标记为“绘制中”。绘制完成后,通过修改指针指向来完成切换。虽然效率不如硬件,但至少不闪了。

3.2 交换(Swap)机制:怎么让两个画板换岗

交换机制,就是让前台缓冲和后台缓冲互换身份。但这里有个关键问题:什么时候换?

常见的交换方式有两种:

交换方式 触发时机 优点 缺点
立即交换 绘制完成后立即执行 响应快,延迟低 容易产生撕裂
垂直同步交换 等待屏幕垂直消隐期 画面完整,无撕裂 可能增加延迟

交换的本质,其实不是把数据从一个缓冲区拷贝到另一个缓冲区。那样太慢了。真正的做法是交换指针——让显示控制器指向新的缓冲区地址,同时把旧的缓冲区地址交给绘制程序。

// 伪代码:双缓冲交换
void swap_buffers() {
    // 等待垂直同步(如果开启)
    wait_for_vsync();
    
    // 交换指针
    uint32_t *temp = front_buffer;
    front_buffer = back_buffer;
    back_buffer = temp;
    
    // 通知显示控制器使用新的前台缓冲
    display_set_buffer(front_buffer);
}

这段代码看起来简单,但实际项目中坑不少。我曾经在一个项目里忘了加 wait_for_vsync(),结果画面撕裂得一塌糊涂。后来加上了,世界清净了。

小技巧:如果你的平台支持双缓冲,尽量使用硬件提供的交换接口,比如 Linux 的 drmModePageFlip() 或 Android 的 eglSwapBuffers()。自己手写指针交换,容易踩内存屏障的坑。

3.3 VSync(垂直同步)的作用:为什么你的画面会撕裂

VSync,全称 Vertical Synchronization,垂直同步。它的作用就是让交换操作发生在屏幕的垂直消隐期。

什么是垂直消隐期?

屏幕刷新是一行一行从上往下扫的。扫完最后一行后,电子枪需要从右下角回到左上角,准备扫下一帧。这个回扫的时间窗口,就是垂直消隐期。在这段时间里,屏幕不显示任何内容,你做什么操作都不会被用户看到。

如果没有 VSync,交换可能发生在屏幕正在扫描中间的时候。比如屏幕刚扫完上半部分,你突然把缓冲换了,下半部分显示的就是新一帧的内容。结果就是:上半部分是旧画面,下半部分是新画面——撕裂。

注意:开启 VSync 虽然能消除撕裂,但也会引入额外的延迟。如果你的应用对延迟极其敏感(比如游戏、触摸笔迹),需要权衡是否开启。我做过一个手写板项目,开启 VSync 后笔迹延迟明显,用户写起来感觉“跟不上手”。最后我们关闭了 VSync,改用三重缓冲来缓解撕裂。

3.4 双缓冲工作流程全景图

下面这张图,把整个流程串起来了。我建议你多看几遍,理解每个环节的时序关系。

双缓冲工作机制流程图 双缓冲工作机制流程图 后台缓冲 应用程序绘制新内容 绘制完成 触发交换请求 等待 VSync 是否在垂直消隐期? 否 → 等待;是 → 继续 交换指针 前台 ↔ 后台 前台缓冲 屏幕读取并显示 后台缓冲(原前台) 清空,准备下一帧 循环:下一帧绘制 💡 关键时序点 1. 绘制完成后,不立即交换,而是等待 VSync 信号。 2. 交换只改指针,不拷贝数据,速度极快。 3. 交换后,原前台缓冲变为后台缓冲,内容被清空或覆盖。

从这张图里你能看到,整个流程是一个闭环:绘制 → 等待 VSync → 交换指针 → 显示 → 清空后台 → 继续绘制。每一步都有它存在的理由。

3.5 实际项目中的避坑指南

讲到这里,我分享几个实际踩过的坑:

  • 交换频率不要超过屏幕刷新率:我曾经在一个 60Hz 的屏幕上,以 100fps 的频率交换缓冲。结果呢?很多帧根本没被显示,白白浪费了 CPU。后来我加了帧率限制,让交换频率和屏幕刷新率对齐。
  • 注意内存对齐:有些显示控制器要求缓冲区地址必须按 4 字节或 8 字节对齐。不对齐的话,显示会花屏。我遇到过一次,排查了整整两天才发现是地址没对齐。
  • 多缓冲区的选择:双缓冲是最基础的方案。如果你的系统内存够大,可以考虑三重缓冲——三个缓冲区轮流用,能进一步降低延迟。但嵌入式系统内存通常紧张,双缓冲已经够用。

我的习惯:在项目初期,我会先跑一个双缓冲的基准测试,测量交换耗时、帧率、撕裂情况。有了这些数据,再决定要不要上 VSync 或三重缓冲。不要一开始就追求最复杂的方案,够用就好。

双缓冲机制,说白了就是“空间换时间”的典型应用。多花一份内存,换来流畅无撕裂的画面。在嵌入式 GUI 里,这笔买卖很划算。