缘由
什么是 KMS ?
要了解 KMS,首先要了解 DRM。
DRM 是 Direct Rendering Manager 的缩写,最初只是用来支持 GPU 的,最初它负责:
Initialize GPU card, load its firmware, etc.
Share the GPU command queue between multiple applications
Manage the memory (allocation, access)
那时候,Mode-Setting (包括更新画面、配置 display pipeline、screen resolution、color depth、refresh rate等) 是在 Userspace 中实现的,这样做的缺点是:
Rendering 和 Mode-Setting 会发生竞争;
不统一,缺少抽象,不同的硬件平台各自为营;
后来就引入了 Kernel Mode-Setting (KMS),其实就是将 Mode-Setting 的活移回到内核,DRM driver 即负责访问 GPU 也负责访问 Display Engine,且将 KMS 作为 DRM API 的一部分,如下图:
目前我们关注最左边的路径就好,不用太关注 GEM、PRIME 等概念。
图形应用是如何进行显示的?
通常,一个普通的图形应用并不会直接通过 KMS 和内核进行交互,而是先和 display server (例如给予 X11 的 Xorg, 或者基于 Wayland 的 Weston,也称 display compositor) 进行交互:将显示的图像提交给 display server, 再由 display server 负责将多个 client 图形应用的图像合成成一张图像,并将这张图像通过 KMS 的接口提交给内核。
简而言之,就是 2 个步骤:
step1: 合成
step2: 提交给内核
何时要用 KMS ?
对于普通的图形应用,一般是不会直接去使用 KMS 的。
只有一些需要 low level control 的应用需要使用 KMS,例如:
Display servers;
Media players,例如 Kodi,它们有自己的 KMS backend,不需要额外的 display server;
游戏相关的应用,例如 RetroArch,为了支持更多的平台,它们会做到不依赖 display server;
VR、XR,它们对性能要求很严格,所以会亲自访问 KMS 以达到最小的延迟;
既然很少直接使用 KMS,为什么还要学习它 ?
DRM Driver 的驱动开发人员有必要了解 KMS api,这样才能理解 DRM Driver 的设计目的,从而编写出正确的驱动程序;
理解原理,可以协助我们定位图形应用不稳定或者性能相关的问题,尤其是嵌入式 Linux 领域,显示相关的功能复杂且容易出现异常;
如果你想要为开源软件 Wayland 或者 Kodi 做贡献的话,则需要了解 KMS api;
如果你的应用对性能和延迟性要求很高的话,也需要了解 KMS api。
如何编写 KMS Demo?
要编写 KMS 程序,首先要了解 KMS 的模型。
KMS 将硬件模块抽象成下面几个对象类型:
Planes:图层,例如在 rockchip 平台里对应 SOC 内部 VOP 模块的 win 图层;
CRTC:显示控制器,例如在 rockchip 平台里对应 SOC 内部的 VOP 模块;
Encoder:输出转换器,指 RGB、LVDS、DSI、eDP、HDMI、CVBS、VGA 等显示接口;
Connector:连接器,指 encoder 和 panel 之间交互的接口部分;
Bridge:桥接设备,一般用于注册 encoder 后面另外再接的转换芯片,如 DSI2HDMI 转换芯片;
Panel:泛指屏,各种 LCD、HDMI 等显示设备的抽象;
应用通过 KMS api 将这些对象连接成一条 display pipeline,最终将图像显示在屏幕上:
KMS 有两套 api: legacy api (已过时) 和 atomic api:
legacy api 虽说已经过时了,但是它其实是很适合 KMS api 初学者的,因为它仍然是基于 plane、crct、encoder、connector 这些核心概念的。atomic api 只是在 legacy api 基础上进行一些改进,待会会细说。
KMS legacy api 最简单示例:
int main(int argc, char **argv)
{
int fd;
drmModeConnector *conn;
drmModeRes *res;
uint32_t conn_id;
uint32_t crtc_id;
/* open the drm device */
fd = open("/dev/dri/card0");
/* get crtc/encoder/connector id */
res = drmModeGetResources(fd);
crtc_id = res->crtcs[0];
conn_id = res->connectors[0];
/* get connector for display mode */
conn = drmModeGetConnector(fd, conn_id);
/* create a dumb-buffer */
drmIoctl(DRM_IOCTL_MODE_CREATE_DUMB);
/* bind the dumb-buffer to an FB object */
drmModeAddFB(...);
/* map the dumb buffer for userspace drawing */
drmIoctl(DRM_IOCTL_MODE_MAP_DUMB);
mmap(...);
/* start display */
drmModeSetCrtc(crtc_id, fb_id, connector_id, mode);
}
大致的思路是:
通过 drmModeGetResources() 获取到 crtc、connector 等对象的 id,然后通过 id 获取到具体的 object;
通过 ioctl(DRM_IOCTL_MODE_CREATE_DUMB) 和 drmModeAddFB() 创建 DRM framebuffer object,并获得 fb id;
通过 ioctl(DRM_IOCTL_MODE_MAP_DUMB) 和 mmap() 将 framebuffer 映射到用户空间,应用将自己要显示的内容写到 framebuffer 中;
将 crtc、connector、fb 的id 通过 drmModeSetCrtc() 告诉 DRM driver,让内核帮我们配置好 display pipeline,从而将 framebuffer 里的内容显示出来;
关于 KMS atomic api:
atomic 的核心思想是将各种设置都保存在一个个的 property 里,最后将所有 property 一次性提交给内核,对于本次 commit 操作,要么成功,要么保持原来的状态完全不变。atomic 的好处在于可以避免操作到一半时中途失败后难以回滚的问题,同时也能避免设置期间屏幕闪烁的问题。
下面的代码同样也是将 crtc、connector 等对象连成一条 display pipeline,只不过这次用的是 atomic api。
req = drmModeAtomicAlloc();
drmModeAtomicAddProperty(req, crtc_id, property_active, 1);
drmModeAtomicAddProperty(req, crtc_id, property_mode_id, blob_id);
drmModeAtomicAddProperty(req, conn_id, property_crtc_id, crtc_id);
drmModeAtomicCommit(fd, req, DRM_MODE_ATOMIC_ALLOW_MODESET, NULL);
drmModeAtomicFree(req);
代码虽然增多了,但是能得到更好的用户体验。
更多内容:
KMS 功能比较多,api 也比较多,需要一系列的文章才能描述清楚,网上已经有一个比较好的教程,我就不再详细描述了。
请参考何小龙的 blog:
https://blog.csdn.net/hexiaolong2009/article/details/83720940
相关文章列表:
最简单的DRM应用程序 (single-buffer)
最简单的DRM应用程序 (double-buffer)
最简单的DRM应用程序 (page-flip)
最简单的DRM应用程序 (plane-test)
DRM应用程序进阶 (Property)
DRM应用程序进阶 (atomic-crtc)
DRM应用程序进阶 (atomic-plane)
其他适合学习 KMS 的开源软件
1. drminfo
https://github.com/ascent12/drm_info
drminfo 是一个命令行工具,它可以将系统里 DRM 设备的所有信息都 dump 出来,很适合用于调试。
编译:
$ apt-get install meson ninja-build
$ git clone https://github.com/ascent12/drm_info drm_info
$ cd drm_info
$ meson build/
$ ninja -C build install
用法:
# ./drm_info
Node: /dev/dri/card0
├───Driver: rockchip (RockChip Soc DRM) version 2.0.0 (20140818)
│ ├───DRM_CLIENT_CAP_STEREO_3D supported
│ ├───DRM_CLIENT_CAP_UNIVERSAL_PLANES supported
│ ├───DRM_CLIENT_CAP_ATOMIC supported
│ ├───DRM_CLIENT_CAP_ASPECT_RATIO supported
│ ├───DRM_CLIENT_CAP_WRITEBACK_CONNECTORS supported
│ ├───DRM_CAP_DUMB_BUFFER = 1
│ ├───DRM_CAP_VBLANK_HIGH_CRTC = 1
│ ├───DRM_CAP_DUMB_PREFERRED_DEPTH = 0
│ ├───DRM_CAP_DUMB_PREFER_SHADOW = 0
│ ├───DRM_CAP_PRIME = 3
│ ├───DRM_CAP_TIMESTAMP_MONOTONIC = 1
│ ├───DRM_CAP_ASYNC_PAGE_FLIP = 1
│ ├───DRM_CAP_CURSOR_WIDTH = 64
│ ├───DRM_CAP_CURSOR_HEIGHT = 64
│ ├───DRM_CAP_ADDFB2_MODIFIERS = 1
│ ├───DRM_CAP_PAGE_FLIP_TARGET = 0
│ ├───DRM_CAP_CRTC_IN_VBLANK_EVENT = 1
│ ├───DRM_CAP_SYNCOBJ = 0
│ └───DRM_CAP_SYNCOBJ_TIMELINE not supported
├───Device: platform rockchip,display-subsystem
│ └───Available nodes: primary, render
├───Framebuffer size
│ ├───Width: [0, 8192]
│ └───Height: [0, 8192]
├───Connectors
│ ├───Connector 0
│ │ ├───Object ID: 77
│ │ ├───Type: eDP
│ │ ├───Status: connected
│ │ ├───Physical size: 256x144 mm
│ │ ├───Subpixel: unknown
│ │ ├───Encoders: {0}
│ │ ├───Modes
│ │ │ └───1920x1080@60.00 nhsync nvsync
│ │ └───Properties
│ │ ├───"EDID" (immutable): blob = 0
│ │ ├───"DPMS": enum {On, Standby, Suspend, Off} = On
│ │ ├───"link-status": enum {Good, Bad} = Good
│ │ ├───"non-desktop" (immutable): range [0, 1] = 0
│ │ ├───"CRTC_ID" (atomic): object CRTC = 54
│ │ ├───"brightness": range [0, 100] = 50
│ │ ├───"contrast": range [0, 100] = 50
│ │ ├───"saturation": range [0, 100] = 50
│ │ └───"hue": range [0, 100] = 50
[...]
├───Encoders
│ ├───Encoder 0
│ │ ├───Object ID: 76
│ │ ├───Type: TMDS
│ │ ├───CRTCS: {0}
│ │ └───Clones: {}
│ ├───Encoder 1
│ │ ├───Object ID: 78
│ │ ├───Type: TMDS
│ │ ├───CRTCS: {0, 1}
│ │ └───Clones: {}
│ └───Encoder 2
│ ├───Object ID: 80
│ ├───Type: TMDS
│ ├───CRTCS: {1}
│ └───Clones: {}
├───CRTCs
│ ├───CRTC 0
│ │ ├───Object ID: 54
│ │ ├───Mode: 1920x1080@60.00 nhsync nvsync
│ │ ├───Gamma size: 256
│ │ └───Properties
│ │ ├───"ACTIVE" (atomic): range [0, 1] = 1
│ │ ├───"MODE_ID" (atomic): blob = 91
│ │ │ └───1920x1080@60.00 nhsync nvsync
│ │ ├───"OUT_FENCE_PTR" (atomic): range [0, UINT64_MAX] = 0
│ │ ├───"left margin": range [0, 100] = 100
│ │ ├───"right margin": range [0, 100] = 100
│ │ ├───"top margin": range [0, 100] = 100
│ │ ├───"bottom margin": range [0, 100] = 100
│ │ ├───"ALPHA_SCALE" (atomic): range [0, 1] = 1
│ │ └───"FEATURE" (immutable): bitmask {afbdc} = ()
[...]
└───Planes
├───Plane 0
│ ├───Object ID: 53
│ ├───CRTCs: {0}
│ ├───FB ID: 128
│ │ ├───Object ID: 128
│ │ ├───Size: 1920x1080
│ │ ├───Pitch: 7680 bytes
│ │ ├───Bits per pixel: 32
│ │ └───Depth: 24
│ ├───Formats:
│ │ ├───XRGB8888 (0x34325258)
│ │ ├───ARGB8888 (0x34325241)
│ │ ├───XBGR8888 (0x34324258)
│ │ ├───ABGR8888 (0x34324241)
│ │ ├───RGB888 (0x34324752)
│ │ ├───BGR888 (0x34324742)
│ │ ├───RGB565 (0x36314752)
│ │ └───BGR565 (0x36314742)
│ └───Properties
│ ├───"type" (immutable): enum {Overlay, Primary, Cursor} = Primary
│ ├───"FB_ID" (atomic): object framebuffer = 128
│ │ ├───Object ID: 128
│ │ ├───Size: 1920x1080
│ │ ├───Pitch: 7680 bytes
│ │ ├───Bits per pixel: 32
│ │ └───Depth: 24
│ ├───"IN_FENCE_FD" (atomic): srange [-1, INT32_MAX] = -1
│ ├───"CRTC_ID" (atomic): object CRTC = 54
│ ├───"CRTC_X" (atomic): srange [INT32_MIN, INT32_MAX] = 0
│ ├───"CRTC_Y" (atomic): srange [INT32_MIN, INT32_MAX] = 0
│ ├───"CRTC_W" (atomic): range [0, INT32_MAX] = 1920
│ ├───"CRTC_H" (atomic): range [0, INT32_MAX] = 1080
│ ├───"SRC_X" (atomic): range [0, UINT32_MAX] = 0
│ ├───"SRC_Y" (atomic): range [0, UINT32_MAX] = 0
│ ├───"SRC_W" (atomic): range [0, UINT32_MAX] = 1920
│ ├───"SRC_H" (atomic): range [0, UINT32_MAX] = 1080
│ ├───"ZPOS" (atomic): range [0, 3] = 0
│ ├───"FEATURE" (immutable): bitmask {scale, alpha, hdr2sdr, sdr2hdr, afbdc} = (alpha | afbdc)
│ ├───"EOTF" (atomic): range [0, 5] = 0
│ ├───"COLOR_SPACE" (atomic): range [0, 12] = 0
│ ├───"GLOBAL_ALPHA" (atomic): range [0, UINT8_MAX] = 255
│ ├───"BLEND_MODE" (atomic): range [0, 1] = 0
│ ├───"ASYNC_COMMIT" (atomic): range [0, 1] = 0
│ └───"SHARE_ID" (atomic): range [0, UINT32_MAX] = 53
[...]
2、libdrm 自带的测试程序:modetest
https://gitlab.freedesktop.org/mesa/drm
modetest 是由 libdrm 提供的测试程序,可以查询显示设备的支持状况,进行基本的显示测试,以及设置显示的模式。
编译:
$ apt-get install meson ninja-build
$ git clone https://gitlab.freedesktop.org/mesa/drm libdrm
$ cd libdrm
$ meson build/
$ ninja -C build install
会生成库文件和测试程序:
libkms
tests/ # 包含 modetest
libdrm.so.2.4.0
libdrm.so.2
libdrm.so
用法:
// 在 edp 屏上显示测试画面
$ modetest -M rockchip -s 77@54:1920x1080
setting mode 1920x1080-60.00Hz on connectors 77, crtc 54
// 在 hdmi 屏上显示测试画面
$ modetest -M rockchip -s 81@65:1920x1080
setting mode 1920x1080-60.00Hz on connectors 81, crtc 65
参数说明:
-M
:用于指定访问哪个 DRM 设备;-s <connector_id>[,<connector_id>][@<crtc_id>]:[#<mode index>]<mode>[-<vrefresh>][@<format>]
:用于在指定的 pipeline 上以某个 mode 显示某个 pattern 的画面。
3、kmscube
https://gitlab.freedesktop.org/mesa/kmscube/
kmscube 是一个演示程序,用于说明如何在没有 X11、wayland 等 compositor 的情况下编写 bare metal 图形应用。它使用了 DRM/KMS(kernel mode setting)、GBM(graphics buffer manager)和 EGL 来使用 OpenGL 或 OpenGL ES 渲染内容。
编译:
$ apt-get install meson ninja-build
$ git clone https://gitlab.freedesktop.org/mesa/kmscube/ kmscube
$ cd kmscube
$ meson build/
$ ninja -C build install
用法:
$ ./kmscube
还有很多优秀的开源软件,例如 Wayland 的参考实现 Weston,媒体播放器 Kodi、复古游戏模拟器前端 RetroArch 等,都是我们学习 KMS api 的优秀学习资料,感兴趣的小伙伴可以自行研究一波。
到此,KMS api 的基础知识就介绍完毕了,感谢阅读!
—— The End ——
推荐阅读: