聊Mali GPU的开源驱动,Panfrost是绕不开的一个名字。它瞄准的是Arm Mali家族里Midgard和Bifrost两代GPU架构,目标很明确:在Mesa框架下用一套完整的开源驱动替代厂商闭源blob,让开发者能在Linux桌面上自由控制这块GPU。无论你是在RK3399、RK3568这类开发板上折腾,还是在ARM笔记本上想用Mali跑桌面特效,Panfrost基本是现阶段最现实的选择。
这篇文章不打算泛泛科普,而是从架构设计出发,把Panfrost到底怎么组织用户态/内核态代码、怎么把OpenGL调用换算成Mali硬件能懂的job chain、编译器又如何把着色器喂给GPU这些核心问题讲透。我自己在RK3399板子上踩过不少坑,也会把那些文档里没写明白的细节一并记录下来,方便后来人少走弯路。
1. 从闭源到开源:Panfrost要解决什么问题
1.1 Mali GPU的生态现状
Mali GPU在移动芯片和嵌入式领域的出货量巨大,从手机SoC到电视盒子、Chromebook、开发板,到处都能看到它的身影。但这颗GPU的Linux驱动体验长期一言难尽:Arm官方提供的用户态库以二进制形式分发,版本匹配极其严格,内核侧的Mali驱动又往往跟着芯片厂商BSP走,主线内核支持一直属于“能用但别折腾”的状态。对于想深度定制、想调试、想研究底层实现的开发者来说,这种黑盒状态非常痛苦。
社区对Mali开源驱动的尝试不是一天两天了。最早是Lima项目对付Utgard架构,也就是Mali-400/450这一代。后来Collabora等公司推动的Panfrost接过了Midgard和Bifrost的重任,再往后Panthor又专门处理新一代Valhall架构。这条演进路径很清晰:每一代Mali硬件的差异都很大,开源驱动必须跟着硬件架构重新设计和实现,不能靠简单修修补补。
1.2 Panfrost盯上的是哪几代Mali
搞清楚Panfrost支持的范围特别重要,因为很多人拿着Mali-400的板子跑来问为什么驱动不起来,那就是没理解架构代际差异。
Mali GPU大致分这么几代:Utgard(Mali-200/400/450)、Midgard(Mali-T600/T700/T800系列)、Bifrost(Mali-G31/G52/G72/G76等)、Valhall(Mali-G57/G78/G710等)。Panfrost处理的是Midgard和Bifrost这两代,具体到常见型号:
| 架构 | 代表GPU型号 | 常见SoC |
|---|---|---|
| Midgard | Mali-T760、T860、T880 | RK3288、RK3399、Exynos 7420 |
| Bifrost | Mali-G52、G72、G76 | RK3566/RK3568、Exynos 9810、Kirin 980部分型号 |
| Valhall | Mali-G57、G78等 | 由Panthor驱动接管的版本 |
从内核DRM驱动的角度来看,Panfrost驱动对应的设备树compatible通常是arm,mali-txxx或者arm,mali-bifrost这一类字符串。如果你拿到的板子内核设备树里GPU节点匹配得上,说明内核侧大概率能绑定。用户态方面,Panfrost作为Mesa的Gallium驱动,只要Mesa版本和内核版本都满足要求,理论上就能跑。
这里有个很容易踩的坑:同一个GPU核心,在Android BSP里叫mali.ko,在主线内核里叫panfrost,两者注册的DRM驱动名字不同、设备树匹配逻辑不同,而且不能同时存在。很多时候你刷了主线内核,但板子的设备树还残留着闭源驱动的节点,或者内核把mali这个platform_driver也编进去了,结果就会冲突。后续实操章节我会专门讲怎么排查。
2. 整体架构拆解:一条命令怎么从应用走进GPU
2.1 用户态那半边:Gallium框架下的Panfrost
现代GPU驱动基本都是用户态与内核态分离的。应用通过OpenGL或者Vulkan API发起渲染,用户态部分负责状态跟踪、着色器编译、命令缓冲生成,然后通过系统调用把任务交给内核,内核驱动再负责把任务真正提交到硬件。Panfrost也不例外,它的用户态落在Mesa的Gallium框架里,代码主要在src/gallium/drivers/panfrost/目录下。
为什么Mesa要搞一个Gallium框架?简单说,Gallium定义了一套面向GPU驱动后端的统一接口。OpenGL、OpenGL ES、Vulkan这些API的语义解析和状态机管理,Mesa已经用客户端侧的状态跟踪器帮你处理了一部分,驱动后端只需要关心资源管理、命令提交、着色器编译这些底层环节。Panfrost作为Gallium驱动,直接复用了这套成熟框架,省去了大量重复劳动,也让GLSL和SPIR-V到最终GPU ISA的这条管线有了统一的中间表示基础。
Panfrost这个用户态驱动有几个关键模块。首先是一个面向Gallium的屏幕与上下文对象,负责管理设备初始化、缓冲区格式、渲染状态;其次是着色器编译器,它把Mesa统一的NIR中间表示逐步降级成Midgard或Bifrost的机器码;再就是命令流构造器,负责把Gallium的draw call、clear、blit等操作翻译成Mali硬件能理解的描述符结构。
2.2 内核态那半边:DRM驱动与uAPI
内核侧,Panfrost对应的驱动在drivers/gpu/drm/panfrost/。它属于DRM子系统,向用户态暴露了一组ioctl接口。别看用户态和内核态都是“Panfrost”,两者分工明确:内核负责内存对象管理、地址空间分配、GPU作业提交和硬件中断处理,用户态负责具体的图形语义和着色器编译。
Panfrost内核驱动的uAPI不算复杂,核心就是下面这几个ioctl:
| ioctl | 作用 |
|---|---|
| DRM_IOCTL_PANFROST_GET_PARAM | 获取GPU参数,比如GPU型号、线程上限、地址空间位数等 |
| DRM_IOCTL_PANFROST_CREATE_BO | 创建buffer object,分配GPU可访问的内存 |
| DRM_IOCTL_PANFROST_MMAP_BO | 把BO映射到用户态虚拟地址空间 |
| DRM_IOCTL_PANFROST_GET_BO_OFFSET | 查询BO在GPU地址空间中的偏移 |
| DRM_IOCTL_PANFROST_SUBMIT | 提交GPU作业 |
| DRM_IOCTL_PANFROST_WAIT_BO | 等待某个BO相关的作业完成 |
| DRM_IOCTL_PANFROST_PERFCNT | 获取性能计数器数据 |
这套uAPI设计得很务实。比如创建BO时,用户态需要告诉内核这个内存的用途:是用于着色器代码、顶点数据还是帧缓冲,内核会据此选择合适的缓存属性和映射方式。提交作业时,用户态会把一个job描述符链表的头指针传进来,然后内核驱动负责把作业挂进GPU的队列并触发硬件执行。
2.3 数据流:从提交到执行
我把一次最简单的glClear或者glDraw的底层数据流拆出来,这样更容易理解整个架构是怎么串起来的。
第一步,应用调用OpenGL API,Mesa的GL状态跟踪器把状态变化和绘制命令记录成Gallium层的调用。这些调用还会触发着色器处理:如果着色器源码此前没有编译过,Mesa会先调用编译器把GLSL转成NIR,再经过Panfrost后端编译成Mali ISA的机器码,打包成一个专门的着色器二进制对象。
第二步,当命令需要真正执行时,Panfrost用户态驱动会把当前帧缓冲、着色器、顶点缓冲、统一缓冲区等资源整理成一组硬件描述符。Mali的调度模型是作业链,也就是一个大的rework任务可以拆成多个有依赖关系的job,每个job有自己的入口描述符,GPU按顺序消费。Panfrost会构造出这些描述符,并写入可被GPU访问的内存区域。
第三步,用户态通过ioctl把当前job chain的地址和一些同步信息传给内核。内核驱动校验参数之后,写入GPU的寄存器或者内存队列,触发GPU开始执行。GPU完成作业后会产生中断,内核驱动在中断处理里更新fence状态,唤醒等待的用户态进程。
第四步,用户态检测到fence信号完成,知道渲染结果已经写入对应的BO。如果这块BO要被显示控制器扫描出来,它就通过drm_fb或者是输出接口完成后续步骤;如果结果要被读回CPU,则用同步机制保证一致性。
这个流程看着不复杂,真正麻烦的地方在于Mali硬件的并发模型。GPU内部有多个执行单元,顶点着色和片段着色可以是不同的job,tiler(几何处理器)和pixel引擎会并行工作,一个frame的处理可能需要好几个job互相依赖。Panfrost用户态代码里大量的逻辑都花在如何把这些job正确串联、如何设置依赖关系上。
3. 核心实现细节:Job chain、内存与编译器
3.1 Job chain:Mali GPU的“任务排队系统”
Mali GPU不像桌面GPU那样有一个统一命令缓冲区,它采用作业描述符链的模型。每个作业是一块结构化的内存区域,里面定义了作业类型(顶点、片段、tiler或者计算)、需要的资源地址、着色器入口等。硬件会从用户态提供的链头开始依次执行,也可以通过显式依赖关系跳转。Panfrost的核心任务之一,就是把Mesa的逻辑绘制命令翻译成这条链上一个个节点。
拿一次普通的三角形绘制举例。Panfrost会先构造一个PANFROST_JD_JOB_TYPE_CSF或SET_VALUE之类的初始化作业,把渲染目标、视口、裁剪状态写入;接着提交一个tiler作业,让tiler遍历几何图元,生成片段列表;然后提交顶点着色作业和片段着色作业;最后再提交一个结束作业来做flush和cache同步。这些细节在源码的pan_cmdstream.c里都能看到。
这里有个容易忽视的点:Mali的作业链并不只是GPU顺序执行那么简单。作业之间可以带依赖,父作业没完成时子作业还不能启动,这种依赖关系也需要Panfrost用户态精确构造。如果依赖设置错了,轻则花屏,重则GPU挂起。Panfrost在这一点上做了很多防御性的校验,调试版还会打印出job chain方便人肉检查。
3.2 内存模型:BO分配与GPU地址空间
Mali有独立的MMU,支持多级页表。Panfrost内核驱动为每个进程(或者说每个地址空间)管理一套GPU页表,用户态创建的每个BO都会在GPU地址空间中占据一段虚拟地址。GPU访问内存时用这些虚拟地址,通过MMU翻译成物理地址或系统内存。
DRM_IOCTL_PANFROST_CREATE_BO这个ioctl看起来只是分配一块内存,实际包含的信息量不小。用户态要指定BO的flags,比如是否可被CPU映射、是否需要与外部设备共享、是否要用特定的缓存策略。内核在分配时还要考虑是否走DMA-BUF机制。Panfrost支持用DMA-BUF做PRIME共享,这样显示控制器、视频解码器等设备就能和GPU共享同一块内存,避免无谓的拷贝。
我在RK3399上调一个视频播放场景时遇到过比较典型的问题:解码器输出格式是NV12,但Panfrost的纹理采样对特定行距有对齐要求,如果DRM-BUF里分配的行stride不满足GPU要求,纹理就会错位或者发绿。这类问题不能只靠Panfrost侧修改,必须同时看DMA-BUF分配端和GPU的布局约束,所以理解内存模型比单纯会写驱动重要得多。
3.3 编译器后端:从NIR到Mali指令
Panfrost的编译器部分是最容易被低估的。Mali Midgard和Bifrost的指令系统与桌面GPU差别很大,属于高度定制的VLIW风格,寄存器数量、执行宽度、ALU单元都有各种限制。Mesa的共通优化阶段把GLSL转成NIR之后,Panfrost需要完成寄存器分配、指令调度、编码等等。
代码结构上,src/panfrost/compiler和src/panfrost/bifrost分别应对Midgard和Bifrost,两者共享不少NIR层的pass,但指令选择和编码逻辑完全独立。这说明驱动开发时并没有走“一套编译器通吃所有型号”的捷径,因为硬件指令集的差异大到无法抽象。
编译器里比较难的是uniform和varying的处理。Mali GPU对uniform存储方式很敏感,有的指令能直接内联立即数,有的必须从uniform缓冲区加载。Panfrost编译器会尽量把小的uniform直接编码进指令槽,减少运行时内存访问。另一个难点是纹理描述符,Mali的纹理采样需要构造特殊的描述符结构,包含格式、过滤器、坐标环绕等参数,编译器需要知道特定架构支持哪些硬件纹理格式,无法直接采样的格式要回退到软件转换或者拒绝编译。
想调试着色器相关问题的话,MESA_SHADER_CAPTURE_PATH这个环境变量很有用,它会把每一个着色器的中间表示导出成文件,方便离线分析。我遇到过一次相同GLSL在T860上正常、在G52上花屏的问题,最后就是用这个变量抓出NIR,对比两个架构的编译结果,才发现是Bifrost后端对某种位操作模式优化不充分。
4. 实操指南:从源码编译到跑起来
4.1 环境准备与依赖确认
想体验Panfrost,前提是有一块使用Midgard或Bifrost GPU、且设备树匹配的硬件。常见选择是RK3399(T860)或者RK3568(G52)。系统方面,建议直接上主线内核,版本尽量新一些,因为Panfrost驱动仍然在活跃迭代,旧内核里的bug可能已经在主线修复。
编译Mesa之前,先确认这些依赖:meson、ninja、python3、libdrm的开发头文件、LLVM(如果开llvmpipe才需要)、flex、bison、libx11等。Ubuntu/Debian环境下,一条apt build-dep mesa就能把大部分依赖装好,这是最省心的做法。
还要检查内核配置。Panfrost需要CONFIG_DRM、CONFIG_DRM_PANFROST、CONFIG_DMA_SHARED_BUFFER、CONFIG_IOMMU_SUPPORT等选项。现代发行版内核一般默认开了,但如果你是自己裁剪内核,务必确认。
4.2 构建Mesa中的Panfrost
Mesa的构建比很多人想象中简单。下载源码后,在根目录执行这样的配置命令:
meson setup build/ \ -Dgallium-drivers=panfrost \ -Dvulkan-drivers=panvk \ -Dglx=gallium-xlib \ -Degl=auto \ -Dgbm=auto \ -Dllvm=disabled \ -Dbuildtype=release meson compile -C build meson install -C build几个参数我解释一下。-Dgallium-drivers=panfrost是告诉Mesa构建系统,Gallium驱动只需要Panfrost。-Dvulkan-drivers=panvk会编译Panfrost的Vulkan驱动PanVK,但这个驱动成熟度不如OpenGL方向,如果你只想跑桌面GL,可以先不开启。-Dglx=gallium-xlib适合在X11环境下用软件管线模拟GLX,这个选项在ARM平台上比较稳。
编译完成后,可以用glxinfo查看渲染器字符串。如果输出里能看到Mali-T860或者类似名称,说明Panfrost已经接管了GPU。这时候可以通过glmark2这类工具快速跑分,看看基本功能是否正常。
4.3 把内核侧驱动绑定好
用户态驱动编译好只是第一步,内核侧绑定才是最容易出问题的地方。
首先要确保内核里Panfrost驱动编译进去或加载。以模块方式编译时,用modprobe panfrost加载,然后看dmesg输出,确认有类似panfrost ffe40000.gpu: clock rate = xxxx的日志。如果驱动没有绑定,先查设备树GPUnode的compatible字符串是否在Panfrost驱动支持的列表里。
其次要禁用旧的Mali闭源驱动。很多板子BSP里会编译出一个mali模块或者把Mali驱动编进内核,它和Panfrost争抢同一个设备节点,必须通过blacklist或者在设备树中移除mali节点来规避。我遇到过最隐蔽的情况是模块名不叫mali而叫mali_kbase,搜索内核模块列表时不容易发现。
最后还要注意用户态库的冲突。系统里如果装了厂商的libmali.so或者libGLESv2.so,Panfrost编译的Mesa库可能不会被优先使用。处理办法是在/etc/ld.so.conf.d/里调整搜索路径,或者直接卸载厂商库。由于各家发行版的库路径策略不一样,最好的排查方式是ldd看某个GL程序实际链接的是哪个so。
5. 调试方法与性能排查实录
5.1 pandecode抓帧与分析
Panfrost的用户态驱动内置了一个非常实用的调试功能:pandecode。设置环境变量PAN_MESA_DEBUG=pandecode后,驱动在提交每个job时会把job chain的完整内容导出成文件,并附带对应的pandecode工具生成的文本解析。这些数据能详细展示每次绘制提交了哪些描述符、内存地址、着色器信息,对定位花屏、GPU hang、命令构造错误都非常有价值。
pandecode的输出量很大,一次简单的glmark2运行都可能生成上万个文件。我习惯先跑一个最小的复现场景,比如只有一次绘制程序的代码,再单独分析生成的.pnd文件。在分析时重点看job type是否合理、依赖关系是否正确、BO地址是否在合理范围。
配合pandecode还可以看系统里有没有用PAN_MESA_DEBUG=gpu之类的选项输出GPU相关错误。Panfrost支持多个DEBUG位,具体可以看源码里的pan_debug.h,不同版本差异不小,但pandecode这个选项长期保留。
5.2 调试环境变量与性能工具
Panfrost相关的环境变量不少,常用的有这些:
| 环境变量 | 作用 |
|---|---|
| PAN_MESA_DEBUG=pandecode | 导出job chain并执行pandecode解析 |
| PAN_MESA_DEBUG=verbose | 输出更详细用户态驱动日志 |
| PAN_MESA_DEBUG=dump | 导出着色器二进制和其他内部对象 |
| MESA_DEBUG=1 | 打开Mesa通用调试输出 |
| EGL_LOG_LEVEL=debug | 输出EGL层的连接日志 |
| MESA_VK_ABORT_ON_DEVICE_LOST | Vulkan设备丢失时直接中断便于抓栈 |
性能方面,如果只是跑整体性能数据,glmark2、glxgears都能用。想定位单个draw call的开销,可以用MESA_DEBUG加perfetto或者简单的计时。但说句实在话,在开源驱动上抓性能一定要先确认CPU瓶颈和GPU瓶颈的区别,Mali的Bifrost对CPU提交开销比较敏感,频繁的小draw call会导致CPU半径很长,有时候优化API用法比调驱动更有效。
5.3 常见故障与排查速查表
我把这些年在Panfrost上遇到比较典型的问题整理成一个表,方便对照排查:
| 现象 | 常见原因 | 处理思路 |
|---|---|---|
| glxinfo显示llvmpipe而不是Mali | Panfrost库没被加载,或GPU设备未绑定 | 检查ls /dev/dri、dmesg、库搜索路径 |
| 花屏或纹理错位 | BO行距对齐问题,或者着色器编译出错 | 用pandecode抓命令,对比不同BO的stride |
| GPU hang,dmesg出现panfrost timeout | job chain构造错误,或者内核与Mesa版本不匹配 | 先升级内核和Mesa,再用简化场景复现 |
| 加载panfrost时报device not found | 设备树节点compatible不匹配或被mask掉 | 查看/sys/firmware/devicetree/base下GPU节点 |
| 某些GL扩展不可用 | Panfrost还未实现对应特性 | 查Mesa文档和源码,确认GPU架构支持情况 |
| 切换VT时黑屏或冻屏 | KMS和Panfrost扫描输出配合问题 | 检查fbcon、drm_kms_helper参数,换内核版本对比 |
如果你在板子上跑桌面,还有一个高频问题:Panfrost申请GPU内存被拒绝。这通常是因为CMA区域或者DMA-BUF配额耗尽,Mali GPU需要连续内存的情况比较多。可以通过内核cmdline调整CMA大小,比如cma=256M。不过这个参数会影响系统整体内存布局,改之前先想清楚。
6. 写在最后:一点个人体会
玩了几年Panfrost,最大的感受是开源驱动最难的不是写代码,而是把硬件行为摸清楚。Mali的很多设计决策在公开文档里表述得比较粗略,Panfrost宏大的逆向工作能走到今天,靠的是一群人用pandecode一条一条命令尝试、对着寄存器手册硬啃。这种精神恰恰是驱动开发最有价值的部分。
如果你也想入坑,我建议先不要急着改驱动代码,而是用Panfrost跑一遍成熟的图形程序,体验完整的编译、部署、调试闭环。等你对job chain和BO分配有了手感,再去看pan_cmdstream.c或者编译器代码,会有一种“原来如此”的通透感。
Mali GPU的下一代Valhall已经由Panthor接管,Panfrost的Midgard/Bifrost支持也逐渐趋于稳定,但这不代表它会立刻退出舞台。大量RK3399、RK3568设备仍然在服役,这些板子上的Linux桌面体验已经比几年前好太多了。对我个人来说,Panfrost的存在至少让“在ARM板子上跑通OpenGL”这件事变成了可调试、可理解、可复现的技术实践,而不是对着闭源库碰运气。