2. GPU与驱动基础

Mali GPU 驱动开发的第一课,不是急着写代码,而是先建立对 GPU 硬件、计算模型和软件栈的整体认知。Mali 系列 GPU 广泛应用于移动 SoC(如 RK3588、麒麟、Exynos、联发科天玑),其驱动架构与桌面 GPU(如 NVIDIA、AMD)有显著差异。本章将围绕三条主线展开:GPU 渲染管线如何工作、并行计算模型如何组织线程、驱动软件如何分层协作。这三者构成了后续所有驱动调试、性能优化和功能开发的基础。

Mali GPU 驱动分层架构与核心概念总览 用户态(User Space) Mali 用户态驱动 (libmali.so) OpenGL ES / Vulkan API 实现 应用框架 (HWC/EGL) SurfaceFlinger / Gralloc Mali 编译器工具链 malioc / maliitc / 离线编译 DDK 配置接口 /dev/mali 访问封装 内核态(Kernel Space) Mali 内核驱动 (mali_kbase) MMU / 内存管理 / 作业调度 电源管理 / 频率调节 DMA-BUF / 内存堆 物理内存分配 / 连续内存 GPU 页表维护 平台集成层 (Mali PM / DVFS) OPP / 时钟 / 稳压器 / 中断 GPU 频率与电压调节 硬件层(Mali GPU 核心) Job Manager 作业队列 / 优先级 硬件调度 MMU 地址转换 / 权限控制 TLB 缓存 着色器核心 Vertex / Fragment / Compute 统一着色器簇 L2 缓存 / 内存接口 AXI 总线 / 一致性 带宽优化 ioctl / mmap 寄存器 / 中断 / DMA 渲染管线:应用 → 用户态驱动 → 内核驱动 → Job Manager → 着色器核心 → 帧缓冲

上图展示了 Mali 驱动栈的整体分层关系。用户态驱动负责 API 语义和命令流生成,内核驱动负责资源管理和硬件访问,硬件层则执行实际的图形与计算任务。理解这条链路,是后续分析驱动问题时的第一张地图。

2.1 GPU 渲染管线:从顶点到像素的流水线

Mali GPU 的渲染管线遵循经典的图形流水线模型,但针对移动端的功耗和带宽约束做了大量优化。管线分为几何阶段和像素阶段,每个阶段内部又包含多个子阶段。驱动开发人员必须清楚每个阶段在硬件中的执行位置,以及哪些阶段是可编程的、哪些是固定功能的。

核心原理:Mali 渲染管线的四个关键阶段

Mali 渲染管线可以简化为四个主要阶段:顶点处理(Vertex Processing)、图元装配(Primitive Assembly)、光栅化(Rasterization)和片段处理(Fragment Processing)。顶点着色器(Vertex Shader)在统一着色器簇(Unified Shader Core)上执行,将模型空间坐标变换到裁剪空间。图元装配阶段由固定功能硬件完成,将顶点连接成三角形或线段。光栅化将图元离散为像素片段,生成每个片段的屏幕坐标和插值属性。片段着色器(Fragment Shader)计算每个片段的颜色、深度和模板值。

Mali 的独特之处在于其基于区块的渲染(Tile-Based Rendering)架构。与立即模式渲染(Immediate Mode Rendering)不同,Mali 先将整个帧缓冲划分为多个小区块(通常为 16x16 或 32x32 像素),在片上 SRAM 中完成区块内的所有渲染操作,最后才将结果写回主存。这种设计大幅降低了对外部 DRAM 的带宽需求,是移动 GPU 能效比的关键。

管线阶段 可编程性 硬件单元 驱动职责
顶点处理 可编程(Vertex Shader) 统一着色器簇 编译着色器、管理顶点缓冲
图元装配 固定功能 图元引擎 设置图元类型、裁剪配置
光栅化 固定功能 光栅化器 配置视口、深度偏移
片段处理 可编程(Fragment Shader) 统一着色器簇 编译着色器、管理帧缓冲

