diff --git a/Docs/LvglProductizationPlan.md b/Docs/LvglProductizationPlan.md new file mode 100644 index 0000000..65e1f9f --- /dev/null +++ b/Docs/LvglProductizationPlan.md @@ -0,0 +1,163 @@ + + +# 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. diff --git a/Scripts/build-armvirt.sh b/Scripts/build-armvirt.sh index 4cdfad0..300185b 100755 --- a/Scripts/build-armvirt.sh +++ b/Scripts/build-armvirt.sh @@ -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 @@ -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( @@ -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" + " \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 @@ -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: @@ -129,7 +153,7 @@ 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", @@ -137,7 +161,7 @@ if display_engine == "modern": ) 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: @@ -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" @@ -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, diff --git a/Scripts/build-riscvvirt.sh b/Scripts/build-riscvvirt.sh index 53afd1f..fc51c30 100755 --- a/Scripts/build-riscvvirt.sh +++ b/Scripts/build-riscvvirt.sh @@ -24,6 +24,9 @@ OVERLAY_DIR="${WORKSPACE}/Build/ModernSetupPkgOverlay" export WORKSPACE export GCC_RISCV64_PREFIX 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}/OvmfPkg/RiscVVirt" ]]; then echo "WORKSPACE does not look like an edk2 checkout with MdePkg and OvmfPkg/RiscVVirt: ${WORKSPACE}" >&2 @@ -63,9 +66,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( @@ -79,14 +82,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" + " \n" + " NULL|CryptoPkg/Library/IntrinsicLib/IntrinsicLib.inf\n" + " }" +) modern_display_fdf_inf = "INF ModernSetupPkg/Universal/ModernDisplayEngineDxe/ModernDisplayEngineDxe.inf" boot_manager_menu_component = " MdeModulePkg/Application/BootManagerMenuApp/BootManagerMenuApp.inf" boot_manager_menu_fdf_inf = "INF MdeModulePkg/Application/BootManagerMenuApp/BootManagerMenuApp.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 @@ -129,7 +151,7 @@ dsc = replace_regex_once( r"\1Build/ModernSetupPkgOverlay/RiscVVirtQemuModernSetup.fdf", "FLASH_DEFINITION", ) -if display_engine == "modern" or replace_uiapp: +if (display_engine == "modern" or display_engine == "lvgl") or replace_uiapp: if "ModernUiEngineLib|ModernSetupPkg" not in dsc: if "[LibraryClasses.common]\n" in dsc: dsc = replace_once(dsc, "[LibraryClasses.common]\n", "[LibraryClasses.common]\n" + library_block, "LibraryClasses.common") @@ -137,6 +159,11 @@ if display_engine == "modern" or replace_uiapp: dsc = replace_once(dsc, "[LibraryClasses]\n", "[LibraryClasses]\n" + library_block, "LibraryClasses") else: raise SystemExit("Expected LibraryClasses anchor: [LibraryClasses.common] or [LibraryClasses]") +if display_engine == "lvgl" and "LvglCoreLib|LvglSpikePkg" not in dsc: + if "[LibraryClasses.common]\n" in dsc: + dsc = replace_once(dsc, "[LibraryClasses.common]\n", "[LibraryClasses.common]\n" + lvgl_library_block, "LibraryClasses.common for lvgl") + elif "[LibraryClasses]\n" in dsc: + dsc = replace_once(dsc, "[LibraryClasses]\n", "[LibraryClasses]\n" + lvgl_library_block, "LibraryClasses for lvgl") if replace_uiapp and "ModernUiPlatformDataLib|ModernSetupPkg" not in dsc: if "[LibraryClasses.common]\n" in dsc: dsc = replace_once(dsc, "[LibraryClasses.common]\n", "[LibraryClasses.common]\n" + app_library_block, "LibraryClasses.common for app") @@ -158,7 +185,7 @@ if replace_uiapp: boot_manager_menu_component + r"\n\1", "QemuKernelLoaderFsDxe component", ) -if display_engine == "modern": +if (display_engine == "modern" or display_engine == "lvgl"): dsc = replace_regex_once( dsc, r"^\s*CustomizedDisplayLib\s*\|\s*MdeModulePkg/Library/CustomizedDisplayLib/CustomizedDisplayLib\.inf\s*$", @@ -168,7 +195,7 @@ if display_engine == "modern": dsc = replace_regex_once( dsc, r"^\s*MdeModulePkg/Universal/DisplayEngineDxe/DisplayEngineDxe\.inf\s*$", - modern_display_component, + modern_display_component_lvgl if display_engine == "lvgl" else modern_display_component, "DisplayEngineDxe component", ) dsc += ( @@ -180,7 +207,7 @@ if display_engine == "modern": fdf = (workspace / "OvmfPkg/RiscVVirt/RiscVVirtQemu.fdf").read_text() fdf = replace_once(fdf, "!include RiscVVirt.fdf.inc", "!include OvmfPkg/RiscVVirt/RiscVVirt.fdf.inc", "RiscVVirt.fdf.inc include") fdf = replace_once(fdf, "!include VarStore.fdf.inc", "!include OvmfPkg/RiscVVirt/VarStore.fdf.inc", "VarStore.fdf.inc include") -if display_engine == "modern": +if (display_engine == "modern" or display_engine == "lvgl"): fdf = replace_regex_once( fdf, r"^\s*INF\s+MdeModulePkg/Universal/DisplayEngineDxe/DisplayEngineDxe\.inf\s*$", diff --git a/Tests/Smoke/smoke_validate.py b/Tests/Smoke/smoke_validate.py index 882f601..577f696 100755 --- a/Tests/Smoke/smoke_validate.py +++ b/Tests/Smoke/smoke_validate.py @@ -2551,15 +2551,23 @@ def check_overlay_generation(root: Path) -> list[str]: ovmf_x64_fixture(workspace) riscvvirt_fixture(workspace) + # supports_lvgl marks the targets whose build script accepts + # MODERN_SETUP_DISPLAY_ENGINE=lvgl today (ovmf-x64 + loongarch). The LVGL + # backend is the SAME ModernDisplayEngineDxe with the LVGL renderer + # library swapped in, so it is CI-gated as a supported overlay here while + # native/modern overlays still must never pull it in. cases = ( - ("armvirt", "build-armvirt.sh", "ArmVirtQemuModernSetup.dsc"), - ("loongarch", "build-loongarchvirt.sh", "LoongArchVirtQemuModernSetup.dsc"), - ("ovmf-x64", "build-ovmf-x64.sh", "OvmfX64ModernSetup.dsc"), - ("riscvvirt", "build-riscvvirt.sh", "RiscVVirtQemuModernSetup.dsc"), + ("armvirt", "build-armvirt.sh", "ArmVirtQemuModernSetup.dsc", True), + ("loongarch", "build-loongarchvirt.sh", "LoongArchVirtQemuModernSetup.dsc", True), + ("ovmf-x64", "build-ovmf-x64.sh", "OvmfX64ModernSetup.dsc", True), + ("riscvvirt", "build-riscvvirt.sh", "RiscVVirtQemuModernSetup.dsc", True), ) - for platform, script_name, dsc_name in cases: + for platform, script_name, dsc_name, supports_lvgl in cases: script = workspace / "ModernSetupPkg" / "Scripts" / script_name - for engine in ("native", "modern"): + engines = ["native", "modern"] + if supports_lvgl: + engines.append("lvgl") + for engine in engines: overlay_dir = workspace / "Build" / "ModernSetupPkgOverlay" if overlay_dir.exists(): shutil.rmtree(overlay_dir) @@ -2580,9 +2588,10 @@ def check_overlay_generation(root: Path) -> list[str]: if not generated_path.exists(): raise SmokeFailure(f"missing generated overlay file: {generated_path}") assert_not_contains_any(generated_path, PROHIBITED_DEFAULT_OVERLAY_TOKENS) - # The LVGL backend is experimental and lvgl-mode only; it must - # never leak into a default native/modern overlay. - assert_not_contains_any(generated_path, PROHIBITED_DEFAULT_OVERLAY_LVGL_TOKENS) + # The LVGL renderer is allowed only in the lvgl overlay; it + # must never leak into a native/modern (GOP) overlay. + if engine != "lvgl": + assert_not_contains_any(generated_path, PROHIBITED_DEFAULT_OVERLAY_LVGL_TOKENS) dsc = workspace / "Build" / "ModernSetupPkgOverlay" / dsc_name if engine == "modern": @@ -2590,6 +2599,13 @@ def check_overlay_generation(root: Path) -> list[str]: assert_contains(dsc, "gModernSetupPkgTokenSpaceGuid.PcdModernSetupTheme|0x02") if not any("ModernDisplayEngineDxe" in path.read_text(encoding="utf-8") for path in generated): raise SmokeFailure(f"{platform} modern overlay did not reference ModernDisplayEngineDxe") + elif engine == "lvgl": + # Same interaction backend as modern, with the LVGL renderer + # library and LVGL core resolved. + assert_contains(dsc, "ModernUiRendererLib|ModernSetupPkg/Library/ModernUiLvglRendererLib/ModernUiRendererLib.inf") + assert_contains(dsc, "LvglCoreLib|LvglSpikePkg/Library/LvglLib/LvglCoreLib.inf") + if not any("ModernDisplayEngineDxe" in path.read_text(encoding="utf-8") for path in generated): + raise SmokeFailure(f"{platform} lvgl overlay did not reference ModernDisplayEngineDxe") else: for generated_path in generated: assert_not_contains_any(