编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

      • QML基础入门
      • 嵌入式GUI技术全景
      • QML引擎与渲染原理
      • QML语法与类型系统
      • 属性绑定与响应式原理
      • 可视元素与布局原理
      • 事件处理与传播机制
      • 模型视图架构原理
      • 动画与状态机原理
      • Canvas与自定义渲染
      • QML与C++集成原理
      • 自定义SceneGraph节点
      • 交叉编译与部署
      • 嵌入式渲染后端
        • 13.1 案例引入
          • 13.1.1 ARM 撕裂
          • 13.1.2 根因分析
          • 13.1.3 三大问题
        • 13.2 运行环境
          • 13.2.1 有无桌面
          • 13.2.2 QPA 层
          • 13.2.3 VSync 原理
          • 13.3.3 输入设备
          • 13.3.4 多屏显示配置
          • 13.3.5 dmabuf 零拷贝
        • 13.4 LinuxFB 后端
          • 13.4.1 CPU 直写
          • 13.4.2 性能与场景
        • 13.5 Wayland 后端
          • 13.5.1 Wayland 协议
          • 13.5.2 多窗口场景
        • 13.6 GPU 驱动适配
          • 13.6.1 驱动对比
          • 13.6.2 DRM 对接
          • 13.6.3 驱动排查
        • 13.7 双屏车机
        • 13.8 新手陷阱
        • 13.9 训练题
        • 13.10 速查表
      • 性能优化与真机调试
    • QT核心库实践

    • Linux系统编程

    • 综合项目实战

  • IoT智能硬件开发

  • Apps
  • Linux应用开发
  • QML基础入门
杨充
2025-06-24
目录

嵌入式渲染后端

# 第 13 章 嵌入式渲染后端

本章定位:从"桌面跑通"到"ARM 点亮屏幕"。第 2 章讲 Scene Graph 时提到了 EGLFS——但为什么嵌入式必须用 EGLFS?同一个 QML 应用在桌面上完美运行、拷贝到 ARM 板上却画面撕裂——答案在渲染后端的选择上。本章深入 EGLFS / LinuxFB / Wayland 三种后端原理、GPU 驱动适配(Mesa/闭源)、DRM/KMS 框架与 Qt QPA 的对接——让 QML 在无桌面环境的 ARM 设备上稳定跑 60fps。

# 目录介绍

  • 13.1 案例引入
    • 13.1.1 ARM 撕裂
    • 13.1.2 根因分析
    • 13.1.3 三大问题
  • 13.2 运行环境
    • 13.2.1 有无桌面
    • 13.2.2 QPA 层
  • 13.3 EGLFS 后端详解
    • 13.3.1 EGL 全流程
    • 13.3.2 KMS 与 VIV
    • 13.3.3 输入设备
    • 13.3.4 多屏显示配置
  • 13.4 LinuxFB 后端
    • 13.4.1 CPU 直写
    • 13.4.2 性能与场景
  • 13.5 Wayland 后端
    • 13.5.1 Wayland 协议
    • 13.5.2 多窗口场景
  • 13.6 GPU 驱动适配
    • 13.6.1 驱动对比
    • 13.6.2 DRM 对接
    • 13.6.3 驱动排查
  • 13.7 双屏车机
  • 13.8 新手陷阱
  • 13.9 训练题
  • 13.10 速查表

# 13.1 案例引入

# 13.1.1 ARM 撕裂

某车机工程师在 Ubuntu x86(X11 桌面环境)上开发了一套仪表盘程序——画面完美、FPS 60。交叉编译部署到 ARM 板(i.MX8 + Vivante GPU + 无桌面环境)后,用 -platform xcb 启动——直接失败(没有 X Server)。换成 -platform eglfs 后画面出来了,但仪表盘指针动画出现明显撕裂和抖晃:

桌面 x86: -platform xcb   → ✅ 完美 60fps、无撕裂
ARM 板:   -platform eglfs  → ⚠️ 画面撕裂、指针动画抖动

