Introduction#
v9.5 was about reach: blur and drop shadow, a rewritten Wayland driver, a full OpenGL renderer, 3D on every major Linux display stack. v9.6 is about stability.
The software renderer got faster, up to 25% off the average render time on the boards we measured, with no configuration change on
your side. Public and private headers now live in separate directories, so you can see at a glance which API is safe to depend on. One
Kconfig tree generates every config file, so lv_conf.h, Kconfig and -D defines can no longer drift apart. LVGL now fetches its own
dependencies, then builds, installs and links like any other system library. On Ubuntu you can even apt install it! Every public
function checks its arguments instead of dereferencing a bad pointer. And the test suite covers more of the library than in any release
before it.
Rendering Performance#
Faster Software Renderer#
The software blend and transform paths were rewritten. Most projects will feel this change first, because it applies to every target LVGL runs on. No GPU nor SIMD support needed.
The blenders now process a word at a time instead of a pixel at a time, wherever the buffers allow it and rotation and scaling were reworked the same way.
Benchmarks#
We ran lv_demo_benchmark on the same boards under v9.5.0 and v9.6.0, with the software renderer doing the work in every case. The numbers below are the change in the all-scenes average.
| Board | Render time | FPS | CPU load |
|---|---|---|---|
| Renesas RA8D2 | −25% | +8% | −8 pts |
| NXP i.MX RT700 | −22% | +10% | −3 pts |
| NXP FRDM-MCXN947 | −20% | +5% | −4 pts |
| Renesas RA6 | −18% | +8% | −7 pts |
| Raspberry Pi RP2350 | −13% | +7% | −1 pt |
| TI SK-AM62B-P1 (1920×1080) | −8% | +5% | — |
| Espressif ESP32-S3-Korvo-1 | −4% | +6% | −1 pt |
Individual scenes move much further than the average:
- Alpha-blended images improve the most. The
Multiple ARGB imagesscene renders 76% faster on the RA8D2 and 60% on the RT700. - Transforms come next. An Espressif ESP32-P4 at 1024×600 renders
Rotated ARGB imagesin 11 ms instead of 33, taking the scene from 27 FPS to 60. - Compositing follows. Containers with opacity cost 41% less on the RA8D2 and 22% less on the RT700; containers with
scrolling, 38% and 28%. On an ESP32-S3-Korvo-1,
Containers with opa_layersaves 14 ms a frame and gains 52% frame rate. - Text costs 24% less on the RA8D2 and 27% less on the RT700 in the screen-sized-text scene.
Don't hesitate to try it out on your boards and share your results with us!
Arm SVE2 Acceleration#
LVGL's software renderer can hand its blend loops to whatever vector unit the target has. v9.6 adds Arm SVE2, available on some Armv9-A cores, to that list. The first paths covered are the RGB888 blenders.
Enable it by setting LV_USE_DRAW_SW_ASM to LV_DRAW_SW_ASM_SVE2.
The full set of accelerated backends is now NEON (Armv7/Armv8), Helium (Cortex-M55/M85), RISC-V V, and SVE2.
Wayland: A DMA-BUF Backend#
On embedded Linux the software renderer is often not the bottleneck. Getting the finished frame to the compositor is. Until v9.6 the Wayland driver had two ways to do that: shared memory, or an OpenGL path that uploaded the rendered buffer into a texture and drew a full-screen quad with it every frame. On a board with a weak GPU, that upload costs more than the drawing itself.
v9.6 adds a DMA-BUF backend. LVGL copies the finished frame into a DMA-BUF-backed buffer and attaches that to the surface through
linux-dmabuf. No texture upload, no full-screen quad, no GPU involved.
Here is what that buys on a Renesas RZ/G2L at 1280×720, measured against the texture-upload path:
| Metric | Before | After | Change |
|---|---|---|---|
| Frame rate | 32fps | 44fps | +38% |
| Flush time | 19ms | 12ms | 37% less |
| Render time | 9ms | 9ms | unchanged |
| CPU load | 46% | 47% | unchanged |
The same drawing work now reaches the screen roughly twice as often. Scenes that were flush-bound gain the most: Moving wallpaper goes from 29 to 58 FPS,
Containers from 35 to 58, Multiple labels from 29 to 55.
The driver also picks its own backend now. It tries them in order and falls back when one is unavailable, so the same binary runs on a board with DMA-BUF and on one without.
SiFli EPIC: An Initial Integration#
v9.6 adds a draw unit for EPIC, the 2D graphics accelerator in SiFli's SF32 series. It covers fills, blends, images, borders, labels and layers. A small OS adaptation layer lets the same draw unit run across the RTOSes those parts ship with.
/* lv_conf.h */
#define LV_USE_SIFLI_EPIC 1
#define LV_USE_SIFLI_EPIC_DRAW_THREAD 1 /* dispatch EPIC work from its own thread */
#define LV_USE_SIFLI_EPIC_ASSERT 0 /* check the status of every EPIC call */cIt has no documentation page in v9.6, and on an SF32 at 390×450 it does not yet match SiFli's own out-of-tree draw unit on every scene. We are shipping it now and we'll make it better as we work on v10.
Architecture#
Public and Private Headers Separation#
Until v9.5 everything lived under src/, so there was no way to tell the public API from the internals. That made every refactor of an
internal detail a potential breaking change.
In v9.6 the public API moved to include/lvgl/. src/ is private by definition.
- lvgl.hthe public umbrella header
- lvgl.humbrella header when integrating LVGL without setting up an include directory
Need an internal type anyway? Include lvgl_private.h, or enable LV_USE_PRIVATE_API in your configuration.
Including a public header from its old src/ location still compiles, with a #warning. Those files are removed in v10.0.
Configuration System#
LVGL can be configured three ways: a hand-written lv_conf.h, Kconfig, or plain compiler defines. All three kept drifting apart. v9.6
fixes that for good.
The Kconfig tree is now the source, and an internal script generates everything else from it: lv_conf_template.h, the
internal header that applies defaults, and the CONFIG_* bridge used on the Kconfig path.
Configuring LVGL with a hand-written lv_conf.h is fully supported and remains the default for most projects. Kconfig being the source
of truth is an internal change, lv_conf_template.h is generated from it now, but you copy and edit it exactly as before.
The rework meant deprecating some symbols, so an lv_conf.h written for v9.5 may not compile against v9.6. Every deprecated option
produces a compile-time warning or error naming exactly what to change.
Check out our migration guide for the full list.
Build & Integration#
LVGL Dependency Management#
Before v9.6, building LVGL against SDL2, FreeType or GStreamer meant installing those yourself first and pointing the build at them.
Now LVGL resolves each dependency in order: find_package first, then pkg-config, then fetching and building it from source.
Fetching is on by default, so a fresh clone configures and builds with no system packages installed at all.
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 is fetched and built if the system does not have itbashAround twenty dependencies are covered, SDL2, FreeType, GStreamer, libdrm, GBM, Wayland, X11, libjpeg, libpng, WebP, fastgltf, GLFW and FFmpeg among them.
-DLV_FETCH_DEPENDENCIES=OFF goes back to system libraries only, and configuration fails loudly if one is missing. Per-dependency
switches like LV_FETCH_SDL2, LV_USE_PKG_CONFIG_FREETYPE let you tweak how each dependency gets brought into the build.
Defconfig Presets#
LVGL now ships ready-made presets for sdl2, wayland, drm, each with a -3d variant, plus minimal and empty in configs/defconfigs/.
Build straight from one:
cmake -B build -DLV_BUILD_USE_KCONFIG=ON \
-DLV_BUILD_DEFCONFIG_PATH=configs/defconfigs/sdl2.defconfig
cmake --build buildbashLV_BUILD_DEFCONFIG_PATH additionally takes a ;-separated list, merged left to right, so a later fragment overrides an earlier one. A family of
related configurations can share one base instead of each repeating it:
cmake -B build -DLV_BUILD_USE_KCONFIG=ON \
"-DLV_BUILD_DEFCONFIG_PATH=configs/defconfigs/sdl2.defconfig;my_overrides.defconfig"bashThe merged result lands in build/.config, which you can then edit by hand or with menuconfig.
Installing and Linking Against LVGL#
An LVGL build on Linux can pull in Wayland, EGL, FreeType, GStreamer, libdrm and more. Reproducing that list in your own build used to be
a pain. Now an installed LVGL writes it down: lvgl.pc for pkg-config and lvglConfig.cmake for find_package, each naming every
library LVGL was actually compiled against.
gcc main.c -o main $(pkg-config --cflags --static --libs lvgl)bashThese two posts go deeper:
Ubuntu Integration#
LVGL now publishes prebuilt Ubuntu packages through a Launchpad PPA. No clone, no CMake, no dependency hunt:
sudo add-apt-repository ppa:lvgl/lvgl
sudo apt update
sudo apt install liblvgl-sdl2-devbashPackages differ only in which display backends they enable and whether 3D is included; the rest of the feature set is the same across all of them.
| Backends | 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 |
Every package installs its lvgl.pc and CMake config into the standard system paths, so pkg-config and find_package(lvgl) work with no
extra setup. Check out this blog post for the walkthrough.
Argument Checking#
This is the widest-reaching change in v9.6: roughly 2,900 checks across 168 source files, covering the public API more or less exhaustively.
Here is the problem they solve: In v9.5, what happened when you passed a bad pointer depended on which function you called. Some asserted and halted. Some logged something vague. Most dereferenced it.
LV_CHECK_ARG replaces all three with one behavior. It takes a condition and an action to run when that condition fails, usually a
return. It logs what went wrong and lets the caller carry on:
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;
}cCompare that to the same function in v9.5, which dereferenced e before checking anything and, when the event code was wrong, told you
only that it was "not interpreted":
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;cThere are three forms:
LV_CHECK_ARG(cond, action)LV_CHECK_ARG_MSG(cond, action, msg)LV_CHECK_ARG_FORMAT_MSG(cond, action, fmt, ...)
The condition is stringified into the log, so a bare LV_CHECK_ARG already tells you
what failed. Add a message only when the value itself is worth printing.
LV_USE_CHECK_ARG is enabled by default. Unless you turn it off, every public function NULL-guards its arguments and returns
cleanly instead of faulting.
The logging is not on by default. LV_CHECK_ARG_LOG_MODE starts at LV_CHECK_ARG_LOG_MODE_NONE, so a failed check returns silently.
Turn it up while developing:
/* lv_conf.h */
#define LV_CHECK_ARG_LOG_MODE LV_CHECK_ARG_LOG_MODE_VERBOSE /* condition + message + file/line */
#define LV_CHECK_ARG_ASSERT_ON_FAIL 1 /* optional: halt on failure, like the old asserts */cWith LV_USE_CHECK_ARG 0 every check compiles to nothing so passing an invalid argument
is undefined behavior. We recommend always keeping it on, even in production.
Widget Checks#
LV_CHECK_OBJ is the object-flavored version, and the direct replacement for LV_ASSERT_OBJ:
LV_ASSERT_OBJ(obj, &lv_label_class);cLV_CHECK_OBJ(obj, &lv_label_class, return);cIt is layered. By default it will check if the pointer is non-NULL and it's enabled with LV_USE_CHECK_ARG.
LV_USE_CHECK_OBJ_CLASSTYPEadds a check that the widget is the class you claimed. (e.g. is the widget you passed to lv_label_set_text and actuallabel?)LV_USE_CHECK_OBJ_VALIDITYadds a check that it is still in the live widget tree, which helps catching use-after-delete.
The two default to off, because they walk the class hierarchy and the widget tree on every call site. Turn them on while developing, and off when deploying.
Assertions themselves are now off by default too, behind LV_USE_ASSERT.
Test Coverage#
Argument checks catch your mistakes. Tests catch ours. Across the seven months and the more than 700 commits between v9.5.0 and v9.6.0, the share of the library the test suite actually exercises rose on every metric we track:
| v9.5.0 | v9.6.0 | ||
|---|---|---|---|
| Line coverage | 78.66% | 82.23% | +3.57 pp |
| Covered lines | 46,756 | 56,021 | +9,265 |
| Uncovered lines | 12,686 | 12,108 | −578 |
| Branch coverage | 61.40% | 62.59% | +1.19 pp |
| Function coverage | 82.1% | 83.7% | +1.6 pp |
The measured codebase grew by 8,687 coverable lines this cycle, and the number of lines no test touches still went down by 578.
New code landed at roughly 81% covered, above the 78.7% the project started at, which is why the average rose instead of being diluted.
Whole modules moved off zero. glTF, NanoVG, GStreamer, barcode and QR code all sat at 0% in v9.5.0, because no test configuration compiled them. They now sit at 85%, 87%, 71%, 91% and 96%. Part of that is new test cases; part is some new test configurations that make it possible to run these tests.
Coverage is now measured on every pull request and posted as a comment. A PR that adds code covered less than 50% by tests does not merge.
New Features#
Text Leading Trim#
Fonts carry leading, vertical space above and below the glyphs that belongs to the line box, not to the letters. It is why a label sized
with LV_SIZE_CONTENT looks taller than the text inside it, and why a button wrapping a label rarely looks optically centered.
text_leading_trim trims that space against a chosen metric: cap height, x-height, or the baseline.

