DRM 与图形

本页目录

帧已渲染,屏幕却未显示;导入 dma-buf 成功,也不意味着能直接扫描输出。DRM 要分别解释渲染、缓冲区共享和显示提交⁠。无显示器的设备仍可能提供渲染能力。

查阅目标阅读位置
用户/内核职责与 KMS第 1~2 节
提交完成与对象句柄第 3~4 节
共享同步与黑屏定位第 5~6 节

1. 用户态与内核态各做什么

用户态驱动(UMD,例如 Mesa 中的 OpenGL/Vulkan 实现)处理图形 API、着色器编译及命令组织。内核驱动(KMD,例如 amdgpu、i915、xe、nouveau)提供设备访问、缓冲区管理、地址空间、调度与同步等机制;支持显示的驱动还实现 KMS。

这是一种职责分离,不是“内核完全不检查命令”。验证、隔离和提交接口依硬件与驱动而异;UMD 也必须匹配内核支持的 UAPI 和硬件能力,升级用户态并不会自动增加缺失的内核功能。

设备节点常见用途权限含义
/dev/dri/cardNprimary node,包括显示控制接口modeset 等操作受 DRM master、lease 等规则约束
/dev/dri/renderDN渲染及相关资源操作不提供 modesetting;仍受文件权限与安全策略控制

card0 是枚举名称,不承诺是桌面当前使用的“主 GPU”。render node 不要求取得 DRM master,但不等于任何用户都天然可访问。具体接口参阅 DRM 用户态 API。

2. KMS:描述显示资源及其连接

渲染完成与显示完成,是两次不同的交接 应用 / UMD 提交 GPU 渲染 共享缓冲区 dma-buf 与同步 合成器 / KMS 选择合成或直接扫描 Plane CRTC Encoder Connector 显示路径:缓冲区作为 plane 的图像来源 旧缓冲区只有在所有使用者都结束访问后才能复用 共享内存不自动解决格式兼容、等待渲染、显示释放和访问权限。
  • Plane:提供一个图像层,包括 framebuffer、源裁剪和目标位置。类型有 primary、overlay、cursor,光标不应一概归为 overlay。
  • CRTC:管理扫描时序及关联平面,将图像按模式输出。这个历史名称不表示现代显示器使用阴极射线管。
  • Encoder:抽象输出编码及路由资源。
  • Connector:表达面向显示终端的连接,可报告连接状态、模式和 EDID 等信息。

连接受硬件资源约束:plane 不一定能接任意 CRTC,显示带宽与缩放能力也有限。一个 CRTC 在硬件支持时可以克隆到多个输出;不能把 CRTC 与显示器简单设为一一对应。

Framebuffer 是图像布局对象,不等于内存分配本身。 它描述尺寸、像素格式、pitch、offset、modifier 及底层缓冲区的关联。即使两个缓冲区字节数相同,也可能因格式或 modifier 不同而不能互换。对象和属性详见 KMS 文档。

用线性图像算一次 pitch 与 offset

取一个 3×2 的线性 XRGB8888 图像,每个像素占 4 字节;假定行距 pitch 为 16 字节,图像从底层对象的 offset=8 开始。每行真正的像素占 12 字节,下一行却要前进 16 字节。像素 (x, y) 起始位置为 offset + y × pitch + x × 4,因此最后一个像素 (2, 1) 位于字节 32~35,而不是把六个像素紧密排在 8~31。

区间(左闭右开)内容
[0, 8)此图像之前的对象空间
[8, 20)第 0 行三个像素
[20, 24)行间填充
[24, 36)第 1 行三个像素
def pixel_span(width, height, pitch, offset, object_size, x, y):
    if min(width, height) <= 0 or offset < 0 or pitch < width * 4:
        raise ValueError("非法线性布局")
    end = offset + (height - 1) * pitch + width * 4
    if end > object_size or not (0 <= x < width and 0 <= y < height):
        raise ValueError("像素或图像超出范围")
    start = offset + y * pitch + x * 4
    return start, start + 4

assert pixel_span(3, 2, 16, 8, 36, 2, 1) == (32, 36)
assert pixel_span(3, 2, 16, 8, 36, 0, 1) == (24, 28)
for args in [(3, 2, 16, 8, 35, 2, 1), (3, 2, 8, 8, 36, 0, 0),
             (3, 2, 16, 8, 36, 3, 0), (3, 2, 16, 8, 36, 0, 2)]:
    try:
        pixel_span(*args)
    except ValueError:
        pass
    else:
        raise AssertionError("错误布局或坐标应拒绝")

