Android圖形顯示系統——上層顯示2:硬件加速實現

Android界面繪製的硬件加速實現

Android的界面繪製的硬件加速採取上下整合的一套流程實現

一、代碼結構

硬件加速結構

(一)Java

HardwareRenderer->ThreadedRenderer:組織硬件加速渲染的類,下發創建顯示列表和回放的指令。
GLES20RecordngCanvas GLES20Canvas HardWareCanvas:與Canvas平級的UI渲染引擎支持,但這個Canvas只能存儲命令到顯示列表中,並在ThreadedRenderer中的渲染線程輔助下運行。
RenderNode:所有View對應一個構成一個RenderNode
RenderNodeAnimator:動畫用
HardwareLayer:調saveLayer時產生,緩存繪製內容爲一個Layer

(二)Hwui

DisplayListRenderer:對應於HardWareCanvas,創建顯示列表的類
RenderNode:一個渲染節點,包含繪製命令和相關資源
BaseRenderNodeAnimator:動畫用
RenderProxy:由於OpenGL上下文是線程私有的,需要使用到OpenGL的操作都必須在同一線程。這個類的作用就是按Commander模式做一箇中轉,把事務轉移到持有上下文的線程中執行。
Layer:對應於上層的HardwareLayer,實際上是緩存繪製過的內容到一張紋理上,至於如何緩存,有fbo方式和copytex方式
OpenGLRenderer:這個類並不直接被上層調用,但它是執行實際渲染任務的入口類,定位性能問題一般直接從這個類看起。

PS:建議仔細看看RenderProxy.cpp裏面的Commander模式實現方法,確實相當之精妙簡潔,不過感覺C11有匿名函數後不需要這麼麻煩了。

二、Hwui引擎設計

(一)顯示列表

1、顯示列表設計難題

很多介紹顯示列表機制的文章都是一帶而過,彷彿得到一個顯示列表並回放是很簡單的事情。但真正動手寫時,就會發現有很多問題:
1、如何存儲每個API及相關參數?爲每個API創建一個類,回放時調類方法?還是把API及參數作一個編碼,然後回放時解碼,用虛擬機的方式執行?用前者實現比較簡單,擴展相對容易,但每個API建一個類,需要非常大的代碼量(越多的代碼意味着越容易出錯);用後者,需要構建編碼解碼的邏輯,總體代碼量較少,但是虛擬機處理中switch case代碼冗長,且不容易作擴展。
2、資源怎麼處理?這個是最爲棘手的,如果拷貝資源,會大幅降低效率,不可取,但如果不拷貝,上層在傳入資源後馬上修改這些資源,回放時結果會是錯誤的(如傳入Bitmap A之後,調用drawBitmap之後,馬上修改A的內容,回放時A就是修改後的)。原則上自然是不拷貝,但如何約束上層行爲呢?

2、Hwui的顯示列表設計

DisplayList
DrawOp即產生渲染效果的算符,StateOp爲產生狀態變更的算符,須與後續的DrawOp配合使用。
之所以採用獨立成類的設計方式,是爲了滿足批處理優化的需要。
Resouces保留在對應的ResouceCaches中。

(1)DrawOp

DrawOp爲渲染算符,分的子類較多,主要是以下幾頂:
callDrawGLFunction->DrawFunctorOp:
用於WebKit/chromium的硬件加速渲染,WebKit/chromium瀏覽器內核中將基於opengl的渲染代碼封裝爲函數Functor,傳入hwui引擎中執行。
DrawSomeTextOp:
繪製文本的算符,歸結爲一個算符的原因是文本解析的步驟是統一的
DrawColorOp:
將區域刷成指定顏色的算符
DrawBoundedOp:
drawRect、drawBitmap及一般的drawPath均繼承於此算符,其特點是渲染存在邊界。可以設法判斷是否覆蓋
DrawLayerOp:
繪製Layer
DrawShadowOp:
繪製陰影

(2)StateOp

