努比亞技術團隊原創內容,轉載請務必注明出處。
Android畫面顯示流程分析(1)
Android畫面顯示流程分析(2)
Android畫面顯示流程分析(3)
Android畫面顯示流程分析(4)
Android畫面顯示流程分析(5)
3. DRM
DRM,英文全稱 Direct Rendering Manager, 即 直接渲染管理器。
DRM是linux內核的一個子系統,它提供一組API,用戶空間程序可以通過它發送畫面數據到GPU或者專用圖形處理硬件(如高通的MDP),也可以使用它執行諸如配置分辨率,刷新率之類的設置操作。原本是設計提供給PC使用來支持復雜的圖形設備,后來也用于嵌入式系統上。目前在高通平臺手機Android系統上的顯示系統也是使用的這組API來完成畫面的渲染更新。
在DRM之前Linux內核已經有一個叫FBDEV的API,用于管理圖形適配器的幀緩存區,但不能用于滿足基于3D加速的現代基于GPU的視頻硬件的需求,FBDEV社區維護者也較少; 且無法提供overlay hw cursor這樣的features; 開發者們本身就鼓勵以后遷移到DRM上。
3.1. 基本組件
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等。
Encoder
負責將數據轉換成合適的格式,送給connector,比如HDMI需要TMDS信息, encoder就將數據轉成HDMI需要的TMDS格式。
Connector
它是具體某種顯示接口的代表,如 hdmi, mipi等。用于傳輸信號給外部硬件顯示設備,探測外部顯示設備接入。
Planes
一個Plane代表一個image layer, 最終的image由一個或者多個Planes組成
在Android系統上DRM就是通過KMS一面接收userspace交付的應用畫面,一面通過其connector來向屏幕提交應用所繪制的畫面。
3.2. DRM使用示例
如下僅是示意代碼,篇幅所限只摘取了完整代碼中的部分關鍵代碼,代碼演示的是初始化,創建surface, 在surface上作畫(這里只是畫了一張全紅色的畫面),然后通過page flip方式將畫面更新到屏幕的過程。
- 定義一些全局變量:
......
#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;
- 打開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);
- 找到所使用的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;
}
}
- 獲取當前顯示器的一些信息如寬高
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
- 創建一個畫布
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
-
在畫布上作畫,這里是畫了一個純紅色的畫面
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; }
-
通過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 重命名或刪掉,才能做測試.
3.3. 本章小結
在本章節中我們認識了linux給userspace提供的屏幕操作的接口,通過一個簡單例子粗略地了解了這些接口的一個用法,讓我們知曉了可以通過這組api來向屏幕來提交我們所繪制的畫面。那么在Android的display架構中是誰在使用這組api呢?
4. Userspace的幀數據流
在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去顯示。
4.1. App到SurfaceFlinger
應用首先通過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本身并不只有一種方式提交畫面)
4.2. SurfaceFlinger到HWC Service
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);
........
4.3. HWC Service到kernel
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函數,這個過程見下圖:
4.4. 本章小結
在本章中我們了解了APP繪畫的畫布是由SurfaceFlinger提供的,而畫布是一塊共享內存,APP向SurfaceFlinger申請到畫布,是將共享內存的地址映射到自身進程空間。 App負責在畫布上作畫,畫完的作品提交給SurfaceFlinger, 這個提交操作并不是把內存復制一份給SurfaceFlinger,而是把共享內存的控制權交還給SurfaceFlinger, SurfaceFlinger把拿來的多個應用的共享內存再送給HWC Service去合成, HWC Service把合成的數據交給DRM去輸出完成app畫面顯示到屏幕上的過程。為了更有效地利用時間這樣的共享內存不止一份,可能有兩份或三份,即常說的double buffering, triple buffering.
那么我們就需要設計一個機制可以管理buffer的控制權,這個就是BufferQueue.