DRM,英文全称 Direct Rendering Manager, 即 直接渲染管理器。
DRM 是 linux 内核的一个子系统,它提供一组 API,用户空间程序可以通过它发送画面数据到 GPU 或者专用图形处理硬件(如高通的 MDP),也可以使用它执行诸如配置分辨率,刷新率之类的设置操作。原本是设计提供给 PC 使用来支持复杂的图形设备,后来也用于嵌入式系统上。目前在高通平台手机 Android 系统上的显示系统也是使用的这组 API 来完成画面的渲染更新。
在 DRM 之前 Linux 内核已经有一个叫 FBDEV 的 API,用于管理图形适配器的帧缓存区,但不能用于满足基于 3D 加速的现代基于 GPU 的视频硬件的需求,FBDEV 社区维护者也较少;且无法提供 overlay hw cursor 这样的 features; 开发者们本身就鼓励以后迁移到 DRM 上。
DRM 主要由如下部分组成:
KMS(Kernel Mode Setting): 主要是配置信息管理,如改变分辨率,刷新率,位深等DRI(Direct Rendering Infrastructure): 可以通过它直接访问一些硬件接口GEM(Graphics Execution Manager): 主要负责内存管理,CPU, GPU 对内存的访问控制由它来完成。DRM Driver in kernel side: 访问硬件
在高通平台上其中部分模块所处位置见下图:
其中 KMS 由 frame buffer, CRTC, Encoder, Connector 等组件组成
CRTC CRT controller, 目前主要用于显示控制,如显示时序,分辨率,刷新率等控制,还要承担将 framebuffer 内容送到 display, 更新 framebuffer 等。
负责将数据转换成合适的格式,送给 connector,比如 HDMI 需要 TMDS 信息,encoder 就将数据转成 HDMI 需要的 TMDS 格式。
它是具体某种显示接口的代表,如 hdmi, mipi 等。用于传输信号给外部硬件显示设备,探测外部显示设备接入。
一个 Plane 代表一个 image layer, 最终的 image 由一个或者多个 Planes 组成
在 Android 系统上 DRM 就是通过 KMS 一面接收 userspace 交付的应用画面,一面通过其 connector 来向屏幕提交应用所绘制的画面。
如下仅是示意代码,篇幅所限只摘取了完整代码中的部分关键代码,代码演示的是初始化,创建 surface, 在 surface 上作画(这里只是画了一张全红色的画面),然后通过 page flip 方式将画面更新到屏幕的过程。
1.定义一些全局变量:
......
#include <drm_fourcc.h>
#include <drm.h>
......
static drmModeCrtc *main_monitor_crtc;
static drmModeConnector *main_monitor_connector;
static int drm_fd = -1;
static drmModeRes *res = NULL;
2.打开 drm 设备节点 /dev/dri/card0
uint64_t cap = 0;
drm_fd = open("/dev/dri/card0", O_RDWR, 0);
ret = drmGetCap(drm_fd, DRM_CAP_DUMB_BUFFER, &cap);
res = drmModeGetResources(drm_fd);
3.找到所使用的 connector 及其 mode
int i = 0;
//find main connector
for(i = 0; i < res->count_connectors;i++) {
drmModeConnector *connector;
connector = drmModeGetConnector(drm_fd, res->connectors[i]);
if(connector) {
if((connector->count_modes > 0) && connector->connection == DRM_MODE_CONNECTED)) {
main_monitor_connector = connector;
break;
}
drmModeFreeConnector(connector);
}
}
......
uint32_t select_mode = 0;
for(int modes = 0; modes < main_monitor_connector->count_modes; modes++) {
if(main_monitor_connector->modes[modes].type & DRM_MODE_TYPE_PREFERRED) {
select_mode = modes;
break;
}
}
4.获取当前显示器的一些信息如宽高
drmModeEncoder *encoder = drmModeGetEncoder(drm_fd, main_monitor_connector->encoder_id);
int32_t crtc = encoder->crtc_id;
drmModeFreeEncoder(encoder);
drmModeCrtc *main_monitor_crtc = drmModeGetCrtc(fd, crtc);
main_monitor_crtc->mode = mina_monitor_connector->modes[select_mode];
int width = main_monitor_crtc->mode.hdisplay;
int height = main_monitor_crtc->mode.vdisplay
5.创建一个画布
struct GRSurface *surface;
struct drm_mode_create_dumb create_dumb;
uint32_t format;
int ret;
surface = (struct GRSurface*)calloc(1, sizeof(*surface));
format = DRM_FORMAT_ARGB8888;
......
create_dumb.height = height;
create_dumb.width = width;
create_dumb.bpp = drm_format_to_bpp(format);
create_dumb.flags = 0;
ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb);
surface->handle = create_dumb.handle;
......
//创建一个FrameBuffer
ret = drmModeAddFB2(drm_fd, width, height, fromat, handles, pitches, offsets, &(surface->fb_id), 0);
......
ret = drmIoCtl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb);
......
//这里通过mmap的方式把画布对应的buffer映射到本进程空间来
surface->data =(unsigned char*)mmap(NULL, height*create_dumb.pitch, PROT_READ|PROT_WRITE, MAP_SHARED, drm_fd, map_dumb.offset);
drmModeSetCrtc(drm_fd, main_monitor_crtc->crtc_id,
0, //fb_id
0, 0,//x,y
NULL, //connectors
0,//connector_count
NULL);//mode
6.在画布上作画,这里是画了一个纯红色的画面
int x, y;
unsigned char* p = surface->data;
for(y = 0; y < height; y++) {
unsigned char *px = p;
for(x = 0; x < width; x++){
*px++ = 255;//r
*px++ = 0; //g
*px++ = 0; //b
*px++ = 255; //a
}
p += surface->row_bytes;
}
7.通过 page flip 将画面送向屏幕
drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id, surface->fb_id, 0, NULL);
需要注意的是 /dev/dri/card0 的使用方式是独占的,也就是如果一个进程 open 了这个节点,其他进程是无法再打开的,在 android 平台测试时要运行测试程序时需要将原来的进程先 kill 掉,如在高通平台上要先 kill 掉这个进程:vendor.qti.hardware.display.composer-service,由于它会自动重启,所以要将它的执行文件 /vendor/bin/hw/vendor.qti.hardware.display.composer-service 重命名或删掉,才能做测试.
在本章节中我们认识了 linux 给 userspace 提供的屏幕操作的接口,通过一个简单例子粗略地了解了这些接口的一个用法,让我们知晓了可以通过这组 api 来向屏幕来提交我们所绘制的画面。那么在 Android 的 display 架构中是谁在使用这组 api 呢?
在 Android 系统上应用要绘制一个画面,首先要向 SurfaceFlinger 申请一个画布,这个画布所使用的 buffer 是 SurfaceFlinger 通过 allocator service(vendor.qti.hardware.display.allocator-service)来分配出来的,allocator service 是通过 ION 从 kernel 开辟出来的一块共享内存,这里申请的都是每个图层所拥有独立 buffer, 这个 buffer 会共享到 HWC Service 里,由 SurfaceFlinger 来作为中枢控制这块 buffer 的所有权,其所有权会随状态不同在 App, SurfaceFlinger, HWC Service 间来回流转。
而 HWC Service 正是那个使用 libdrm 和 kernel 打交道的人 ,它负责把 SurfaceFlinger 交来的图层做合成,将合成后的画画提交给 DRM 去显示。
应用首先通过 Surface 的接口向 SurfaceFlinger 申请一块 buffer, 需要留意的是 Surface 刚创建时是不会有 buffer 被 alloc 出来的,只有应用第一次要绘制画面时 dequeueBuffer 会让 SurfaceFlinger 去 alloc buffer, 在应用侧会通过 importBuffer 把这块内存映射到应用的进程空间来,这个过程可以在 systrace 上观察到:
之后 App 通过 dequeueBuffer 拿到画布, 通过 queueBuffer 来提交绘制好的数据, 这个过程可以在如下 systrace 上观察到:
HWC Service 负责将 SurfaceFlinger 送来的图层做合成,形成最终的画面,然后通过 drm 的接口更新到屏幕上去(注意:在 DRM 一章中给出的使用 DRM 的例子 demo 是通过 page flip 方式提交数据的,但 hwc service 使用的是另一 api atomic commit 的方式提交数据的,drm 本身并不只有一种方式提交画面)
HWC Service 的代码位置在 hardware/qcom/display, HWC Service 使用 libdrm 提交帧数据的地方我们可以在 systrace 上观察到:
而上图中的 DRMAtomicReq::Commit 会调用到 drmModeAtomicCommit 这个接口,该接口定义在 externel/libdrm/xf86drmMode.h, 其原型如下
........
extern int drmModeAtomicCommit(int fd, drmModeAtomicReqPtr req, uint32_t flags, void *user_data);
.......
PageFlip 方式的接口也是定义在这里:
........
extern int drmModePageFlip(int fd, uint32_t crtc_id, uint32_t fb_id, uint32_t flags, void *user_data);
........
hwc service 通过 drmModeAtomicCommit 接口向 kernel 提交合成数据:
代码位置位于:hardware/qcom/display/sde-drm/drm_atomic_req.cpp
int DRMAtomicReq::Commit(bool synchronous, bool retain_planes) {
DTRACE_SCOPED();//trace
......
int ret = drmModeAtomicCommit(fd_, drm_atomic_req_, flags, nullptr);
......
}
drmModeAtomicCommit 通过 ioctl 调用到 kernel:
kernel/msm-5.4/techpack/display/msm/msm_atomic.c
static void _msm_drm_commit_work_cb(struct kthread_work *work) {
......
SDE_ATRACE_BEGIN("complete_commit");
complete_commit(commit);
SDE_ATRACE_END("complete_commit");
......
}
static struct msm_commit *commit_init(struct drm_atomic_state *state, bool nonblock) {
......
kthread_init_work(&c->commit_work, _msm_drm_commit_work_cb);//将callback注册到commit_work
......
}
static void msm_atomic_commit_dispatch(struct drm_device *dev,
struct drm_atomic_state *state, struct msm_commit *commit) {
......
kthread_queue_work(&priv->disp_thread[j].worker, &commit->commit_work);//向消息队列中加入一个消息,disp thread处理到该消息时会调用到_msm_drm_commit_work_cb
......
}
drmModeAtomicCommit 的 ioctl 会触发 msm_atomic_commit_dispatch,然后通知 disp thread 也就是如下图所示的 crtc_commit 线程去处理这个消息,然后执行
complete_commit 函数,这个过程见下图:
在本章中我们了解了 APP 绘画的画布是由 SurfaceFlinger 提供的,而画布是一块共享内存,APP 向 SurfaceFlinger 申请到画布,是将共享内存的地址映射到自身进程空间。App 负责在画布上作画,画完的作品提交给 SurfaceFlinger, 这个提交操作并不是把内存复制一份给 SurfaceFlinger,而是把共享内存的控制权交还给 SurfaceFlinger, SurfaceFlinger 把拿来的多个应用的共享内存再送给 HWC Service 去合成, HWC Service 把合成的数据交给 DRM 去输出完成 app 画面显示到屏幕上的过程。为了更有效地利用时间这样的共享内存不止一份,可能有两份或三份,即常说的 double buffering, triple buffering.
那么我们就需要设计一个机制可以管理 buffer 的控制权,这个就是 BufferQueue.
[1]Android 画面显示流程分析 (1): https://www.jianshu.com/p/df46e4b39428
[2]Android 画面显示流程分析 (2): https://www.jianshu.com/p/f96ab6646ae3
[3]Android 画面显示流程分析 (3): https://www.jianshu.com/p/3c61375cc15b
[4]Android 画面显示流程分析 (4): https://www.jianshu.com/p/7a18666a43ce
[5]Android 画面显示流程分析 (5): https://www.jianshu.com/p/dcaf1eeddeb1
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8