保存/恢復狀態/建層:SaveOp/RestoreToCountOp/SaveLayerOp
矩陣變換相關:TranslateOp/RotateOp/SkewOp/SetMatrixOp/ConcatMatrixOp
設置裁剪區域的算符:ClipOp/ClipRectOp/ClipPathOp/ClipRegionOp
設置Paint的採樣模式:ResetPaintFilterOp/SetupPaintFilterOp

(二)渲染緩存

Hwui的緩存是比較複雜的,一方面,由於採用基於顯示列表的異步渲染機制,用於渲染的資源本身需要在列表中緩存。另一方面,由於GPU/顯卡渲染的異構性,其所需要的資源必須要由顯示列表中的資源上傳或映射而來,上傳的資源和映射關係本身構成顯存的緩存。

Caches 作爲單例,存儲了所有的渲染緩存,主要內容如下:

    TextureCache textureCache;
    LayerCache layerCache;
    RenderBufferCache renderBufferCache;
    GradientCache gradientCache;
    ProgramCache programCache;
    PathCache pathCache;
    PatchCache patchCache;
    TessellationCache tessellationCache;
    TextDropShadowCache dropShadowCache;
    FboCache fboCache;
    ResourceCache resourceCache;

這種單例設計模式自然完全沒有考慮同一進程中可能有多個線程使用Hwui的情況,因此如果要將Hwui改成支持多線程分別使用,需要作不少手術。

如圖所示:
Cache
上層Canvas的API中所夾帶的資源,創建顯示列表時在ResourceCache中緩存一次(Bitmap僅引用,其餘的全部拷貝),在回放顯示列表時再繼續構建各自對應的Cache。
Caches中的所有緩存,除resourceCache之外的不妨統稱爲EngineCache。這個緩存關係就是:
API(Java Virtual Machine)——ResourceCache——EngineCache

ResourceCache

緩存匹配的查詢機制都是依靠指針,由於Path、Paint等資源中會夾帶Effect、Shader等特效,當應用層修改這些東西后,由於指針沒變,緩存無法感知其變化而更新。
因此在Skia裏面爲SkPath、SkPaint加入了generationId,當它們附帶的特效發生改變時,這個id同時修改,依此來校驗API-ResourceCache,ResourceCache—EngineCache是否一致,若不一致自然是要重新再拷貝一遍/重新生成一次Cache。
由於ResourceCache不復制Bitmap,必須要防止在渲染過程中上層把Bitmap給釋放/修改掉。
但它只防止了釋放,並沒有阻止修改的實現,因此這個只能靠應用開發者自覺。
代碼見 frameworks/base/core/jni/android/graphics/Bitmap.cpp

static jboolean Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
    SkBitmap* bitmap = reinterpret_cast<SkBitmap*>(bitmapHandle);
#ifdef USE_OPENGL_RENDERER
    if (android::uirenderer::Caches::hasInstance()) {
        bool result;
        result = android::uirenderer::Caches::getInstance().resourceCache.recycle(bitmap);
        return result ? JNI_TRUE : JNI_FALSE;
    }
#endif // USE_OPENGL_RENDERER
    bitmap->setPixels(NULL, NULL);
    return JNI_TRUE;
}

ResourceCache不包含Bitmap(雖然會阻止上層回收),佔用內存還是很少的,緩存大頭還在 EngineCache

Paint/Shader——ProgramCache

Hwui用的是2.0以上的OpenGLES版本,着色器的構建是很重要的部分。不過,2D繪圖的着色器相對也較簡單。
這裏的設計思想是先翻譯 SkPaint 及其中的 SkShader爲 ProgramDescription 結構,然後由 ProgramCache 去根據這個結構,選擇合適的着色器語言片斷,拼裝起來,組成 GLProgram
SkShader 是一個父類,包含 Bitmap shader,Gradient shader 等好幾類,因此這裏對每一類都要有對應的函數去解析。
主要函數:
SkiaShader::describe
ProgramCache::generateVertexShader
ProgramCache::generateFragmentShader
至於ProgramCache的着色器代碼怎麼寫的,用的正交投影還是透視投影,紋理貼圖怎麼實現等,這裏就不詳述了。

