Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions Docs/LvglProductizationPlan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<!--
Copyright (c) 2026, MarsDoge. All rights reserved.
Author: MarsDoge (Dongyan Qian)
Open source: https://github.com/MarsDoge/ModernSetupPkg
SPDX-License-Identifier: BSD-2-Clause-Patent
-->

# LVGL Display Backend — Productization Plan

The detailed, execution-grade plan to take the LVGL renderer
(`ModernUiLvglRendererLib`) from "experimental, great-looking sample" to a
**shippable** modern DisplayEngine backend across the four XArch targets
(OVMF X64, ArmVirtQemu, LoongArchVirtQemu, RiscVVirtQemu).

This is the depth-companion to `Docs/PRODUCTIZATION_STATUS.md` (the snapshot) and
is scoped to the **engine LVGL line only**. The front-page app content track and
edk2's HII/FormBrowser ownership are out of scope and unchanged.

Last updated: 2026-06-08. Effort key: **S** ≈ <1 day, **M** ≈ days, **L** ≈
week+ / external dependency.

## 0. Current architecture (grounded)

```
ModernDisplayEngineDxe (edk2 DisplayEngine fork; owns no HII semantics)
-> ModernUiCustomizedDisplayLib (text-grid -> modern draw bridge)
-> ModernUiEngineLib (row/page/value visual model)
-> ModernUiRendererLib class
-> ModernUiLvglRendererLib (selected by MODERN_SETUP_DISPLAY_ENGINE=lvgl)
-> LvglCoreLib (upstream lvgl + lv_conf, External/lvgl)
-> GOP
```

Measured facts that drive this plan:

| Aspect | Reality |
| --- | --- |
| Shadow canvas | Full-screen **ARGB8888**, allocated once in init and reused; re-init only when the active GOP resolution changes. ~4 MB at 1280×800. |
| LVGL allocator | `LV_USE_STDLIB_MALLOC = LV_STDLIB_CUSTOM` → `LvglUefiPort.c` maps `lv_malloc`→`AllocatePool`. The 64 KB `LV_MEM_SIZE` is **unused**; no fixed heap ceiling. |
| Per-refresh cost | Each value widget is built as a transient `lv_obj`, `lv_snapshot_take(ARGB8888)`, composited, then `lv_draw_buf_destroy` — **alloc/free churn per widget per refresh**. |
| DXEFV (OVMF X64, DEBUG, lvgl) | **43% full** — comfortable. The ~99% case is LoongArch + REPLACE_UIAPP + DriverSample (DEBUG): a config/platform corner, not the base. |
| Resolution | Taken from `Gop->Mode->Info->HorizontalResolution` (the active mode); adaptive, but unvalidated across a mode matrix. |
| Gating | `ModernUiLvglRendererLib.inf` is labelled `experimental/lvgl-spike`; smoke blocks experimental libs from default overlays; selected only by `MODERN_SETUP_DISPLAY_ENGINE=lvgl`. |

## Gate 1 — Backend graduation (experimental → product-eligible)

**Goal:** the LVGL renderer is a *supported, validated* backend, buildable in CI
for all targets, without yet forcing it as the default.

| ✓ | Step | Effort | Note |
| --- | --- | --- | --- |
| [ ] | **Decide the default policy** | S (decision) | Recommendation: keep `modern` (GOP) as the safe default; make `lvgl` a first-class *opt-in* until hardware-proven, then flip per validated platform (Gate 6). |
| [ ] | Re-frame the package boundary | M | Keep `External/lvgl` upstream-pinned; promote `ModernUiLvglRendererLib` from "spike" wording to a supported library, and decide whether `LvglCoreLib`/`LvglSpikePkg` keeps its name or graduates to `LvglPkg`. No code move required to ship — naming + docs. |
| [ ] | Smoke/CI: permit `lvgl` in a *supported* overlay | M | Today smoke asserts the LVGL libs never appear in a default overlay. Add a "supported opt-in" overlay class so `lvgl` builds are CI-gated without being default. Keep the experimental-only guard for `ModernUiHiiBridgeLib`/`PageAdapterLib`. |
| [ ] | Renderer API stability | S | `Include/ModernUi/ModernUiRenderer.h` is the contract for both backends; freeze it per `Docs/API_COMPATIBILITY.md` before declaring supported. |