判断方法:如何定位渲染管线瓶颈

在驱动开发中,判断管线瓶颈是常见任务。首先查看 Mali 提供的性能计数器(Performance Counter),关注顶点吞吐量(Vertex Throughput)和片段吞吐量(Fragment Throughput)。如果顶点吞吐量接近硬件上限而片段吞吐量较低,说明几何阶段是瓶颈;反之则是像素填充率受限。其次,检查区块渲染的命中率(Tile Hit Rate),如果命中率低于 80%,说明帧缓冲访问过于频繁,可能需要调整渲染顺序或使用 Early-Z 优化。

经验提醒:Mali 的 Midgard 和 Bifrost 架构在管线设计上有差异。Midgard 的顶点和片段着色器共享同一组执行单元,但调度策略不同;Bifrost 引入了更细粒度的执行单元划分。开发驱动时,必须针对具体 GPU 架构(如 Mali-G52、G76、G78)查阅对应 DDK 文档,不能一概而论。

案例拆解:一次简单的三角形渲染流程

假设应用提交一个三角形绘制调用。用户态驱动首先将顶点数据从 CPU 内存拷贝到 GPU 可访问的内存区域(通过 DMA-BUF 或 Mali 专有内存分配)。然后,驱动将顶点着色器编译为 Mali 的中间表示(Midgard/Bifrost 指令),并打包成作业描述符(Job Descriptor)。内核驱动通过 ioctl 接收作业描述符,将其放入硬件作业队列。Job Manager 按优先级调度作业,将顶点作业分发给着色器核心执行。顶点处理完成后,图元引擎执行裁剪和背面剔除,光栅化器生成片段,片段着色器计算最终颜色。最后,区块写入单元将结果从片上 SRAM 写回帧缓冲。

这个流程中,驱动需要管理多个资源生命周期:顶点缓冲的映射与解映射、着色器程序的加载与缓存、作业描述符的内存分配与回收。任何一个环节出错,都可能导致渲染异常或 GPU 崩溃。

常见误区与总结

  • 误区一:认为渲染管线是纯线性的。实际上 Mali 支持多级流水线并行,顶点处理和片段处理可以同时进行,驱动需要合理设置作业依赖关系。
  • 误区二:忽略区块渲染的影响。在 Mali 上,跨区块的渲染操作(如全屏后处理)需要特殊处理,否则会产生额外的内存读写。
  • 误区三:将桌面 GPU 的优化经验直接套用到 Mali。桌面 GPU 的立即模式渲染与 Mali 的区块渲染在带宽优化策略上完全不同。

2.2 并行计算模型:线程、线程组与内存模型

Mali GPU 不仅用于图形渲染,还支持通用计算(GPGPU),通过 OpenCL 或 Vulkan Compute 接口暴露并行计算能力。理解 Mali 的并行计算模型,是开发计算着色器驱动和优化计算性能的前提。

核心原理:Mali 的并行执行层次

Mali 的并行计算模型遵循 OpenCL 的层次结构:NDRange(索引空间) → 工作组(Workgroup) → 工作项(Workitem)。每个工作项对应一个线程,多个工作项组成工作组,工作组内的线程可以共享局部内存并同步。在硬件层面,Mali 的着色器核心以线程束(Warp)或子组(Subgroup)为单位调度执行,每个线程束包含 4 到 16 个线程(具体取决于架构)。

Mali 的内存模型分为三层:全局内存(Global Memory)、局部内存(Local Memory)和私有内存(Private Memory)。全局内存对所有工作项可见,通常映射到系统 DRAM;局部内存位于片上 SRAM,访问延迟低但容量有限;私有内存是每个工作项独占的寄存器或栈空间。驱动需要负责这些内存区域的分配、映射和一致性维护。