这里的 36 字节只是覆盖最后一个有效像素的数学下界,真实分配还受驱动对齐、页大小及接口要求影响,应使用接口返回的 pitch 和 size。Python 整数不会溢出;把公式搬到 C 时还必须检查乘加溢出。

这套寻址只适用于所假定的线性、单个格式平面的布局。带 tile 或压缩 modifier 的存储不能照此寻址;NV12 等多平面像素格式要分别描述各平面的行距和偏移。⁠像素格式中的 plane 与 KMS 显示叠加层 Plane 也不是同一个概念⁠:一个 KMS Plane 可以扫描由多个格式平面共同组成的图像。

3. Atomic modesetting:先验证一组状态

用户态通过 atomic ioctl 提交对象属性集合,例如将某个 framebuffer 放到 plane,并设置 CRTC 的模式与 connector 路由。内核为相关对象构造状态,检查资源、格式、带宽及驱动约束;drm_atomic_state 是内核内部结构,不是用户态直接分配后传入的通用 C 结构。

TEST_ONLY 可以只验证而不改变显示状态,但测试通过不为未来提交预留资源。真正提交时仍需检查当时条件。

atomic 的价值是避免用户态必须逐条修改对象并自行处理不可用的中间配置。它不承诺所有硬件寄存器在同一个 CPU 指令中改变,也不保证任何改模式操作都完全无黑屏。涉及多个 CRTC 时,还不能推导出所有显示器物理上同时刷新。

为什么必须验证组合,而不只是验证每个对象

假设两个显示管线共享一个教学用带宽预算 10,每条当前消耗 4。单独测试 A 升到 6 时,总量是 6+4=10;单独测试 B 升到 6 也通过。但是同时升到 6 的组合需要 12,必须拒绝。分别通过的局部配置,不能据此拼成有效的全局配置。

同理,测试 A 升级通过后,如果 B 先提交升级,稍后真正提交 A 时就要面对新状态 4+6。TEST_ONLY 的意义是检查候选状态,不是预约那份剩余资源。实际驱动还会检查时钟、路由、缩放器和链路等条件;这里的单一数字只帮助理解共享约束,不能用于计算具体 GPU 的可用显示带宽。

返回成功与画面已经显示

阻塞和 nonblocking commit 的等待行为不同。非阻塞提交接受后,硬件工作可以继续进行,需要通过事件或输出 fence 等机制判断完成。输入 fence 则可表示新缓冲区的渲染依赖。

普通翻页常与 vblank 配合以避免撕裂,但 async 更新、完整 modeset 和不同驱动路径有各自约束,不能统一写成“验证后在 vblank 无闪烁原子应用”。旧 framebuffer 的复用应遵循对应完成与释放协议,不能仅凭 ioctl 已返回。

新帧开始显示,不代表新帧可以马上覆盖

设 CRTC A 当前扫描缓冲 X,准备翻页到 Y。Y 的输入 fence 完成说明渲染者写完了 Y;A 的翻页完成说明这次显示切换已完成,X 在 A 上的旧扫描使用可以结束。此时 Y 是当前扫描对象⁠,不是刚刚被释放的对象。如果 CRTC B 还在扫描 X,X 也不能因为 A 已完成翻页就交还生产者覆盖。

A 已切换到 Y,X 仍可能被另一条管线使用 初始 A → X;B → X X:仍在扫描 A 翻页完成 A → Y;B → X X:仍不可覆盖 B 翻页完成 A → Y;B → Z X:无扫描使用 Y 在 A 上成为当前帧,仍需等待后续替换才能回收 仅画扫描使用;其他消费者也必须完成,缓冲区才能安全复用。

下面把“使用”记录为 (CRTC, buffer) 关系,只模拟每个 CRTC 有一个扫描图层、同一时刻最多一个待完成切换的普通翻页。完成通知按其所属 CRTC 处理,不假设两个屏幕同时完成。

class Scanout:
    def __init__(self):
        self.current = {"A": "X", "B": "X"}
        self.pending = {}
        self.ready = {"X"}

    def render_done(self, buffer):
        self.ready.add(buffer)

    def submit(self, crtc, new):
        assert crtc in self.current and crtc not in self.pending
        self.pending[crtc] = new           # 接受请求不等于切换完成

    def complete_flip(self, crtc):
        new = self.pending[crtc]
        if new not in self.ready:
            raise RuntimeError("输入渲染依赖尚未完成")
        self.current[crtc] = new
        del self.pending[crtc]

    def reusable(self, buffer):
        return buffer not in self.current.values() and buffer not in self.pending.values()

