2. DRM基础概念

在进入硬件光标与 VBlank 同步的实战之前,必须先建立一张清晰的 DRM 术语地图。很多初学者在阅读内核代码时会被 drm_crtc、drm_plane、drm_encoder、drm_connector 这些结构体绕晕,根本原因是没有理解它们各自在显示流水线中的位置与职责。本节将 DRM 的架构拆解为“用户空间—内核 DRM 核心—KMS 对象—硬件驱动”四层,并逐一说明每个核心术语的作用与相互关系。

一、DRM 的整体架构分层

DRM(Direct Rendering Manager)是 Linux 内核中负责管理 GPU 与显示控制器的子系统。它并不是一个单一模块,而是由多个层次协作构成的框架:

  • 用户空间层:包括 libdrm、Mesa、Wayland 合成器、X Server 等,通过 ioctl 与内核 DRM 设备节点(如 /dev/dri/card0)交互。
  • DRM 核心层:提供通用对象管理、内存管理(GEM/TTM)、模式设置(KMS)、原子提交(Atomic Commit)等基础设施。
  • KMS 对象层:由 CRTC、Plane、Encoder、Connector、Framebuffer 等对象组成,描述“显示什么、显示到哪里、怎么显示”。
  • 硬件驱动层:如 i915、amdgpu、msm、rockchip 等,负责把 KMS 对象的配置翻译成寄存器操作。

理解这四层的关键在于:用户空间只操作抽象对象,内核核心负责校验与调度,硬件驱动负责落地。硬件光标与 VBlank 同步正是发生在 KMS 对象层与硬件驱动层之间的交互。

二、KMS 核心对象及其职责

KMS(Kernel Mode Setting)是 DRM 中负责显示模式设置与画面合成的子系统。它的核心对象可以类比为一条“显示流水线”:

对象 职责 典型结构体 与光标/VBlank 的关系
Framebuffer 描述一块像素内存的格式、宽高、pitch、修饰符 drm_framebuffer 光标图像通常也以 Framebuffer 形式提供
Plane 图像层,负责从 Framebuffer 取像素并合成到 CRTC drm_plane 光标 Plane 是独立于主 Plane 的叠加层
CRTC 显示控制器通道,负责扫描输出与时序生成 drm_crtc VBlank 中断由 CRTC 产生,光标位置在 CRTC 内生效
Encoder 将 CRTC 的像素流转换为 Connector 可接受的信号 drm_encoder 不直接参与光标,但决定输出链路
Connector 表示物理输出接口,如 HDMI、DP、DSI、eDP drm_connector 决定最终显示设备与 EDID 模式

这五个对象的关系可以用一句话概括:Framebuffer 提供像素,Plane 负责合成,CRTC 负责扫描与时序,Encoder 负责信号转换,Connector 负责物理输出。 硬件光标本质上是一个特殊的 Plane,它拥有独立的 Framebuffer 和位置属性,由 CRTC 在扫描时叠加到主画面上。

三、对象之间的连接关系与数据流

为了更直观地理解这些对象如何串联,下面用一张结构关系图展示 KMS 对象在显示流水线中的位置与连接方向。图中同时标注了硬件光标 Plane 与 VBlank 信号的位置,便于后续章节对照。

KMS 对象关系与显示数据流 Framebuffer 像素内存 + 格式 Primary Plane 主画面合成层 Cursor Plane 硬件光标叠加层 CRTC 显示控制器通道 扫描输出 + 时序生成 VBlank 中断源 光标位置寄存器 原子提交生效点 Encoder 像素流 → 信号 Connector HDMI / DP / DSI / eDP VBlank 中断 同步光标更新时机 图例: 主数据流 光标叠加路径

从图中可以看到,主画面与光标画面分别从各自的 Framebuffer 出发,经过 Plane 合成后进入 CRTC。CRTC 在扫描输出时,会在每个 VBlank 周期产生中断,驱动可以借此机会安全地更新光标位置或切换光标图像,避免撕裂。

四、典型场景:一次光标移动的完整路径

假设用户移动鼠标,Wayland 合成器需要把光标从 (100, 200) 移动到 (120, 220)。这条路径大致如下:

  1. 合成器通过 libdrm 构造一个原子提交请求,设置 Cursor Plane 的 CRTC_X、CRTC_Y 属性。
  2. DRM 核心校验该 Plane 是否属于目标 CRTC,格式是否支持,坐标是否越界。
  3. 驱动在原子提交的 atomic_flush 阶段把新坐标写入 CRTC 的光标位置寄存器。
  4. CRTC 在下一个 VBlank 到来时,用新坐标扫描光标,画面无撕裂。
  5. 若驱动支持 DRM_CAP_CURSOR_WIDTH/HEIGHT,用户空间可查询硬件光标的最大尺寸。

下面是一段查询光标能力的典型代码片段:

uint64_t cap;
drmGetCap(fd, DRM_CAP_CURSOR_WIDTH, &cap);
printf("cursor max width: %llu\n", (unsigned long long)cap);

drmGetCap(fd, DRM_CAP_CURSOR_HEIGHT, &cap);
printf("cursor max height: %llu\n", (unsigned long long)cap);

/* 查询 CRTC 是否支持光标 Plane */
drmModePlaneRes *planes = drmModeGetPlaneResources(fd);
for (uint32_t i = 0; i < planes->count_planes; i++) {
    drmModePlane *p = drmModeGetPlane(fd, planes->planes[i]);
    if (p->possible_crtcs & (1 << crtc_index)) {
        printf("plane %u can drive crtc %u\n", p->plane_id, crtc_index);
    }
    drmModeFreePlane(p);
}

五、常见误区与复盘

误区一:把 Plane 当成 Framebuffer。 Framebuffer 只是内存描述,Plane 才是合成单元。一个 Framebuffer 可以被多个 Plane 引用,但每个 Plane 在同一时刻只能绑定一个 Framebuffer。

误区二:认为 Encoder 和 Connector 是一一对应。 实际上一个 Encoder 可以连接多个 Connector,一个 Connector 也可以由多个 Encoder 驱动,具体取决于硬件拓扑与 possible_crtcs、possible_clones 的约束。

误区三:忽略 CRTC 的索引与位掩码。 在原子提交中,crtc_mask 和 possible_crtcs 都是位掩码,第 n 位代表第 n 个 CRTC。写错位偏移会导致 Plane 无法绑定到目标 CRTC。

复盘建议: 调试光标问题时,先用 modetest -p 或 drm_info 打印当前 KMS 对象拓扑,确认 Cursor Plane 的 plane_id、possible_crtcs 以及当前绑定的 CRTC。再结合 dmesg | grep vblank 观察 VBlank 中断是否正常上报。只有对象关系清晰,后续的硬件光标与 VBlank 同步才有稳定的调试基础。