diff --git a/docs/src/changelog/meta.json b/docs/src/changelog/meta.json
index dc9ad71d4c..bfd502b62f 100644
--- a/docs/src/changelog/meta.json
+++ b/docs/src/changelog/meta.json
@@ -1,6 +1,3 @@
{
- "pages": [
- "CHANGELOG",
- "migration-v10"
- ]
+ "pages": ["CHANGELOG", "migration-v9-6", "migration-v10"]
}
diff --git a/docs/src/changelog/migration-v9-6.mdx b/docs/src/changelog/migration-v9-6.mdx
new file mode 100644
index 0000000000..cd0f757f01
--- /dev/null
+++ b/docs/src/changelog/migration-v9-6.mdx
@@ -0,0 +1,905 @@
+---
+title: Migrating from v9.5 to v9.6
+description: How to update your LVGL v9.5 project to v9.6
+---
+
+{/*
+ CONTRIBUTOR GUIDE
+ -----------------
+ Add an entry here in the same PR where you introduce a breaking change
+ or deprecation.
+
+ Structure:
+ - Top-level sections: ## Widgets, ## Core, ## Drawing, etc.
+ - Second level: ### lv_foo (the widget or module)
+ - Entries: a short bullet per changed symbol, with before/after code if helpful.
+*/}
+
+
+ LVGL v9.6 is the final release in the v9 series. All symbols marked
+ `LV_DEPRECATED` are removed in v10.0.
+
+
+---
+
+## Configuration
+
+Starting with v9.6, LVGL's configuration was overhauled. [Kconfig](https://github.com/lvgl/lvgl/blob/master/Kconfig)
+is now the **single source of truth** for every configuration option. The files
+you actually use, `lv_conf_template.h` (the file you copy to `lv_conf.h`), the
+internal headers that apply defaults, and the `CONFIG_*` bridge, are all
+*generated* from the Kconfig tree, so the three ways of configuring LVGL
+(`lv_conf.h`, Kconfig, and compiler `-D` defines) can no longer drift apart.
+
+
+ Configuring LVGL with a hand-written `lv_conf.h` is still **fully supported**
+ and remains the default for most projects. Kconfig being the source of truth
+ is an internal change: `lv_conf_template.h` is now generated from it, but you
+ copy and edit it exactly as before.
+
+
+### `lv_conf_template.h` overhaul
+
+`lv_conf_template.h` is now generated from Kconfig rather than hand-maintained.
+The practical effects when you upgrade:
+
+- Options now appear in a consistent order with consistent comments, and every
+ option's default matches the `Kconfig` default exactly.
+- A few options were renamed or restructured (see below). Old names still work
+ for now, they produce warnings or errors at compile time but they will be
+ **removed in v10.0**, so update them.
+
+The simplest way to migrate an existing `lv_conf.h` is to copy the new
+`lv_conf_template.h` over a fresh `lv_conf.h` and re-apply your changes, or to
+re-run `scripts/generate_lv_conf.py` if you're using `lv_conf.defaults`. Either way,
+watch the compiler output for the deprecation `#warning`s listed below.
+
+### Minimal configuration
+
+The `LV_CONF_MINIMAL` switch, which disabled all widgets, themes, layouts and
+fonts in one go, has been **removed**. A one-off switch like this doesn't fit a
+Kconfig-driven model, where presets are expressed as *defconfigs*. The minimal
+preset now lives at `configs/defconfigs/empty.defconfig`:
+
+```bash
+# Build LVGL from the empty preset
+cmake -B build -DLV_BUILD_USE_KCONFIG=ON -DLV_BUILD_DEFCONFIG_PATH=configs/defconfigs/empty.defconfig
+```
+
+If you set `LV_CONF_MINIMAL` (in `lv_conf.h` or Kconfig) you'll get a build
+warning. Start from the "empty" defconfig and re-enable only what you need.
+
+### Renamed and restructured options
+
+Every old name below **still compiles** for now: it emits a `#warning` at build
+time and continues to work (it is mapped to its replacement, or still honored in
+place), and is **removed in v10.0**.
+
+Watch your build log after upgrading and update each one the warning/error points at.
+
+#### Color format
+
+`LV_COLOR_DEPTH` is replaced by `LV_COLOR_FORMAT_DEFAULT`, which names the format
+you want directly instead of describing how many bits it takes.
+
+The reasoning is that a bit count is ambiguous: `LV_COLOR_DEPTH 16` means either
+`RGB565` or `RGB565_SWAPPED`, and `32` means either `XRGB8888` or `ARGB8888`
+
+This new configuration makes the choice explicit:
+
+| Before | After |
+|---|---|
+| `LV_COLOR_DEPTH 1` | `LV_COLOR_FORMAT_DEFAULT LV_COLOR_FORMAT_I1` |
+| `LV_COLOR_DEPTH 8` | `LV_COLOR_FORMAT_DEFAULT LV_COLOR_FORMAT_L8` |
+| `LV_COLOR_DEPTH 16` | `LV_COLOR_FORMAT_DEFAULT LV_COLOR_FORMAT_RGB565` |
+| `LV_COLOR_DEPTH 24` | `LV_COLOR_FORMAT_DEFAULT LV_COLOR_FORMAT_RGB888` |
+| `LV_COLOR_DEPTH 32` | `LV_COLOR_FORMAT_DEFAULT LV_COLOR_FORMAT_XRGB8888` |
+
+Beyond those five, `LV_COLOR_FORMAT_DEFAULT` also accepts formats that no
+`LV_COLOR_DEPTH` value could reach:
+
+- `LV_COLOR_FORMAT_RGB565_SWAPPED`: big-endian RGB565 panels, previously handled
+ by swapping bytes in the flush callback
+- `LV_COLOR_FORMAT_ARGB8888` — a screen with a real alpha channel
+- `LV_COLOR_FORMAT_ARGB8888_PREMULTIPLIED` — premultiplied alpha, what compositors
+ such as Wayland expect
+
+`LV_COLOR_DEPTH` is still defined and still readable from your own code
+(`#if LV_COLOR_DEPTH == 32` keeps working), but it is now **derived** from
+`LV_COLOR_FORMAT_DEFAULT` rather than something you set, and it no longer appears
+in `lv_conf_template.h`. Being a bit count it is lossy: both RGB565 formats report
+`16`, and all three 8888 formats report `32`.
+
+`LV_COLOR_FORMAT_NATIVE` and `LV_COLOR_FORMAT_NATIVE_WITH_ALPHA` were
+`lv_color_format_t` enumerators picked by `LV_COLOR_DEPTH`. They are no longer part of
+the enum and live on as compatibility macros in `lv_api_map_v9_5.h`, so they keep
+working unless you build with `LV_DISABLE_API_MAPPING`. `NATIVE` is a plain alias of
+`LV_COLOR_FORMAT_DEFAULT`; `NATIVE_WITH_ALPHA` maps each format to its alpha-capable
+companion, unchanged from v9.5 for the five formats a color depth could select:
+
+| `LV_COLOR_FORMAT_DEFAULT` | `LV_COLOR_FORMAT_NATIVE_WITH_ALPHA` |
+|---|---|
+| `I1` | `I1` (there is no alpha-capable 1-bit format) |
+| `L8` | `AL88` |
+| `RGB565`, `RGB565_SWAPPED` | `RGB565A8` |
+| `RGB888`, `XRGB8888`, `ARGB8888` | `ARGB8888` |
+| `ARGB8888_PREMULTIPLIED` | `ARGB8888_PREMULTIPLIED` |
+
+In new code name the format you want explicitly (`LV_COLOR_FORMAT_ARGB8888` in most
+cases), or read a display's actual format with `lv_display_get_color_format()`.
+
+
+ The deprecated color depth choice is still there, and the default color format is
+ seeded from it: an existing `.config` or `defconfig` holding
+ `CONFIG_LV_COLOR_DEPTH_32=y` selects `XRGB8888` with no change on your side.
+ Replace it with `CONFIG_LV_COLOR_FORMAT_XRGB8888=y`
+
+
+
+`LV_COLOR_FORMAT_DEFAULT` sets the format each display *starts* with, not a global
+constant. Any display's format can be changed at runtime with
+,
+and two displays can run different formats. This needs support from the driver:
+its flush callback has to handle the pixel layout it is handed, and the draw
+buffer(s) must be large enough for the new format.
+
+
+#### Memory
+
+| Removed | Replacement |
+|---|---|
+| `LV_MEM_SIZE_KILOBYTES` | `LV_MEM_SIZE` (value in **bytes**) |
+| `LV_MEM_POOL_EXPAND_SIZE_KILOBYTES` | `LV_MEM_SIZE` (set the full size in **bytes**) |
+| `LV_MEM_POOL_EXPAND_SIZE` | `LV_MEM_SIZE` (set the full pool size) |
+
+```c
+/* Before */
+#define LV_MEM_SIZE_KILOBYTES 64
+/* After */
+#define LV_MEM_SIZE (64 * 1024)
+```
+
+#### Threading
+
+| Removed | Replacement |
+|---|---|
+| `LV_DRAW_THREAD_STACKSIZE` | `LV_DRAW_THREAD_STACK_SIZE` |
+
+#### LZ4 / ThorVG
+
+`LZ4` and `ThorVG` no longer have separate `_INTERNAL`/`_EXTERNAL` enables.
+Instead, enable the library itself with `LV_USE_LZ4` / `LV_USE_THORVG`,
+and use the `_INTERNAL` flag to choose between the bundled source and an externally provided one:
+
+| Removed | Replacement |
+|---|---|
+| `LV_USE_LZ4_EXTERNAL` | `LV_USE_LZ4` = 1 and `LV_USE_LZ4_INTERNAL` = 0 |
+| `LV_USE_THORVG_EXTERNAL` | `LV_USE_THORVG` = 1 and `LV_USE_THORVG_INTERNAL` = 0 |
+
+
+ If your project previously had `LV_USE_THORVG_INTERNAL` enabled (with no `LV_USE_THORVG_EXTERNAL`), you must now also enable `LV_USE_THORVG`.
+ Leaving `LV_USE_THORVG` disabled while `LV_USE_THORVG_INTERNAL` is enabled will result in a build error.
+ The same applies to `LV_USE_LZ4` and `LV_USE_LZ4_INTERNAL`.
+
+
+#### GPU (VG-Lite)
+
+The VG-Lite GPU is now chosen with a single `LV_VG_LITE_GPU` option instead of a free-form series/revision pair.
+Set it to one of the predefined GPU/revision combinations:
+
+| Removed | Replacement |
+|---|---|
+| `LV_VG_LITE_HAL_GPU_SERIES` | `LV_VG_LITE_GPU` |
+| `LV_VG_LITE_HAL_GPU_REVISION` | `LV_VG_LITE_GPU` |
+
+`LV_VG_LITE_GPU` accepts one of the following values:
+
+| Value | GPU | Revision |
+|---|---|---|
+| `LV_VG_LITE_GPU_GC255_0X40A` | GC255 | 0x40A |
+| `LV_VG_LITE_GPU_GC355_0X0_1215` | GC355 | 0x0_1215 |
+| `LV_VG_LITE_GPU_GC355_0X0_1216` | GC355 | 0x0_1216 |
+| `LV_VG_LITE_GPU_GC555_0X423` | GC555 | 0x423 |
+| `LV_VG_LITE_GPU_GC555_0X423_ECO` | GC555 | 0x423 ECO |
+| `LV_VG_LITE_GPU_GCNANOULTRAV_0X1003` | GCNanoUltraV | 0x1003 |
+
+#### Drivers
+
+No driver infers its rendering backend from the enabled draw units any more. SDL and DRM now expose an explicit `LV__BACKEND` choice; Wayland instead lets you
+enable each of its backends individually and resolves between them at runtime. See below.
+
+Their `LV__AUTO_BACKEND` flags (enabled by default) exist purely to ease migration: they let LVGL reproduce the old auto-detected backend so existing projects
+don't silently switch to a different backend on update simply because they never set one explicitly. Leaving an `_AUTO_BACKEND` flag enabled triggers a build-time warning.
+These flags are not meant to be relied on going forward and will be removed for v10, every project should set its backend explicitly with `LV__BACKEND` and then
+disable the corresponding `_AUTO_BACKEND` flag to silence the warning.
+
+- **SDL** — while `LV_SDL_AUTO_BACKEND` is enabled, the backend is inferred the legacy way: EGL when `LV_USE_OPENGLES` and (`LV_USE_DRAW_NANOVG` or `LV_USE_DRAW_OPENGLES`) are enabled,
+otherwise Textures when `LV_USE_DRAW_SDL` is enabled, otherwise Software. Set `LV_SDL_AUTO_BACKEND` to 0 and select a backend explicitly with `LV_SDL_BACKEND`:
+
+ | Value | Backend |
+ |---|---|
+ | `LV_SDL_BACKEND_SW` | Software (SDL surface) |
+ | `LV_SDL_BACKEND_TEXTURE` | Cached SDL textures (enable: `LV_USE_DRAW_SDL`) |
+ | `LV_SDL_BACKEND_EGL` | EGL (OpenGL ES, hardware-accelerated; requires `LV_USE_DRAW_OPENGLES` or `LV_USE_DRAW_NANOVG`) |
+
+ On Kconfig's side, buffer count is now a single integer instead of two separate flags:
+
+ | Removed | Replacement |
+ |---|---|
+ | `LV_SDL_SINGLE_BUFFER` | `LV_SDL_BUF_COUNT` = 1 |
+ | `LV_SDL_DOUBLE_BUFFER` | `LV_SDL_BUF_COUNT` = 2 |
+
+- **Linux DRM** — while `LV_LINUX_DRM_AUTO_BACKEND` is enabled, the backend is inferred the legacy way: EGL when `LV_USE_OPENGLES` is enabled, otherwise FBDEV.
+Set `LV_LINUX_DRM_AUTO_BACKEND` to 0 and select a backend explicitly with `LV_LINUX_DRM_BACKEND`:
+
+ | Value | Backend |
+ |---|---|
+ | `LV_LINUX_DRM_BACKEND_FBDEV` | Dumb buffers (no GPU) |
+ | `LV_LINUX_DRM_BACKEND_GBM` | GBM DMA buffers |
+ | `LV_LINUX_DRM_BACKEND_EGL` | EGL (OpenGL ES, hardware-accelerated; enable: `LV_USE_OPENGLES`) |
+
+ `LV_USE_LINUX_DRM_GBM_BUFFERS` is no longer a user-facing setting. It is now set internally and automatically enabled whenever the `EGL` or `GBM` backend is selected; it should no longer be set directly in `lv_conf.h`.
+
+- **Wayland** — the backend is no longer inferred, and no longer has to be a single one.
+
+ Previously `LV_WAYLAND_USE_SHM`, `LV_WAYLAND_USE_EGL` and `LV_WAYLAND_USE_G2D` were derived internally and mutually exclusive: enabling `LV_USE_OPENGLES` gave you EGL,
+ otherwise enabling `LV_USE_G2D` gave you G2D, otherwise you got SHM. Setting them in `lv_conf.h` had no effect.
+
+ They are now ordinary options that you set yourself, and **more than one may be enabled at a time**:
+
+ | Symbol | Backend | Default |
+ |---|---|---|
+ | `LV_WAYLAND_USE_SHM` | SHM (Shared Memory) | on, except with a GPU draw unit |
+ | `LV_WAYLAND_USE_EGL` | EGL (OpenGL ES, hardware-accelerated; enable: `LV_USE_OPENGLES`) | off; under Kconfig, on with a GPU draw unit |
+ | `LV_WAYLAND_USE_G2D` | G2D (NXP i.MX hardware accelerator; enable: `LV_USE_DRAW_G2D`) | off |
+
+ There is also a new fourth backend, `LV_WAYLAND_USE_DMABUF`, which presents software-rendered frames as linear DMA-BUFs.
+ See the [Wayland driver docs](/integration/embedded_linux/drivers/wayland#dma-buf).
+
+ To reproduce exactly what you had before, enable the one backend the old rules would have picked for your config. `LV_USE_G2D` no longer plays a part in the choice:
+ it is a deprecated no-op (see [No-op options kept for compatibility](#no-op-options-kept-for-compatibility)) and the G2D backend is now tied to `LV_USE_DRAW_G2D`.
+
+ | You had | Backend you got | Add |
+ |---|---|---|
+ | `LV_USE_OPENGLES` = 1 | EGL | `LV_WAYLAND_USE_EGL` = 1 (keep `LV_USE_OPENGLES`) |
+ | `LV_USE_G2D` = 1 | G2D | `LV_WAYLAND_USE_G2D` = 1 and `LV_USE_DRAW_G2D` = 1 |
+ | neither | SHM | nothing — `LV_WAYLAND_USE_SHM` is on by default |
+
+ See the [Wayland driver docs](/integration/embedded_linux/drivers/wayland#runtime-backend-selection) for more information.
+
+- **X11** — render mode (unrelated to backend selection) is now a single value mapped onto the standard `LV_DISPLAY_RENDER_MODE_*` constants:
+
+ | Removed | Replacement |
+ |---|---|
+ | `LV_X11_RENDER_MODE_PARTIAL` | `LV_X11_RENDER_MODE` = `LV_DISPLAY_RENDER_MODE_PARTIAL` |
+ | `LV_X11_RENDER_MODE_DIRECT` | `LV_X11_RENDER_MODE` = `LV_DISPLAY_RENDER_MODE_DIRECT` |
+ | `LV_X11_RENDER_MODE_FULL` | `LV_X11_RENDER_MODE` = `LV_DISPLAY_RENDER_MODE_FULL` |
+
+#### Calendar day/month names
+
+The two array macros became individual short strings, so each name is now a plain
+Kconfig-settable string and you can localize one without redefining the whole
+array. `LV_CALENDAR_WEEK_STARTS_MONDAY` is unchanged, ordering is handled
+internally. The old array macros still work (with a `#warning`).
+
+| Removed | Replacement |
+|---|---|
+| `LV_CALENDAR_DEFAULT_DAY_NAMES` | `LV_MONDAY_STR` ... `LV_SUNDAY_STR` |
+| `LV_CALENDAR_DEFAULT_MONTH_NAMES` | `LV_JANUARY_STR` ... `LV_DECEMBER_STR` |
+
+#### libinput XKB keymap
+
+The single keymap struct became individual string options (again, so Kconfig can
+set each one). The old struct macro still works (with a `#warning`):
+
+| Removed | Replacement |
+|---|---|
+| `LV_LIBINPUT_XKB_KEY_MAP` | `LV_LIBINPUT_XKB_RULES`, `LV_LIBINPUT_XKB_MODEL`, `LV_LIBINPUT_XKB_LAYOUT`, `LV_LIBINPUT_XKB_VARIANT`, `LV_LIBINPUT_XKB_OPTIONS` |
+
+```c
+/* Before */
+#define LV_LIBINPUT_XKB_KEY_MAP { .rules = NULL, .model = "pc101", .layout = "us", .variant = NULL, .options = NULL }
+
+/* After */
+#define LV_LIBINPUT_XKB_MODEL "pc101"
+#define LV_LIBINPUT_XKB_LAYOUT "us"
+/* rules / variant / options default to "" */
+```
+
+### Macros that can't be set from Kconfig
+
+Some configuration values are function-like or expression macros — for example
+`LV_FONT_CUSTOM_DECLARE`, the custom `LV_ASSERT_HANDLER`, the `LV_ATTRIBUTE_*`
+hooks, and NemaGFX pool attributes. Kconfig only stores booleans, integers and
+strings, so it cannot hold these.
+
+**If you configure with `lv_conf.h`, nothing changes** — keep defining these
+macros inline exactly as before. `LV_FONT_CUSTOM_DECLARE`, `LV_ASSERT_HANDLER`,
+the attribute hooks, etc. are all still honored when defined in `lv_conf.h`.
+
+**If you configure with Kconfig**, you can't write such a macro as a Kconfig
+value, so each module now offers a `LV__USE_CUSTOM_INCLUDE` switch plus a
+`LV__CUSTOM_INCLUDE` path. Point it at a small header that defines the
+macros, e.g.:
+
+```c
+/* my_lvgl_extra.h, referenced by LV_FONT_CUSTOM_INCLUDE */
+#define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(my_font_24)
+#define LV_FONT_DEFAULT &my_font_24
+```
+
+This pair exists for `FONT`, `ASSERT`, `ATTRIBUTE`, `SYSMON`, `NEMA` and the
+global custom include. As part of unifying this convention, a couple of old
+include/enable names were renamed (each still works with a `#warning`):
+
+| Removed | Replacement |
+|---|---|
+| `LV_ASSERT_HANDLER_INCLUDE` | `LV_ASSERT_CUSTOM_INCLUDE` (enable with `LV_ASSERT_USE_CUSTOM_INCLUDE`) |
+
+If you still rely on the old `LV_ASSERT_HANDLER_INCLUDE` and can't migrate
+immediately, you can silence the deprecation warning with
+`LV_DISABLE_ASSERT_HANDLER_INCLUDE_WARNING`. This only suppresses the
+compiler warning, `LV_ASSERT_HANDLER_INCLUDE` itself still works the same
+way. New projects should use `LV_ASSERT_CUSTOM_INCLUDE` and define their
+custom assertion handler directly instead.
+
+A related effect: `lv_conf_template.h` no longer prints an empty stub for every
+overridable macro (`LV_ATTRIBUTE_*`, `LV_PROFILER_*`, `LV_EXPORT_CONST_INT`,
+`LV_ASSERT_HANDLER`, ...). They are **not removed** — define them in `lv_conf.h`
+or via a `*_CUSTOM_INCLUDE` and LVGL still picks them up; they just default to
+empty/no-op otherwise.
+
+### No-op options kept for compatibility
+
+`LV_USE_PXP` and `LV_USE_G2D` never enabled drawing on their own, you always
+also needed `LV_USE_DRAW_PXP` / `LV_USE_DRAW_G2D`. They are kept as **deprecated
+no-ops** (and excluded from the generated headers) purely so existing
+`defconfig`s that set `CONFIG_LV_USE_PXP` / `CONFIG_LV_USE_G2D` keep loading.
+They have no effect and emit no warning. Use the real switches:
+
+| No-op | Real switch |
+|---|---|
+| `LV_USE_PXP` | `LV_USE_DRAW_PXP` |
+| `LV_USE_G2D` | `LV_USE_DRAW_G2D` |
+
+See [Configuring LVGL](/integration/configuration) for the full picture of how
+configuration works now.
+
+---
+
+## Core
+
+### Argument checking
+
+LVGL v9.6 standardizes public-API argument validation. `LV_ASSERT_OBJ` is
+replaced by , which logs a warning and lets the
+caller recover gracefully instead of halting the program.
+
+```c
+/* Before */
+LV_ASSERT_OBJ(obj, &lv_label_class);
+/* After */
+LV_CHECK_OBJ(obj, &lv_label_class, return);
+```
+
+#### What `LV_CHECK_OBJ` actually checks
+
+The macro is layered — each layer is independently configurable in `lv_conf.h`:
+
+| Layer | Guard | What it checks |
+|---|---|---|
+| Base | `LV_USE_CHECK_ARG` | Enables the whole system; when 0 all three macros expand to nothing. Also enables NULL checks on all arguments regardless of whether Class or Validity guards are set. |
+| Class | `LV_USE_CHECK_OBJ_CLASSTYPE` | `lv_obj_has_class(obj, cls)` — verifies the object is the expected widget type. |
+| Validity | `LV_USE_CHECK_OBJ_VALIDITY` | `lv_obj_is_in_widget_tree(obj)` — verifies the object is still part of the live widget tree. |
+
+All three layers default to `0` (disabled). Enable them selectively:
+
+```c
+/* lv_conf.h */
+#define LV_USE_CHECK_ARG 1 /* master switch */
+#define LV_USE_CHECK_OBJ_CLASSTYPE 1 /* also check class type */
+#define LV_USE_CHECK_OBJ_VALIDITY 1 /* also check widget-tree membership */
+```
+
+When `LV_USE_CHECK_OBJ_CLASSTYPE` is `0`, `LV_CHECK_OBJ` still performs a
+NULL check; when `LV_USE_CHECK_OBJ_VALIDITY` is `0`, it still performs the
+class check (if enabled). The checks are cumulative.
+
+
+ `lv_obj_is_in_widget_tree()` walks the widget tree and `lv_obj_has_class()` traverses
+ the class hierarchy — both add non-trivial overhead on every call site.
+ Leave `LV_USE_CHECK_OBJ_CLASSTYPE` and `LV_USE_CHECK_OBJ_VALIDITY` at `0`
+ in production builds.
+
+
+#### Failure behavior
+
+Two further options control what happens when a check fails:
+
+```c
+/* lv_conf.h */
+
+/* Also call LV_ASSERT_HANDLER (halts / breakpoints like the old assert). */
+#define LV_CHECK_ARG_ASSERT_ON_FAIL 0
+
+/* How much to log when a check fails (requires LV_USE_LOG = 1). */
+/* LV_CHECK_ARG_LOG_MODE_NONE (0) – no output */
+/* LV_CHECK_ARG_LOG_MODE_MINIMAL (1) – "Check failed" + file/line */
+/* LV_CHECK_ARG_LOG_MODE_VERBOSE (2) – "Check failed: " + caller message */
+#define LV_CHECK_ARG_LOG_MODE LV_CHECK_ARG_LOG_MODE_VERBOSE
+```
+
+Setting `LV_CHECK_ARG_ASSERT_ON_FAIL 1` restores the hard-abort behavior of
+`LV_ASSERT_OBJ`, which can be useful during a debugging session.
+`LV_CHECK_ARG_LOG_MODE` has no effect when `LV_USE_LOG` is `0`.
+
+See the [Argument Checking](contributing/assertions/#argument-checking) docs for
+the full and API
+if you need finer control.
+
+### Assertions
+
+Assertions in LVGL are now disabled by default and can be enabled with .
+
+Note that these assertions are not required and should be disabled to improve performance when deploying your LVGL application.
+
+### lv_obj
+
+- `lv_obj_find_by_id()` is deprecated, use `lv_obj_find_by_name()` instead:
+ ```c
+ /* Before */
+ lv_obj_t * obj = lv_obj_find_by_id(parent, my_id);
+
+ /* After */
+ lv_obj_t * obj = lv_obj_find_by_name(parent, "my_widget");
+ ```
+
+- The generic flag API — `lv_obj_add_flag()`, `lv_obj_remove_flag()`,
+ `lv_obj_set_flag()`, `lv_obj_has_flag()` and `lv_obj_has_flag_any()` — is
+ deprecated. Each flag now has a dedicated setter (`lv_obj_set_(obj, en)`)
+ and getter (`lv_obj_is_(obj)`):
+ ```c
+ /* Before */
+ lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE);
+ lv_obj_set_flag(obj, LV_OBJ_FLAG_SCROLLABLE, en);
+ bool h = lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN);
+
+ /* After */
+ lv_obj_set_hidden(obj, true);
+ lv_obj_set_clickable(obj, false);
+ lv_obj_set_scrollable(obj, en);
+ bool h = lv_obj_is_hidden(obj);
+ ```
+ The custom-bit flags (`LV_OBJ_FLAG_USER_1`–`4`) can be accessed via
+ `lv_obj_set/get_user_flag()`, which take a bit from 0 to 3 e.g `lv_obj_set_user_flag(obj, 0, true)`
+ instead of lv_obj_get/set-flag(obj, LV_OBJ_FLAG_USER_1).
+
+ A migration script is available at `scripts/migration/migrate_obj_flags.py`:
+ ```bash
+ python3 scripts/migration/migrate_obj_flags.py path/to/your/project
+ ```
+ _Note: Only raw flag constants can be converted automatically. Expressions using variables will be skipped for safety._
+
+- `lv_obj_style_set_disabled()` and `lv_obj_style_get_disabled()` are deprecated
+ in favor of the positive-logic and
+ . Note that the meaning of the
+ `bool` is inverted:
+ ```c
+ /* Before */
+ lv_obj_style_set_disabled(obj, &style, LV_PART_MAIN, true); /*Disable the style*/
+ bool dis = lv_obj_style_get_disabled(obj, &style, LV_PART_MAIN);
+
+ /* After */
+ lv_obj_set_style_enabled(obj, &style, LV_PART_MAIN, false); /*Disable the style*/
+ bool en = lv_obj_get_style_enabled(obj, &style, LV_PART_MAIN);
+ ```
+
+---
+
+## Drawing/Rendering
+
+The `lv_draw_sw_xxx` functions are renamed to `lv_draw_xxx`.
+
+- `lv_draw_sw_i1_to_argb8888` ->
+- `lv_draw_sw_rgb565_swap` ->
+- `lv_draw_sw_rotate` ->
+- `lv_draw_sw_i1_invert` ->
+- `lv_draw_sw_i1_convert_to_vtiled` ->
+
+The old names can still be used via LVGL's v9.5 API map which is automatically included via `lvgl.h`.
+
+---
+
+## Display
+
+Calling the following functions with `NULL` as the display parameter is deprecated and will be considered an error in future versions. You can use to query the default display.
+
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+
+---
+
+## Sysmon
+
+Calling the following functions with `NULL` as the display parameter is deprecated and will be considered an error in future versions. You can use to query the default display.
+
+-
+-
+-
+-
+-
+-
+-
+-
+
+---
+
+## Observer
+
+### Subject create/delete
+
+The `lv_subject_init_()` family and are
+deprecated. A Subject is now allocated by LVGL with
+, which takes the type and returns a pointer, and
+freed with . Every Subject is tracked by LVGL, so
+any Subject still alive at `lv_deinit()` is deleted automatically.
+
+Keep the returned pointer where the `lv_subject_t` value used to live, and drop the
+`&` from every call that took its address:
+
+```c
+/* Before */
+static lv_subject_t temperature;
+lv_subject_init_int(&temperature, 20);
+lv_label_bind_text(label, &temperature, "%d °C");
+lv_subject_deinit(&temperature);
+
+/* After */
+static lv_subject_t * temperature;
+temperature = lv_subject_create(LV_SUBJECT_TYPE_INT);
+lv_subject_set_int(temperature, 20);
+lv_label_bind_text(label, temperature, "%d °C");
+lv_subject_delete(temperature);
+```
+
+The type is one of `LV_SUBJECT_TYPE_INT`, `LV_SUBJECT_TYPE_FLOAT`,
+`LV_SUBJECT_TYPE_STRING`, `LV_SUBJECT_TYPE_POINTER`, `LV_SUBJECT_TYPE_COLOR` or
+`LV_SUBJECT_TYPE_GROUP`.
+
+There is no initial value parameter any more. `lv_subject_create()` starts the Subject
+from a neutral value — `0` for int and float, `NULL` for pointer, black for color, an
+empty string for string, an empty list for group — and the initial value is set with
+the regular `lv_subject_set_...()` function. Two consequences to keep in mind:
+
+- Set `lv_subject_set_min_value_int()`/`lv_subject_set_max_value_int()` (and the float
+ variants) *before* the first value, because `lv_subject_set_int()` clamps to them,
+ while `lv_subject_init_int()` did not.
+- After the first `lv_subject_set_...()` the previous value is the neutral one, not the
+ initial one. `lv_subject_init_...()` set both to the initial value.
+
+The string buffers and the group list moved out of the init call into their own
+setters. Both take the memory as-is, so it still has to out-live the Subject:
+
+```c
+/* Before */
+static lv_subject_t title;
+lv_subject_init_string(&title, buf, prev_buf, sizeof(buf), "Hello");
+
+/* After */
+static lv_subject_t * title;
+title = lv_subject_create(LV_SUBJECT_TYPE_STRING);
+lv_subject_set_string_buffer_static(title, buf, prev_buf, sizeof(buf));
+lv_subject_set_string(title, "Hello");
+```
+
+```c
+/* Before */
+static lv_subject_t * list[3] = {&mode, &value, &unit};
+static lv_subject_t measurement;
+lv_subject_init_group(&measurement, list, 3);
+
+/* After */
+static lv_subject_t * list[3];
+static lv_subject_t * measurement;
+list[0] = mode;
+list[1] = value;
+list[2] = unit;
+measurement = lv_subject_create(LV_SUBJECT_TYPE_GROUP);
+lv_subject_set_group_list_static(measurement, list, 3);
+```
+
+Because the group list is filled at run time, a static initializer taking the
+addresses of the member Subjects has to become run-time assignments.
+
+ accepts `NULL`, and unlike
+`lv_subject_deinit()` it also frees the Subject itself, so the pointer must not be
+used afterwards.
+
+- `lv_subject_copy_string()` is deprecated in favor of
+ , so that every Subject type is written
+ with `lv_subject_set_()`. The behavior is unchanged: the string is still
+ copied into the Subject's own buffer.
+ ```c
+ /* Before */
+ lv_subject_copy_string(title, "Hello");
+
+ /* After */
+ lv_subject_set_string(title, "Hello");
+ ```
+
+### lv_obj_bind_flag_if_* / lv_obj_bind_state_if_*
+
+- The `lv_obj_bind_flag_if_*` and `lv_obj_bind_state_if_*` families
+ (`_eq`/`_not_eq`/`_gt`/`_ge`/`_lt`/`_le`) are deprecated. For a boolean subject,
+ bind a widget flag with `lv_obj_bind_bool()` — the dedicated per-flag setters
+ (`lv_obj_set_hidden()`, `lv_obj_set_clickable()`, …) can be passed directly as
+ the callback. For a state, or for any comparison against a reference value, add
+ a custom observer with `lv_subject_add_observer_obj()`:
+ ```c
+ /* Before: hide a widget while a 0/1 subject is non-zero */
+ lv_obj_bind_flag_if_not_eq(obj, &subject, LV_OBJ_FLAG_HIDDEN, 0);
+
+ /* After */
+ lv_obj_bind_bool(obj, subject, lv_obj_set_hidden);
+
+ /* Before: disable a widget while a subject is greater than 80 */
+ lv_obj_bind_state_if_gt(obj, &subject, LV_STATE_DISABLED, 80);
+
+ /* After */
+ static void disabled_observer_cb(lv_observer_t * observer, lv_subject_t * subject)
+ {
+ lv_obj_t * obj = lv_observer_get_target_obj(observer);
+ lv_obj_set_state(obj, LV_STATE_DISABLED, lv_subject_get_int(subject) > 80);
+ }
+ lv_subject_add_observer_obj(subject, disabled_observer_cb, obj, NULL);
+ ```
+
+---
+
+## Widgets
+
+### lv_qrcode
+
+- No API change, but a behavioral one: `lv_qrcode_set_size()` and
+ `lv_qrcode_set_quiet_zone()` now re-encode the QR code, and the colors are
+ re-applied, when they are called *after* the data. Previously such a change was
+ silently dropped and the widget kept showing the bitmap produced by
+ `lv_qrcode_update()` / `lv_qrcode_set_data()`. The properties may now be set in
+ any order.
+
+ To make this possible the widget keeps a copy of the payload, which costs
+ `data_len` bytes per QR code object.
+
+- New: `lv_qrcode_set_update_mode(qr, LV_QRCODE_UPDATE_MODE_DEFERRED)` collapses
+ several property changes into a single re-encode on the next redraw. The default,
+ `LV_QRCODE_UPDATE_MODE_IMMEDIATE`, re-encodes inside the setter.
+
+- New: `lv_qrcode_render()` re-encodes the stored payload without taking it as an
+ argument, which is how property changes made in the deferred update mode are applied.
+
+- New: `lv_qrcode_is_render_valid()` reports whether the last encode succeeded, for the
+ re-encodes whose result cannot be returned (the void property setters, and the
+ deferred re-encode done by the draw pass).
+
+### lv_span
+
+- `lv_spangroup_set_align()` is deprecated, use the `text_align` style property instead:
+ ```c
+ /* Before */
+ lv_spangroup_set_align(obj, LV_TEXT_ALIGN_CENTER);
+
+ /* After */
+ lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
+ ```
+
+- `lv_spangroup_set_mode()` is deprecated, control expanding/wrapping by setting the widget width instead:
+ ```c
+ /* Before */
+ lv_spangroup_set_mode(obj, LV_SPAN_MODE_EXPAND);
+
+ /* After — LV_SIZE_CONTENT expands to fit, a fixed value wraps */
+ lv_obj_set_width(obj, LV_SIZE_CONTENT);
+ ```
+
+### lv_textarea
+
+- `lv_textarea_set_align()` is deprecated, use the `text_align` style property instead:
+ ```c
+ /* Before */
+ lv_textarea_set_align(obj, LV_TEXT_ALIGN_CENTER);
+
+ /* After */
+ lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
+ ```
+
+### lv_scale
+
+- `lv_scale_section_set_style()` is deprecated, use the per-part setters instead:
+ ```c
+ /* Before */
+ lv_scale_section_set_style(section, LV_PART_MAIN, &my_style);
+ lv_scale_section_set_style(section, LV_PART_INDICATOR, &my_style);
+ lv_scale_section_set_style(section, LV_PART_ITEMS, &my_style);
+
+ /* After */
+ lv_scale_set_section_style_main(scale, section, &my_style);
+ lv_scale_set_section_style_indicator(scale, section, &my_style);
+ lv_scale_set_section_style_items(scale, section, &my_style);
+ ```
+
+### lv_menu
+
+- The `lv_menu` widget is deprecated. A menu is page navigation over base widgets
+ — pages built from `lv_obj` and a back button that swaps the visible page — so
+ build it directly instead. See the
+ [`lv_example_menu_navigation`](widgets/menu#building-a-menu-without-lv_menu)
+ example for a starting point.
+
+### lv_list
+
+- The `lv_list` widget is deprecated. A list is just a flex container with a
+ column flow, so build one directly from `lv_obj` with a `LV_FLEX_FLOW_COLUMN`
+ layout instead. See the
+ [`lv_example_flex_list`](common-widget-features/layouts/flex#building-a-list)
+ example for a starting point.
+
+### lv_win
+
+- The `lv_win` widget is deprecated. A window is just a flex column with a header
+ bar and a content area, so build one directly from `lv_obj` instead. See the
+ [`lv_example_flex_win`](common-widget-features/layouts/flex#building-a-window)
+ example for a starting point.
+
+---
+
+## Drawing
+
+### lv_draw_buf
+
+- `lv_image_buf_set_palette()` is deprecated, use `lv_draw_buf_set_palette()` instead.
+- `lv_image_buf_free()` is deprecated, use `lv_draw_buf_destroy()` instead.
+
+### lv_snapshot
+
+- `lv_snapshot_free()` is deprecated, use `lv_draw_buf_destroy()` directly instead.
+- `lv_snapshot_take_to_buf()` is deprecated, use `lv_snapshot_take_to_draw_buf()` instead:
+
+```c
+ /* Before */
+ lv_snapshot_take_to_buf(obj, cf, dsc, buf, buf_size);
+ /* After */
+ lv_snapshot_take_to_draw_buf(obj, cf, draw_buf);
+ lv_draw_buf_destroy(draw_buf);
+```
+
+---
+
+## Layouts
+
+ is deprecated and is replaced with .
+
+```c
+uint32_t layout = lv_layout_register(my_layout_update, &user_data);
+```
+
+Becomes:
+
+```c
+lv_layout_callbacks_t callbacks = { .layout_update_cb = my_layout_update };
+uint32_t layout = lv_layout_create(callbacks, &user_data);
+```
+
+---
+
+## Image
+
+### SVG
+
+The SVG module internal structures were previously leaked to the public API.
+v9.6 moves these structures to the private API. If you have code that relies on this module, you
+can enable LVGL's private api by including `lvgl_private.h` or simply enabling `LV_USE_PRIVATE_API`
+
+
+No public API usage required the user to access these private structures so
+if you were simply using the public API, this doesn't change anything for you.
+
+
+---
+
+## Misc
+
+### Event
+
+The `lv_event_list_t` structure was previously leaked to the public API.
+v9.6 moves it to the private API. If you have code that relies on the internals of this structure,
+you can enable LVGL's private api by including `lvgl_private.h` or simply enabling `LV_USE_PRIVATE_API`
+
+
+No public API usage required the user to access this private structure so
+if you were simply using the public API, this doesn't change anything for you.
+
+
+### Array
+
+The `lv_array` module was previously leaked to the public LVGL API through the SVG and Event
+modules. v9.6 moves it to the private API. If you have code that relies on this module, you
+can enable LVGL's private api by including `lvgl_private.h` or simply enabling `LV_USE_PRIVATE_API`
+
+### Tree
+
+The `lv_tree` module was previously leaked to the public LVGL API through the SVG module.
+v9.6 moves it to the private API. If you have code that relies on this module, you
+can enable LVGL's private api by including `lvgl_private.h` or simply enabling `LV_USE_PRIVATE_API`
+
+---
+
+## Others
+
+### lv_file_explorer
+
+- The `lv_file_explorer` widget is deprecated. A file explorer is a path header
+ plus a table of directory entries read with the [`lv_fs`](main-modules/fs) API,
+ so build it directly instead. See the
+ [`lv_example_table_file_browser`](widgets/table#building-a-file-explorer) example
+ for a starting point.
+
+---
+
+## Libraries
+
+### rlottie
+
+- The rlottie player is deprecated. Use the [`lv_lottie`](widgets/lottie) widget
+ instead.