为什么?都是同一套 QML 代码、同一个 Scene Graph!

# 13.1.2 根因分析

两种后端在 Swap 阶段的行为不同:

X11 (xcb):
  eglSwapBuffers() → X11 Present extension → compositor 合成
  → 自动等待 VSync → 无撕裂 ✅

EGLFS (默认):
  eglSwapBuffers() → DRM page flip → 等待 VSync
  → 但 Vivante 闭源驱动不支持 DRM atomic modesetting
  → eglSwapBuffers 不等待 VSync → 画面撕裂 ❌

修复:
  export QT_QPA_EGLFS_FORCEVSYNC=1       # 强制等待 VSync
  或
  export QT_QPA_EGLFS_KMS_CONFIG=1       # 启用 KMS 原子模式

# 13.1.3 三大问题

问题 在哪节回答
EGLFS 从 eglGetDisplay 到 eglSwapBuffers 的完整启动链路是什么? §13.3
没有 GPU 的板子怎么跑 QML?LinuxFB 性能如何? §13.4
Vivante / Adreno / Mali 各家 GPU 的闭源驱动有什么坑? §13.6

# 13.2 运行环境

# 13.2.1 有无桌面

有桌面环境(X11 / Wayland)            无桌面环境(裸 framebuffer)
┌────────────────────────────┐        ┌────────────────────────────┐
│ QML App                    │        │ QML App                    │
│   → Qt (libQt6Gui)        │        │   → Qt (libQt6Gui)        │
│     → X11 / Wayland 协议   │        │     → QPA (eglfs 插件)    │
│       → compositor 合成    │        │       → DRM/KMS → GPU     │
│         → 显示器           │        │         → 显示器           │
│                            │        │                            │
│ 启动 5s+(加载桌面环境)    │        │ 启动 < 1s(直接接管屏幕)  │
│ 多窗口、窗口管理器 ✅       │        │ 单进程独占、无标题栏 ❌    │
│ 开发期调试方便             │        │ 生产环境标准                                │
└────────────────────────────┘        └────────────────────────────┘
维度 有桌面 无桌面(eglfs)
启动速度 3-5s(加载 X11/Wayland compositor) < 1s
内存占用 +200 MB(桌面环境) +0 MB
多窗口 ✅ ❌(单进程独占)
垂直同步 X11 Present / Wayland 协议保证 依赖 DRM KMS + 驱动
输入设备 X11 input / libinput 直接读 /dev/input/event*
适用 开发调试 生产环境 99%

# 13.2.2 QPA 层

Qt 用 QPA(Qt Platform Abstraction)隔离不同平台的底层差异:

QML App
  → QtGui (QWindow / QScreen / QBackingStore)
    → QPA Plugin ← 平台后端——编译时链接
      ├── qxcb       → X11
      ├── qeglfs     → EGL Full Screen(嵌入式)
      ├── qlinuxfb   → Linux Framebuffer
      ├── qwayland   → Wayland
      └── qoffscreen → 离屏渲染(CI/测试)
# 选择 QPA 后端——环境变量
export QT_QPA_PLATFORM=eglfs      # 嵌入式(默认如果有 libqeglfs.so)
export QT_QPA_PLATFORM=xcb       # X11 桌面
export QT_QPA_PLATFORM=linuxfb    # 无 GPU 的 framebuffer
export QT_QPA_PLATFORM=wayland    # Wayland
export QT_QPA_PLATFORM=offscreen  # 无头测试

# 查看可用的 QPA 后端
./myapp -platform minimal --help   # 列出所有已编译的 QPA 插件

# 13.2.3 VSync 原理

案例引入中"桌面不撕裂、ARM 撕裂"的根因在 Swap 阶段——要理解它,必须深入帧缓冲交换机制。

双缓冲——最基础的无撕裂方案

没有双缓冲时,GPU 直接写屏幕的 Front Buffer——如果写到一半屏幕刷新,用户看到的就是"半帧旧图 + 半帧新图":