Because the trim changes the text box, it changes layout: an LV_SIZE_CONTENT button hugs its label instead of inheriting the font's
phantom leading.
Variable Font Weights#
FreeType fonts with a wght axis can now be instantiated at any weight on that axis, not just the regular and bold cuts.

The weight is baked in when the face is opened and is part of the font's cache key, so pick your weights up front rather than creating a font per frame.
One glTF Model, Many Viewers#
A glTF model can now be loaded independently of any viewer and handed to as many lv_gltf widgets as you like. Parsing, textures, meshes
and shaders are paid for once; each viewer contributes only its own camera.

lv_gltf_remove_model() and lv_gltf_remove_all_models() detach a model without destroying it, and deleting a model detaches it from
every viewer showing it. Animations also gained direct control over playback position with
lv_gltf_model_set_animation_time() and lv_gltf_model_set_animation_ratio().
Also in v9.6#
Drivers
- Wayland: buffered keyboard input so key presses are not dropped, outputs identified by connector name, and an API to read fullscreen and maximized state.
- DRM: a libdrm fallback for device probing.
- fbdev: L8 support.
- SDL: runtime color format changes for the software backend.
Rendering
- NanoVG: blur and drop shadow on the GPU, plus image recolor.
- VG-Lite: matrix transformation on images, and pre-allocated outline path memory.
- NemaGFX: cache management, transformed images clipped against the real draw area, and a fix that kept the matrix path off double-precision soft-float.
- DMA2D: cache handling reworked onto
lv_draw_buf, ASYNC/INTERRUPT support on Zephyr, and a warning when buffers are misaligned. - EGL: premultiplied ARGB8888 support.
Widgets and core
- Dropdown and roller gained translation support.
lv_label_set_max_lines()caps a label at a line count.LV_IMAGE_ALIGN_CONTAIN_DOWNSCALEscales an image down to fit without ever scaling it up.- Binary fonts support dynamic glyph loading.
- Input devices gained a dedicated double-click time.
- The observer module gained
lv_subject_create(), per-typelv_obj_bind_<type>()binders, and observer user data. - The Chinese calendar gained the 24 solar terms.
- GStreamer integrates the WebRTC plugin.
Upgrading#
Most of what changed in v9.6 will not stop your build. Options and APIs were renamed, and every old name still compiles: it emits a
#warning, does exactly what it always did, and disappears in v10.0.
So the upgrade path is short. Build, read the warnings, fix what they point at.
Three things are worth walking through here, because they are what most projects will hit.
LV_COLOR_DEPTHbecomesLV_COLOR_FORMAT_DEFAULT
Name the format instead of counting its bits. LV_COLOR_DEPTH is still defined and still readable from your own code but it is now derived
from the format, rather than something you set.
LV_ASSERT_HANDLER_INCLUDEbecomesLV_ASSERT_CUSTOM_INCLUDE
Some configuration values are function-like macros: a custom LV_ASSERT_HANDLER, LV_FONT_CUSTOM_DECLARE, the LV_ATTRIBUTE_* hooks.
Kconfig stores booleans, integers and strings, so it cannot hold any of them. So moving forward this pattern is accomplished by a header
of your own that LVGL includes. Every module that needs one now takes the same pair of options: LV_<MODULE>_USE_CUSTOM_INCLUDE to switch it on,
LV_<MODULE>_CUSTOM_INCLUDE to point at the file, this pattern exists for FONT, ASSERT, ATTRIBUTE, SYSMON and NEMA.
That convention renamed the one option which predated it:
#define LV_ASSERT_HANDLER_INCLUDE "my_assert.h"c#define LV_ASSERT_USE_CUSTOM_INCLUDE 1
#define LV_ASSERT_CUSTOM_INCLUDE "my_assert.h"cFive Widgets Are Deprecated#
lv_file_explorer, lv_menu, lv_list and lv_win are deprecated in v9.6, along with the rlottie binding. Each was a thin wrapper
over things the base widgets already do, so each is replaced by an example you can copy and own instead:
| Deprecated | Replacement |
|---|---|
lv_list | a flex-column example |
lv_win | a flex-column example |
lv_menu | a base-widget navigation example |
lv_file_explorer | a table-based file browser example |
rlottie | the lv_lottie widget |
They all still compile in v9.6 and all disappear in v10.0. If you use one, this is the release to move off it.
A handful of things fail to compile rather than warn. lv_array, lv_tree, the SVG structures and the event list structure all moved
to the private API, and lv_drm no longer exposes xf86drmMode.h from its public header. If you use any of them, include
lvgl_private.h or set LV_USE_PRIVATE_API.
Those, and every rename not covered above, are listed with before/after code in the migration guide.
Wrapping Up#
v9.6 closes the v9 series. It does not have the longest feature list, but we didn't feel like it had to. Instead it makes the software renderer measurably faster wherever drawing is the bottleneck. It draws a real line between the API you can use and the internals you cannot. It gives configuration a single source of truth. It makes LVGL something you install and link like any other library. And it puts 82% of the codebase under test, with CI to keep it there.
Upgrade, read the compiler warnings, fix what they point at. Everything they warn about is gone in v10.0.
As always, the full changelog is on GitHub with every commit listed. If you run into issues, the forum and GitHub issues are the best places to reach the team.
Thanks to everyone who contributed, reporters, reviewers, and coders alike.