圖片——TextureCache

(1)普通圖片——SkBitmap

代碼參考TextureCache::get 和 TextureCache::generateTexture
基礎的紋理上傳,不多述。

(2)資源圖集——AssetAtlas

這個是Android 4.3起引入的機制,將預加載所得的圖片,先整合到一張 GraphicBuffer 上,轉變爲一張EGLImage。然後各應用在使用硬件加速渲染UI時,將此EGLImage映射爲自身的OpenGL紋理,從而免去這部分資源紋理上傳的過程,且由於應用間共享紋理,節省了內存。
詳細看老羅的博客吧,雖然個人感覺把這一個簡單的功能講太細了:
http://blog.csdn.net/luoshengyang/article/details/45831269

文字——FontRenderer

文字/文本繪製對任何一個2D渲染引擎來說,都是一個棘手的事。主要是因爲文本解析本身需要大量的時間,肯定需要緩存,但使用緩存的話,由於各個文字在各種字體下的解析結果都不一樣,全緩存進來內存耗費極高,是不可能的。
沒有什麼完美的方案去設計一個文本緩存機制,正好比沒有絕對正確的企業管理模式。
Hwui中是這麼處理的:
緩存的設計(這個是每一個FontRenderer都包含的):
TextCache
代碼見FontRenderer::initTextTexture

對Skia解析出來的字形SkGlyph,會按PixelBuffer 由小到大逐次去找一個對應位置,然後複製上去,如果是有變換需要(mGammaTable存在),則在這個過程順便把gammatable變換做了。
在後面渲染時,PixelBuffer會上傳爲Texture,然後GPU就可以使用字形渲染的結果了。
至於 mGammaTable,可詳細看 GammaFontRenderer 和 Lookup3GammaFontRenderer 類。

PixelBuffer 根據 設備支持的OpenGL ES 版本和屬性配置(ro.hwui.use_gpu_pixel_buffers)選用CpuPixelBuffer或GpuPixelBuffer(需要3.0以上版本和屬性開關開啓)。CpuPixelBuffer就是malloc出來的內存,GpuPixelBuffer是PBO。OpenGLES 3.0 標準有PBO映射爲CPU內存的API(glMapBufferRange),會提升緩存過程中上傳的效率。
(注:GpuPixelBuffer這一段代碼也是使用PBO的好教材,需要了解PBO如何使用的可以參考下)。

在渲染時先根據已經緩存好的字形位置,算出紋理採樣的座標,塞進對應cache的vbo,然後遍歷所有的cacheTexture,渲染包含有待渲染文字的cache即可。
代碼見 FontRenderer::issueDrawCommand
延遲渲染模式下,不管是多少個字,始終是根據cache數來調drawCall,這樣,drawCall的調用次數就比較少了。
出於內存優化的考慮,中間一層 PixelBuffer 是可以不要的,但相應地就要在外面把 gammaTable 映射做掉,邏輯會複雜一些。

路徑Path和TessellationCache

Hwui引擎中實現drawPath時,沒有自己去計算路徑點,而是調用skia的drawPath接口繪製一張A8的模板,然後按模板把Shader混合進去。對應的PathCache就是存儲這個模板的。
Demo

在Android 4.0時,繪製圓角矩形、圓形等特殊形狀時,是按drawPath的方式,生成模板再混合,這種方式需要佔用內存,且不是很效率,因此後面Hwui中加入了處理形體的功能,這就是曲紋細分器Tessellator,它通過解析SkPath,生成一系列頂點來描述形體。
目前主要支持凸形狀(詳見PathTessellator的實現),目前細分過程仍然是靠CPU實現的,在未來手機上的GPU支持曲紋細分的Shader後,可以把這部分工作轉移到GPU上。
TessellationCache就是曲紋細分器生成的vbo(vetex buffer object),相對於模板(一張A8紋理)而言節省不少內存,且執行時一般效率更高(曲紋細分方式由於頂點數多,Vertex Shader負荷較大,但相對於模板方式,Fragment Shader負荷較小,內存帶寬佔用較少)。

