Try out LVGL Pro - A complete toolkit to build, test, share, and ship UIs efficiently!
LVGL
Release

LVGL v9.6: The Stability Release

LVGL v9.6 makes software rendering measurably faster, adds Arm SVE2 acceleration and a Wayland DMA-BUF backend, lifts test coverage to 82%, and rebuilds the foundations with public/private headers, a Kconfig-driven configuration, installable packages, and Ubuntu support.

Andre CostaAndre Costa19 min read

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.

BoardRender timeFPSCPU 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 images scene renders 76% faster on the RA8D2 and 60% on the RT700.
  • Transforms come next. An Espressif ESP32-P4 at 1024×600 renders Rotated ARGB images in 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_layer saves 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.

Tip

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:

RZ/G2L 1280×720 — all-scenes average
MetricBeforeAfterChange
Frame rate32fps44fps+38%
Flush time19ms12ms37% less
Render time9ms9msunchanged
CPU load46%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 */
c
Initial Integration. No Documentation Yet

It 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
      • 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.

Old Include Paths Still Work

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.

lv_conf.h Is NOT Going Away

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 it
bash

Around twenty dependencies are covered, SDL2, FreeType, GStreamer, libdrm, GBM, Wayland, X11, libjpeg, libpng, WebP, fastgltf, GLFW and FFmpeg among them.

Turning It Off

-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 build
bash

LV_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"
bash

The 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)
bash

These 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-dev
bash

Packages 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.

Backends2D3D
SDL2liblvgl-sdl2-devliblvgl-sdl2-3d-dev
Waylandliblvgl-wayland-devliblvgl-wayland-3d-dev
DRMliblvgl-drm-devliblvgl-drm-3d-dev
SDL2/Wayland/DRMliblvgl-full-2d-devliblvgl-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;
}
c

Compare 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":

v9.5
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;
}
c
v9.6
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

There 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 */
c
Disabling Checks

With 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:

v9.5
LV_ASSERT_OBJ(obj, &lv_label_class);
c
v9.6
LV_CHECK_OBJ(obj, &lv_label_class, return);
c

It 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_CLASSTYPE adds a check that the widget is the class you claimed. (e.g. is the widget you passed to lv_label_set_text and actual label ?)
  • LV_USE_CHECK_OBJ_VALIDITY adds 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.0v9.6.0
Line coverage78.66%82.23%+3.57 pp
Covered lines46,75656,021+9,265
Uncovered lines12,68612,108−578
Branch coverage61.40%62.59%+1.19 pp
Function coverage82.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.

CI Checks Ensure It Doesn't Decrease

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.

Text Leading Trim
The five LV_TEXT_LEADING_TRIM modes applied to a label

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.

Variable Font Weight
A word animated along a variable font's weight axis

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.

Shared glTF Models
One glTF model rendered by four viewers from different angles

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_DOWNSCALE scales 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-type lv_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.

  1. LV_COLOR_DEPTH becomes LV_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.

  1. LV_ASSERT_HANDLER_INCLUDE becomes LV_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:

v9.5
#define LV_ASSERT_HANDLER_INCLUDE "my_assert.h"
c
v9.6
#define LV_ASSERT_USE_CUSTOM_INCLUDE 1
#define LV_ASSERT_CUSTOM_INCLUDE     "my_assert.h"
c

Five 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:

DeprecatedReplacement
lv_lista flex-column example
lv_wina flex-column example
lv_menua base-widget navigation example
lv_file_explorera table-based file browser example
rlottiethe 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.

Breaking Changes

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.

Migration Guide

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.

If You Are on v9.5

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.


Frequently Asked Questions#

About the author

Andre Costa
Andre Costa

Team Lead - LVGL Open

Team lead at LVGL Open, driving the development and adoption of the open-source embedded graphics library.

Meet the people behind the blog

Discover the talented writers sharing their knowledge about LVGL

View Authors

Subscribe to our newsletter to not miss any news about LVGL. We will send maximum of 2 mails per month.

LVGL

LVGL is the most popular free and open source embedded graphics library targeting any MCU, MPU and display type to build beautiful UIs.

We also do services like UI design, implementation and consulting.

© 2026 LVGL. All rights reserved.
YouTubeGitHubLinkedIn