层次 作用域 存储位置 驱动管理要点
NDRange 整个计算任务 全局内存 分配缓冲区、设置参数
工作组 组内共享 局部内存(SRAM) 分配局部内存、处理同步
工作项 单个线程 寄存器 / 私有内存 寄存器分配、栈管理

判断方法:计算任务是否适合 Mali GPU

判断一个计算任务是否适合 Mali GPU,可以从三个维度分析:并行度、数据复用度和内存访问模式。并行度要求任务可以拆分为大量独立的小任务(至少数千个工作项);数据复用度要求相邻工作项访问的数据有重叠,以便利用局部内存或缓存;内存访问模式要求尽量顺序访问,避免随机访问导致缓存命中率下降。

驱动开发中,可以通过 Mali 的性能计数器(如 GPU Active Cycles、Shader Core Utilization)来评估计算任务的执行效率。如果着色器核心利用率低于 50%,说明并行度不足或内存访问瓶颈严重。

风险提示:Mali 的局部内存容量有限(通常为 16KB 到 64KB 每着色器核心)。如果工作组申请的局部内存超过硬件限制,驱动会回退到全局内存模拟,导致性能急剧下降。开发驱动时,必须检查局部内存使用量,并在文档中明确限制。

案例拆解:图像卷积的并行化实现

以 3x3 图像卷积为例。假设输入图像为 1920x1080,我们将其划分为 16x16 的工作组,每个工作组处理 16x16 像素块。每个工作项计算一个输出像素,需要访问输入图像中对应的 3x3 邻域。由于相邻工作项访问的输入数据有重叠,可以将输入块加载到局部内存中,减少全局内存访问次数。

驱动需要做的工作包括:分配输入和输出缓冲区(全局内存)、为每个工作组分配局部内存(16x16+2 边界像素)、设置 NDRange 参数(全局尺寸 1920x1080,局部尺寸 16x16)、编译并加载计算着色器。在 Mali 上,还需要考虑像素块与区块边界的对齐,避免跨区块访问导致性能损失。

实际测试中,使用局部内存优化后,卷积操作的带宽消耗降低了约 60%,执行时间缩短了 40%。但代价是局部内存占用增加,且需要处理边界条件(图像边缘的工作组需要特殊处理)。

常见误区与总结

  • 误区一:认为工作项越多越好。过多的线程束切换会增加调度开销,Mali 的每个着色器核心同时执行的线程束数量有限。
  • 误区二:忽略内存对齐。Mali 对内存对齐有严格要求(通常 16 字节对齐),未对齐的访问会导致性能惩罚或硬件异常。
  • 误区三:过度使用同步。工作组内的同步(barrier)会强制线程束等待,频繁同步会严重降低并行效率。

2.3 驱动分层架构:用户态、内核态与硬件接口

Mali 驱动采用经典的三层架构:用户态驱动(User-Space Driver)、内核态驱动(Kernel-Space Driver)和硬件抽象层。每一层都有明确的职责边界,层与层之间通过标准接口通信。理解这个分层结构,是驱动开发的基本功。

核心原理:三层架构的职责划分

用户态驱动(通常以 libmali.so 形式提供)实现了 OpenGL ES、Vulkan、OpenCL 等 API 的语义。它负责将 API 调用转换为 Mali 的硬件命令流(Command Stream),管理着色器编译、纹理上传、顶点缓冲等资源。用户态驱动运行在应用进程的地址空间中,可以直接访问 GPU 内存映射(通过 mmap),但无法直接操作硬件寄存器。

内核态驱动(mali_kbase 模块)运行在内核空间,负责硬件资源的管理和调度。主要功能包括:内存管理(GPU MMU 页表维护、DMA-BUF 导入导出)、作业调度(Job Manager 的作业队列管理)、电源管理(DVFS、运行时电源管理)、中断处理(GPU 中断的响应和分发)。内核驱动通过 ioctl 接口与用户态驱动通信,通过寄存器读写和中断与硬件交互。