Layer——LayerCache

(略,以後有空再補)

三、基本流程

總體流程

總流程
關於顯示列表的創建過程可以參考老羅博客:
http://blog.csdn.net/luoshengyang/article/details/45943255

延遲渲染

延遲渲染是在回放顯示列表時,先做一步預處理(defer),然後再執行處理後的命令(flush)。

status_t OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t replayFlags) {
/*.......*/
        DeferredDisplayList deferredList(*currentClipRect(), avoidOverdraw);
        DeferStateStruct deferStruct(deferredList, *this, replayFlags);
        renderNode->defer(deferStruct, 0);
/*.......*/
        return deferredList.flush(*this, dirty) | status;
    }
/*.......*/
}

看這段回放的主代碼可以知道,延遲渲染是先創建一個延遲渲染列表,然後把顯示列表中的命令全部往裏面加進去(這個過程中做預處理),然後交由延遲渲染列表去回放(flush)。

作用

預處理的作用主要是:
(1)合併渲染,減少drawCall調用
(2)避免部分的過度繪製
過度繪製/OverDraw是指同一個像素被渲染多次的情形。解決OverDraw的方法要使用命令列表(顯示列表),對列表中每個繪製命令計算其涵蓋區域。然後是計算重複渲染的區域,設法將這個區域上面的繪製命令合併

Defer信息獲取

DrawOp算子需要實現onDefer這個方法,爲 DeferredDisplayList 提供兩個信息:DeferInfo和DeferredDisplayState。
DeferInfo反映這個DrawOp算子本身的性質(能否合併,是否透明,歸屬哪一類),DeferredDisplayState則是結合算子所處的矩陣變換狀態,反映該算子在最終顯示屏的地位(渲染邊界、矩陣變換)

Defer的作用體現

Hwui中,避免過度繪製的條件很苛刻,需要完全不透明且完全覆蓋,因此Defer作用主要體現在合併渲染上了。
支持合併渲染的DrawOp需要實現一個特殊的multiDraw函數,用以將同類一系列DrawOp的渲染在同一函數完成。
目前所看到合併渲染僅限於繪製AssetAtlas資源的操作合併與多次繪製文字繪製的合併。

資源回收

當應用內存不足時,會盡量去回收內存,其中Hwui所佔的Cache在回收的範圍之內,最終調用Caches::flush回收,有三種模式:
kFlushMode_Layers:
清除LayerCache和RenderBufferCache
kFlushMode_Moderate:
除上面外,清除部分字體緩存、圖片紋理、路徑紋理
kFlushMode_Full:
字體緩存全清,再把fbo、dither清除掉
Cache流程
請注意:
Program是不清的。在內存依然緊張時,會在上層直接摧毀OpenGL上下文。

四、Android硬件加速機制評價

(一)優點

1、完備的GPU繪製流程,在上層API不變的前提下,妥善解決了2D渲染的性能問題
2、延遲渲染合併了大量的渲染指令,drawCall調用少效率高,且有一定的防止過度繪製的功能
3、有一層一層回收緩存的機制
4、相當好的基於OpenGLES 2D 的引擎範本,很多代碼(比如:紋理上傳、PBO、曲紋細分)很有參考價值。

(二)槽點

1、上下層耦合關係嚴重,對上依賴於Java層的合理調用,對下依賴於Skia,不容易提供單獨的基於硬件加速的2D渲染引擎,容易出現內存/資源泄露
2、資源在CPU和GPU中均做Cache,佔用內存較多:顯示列表中的圖片資源和紋理圖片同時存在,字體三重緩存
3、延遲渲染機制做得還是不夠好,消除過度繪製的能力有限,而且每幀都要算一次延遲渲染信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章