单缓冲(无 VSync):
  GPU 写 Front Buffer: ████████░░░░░░░░
  屏幕这时刷新:              ↑  ← 读到撕裂边界
  → 上半帧新内容 + 下半帧旧内容 = "撕裂"

双缓冲 + VSync:
  帧 N: Front=A(完整显示) Back=B(GPU正在画)  屏幕读 A ✅
  帧 N+1: eglSwapBuffers() → 交换 A↔B → Front=B  Back=A
  帧 N+1: 屏幕读 B ✅ GPU画A

DRM Page Flip——硬件级指针交换

EGLFS 不靠 memcpy 交换帧缓冲,而是用 DRM kernel API Page Flip——硬件修改显示控制器的扫描起始地址:

drmModePageFlip():
  1. 新 framebuffer 物理地址 → 提交到硬件队列
  2. 注册 VBlank 中断
  3. VBlank 瞬间 → 硬件切换扫描地址 → 显示新帧
  4. 旧 framebuffer → 标记为空闲 → 应用可回收

关键: 步骤 3 发生在屏幕"不刷新"的垂直消隐期——没有撕裂窗口

三缓冲——GPU 不空转

双缓冲的问题:GPU 画完 → 等 VSync → swap → 才能画下一帧。渲染接近 16.6ms 时 GPU 空转:

双缓冲(渲染 14ms/帧):
 GPU: ██████████████░░  ██████████████░░  ← 每帧闲置 2.6ms
 帧:     A → 屏    swap     B → 屏    swap

三缓冲(渲染 14ms/帧):
 GPU: ████████████████████████████████████  ← 连续工作无闲置
 Buf:  A→屏  B→Back1  C→Back2  A→Back1...
 代价: +1 帧延迟(16.6ms) + 多一帧 VRAM

VSync 三层控制点:

层次 机制 控制方式
应用层 eglSwapInterval QSurfaceFormat::setSwapInterval(1)
驱动层 eglSwapBuffers 阻塞等待 QT_QPA_EGLFS_FORCEVSYNC=1
内核层 DRM atomic page flip 驱动实现 drm_mode_config_funcs.atomic_commit

探索性问题——FORCEVSYNC=1 后反而掉到 30fps?

现象: Vivante 闭源驱动 + FORCEVSYNC=1 → 55fps → 30fps
  ↓ QSG_RENDER_TIMING: render_time 12ms, swap_time 21ms
  ↓ swap_time 不应该 > 16.6ms——除非不是真正的 page flip
  ↓ 根因: 闭源驱动用 glFinish() + 忙等(16.6 - render_time) 模拟 VSync
  ↓       → render_time 波动时忙等算不准 → 偶尔等超 1 个 VSync → 掉帧
  ↓ 修复: 改用开源 Mesa(etnaviv) 驱动 → 真正的 DRM atomic page flip

---

## 13.3 EGLFS 后端详解

### 13.3.1 EGL 全流程

EGLFS = **EGL** + **FullScreen**。一个 QML 进程独占整个屏幕,不经过任何窗口管理器:

┌─────────────────────────────────────────────────┐ │ QML 应用进程 │ ├─────────────────────────────────────────────────┤ │ Qt Quick (Scene Graph) │ ├─────────────────────────────────────────────────┤ │ QPA: eglfs 插件 │ │ ┌───────────────────────────────────────────┐ │ │ │ ① eglGetDisplay(EGL_DEFAULT_DISPLAY) │ │ ← 打开 GPU 显示设备 │ │ → 在 DRM/KMS 路径: /dev/dri/card0 │ │ │ │ → 在 fbdev 路径: /dev/fb0 │ │ │ │ │ │ │ │ ② eglInitialize(major, minor) │ │ ← 初始化 EGL 驱动 │ │ → 加载 GPU 厂商的 EGL 实现库 │ │ │ │ → libEGL.so → libGLESv2.so │ │ │ │ │ │ │ │ ③ eglChooseConfig(attrs, &config) │ │ ← 选择 framebuffer 配置 │ │ attrs: EGL_RED_SIZE=8, │ │ │ │ EGL_GREEN_SIZE=8, │ │ │ │ EGL_BLUE_SIZE=8, │ │ │ │ EGL_DEPTH_SIZE=0, ... │ │ │ │ │ │ │ │ ④ eglCreateWindowSurface(display, config, │ │ ← 创建渲染目标 │ │ nativeWindow) │ │ nativeWindow: │ │ → DRM/KMS: 创建一个 DRM plane │ │ │ │ → fbdev: 直接映射 /dev/fb0 │ │ │ │ │ │ │ │ ⑤ eglMakeCurrent(display, surface, │ │ ← 绑定 GL 上下文 │ │ surface, context) │ │ │ │ │ │ │ │ ⑥ glViewport / glClear / glDrawArrays... │ │ ← Scene Graph 渲染 │ │ │ │ │ │ ⑦ eglSwapBuffers(display, surface) │ │ ← 切换到前面缓冲 │ │ → DRM: drmModePageFlip → 等待 VSync │ │ │ │ → fbdev: memcpy → 无 VSync │ │ │ └───────────────────────────────────────────┘ │ ├─────────────────────────────────────────────────┤ │ Linux Kernel │ │ ┌───────────────────────────────────────────┐ │ │ │ DRM / KMS / fbdev driver │ │ │ │ → GPU → HDMI / LVDS / MIPI-DSI 输出 │ │ │ └───────────────────────────────────────────┘ │ └─────────────────────────────────────────────────┘


### 13.3.2 KMS 与 VIV

EGLFS 内部有多个"后端"——通过配置 JSON 选择:

```bash
# DRM/KMS 后端(默认——使用 Linux DRM 接口)
export QT_QPA_EGLFS_INTEGRATION=eglfs_kms

# Vivante 专用后端(NXP i.MX6/8 闭源驱动)
export QT_QPA_EGLFS_INTEGRATION=eglfs_viv

# Mali 专用后端
export QT_QPA_EGLFS_INTEGRATION=eglfs_mali
后端 GPU 厂商 垂直同步 注意事项
eglfs_kms 通用(Mesa drm) ✅ DRM atomic page flip 通用性最强
eglfs_viv Vivante(NXP i.MX) ⚠️ 需 QT_QPA_EGLFS_FORCEVSYNC=1 闭源驱动不支持 KMS atomic
eglfs_mali ARM Mali ✅ 需要 Mali 用户空间库
eglfs_emu 虚拟(QEMU) — 模拟用

# 13.3.3 输入设备

EGLFS 不经过 X11/Wayland 做输入转发——它直接读 Linux evdev:

# EGLFS 启动时扫描 /dev/input/ 下的设备
export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event2:rotate=180
export QT_QPA_EVDEV_KEYBOARD_PARAMETERS=/dev/input/event1:grab=1
export QT_QPA_EVDEV_MOUSE_PARAMETERS=/dev/input/event0

# 查看可用输入设备
cat /proc/bus/input/devices | grep -E "^[NH]:|^$" | grep -A1 -i touch
// EGLFS 内部的等价逻辑(简化)
void EvdevTouchHandler::readEvents() {
    struct input_event ev;
    while (read(m_fd, &ev, sizeof(ev)) > 0) {
        if (ev.type == EV_ABS) {
            // 坐标事件 → QTouchEvent
            updateTouchPoint(ev.code, ev.value);
        } else if (ev.type == EV_KEY && ev.code == BTN_TOUCH) {
            if (ev.value == 1) emit touchPressed();
            else               emit touchReleased();
        }
    }
}

# 13.3.4 多屏显示配置

车机典型场景——仪表盘 + 中控 + HUD 三屏拼接:

// eglfs_kms_config.json
{
    "device": "/dev/dri/card0",
    "outputs": [
        {
            "name": "HDMI-1",
            "mode": "1920x720@60",
            "virtualIndex": 0         // 仪表盘
        },
        {
            "name": "HDMI-2",
            "mode": "1280x800@60",
            "virtualIndex": 1         // 中控屏
        },
        {
            "name": "LVDS-1",
            "mode": "480x240@60",
            "virtualIndex": 2         // HUD
        }
    ]
}
export QT_QPA_EGLFS_KMS_CONFIG=/etc/eglfs_kms_config.json
./dashboard

QML 侧通过 QScreen API 访问不同的物理输出:

Window {
    screen: Qt.application.screens[0]   // 仪表盘 → HDMI-1
}
Window {
    screen: Qt.application.screens[1]   // 中控   → HDMI-2
}

# 13.3.5 dmabuf 零拷贝

摄像头帧、视频解码帧要显示到 QML 画面里——如果从 V4L2 buffer → CPU memcpy → QImage → GPU 纹理,ARM 板 CPU 瞬间打满。EGLFS 支持 DMA-BUF 零拷贝:内核里共享 buffer,不经过 CPU:

传统路径(4 次拷贝):
  [V4L2 驱动 DMA → 内核 buffer]
    → ① copy_to_user → 用户空间 buffer(CPU 参与)
      → ② memcpy → QImage
        → ③ QSGTexture::fromImage → GPU 驱动层 buffer(CPU 参与)
          → ④ glTexImage2D → GPU 纹理
  总计: 1920×1080×4 × 4次 = 33 MB/frame → ARM A53 @1.2GHz 单核打满 90%

零拷贝路径(0 次 CPU 参与):
  [V4L2 驱动 DMA → dma_buf fd]
    → ① 导出 dma_buf fd 到用户空间(仅传文件描述符,4 bytes!)
      → ② eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT, fd) → EGLImage
        → ③ glEGLImageTargetTexture2DOES → GPU 纹理
  总计: 0 CPU 拷贝 → CPU 占用 < 3%

V4L2 摄像头帧到 EGLFS 的零拷贝代码骨架:

// 1. 从 V4L2 设备获取 DMA buffer
struct v4l2_exportbuffer expbuf = {0};
expbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
expbuf.index = bufIndex;
expbuf.plane = 0;
ioctl(v4l2_fd, VIDIOC_EXPBUF, &expbuf);
int dma_fd = expbuf.fd;  // ← 这是导出的 DMA-BUF 文件描述符

// 2. 用 DMA-BUF 创建 EGLImage(不拷贝数据!)
EGLint img_attrs[] = {
    EGL_WIDTH, 1920, EGL_HEIGHT, 1080,
    EGL_LINUX_DRM_FOURCC_EXT, DRM_FORMAT_YUYV,
    EGL_DMA_BUF_PLANE0_FD_EXT, dma_fd,
    EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0,
    EGL_DMA_BUF_PLANE0_PITCH_EXT, 3840,
    EGL_NONE
};
EGLImageKHR eglImage = eglCreateImageKHR(
    eglDisplay, EGL_NO_CONTEXT,
    EGL_LINUX_DMA_BUF_EXT, (EGLClientBuffer)nullptr, img_attrs);

// 3. 把 EGLImage 绑定为 GL 纹理
glBindTexture(GL_TEXTURE_2D, textureId);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage);
// → 现在 scene graph 可以直接用这个 textureId 渲染摄像头帧
// → 全程 0 CPU 拷贝,V4L2 → GPU 直通

零拷贝的先决条件:

条件 检查命令 不满足的后果
内核 DMA-BUF 支持 grep DMA_BUF /boot/config-$(uname -r) 无零拷贝——必须 memcpy
GPU 驱动支持 EGL_LINUX_DMA_BUF_EXT eglinfo | grep dma_buf EGLImage 创建失败
V4L2 驱动支持 VIDIOC_EXPBUF v4l2-ctl --list-formats dma_fd 导出失败

# 13.4 LinuxFB 后端

# 13.4.1 CPU 直写

无 GPU 的板子(STM32MP1、低端 Allwinner H 系列)仍能跑 QML——但所有渲染由 CPU 完成:

export QT_QPA_PLATFORM=linuxfb
export QT_QPA_FB_DRM=1            # 使用 DRM dumb buffer(替代直接写 /dev/fb0)
./myapp
LinuxFB 渲染路径:
  Scene Graph → 遍历 QSGNode 树
    → 每个节点:CPU 光栅化(QPainter 模拟 OpenGL)
      → 写入 /dev/fb0 的 mmap 映射内存
        → 帧率 = CPU 算力 / 分辨率 / 像素复杂度

示例:STM32MP1 @ 650MHz, 800×480 简单 UI:
  渲染一帧约 50-80ms → 约 12-20fps

# 13.4.2 性能与场景

场景 分辨率 FPS 可用性
按钮 + 文本 + 静态背景 800×480 20-25 ✅ 可用
按钮 + 文本 + ListView 800×480 15-20 ⚠️ 勉强
动画仪表盘 800×480 5-10 ❌ 不可用
全屏视频 — — ❌ 不可用

LinuxFB 的底线:512×320 以下低分屏 + 纯静态 UI,勉强可用;任何动画、视频、高频更新的场景,必须 GPU + EGLFS。


# 13.5 Wayland 后端

# 13.5.1 Wayland 协议

Wayland 是 X11 的现代继承者——嵌入式上最常用于需要多窗口的场景:

# 启动 Wayland compositor(嵌入式常用 Weston)
weston --backend=drm-backend.so --tty=1 &

# QML 应用——通过 Wayland 协议连接
export QT_QPA_PLATFORM=wayland
export WAYLAND_DISPLAY=wayland-0
./myapp

Qt Wayland Compositor——用 QML 自己做窗口管理器:

import QtWayland.Compositor

WaylandCompositor {
    id: compositor
    // 自定义窗口管理逻辑——完全用 QML 写
    WlShell { onWlShellSurfaceCreated: (shellSurface) => {
        var chrome = chromeComponent.createObject(defaultOutput.surfaceArea, {
            shellSurface: shellSurface
        })
    }}
}

# 13.5.2 多窗口场景

场景 推荐后端 原因
单屏车机仪表盘 EGLFS 无窗口开销
双屏(仪表 + 中控) EGLFS 多 output 同一进程、多 plan
多进程(仪表 + 地图 + 设置) Wayland 进程隔离
家电控制面板 EGLFS 单屏全屏
零售 POS 机 LinuxFB 廉价无 GPU

# 13.6 GPU 驱动适配

# 13.6.1 驱动对比

驱动 GPU 提供商 OpenGL ES DRM/KMS EGLFS 兼容
Mesa (etnaviv) Vivante (i.MX6) 开源 ✅ ⚠️ 有限 eglfs_kms
Mesa (vc4/v3d) Broadcom (RPi 3/4) 开源 ✅ ✅ eglfs_kms
Mesa (panfrost) ARM Mali (G31/G52) 开源 ✅ ✅ eglfs_kms
Vivante 闭源 Vivante (i.MX6/8) 闭源 ✅ ❌ eglfs_viv
Adreno 闭源 Qualcomm 闭源 ✅ ⚠️ eglfs_kms_egldevice
Mali 闭源 ARM Mali (旧) 闭源 ✅ ❌ eglfs_mali

# 13.6.2 DRM 对接

用户空间                    内核空间
┌────────────┐              ┌──────────────┐
│ QML App    │              │   DRM 驱动    │
│  └─ Scene  │              │  /dev/dri/   │
│     Graph  │              │   card0      │
│      ↓     │  ioctl()     │     ↓        │
│  libEGL.so │ ───────────→ │  GPU 命令队列 │
│      ↓     │              │     ↓        │
│  eglSwap   │ ── page flip→│  KMS (显示)   │
└────────────┘              └──────────────┘

DRM/KMS 的两个关键设备节点:

ls /dev/dri/
card0    ← GPU 渲染接口(ioctl 提交命令)
renderD128 ← 无头渲染(不需要显示输出)

DRM Atomic Modesetting——为什么"atomic"对 VSync 至关重要

传统的 DRM legacy API 分两步:先设 CRTC、再设 Plane——中间可能被 VBlank 打断,产生中间态。Atomic 提交是一次性的全有或全无操作:

Legacy DRM API(可能撕裂):
  ioctl(CRTC, SET_FB)     ← 改扫描源 → 屏幕下一帧从新地址开始读
  ... 
  ioctl(PLANE, SET_BUF)   ← 改图层缓冲 → 中间过了几毫秒
  → 问题: 两步之间有 VBlank → 屏幕读到"CRTC 已改、Plane 未改"的中间态→撕裂

Atomic DRM API(保证 VSync):
  struct drm_mode_atomic req;
  req.flags |= DRM_MODE_ATOMIC_NONBLOCK;
  req.flags |= DRM_MODE_ATOMIC_ALLOW_MODESET;
  // 原子提交——CRTC + Plane + Connector 一次打包
  drmModeAtomicCommit(fd, &req, flags, user_data);
  → 内核在下一个 VBlank 瞬间一次性应用全部变更
  → 没有中间态 → 不撕裂

Atomic 提交的 三阶段生命周期:

1. CHECK:  drmModeAtomicCommit(DRM_MODE_ATOMIC_TEST_ONLY)
   → 验证所有 CRTC/Plane/Connector 配置是否合法
   → 不实际应用——纯验证
   → 如果失败:回滚 QPA 配置、降级到 legacy 路径

2. COMMIT: drmModeAtomicCommit(flags)
   → 内核把 Page Flip 请求排队
   → 等下一个 VBlank → 硬件切换所有配置 → 完成
   → NONBLOCK 模式: 立即返回,通过 DRM event 异步通知完成

3. COMPLETE: DRM event fd 可读
   → 内核通知 "上一帧已完成显示"
   → 释放上一帧 framebuffer → 应用可以重新使用
   → Qt 的 EGLFS 在此阶段释放旧 buffer、调度下一帧渲染

Vivante 闭源驱动的死结:

Vivante 闭源驱动(i.MX6/8):
  ├── 实现: eglSwapBuffers → glFinish + 忙等
  ├── 缺失: DRM atomic modesetting
  └── 结果: QT_QPA_EGLFS_KMS_CONFIG=1 无效——内核没有 atomic_commit

开源 Mesa (etnaviv) 驱动:
  ├── 实现: eglSwapBuffers → drmModeAtomicCommit(NONBLOCK)
  ├── 完整: DRM atomic + page flip + event
  └── 结果: swap_time < 2ms, 60fps 稳定

# 13.6.3 驱动排查

# ① 检查 GPU 驱动是否加载
dmesg | grep -i drm
dmesg | grep -i vivante
lsmod | grep -E "vivante|etnaviv|panfrost|vc4"

# ② 检查 DRM/KMS 原子模式支持
cat /sys/kernel/debug/dri/0/state | head -20

# ③ 测试 EGL 是否正常工作
export QT_QPA_PLATFORM=eglfs
export QT_LOGGING_RULES="qt.qpa.eglfs*=true"   # 打印详细的 EGLFS 日志
./myapp 2>&1 | grep -i egl

# ④ Vivante 闭源驱动特殊设置
export QT_QPA_EGLFS_INTEGRATION=eglfs_viv
export QT_QPA_EGLFS_FORCEVSYNC=1               # 强制 VSync
export QT_QPA_EGLFS_FORCE888=1                 # 强制 24-bit 颜色

# 13.7 双屏车机

车机典型方案——仪表盘(EGLFS 单进程占主屏)+ 中控导航(独立 Wayland compositor):

#!/bin/sh
# 主驾仪表盘(EGLFS 独占 HDMI-1)
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_KMS_CONFIG=/etc/eglfs_kms.json
export QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event2
/dashboard &

# 副驾中控(Wayland——可以同时跑导航和音乐)
weston --backend=drm-backend.so --output=HDMI-2 &
export QT_QPA_PLATFORM=wayland
/navigation &
/music_player &
// /etc/eglfs_kms.json
{
    "device": "/dev/dri/card0",
    "outputs": [
        { "name": "HDMI-1", "mode": "1920x720@60" }
    ]
}

案例知识融合:本案例演示了车机双屏的标准架构——①仪表盘用 EGLFS 独占主屏(< 1s 启动、零窗口管理器开销);②中控用 Wayland 多进程(导航+音乐互不干扰);③两个屏幕各自独立的 QPA 后端,不互相影响。


# 13.8 新手陷阱

# 陷阱 说明 修复
1 -platform eglfs 画面撕裂 闭源驱动不支持 DRM atomic → swap 不等 VSync QT_QPA_EGLFS_FORCEVSYNC=1
2 EGLFS 启动报 Could not open egl display GPU 驱动未加载或权限不足 sudo chmod 666 /dev/dri/card0
3 触摸屏在 EGLFS 下无响应 evdev 匹配不到正确的 /dev/input/event* 手动指定:QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS=/dev/input/event2
4 LinuxFB 下 QT_QPA_FB_DRM=1 不生效 内核未开启 DRM dumb buffer 支持 检查 CONFIG_DRM_DUMB_BUFFER=y
5 Wayland 下 QML Window 无法全屏 没有设置 Window { visibility: FullScreen } QML 窗口级全屏声明 + compositor 配置

# 13.9 训练题

诊断 EGLFS 启动失败

ARM 板执行 ./myapp -platform eglfs 后输出:

Could not initialize egl display
Aborted (core dumped)

列出三条排查命令并解释原因。

参考答案
# ① 检查 GPU 节点权限
ls -la /dev/dri/card0
# 如果权限是 crw-rw---- → sudo chmod 666 /dev/dri/card0

# ② 检查 EGL 库是否存在
ldd ./myapp | grep libEGL
# 如果 not found → 部署缺失的 libEGL.so

# ③ 检查 GPU 驱动是否加载
dmesg | grep -i drm
# 如果无输出 → 内核未加载 GPU 驱动 → 检查 dtb / 内核配置

# 13.10 速查表

概念 一句话
QPA Qt 平台抽象层——隔离 X11/EGLFS/LinuxFB/Wayland
EGLFS EGL Full Screen——单进程直接接管屏幕(嵌入式首选)
eglfs_kms EGLFS 的 DRM/KMS 后端——启动 page flip + VSync
eglfs_viv Vivante 专用后端——需 FORCEVSYNC=1
LinuxFB CPU 直写 /dev/fb0——无 GPU 扳子降级方案
Wayland 现代显示协议——多窗口嵌入式场景
DRM/KMS Linux 内核的 GPU 显示框架——EGLFS 底层依赖
vsync 垂直同步——eglSwapBuffers 等待显示器刷新
evdev Linux 输入事件设备——EGLFS 直接读取 /dev/input/event*

核心哲学:

嵌入式 GUI 的渲染后端 = 谁把 framebuffer 交给显示器。

EGLFS = QML 进程自己通过 DRM/KMS 直接上屏——无中间商。
LinuxFB = CPU 写 /dev/fb0——最后一根稻草。
Wayland = 现代多窗口协议——进程隔离保安全。

选对后端 = 60fps。选错 = 撕裂或 12fps。

下一篇:14. 性能优化与真机调试 (opens new window)

上次更新: 2026/07/12, 19:39:51
交叉编译与部署
性能优化与真机调试

← 交叉编译与部署 性能优化与真机调试→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式