s = Scanout()
s.submit("A", "Y")
assert not s.reusable("X") and not s.reusable("Y")
try:
    s.complete_flip("A")
except RuntimeError:
    pass
else:
    raise AssertionError("不能越过输入依赖")
s.render_done("Y")
s.complete_flip("A")
assert not s.reusable("X")                 # B 还在使用 X
assert not s.reusable("Y")                 # A 正在扫描 Y
s.render_done("Z")
s.submit("B", "Z")
assert not s.reusable("X")                 # ioctl 接受仍不足以释放
s.complete_flip("B")
assert s.reusable("X")
assert not s.reusable("Y") and not s.reusable("Z")

模型中可复用只表示没有剩余扫描或待提交使用;真实系统还必须合并 GPU、视频编码器等其他消费者的释放条件。OUT_FENCE_PTR 属于 CRTC,不能解释成任意 dma-buf 的全局“无人使用”证明;writeback 的输出 fence 又表示写回目标的写入完成,应按对应属性协议解释。

选择 A/B buffer 并直接渲染、提交或推进 vblank。屏幕保留当前扫描内容,直到排队的帧满足 fence 和呈现条件;TEST_ONLY 不会偷偷改变屏幕。

正在呈现知识画面
渲染完成、提交接受、屏幕显示是三个时刻

缓冲区存在不代表渲染已完成,atomic ioctl 成功也不代表已显示。旧 scanout 引用释放后,旧缓冲区才允许再次覆盖。

4. GEM:对象、句柄与实际存储

GEM 提供图形缓冲区对象及相关生命周期框架,分配方法由驱动和具体接口决定,并不存在可普遍调用的 drm_gem_object_create(dev, size) 来代表所有 GPU 分配。

对象的存储可来自系统内存或显存;TTM 等机制可以管理放置和迁移,但不是所有 GEM 驱动都必须使用 TTM。CPU 映射、GPU 映射和显示扫描能力也是分别建立的能力,不是创建对象后自然全部可用。

跨进程共享:传递引用,不是照抄整数 handle

GEM handle 通常属于某个 DRM 文件上下文。把整数 handle 发给另一个独立打开 DRM 设备的进程,不会自动指向同一个对象。跨进程共享常通过导出 dma-buf fd,再用 Unix socket 的 SCM_RIGHTS 传递;接收方取得自己的引用并按接口导入。见 DRM 内存管理。

5. dma-buf:共享底层存储,也要协调访问

例子:应用渲染一帧,合成器接收其 dma-buf,并决定将它作为纹理合成到输出缓冲区,或满足条件时直接交给显示 plane 扫描。

两种路径都可以共享应用的底层存储,但后一种还要求图像格式、modifier、缩放、位置、输出颜色处理等约束全部满足。导入成功不保证可以直接 scanout;跨设备也可能需要迁移或复制,所以 dma-buf 不是无条件零拷贝承诺。

交接方向必须等待什么
生产者把新帧交给消费者GPU 渲染完成,消费者才可读取
消费者把旧帧交还生产者显示或其他消费完成,生产者才可覆盖

引用保证对象仍存在,不证明读写之间没有竞争。

隐式同步可通过 reservation object 和 fence 跟踪依赖,显式同步则由 API/协议传递 fence、syncobj 等同步对象。选择哪种机制取决于整条路径。CPU 访问 dma-buf 时的缓存同步也不能替代等待 GPU 工作完成:缓存可见性与任务完成是不同条件。详见 dma-buf 文档。

6. fbdev 与排查入口

fbdev 提供传统 framebuffer 接口,功能及一致的资源建模能力较有限,但历史上存在加速和等待垂直同步等接口,不能绝对说“没有 GPU 加速、没有 vsync”。现代 DRM 驱动可按配置提供 fbdev 兼容层,并非每个系统都一定暴露 /dev/fb0。

查问题时按边界定位:应用是否成功提交渲染、fence 是否完成、合成器是否接收缓冲区、KMS 配置是否通过、connector 是否连接。不要把任何黑屏都归为“显存没有分配”。

只读查看节点和连接状态:

ls -l /dev/dri/
from pathlib import Path

for status in sorted(Path('/sys/class/drm').glob('card*-*/status')):
    try:
        print(status.parent.name, status.read_text().strip())
    except OSError as exc:
        print(status.parent.name, type(exc).__name__)

connected 仅表示连接检测状态,不证明该输出正在扫描有效画面;没有这些目录也不证明系统完全没有 GPU,设备可能只提供 render 功能。