**Acceptance:** CI builds `MODERN_SETUP_DISPLAY_ENGINE=lvgl` for all four targets;
smoke passes with the LVGL libs in a supported (non-default) overlay.

## Gate 2 — CJK / i18n coverage (the first true product gate)

**Current:** demand-driven Noto Sans CJK SC subset (182 glyphs, **18×18 A8**,
OFL) generated by `Scripts/generate-font-glyphs.py` from the package's own
strings + selected demo `.uni`; anything outside falls back to the firmware HII
font. Single glyph size.

| ✓ | Step | Effort | Note |
| --- | --- | --- | --- |
| [ ] | **Decide the coverage tier** | S (decision) | Options + memory (18×18 A8 ≈ 324 B/glyph): **(a)** demand-driven per-platform scan — lightest (~tens of KB), needs build-time string knowledge, misses late third-party driver strings; **(b)** standard **GB2312 L1** (~3,755) ≈ **1.2 MB** — predictable arbitrary coverage; **(c)** runtime font from an FFS/font HII package — no embed cost, platform supplies it. Recommendation: **tiered** — keep (a) for our own UI + ship an optional **(b)** behind a PCD (`PcdModernSetupCjkSubset = none\|gb2312l1`) for platforms that need it; HII fallback last. |
| [ ] | **Size matching** | M | The embedded glyph is composited at its native 18 px; the Latin run uses Montserrat 14–24. Decide: generate the CJK subset at the UI's actual sizes (14/16/18/20 → ~4× memory) **or** standardize in-setup body text to one size and verify CJK/Latin baseline alignment. Verify mixed-run vertical alignment in `ModernUiDrawText`. |
| [ ] | zh HII form validation | M | Render a Chinese HII form (zh DriverSample / a platform zh formset) and confirm no missing glyphs at the chosen tier; confirm fallback quality when a glyph is absent (no tofu boxes — define a `?`/placeholder policy). |
| [ ] | Build integration | S | Keep the generated table committed (no build-time Python dep); document regeneration; OFL attribution already in `THIRD_PARTY_NOTICES.md`. |

**Acceptance:** the target coverage tier renders a Chinese platform form with no
missing glyphs, size-consistent with Latin, within the platform memory budget.

## Gate 3 — Memory & performance budget

**Reality check:** base OVMF X64 lvgl DEBUG is 43% DXEFV; the canvas is a
one-time ~4 MB BS-pool allocation freed at ExitBootServices; LVGL uses the UEFI
pool (no 64 KB wall). The real costs are (1) per-refresh **snapshot churn** and
(2) tight FV on heavily-loaded platforms (LoongArch + app + DriverSample).

| ✓ | Step | Effort | Note |
| --- | --- | --- | --- |
| [ ] | **Snapshot scratch-buffer reuse** | M | Replace per-widget `lv_snapshot_take`/`destroy` with a pre-allocated max-row-size ARGB8888 scratch buffer reused across widgets and refreshes. Cuts allocation churn and fragmentation on sustained navigation. Biggest perf lever. |
| [ ] | **RELEASE size baseline** | S | Measure DXEFV for `TARGET=RELEASE` lvgl on each target (DEBUG is not the ship config; `-Os`+LTO+`MDEPKG_NDEBUG` shrink it materially). Record per-target headroom. |
| [ ] | `lv_conf` trim | S | Disable unused LVGL widgets/features/decoders to shrink `LvglCoreLib` code; the renderer only needs label/rect/dropdown/checkbox/textarea/list/canvas/snapshot. |
| [ ] | Canvas footprint policy | M | For memory-tight platforms, options: keep full ARGB8888 (simplest), or a region-limited canvas. Decide a per-platform DXEFV budget and whether the 4 MB BS-pool canvas is acceptable there. |
| [ ] | Sustained-navigation soak | S | Drive a long key sequence (scroll, open/close popups, edit) and confirm no pool-exhaustion / leak (every `AllocatePool` has a matching `FreePool`). |

**Acceptance:** RELEASE DXEFV within each platform's budget; no allocation
failures or leaks under sustained navigation.

