From abbbdc5ae5266990da320d03c73c8eda56a885c8 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:30:45 +0200 Subject: [PATCH] perf(l2): cheaper frames -- less overdraw and blending Measured on the L2 (steady full frames): Settings page 37.7 -> 31.4 ms, Display 41.5 -> 37.9, Contacts 34.6 -> 32.9, Home 37.3 -> 32.4. - styleSurface() leaves boxes in the page colour unpainted; styleOpaque() for the screen and the status bar, which things slide under. - group() cards clip their corners only while a row is held (rowPressable(), shared by the tappable rows). - rowSlider(): the track an opaque tint (theme::mix(), also behind ACCENT_DIM) instead of 20 % accent blended every frame. - Map buttons, pills, scale, nav bar and the Home minimap labels opaque. - Screens drift in without the fading cover: its full-screen blend was in every entrance frame (page 39 -> 29 ms). Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/ui-lvgl/Anim.h | 18 ++--- examples/companion_radio/ui-lvgl/BotScreen.h | 4 +- examples/companion_radio/ui-lvgl/HomeScreen.h | 4 +- examples/companion_radio/ui-lvgl/MapScreen.h | 7 +- examples/companion_radio/ui-lvgl/NavMap.h | 2 +- examples/companion_radio/ui-lvgl/Theme.h | 17 +++-- examples/companion_radio/ui-lvgl/UITask.cpp | 73 ++++++++++++------- examples/companion_radio/ui-lvgl/WifiScreen.h | 4 +- 8 files changed, 72 insertions(+), 57 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/Anim.h b/examples/companion_radio/ui-lvgl/Anim.h index 1b3ca1ae..1837ffe0 100644 --- a/examples/companion_radio/ui-lvgl/Anim.h +++ b/examples/companion_radio/ui-lvgl/Anim.h @@ -47,19 +47,13 @@ namespace anim { if (panel) rise(panel); } - // A new screen emerges: a cover in the background colour fades away over - // it while its content drifts a few pixels into place (up going deeper, - // down coming back). A plain fill blended on top -- far cheaper than - // fading the screen itself (a full-screen layer) or sliding two screens. + // A fading cover's end: it goes. static void coverDone(lv_anim_t* a) { lv_obj_delete((lv_obj_t*)a->var); } - static void screenIn(lv_obj_t* scr, lv_obj_t* body, bool back) { - lv_obj_t* cover = lv_obj_create(scr); - lv_obj_remove_style_all(cover); - lv_obj_remove_flag(cover, LV_OBJ_FLAG_CLICKABLE); // taps go through to the screen - lv_obj_add_flag(cover, LV_OBJ_FLAG_IGNORE_LAYOUT); - lv_obj_set_size(cover, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_bg_color(cover, lv_obj_get_style_bg_color(scr, LV_PART_MAIN), 0); - run(cover, setBgOpa, LV_OPA_COVER, LV_OPA_TRANSP, SCREEN_MS, coverDone); + + // A new screen emerges: its content drifts a few pixels into place (up + // going deeper, down coming back). No fade: a cover fading over the screen + // blended a full-screen fill into every frame of it (~7 ms each on the L2). + static void screenIn(lv_obj_t* body, bool back) { if (body) run(body, setTy, back ? -6 : 6, 0, SCREEN_MS); } diff --git a/examples/companion_radio/ui-lvgl/BotScreen.h b/examples/companion_radio/ui-lvgl/BotScreen.h index bb706267..d25cac29 100644 --- a/examples/companion_radio/ui-lvgl/BotScreen.h +++ b/examples/companion_radio/ui-lvgl/BotScreen.h @@ -124,9 +124,7 @@ void UITask::buildBot() { lv_obj_set_width(v, botcfg::isHour(r.kind) ? 60 : 150); // fixed: LONG_DOT needs a width lv_obj_set_style_text_align(v, LV_TEXT_ALIGN_RIGHT, 0); label(row, LV_SYMBOL_RIGHT, THEME_FONT_SMALL, theme::TEXT_MUTED); - lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); - lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED); + rowPressable(row); lv_obj_add_event_cb(row, onBotRow, LV_EVENT_CLICKED, (void*)(uintptr_t)i); } } diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index 4973e616..98214d71 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -600,7 +600,7 @@ void UITask::buildHomeMap(lv_obj_t* box) { s_fitted = false; s_hint = label(s_area, "", THEME_FONT_SMALL, theme::TEXT); lv_obj_set_style_bg_color(s_hint, lv_color_hex(theme::BG), 0); - lv_obj_set_style_bg_opa(s_hint, LV_OPA_80, 0); + lv_obj_set_style_bg_opa(s_hint, LV_OPA_COVER, 0); // opaque over the tiles, as the map's pills lv_obj_set_style_radius(s_hint, theme::RADIUS_SM, 0); lv_obj_set_style_pad_hor(s_hint, 8, 0); lv_obj_set_style_pad_ver(s_hint, 4, 0); @@ -609,7 +609,7 @@ void UITask::buildHomeMap(lv_obj_t* box) { lv_obj_add_flag(s_hint, LV_OBJ_FLAG_HIDDEN); s_caption = label(s_area, "", THEME_FONT_SMALL, theme::TEXT); // who's on it lv_obj_set_style_bg_color(s_caption, lv_color_hex(theme::BG), 0); - lv_obj_set_style_bg_opa(s_caption, LV_OPA_80, 0); + lv_obj_set_style_bg_opa(s_caption, LV_OPA_COVER, 0); lv_obj_set_style_radius(s_caption, theme::RADIUS_SM, 0); lv_obj_set_style_pad_hor(s_caption, 6, 0); lv_obj_set_style_pad_ver(s_caption, 2, 0); diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index 12b231a5..95964620 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -259,7 +259,7 @@ static lv_obj_t* mapButton(lv_obj_t* parent, const char* text, lv_event_cb_t cb) lv_obj_set_style_radius(b, theme::RADIUS, 0); lv_obj_set_style_shadow_width(b, 0, 0); lv_obj_set_style_bg_color(b, lv_color_hex(theme::BG), 0); - lv_obj_set_style_bg_opa(b, LV_OPA_80, 0); + lv_obj_set_style_bg_opa(b, LV_OPA_COVER, 0); // opaque: blended over the tiles, each cost ~1.6 ms a frame lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); lv_obj_set_style_border_color(b, lv_color_hex(theme::SURFACE_2), 0); lv_obj_set_style_border_width(b, 1, 0); @@ -271,7 +271,7 @@ static lv_obj_t* mapButton(lv_obj_t* parent, const char* text, lv_event_cb_t cb) static lv_obj_t* mapPill(lv_obj_t* parent, const char* text) { lv_obj_t* l = label(parent, text, THEME_FONT_SMALL, theme::TEXT); lv_obj_set_style_bg_color(l, lv_color_hex(theme::BG), 0); - lv_obj_set_style_bg_opa(l, LV_OPA_70, 0); + lv_obj_set_style_bg_opa(l, LV_OPA_COVER, 0); lv_obj_set_style_pad_hor(l, 5, 0); lv_obj_set_style_pad_ver(l, 1, 0); lv_obj_set_style_radius(l, 4, 0); @@ -344,6 +344,7 @@ void UITask::buildMap() { lv_obj_set_style_pad_all(body, 0, 0); lv_obj_remove_flag(body, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(body, lv_color_hex(0x1A1A1E), 0); // unloaded / missing tiles + lv_obj_set_style_bg_opa(body, LV_OPA_COVER, 0); lv_obj_add_event_cb(body, mapview::drawGrid, LV_EVENT_DRAW_MAIN_END, &mapview::s_grid); lv_obj_add_flag(body, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(body, onMapPress, LV_EVENT_PRESSED, NULL); @@ -426,7 +427,7 @@ void UITask::buildMap() { lv_obj_remove_flag(sc, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_size(sc, LV_SIZE_CONTENT, LV_SIZE_CONTENT); lv_obj_set_style_bg_color(sc, lv_color_hex(theme::BG), 0); - lv_obj_set_style_bg_opa(sc, LV_OPA_70, 0); + lv_obj_set_style_bg_opa(sc, LV_OPA_COVER, 0); lv_obj_set_style_radius(sc, 4, 0); lv_obj_set_style_pad_hor(sc, 5, 0); lv_obj_set_style_pad_ver(sc, 3, 0); diff --git a/examples/companion_radio/ui-lvgl/NavMap.h b/examples/companion_radio/ui-lvgl/NavMap.h index 4672d4bc..b4a9c69e 100644 --- a/examples/companion_radio/ui-lvgl/NavMap.h +++ b/examples/companion_radio/ui-lvgl/NavMap.h @@ -167,7 +167,7 @@ void UITask::buildNavControls(lv_obj_t* body) { lv_obj_set_style_radius(_nav_bar, 0, 0); lv_obj_set_style_shadow_width(_nav_bar, 0, 0); lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(theme::BG), 0); - lv_obj_set_style_bg_opa(_nav_bar, LV_OPA_90, 0); + lv_obj_set_style_bg_opa(_nav_bar, LV_OPA_COVER, 0); // opaque, as the map buttons lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); lv_obj_set_style_border_side(_nav_bar, LV_BORDER_SIDE_TOP, 0); lv_obj_set_style_border_color(_nav_bar, lv_color_hex(theme::SURFACE_2), 0); diff --git a/examples/companion_radio/ui-lvgl/Theme.h b/examples/companion_radio/ui-lvgl/Theme.h index 3b65ffc3..ed3a99df 100644 --- a/examples/companion_radio/ui-lvgl/Theme.h +++ b/examples/companion_radio/ui-lvgl/Theme.h @@ -66,16 +66,21 @@ namespace theme { }; static const int ACCENT_COUNT = sizeof(ACCENTS) / sizeof(ACCENTS[0]); static int s_accent = 0; + // `fg` at `pct` % over `bg`, worked out once: an opaque fill in the colour + // a translucent one would blend to, without blending it every frame. + static uint32_t mix(uint32_t fg, uint32_t bg, uint32_t pct) { + uint32_t out = 0; + for (int sh = 0; sh <= 16; sh += 8) { + uint32_t c = (fg >> sh) & 0xFF, b = (bg >> sh) & 0xFF; + out |= ((c * pct + b * (100 - pct)) / 100) << sh; + } + return out; + } static void setAccent(int idx) { if (idx < 0 || idx >= ACCENT_COUNT) idx = 0; s_accent = idx; ACCENT = ACCENTS[idx].col; - uint32_t dim = 0; - for (int sh = 0; sh <= 16; sh += 8) { - uint32_t c = (ACCENT >> sh) & 0xFF, b = (BG >> sh) & 0xFF; - dim |= ((c * 42 + b * 58) / 100) << sh; - } - ACCENT_DIM = dim; + ACCENT_DIM = mix(ACCENT, BG, 42); } // Over LVGL's default theme, for every widget: a checked button is diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index e0ceec15..6ef23b64 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -119,13 +119,21 @@ extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_get_volume() { return s_ui ? (int // ── Small helpers ───────────────────────────────────────────────────────────── +// A plain box in `bg`. One in the page colour is left unpainted: the screen +// under it (or the popup panel) already is that colour, and filling it again +// costs a full-area blend per frame. The screen and the status bar, which +// something may scroll or slide under, are made opaque with styleOpaque(). static void styleSurface(lv_obj_t* o, uint32_t bg) { lv_obj_set_style_bg_color(o, lv_color_hex(bg), 0); - lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0); + lv_obj_set_style_bg_opa(o, bg == theme::BG ? LV_OPA_TRANSP : LV_OPA_COVER, 0); lv_obj_set_style_border_width(o, 0, 0); lv_obj_set_style_radius(o, 0, 0); lv_obj_set_style_pad_all(o, 0, 0); } +static void styleOpaque(lv_obj_t* o, uint32_t bg) { + styleSurface(o, bg); + lv_obj_set_style_bg_opa(o, LV_OPA_COVER, 0); +} static lv_obj_t* label(lv_obj_t* parent, const char* text, const lv_font_t* font, uint32_t color) { lv_obj_t* l = lv_label_create(parent); @@ -341,11 +349,26 @@ static lv_obj_t* group(lv_obj_t* parent, const char* title) { if (title) sectionTitle(parent, title); lv_obj_t* c = infoCard(parent); lv_obj_set_style_pad_hor(c, 0, 0); // rows pad themselves: a pressed row lights edge to edge - lv_obj_set_style_clip_corner(c, true, 0); lv_obj_add_flag(c, LV_OBJ_FLAG_USER_1); return c; } +// A row that lights up under the finger. In a group() the card clips its +// corners while a row is held, so the first / last row's light is rounded +// with it -- only then: clipping a card's corners costs a few ms every frame. +static void groupPressClip(lv_event_t* e) { + lv_obj_t* card = lv_obj_get_parent((lv_obj_t*)lv_event_get_current_target(e)); + if (isGroup(card)) lv_obj_set_style_clip_corner(card, lv_event_get_code(e) == LV_EVENT_PRESSED, 0); +} +static void rowPressable(lv_obj_t* row) { + lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); + lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); + lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED); + lv_obj_add_event_cb(row, groupPressClip, LV_EVENT_PRESSED, NULL); + lv_obj_add_event_cb(row, groupPressClip, LV_EVENT_RELEASED, NULL); + lv_obj_add_event_cb(row, groupPressClip, LV_EVENT_PRESS_LOST, NULL); +} + static lv_obj_t* groupNote(lv_obj_t* parent, const char* text) { lv_obj_t* l = noteLabel(parent, text); lv_obj_set_style_pad_hor(l, theme::PAD, 0); @@ -362,11 +385,7 @@ static lv_obj_t* groupLine(lv_obj_t* card, bool tappable) { lv_obj_set_flex_flow(r, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(r, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(r, 8, 0); - if (tappable) { - lv_obj_add_flag(r, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_style_bg_color(r, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); - lv_obj_set_style_bg_opa(r, LV_OPA_COVER, LV_STATE_PRESSED); - } + if (tappable) rowPressable(r); return r; } @@ -423,6 +442,20 @@ static lv_obj_t* settingRow(lv_obj_t* parent, const char* text, const char* hint return row; } +// A slider on the right of a settingRow(). Its track is the theme's 20 % +// accent tint worked out over the card, opaque: blended, it cost ~3 ms a frame. +static lv_obj_t* rowSlider(lv_obj_t* row, int32_t min, int32_t max, int32_t v) { + lv_obj_t* sl = lv_slider_create(row); + lv_slider_set_range(sl, min, max); + lv_slider_set_value(sl, v, LV_ANIM_OFF); + lv_obj_set_size(sl, 150, 8); + lv_obj_set_style_margin_right(sl, 8, 0); + lv_obj_set_ext_click_area(sl, 14); + lv_obj_set_style_bg_color(sl, lv_color_hex(theme::mix(theme::ACCENT, theme::SURFACE, 20)), LV_PART_MAIN); + lv_obj_set_style_bg_opa(sl, LV_OPA_COVER, LV_PART_MAIN); + return sl; +} + // Does something at once (Reboot, Delete ...): its text in `col`, no chevron. // Returns the label, for a confirm-by-second-tap text. static lv_obj_t* actionRow(lv_obj_t* card, const char* text, lv_event_cb_t cb, void* user, @@ -1458,7 +1491,7 @@ void UITask::buildStatusBar() { lv_obj_set_style_text_align(tl, LV_TEXT_ALIGN_CENTER, 0); lv_obj_add_flag(_toast, LV_OBJ_FLAG_HIDDEN); lv_obj_t* bar = lv_obj_create(lv_layer_top()); - styleSurface(bar, theme::BG); + styleOpaque(bar, theme::BG); lv_obj_remove_flag(bar, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(bar, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_size(bar, LV_PCT(100), theme::STATUS_H); @@ -1788,7 +1821,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { for (lv_obj_t*& t : _map_tiles) t = nullptr; lv_obj_t* prev = _scr; lv_obj_t* scr = lv_obj_create(NULL); - styleSurface(scr, theme::BG); + styleOpaque(scr, theme::BG); lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE); int top = theme::STATUS_H; @@ -1839,7 +1872,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) { } else { lv_screen_load(scr); if (prev) lv_obj_delete_async(prev); // this usually runs from one of its own widgets - if (prev && !same && !_asleep) anim::screenIn(scr, body, backward); + if (prev && !same && !_asleep) anim::screenIn(body, backward); } _fade_next = false; return body; @@ -3568,26 +3601,14 @@ void UITask::schemaRow(lv_obj_t* card, int i) { const settings::Setting& st = settings::ALL[i]; uint8_t v = settings::get(*_prefs, st); if (st.offset == offsetof(NodePrefs, buzzer_volume)) { // a five-step slider, heard on release - lv_obj_t* row = settingRow(card, st.label, NULL); - lv_obj_t* sl = lv_slider_create(row); - lv_slider_set_range(sl, 0, 4); - lv_slider_set_value(sl, v, LV_ANIM_OFF); - lv_obj_set_size(sl, 150, 8); - lv_obj_set_style_margin_right(sl, 8, 0); - lv_obj_set_ext_click_area(sl, 14); + lv_obj_t* sl = rowSlider(settingRow(card, st.label, NULL), 0, 4, v); lv_obj_add_event_cb(sl, onVolumeSlider, LV_EVENT_RELEASED, NULL); return; } if (st.offset == offsetof(NodePrefs, display_brightness)) { // a slider here instead of five steps - lv_obj_t* row = settingRow(card, st.label, NULL); - lv_obj_t* sl = lv_slider_create(row); - lv_slider_set_range(sl, 5, 100); uint8_t pct = _prefs->display_brightness_pct ? _prefs->display_brightness_pct : (uint8_t)(_prefs->display_brightness * 25 > 5 ? _prefs->display_brightness * 25 : 5); - lv_slider_set_value(sl, pct, LV_ANIM_OFF); - lv_obj_set_size(sl, 150, 8); - lv_obj_set_style_margin_right(sl, 8, 0); - lv_obj_set_ext_click_area(sl, 14); + lv_obj_t* sl = rowSlider(settingRow(card, st.label, NULL), 5, 100, pct); lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_add_event_cb(sl, onBrightnessSlider, LV_EVENT_RELEASED, NULL); return; @@ -3692,9 +3713,7 @@ void UITask::settingsGroup(int i) { if (_core->gpsEnabled()) lv_obj_add_state(sw, LV_STATE_CHECKED); lv_obj_add_event_cb(sw, onGpsSwitch, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_t* row = lv_obj_get_parent(sw); - lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); - lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED); + rowPressable(row); lv_obj_add_event_cb(row, [](lv_event_t* e) { if (lv_event_get_target(e) == lv_event_get_current_target(e)) onOpenGpsFromSettings(e); // not the switch }, LV_EVENT_CLICKED, NULL); diff --git a/examples/companion_radio/ui-lvgl/WifiScreen.h b/examples/companion_radio/ui-lvgl/WifiScreen.h index 35f79a25..e3833a9e 100644 --- a/examples/companion_radio/ui-lvgl/WifiScreen.h +++ b/examples/companion_radio/ui-lvgl/WifiScreen.h @@ -181,9 +181,7 @@ static void wifiRow(lv_obj_t* body) { if (lvport::wifiAllowed()) lv_obj_add_state(sw, LV_STATE_CHECKED); lv_obj_add_event_cb(sw, onWifiAllowed, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_t* row = lv_obj_get_parent(sw); - lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_style_bg_color(row, lv_color_hex(theme::SURFACE_2), LV_STATE_PRESSED); - lv_obj_set_style_bg_opa(row, LV_OPA_COVER, LV_STATE_PRESSED); + rowPressable(row); lv_obj_add_event_cb(row, [](lv_event_t* e) { if (lv_event_get_target(e) == lv_event_get_current_target(e)) s_ui->showWifi(); // not the switch }, LV_EVENT_CLICKED, NULL);