一帧画面怎么上屏:buffer、fence、plane 和 KMS
结论:GPU 把像素写进 buffer,fence 通知下游这块 buffer 什么时候写完,plane 从 buffer 里取图,CRTC 把多个 plane 混成一帧并按刷新率扫描到屏上。 KMS 是内核提供的显示接口,负责管理 plane、CRTC 这些显示对象。Android 和 Hyprland 都建在这套模型上,看懂它,后面两套框架就只剩“谁来调用”的差别。
本文按三个视图展开:物理视图讲硬件怎么分工,逻辑视图讲 KMS 怎么描述显示通路,进程视图讲一块 buffer 在一帧里怎么流转。
物理视图:渲染和显示由两块独立硬件完成,靠 DDR 里的 buffer 交接
Section titled “物理视图:渲染和显示由两块独立硬件完成,靠 DDR 里的 buffer 交接”GPU 和显示控制器之间没有直连的数据通路,它们只通过 DDR 里的 buffer 交接。
- CPU 写命令:应用和合成器通过驱动,把渲染命令写进 DDR 里的命令队列,再通知 GPU。
- GPU 写像素:Mali 执行命令,把算出的像素写进 buffer。GPU 经自己的 MMU 访问内存,不同进程的地址空间互相隔离。
- 视频解码器写视频帧:解码器直接输出 YUV 格式的帧。这一路不经过 GPU。
- 显示控制器读 buffer:显示控制器按刷新率,每帧从 DDR 读出要显示的 buffer。它内部有多个 plane,可以同时读多块 buffer,在扫描时实时混合。
- 送屏:混合好的像素流经 DSI 接口送到屏幕。
几个术语的区别:
| 术语 | 指什么 | 谁来做 |
|---|---|---|
| 渲染 | 根据绘图命令算出像素,写进 buffer | GPU(也可以是 CPU 软件渲染) |
| 合成 | 把多个窗口的 buffer 拼成一帧 | GPU,或显示控制器的 plane |
| 显示 | 按固定时序把 buffer 扫描输出到屏幕 | 显示控制器 |
合成有两种做法,这是后面两套框架最大的区别:
- GPU 合成:再渲染一次,把各窗口画进一块新 buffer。要多写一整帧,耗电。
- plane 叠加:显示控制器扫描时逐行混合多块 buffer,不产生新 buffer,省电。
逻辑视图:KMS 用六类对象描述显示通路,合成器每帧做一次 atomic 提交
Section titled “逻辑视图:KMS 用六类对象描述显示通路,合成器每帧做一次 atomic 提交”KMS 把显示通路抽象成一串对象,每类对象对应一段硬件。合成器不直接写寄存器,而是修改这些对象的属性,每帧一次性提交。
- GEM 对象:内核里的一块 buffer。传给别的驱动或别的进程时,导出成 dma-buf 文件描述符。
- framebuffer:GEM 对象加上宽高、像素格式和 modifier(内存排布方式,例如线性、分块、AFBC 压缩)。它只是一份描述,没有对应硬件。
- plane:显示控制器里的图层通道。每个 plane 从一个 framebuffer 里取一块区域,做缩放、旋转、格式转换,放到屏幕的指定位置。数量有限,能力也不尽相同,有的不支持 YUV,有的不能缩放。
- CRTC:混合器加时序发生器。它把挂在自己上面的所有 plane 混成一帧,按刷新率扫描输出。
- encoder / bridge:把像素流转成 DSI、eDP、HDMI 等接口信号。
- connector:代表一块屏幕,提供支持的分辨率、刷新率和热插拔状态。
atomic 提交是 KMS 的核心接口。合成器把这一帧所有对象的属性改动打包成一次请求:
- 内核要么全部生效,要么全部不生效。所有 plane 在同一个 VSYNC 切换,不会出现半新半旧的画面。
- 请求带上
TEST_ONLY标志时,只检查不生效。合成器可以先试探硬件能否接受某种 plane 分配,再决定正式提交什么。
DRM 和 KMS 的关系:DRM 是内核整个图形子系统,KMS 是其中管显示的部分。DRM 还包括渲染和显存管理。在 SoC 上,GPU 驱动和显示控制器驱动通常是两个独立驱动,各用 DRM 的一半:
| 驱动 | 用到 DRM 的哪部分 | 设备节点 |
|---|---|---|
| 显示控制器驱动 | KMS + 显存,不做渲染 | /dev/dri/card0 |
| 上游 Mali 驱动 panthor | 渲染 + 显存,没有 KMS | /dev/dri/renderD128 |
| Arm 的 Mali 驱动 kbase | 不是 DRM 驱动,自成一套接口 | /dev/mali0 |
我们的 Android 用的是 kbase,所以 GPU 和显示控制器之间唯一的公共语言是 dma-buf 和 fence。
进程视图:一块 buffer 每帧转一圈,fence 保证读写不冲突
Section titled “进程视图:一块 buffer 每帧转一圈,fence 保证读写不冲突”GPU 和显示控制器都异步工作:应用提交渲染命令后马上返回,GPU 可能还没开始画;显示控制器正在读的 buffer,也不能被同时改写。fence 就是解决这两个问题的“完成通知”:硬件做完时,它从未触发变成已触发。fence 可以变成一个文件描述符(sync_file)跨进程传递。
一块 buffer 在一帧里要经过四个状态和两个 fence:
- 空闲 → GPU 在写:应用取到一块空闲 buffer。GPU 动笔前要等 release fence,确认上一轮的读者(显示控制器或合成器)已经放手。
- GPU 在写 → 已交给合成器:应用提交渲染命令后立刻把 buffer 交出去,不等 GPU 画完,同时附上 acquire fence,GPU 写完时它才触发。
- 已交给合成器 → 正在显示:合成器等 acquire fence 触发后,在下一个 VSYNC 把 plane 切到这块 buffer。
- 正在显示 → 空闲:下一帧换上别的 buffer 后,显示控制器不再读这块,内核触发 release fence,buffer 回到空闲。
一个窗口通常有两三块 buffer 轮流转。屏幕上显示一块的同时,GPU 在画另一块,所以屏幕上永远是一帧完整的画面。
fence 有两种传法,后面两套框架各用一种:
| 方式 | 做法 | 谁在用 |
|---|---|---|
| 显式同步 | fence 变成 sync_file,跟着 buffer 一起显式传递 | Android 全程使用 |
| 隐式同步 | fence 挂在 dma-buf 内部的预约对象(dma_resv)上,读者从 buffer 本身取 | Linux 桌面的客户端大多仍用这种 |
| 术语 | 一句话解释 |
|---|---|
| buffer | 内存里按图像格式组织的一块区域 |
| dma-buf | 内核里跨驱动、跨进程共享 buffer 的机制,用户态拿到的是一个文件描述符 |
| modifier | buffer 的内存排布方式;GPU 和显示控制器必须对它理解一致 |
| fence / sync_file | “某件事做完了”的通知;sync_file 是它的文件描述符形式 |
| plane | 显示控制器的图层通道 |
| CRTC | 混合各 plane 并按时序扫描输出的部件 |
| atomic 提交 | 一次性修改所有显示对象属性的 KMS 接口,全部生效或全部不生效 |
| VSYNC | 屏幕开始刷新新一帧的时刻,切换 buffer 都对齐它 |