## Gate 4 — Resolution & robustness

**Current:** canvas sizes to the active GOP mode and re-inits on change; guards
skip draws when a region is too small. No mode-matrix validation, no defined
GOP-absent path.

| ✓ | Step | Effort | Note |
| --- | --- | --- | --- |
| [ ] | Resolution matrix | M | Validate 1024×768, 1280×800, 1920×1080, and a small mode (e.g. 800×600). Confirm chrome/rows/right-rail/popups/watermark scale and the size guards behave. |
| [ ] | Re-init correctness on mode change | S | Verify the `mCanvasW != Context->Width` re-init path frees+reallocates cleanly and the first post-change frame is correct. |
| [ ] | GOP-absent / degenerate fallback | M | Define behavior when GOP is unavailable or the mode is below the minimum usable size: the engine should degrade gracefully (skip modern draw, let native text render) rather than blank the screen. |

**Acceptance:** correct rendering across the matrix; graceful degradation with no
GOP or an unusably small mode.

## Gate 5 — Interaction completeness & polish

| ✓ | Item | Effort | Status |
| --- | --- | --- | --- |
| [x] | Value opcodes → real widgets (one-of/checkbox/numeric/string/password/ordered-list/date-time) | — | Done (PR #40). |
| [x] | Confirm/error/selectable popups: full text, modern panel, no box-draw seam | — | Done. |
| [x] | Clean selection styling + localized chrome | — | Done. |
| [ ] | Date/time + ordered-list **editing** affordances (display is done; editing is native) | M | Confirm the native edit paths render acceptably; optionally elevate. |
| [ ] | Multi-line help / long-form text rendering audit | S | Confirm wrap/scroll in help and long values. |
| [ ] | Optional dialog elevation (accent title / Y-N affordances) | S | Cosmetic; defer. |

**Acceptance:** no interaction path falls back to a raw text-grid seam.

## Gate 6 — Validation & CI

| ✓ | Step | Effort | Note |
| --- | --- | --- | --- |
| [ ] | CI: lvgl build all four targets | M | Extend the workflow beyond smoke to compile lvgl overlays per target. |
| [ ] | Screendump regression | M | Golden screendumps for key forms (DriverSample, a popup, zh chrome) compared per change on OVMF X64 (the one target with a capture helper). |
| [ ] | **Hardware bring-up** | L | At least one real platform per arch — the dominant external dependency and the gate between "validated sample" and "shipped". |

## Sequenced roadmap

| Phase | Contents | Rough size |
| --- | --- | --- |
| **A** (now) | Gate 1 decisions + smoke/CI opt-in class; Gate 3 snapshot reuse + RELEASE baseline + lv_conf trim | days |
| **B** | Gate 2 CJK tier + size matching + zh validation | week+ |
| **C** | Gate 4 resolution matrix + GOP-absent fallback; Gate 5 remaining audits | week+ |
| **D** | Gate 6 CI extension + screendump regression | week+ |
| **E** | Gate 6 hardware bring-up, then flip default per validated platform | external |

## Decisions (locked 2026-06-08)

1. **Default backend → `lvgl`.** The maintainer chose to make LVGL the default
backend (overriding the conservative "GOP-default until hardware-proven"
recommendation). This raises real prerequisites before it is safe to flip the
bare default on every target: (a) `build-armvirt.sh` / `build-riscvvirt.sh`
accept only `{modern, native}` today — they need an lvgl branch; (b) the
default build would require `External/lvgl`, so `Scripts/bootstrap-edk2.sh`
must init that submodule; (c) the hardware gate (Gate 6) still stands. So the
default flip is sequenced *after* all-target lvgl support + CI.
2. **CJK coverage → tiered.** Demand-driven Noto subset for the package's own UI
(done) + an optional **GB2312 L1** subset behind a PCD for platforms needing
arbitrary coverage + HII fallback last.
3. **Per-platform DXEFV budget** — open; measure RELEASE baselines (Gate 3) then
decide whether the ~4 MB BS-pool canvas is acceptable on the tightest target.
4. **Hardware availability** — open; which real boards exist per arch (Gate 6).

## Progress log

- 2026-06-08: Gate 1 started — smoke now CI-gates the lvgl overlay on ovmf-x64 +
loongarch (the targets that accept `lvgl` today). Remaining Gate 1: add lvgl
support to armvirt + riscvvirt, de-spike framing, then flip the default with
the bootstrap submodule init.
48 changes: 36 additions & 12 deletions Scripts/build-armvirt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export PATH="/opt/homebrew/bin:/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/lld/
export CLANGDWARF_BIN="${CLANGDWARF_BIN:-/opt/homebrew/opt/llvm/bin/}"
export WORKSPACE
ConfigureModernSetupPackagePath
# Experimental/ hosts LvglSpikePkg, consumed only by MODERN_SETUP_DISPLAY_ENGINE=lvgl.
AppendPackagePath "${PKG_DIR}/Experimental"
export PACKAGES_PATH

if [[ ! -d "${WORKSPACE}/MdePkg" || ! -d "${WORKSPACE}/ArmVirtPkg" ]]; then
echo "WORKSPACE does not look like an edk2 checkout: ${WORKSPACE}" >&2
Expand Down Expand Up @@ -58,9 +61,9 @@ if theme_pcd is None:
f"Unsupported MODERN_SETUP_THEME={theme_name!r}; "
"use orange, amber, dark-orange, red, accent-red, dark-red, graphite, or graphite-gold"
)
if display_engine not in {"modern", "native"}:
if display_engine not in {"modern", "native", "lvgl"}:
raise SystemExit(
f"Unsupported MODERN_SETUP_DISPLAY_ENGINE={display_engine!r}; use modern or native"
f"Unsupported MODERN_SETUP_DISPLAY_ENGINE={display_engine!r}; use modern, native, or lvgl"
)
if replace_uiapp_flag not in {"0", "1", "false", "true", "no", "yes"}:
raise SystemExit(
Expand All @@ -74,14 +77,33 @@ modern_setup_app_component_boot_manager_fallback = """ ModernSetupPkg/Applicati
}"""
modern_setup_app_uiapp_fdf_inf = " INF RuleOverride = MODERN_SETUP_UIAPP ModernSetupPkg/Application/ModernSetupApp/ModernSetupApp.inf"
modern_display_component = " ModernSetupPkg/Universal/ModernDisplayEngineDxe/ModernDisplayEngineDxe.inf"
# In lvgl mode the ModernDisplayEngine force-links compiler intrinsics
# (memcpy/memset) pulled by the LVGL software draw pipeline.
modern_display_component_lvgl = (
" ModernSetupPkg/Universal/ModernDisplayEngineDxe/ModernDisplayEngineDxe.inf {\n"
" <LibraryClasses>\n"
" NULL|CryptoPkg/Library/IntrinsicLib/IntrinsicLib.inf\n"
" }"
)
modern_display_fdf_inf = " INF ModernSetupPkg/Universal/ModernDisplayEngineDxe/ModernDisplayEngineDxe.inf"
driver_sample_component = " MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf"
driver_sample_fdf_inf = " INF MdeModulePkg/Universal/DriverSampleDxe/DriverSampleDxe.inf"
library_block = """ ModernUiEngineLib|ModernSetupPkg/Library/ModernUiEngineLib/ModernUiEngineLib.inf
ModernUiRendererLib|ModernSetupPkg/Library/ModernUiRendererLib/ModernUiRendererLib.inf
ModernUiThemeLib|ModernSetupPkg/Library/ModernUiThemeLib/ModernUiThemeLib.inf
ModernUiStringLib|ModernSetupPkg/Library/ModernUiStringLib/ModernUiStringLib.inf
"""
# LVGL core (upstream lvgl sources as a BASE library); resolved only in lvgl mode
# and consumed transitively through the LVGL renderer library.
lvgl_library_block = " LvglCoreLib|LvglSpikePkg/Library/LvglLib/LvglCoreLib.inf\n"
# The renderer library class resolves to the LVGL-backed implementation in lvgl
# mode and to the hand-rolled GOP rasterizer otherwise (identical API).
renderer_inf = (
"ModernSetupPkg/Library/ModernUiLvglRendererLib/ModernUiRendererLib.inf"
if display_engine == "lvgl"
else "ModernSetupPkg/Library/ModernUiRendererLib/ModernUiRendererLib.inf"
)
library_block = (
" ModernUiEngineLib|ModernSetupPkg/Library/ModernUiEngineLib/ModernUiEngineLib.inf\n"
f" ModernUiRendererLib|{renderer_inf}\n"
" ModernUiThemeLib|ModernSetupPkg/Library/ModernUiThemeLib/ModernUiThemeLib.inf\n"
" ModernUiStringLib|ModernSetupPkg/Library/ModernUiStringLib/ModernUiStringLib.inf\n"
)
app_library_block = """ ModernUiPlatformDataLib|ModernSetupPkg/Library/ModernUiPlatformDataLib/ModernUiPlatformDataLib.inf
ModernUiBootDataLib|ModernSetupPkg/Library/ModernUiBootDataLib/ModernUiBootDataLib.inf
ModernUiDeviceDataLib|ModernSetupPkg/Library/ModernUiDeviceDataLib/ModernUiDeviceDataLib.inf
Expand Down Expand Up @@ -118,8 +140,10 @@ dsc = dsc.replace(
" FLASH_DEFINITION = Build/ModernSetupPkgOverlay/ArmVirtQemuModernSetup.fdf",
)
if "ModernUiEngineLib|ModernSetupPkg" not in dsc:
if display_engine == "modern" or replace_uiapp:
if (display_engine == "modern" or display_engine == "lvgl") or replace_uiapp:
dsc = dsc.replace("[LibraryClasses.common]\n", "[LibraryClasses.common]\n" + library_block, 1)
if display_engine == "lvgl" and "LvglCoreLib|LvglSpikePkg" not in dsc:
dsc = dsc.replace("[LibraryClasses.common]\n", "[LibraryClasses.common]\n" + lvgl_library_block, 1)
if replace_uiapp and "ModernUiPlatformDataLib|ModernSetupPkg" not in dsc:
dsc = dsc.replace("[LibraryClasses.common]\n", "[LibraryClasses.common]\n" + app_library_block, 1)
if replace_uiapp:
Expand All @@ -129,15 +153,15 @@ if replace_uiapp:
modern_setup_app_component_boot_manager_fallback + "\n",
"UiApp DSC component",
)
if display_engine == "modern":
if (display_engine == "modern" or display_engine == "lvgl"):
dsc = dsc.replace(
" CustomizedDisplayLib|MdeModulePkg/Library/CustomizedDisplayLib/CustomizedDisplayLib.inf",
" CustomizedDisplayLib|ModernSetupPkg/Library/ModernUiCustomizedDisplayLib/ModernUiCustomizedDisplayLib.inf",
1,
)
dsc = dsc.replace(
" MdeModulePkg/Universal/DisplayEngineDxe/DisplayEngineDxe.inf",
modern_display_component,
modern_display_component_lvgl if display_engine == "lvgl" else modern_display_component,
1,
)
if enable_driver_sample and driver_sample_component not in dsc:
Expand All @@ -153,7 +177,7 @@ if enable_driver_sample and driver_sample_component not in dsc:
driver_sample_component + "\n MdeModulePkg/Application/UiApp/UiApp.inf {",
1,
)
if display_engine == "modern":
if (display_engine == "modern" or display_engine == "lvgl"):
dsc += (
"\n[PcdsFixedAtBuild]\n"
f" gModernSetupPkgTokenSpaceGuid.PcdModernSetupTheme|{theme_pcd}\n"
Expand All @@ -171,7 +195,7 @@ fdf = fdf.replace(

fv = (workspace / "ArmVirtPkg/ArmVirtQemuFvMain.fdf.inc").read_text()
fv = fv.replace("!include ArmVirtRules.fdf.inc", "!include ArmVirtPkg/ArmVirtRules.fdf.inc")
if display_engine == "modern":
if (display_engine == "modern" or display_engine == "lvgl"):
fv = fv.replace(
" INF MdeModulePkg/Universal/DisplayEngineDxe/DisplayEngineDxe.inf",
modern_display_fdf_inf,
Expand Down
Loading
Loading