跳转到内容

一帧画面怎么上屏: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 交接。

物理视图:SoC 上渲染、解码、显示三类设备通过 DDR 交接画面

  1. CPU 写命令:应用和合成器通过驱动,把渲染命令写进 DDR 里的命令队列,再通知 GPU。
  2. GPU 写像素:Mali 执行命令,把算出的像素写进 buffer。GPU 经自己的 MMU 访问内存,不同进程的地址空间互相隔离。
  3. 视频解码器写视频帧:解码器直接输出 YUV 格式的帧。这一路不经过 GPU。
  4. 显示控制器读 buffer:显示控制器按刷新率,每帧从 DDR 读出要显示的 buffer。它内部有多个 plane,可以同时读多块 buffer,在扫描时实时混合。
  5. 送屏:混合好的像素流经 DSI 接口送到屏幕。

几个术语的区别:

术语指什么谁来做
渲染根据绘图命令算出像素,写进 bufferGPU(也可以是 CPU 软件渲染)
合成把多个窗口的 buffer 拼成一帧GPU,或显示控制器的 plane
显示按固定时序把 buffer 扫描输出到屏幕显示控制器

合成有两种做法,这是后面两套框架最大的区别:

  • GPU 合成:再渲染一次,把各窗口画进一块新 buffer。要多写一整帧,耗电。
  • plane 叠加:显示控制器扫描时逐行混合多块 buffer,不产生新 buffer,省电。

逻辑视图:KMS 用六类对象描述显示通路,合成器每帧做一次 atomic 提交

Section titled “逻辑视图:KMS 用六类对象描述显示通路,合成器每帧做一次 atomic 提交”

KMS 把显示通路抽象成一串对象,每类对象对应一段硬件。合成器不直接写寄存器,而是修改这些对象的属性,每帧一次性提交。

逻辑视图: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:

进程视图:一块 buffer 的四个状态和两个 fence

  1. 空闲 → GPU 在写:应用取到一块空闲 buffer。GPU 动笔前要等 release fence,确认上一轮的读者(显示控制器或合成器)已经放手。
  2. GPU 在写 → 已交给合成器:应用提交渲染命令后立刻把 buffer 交出去,不等 GPU 画完,同时附上 acquire fence,GPU 写完时它才触发。
  3. 已交给合成器 → 正在显示:合成器等 acquire fence 触发后,在下一个 VSYNC 把 plane 切到这块 buffer。
  4. 正在显示 → 空闲:下一帧换上别的 buffer 后,显示控制器不再读这块,内核触发 release fence,buffer 回到空闲。

一个窗口通常有两三块 buffer 轮流转。屏幕上显示一块的同时,GPU 在画另一块,所以屏幕上永远是一帧完整的画面。

fence 有两种传法,后面两套框架各用一种:

方式做法谁在用
显式同步fence 变成 sync_file,跟着 buffer 一起显式传递Android 全程使用
隐式同步fence 挂在 dma-buf 内部的预约对象(dma_resv)上,读者从 buffer 本身取Linux 桌面的客户端大多仍用这种
术语一句话解释
buffer内存里按图像格式组织的一块区域
dma-buf内核里跨驱动、跨进程共享 buffer 的机制,用户态拿到的是一个文件描述符
modifierbuffer 的内存排布方式;GPU 和显示控制器必须对它理解一致
fence / sync_file“某件事做完了”的通知;sync_file 是它的文件描述符形式
plane显示控制器的图层通道
CRTC混合各 plane 并按时序扫描输出的部件
atomic 提交一次性修改所有显示对象属性的 KMS 接口,全部生效或全部不生效
VSYNC屏幕开始刷新新一帧的时刻,切换 buffer 都对齐它