我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

OpenHarmony图形栈重构:基于Mesa3D Gallium的渲染后端实践

OpenHarmony图形栈重构:基于Mesa3D Gallium的渲染后端实践 如果你和我一样在一款自带GPU的国产SoC上折腾OpenHarmony图形栈大概率见过下面这幅场景系统窗口能刷出来光标也在动可只要一跑带OpenGL ES的测试程序要么黑屏要么隔几秒崩出满屏EGL_BAD_ALLOC。去年我做Mesa3D移植第一版时就是这样老render框架能点亮屏幕但整条链路里到处都是临时判断改一处坏一处。所以才有了这篇移植二也是这次真正想聊的render新框架一个在Mesa3D Gallium之上重新梳理的渲染后端目标是把OpenHarmony的NativeWindow、BufferQueue和显示合成链路真正串成一条可以扩展的管线。这篇文章不重复第一版的基础步骤只讲新框架的模块划分、关键移植点以及我实测过程中踩到的坑。1. 重做之前的痛点旧render框架到底卡在哪1.1 第一版“能跑”和“好用”之间隔着一整条渲染链路第一版我为了快速验证实际是把Mesa的EGL初始化直接绑到了某个远程窗口模块的全局单例上。听起来挺省事Mesa初始化完就能通过opengles测试但问题在后期集中爆发系统有三块显示设备HDMI、MIPI屏、还有一层虚拟屏旧框架里EGLDisplay只有一个于是三个屏幕只能轮流抢同一个渲染target。另外OpenHarmony的资源管理器有自己一套生命周期它认为buffer还挂在队列里就应该继续显示但Mesa那边已经把资源释放了。结果就是开机一小时后突然花屏或者应用退到后台再回来时画面上残留上一帧的内容。这些都是第一版完全没考虑进去的状态同步问题。更麻烦的是线程。旧框架在设计时没有把GL Context的创建和绑定的线程归属理顺我不得不在应用层加一个全局互斥量来串行化所有eglMakeCurrent调用。这相当于让GPU渲染退化成单线程。一旦某个app开了子线程做后台加载主线程的渲染帧率就会剧烈抖动。1.2 帧缓冲管理没有一个清晰的资源归属旧框架里的framebuffer管理非常“野”直接把Mesa内部生成的pipe_resource当作成前端要的buffer来用再映射给显示控制器扫描。这样做的好处是代码短坏处是资源所有权不明确。谁能改这个buffer谁能读这个buffer什么时候改完在没有Fence约束的情况下全靠运气。我实际遇到过的现象是vblank回调已经触发显示控制器开始读取这一帧但GPU这边还在绘制同一个buffer于是一帧画面里上半部分是新的、下半部分是撕裂的。旧框架我试着加过一版“检测到绘制中就多等一个vblank”的逻辑结果因为不判断绘制是否真结束只是盲目多等导致掉帧。真正的问题是缺乏明确的同步机制而不是等待时间不够。1.3 从问题反推设计目标把上面这些痛点列成表格之后新框架该做什么其实已经很清楚问题根因新框架目标EGLDisplay只能绑定一个屏幕单例设计没有抽象显示设备支持多Display每个Display独立EGLSurface后台线程导致帧率抖动context绑定线程不清晰由框架统一管理context归属花屏、撕裂、残留画面资源生命周期不明确、缺少同步引入buffer所有权转移与Fence同步改一处坏一处平台适配代码侵入Mesa内部把平台适配收敛到固定适配层所以这版我确定的核心原则只有一条绝不在Mesa源码里到处打补丁所有硬件相关逻辑全部收口到一个薄适配层。这个决策直接决定了后面所有模块的代码结构。2. render新框架的模块划分在Gallium之上加一个适配层2.1 所谓“新框架”本质是给EGL和Gallium之间加一个中间层很多人一听到“重写render框架”就觉得要动Mesa的渲染核心其实恰恰相反。Mesa的Gallium架构本身足够稳定状态跟踪器、编译器、驱动后端分得很清楚没必要为了适配一个操作系统去改渲染引擎内部。新框架真正改的是Mesa和OpenHarmony平台之间的衔接方式。我这边最后落地的模块分成三块Display管理模块负责发现显示设备、创建EGLDisplay、管理原生窗口和屏幕的映射关系。旧版是把一个全局对象到处传新版则是注册一张表每个物理屏幕对应一个Display实例前端通过类似ohos_display_get_default的接口拿到句柄。Surface管理模块负责把OpenHarmony的NativeWindow封装成Mesa可识别的EGLSurface。这里包含buffer的申请、提交、释放以及和BufferQueue的交互。前端的SurfaceView、XComponent最终都落到这个模块里的一个window_surface结构体。资源管理模块负责buffer的导入导出、DMA-BUF的转换、Fence同步。说白了就是让GPU画的帧能被显示控制器安全地拿走去扫。这三个模块统一收口在一个叫render_backend的接口里只有一组固定的创建、配置、提帧、释放操作。内部再根据底层GPU能力选择软渲染还是硬件驱动前端完全无感。2.2 一次draw调用从应用走到显示器的完整路径讲代码之前先过一次数据流不然看后面细节容易懵。我在新框架里理顺后的完整链路是这样的应用调用eglGetDisplay框架根据设备节点返回一个Display实例。应用调用eglCreateWindowSurface框架从NativeWindow里拿到一个空闲buffer交给Mesa作为drawbuffer。应用执行glDrawElementsMesa的GL状态跟踪器把API调用翻译成Gallium命令再由后端驱动生成实际的GPU命令。GPU命令执行完通过Fence通知框架“这帧可以拿去显示了”。框架把buffer提交给OpenHarmony的BufferQueue显示合成器拿到buffer后和UI层叠加最终送屏。这版框架最核心的变化在第三步和第四步之间。旧框架里GPU画完后直接把buffer地址丢给显示控制器新框架则引入了Fence机制确保显示控制器读取时GPU的写入已经完整落定。2.3 为什么保留Gallium而不是干脆自研一个小渲染器有人可能会问既然OpenHarmony本身有自己的render service为什么还要费劲移植Mesa答案是应用生态。OpenHarmony的富媒体应用大量依赖OpenGL ES做特效、游戏、地图渲染这些API的实现不是三天两头能重写干净的。而Mesa的Gallium架构把着色器编译、状态管理、资源管理都做了很好的抽象我只需要让平台层符合它的接口预期。相比第一版那种“能用就行”的思路新框架等于在平台和Mesa之间划了一条明确的契约线。后续想换硬件驱动、想支持Vulkan、想接更多buffer格式都只动适配层不需要重新排查整条渲染链路。这是我做这次重构最看重的东西。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时走我们自己的初始化分支。这里有个关键坑OpenHarmony的NativeWindow结构体和传统X11/Wayland的window类型完全不同不能直接强转。正确的做法是在适配层写一个转换函数把OHOS的OHNativeWindow指针封装成结构同时把窗口的宽高、格式、usage flag一起传给Mesa的surface配置。当时我卡了最久的地方是颜色格式。Mesa默认expect的framebuffer格式经常是XRGB8888而OpenHarmony的BufferQueue默认给的是RGBA8888。这两个在字节排列上看起来很像但alpha位的处理逻辑不一样。如果不显式设置EGL_ALPHA_SIZE就会出现半透明窗口背景下文字发虚的奇怪bug。3.2 DMA-BUF资源导入与缓存同步新框架里buffer在GPU和显示控制器之间流转靠的是DMA-BUF的fd传递。Mesa这边有现成的resource_from_handle接口可以基于一个已有的fd创建pipe_resource这样就不需要把GPU显存内容拷贝一份到系统内存再拷回去。但这块有一个必须注意的点不是所有fd都能直接拿去mmap。我遇到过MMU配置不对导致map出来的地址访问直接段错误后来排查发现是设备的dma地址范围和CPU地址空间之间需要做偏移转换。这个属于平台级问题Mesa本身不会帮你处理必须在buffer导入前把物理地址信息填对。缓存同步也是大坑。GPU用Cache写数据CPU那边直接读的时候可能看到的是旧数据。我在调试一个纹理上传慢的问题时发现每次用CPU往buffer里写数据后GPU那边拿到的总是上一次的内容。后来在提交缓冲前增加了cache clean操作在GPU访问前增加cache invalidate操作才把问题解决。这些东西在PC上一般由驱动自动处理但在这种嵌入式移植场景里往往要显式调平台接口。3.3 线程模型与Context切换新框架把GL Context的管理收口后线程问题就从应用层转移到了框架层框架必须自己保证并发安全。我最后采用的方案是“Display级别的锁”同一个Display下eglMakeCurrent切换context时加锁但不同Display之间互不影响。这样主线程和渲染线程可以同时跑在不同Display上不会互相卡死。另一个细节是eglDestroyContext的时机。某些驱动在context销毁时还会尝试访问它绑定过的surface如果surface已经提前释放就会触发use-after-free。我在框架里规定销毁context前必须先解除它和surface的绑定关系并等待正在执行的GPU命令完成再回收内存。这个顺序错一步就容易在退出应用时偶发崩溃。4. 显示链路改造让合成器正常消费GPU的产出4.1 从BufferQueue到HWC的buffer所有权交接在OpenHarmony的显示架构里应用拿到的buffer来自BufferQueue最终展示则要交给HWC合成。Mesa渲染完的buffer不能直接推给屏幕必须把所有权沿着“应用 - BufferQueue - 合成器”的顺序依次移交。新框架实现这套交接依赖一个buffer_release回调。当BufferQueue认为buffer已经被合成器读取后会回调框架层框架再把这个buffer归还给Mesa的资源池供下一次渲染复用。如果这个回调时机不对要么buffer被过早回收合成器读了一半被覆盖要么buffer一直不归还应用申请不到新buffer开始卡顿。我在调试时观察过一个诡异现象只有连续滑动桌面时才卡停下来就正常。后来抓log发现是合成器持有了两个buffer还没释放应用侧的缓冲池已经见底。调整成三缓冲并增加超时回收后才算稳定。4.2 像素格式和plane对齐显示控制器对buffer有几个硬性要求内存对齐、像素格式匹配、扫描线地址按行对齐。这些在PC上通常由驱动绕过了但OpenHarmony的HWC会直接检查。我移植的显示核心虽然支持常见的RGBA8888和RGB888但有一个tiled模式的问题。GPU那边默认生成tiled布局提升纹理效率但显示控制器只认linear布局。旧框架里我直接让GPU用linear模式性能损失明显。新框架的做法是开启GPU的写压缩/转置引擎在buffer提交前做一次硬件格式转换让GPU保持高效率的排列显示侧也能读到正确的linear数据。这个代价是增加了大约一毫秒的转换时间相比让GPU全程跑linear带来的带宽损失值很多。4.3 三缓冲下的帧间隔和Fence策略OpenHarmony默认倾向三缓冲来减少jank但三缓冲如果配合Fence策略不当反而会增加延迟。我试过两种方案第一种是渲染完立即释放不等合成器读第二种是等Fence信号后再释放。第一种在负载低时流畅高负载时花屏第二种在高负载时稳定但延迟多一到两帧。最终我做的决定是动态判断当BufferQueue里可用buffer少于2个时强制开启Fence等待可用buffer充足时走快速路径。实测下来在高负载场景下花屏消失普通场景延迟没有明显恶化。5. 验证与调优从能出图到稳定跑分5.1 dEQP测试结果出来的那一晚渲染框架重构完第一件事不是跑性能而是验证正确性。我用了Khronos的dEQP工具集分别跑GLES2、GLES3的核心测试组。第一轮结果非常惨大量shader compile失败的case几乎全是精度修饰符和循环展开相关的差异。仔细排查后发现不是Mesa的问题而是我在EGL配置里把EGL_CONTEXT_MAJOR_VERSION设置错了导致app请求到的是软件兼容profile而不是完整GLES3上下文。修正之后GLES2的通过率从40%跳到接近100%GLES3也有99.4%左右剩下个别case是纹理压缩格式扩展没接全。还有一个必须提的验证环节是压力测试。dEQP跑的是单帧正确性不管长时间运行的稳定性。我额外写了脚本循环跑图形demo两小时同时反复切换前后台、开关屏幕确认没有buffer泄漏和Fence死锁。5.2 软渲染和硬件加速的性能差异这套平台目前适配的是内置GPU所以硬件路径为主。但调试初期Mesa驱动还没完全工作我先用llvmpipe软渲染验证框架逻辑那时候性能很能说明问题。llvmpipe在OpenHarmony这种轻量系统上帧率大致能到30到40帧但对内存带宽非常敏感。新框架dma-buf共享绕过了很多拷贝软渲染性能比第一版反而提升了。这也印证了我之前说的很多时候瓶颈不在GPU计算而在于buffer搬运。硬件加速跑起来后我用一个标准UI交互场景和GPU benchmark分别测试。差异最明显的是1080p全屏天气动效场景软渲染在复杂模糊shader下掉到个位数硬件加速能稳定在50帧以上。这说明针对该GPU所做的内存布局转换和命令提交优化是有效果的。5.3 黑屏、花屏问题的定位套路新框架上线后我总结出一套黑屏花屏的排查顺序基本上是看EGL_BAD_ALLOC是否出现在buffer申请阶段如果是优先查usage flag和format组合是否被allocator拒绝。看显示控制器是否报“pixel format not supported”如果是重点查提交前的格式转换有没有生效。看Fence回调是否迟迟不触发如果是多半是DMA-BUF sync对象没有绑定正确的fd。如果以上都正常查buffer地址对齐特别是stride大于width时的行对齐问题。这套顺序帮我节省了大量时间。大多数黑屏不是渲染完全没输出而是某一环的格式或尺寸信息传错了显示控制器根本没把buffer当作有效输入。6. 回头看除了代码几个值得说说的选择6.1 选稳定的Mesa分支而不是追最新Mesa的master分支变化很快特别是在vulkan和新的gallium驱动上。我在项目中期换过一次新分支结果EGL平台接口内部结构调整导致我写的适配代码大面积需要重改。后来固定在某个长期稳定版本上只做小版本跟进。奉劝做嵌入式移植的朋友除非需要新特性否则别追主线稳定分支能少掉一半头发。6.2 “新框架”不是造轮子而是把边界划清楚一开始我也有点抵触重写毕竟第一版能跑谁愿意动它。但实践下来的体会是所谓render新框架真正带来的价值不是新的渲染算法而是把“谁负责分配、谁负责渲染、谁负责显示”这条责任链划清了。Mesa内部的功能依然沿用原样OpenHarmony侧的资源管理模式也基本没动我只在中间补了一层防腐逻辑结果所有以前“改了这里坏了那边”的毛病都消失了。6.3 后续可以怎么扩展这套框架跑通OpenGL ES之后后续最自然的扩展是支持Vulkan。Mesa本身就带Vulkan的驱动支持如果再配一个Vulkan ICD加载器很多游戏应用可以直接受益。另一个可做的方向是扩展buffer格式比如支持10bit HDR的RGB10A2适配高端视频播放场景。框架层面我已经预留了format清单的注册入口增加新格式不再需要改代码逻辑。最后分享一个实战建议拿到一块新板子别上来就跑dEQP或者游戏demo先用一个简单的全屏色块程序验证buffer从GPU到显示器的全链路。只要色块颜色正确、切换流畅说明最底层的传输没坏后面的功能再逐个加。渲染栈的问题绝大多数是资源和时序问题不是数学问题。把这两样理顺了Mesa3D移植基本就成功了一大半。
返回列表