简介#
v9.5 讲的是覆盖面:模糊和阴影、全新重写的 Wayland 驱动、完整的 OpenGL 渲染器,以及在所有主流 Linux 显示栈上的 3D 支持。 v9.6 讲的是稳定性。
下面说说这具体意味着什么。
软件渲染器更快了,在我们实测的开发板上平均渲染时间最多下降 25%,你这边不需要改任何配置。公有头文件和私有头文件现在放在不同的目录里,
一眼就能看出哪些 API 可以放心依赖。整个配置由一棵 Kconfig 树生成,所以 lv_conf.h、Kconfig 和 -D 宏定义再也不会互相脱节。
LVGL 现在会自己拉取依赖,然后像任何一个系统库那样构建、安装和链接。在 Ubuntu 上你甚至可以直接 apt install!
每个公有函数都会检查自己的参数,而不是去解引用一个错误的指针。测试套件覆盖的库代码也比以往任何一个版本都多。
渲染性能#
更快的软件渲染器#
软件混合和变换路径被重写了。大多数项目会最先感受到这个改动,因为它对 LVGL 能跑的每一个目标平台都有效。不需要 GPU,也不需要 SIMD。
一共改了四处。在缓冲区条件允许的地方,混合器现在一次处理一个字(word),而不是一个像素。RGB565 混合把一个像素摊开到 32 位上, 这样一次乘法就能混合完整个像素。逐像素的混合辅助函数被移到了头文件里,改成宏和内联函数。旋转和缩放也按同样的思路做了重构。
基准测试#
我们在同一批开发板上分别用 v9.5.0 和 v9.6.0 运行了 lv_demo_benchmark, 所有测试都由软件渲染器完成绘制。下表是全场景平均值的变化。
| 开发板 | 渲染时间 | FPS | CPU 占用 |
|---|---|---|---|
| Renesas RA8D2 | −25% | +8% | −8 个百分点 |
| NXP i.MX RT700 | −22% | +10% | −3 个百分点 |
| NXP FRDM-MCXN947 | −20% | +5% | −4 个百分点 |
| Renesas RA6 | −18% | +8% | −7 个百分点 |
| Raspberry Pi RP2350 | −13% | +7% | −1 个百分点 |
| TI SK-AM62B-P1(1920×1080) | −8% | +5% | — |
| Espressif ESP32-S3-Korvo-1 | −4% | +6% | −1 个百分点 |
单个场景的提升幅度比平均值大得多。有四个方面特别突出:
- 带 Alpha 混合的图像提升最大。
Multiple ARGB images在 RA8D2 上的渲染开销降低了 76%,在 RT700 上降低了 60%。 - 变换紧随其后。一块 1024×600 的 Espressif ESP32-P4 渲染
Rotated ARGB images的耗时从 33 ms 降到了 11 ms,这个场景也从 27 FPS 提升到 60 FPS。 - 合成排在第三。带不透明度的容器在 RA8D2 上开销降低 41%,在 RT700 上降低 22%;带滚动的容器分别降低 38% 和 28%。
在 ESP32-S3-Korvo-1 上,
Containers with opa_layer每帧节省 14 ms,帧率提升 52%。 - 文本在整屏文本场景中,RA8D2 上开销降低 24%,RT700 上降低 27%。
欢迎在你自己的开发板上试一试,把结果分享给我们!
Arm SVE2 加速#
LVGL 的软件渲染器可以把混合循环交给目标平台上任何一种向量单元来做。v9.6 把部分 Armv9-A 内核上提供的 Arm SVE2 也加入了这个列表。 最先支持的是 RGB888 混合器。
把 LV_USE_DRAW_SW_ASM 设为 LV_DRAW_SW_ASM_SVE2 即可启用。
现在完整的加速后端包括 NEON(Armv7/Armv8)、Helium(Cortex-M55/M85)、RISC-V V 和 SVE2。
Wayland:DMA-BUF 后端#
在嵌入式 Linux 上,瓶颈往往不在软件渲染器,而在于把画好的一帧送到合成器。在 v9.6 之前,Wayland 驱动只有两条路:共享内存, 或者走 OpenGL,把渲染好的缓冲区上传成纹理,再每帧画一个全屏四边形。在 GPU 较弱的板子上,这次上传比绘制本身还要费时间。
v9.6 新增了 DMA-BUF 后端。LVGL 把画好的一帧拷贝进一块由 DMA-BUF 支撑的缓冲区,再通过 linux-dmabuf 把它附加到 surface 上。
没有纹理上传,没有全屏四边形,GPU 完全不参与。
下面是在 1280×720 的 Renesas RZ/G2L 上,相对纹理上传方案测得的效果:
| 指标 | 之前 | 之后 | 变化 |
|---|---|---|---|
| 帧率 | 32fps | 44fps | +38% |
| 刷新耗时 | 19ms | 12ms | 降低 37% |
| 渲染耗时 | 9ms | 9ms | 无变化 |
| CPU 占用 | 46% | 47% | 无变化 |
同样的绘制工作,现在送到屏幕上的次数差不多翻了一倍。原先受刷新限制的场景收益最大:Moving wallpaper 从 29 FPS 提升到 58 FPS,
Containers 从 35 提升到 58,Multiple labels 从 29 提升到 55。
驱动现在也会自己挑选后端。它按顺序逐个尝试,某个后端不可用时就回退到下一个,所以同一个二进制既能跑在支持 DMA-BUF 的板子上, 也能跑在不支持的板子上。
SiFli EPIC:初步集成#
v9.6 为 EPIC 新增了一个绘制单元,EPIC 是思澈科技(SiFli)SF32 系列中的 2D 图形加速器。它覆盖了填充、混合、图像、边框、标签和图层。 一个很薄的 OS 适配层让同一个绘制单元可以运行在这些芯片所配套的各种 RTOS 上。
/* lv_conf.h */
#define LV_USE_SIFLI_EPIC 1
#define LV_USE_SIFLI_EPIC_DRAW_THREAD 1 /* 在独立线程中派发 EPIC 任务 */
#define LV_USE_SIFLI_EPIC_ASSERT 0 /* 检查每一次 EPIC 调用的返回状态 */c这是第一个版本。v9.6 中还没有对应的文档页面,而且在 390×450 的 SF32 上,它还不能在每个场景上都追平 SiFli 自己维护的树外绘制单元。 我们先把它发布出来,并会在开发 v10 的过程中继续改进。
架构#
公有与私有头文件分离#
在 v9.5 之前,所有东西都放在 src/ 下,因此没有办法区分公有 API 和内部实现。这让每一次内部细节的重构都可能变成破坏性变更。
在 v9.6 中,公有 API 被移到了 include/lvgl/。src/ 从定义上就是私有的。
- lvgl.h公有总头文件
- lvgl.h集成 LVGL 时不想配置 include 目录,就用这个总头文件
确实需要用到某个内部类型?包含 lvgl_private.h,或者在配置中打开 LV_USE_PRIVATE_API。
从旧的 src/ 位置包含一个公有头文件仍然可以编译,只会给出一条 #warning。这些文件会在 v10.0 中移除。
配置体系#
LVGL 有三种配置方式:手写的 lv_conf.h、Kconfig,或者直接用编译器宏定义。这三者一直在互相脱节。v9.6 彻底解决了这个问题。
Kconfig 树现在是唯一的源头,一个内部脚本从它生成其余所有内容:lv_conf_template.h、负责应用默认值的内部头文件,
以及 Kconfig 路径上使用的 CONFIG_* 桥接层。
用手写的 lv_conf.h 配置 LVGL 是完整受支持的,对大多数项目来说仍然是默认方式。Kconfig 作为唯一事实来源只是一个内部改动,
lv_conf_template.h 现在由它生成,但你还是像以前一样复制并编辑它。
这次重构意味着一些符号被废弃了,所以为 v9.5 写的 lv_conf.h 可能无法在 v9.6 上编译。每一个被废弃的选项都会在编译期产生警告或错误,
并明确指出要改成什么。
迁移指南里有完整的清单。
构建与集成#
LVGL 依赖管理#
在 v9.6 之前,要让 LVGL 用上 SDL2、FreeType 或 GStreamer,你得先自己把它们装好,再把构建系统指向它们。
现在 LVGL 会按顺序解析每一个依赖:先 find_package,然后 pkg-config,最后从源码拉取并构建。拉取默认是开启的,
所以一个全新的克隆,在系统上一个软件包都没装的情况下也能完成配置和构建。
git clone https://github.com/lvgl/lvgl && cd lvgl
cmake -B build -DLV_BUILD_USE_KCONFIG=ON \
-DLV_BUILD_DEFCONFIG_PATH=configs/defconfigs/sdl2.defconfig
cmake --build build # 系统上没有 SDL2 的话,会自动拉取并构建bash覆盖的依赖有二十个左右,其中包括 SDL2、FreeType、GStreamer、libdrm、GBM、Wayland、X11、libjpeg、libpng、WebP、fastgltf、GLFW 和 FFmpeg。
-DLV_FETCH_DEPENDENCIES=OFF 会回到只用系统库的模式,缺少任何一个库时配置阶段都会直接报错。
还有 LV_FETCH_SDL2、LV_USE_PKG_CONFIG_FREETYPE 这类针对单个依赖的开关,可以把某一个依赖固定成任意一种方式。
Defconfig 预设#
LVGL 现在在 configs/defconfigs/ 中提供了现成的预设:sdl2、wayland、drm,每个都有对应的 -3d 变体,另外还有 minimal 和 empty。
直接用其中一个来构建:
cmake -B build -DLV_BUILD_USE_KCONFIG=ON \
-DLV_BUILD_DEFCONFIG_PATH=configs/defconfigs/sdl2.defconfig
cmake --build buildbashLV_BUILD_DEFCONFIG_PATH 还接受一个用 ; 分隔的列表,从左往右合并,后面的片段会覆盖前面的。这样一组相关的配置就可以共用一个基础配置,
而不必各自重复一遍:
cmake -B build -DLV_BUILD_USE_KCONFIG=ON \
"-DLV_BUILD_DEFCONFIG_PATH=configs/defconfigs/sdl2.defconfig;my_overrides.defconfig"bash合并后的结果会写到 build/.config,你可以手工编辑它,也可以用 menuconfig 来改。
安装并链接 LVGL#
在 Linux 上构建 LVGL 可能会牵扯进 Wayland、EGL、FreeType、GStreamer、libdrm 等等。以前想在自己的构建里把这串库复现出来相当麻烦。
现在安装好的 LVGL 会把它们记录下来:给 pkg-config 用的 lvgl.pc,以及给 find_package 用的 lvglConfig.cmake,
两者都列出了 LVGL 实际链接过的每一个库。
gcc main.c -o main $(pkg-config --cflags --static --libs lvgl)bash这两篇文章讲得更深入:
Ubuntu 集成#
LVGL 现在通过 Launchpad PPA 提供预编译的 Ubuntu 软件包。 不用克隆,不用 CMake,也不用到处找依赖:
sudo add-apt-repository ppa:lvgl/lvgl
sudo apt update
sudo apt install liblvgl-sdl2-devbash各个软件包的差别只在于启用了哪些显示后端,以及是否包含 3D;其余功能集在所有包里都是一样的。
| 后端 | 2D | 3D |
|---|---|---|
| SDL2 | liblvgl-sdl2-dev | liblvgl-sdl2-3d-dev |
| Wayland | liblvgl-wayland-dev | liblvgl-wayland-3d-dev |
| DRM | liblvgl-drm-dev | liblvgl-drm-3d-dev |
| SDL2/Wayland/DRM | liblvgl-full-2d-dev | liblvgl-full-3d-dev |
每个软件包都会把自己的 lvgl.pc 和 CMake 配置文件安装到标准系统路径下,所以 pkg-config 和 find_package(lvgl) 不需要任何额外设置就能用。
完整的步骤可以看这篇文章。
参数检查#
这是 v9.6 中影响面最广的改动:168 个源文件里大约 2900 处检查,基本上把公有 API 完整覆盖了一遍。
它们要解决的问题是这样的:在 v9.5 中,你传进一个错误的指针会发生什么,取决于你调用的是哪个函数。有的会断言并停机, 有的会打一条含糊的日志,大多数会直接解引用它。
LV_CHECK_ARG 用一种统一的行为替代了上面三种。它接受一个条件,以及条件不满足时要执行的动作,通常是一条 return。
它会记录出了什么问题,同时让调用方继续往下走:
uint32_t lv_event_get_key(lv_event_t * e)
{
LV_CHECK_ARG(e != NULL, return 0);
LV_CHECK_ARG_FORMAT_MSG(e->code == LV_EVENT_KEY, return 0,
"invalid event code %" LV_PRId32, (int32_t)e->code);
uint32_t * k = lv_event_get_param(e);
return k ? *k : 0;
}c对比一下 v9.5 中的同一个函数:它在做任何检查之前就解引用了 e,而且事件码不对时,只告诉你这个事件"没有被解释":
if(e->code == LV_EVENT_KEY) {
uint32_t * k = lv_event_get_param(e);
if(k) return *k;
else return 0;
}
else {
LV_LOG_WARN("Not interpreted with this event code");
return 0;
}cLV_CHECK_ARG(e != NULL, return 0);
LV_CHECK_ARG_FORMAT_MSG(e->code == LV_EVENT_KEY, return 0,
"invalid event code %" LV_PRId32,
(int32_t)e->code);
uint32_t * k = lv_event_get_param(e);
return k ? *k : 0;c一共有三种形式:LV_CHECK_ARG(cond, action)、LV_CHECK_ARG_MSG(cond, action, msg) 和
LV_CHECK_ARG_FORMAT_MSG(cond, action, fmt, ...)。条件本身会被字符串化写进日志,所以光是一个裸的 LV_CHECK_ARG 就已经能告诉你是哪里失败了。
只有当具体的值也值得打印时,才需要再加一条消息。
LV_USE_CHECK_ARG 默认开启。除非你主动关掉它,否则每个公有函数都会对参数做 NULL 检查,出错时干净地返回,而不是直接崩掉。
日志则不是默认开启的。LV_CHECK_ARG_LOG_MODE 的初始值是 LV_CHECK_ARG_LOG_MODE_NONE,所以检查失败时会静默返回。开发阶段可以把它调高:
/* lv_conf.h */
#define LV_CHECK_ARG_LOG_MODE LV_CHECK_ARG_LOG_MODE_VERBOSE /* 条件 + 消息 + 文件/行号 */
#define LV_CHECK_ARG_ASSERT_ON_FAIL 1 /* 可选:失败时停机,行为和过去的断言一样 */c设置 LV_USE_CHECK_ARG 0 之后,每一处检查都会被编译成空,此时传入非法参数属于未定义行为。
我们建议始终保持它开启,生产环境也一样。
控件检查#
LV_CHECK_OBJ 是面向对象的版本,也是 LV_ASSERT_OBJ 的直接替代:
LV_ASSERT_OBJ(obj, &lv_label_class);cLV_CHECK_OBJ(obj, &lv_label_class, return);c它是分层的。NULL 检查随 LV_USE_CHECK_ARG 一起提供。LV_USE_CHECK_OBJ_CLASSTYPE 会额外检查这个控件确实属于你声称的那个类。
LV_USE_CHECK_OBJ_VALIDITY 会额外检查它仍然在活的控件树里,这能抓到删除后继续使用的问题。后两者默认关闭,
因为它们在每一个调用点都要遍历类层次结构和控件树。开发时打开它们,部署时关掉。
断言本身现在也默认关闭了,由 LV_USE_ASSERT 控制。
测试覆盖率#
参数检查抓的是你的错误,测试抓的是我们的错误。在 v9.5.0 到 v9.6.0 之间的七个月、700 多次提交里, 测试套件实际跑到的库代码比例在我们跟踪的每一项指标上都上升了:
| v9.5.0 | v9.6.0 | ||
|---|---|---|---|
| 行覆盖率 | 78.66% | 82.23% | +3.57 个百分点 |
| 已覆盖行数 | 46,756 | 56,021 | +9,265 |
| 未覆盖行数 | 12,686 | 12,108 | −578 |
| 分支覆盖率 | 61.40% | 62.59% | +1.19 个百分点 |
| 函数覆盖率 | 82.1% | 83.7% | +1.6 个百分点 |
这个周期里被统计的代码量增加了 8,687 行可覆盖代码,而没有任何测试碰到的行数仍然下降了 578 行。
新增代码的覆盖率大约是 81%,高于项目起点的 78.7%,所以平均值是被拉高而不是被稀释。
有几个模块整体从零开始起步。glTF、NanoVG、GStreamer、条形码和二维码在 v9.5.0 中的覆盖率全都是 0%,因为没有任何一个测试配置会编译它们。 现在它们分别是 85%、87%、71%、91% 和 96%。一部分来自新增的测试用例,一部分来自新增的测试配置,正是这些配置让这些测试跑得起来。
现在每一个 pull request 都会测量覆盖率,并把结果作为评论贴出来。如果一个 PR 新增的代码被测试覆盖的比例低于 50%,它无法合并。
新特性#
文本行距裁剪#
字体自带行距(leading),也就是字形上下那部分属于行框、而不属于字母本身的垂直空间。正因为如此,用 LV_SIZE_CONTENT 设定尺寸的标签
看上去会比里面的文字高,包着标签的按钮在视觉上也很少是真正居中的。
text_leading_trim 会按你选定的度量来裁掉这部分空间:大写字母高度、x 高度,或者基线。

