4. DRM模式设置与原子提交
在传统 DRM 模式设置中,应用通过 ioctl 逐个修改 CRTC、连接器、平面等对象,内核在每次调用时立即生效。这种“即时生效”的方式在 Wayland 合成器里会带来明显问题:一次画面更新往往涉及多个对象,如果分多次提交,中间状态可能被显示出来,造成闪烁、撕裂或短暂黑屏。原子模式设置(Atomic Modesetting)把一次显示更新抽象为一个不可分割的事务,所有对象的状态要么全部生效,要么全部不生效,从根本上解决了多对象协同更新的原子性问题。
核心原理:State、Property 与 Blob
原子提交的核心是“状态对象”。每个 DRM 对象(CRTC、Connector、Plane)都有一组属性(Property),属性是内核暴露给用户空间的键值对,例如 CRTC 的 ACTIVE、MODE_ID,Plane 的 FB_ID、CRTC_ID、SRC_X/SRC_Y、SRC_W/SRC_H、CRTC_X/CRTC_Y、CRTC_W/CRTC_H。用户空间不能直接写这些属性,而是先申请一个原子状态(atomic state),在状态里修改属性值,再一次性提交。
其中 MODE_ID 指向一个 Blob。Blob 是内核管理的不可变二进制数据块,模式信息(drm_mode_modeinfo)被封装成 Blob 后通过属性引用。这样做的好处是模式数据在提交期间不会被并发修改,且多个 CRTC 可以共享同一个 Blob,减少重复拷贝。类似地,连接器的 EDID、色域、HDR 元数据等也常以 Blob 形式传递。
Property:对象可配置的键值对,分不可变、可变、原子三类;
State:一次事务中所有对象属性的快照,分 pending 与 committed;
Blob:不可变二进制数据,用于模式、EDID、LUT 等大块数据;
Commit:把 pending state 校验并应用到硬件的动作。
方法流程:一次原子提交的完整链路
以 Wayland 合成器提交一帧为例,典型流程如下:
- 打开设备,获取 DRM 资源,枚举 CRTC、Connector、Plane 及其 Property。
- 调用
drmModeCreatePropertyBlob把目标模式封装成 Blob,得到mode_blob_id。 - 调用
drmModeAtomicAlloc申请原子状态。 - 用
drmModeAtomicAddProperty依次设置 Connector 的CRTC_ID、CRTC 的MODE_ID与ACTIVE、Plane 的FB_ID与坐标缩放属性。 - 调用
drmModeAtomicCommit,传入DRM_MODE_ATOMIC_TEST_ONLY先做校验,确认无冲突后再带DRM_MODE_ATOMIC_NONBLOCK正式提交。 - 提交成功后,内核在垂直消隐期切换状态;若失败,返回
-EINVAL并给出具体属性冲突信息。
uint32_t flags = DRM_MODE_ATOMIC_TEST_ONLY | DRM_MODE_ATOMIC_ALLOW_MODESET;
int ret = drmModeAtomicCommit(fd, req, flags, NULL);
if (ret) {
drmModeAtomicFree(req);
return ret; /* 校验失败,属性组合非法 */
}
flags = DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_ATOMIC_ALLOW_MODESET;
ret = drmModeAtomicCommit(fd, req, flags, NULL);
drmModeAtomicFree(req);
注意 ALLOW_MODESET 标志:只有涉及分辨率、刷新率、连接器绑定等需要重新训练链路的变更才需要它;纯平面位置或帧缓冲切换不应带该标志,否则会触发昂贵的完整模式设置,造成卡顿。
异步更新与 Page Flip
合成器每帧都要换帧缓冲,如果每次都同步等待垂直消隐,CPU 会被阻塞。原子提交支持异步路径:带 DRM_MODE_ATOMIC_NONBLOCK 提交后立即返回,内核在合适的时机完成切换,并通过 DRM_EVENT_FLIP_COMPLETE 事件通知用户空间。用户空间用 poll 监听 drm fd,收到事件后再提交下一帧,形成“提交—等待—再提交”的流水线。这正是 Wayland 合成器实现稳定帧率的关键机制。
此外还有 DRM_MODE_PAGE_FLIP_ASYNC,它允许在非垂直消隐期立即翻转,延迟更低但可能撕裂,通常只在游戏、VR 等对延迟极敏感且能接受撕裂的场景使用。
案例场景:Wayland 合成器的一帧提交
假设合成器要把一个全屏窗口和鼠标光标合成到两个 Plane 上。它先为全屏窗口分配 Plane A,设置 FB_ID、CRTC_ID、SRC_* 与 CRTC_*;再为光标分配 Plane B,设置较小的 CRTC_W/CRTC_H 与位置。两个 Plane 的属性写入同一个 atomic request,一次 commit 完成。若光标 Plane 不支持当前格式,内核在 TEST_ONLY 阶段就返回错误,合成器可以降级为把光标画进主帧缓冲,而不是让整帧失败。
| 阶段 | 调用 | 作用 | 失败处理 |
|---|---|---|---|
| 准备模式 | drmModeCreatePropertyBlob | 封装 modeinfo | 检查返回值,释放旧 Blob |
| 申请状态 | drmModeAtomicAlloc | 创建 pending state | 内存不足则跳过本帧 |
| 写属性 | drmModeAtomicAddProperty | 设置对象属性 | 记录属性 ID,避免重复 |
| 校验 | Commit(TEST_ONLY) | 不生效,只验证 | 降级 Plane 或回退合成 |
| 提交 | Commit(NONBLOCK) | 异步生效 | 等待 FLIP_COMPLETE 再下一帧 |
drm_atomic 调试项,或使用 modetest -a 查看属性列表。遇到 -EINVAL 时,优先用 TEST_ONLY 单独验证每个对象的属性组合,再逐步合并,定位冲突属性。
误区与复盘
常见误区一:把 ALLOW_MODESET 当成万能标志,每帧都带上,导致每次提交都触发完整模式设置,帧率骤降。正确做法是仅在分辨率、刷新率或连接器拓扑变化时使用。误区二:提交后立即释放 atomic request 和 Blob,但内核在异步路径中仍可能引用它们,造成 use-after-free。应在收到 FLIP_COMPLETE 后再释放。误区三:忽略 TEST_ONLY,直接正式提交,一旦失败整帧丢失且难以定位。复盘时应把“先校验、后提交、事件驱动下一帧”作为固定流水线,并记录每次 commit 的 flags 与返回值,便于对比不同硬件的能力差异。
理解 State、Blob 与 Commit 三者的关系,是掌握 DRM 原子模式设置的关键。State 描述“想变成什么样”,Blob 承载“大块不可变数据”,Commit 决定“何时以及如何生效”。Wayland 合成器正是依靠这套机制,把多 Plane、多输出、多帧的复杂显示需求,组织成一条稳定、可预测的提交流水线。