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