Android畫面顯示流程分析(2)

努比亞技術團隊原創內容,轉載請務必注明出處。

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: 訪問硬件

在高通平臺上其中部分模塊所處位置見下圖:

image-20210915104207418.png

其中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方式將畫面更新到屏幕的過程。

  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;
  1. 打開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);
  1. 找到所使用的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;
        }
    }
  1. 獲取當前顯示器的一些信息如寬高
 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
  1. 創建一個畫布
 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    
  1. 在畫布上作畫,這里是畫了一個純紅色的畫面

        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;
        }
    
  2. 通過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間來回流轉。


image-20210915175702338.png

而HWC Service正是那個使用libdrm和kernel打交道的人 ,它負責把SurfaceFlinger交來的圖層做合成,將合成后的畫畫提交給DRM去顯示。

4.1. App到SurfaceFlinger

應用首先通過Surface的接口向SurfaceFlinger申請一塊buffer, 需要留意的是Surface剛創建時是不會有buffer被alloc出來的,只有應用第一次要繪制畫面時dequeueBuffer會讓SurfaceFlinger去alloc buffer, 在應用側會通過importBuffer把這塊內存映射到應用的進程空間來,這個過程可以在systrace上觀察到:


image-20210916102456850.png

之后App通過dequeueBuffer拿到畫布, 通過queueBuffer來提交繪制好的數據, 這個過程可以在如下systrace上觀察到:


image-20210915204636105.png

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上觀察到:

image-20210915202625562.png

而上圖中的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函數,這個過程見下圖:

image-20210916094646924.png

4.4. 本章小結

在本章中我們了解了APP繪畫的畫布是由SurfaceFlinger提供的,而畫布是一塊共享內存,APP向SurfaceFlinger申請到畫布,是將共享內存的地址映射到自身進程空間。 App負責在畫布上作畫,畫完的作品提交給SurfaceFlinger, 這個提交操作并不是把內存復制一份給SurfaceFlinger,而是把共享內存的控制權交還給SurfaceFlinger, SurfaceFlinger把拿來的多個應用的共享內存再送給HWC Service去合成, HWC Service把合成的數據交給DRM去輸出完成app畫面顯示到屏幕上的過程。為了更有效地利用時間這樣的共享內存不止一份,可能有兩份或三份,即常說的double buffering, triple buffering.

那么我們就需要設計一個機制可以管理buffer的控制權,這個就是BufferQueue.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容