1. 为什么我要在Linux下折腾libdrm显示程序
先说说我为什么会对libdrm这个东西产生兴趣。几年前我在做一个嵌入式项目,板子跑的是Linux,屏幕是一块MIPI DSI接口的竖屏,客户要求改成横屏显示。当时第一反应是去改应用层,结果发现应用层用的是Qt,旋转一下确实能显示,但性能掉得厉害,帧率直接腰斩。后来顺着驱动层往上查,才发现真正控制显示管线的是DRM子系统,而用户态跟DRM打交道最直接的入口就是libdrm。从那以后我就养成了一个习惯:凡是跟显示相关的底层问题,先看DRM,再看libdrm。
这篇文章要聊的,就是怎么从零开始,在Linux环境下用libdrm写一个最简单的DRM显示程序。说它简单,是因为它只做一件事:把一块内存缓冲区的内容刷到屏幕上。但麻雀虽小五脏俱全,DRM的核心概念——设备节点、资源枚举、CRTC、Encoder、Connector、Framebuffer、Dumb Buffer——全都会碰到。你把这套流程跑通了,后面再去看 Weston、Xorg、KMS 那些东西,会发现底层逻辑是一脉相承的。
适合谁看?如果你已经会写C语言,能在Linux下用gcc编译程序,对文件描述符、ioctl这些概念不陌生,那这篇文章就是给你准备的。如果你连Linux都没装过,建议先找个虚拟机把系统跑起来,把常用的命令行操作熟悉一下,再回来看。我不打算把每个系统调用都讲成教科书,而是按照一个实际能跑通的程序来拆解,每一步为什么这么做、踩过什么坑,都会说清楚。
需要提前说明的是,DRM这套东西在不同内核版本、不同显卡驱动上行为会有差异。我下面给出的代码和步骤,是在一台x86_64的机器上、用Intel核显、内核版本5.x以上的环境验证过的。如果你用的是NVIDIA闭源驱动或者比较老的ARM板子,部分细节可能需要微调,但整体思路是通用的。
2. 动手之前先把DRM这套机制理清楚
2.1 DRM到底管什么,libdrm又扮演什么角色
DRM全称Direct Rendering Manager,直译过来就是直接渲染管理器。它最早是为了解决多个程序同时访问显卡导致的冲突问题,后来逐渐演变成Linux下显示和渲染的统一框架。你可以把它想象成一个交通枢纽:上面跑着各种应用,下面连着各种显示硬件,DRM负责调度谁在什么时候用哪块资源。
内核里的DRM子系统分成两部分:一部分是通用的DRM核心,处理设备管理、内存管理、命令队列这些公共逻辑;另一部分是各个显卡厂商提供的驱动模块,比如i915对应Intel核显,amdgpu对应AMD显卡。这些驱动会向用户态暴露一个字符设备节点,通常是/dev/dri/card0,有时候还会有/dev/dri/renderD128这种专门用于渲染的节点。
libdrm就是用户态访问这些设备节点的封装库。它把那些又臭又长的ioctl调用包装成了相对友好的函数,比如drmModeGetResources、drmModeSetCrtc。没有libdrm你也能写DRM程序,但你就得自己拼ioctl结构体,那工作量不是一般的大。libdrm本身不包含任何显卡特定的逻辑,它只是把内核暴露的接口翻译了一遍,所以它非常轻量,编译出来也就几百KB。
这里要区分一个概念:DRM负责的是显示控制,也就是把画面送到屏幕上;而渲染是另一回事,通常由OpenGL、Vulkan这些图形API负责。我们这个程序只做显示,不做渲染,所以只需要libdrm就够了,不需要引入Mesa那一大套东西。
2.2 五个核心对象:CRTC、Encoder、Connector、Plane、Framebuffer
DRM的显示管线可以抽象成一条流水线,数据从内存出发,经过若干环节,最终到达屏幕。这条流水线上有五个关键角色,我一个个说。
Framebuffer是内存里的一块像素数据,你可以把它理解成一张画布。在DRM里,framebuffer是一个抽象概念,它指向一块实际的内存缓冲区,同时描述了这块缓冲区的格式(比如RGB888、ARGB8888)、宽度、高度、pitch(每行占多少字节)。我们程序里要显示的内容,就是往framebuffer对应的内存里写。
CRTC全称Cathode Ray Tube Controller,名字是历史遗留的,现在早就不是显像管了,但名字一直没改。它代表一个显示控制器,负责从framebuffer里读取像素数据,按照一定的时序(比如行同步、场同步信号)输出出去。一块显卡可能有多个CRTC,对应多个显示输出。你可以把CRTC想象成一个扫描仪,它一行一行地把framebuffer的内容扫出来。
Encoder是编码器,负责把CRTC输出的像素信号转换成特定接口需要的格式。比如HDMI、DisplayPort、MIPI DSI,每种接口的电气特性和信号格式都不一样,Encoder就是做这个转换的。一个CRTC可以接多个Encoder,但同一时刻通常只用一个。
Connector是连接器,代表物理上的显示接口,比如HDMI口、VGA口、笔记本的内屏排线。它负责检测显示器的插拔状态,读取EDID信息(里面包含显示器支持的分辨率、刷新率等)。Connector和Encoder是多对多的关系,一个Connector可以接受来自不同Encoder的信号。
Plane是图层,现代显示控制器都支持多个图层叠加。比如你可以有一个背景层、一个视频层、一个UI层,硬件自动把它们合成后输出。最简单的用法是只用主图层(Primary Plane),把framebuffer直接贴上去。
这五个对象的关系是:Framebuffer提供数据,Plane把Framebuffer挂到CRTC上,CRTC通过Encoder和Connector把信号送出去。我们写程序的时候,就是按照这个顺序去枚举资源、配置参数、最后提交。
2.3 为什么选Dumb Buffer而不是GBM
创建framebuffer需要一块内存缓冲区。在DRM里创建缓冲区有好几种方式,最常用的是Dumb Buffer和GBM Buffer。
Dumb Buffer是DRM提供的一种最基础的缓冲区,它的特点是:CPU可以直接访问(通过mmap映射),驱动保证它能被显示控制器读取,但不保证它能被GPU高效渲染。名字里的“dumb”就是“笨”的意思,因为它不支持硬件加速的渲染操作,只能由CPU往里写数据。
GBM全称Generic Buffer Management,是Mesa提供的一套缓冲区管理接口,它创建的缓冲区可以被GPU直接渲染,也能被显示控制器扫描输出,适合做高性能图形应用。
我们这个程序只是演示显示流程,不需要GPU渲染,所以用Dumb Buffer最合适。它简单、直接、依赖少,只要内核支持DRM,基本都能用。而且Dumb Buffer的创建流程很清晰:先通过ioctl申请一块内存,然后mmap到用户空间,往里面写像素,最后创建framebuffer对象指向这块内存。
注意:Dumb Buffer在有些驱动上可能不支持,比如某些NVIDIA的闭源驱动。如果你调用
DRM_IOCTL_MODE_CREATE_DUMB返回错误,可以试试用GBM,或者换一台机器测试。
3. 从零开始写一个能跑的DRM程序
3.1 环境准备和依赖安装
先把开发环境搭起来。我用的是一台装了Ubuntu 22.04的机器,内核是5.15。如果你用的是其他发行版,包管理命令可能不一样,但需要装的库是相同的。
第一步,确认你的系统里有DRM设备节点。打开终端,执行:
ls -l /dev/dri/正常的话你会看到card0、renderD128这样的节点。如果这个目录不存在,说明你的内核没有加载DRM驱动,或者你是在一个没有显卡的虚拟机里。这种情况下后面的步骤没法进行,需要先解决驱动问题。
第二步,安装libdrm的开发包。在Debian/Ubuntu系上:
sudo apt update sudo apt install libdrm-dev在Fedora/RHEL系上:
sudo dnf install libdrm-devel安装完成后,确认头文件位置:
ls /usr/include/libdrm/ ls /usr/include/xf86drm.h ls /usr/include/xf86drmMode.h你应该能看到xf86drm.h和xf86drmMode.h这两个关键头文件。前者包含基础的DRM操作接口,后者包含模式设置(Mode Setting)相关的接口。
第三步,确认编译器可用:
gcc --version如果没有gcc,装一下build-essential或者gcc包就行。
3.2 打开设备节点并获取DRM资源
程序的第一步是打开DRM设备节点。通常用/dev/dri/card0,但如果你的系统有多个显卡,可能需要试其他编号。打开的时候用O_RDWR标志,因为后面要执行ioctl操作。
int fd = open("/dev/dri/card0", O_RDWR | O_CLOEXEC); if (fd < 0) { perror("open /dev/dri/card0"); return -1; }O_CLOEXEC这个标志是为了在执行exec时自动关闭文件描述符,避免子进程继承不必要的句柄。这是个好习惯,建议加上。
打开设备后,需要告诉DRM我们要用它的模式设置功能。在老版本的内核里,需要调用drmSetMaster来获取主控权,但新版本内核里这个操作已经不需要了,直接调用drmModeGetResources就行。
drmModeRes *res = drmModeGetResources(fd); if (!res) { fprintf(stderr, "drmModeGetResources failed\n"); close(fd); return -1; }drmModeGetResources返回一个drmModeRes结构体,里面包含了当前设备的所有显示资源:CRTC列表、Connector列表、Encoder列表,以及framebuffer的数量范围。这个结构体是后面所有操作的基础,用完记得用drmModeFreeResources释放。
拿到资源后,先打印一下看看:
printf("CRTC count: %d\n", res->count_crtcs); printf("Connector count: %d\n", res->count_connectors); printf("Encoder count: %d\n", res->count_encoders);这一步很重要,因为不同设备的资源数量差别很大。比如我的Intel核显有3个CRTC、5个Connector,而有些嵌入式板子可能只有1个CRTC、1个Connector。你需要根据实际情况选择合适的对象。
3.3 找到可用的Connector和CRTC
接下来要找到一个已经连接了显示器的Connector。遍历res->connectors数组,对每个Connector调用drmModeGetConnector获取详细信息:
drmModeConnector *conn = NULL; for (int i = 0; i < res->count_connectors; i++) { conn = drmModeGetConnector(fd, res->connectors[i]); if (conn && conn->connection == DRM_MODE_CONNECTED && conn->count_modes > 0) { printf("Using connector %d, type %d\n", conn->connector_id, conn->connector_type); break; } if (conn) { drmModeFreeConnector(conn); conn = NULL; } } if (!conn) { fprintf(stderr, "No connected connector found\n"); drmModeFreeResources(res); close(fd); return -1; }这里判断两个条件:connection == DRM_MODE_CONNECTED表示显示器已经插上并且被检测到了;count_modes > 0表示这个显示器至少支持一种显示模式。两个条件都满足才用。
拿到Connector后,需要找一个能驱动它的Encoder。Connector结构体里有个encoder_id字段,如果这个字段不为0,说明当前已经有一个Encoder绑定在这个Connector上了,直接用就行。如果为0,就需要遍历res->encoders,找到possible_encoders里包含这个Connector的Encoder。
drmModeEncoder *enc = NULL; if (conn->encoder_id) { enc = drmModeGetEncoder(fd, conn->encoder_id); } if (!enc) { for (int i = 0; i < res->count_encoders; i++) { enc = drmModeGetEncoder(fd, res->encoders[i]); if (enc) { if (enc->possible_crtcs & (1 << 0)) { break; } drmModeFreeEncoder(enc); enc = NULL; } } }possible_crtcs是一个位掩码,表示这个Encoder可以连接到哪些CRTC。位0对应CRTC数组的第0个元素,位1对应第1个,以此类推。我这里简单起见直接选了位0,实际项目中应该根据Connector和CRTC的匹配关系来选。
找到Encoder后,就能确定用哪个CRTC了。enc->crtc_id如果非0,说明已经绑定了CRTC;否则需要从res->crtcs里选一个possible_crtcs允许的。
uint32_t crtc_id = enc->crtc_id; if (crtc_id == 0) { for (int i = 0; i < res->count_crtcs; i++) { if (enc->possible_crtcs & (1 << i)) { crtc_id = res->crtcs[i]; break; } } }到这里,我们已经确定了三个关键ID:connector_id、crtc_id,以及要用的显示模式。显示模式从conn->modes[0]取,这是Connector支持的首选模式,通常就是显示器的原生分辨率。
3.4 创建Dumb Buffer并映射到用户空间
现在开始创建缓冲区。Dumb Buffer的创建分两步:先申请内存,再映射。
struct drm_mode_create_dumb create_arg = {0}; create_arg.width = conn->modes[0].hdisplay; create_arg.height = conn->modes[0].vdisplay; create_arg.bpp = 32; create_arg.flags = 0; if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_arg) < 0) { perror("DRM_IOCTL_MODE_CREATE_DUMB"); return -1; }这里width和height取自显示模式,bpp设为32表示每个像素32位,也就是ARGB8888格式。创建成功后,create_arg里的handle、pitch、size字段会被内核填上。handle是这块缓冲区的标识,pitch是每行占的字节数(注意它不一定等于width乘以4,因为可能有对齐要求),size是总字节数。
接下来创建framebuffer对象,把它和刚才申请的缓冲区关联起来:
uint32_t fb_id; if (drmModeAddFB(fd, create_arg.width, create_arg.height, 24, 32, create_arg.pitch, create_arg.handle, &fb_id) < 0) { perror("drmModeAddFB"); return -1; }drmModeAddFB的参数里,24表示每个像素的颜色深度是24位(RGB各8位),32表示每个像素占32位存储空间(多出来的8位是alpha通道,但我们不用)。这两个值要跟前面的bpp对应好,否则显示会出问题。
然后把这快缓冲区映射到用户空间,这样我们才能往里面写像素:
struct drm_mode_map_dumb map_arg = {0}; map_arg.handle = create_arg.handle; if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_arg) < 0) { perror("DRM_IOCTL_MODE_MAP_DUMB"); return -1; } uint8_t *fb_map = mmap(NULL, create_arg.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map_arg.offset); if (fb_map == MAP_FAILED) { perror("mmap"); return -1; }map_arg.offset是内核返回的偏移量,mmap的时候要传这个值而不是0。映射成功后,fb_map就指向了那块显存,往里面写数据就等于往屏幕上画。
3.5 填充像素并提交显示
现在往缓冲区里写点东西。最简单的做法是画一个渐变或者纯色。我这里画一个红绿蓝的渐变,方便验证显示是否正常:
uint32_t *pixels = (uint32_t *)fb_map; int width = create_arg.width; int height = create_arg.height; int pitch_pixels = create_arg.pitch / 4; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { uint8_t r = (x * 255) / width; uint8_t g = (y * 255) / height; uint8_t b = 128; pixels[y * pitch_pixels + x] = (0xff << 24) | (r << 16) | (g << 8) | b; } }注意这里用pitch_pixels而不是width来算行偏移,因为pitch可能大于width乘以4。如果直接用width,图像会错位。
写完之后,调用drmModeSetCrtc把framebuffer提交上去:
if (drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &conn->connector_id, 1, &conn->modes[0]) < 0) { perror("drmModeSetCrtc"); return -1; }这个函数的参数依次是:设备fd、CRTC ID、framebuffer ID、显示起始坐标x和y、Connector ID数组、Connector数量、显示模式。调用成功后,屏幕应该立刻显示出我们画的渐变图案。
到这里,一个最基础的DRM显示程序就完成了。完整的代码我会在下一节给出,包括错误处理和资源释放。
4. 完整代码和编译运行方法
4.1 完整代码清单
把上面的片段拼起来,加上必要的头文件和错误处理,就是下面这个程序。我把它保存为drm_demo.c:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <xf86drm.h> #include <xf86drmMode.h> int main(int argc, char **argv) { const char *dev_path = "/dev/dri/card0"; if (argc > 1) { dev_path = argv[1]; } int fd = open(dev_path, O_RDWR | O_CLOEXEC); if (fd < 0) { perror("open"); return 1; } drmModeRes *res = drmModeGetResources(fd); if (!res) { fprintf(stderr, "drmModeGetResources failed\n"); close(fd); return 1; } printf("Resources: %d CRTCs, %d Connectors, %d Encoders\n", res->count_crtcs, res->count_connectors, res->count_encoders); drmModeConnector *conn = NULL; for (int i = 0; i < res->count_connectors; i++) { drmModeConnector *c = drmModeGetConnector(fd, res->connectors[i]); if (c && c->connection == DRM_MODE_CONNECTED && c->count_modes > 0) { conn = c; printf("Connector %u connected, %d modes\n", c->connector_id, c->count_modes); break; } if (c) drmModeFreeConnector(c); } if (!conn) { fprintf(stderr, "No connected connector\n"); drmModeFreeResources(res); close(fd); return 1; } drmModeEncoder *enc = NULL; if (conn->encoder_id) { enc = drmModeGetEncoder(fd, conn->encoder_id); } if (!enc) { for (int i = 0; i < res->count_encoders; i++) { drmModeEncoder *e = drmModeGetEncoder(fd, res->encoders[i]); if (e) { if (e->possible_crtcs & 1) { enc = e; break; } drmModeFreeEncoder(e); } } } if (!enc) { fprintf(stderr, "No suitable encoder\n"); drmModeFreeConnector(conn); drmModeFreeResources(res); close(fd); return 1; } uint32_t crtc_id = enc->crtc_id; if (crtc_id == 0) { for (int i = 0; i < res->count_crtcs; i++) { if (enc->possible_crtcs & (1 << i)) { crtc_id = res->crtcs[i]; break; } } } if (crtc_id == 0) { fprintf(stderr, "No suitable CRTC\n"); drmModeFreeEncoder(enc); drmModeFreeConnector(conn); drmModeFreeResources(res); close(fd); return 1; } drmModeModeInfo mode = conn->modes[0]; printf("Mode: %dx%d @ %dHz\n", mode.hdisplay, mode.vdisplay, mode.vrefresh); struct drm_mode_create_dumb create_arg = {0}; create_arg.width = mode.hdisplay; create_arg.height = mode.vdisplay; create_arg.bpp = 32; if (drmIoctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_arg) < 0) { perror("CREATE_DUMB"); goto cleanup; } uint32_t fb_id; if (drmModeAddFB(fd, create_arg.width, create_arg.height, 24, 32, create_arg.pitch, create_arg.handle, &fb_id) < 0) { perror("drmModeAddFB"); goto cleanup; } struct drm_mode_map_dumb map_arg = {0}; map_arg.handle = create_arg.handle; if (drmIoctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &map_arg) < 0) { perror("MAP_DUMB"); goto cleanup; } uint8_t *fb_map = mmap(NULL, create_arg.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, map_arg.offset); if (fb_map == MAP_FAILED) { perror("mmap"); goto cleanup; } uint32_t *pixels = (uint32_t *)fb_map; int pitch_pixels = create_arg.pitch / 4; for (int y = 0; y < (int)mode.vdisplay; y++) { for (int x = 0; x < (int)mode.hdisplay; x++) { uint8_t r = (x * 255) / mode.hdisplay; uint8_t g = (y * 255) / mode.vdisplay; uint8_t b = 128; pixels[y * pitch_pixels + x] = (0xff << 24) | (r << 16) | (g << 8) | b; } } if (drmModeSetCrtc(fd, crtc_id, fb_id, 0, 0, &conn->connector_id, 1, &mode) < 0) { perror("drmModeSetCrtc"); goto cleanup; } printf("Displaying gradient. Press Enter to exit.\n"); getchar(); cleanup: if (fb_map && fb_map != MAP_FAILED) { munmap(fb_map, create_arg.size); } if (fb_id) { drmModeRmFB(fd, fb_id); } if (create_arg.handle) { struct drm_mode_destroy_dumb destroy_arg = {0}; destroy_arg.handle = create_arg.handle; drmIoctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &destroy_arg); } if (enc) drmModeFreeEncoder(enc); if (conn) drmModeFreeConnector(conn); drmModeFreeResources(res); close(fd); return 0; }4.2 编译命令和Makefile
编译这个程序需要链接libdrm。最简单的命令是:
gcc -o drm_demo drm_demo.c -ldrm如果头文件不在默认路径,可能需要加-I和-L参数。在大多数发行版上,libdrm-dev装好后路径都是对的,直接编译就行。
为了方便,我一般会写个简单的Makefile:
CC = gcc CFLAGS = -Wall -O2 LDFLAGS = -ldrm drm_demo: drm_demo.c $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) clean: rm -f drm_demo .PHONY: clean编译成功后,直接运行:
sudo ./drm_demo为什么要sudo?因为访问/dev/dri/card0通常需要root权限,或者你的用户得在video组里。如果不想用sudo,可以把自己加到video组:
sudo usermod -aG video $USER然后重新登录生效。
4.3 运行效果和验证方法
程序运行后,屏幕上应该显示一个从左上角红色渐变到右下角绿色的图案,蓝色分量固定为128。按回车键程序退出,屏幕会恢复原来的显示内容(因为CRTC被释放了)。
如果屏幕没有变化,或者显示花屏,可以从这几个方面排查:
第一,确认你用的设备节点是对的。有些系统上card0可能对应的是没有连接显示器的显卡,试试card1或者用ls /dev/dri/by-path/看看每个节点的物理路径。
第二,确认Connector确实处于连接状态。可以在程序里加打印,看看选中的是哪个Connector。如果是笔记本,通常eDP接口是内屏,HDMI或DP是外接。
第三,检查pitch的处理。如果图像错位或者倾斜,多半是pitch没算对。可以打印create_arg.pitch和width * 4对比一下,如果不相等就说明有对齐填充。
第四,如果drmModeSetCrtc返回EINVAL,可能是模式不支持,或者CRTC已经被其他程序占用了。可以先关掉Xorg或者Weston再试。
5. 踩过的坑和常见问题排查
5.1 权限问题导致打开设备失败
这是新手最容易遇到的问题。直接运行程序,open返回Permission denied。原因很简单:/dev/dri/card0的默认权限是root:video 660,普通用户不在video组里就没法访问。
解决办法有两个:一是用sudo运行,简单粗暴但不推荐长期这样;二是把当前用户加入video组。执行sudo usermod -aG video $USER之后,一定要重新登录(或者用newgrp video开个新shell),否则组权限不会生效。
还有一种情况是SELinux或者AppArmor拦截了访问。如果确认权限没问题但还是打不开,可以看看dmesg里有没有avc denied的记录。不过这种情况在桌面发行版上比较少见,嵌入式系统上偶尔会遇到。
5.2 找不到可用的Connector
程序打印“No connected connector”,说明没有检测到任何已连接的显示器。可能的原因:显示器没开机、线没插好、或者驱动没正确识别。
可以先在终端里执行cat /sys/class/drm/card0-*/status,看看每个接口的状态。正常连接的话会显示connected,没插线显示disconnected,未知状态显示unknown。
如果所有接口都是disconnected,但显示器确实插着,那可能是驱动问题。比如某些USB-C转HDMI的扩展坞,需要特定的驱动支持才能识别。这种情况只能换接口或者更新内核。
另外要注意,有些虚拟机上模拟的DRM设备可能没有真实的Connector,或者Connector状态始终是unknown。这种环境下跑不了这个程序,需要找一台有真实显卡的机器。
5.3 drmModeSetCrtc返回EINVAL的几种原因
EINVAL是DRM程序里最常见的错误之一,原因很多,我列几个典型的:
- 模式不匹配:传进去的
drmModeModeInfo跟Connector实际支持的不一致。解决办法是直接用conn->modes[0],不要自己构造。 - CRTC已经被占用:如果Xorg或者Weston正在运行,它们已经控制了CRTC,你再调用就会失败。可以先切换到文本终端(Ctrl+Alt+F3),停掉显示服务再试。
- framebuffer格式不对:
drmModeAddFB时传的depth和bpp跟实际缓冲区不匹配。比如创建Dumb Buffer时bpp是32,AddFB时depth传了32而不是24,有些驱动会拒绝。 - Connector和Encoder不匹配:选错了Encoder,导致CRTC无法驱动这个Connector。检查
possible_crtcs和possible_clones字段。
排查的时候可以在每次调用后打印errno,结合strerror(errno)看具体描述。虽然DRM的错误信息不算特别详细,但至少能缩小范围。
5.4 显示花屏或颜色不对
如果图像能显示但颜色明显不对,比如红色和蓝色互换了,那多半是像素格式的问题。ARGB8888和ABGR8888在内存里的字节顺序是不一样的。我上面的代码用的是(0xff << 24) | (r << 16) | (g << 8) | b,对应的是ARGB格式,在小端机器上内存布局是B、G、R、A。如果你的设备用的是其他格式,需要调整移位顺序。
如果图像有斜纹或者错位,检查pitch。Dumb Buffer的pitch是内核根据硬件对齐要求算出来的,可能大于width * 4。写像素的时候必须用pitch来算行偏移,不能用width。
如果只有部分屏幕有内容,或者图像被拉伸,那可能是分辨率设置不对。确认mode.hdisplay和mode.vdisplay跟实际屏幕匹配。
5.5 程序退出后屏幕没有恢复
正常情况下,程序退出时CRTC会被释放,显示应该恢复到之前的状态。但如果程序异常崩溃,没有执行清理代码,CRTC可能还占着,屏幕会一直显示最后的内容。
这种情况下可以手动恢复:切换到其他虚拟终端再切回来,或者重启显示服务。如果是在嵌入式设备上,可能需要重启。
为了避免这个问题,写代码的时候一定要用goto cleanup的方式统一处理错误路径,确保任何情况下资源都能被释放。我上面的代码就是按这个模式写的。
6. 这个基础框架还能怎么扩展
把上面这个程序跑通之后,你就有了一个DRM显示的最小可用原型。基于这个框架,可以往几个方向扩展。
第一个方向是双缓冲和页面翻转(Page Flip)。现在的代码是直接写framebuffer然后提交,如果要在显示的同时更新内容,会出现撕裂。解决办法是创建两个framebuffer,一个正在显示,另一个在后台绘制,绘制完成后通过drmModePageFlip切换。这个机制在DRM里叫原子提交(Atomic Commit),是現代DRM推荐的方式。
第二个方向是支持多种像素格式。现在只用了ARGB8888,实际项目中可能需要RGB565、NV12(视频常用)等格式。drmModeAddFB2支持更丰富的格式指定,比drmModeAddFB更灵活。
第三个方向是结合GPU渲染。用Dumb Buffer的话只能CPU写像素,性能有限。如果要跑OpenGL或者Vulkan,需要用GBM创建缓冲区,然后通过DRM的prime机制共享给显示控制器。这套流程稍微复杂一些,但性能提升很明显。
第四个方向是处理热插拔事件。现在的程序启动时枚举一次资源就完事了,如果运行过程中显示器被拔掉或者换上新的,程序不会感知。DRM提供了drmModeGetConnector的轮询机制和udev事件,可以实现动态响应。
我自己在实际项目里,最常用的扩展是第一个和第三个。双缓冲解决撕裂问题,GBM解决性能问题,这两个加起来就能支撑一个基本的嵌入式显示应用了。至于更上层的窗口管理、输入处理,那就是Weston或者Qt的事情,不在libdrm的范畴里。
最后分享一个小技巧:调试DRM程序的时候,如果不想每次都sudo,可以临时改一下设备节点的权限:sudo chmod 666 /dev/dri/card0。但这只是权宜之计,重启后就失效了,正式环境还是老老实实配用户组。另外,drmModeGetResources返回的Connector顺序在不同内核版本上可能不一样,不要假设第一个就是主显示器,一定要根据connection状态和count_modes来判断。