3. Legacy API 回顾:drmModeSetCrtc、drmModePageFlip 等传统接口的调用方式与局限性
在 DRM 子系统的演进历史中,Legacy API 是早期用户态与内核态显示交互的核心通道。虽然 Atomic API 已成为现代 Linux 显示栈的标准,但大量遗留代码、老旧硬件平台以及部分轻量级合成场景仍在使用这些传统接口。深入理解 Legacy API 的调用方式与内在局限性,是掌握 Atomic 优化价值的前提。
3.1 核心 Legacy 接口一览
Legacy API 以 drmMode* 系列函数为主,每个函数对应一次独立的硬件状态更新。下表列出了最常用的三个接口及其典型用途:
| 接口函数 | 功能描述 | 典型调用场景 |
|---|---|---|
drmModeSetCrtc |
设置 CRTC 的扫描输出模式,绑定 Framebuffer、连接器与模式 | 初始化显示、模式切换、全屏应用启动 |
drmModePageFlip |
在 VBlank 期间切换 Framebuffer,实现无撕裂页面翻转 | 游戏渲染循环、视频播放、窗口合成器 |
drmModeSetPlane |
配置硬件 Plane(覆盖层/光标层)的位置、大小与源裁剪 | 光标移动、视频叠加、UI 元素分层 |
3.2 drmModeSetCrtc 调用方式与内部流程
drmModeSetCrtc 是 Legacy 接口中最基础的显示配置函数。用户态通过它完成“CRTC + Framebuffer + Connector + Mode”的绑定。其函数原型如下:
int drmModeSetCrtc(int fd, uint32_t crtc_id, uint32_t buffer_id,
uint32_t x, uint32_t y,
uint32_t *connectors, int connector_count,
drmModeModeInfo *mode);
调用参数中,buffer_id 指向一个已创建的 Framebuffer,connectors 数组指定要关联的物理输出端口,mode 定义时序参数(分辨率、刷新率等)。内核侧的处理流程可概括为以下步骤:
- 参数校验:检查 CRTC ID、Connector ID、Framebuffer 是否合法且匹配。
- 解除旧绑定:断开 CRTC 上原有的 Connector 和 Plane 关联。
- 新绑定建立:将 Framebuffer 挂载到 CRTC 的主扫描链路上。
- 时序生效:通过硬件寄存器设置新的显示时序(Mode)。
- 输出使能:启动 CRTC 扫描输出,Connector 开始发送视频信号。
drmModeSetCrtc 调用都会触发完整的 CRTC 重置,包括关闭输出、重新协商链路、重新初始化时钟。在频繁模式切换场景下,此开销可达数十毫秒,直接导致显示黑屏或闪烁。
3.3 drmModePageFlip 的 VBlank 同步机制
drmModePageFlip 是 Legacy 接口中实现无撕裂页面翻转的唯一途径。它依赖 VBlank 中断来同步缓冲区切换,确保新帧在垂直消隐期开始显示。其典型调用方式如下:
int drmModePageFlip(int fd, uint32_t crtc_id, uint32_t fb_id,
uint32_t flags, void *user_data);
调用时,用户态需指定目标 CRTC 和新的 Framebuffer ID。flags 参数可控制是否阻塞等待(DRM_MODE_PAGE_FLIP_EVENT 表示异步等待事件)。内核在下一个 VBlank 到来时执行以下操作:
- 将 CRTC 的主扫描地址寄存器更新为
fb_id对应的物理地址。 - 通过 DRM 事件队列向用户态发送
DRM_EVENT_FLIP_COMPLETE事件。 - 如果设置了
DRM_MODE_PAGE_FLIP_ASYNC,则跳过 VBlank 等待,立即翻转(可能产生撕裂)。
drmModePageFlip 只能切换主 Framebuffer,无法同时更新 Plane 或 Connector 状态。若需要同时移动光标或调整叠加层,必须额外调用 drmModeSetPlane,导致多次 VBlank 延迟累积。
3.4 Legacy API 的架构局限:串行更新与竞态条件
Legacy API 最根本的架构问题在于其“串行、非原子”的更新模型。下图展示了在 Legacy 模式下,一次完整的显示状态更新(同时修改 CRTC 模式、Plane 位置、Framebuffer)所需的多次调用与时间线:
从上图可以清晰看到,Legacy API 的串行更新模式存在三个核心缺陷:
- 多 VBlank 延迟:每个独立调用都需要等待下一个 VBlank 才能生效,组合操作的总延迟随调用次数线性增长。
- 中间状态暴露:由于各硬件块更新不同步,用户态无法保证在某一 VBlank 时刻所有状态同时切换,导致撕裂、闪烁或内容错位。
- 缺乏回滚机制:如果
drmModeSetCrtc在drmModePageFlip之后失败,系统将处于不一致状态——Framebuffer 已切换但模式未更新,无法安全恢复。
3.5 Legacy API 的局限性总结
下表从多个维度对比 Legacy API 与 Atomic API 的关键差异,帮助理解为何 Atomic 成为性能优化的必然选择:
| 对比维度 | Legacy API | Atomic API |
|---|---|---|
| 更新粒度 | 单对象单属性 | 多对象多属性一次提交 |
| 同步机制 | 隐式 VBlank 等待(每个调用独立) | 显式 Commit + 统一 VBlank 事件 |
| 中间状态 | 存在部分更新窗口 | 全有或全无(All-or-Nothing) |
| 错误恢复 | 无自动回滚,需用户态手动补偿 | 内核自动回滚到上一个有效状态 |
| 性能开销 | 多次 ioctl + 多次 VBlank 等待 | 单次 ioctl + 单次 VBlank 等待 |
| 测试/调试 | 难以复现竞态条件 | 支持 TEST_ONLY 校验,可预演 |
drmModeAtomicAddProperty 模拟批量提交,减少 ioctl 次数。
3.6 从 Legacy 到 Atomic 的迁移路径
理解 Legacy API 的局限性后,迁移到 Atomic API 的核心思路是将多次独立调用合并为一次原子提交。以下是一个典型的迁移对照:
/* Legacy 方式:三次独立调用 */
drmModeSetPlane(fd, plane_id, crtc_id, fb_id1, 0, flags, ...);
drmModePageFlip(fd, crtc_id, fb_id2, DRM_MODE_PAGE_FLIP_EVENT, NULL);
drmModeSetCrtc(fd, crtc_id, fb_id3, 0, 0, connectors, 1, &mode);
/* Atomic 方式:一次提交 */
drmModeAtomicReq *req = drmModeAtomicAlloc();
drmModeAtomicAddProperty(req, crtc_id, crtc_prop_mode, &mode);
drmModeAtomicAddProperty(req, crtc_id, crtc_prop_active, 1);
drmModeAtomicAddProperty(req, plane_id, plane_prop_fb_id, fb_id3);
drmModeAtomicAddProperty(req, plane_id, plane_prop_crtc_id, crtc_id);
drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
drmModeAtomicFree(req);
迁移过程中需注意:Atomic API 要求驱动支持 DRIVER_ATOMIC 标志,且用户态需通过 drmGetCap 查询 DRM_CAP_ATOMIC 能力。对于不支持 Atomic 的旧驱动,Legacy API 仍是唯一选择,但应将其视为技术债务,在硬件升级时同步淘汰。