因为裁剪改变了文本框的大小,它也会改变布局:一个 LV_SIZE_CONTENT 的按钮会紧贴自己的标签,而不是把字体那部分虚高的行距也继承过来。
可变字体字重#
带 wght 轴的 FreeType 字体,现在可以在该轴上取任意字重来实例化,而不再局限于常规体和粗体这两个切片。

字重在打开字体时就被固定下来,并且是字体缓存键的一部分,所以请提前把需要的字重定好,不要每一帧都去创建一个字体。
一个 glTF 模型,多个查看器#
glTF 模型现在可以脱离任何查看器单独加载,然后交给任意多个 lv_gltf 控件使用。解析、纹理、网格和着色器只需要付出一次代价;
每个查看器只额外贡献自己的相机。

lv_gltf_remove_model() 和 lv_gltf_remove_all_models() 可以把模型从查看器上摘下来而不销毁它;删除一个模型时,
它会从所有正在显示它的查看器上自动摘除。动画方面也新增了对播放位置的直接控制:
lv_gltf_model_set_animation_time() 和 lv_gltf_model_set_animation_ratio()。
v9.6 中的其他更新#
驱动
- Wayland:键盘输入改为带缓冲,按键不会再丢失;输出设备可以按接口名识别;新增读取全屏和最大化状态的 API。
- DRM:设备探测新增 libdrm 回退路径。
- fbdev:支持 L8。
- SDL:软件后端支持运行时切换颜色格式。
渲染
- NanoVG:GPU 上的模糊和阴影,以及图像重着色。
- VG-Lite:图像的矩阵变换,以及预分配的轮廓路径内存。
- NemaGFX:缓存管理、变换后的图像按真实绘制区域裁剪,以及一个让矩阵路径不再走双精度软浮点的修复。
- DMA2D:缓存处理改为基于
lv_draw_buf,在 Zephyr 上支持 ASYNC/INTERRUPT,缓冲区未对齐时会给出警告。 - EGL:支持预乘的 ARGB8888。
控件与核心
- 下拉框和滚轮新增翻译支持。
lv_label_set_max_lines()可以把标签限制在指定行数内。LV_IMAGE_ALIGN_CONTAIN_DOWNSCALE会把图像缩小到能放下为止,但绝不放大。- 二进制字体支持动态加载字形。
- 输入设备新增独立的双击时间设置。
- 观察者模块新增
lv_subject_create()、按类型区分的lv_obj_bind_<type>()绑定函数,以及观察者用户数据。 - 中国农历新增二十四节气。
- GStreamer 集成了 WebRTC 插件。
升级#
v9.6 中的大部分改动都不会让你的构建失败。有些配置项和 API 被改了名字,但每一个旧名字仍然可以编译:它会给出一条 #warning,
行为和过去完全一致,并在 v10.0 中消失。
所以升级路径很短。构建,读警告,按警告指的地方改。
有三件事值得在这里单独讲一讲,因为大多数项目都会碰到它们。
1. 用 LV_COLOR_FORMAT_DEFAULT 取代 LV_COLOR_DEPTH#
直接指明格式,而不是去数它有多少位。LV_COLOR_DEPTH 仍然有定义,你的代码里也仍然可以读它,但它现在是从格式推导出来的,
而不再是由你设置的东西。
2. 用 LV_ASSERT_CUSTOM_INCLUDE 取代 LV_ASSERT_HANDLER_INCLUDE#
有些配置值是类函数宏:自定义的 LV_ASSERT_HANDLER、LV_FONT_CUSTOM_DECLARE,以及 LV_ATTRIBUTE_* 这些钩子。
Kconfig 只能存布尔值、整数和字符串,装不下它们中的任何一个。所以从现在起,这种用法改为由你自己提供一个头文件,再由 LVGL 包含进去。
每个需要它的模块现在都用同一对选项:LV_<MODULE>_USE_CUSTOM_INCLUDE 负责打开开关,LV_<MODULE>_CUSTOM_INCLUDE 负责指向文件;
这套模式适用于 FONT、ASSERT、ATTRIBUTE、SYSMON 和 NEMA。
这个约定也顺带给早于它存在的那个选项改了名:
#define LV_ASSERT_HANDLER_INCLUDE "my_assert.h"c#define LV_ASSERT_USE_CUSTOM_INCLUDE 1
#define LV_ASSERT_CUSTOM_INCLUDE "my_assert.h"c3. 五个控件被废弃#
lv_file_explorer、lv_menu、lv_list 和 lv_win 在 v9.6 中被废弃,rlottie 绑定也一样。它们各自都只是对基础控件已有能力的一层薄封装,
所以每一个都由一个示例来代替,你可以把示例复制过去,完全掌握在自己手里:
| 已废弃 | 替代方案 |
|---|---|
lv_list | 一个 flex 列布局示例 |
lv_win | 一个 flex 列布局示例 |
lv_menu | 一个基于基础控件的导航示例 |
lv_file_explorer | 一个基于表格的文件浏览器示例 |
rlottie | lv_lottie 控件 |
它们在 v9.6 中都仍然可以编译,也都会在 v10.0 中消失。如果你正在用其中某一个,这就是该迁移的版本。
有少数几处是直接编译失败,而不是给警告。lv_array、lv_tree、SVG 结构体和事件列表结构体都被移到了私有 API,
lv_drm 的公有头文件也不再暴露 xf86drmMode.h。如果你用到了其中任何一个,请包含 lvgl_private.h 或者设置 LV_USE_PRIVATE_API。
这些,以及上面没有提到的每一处改名,都在迁移指南里列出了改动前后的代码。
总结#
v9.6 为 v9 系列画上了句号。它的特性清单不是最长的,但我们觉得它也不需要是。相反,它让软件渲染器在所有以绘制为瓶颈的场景下实测更快; 它在你可以使用的 API 和你不该碰的内部实现之间划出了一条真正的界线;它给配置体系确立了唯一的事实来源; 它让 LVGL 变成一个可以像任何其他库那样安装和链接的东西。它还把 82% 的代码纳入了测试,并用 CI 守住这个数字。
升级,读编译器警告,按警告指的地方改。它们警告的每一件事都会在 v10.0 中消失。
和往常一样,完整的变更日志在 GitHub 上,逐条列出了每一次提交。如果遇到问题,论坛和 GitHub issues 是联系我们团队最好的地方。
感谢每一位贡献者,包括提出问题的人、参与评审的人和写代码的人。