硬件抽象层是 Mali GPU 的物理实现,包括 Job Manager、MMU、着色器核心、L2 缓存等模块。硬件提供了寄存器接口(如 JOB_CONTROL、MMU_CONTROL)和中断信号,驱动通过这些接口控制硬件行为。

层次 运行环境 主要接口 典型文件/模块
用户态驱动 应用进程 EGL/GLES/Vulkan API libmali.so, mali_gles.c
内核态驱动 内核空间 ioctl, mmap, DMA-BUF mali_kbase_core.c, mali_mmu.c
硬件抽象层 GPU 硬件 寄存器, 中断, DMA Job Manager, MMU, Shader Core

操作流程:一次 GPU 作业的完整提交路径

以 Vulkan 绘制调用为例,作业提交的完整流程如下:

  1. 应用调用 vkQueueSubmit(),用户态驱动将命令缓冲(Command Buffer)解析为 Mali 的作业描述符。
  2. 用户态驱动通过 ioctl(如 KBASE_IOCTL_JOB_SUBMIT)将作业描述符传递给内核驱动。
  3. 内核驱动检查作业的合法性(内存映射、权限),将作业放入 Job Manager 的硬件队列。
  4. Job Manager 根据优先级和硬件状态调度作业,将作业分配给可用的着色器核心。
  5. 着色器核心执行作业,完成后通过中断通知内核驱动。
  6. 内核驱动处理中断,更新作业状态,并通过信号量或事件通知用户态驱动。
  7. 用户态驱动唤醒等待的应用线程,应用继续执行后续操作。

这个流程中,每一步都可能成为性能瓶颈或错误源。例如,ioctl 调用次数过多会增加系统调用开销;作业描述符的内存分配不当会导致 GPU 页错误;中断处理延迟过高会降低 GPU 利用率。

关键设计决策:Mali 驱动采用"用户态尽量多做"的设计哲学。用户态驱动负责所有资源跟踪和命令生成,内核驱动只做必要的资源管理和硬件访问。这种设计减少了系统调用次数,但也要求用户态驱动具备高度的健壮性——用户态驱动的崩溃不应导致内核崩溃。

案例拆解:调试一次 GPU 页错误

假设在运行一个图形应用时,内核日志出现 Mali MMU page fault 错误。初始现象是应用渲染黑屏,随后 GPU 作业被终止。关键日志如下:

[ 1234.567] mali_kbase: Mali MMU page fault at address 0x00000000f2a34000
[ 1234.567] mali_kbase: Fault status: 0x00000002 (permission fault)
[ 1234.567] mali_kbase: Faulting job: 0x0000000012345678
[ 1234.567] mali_kbase: Memory region: [0xf2a34000 - 0xf2a35000] (4KB)

分析过程:首先确认故障地址 0xf2a34000 是否在已映射的 GPU 内存范围内。通过 cat /proc/mali/memory 查看当前 GPU 内存映射,发现该地址属于一个已释放的缓冲区。进一步检查用户态驱动的内存管理代码,发现缓冲区在作业提交前被提前释放,导致 GPU 访问时发生页错误。

修复方案:在用户态驱动中增加缓冲区引用计数,确保作业完成前缓冲区不会被释放。具体操作是:在作业提交时增加引用计数,在作业完成回调中减少引用计数。修复后,页错误消失,应用恢复正常渲染。

复盘经验:这类问题在驱动开发中非常常见,根源是用户态驱动的资源生命周期管理不当。建议在开发初期就建立严格的资源跟踪机制,并在调试版本中启用内存访问检查。

常见误区与总结

  • 误区一:混淆用户态和内核态的职责。用户态驱动不应直接操作硬件寄存器,内核驱动不应处理 API 语义。
  • 误区二:忽略 ioctl 的性能开销。频繁的小型 ioctl 调用会显著降低性能,应尽量批量提交作业。
  • 误区三:不重视中断处理。中断处理函数应尽量短小,耗时操作应放到工作队列或线程中。