努比亞技術(shù)團(tuán)隊(duì)原創(chuàng)內(nèi)容,轉(zhuǎn)載請(qǐng)務(wù)必注明出處。
8. 應(yīng)用是如何繪圖的
目前很多游戲類應(yīng)用都是借由SurfaceView申請(qǐng)到畫(huà)布,然后自主上幀,并不依賴Vsync信號(hào), 所以本章通過(guò)幾個(gè)helloworld示例來(lái)看下應(yīng)用側(cè)是如何繪圖和上幀的。
由于java層很多接口是對(duì)C層接口的JNI封裝,這里我們只看一些C層接口的用法。下面的示例代碼為縮減篇幅把一些異常處理部分的代碼去除了,只保留了重要的部分,如果讀者需要執(zhí)行示例代碼,可以自行加入一些異常處理部分。
8.1. 無(wú)圖形庫(kù)支持下的繪圖
下面的示例中演示的是如何使用C層接口向SurfaceFlinger申請(qǐng)一塊畫(huà)布,然后不使用任何圖形庫(kù),直接修改畫(huà)布上的像素值,最后提交給SurfaceFlinger顯示。
int main()
{
sp<ProcessState> proc(ProcessState::self());
ProcessState::self()->startThreadPool();//在應(yīng)用和SurfaceFlinger溝通過(guò)程中要使用到binder, 所以這里要先初始化binder線程池
sp<SurfaceComposerClient> client = new SurfaceComposerClient();//SurfaceComposerClient是SurfaceFlinger在應(yīng)用側(cè)的代表, SurfaceFlinger的接口通過(guò)它來(lái)提供
client->initCheck();
//先通過(guò)createSurface接口來(lái)申請(qǐng)一塊畫(huà)布,參數(shù)里包含對(duì)畫(huà)布起的名字,大小,位深信息
sp<SurfaceControl> surfaceControl = client->createSurface(String8("Console Surface"),800, 600, PIXEL_FORMAT_RGBA_8888);
SurfaceComposerClient::Transaction t;
t.setLayer(surfaceControl, 0x40000000).apply();
//通過(guò)getSurface接口獲取到Surface對(duì)象
sp<Surface> surface = surfaceControl->getSurface();
ANativeWindow_Buffer buffer;
//通過(guò)Surface的lock方法調(diào)用到dequeueBuffer,獲取到一個(gè)BufferQueue可用的Slot
status_t err = surface->lock(&buffer, NULL);// &clipRegin
void* addr = buffer.bits;
ssize_t len = buffer.stride * 4 * buffer.height;
memset(addr, 255, len);//這里繪圖,由于我們沒(méi)有使用任何圖形庫(kù),所以這里把內(nèi)存填成255, 畫(huà)一個(gè)純色畫(huà)面
surface->unlockAndPost();//這里會(huì)調(diào)用到queueBuffer,把我們繪制好的畫(huà)面提交給SurfaceFlinger
printf("sleep...\n");
usleep(5 * 1000 * 1000);
surface.clear();
surfaceControl.clear();
printf("complete. CTRL+C to finish.\n");
IPCThreadState::self()->joinThreadPool();
return 0;
}
在上面的示例中,幾個(gè)關(guān)建點(diǎn)是,第一步,先創(chuàng)建出一個(gè)SurfaceComposerClient,它是我們和Surfaceflinger溝通的橋梁,第二步,通過(guò)SurfaceComposerClient的createLayer接口創(chuàng)建一個(gè)SurfaceControl,這是我們控制Surface的一個(gè)工具,第三步,從SurfaceControl的getSurface接口來(lái)獲取Surface對(duì)象,這是我們操作BufferQueue的接口。
有了Surface對(duì)象,我們可以通過(guò)Surface的lock方法來(lái)dequeueBuffer, 再通過(guò)unlockAndPost接口來(lái)queueBuffer, 循環(huán)執(zhí)行,我們就可以對(duì)畫(huà)布進(jìn)行連續(xù)繪制和提交數(shù)據(jù)了,屏幕上動(dòng)態(tài)的畫(huà)面就出來(lái)了。
所以對(duì)于SurfaceFlinger或者說(shuō)對(duì)于Display系統(tǒng)底層所提供的接口主要就是這三個(gè)SurfaceComposerClient, SurfaceControl和Surface. 這里我們不妨稱其為Display系統(tǒng)接口三大件。
8.2. 有圖形庫(kù)支持下的繪圖
在上節(jié)示例中,我們并沒(méi)有去繪畫(huà)復(fù)雜的圖案,只是使用內(nèi)存填充的方式畫(huà)了一個(gè)純色畫(huà)面,在本節(jié)中我們將嘗試使用圖形庫(kù)在給定的畫(huà)布上畫(huà)一些復(fù)雜的圖案,比如畫(huà)一張圖片上去。
在上節(jié)的討論中我們知道要畫(huà)畫(huà)面出來(lái),要拿到Display的三大件(SurfaceComposerClient, SurfaceControl和Surface),接下來(lái)拿到畫(huà)布后我們使用skia庫(kù)來(lái)畫(huà)一張圖片到屏幕上。
using namespace android;
//先寫一個(gè)函數(shù)把圖片轉(zhuǎn)成一個(gè)bitmap
static status_t initBitmap(SkBitmap* bitmap, const char* fileName) {
if (fileName == NULL) {
return NO_INIT;
}
sk_sp<SkData> data = SkData::MakeFromFileName(fileName);
sk_sp<SkImage> image = SkImage::MakeFromEncoded(data);
bool result = image->asLegacyBitmap(bitmap, SkImage::kRO_LegacyBitmapMode);
if(!result ){
printf("decode picture fail!");
return NO_INIT;
}
return NO_ERROR;
}
int main()
{
sp<ProcessState> proc(ProcessState::self());
ProcessState::self()->startThreadPool();//和上一示例一樣要開(kāi)啟binder線程池
// create a client to surfaceflinger
sp<SurfaceComposerClient> client = new SurfaceComposerClient();//三大件第一件
client->initCheck();
sp<SurfaceControl> surfaceControl = client->createSurface(String8("Consoleplayer Surface"),800, 600, PIXEL_FORMAT_RGBA_8888);//三大件第二件
SurfaceComposerClient::Transaction t;
t.setLayer(surfaceControl, 0x40000000).apply();
sp<Surface> surface = surfaceControl->getSurface();//三大件第三件
sp<IGraphicBufferProducer> graphicBufferProducer = surface->getIGraphicBufferProducer();
ANativeWindow_Buffer buffer;
status_t err = surface->lock(&buffer, NULL);//調(diào)用dequeueBuffer把buffer拿來(lái)
SkBitmap* bitmapDevice = new SkBitmap;
SkIRect* updateRect = new SkIRect;
SkBitmap* bitmap = new SkBitmap;
initBitmap(bitmap, "/sdcard/picture.png");//從文件讀一個(gè)bitmap出來(lái)
printf("decode picture done.\n");
ssize_t bpr = buffer.stride * bytesPerPixel(buffer.format);
SkColorType config = convertPixelFormat(buffer.format);
bitmapDevice->setInfo(SkImageInfo::Make(buffer.width, buffer.height, config, kPremul_SkAlphaType), bpr);
//上面我們創(chuàng)建了另一個(gè)SkBitmap對(duì)象bitmapDevice
if (buffer.width > 0 && buffer.height > 0) {
bitmapDevice->setPixels(buffer.bits);//這里把幀緩沖區(qū)buffer的地址設(shè)給了bitmapDevice,這時(shí)和bitmapDevice畫(huà)東西就是在向幀緩沖區(qū)buffer畫(huà)東西
} else {
bitmapDevice->setPixels(NULL);
}
//SkRegion region;
printf("to create canvas..\n");
SkCanvas* nativeCanvas = new SkCanvas(*bitmapDevice);
SkRect sr;
sr.set(*updateRect);
nativeCanvas->clipRect(sr);
SkPaint paint;
nativeCanvas->clear(SK_ColorBLACK);
const SkRect dst = SkRect::MakeXYWH(0,0,800, 600);
paint.setAlpha(255);
const SkIRect src1 = SkIRect::MakeXYWH(0, 0, bitmap->width(), bitmap->height());
printf("draw ....\n");
nativeCanvas->drawBitmapRect((*bitmap), src1, dst, &paint);//調(diào)用SkCanvas的drawBitmapRect把圖片畫(huà)到bitmapDevice,也就是畫(huà)到了從Surface申請(qǐng)到的幀緩沖區(qū)buffer中
surface->unlockAndPost();//調(diào)用queueBuffer把buffer提交給SurfaceFlinger顯示
printf("sleep...\n");
usleep(10 * 1000 * 1000);
surface.clear();
surfaceControl.clear();
printf("test complete. CTRL+C to finish.\n");
IPCThreadState::self()->joinThreadPool();
return 0;
}
在上面的示例中獲取到幀緩沖區(qū)buffer的方式和上一個(gè)例子是一樣的,不同點(diǎn) 是我們把申請(qǐng)到的buffer的地址空間給到了skia庫(kù),然后我們通過(guò)skia提供的操作接口把一張圖片畫(huà)到了幀緩沖區(qū)buffer中,由此可以看出我們想使用圖形庫(kù)來(lái)操作幀緩沖區(qū)的關(guān)鍵是要把幀緩沖區(qū)buffer的地址對(duì)接到圖形庫(kù)提供的接口上。
在android平臺(tái)上,我們通常不會(huì)直接使用CPU去繪圖,通常是調(diào)用opengl或其他圖形庫(kù)去指揮GPU去做這些繪圖的事情,那么又是如何使用opengl庫(kù)來(lái)完成繪圖的呢?
8.3. 使用OpenGL&EGL的繪圖
由上面第二個(gè)例子可知,要想使用一個(gè)圖形庫(kù)來(lái)向幀緩沖區(qū)buffer繪圖的關(guān)建是要把對(duì)應(yīng)的buffer給到圖形庫(kù), 我們知道opengl是一套設(shè)備無(wú)關(guān)的api接口,它和平臺(tái)是無(wú)關(guān)的,所以和Surface接口的任務(wù)是由EGL庫(kù)來(lái)完成的,幀緩沖區(qū)buffer要和EGL庫(kù)對(duì)接。
在hwui繪圖中是以如下結(jié)構(gòu)對(duì)接的:
首先EGL庫(kù)會(huì)提供一個(gè)EGLSurface的對(duì)象,這個(gè)對(duì)象是對(duì)三大件中的Surface的一個(gè)封裝,它本身與幀提交相關(guān)部分提供了兩個(gè)接口:dequeue/queue,分別對(duì)應(yīng)Surface的dequeueBuffer和queueBuffer.
下面我們通過(guò)一個(gè)示例來(lái)看下它在C層是如何使用和與三大件對(duì)接的:
using namespace android;
int main()
{
sp<ProcessState> proc(ProcessState::self());
ProcessState::self()->startThreadPool();//同樣地開(kāi)啟binder線程池
// create a client to surfaceflinger
sp<SurfaceComposerClient> client = new SurfaceComposerClient();//三大件第一件
client->initCheck();
sp<SurfaceControl> surfaceControl = client->createSurface(String8("Consoleplayer Surface"),800, 600, PIXEL_FORMAT_RGBA_8888);//三大件第二件
SurfaceComposerClient::Transaction t;
t.setLayer(surfaceControl, 0x40000000).apply();
sp<Surface> surface = surfaceControl->getSurface();//三大件第三件
// initialize opengl and egl
const EGLint attribs[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_DEPTH_SIZE, 0,
EGL_NONE
};
//開(kāi)始初始化EGL庫(kù)
EGLint w, h;
EGLSurface eglSurface;
EGLint numConfigs;
EGLConfig config;
EGLContext context;
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, 0, 0);
eglChooseConfig(display, attribs, &config, 1, &numConfigs);
eglSurface = eglCreateWindowSurface(display, config, surface.get(), NULL);//創(chuàng)建eglSurface(對(duì)Surface的一個(gè)封裝)
context = eglCreateContext(display, config, NULL, NULL);
eglQuerySurface(display, eglSurface, EGL_WIDTH, &w);
eglQuerySurface(display, eglSurface, EGL_HEIGHT, &h);
if (eglMakeCurrent(display, eglSurface, eglSurface, context) == EGL_FALSE)//會(huì)調(diào)用dequeue以獲取幀緩沖區(qū)buffer
return NO_INIT;
glShadeModel(GL_FLAT);
glDisable(GL_DITHER);
glDisable(GL_SCISSOR_TEST);
//draw red
glClearColor(255,0,0,1);//這里用opengl庫(kù)來(lái)一個(gè)純紅色的畫(huà)面
glClear(GL_COLOR_BUFFER_BIT);
eglSwapBuffers(display, eglSurface);//這里會(huì)調(diào)用到Surface的queueBuffer方法,提交畫(huà)好的幀緩沖區(qū)數(shù)據(jù)
printf("sleep...\n");
usleep(10 * 1000 * 1000);
surface.clear();
surfaceControl.clear();
printf("test complete. CTRL+C to finish.\n");
IPCThreadState::self()->joinThreadPool();
return 0;
}
在上面的例子中我們看到了opengl&egl庫(kù)對(duì)幀緩沖區(qū)buffer的使用方式,首先和8.1的示例中一樣從三大件中獲取的幀緩沖區(qū)操作接口,只是這里我們不再直接使用該接口,而是把Surface對(duì)象給到EGL庫(kù),由EGL庫(kù)去使用它,我們使用opengl 的api來(lái)間接操作幀緩沖區(qū)buffer,這些操作包括申請(qǐng)新的BufferQueue slot和提交繪制好的BufferQueue slot.
本章小結(jié)
本章我們通過(guò)三個(gè)示例程序了解了下display部分給應(yīng)用層設(shè)計(jì)的接口,了解到了通過(guò)三大件可以拿到幀緩沖區(qū)buffer, 之后應(yīng)用如何作畫(huà)就是應(yīng)用層的事情了,應(yīng)用可以選擇不使用圖形庫(kù),也可以選擇圖形庫(kù)讓cpu來(lái)作畫(huà),也可以使用像opengl&egl這樣的庫(kù)來(lái)指揮GPU來(lái)作畫(huà)。
9. 應(yīng)用畫(huà)面更新總結(jié)
通過(guò)以上章節(jié)的了解,APP的畫(huà)面要顯示到屏幕上大致上要經(jīng)過(guò)如下圖所示系統(tǒng)組件的處理:
首先App向SurfaceFlinger申請(qǐng)畫(huà)布(通過(guò)dequeueBuffer接口),SurfaceFlinger內(nèi)部有一個(gè)BufferQueue的管理實(shí)體,它會(huì)分配一個(gè)GraphicBuffer給到APP, App拿到buffer后調(diào)用圖形庫(kù)向這塊buffer內(nèi)繪畫(huà)。
APP繪畫(huà)完成后使用向SurfaceFlinger提交繪制完成的buffer(通過(guò)queueBuffer接口), 當(dāng)然這時(shí)候的繪制完成只是說(shuō)在CPU側(cè)繪制完成,此時(shí)GPU可能還在該buffer上作畫(huà),所以這時(shí)向SurfaceFlinger提交數(shù)據(jù)的同時(shí)還會(huì)帶上一個(gè)acquireFence,使用接下來(lái)使用該buffer的人能知道什么時(shí)候buffer使用完畢了。
SurfaceFlinger收到應(yīng)用提交的幀緩沖區(qū)buffer后是在下一個(gè)vsync-sf信號(hào)來(lái)時(shí)做處理,首先遍歷所有的Layer, 找到哪些Layer有上幀, 通過(guò)acquireBuffer把Buffer拿出來(lái),通知給HWC Service去參與合成, 最后調(diào)用HWC Service的presentDisplay接口來(lái)告知HWC Service SurfaceFlinger的工作已完成。
HWC Service收到合成任務(wù)后開(kāi)始合成數(shù)據(jù),在SurfaceFlinger調(diào)用presetDisplay時(shí)會(huì)去調(diào)用DRM接口DRMAtomicReq::Commit通知kernel可以向DDIC發(fā)送數(shù)據(jù)了.
如果有TE信號(hào)來(lái)提示已進(jìn)入消隱區(qū),這時(shí)DRM驅(qū)動(dòng)會(huì)馬上開(kāi)始通過(guò)DSI總線向DDIC傳輸數(shù)據(jù),與此同時(shí)Panel的Disp Scan也在進(jìn)行中,傳輸完成后這幀畫(huà)面就完整地顯示到了屏幕上。
至此,一幀畫(huà)面的更新過(guò)程就完成了,我們這里講了這么久的一個(gè)復(fù)雜的過(guò)程,其實(shí)在高刷手機(jī)上一秒鐘要重復(fù)做100多次!_
10. 結(jié)語(yǔ)
Android的Display系統(tǒng)是Android平臺(tái)上一個(gè)相對(duì)比較復(fù)雜的系統(tǒng),文中所述均是筆者通過(guò)閱讀源碼、閱讀網(wǎng)上其他人分享的文章、平時(shí)工作中的感悟以及在工作中向同事請(qǐng)教總結(jié)而來(lái)。限于自身的知識(shí)結(jié)構(gòu)和技術(shù)背景,未必有些理解是正確的,請(qǐng)讀者閱讀過(guò)程中多思考,多以源碼為準(zhǔn),文中所述請(qǐng)僅做參考。文中有不正確的地方也歡迎大家批評(píng)指正。
特別感謝如下作者的知識(shí)分享:
作者: ariesjzj 題目:《Android中的GraphicBuffer同步機(jī)制-Fence》 地址:https://blog.csdn.net/jinzhuojun/article/details/39698317
作者:-Yaong- 題目:《linux GPU上多個(gè)buffer間的同步之ww_mutex、dma_fence的使用 筆記》地址https://www.cnblogs.com/yaongtime/p/14332526.html
作者:lyf 題目《android graphic(16)—fence(簡(jiǎn)化)》 地址:https://zhuanlan.zhihu.com/p/68782817
作者:何小龍 題目:《LCD顯示異常分析——撕裂(tear effect)》 地址:https://blog.csdn.net/hexiaolong2009/article/details/79319512?spm=1001.2014.3001.5501
作者:迅猛一只虎 題目:《LCD timing 時(shí)序參數(shù)總結(jié)》 地址:https://blog.csdn.net/wending1986/article/details/106837597
作者:kerneler_ 題目:《LCD屏?xí)r序分析》 地址:https://blog.csdn.net/skyflying2012/article/details/8553893