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