Page Flip 机制:从用户态到硬件的最后一跃
双缓冲的核心,说白了就是「前台画布给你看,后台画布给我画」。但真正让这个机制跑起来的,是 Page Flip——也就是页面翻转。我最早接触这个概念时,以为就是改个指针地址那么简单。后来踩了坑才发现,这里面的门道,比想象中深得多。
drmModePageFlip:用户态的唯一入口
在 DRM 框架里,drmModePageFlip 是用户态程序用来触发翻转的标准接口。它的原型长这样:
int drmModePageFlip(int fd, uint32_t crtc_id, uint32_t fb_id,
uint32_t flags, void *user_data);
参数不多,但每个都很关键:
- fd:DRM 设备文件描述符,你打开
/dev/dri/card0拿到的那个。 - crtc_id:你要翻转哪个 CRTC(显示控制器)的扫描缓冲区。
- fb_id:新 framebuffer 的 ID,也就是后台画好了的那一帧。
- flags:控制翻转行为的标志位,后面细说。
- user_data:用户自定义数据,通常用来传一个事件回调的上下文。
调用这个函数后,内核会做两件事:
- 把 CRTC 的扫描指针指向新的 framebuffer。
- 在下次垂直消隐期(VBlank)到来时,硬件真正切换显示源。
嗯,这里要注意——翻转不是立即生效的。你调用完 drmModePageFlip,返回 0 只代表请求提交成功,不代表画面已经切过去了。真正的切换,要等下一个 VBlank。
drmModePageFlip 后,我都会检查返回值。如果返回 -EACCES,说明你还没设置好 CRTC 的 mode;如果返回 -EBUSY,说明上一次翻转还没完成。这两个错误我在项目里都遇到过,尤其是 -EBUSY,调试起来特别容易忽略。
异步 vs 同步翻转:两种模式,两种代价
flags 参数里有两个最重要的标志:DRM_MODE_PAGE_FLIP_EVENT 和 DRM_MODE_ATOMIC_NONBLOCK。但真正区分翻转行为的,是 同步 和 异步 这两个概念。
| 特性 | 同步翻转 | 异步翻转 |
|---|---|---|
| 触发时机 | 等待下一个 VBlank | 立即切换,不等待 VBlank |
| 画面撕裂 | 无撕裂 | 可能出现撕裂 |
| 延迟 | 固定延迟(1帧以内) | 低延迟(立即生效) |
| 适用场景 | 视频播放、UI 渲染 | 游戏、VR、低延迟交互 |
| 内核实现 | 在 VBlank 中断中切换 | 在调用进程上下文中切换 |
同步翻转是默认行为。你调用 drmModePageFlip 时不加 DRM_MODE_PAGE_FLIP_ASYNC 标志,就是同步模式。内核会在下一个 VBlank 到来时,在中断处理函数里完成指针切换。这样做的好处是画面完整,不会出现上半帧是旧的、下半帧是新的那种撕裂现象。
异步翻转则激进得多。它不等待 VBlank,直接改硬件寄存器。你想想看,如果此时显示器正在扫描中间某一行,你突然把 framebuffer 地址换了——那这一帧的画面就会从中间劈开,上半部分来自旧帧,下半部分来自新帧。这就是撕裂。
Flip 完成事件:你怎么知道翻转成功了?
调用 drmModePageFlip 返回 0,只代表请求被内核接受了。那你怎么知道硬件真的切过去了?答案是:Flip 完成事件。
当你设置了 DRM_MODE_PAGE_FLIP_EVENT 标志,内核会在翻转真正完成后,通过 DRM 事件队列通知你。这个事件通过 read() 或 drmHandleEvent() 读取,结构体定义如下:
struct drm_event_vblank {
struct drm_event base;
uint32_t user_data; // 你传进去的 user_data
uint32_t tv_sec;
uint32_t tv_usec;
uint32_t sequence; // VBlank 序列号
uint32_t reserved;
};
事件处理流程大概是这样的:
- 你调用
drmModePageFlip(fd, crtc_id, fb_id, DRM_MODE_PAGE_FLIP_EVENT, &my_data)。 - 内核把请求加入队列,返回 0。
- 下一个 VBlank 到来,硬件切换 framebuffer。
- 内核向 DRM 事件队列写入一个
drm_event_vblank结构体。 - 你的程序通过
drmHandleEvent(fd, &event_context)读取并分发这个事件。 - 在
event_context.page_flip_handler回调里,你拿到user_data,知道这一帧已经显示出去了。
这个机制保证了 帧率同步。你可以在回调里释放旧 framebuffer、提交新渲染任务,形成一个完整的流水线。
drmHandleEvent。这样你的渲染线程可以继续准备下一帧,不会因为等待而空转。
知识体系:Page Flip 的核心逻辑
下面这张图,把整个 Page Flip 的流程和关键节点串起来了。我建议你把它存下来,调试时对照着看。
实际项目中的选择策略
我做过几个不同场景的项目,总结了一套选择策略,分享给你:
- 视频播放器:用同步翻转 + Flip 完成事件。每一帧都等 VBlank,画面稳定,不会撕裂。帧率由视频源决定,不需要额外同步。
- 游戏引擎:用同步翻转为主,但在某些低延迟操作(如鼠标点击反馈)时临时切到异步。注意切回同步时要等一帧,否则会撕裂。
- VR/AR 头显:必须用异步翻转。延迟每多 1ms,用户就多一分眩晕感。但代价是你要自己处理撕裂——通常通过双缓冲加垂直同步信号来缓解。
- 桌面合成器(Compositor):用同步翻转 + 原子操作(Atomic API)。因为桌面环境对画面完整性要求极高,撕裂会严重影响体验。
drmSetClientCap(fd, DRM_CLIENT_CAP_ASYNC_PAGE_FLIP, 1) 来尝试启用。如果返回 0,说明支持;如果返回 -EINVAL,说明你的显卡驱动没实现这个功能。我遇到过一些嵌入式 GPU,异步翻转就是个空壳,调了等于没调。