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 合成器提交一帧为例,典型流程如下:

  1. 打开设备,获取 DRM 资源,枚举 CRTC、Connector、Plane 及其 Property。
  2. 调用 drmModeCreatePropertyBlob 把目标模式封装成 Blob,得到 mode_blob_id。
  3. 调用 drmModeAtomicAlloc 申请原子状态。
  4. 用 drmModeAtomicAddProperty 依次设置 Connector 的 CRTC_ID、CRTC 的 MODE_ID 与 ACTIVE、Plane 的 FB_ID 与坐标缩放属性。
  5. 调用 drmModeAtomicCommit,传入 DRM_MODE_ATOMIC_TEST_ONLY 先做校验,确认无冲突后再带 DRM_MODE_ATOMIC_NONBLOCK 正式提交。
  6. 提交成功后,内核在垂直消隐期切换状态;若失败,返回 -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 等对延迟极敏感且能接受撕裂的场景使用。

原子提交:State → Commit → 硬件生效 用户空间 Atomic State AddProperty Commit(flags) 内核 DRM 校验属性组合 构建 committed state 排队到 flip 工作 显示硬件 CRTC / Plane 垂直消隐切换 扫描输出 Property 示例 CRTC.MODE_ID → Blob CRTC.ACTIVE → 0/1 Plane.FB_ID → 帧缓冲 Plane.CRTC_ID → 绑定 Connector.CRTC_ID Commit 标志 TEST_ONLY 仅校验 NONBLOCK 异步返回 ALLOW_MODESET 重设 PAGE_FLIP_ASYNC 低延迟 PAGE_FLIP_EVENT 通知 常见失败原因 属性组合非法 -EINVAL Plane 与 CRTC 不兼容 缺少 ALLOW_MODESET Blob 已释放 FB 尺寸越界 FLIP_COMPLETE

案例场景: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、多输出、多帧的复杂显示需求,组织成一条稳定、可预测的提交流水线。