:基于Mesa3D Gallium的渲染后端實(shí)踐)
如果你和我一樣在一款自帶GPU的國(guó)產(chǎn)SoC上折騰OpenHarmony圖形棧大概率見(jiàn)過(guò)下面這幅場(chǎng)景系統(tǒng)窗口能刷出來(lái)光標(biāo)也在動(dòng)可只要一跑帶OpenGL ES的測(cè)試程序要么黑屏要么隔幾秒崩出滿屏EGL_BAD_ALLOC。去年我做Mesa3D移植第一版時(shí)就是這樣老render框架能點(diǎn)亮屏幕但整條鏈路里到處都是臨時(shí)判斷改一處壞一處。所以才有了這篇移植二也是這次真正想聊的render新框架一個(gè)在Mesa3D Gallium之上重新梳理的渲染后端目標(biāo)是把OpenHarmony的NativeWindow、BufferQueue和顯示合成鏈路真正串成一條可以擴(kuò)展的管線。這篇文章不重復(fù)第一版的基礎(chǔ)步驟只講新框架的模塊劃分、關(guān)鍵移植點(diǎn)以及我實(shí)測(cè)過(guò)程中踩到的坑。1. 重做之前的痛點(diǎn)舊render框架到底卡在哪1.1 第一版“能跑”和“好用”之間隔著一整條渲染鏈路第一版我為了快速驗(yàn)證實(shí)際是把Mesa的EGL初始化直接綁到了某個(gè)遠(yuǎn)程窗口模塊的全局單例上。聽(tīng)起來(lái)挺省事Mesa初始化完就能通過(guò)opengles測(cè)試但問(wèn)題在后期集中爆發(fā)系統(tǒng)有三塊顯示設(shè)備HDMI、MIPI屏、還有一層虛擬屏舊框架里EGLDisplay只有一個(gè)于是三個(gè)屏幕只能輪流搶同一個(gè)渲染target。另外OpenHarmony的資源管理器有自己一套生命周期它認(rèn)為buffer還掛在隊(duì)列里就應(yīng)該繼續(xù)顯示但Mesa那邊已經(jīng)把資源釋放了。結(jié)果就是開(kāi)機(jī)一小時(shí)后突然花屏或者應(yīng)用退到后臺(tái)再回來(lái)時(shí)畫面上殘留上一幀的內(nèi)容。這些都是第一版完全沒(méi)考慮進(jìn)去的狀態(tài)同步問(wèn)題。更麻煩的是線程。舊框架在設(shè)計(jì)時(shí)沒(méi)有把GL Context的創(chuàng)建和綁定的線程歸屬理順我不得不在應(yīng)用層加一個(gè)全局互斥量來(lái)串行化所有eglMakeCurrent調(diào)用。這相當(dāng)于讓GPU渲染退化成單線程。一旦某個(gè)app開(kāi)了子線程做后臺(tái)加載主線程的渲染幀率就會(huì)劇烈抖動(dòng)。1.2 幀緩沖管理沒(méi)有一個(gè)清晰的資源歸屬舊框架里的framebuffer管理非?!耙啊敝苯影袽esa內(nèi)部生成的pipe_resource當(dāng)作成前端要的buffer來(lái)用再映射給顯示控制器掃描。這樣做的好處是代碼短壞處是資源所有權(quán)不明確。誰(shuí)能改這個(gè)buffer誰(shuí)能讀這個(gè)buffer什么時(shí)候改完在沒(méi)有Fence約束的情況下全靠運(yùn)氣。我實(shí)際遇到過(guò)的現(xiàn)象是vblank回調(diào)已經(jīng)觸發(fā)顯示控制器開(kāi)始讀取這一幀但GPU這邊還在繪制同一個(gè)buffer于是一幀畫面里上半部分是新的、下半部分是撕裂的。舊框架我試著加過(guò)一版“檢測(cè)到繪制中就多等一個(gè)vblank”的邏輯結(jié)果因?yàn)椴慌袛嗬L制是否真結(jié)束只是盲目多等導(dǎo)致掉幀。真正的問(wèn)題是缺乏明確的同步機(jī)制而不是等待時(shí)間不夠。1.3 從問(wèn)題反推設(shè)計(jì)目標(biāo)把上面這些痛點(diǎn)列成表格之后新框架該做什么其實(shí)已經(jīng)很清楚問(wèn)題根因新框架目標(biāo)EGLDisplay只能綁定一個(gè)屏幕單例設(shè)計(jì)沒(méi)有抽象顯示設(shè)備支持多Display每個(gè)Display獨(dú)立EGLSurface后臺(tái)線程導(dǎo)致幀率抖動(dòng)context綁定線程不清晰由框架統(tǒng)一管理context歸屬花屏、撕裂、殘留畫面資源生命周期不明確、缺少同步引入buffer所有權(quán)轉(zhuǎn)移與Fence同步改一處壞一處平臺(tái)適配代碼侵入Mesa內(nèi)部把平臺(tái)適配收斂到固定適配層所以這版我確定的核心原則只有一條絕不在Mesa源碼里到處打補(bǔ)丁所有硬件相關(guān)邏輯全部收口到一個(gè)薄適配層。這個(gè)決策直接決定了后面所有模塊的代碼結(jié)構(gòu)。2. render新框架的模塊劃分在Gallium之上加一個(gè)適配層2.1 所謂“新框架”本質(zhì)是給EGL和Gallium之間加一個(gè)中間層很多人一聽(tīng)到“重寫render框架”就覺(jué)得要?jiǎng)覯esa的渲染核心其實(shí)恰恰相反。Mesa的Gallium架構(gòu)本身足夠穩(wěn)定狀態(tài)跟蹤器、編譯器、驅(qū)動(dòng)后端分得很清楚沒(méi)必要為了適配一個(gè)操作系統(tǒng)去改渲染引擎內(nèi)部。新框架真正改的是Mesa和OpenHarmony平臺(tái)之間的銜接方式。我這邊最后落地的模塊分成三塊Display管理模塊負(fù)責(zé)發(fā)現(xiàn)顯示設(shè)備、創(chuàng)建EGLDisplay、管理原生窗口和屏幕的映射關(guān)系。舊版是把一個(gè)全局對(duì)象到處傳新版則是注冊(cè)一張表每個(gè)物理屏幕對(duì)應(yīng)一個(gè)Display實(shí)例前端通過(guò)類似ohos_display_get_default的接口拿到句柄。Surface管理模塊負(fù)責(zé)把OpenHarmony的NativeWindow封裝成Mesa可識(shí)別的EGLSurface。這里包含buffer的申請(qǐng)、提交、釋放以及和BufferQueue的交互。前端的SurfaceView、XComponent最終都落到這個(gè)模塊里的一個(gè)window_surface結(jié)構(gòu)體。資源管理模塊負(fù)責(zé)buffer的導(dǎo)入導(dǎo)出、DMA-BUF的轉(zhuǎn)換、Fence同步。說(shuō)白了就是讓GPU畫的幀能被顯示控制器安全地拿走去掃。這三個(gè)模塊統(tǒng)一收口在一個(gè)叫render_backend的接口里只有一組固定的創(chuàng)建、配置、提幀、釋放操作。內(nèi)部再根據(jù)底層GPU能力選擇軟渲染還是硬件驅(qū)動(dòng)前端完全無(wú)感。2.2 一次draw調(diào)用從應(yīng)用走到顯示器的完整路徑講代碼之前先過(guò)一次數(shù)據(jù)流不然看后面細(xì)節(jié)容易懵。我在新框架里理順后的完整鏈路是這樣的應(yīng)用調(diào)用eglGetDisplay框架根據(jù)設(shè)備節(jié)點(diǎn)返回一個(gè)Display實(shí)例。應(yīng)用調(diào)用eglCreateWindowSurface框架從NativeWindow里拿到一個(gè)空閑buffer交給Mesa作為drawbuffer。應(yīng)用執(zhí)行g(shù)lDrawElementsMesa的GL狀態(tài)跟蹤器把API調(diào)用翻譯成Gallium命令再由后端驅(qū)動(dòng)生成實(shí)際的GPU命令。GPU命令執(zhí)行完通過(guò)Fence通知框架“這幀可以拿去顯示了”??蚣馨裝uffer提交給OpenHarmony的BufferQueue顯示合成器拿到buffer后和UI層疊加最終送屏。這版框架最核心的變化在第三步和第四步之間。舊框架里GPU畫完后直接把buffer地址丟給顯示控制器新框架則引入了Fence機(jī)制確保顯示控制器讀取時(shí)GPU的寫入已經(jīng)完整落定。2.3 為什么保留Gallium而不是干脆自研一個(gè)小渲染器有人可能會(huì)問(wèn)既然OpenHarmony本身有自己的render service為什么還要費(fèi)勁移植Mesa答案是應(yīng)用生態(tài)。OpenHarmony的富媒體應(yīng)用大量依賴OpenGL ES做特效、游戲、地圖渲染這些API的實(shí)現(xiàn)不是三天兩頭能重寫干凈的。而Mesa的Gallium架構(gòu)把著色器編譯、狀態(tài)管理、資源管理都做了很好的抽象我只需要讓平臺(tái)層符合它的接口預(yù)期。相比第一版那種“能用就行”的思路新框架等于在平臺(tái)和Mesa之間劃了一條明確的契約線。后續(xù)想換硬件驅(qū)動(dòng)、想支持Vulkan、想接更多buffer格式都只動(dòng)適配層不需要重新排查整條渲染鏈路。這是我做這次重構(gòu)最看重的東西。3. 移植過(guò)程中真正難啃的幾個(gè)硬骨頭3.1 給Mesa的EGL增加一個(gè)OHOS PlatformMesa本身有一套EGL平臺(tái)枚舉機(jī)制常見(jiàn)的像EGL_PLATFORM_X11、EGL_PLATFORM_WAYLAND、EGL_PLATFORM_DRM。OpenHarmony不在其中所以第一步是讓Mesa能識(shí)別OpenHarmony的native window類型。我參考了Android platform的做法在Mesa的egl源碼里增加了一個(gè)平臺(tái)入口讓eglGetPlatformDisplayEXT在檢測(cè)到EGL_PLATFORM_OHOS時(shí)走我們自己的初始化分支。這里有個(gè)關(guān)鍵坑OpenHarmony的NativeWindow結(jié)構(gòu)體和傳統(tǒng)X11/Wayland的window類型完全不同不能直接強(qiáng)轉(zhuǎn)。正確的做法是在適配層寫一個(gè)轉(zhuǎn)換函數(shù)把OHOS的OHNativeWindow指針?lè)庋b成結(jié)構(gòu)同時(shí)把窗口的寬高、格式、usage flag一起傳給Mesa的surface配置。當(dāng)時(shí)我卡了最久的地方是顏色格式。Mesa默認(rèn)expect的framebuffer格式經(jīng)常是XRGB8888而OpenHarmony的BufferQueue默認(rèn)給的是RGBA8888。這兩個(gè)在字節(jié)排列上看起來(lái)很像但alpha位的處理邏輯不一樣。如果不顯式設(shè)置EGL_ALPHA_SIZE就會(huì)出現(xiàn)半透明窗口背景下文字發(fā)虛的奇怪bug。3.2 DMA-BUF資源導(dǎo)入與緩存同步新框架里buffer在GPU和顯示控制器之間流轉(zhuǎn)靠的是DMA-BUF的fd傳遞。Mesa這邊有現(xiàn)成的resource_from_handle接口可以基于一個(gè)已有的fd創(chuàng)建pipe_resource這樣就不需要把GPU顯存內(nèi)容拷貝一份到系統(tǒng)內(nèi)存再拷回去。但這塊有一個(gè)必須注意的點(diǎn)不是所有fd都能直接拿去mmap。我遇到過(guò)MMU配置不對(duì)導(dǎo)致map出來(lái)的地址訪問(wèn)直接段錯(cuò)誤后來(lái)排查發(fā)現(xiàn)是設(shè)備的dma地址范圍和CPU地址空間之間需要做偏移轉(zhuǎn)換。這個(gè)屬于平臺(tái)級(jí)問(wèn)題Mesa本身不會(huì)幫你處理必須在buffer導(dǎo)入前把物理地址信息填對(duì)。緩存同步也是大坑。GPU用Cache寫數(shù)據(jù)CPU那邊直接讀的時(shí)候可能看到的是舊數(shù)據(jù)。我在調(diào)試一個(gè)紋理上傳慢的問(wèn)題時(shí)發(fā)現(xiàn)每次用CPU往buffer里寫數(shù)據(jù)后GPU那邊拿到的總是上一次的內(nèi)容。后來(lái)在提交緩沖前增加了cache clean操作在GPU訪問(wèn)前增加cache invalidate操作才把問(wèn)題解決。這些東西在PC上一般由驅(qū)動(dòng)自動(dòng)處理但在這種嵌入式移植場(chǎng)景里往往要顯式調(diào)平臺(tái)接口。3.3 線程模型與Context切換新框架把GL Context的管理收口后線程問(wèn)題就從應(yīng)用層轉(zhuǎn)移到了框架層框架必須自己保證并發(fā)安全。我最后采用的方案是“Display級(jí)別的鎖”同一個(gè)Display下eglMakeCurrent切換context時(shí)加鎖但不同Display之間互不影響。這樣主線程和渲染線程可以同時(shí)跑在不同Display上不會(huì)互相卡死。另一個(gè)細(xì)節(jié)是eglDestroyContext的時(shí)機(jī)。某些驅(qū)動(dòng)在context銷毀時(shí)還會(huì)嘗試訪問(wèn)它綁定過(guò)的surface如果surface已經(jīng)提前釋放就會(huì)觸發(fā)use-after-free。我在框架里規(guī)定銷毀context前必須先解除它和surface的綁定關(guān)系并等待正在執(zhí)行的GPU命令完成再回收內(nèi)存。這個(gè)順序錯(cuò)一步就容易在退出應(yīng)用時(shí)偶發(fā)崩潰。4. 顯示鏈路改造讓合成器正常消費(fèi)GPU的產(chǎn)出4.1 從BufferQueue到HWC的buffer所有權(quán)交接在OpenHarmony的顯示架構(gòu)里應(yīng)用拿到的buffer來(lái)自BufferQueue最終展示則要交給HWC合成。Mesa渲染完的buffer不能直接推給屏幕必須把所有權(quán)沿著“應(yīng)用 - BufferQueue - 合成器”的順序依次移交。新框架實(shí)現(xiàn)這套交接依賴一個(gè)buffer_release回調(diào)。當(dāng)BufferQueue認(rèn)為buffer已經(jīng)被合成器讀取后會(huì)回調(diào)框架層框架再把這個(gè)buffer歸還給Mesa的資源池供下一次渲染復(fù)用。如果這個(gè)回調(diào)時(shí)機(jī)不對(duì)要么buffer被過(guò)早回收合成器讀了一半被覆蓋要么buffer一直不歸還應(yīng)用申請(qǐng)不到新buffer開(kāi)始卡頓。我在調(diào)試時(shí)觀察過(guò)一個(gè)詭異現(xiàn)象只有連續(xù)滑動(dòng)桌面時(shí)才卡停下來(lái)就正常。后來(lái)抓log發(fā)現(xiàn)是合成器持有了兩個(gè)buffer還沒(méi)釋放應(yīng)用側(cè)的緩沖池已經(jīng)見(jiàn)底。調(diào)整成三緩沖并增加超時(shí)回收后才算穩(wěn)定。4.2 像素格式和plane對(duì)齊顯示控制器對(duì)buffer有幾個(gè)硬性要求內(nèi)存對(duì)齊、像素格式匹配、掃描線地址按行對(duì)齊。這些在PC上通常由驅(qū)動(dòng)繞過(guò)了但OpenHarmony的HWC會(huì)直接檢查。我移植的顯示核心雖然支持常見(jiàn)的RGBA8888和RGB888但有一個(gè)tiled模式的問(wèn)題。GPU那邊默認(rèn)生成tiled布局提升紋理效率但顯示控制器只認(rèn)linear布局。舊框架里我直接讓GPU用linear模式性能損失明顯。新框架的做法是開(kāi)啟GPU的寫壓縮/轉(zhuǎn)置引擎在buffer提交前做一次硬件格式轉(zhuǎn)換讓GPU保持高效率的排列顯示側(cè)也能讀到正確的linear數(shù)據(jù)。這個(gè)代價(jià)是增加了大約一毫秒的轉(zhuǎn)換時(shí)間相比讓GPU全程跑linear帶來(lái)的帶寬損失值很多。4.3 三緩沖下的幀間隔和Fence策略O(shè)penHarmony默認(rèn)傾向三緩沖來(lái)減少jank但三緩沖如果配合Fence策略不當(dāng)反而會(huì)增加延遲。我試過(guò)兩種方案第一種是渲染完立即釋放不等合成器讀第二種是等Fence信號(hào)后再釋放。第一種在負(fù)載低時(shí)流暢高負(fù)載時(shí)花屏第二種在高負(fù)載時(shí)穩(wěn)定但延遲多一到兩幀。最終我做的決定是動(dòng)態(tài)判斷當(dāng)BufferQueue里可用buffer少于2個(gè)時(shí)強(qiáng)制開(kāi)啟Fence等待可用buffer充足時(shí)走快速路徑。實(shí)測(cè)下來(lái)在高負(fù)載場(chǎng)景下花屏消失普通場(chǎng)景延遲沒(méi)有明顯惡化。5. 驗(yàn)證與調(diào)優(yōu)從能出圖到穩(wěn)定跑分5.1 dEQP測(cè)試結(jié)果出來(lái)的那一晚渲染框架重構(gòu)完第一件事不是跑性能而是驗(yàn)證正確性。我用了Khronos的dEQP工具集分別跑GLES2、GLES3的核心測(cè)試組。第一輪結(jié)果非常慘大量shader compile失敗的case幾乎全是精度修飾符和循環(huán)展開(kāi)相關(guān)的差異。仔細(xì)排查后發(fā)現(xiàn)不是Mesa的問(wèn)題而是我在EGL配置里把EGL_CONTEXT_MAJOR_VERSION設(shè)置錯(cuò)了導(dǎo)致app請(qǐng)求到的是軟件兼容profile而不是完整GLES3上下文。修正之后GLES2的通過(guò)率從40%跳到接近100%GLES3也有99.4%左右剩下個(gè)別case是紋理壓縮格式擴(kuò)展沒(méi)接全。還有一個(gè)必須提的驗(yàn)證環(huán)節(jié)是壓力測(cè)試。dEQP跑的是單幀正確性不管長(zhǎng)時(shí)間運(yùn)行的穩(wěn)定性。我額外寫了腳本循環(huán)跑圖形demo兩小時(shí)同時(shí)反復(fù)切換前后臺(tái)、開(kāi)關(guān)屏幕確認(rèn)沒(méi)有buffer泄漏和Fence死鎖。5.2 軟渲染和硬件加速的性能差異這套平臺(tái)目前適配的是內(nèi)置GPU所以硬件路徑為主。但調(diào)試初期Mesa驅(qū)動(dòng)還沒(méi)完全工作我先用llvmpipe軟渲染驗(yàn)證框架邏輯那時(shí)候性能很能說(shuō)明問(wèn)題。llvmpipe在OpenHarmony這種輕量系統(tǒng)上幀率大致能到30到40幀但對(duì)內(nèi)存帶寬非常敏感。新框架dma-buf共享繞過(guò)了很多拷貝軟渲染性能比第一版反而提升了。這也印證了我之前說(shuō)的很多時(shí)候瓶頸不在GPU計(jì)算而在于buffer搬運(yùn)。硬件加速跑起來(lái)后我用一個(gè)標(biāo)準(zhǔn)UI交互場(chǎng)景和GPU benchmark分別測(cè)試。差異最明顯的是1080p全屏天氣動(dòng)效場(chǎng)景軟渲染在復(fù)雜模糊shader下掉到個(gè)位數(shù)硬件加速能穩(wěn)定在50幀以上。這說(shuō)明針對(duì)該GPU所做的內(nèi)存布局轉(zhuǎn)換和命令提交優(yōu)化是有效果的。5.3 黑屏、花屏問(wèn)題的定位套路新框架上線后我總結(jié)出一套黑屏花屏的排查順序基本上是看EGL_BAD_ALLOC是否出現(xiàn)在buffer申請(qǐng)階段如果是優(yōu)先查usage flag和format組合是否被allocator拒絕??达@示控制器是否報(bào)“pixel format not supported”如果是重點(diǎn)查提交前的格式轉(zhuǎn)換有沒(méi)有生效??碏ence回調(diào)是否遲遲不觸發(fā)如果是多半是DMA-BUF sync對(duì)象沒(méi)有綁定正確的fd。如果以上都正常查buffer地址對(duì)齊特別是stride大于width時(shí)的行對(duì)齊問(wèn)題。這套順序幫我節(jié)省了大量時(shí)間。大多數(shù)黑屏不是渲染完全沒(méi)輸出而是某一環(huán)的格式或尺寸信息傳錯(cuò)了顯示控制器根本沒(méi)把buffer當(dāng)作有效輸入。6. 回頭看除了代碼幾個(gè)值得說(shuō)說(shuō)的選擇6.1 選穩(wěn)定的Mesa分支而不是追最新Mesa的master分支變化很快特別是在vulkan和新的gallium驅(qū)動(dòng)上。我在項(xiàng)目中期換過(guò)一次新分支結(jié)果EGL平臺(tái)接口內(nèi)部結(jié)構(gòu)調(diào)整導(dǎo)致我寫的適配代碼大面積需要重改。后來(lái)固定在某個(gè)長(zhǎng)期穩(wěn)定版本上只做小版本跟進(jìn)。奉勸做嵌入式移植的朋友除非需要新特性否則別追主線穩(wěn)定分支能少掉一半頭發(fā)。6.2 “新框架”不是造輪子而是把邊界劃清楚一開(kāi)始我也有點(diǎn)抵觸重寫畢竟第一版能跑誰(shuí)愿意動(dòng)它。但實(shí)踐下來(lái)的體會(huì)是所謂render新框架真正帶來(lái)的價(jià)值不是新的渲染算法而是把“誰(shuí)負(fù)責(zé)分配、誰(shuí)負(fù)責(zé)渲染、誰(shuí)負(fù)責(zé)顯示”這條責(zé)任鏈劃清了。Mesa內(nèi)部的功能依然沿用原樣OpenHarmony側(cè)的資源管理模式也基本沒(méi)動(dòng)我只在中間補(bǔ)了一層防腐邏輯結(jié)果所有以前“改了這里壞了那邊”的毛病都消失了。6.3 后續(xù)可以怎么擴(kuò)展這套框架跑通OpenGL ES之后后續(xù)最自然的擴(kuò)展是支持Vulkan。Mesa本身就帶Vulkan的驅(qū)動(dòng)支持如果再配一個(gè)Vulkan ICD加載器很多游戲應(yīng)用可以直接受益。另一個(gè)可做的方向是擴(kuò)展buffer格式比如支持10bit HDR的RGB10A2適配高端視頻播放場(chǎng)景??蚣軐用嫖乙呀?jīng)預(yù)留了format清單的注冊(cè)入口增加新格式不再需要改代碼邏輯。最后分享一個(gè)實(shí)戰(zhàn)建議拿到一塊新板子別上來(lái)就跑dEQP或者游戲demo先用一個(gè)簡(jiǎn)單的全屏色塊程序驗(yàn)證buffer從GPU到顯示器的全鏈路。只要色塊顏色正確、切換流暢說(shuō)明最底層的傳輸沒(méi)壞后面的功能再逐個(gè)加。渲染棧的問(wèn)題絕大多數(shù)是資源和時(shí)序問(wèn)題不是數(shù)學(xué)問(wèn)題。把這兩樣理順了Mesa3D移植基本就成功了一大半。