diff --git a/docs/audits/UI_STORAGE_HOT_PATH_BYPASS_AUDIT.md b/docs/audits/UI_STORAGE_HOT_PATH_BYPASS_AUDIT.md new file mode 100644 index 00000000..f1122bee --- /dev/null +++ b/docs/audits/UI_STORAGE_HOT_PATH_BYPASS_AUDIT.md @@ -0,0 +1,127 @@ +# UI Storage Hot-Path Bypass Audit + +Status date: 2026-06-16 + +This audit is the separate map / SD / UI burn-down cut requested after the ESP +UI freeze regressions. It lists active UI hot-path entries that can touch SD, +LVGL FS, or display SPI, then records whether each entry is already behind a +runtime/worker boundary or still needs deletion/consolidation. + +The rule from `UI_STORAGE_EVENT_RUNTIME_DESIGN_SPEC.md` is tightened here: + +```text +UI owner context may build value objects, update renderer objects, and submit +runtime intents. It must not synchronously open/read/write/list storage, call +LVGL FS for SD-backed files, or wait on a display-shared SPI resource. +``` + +## Entry Inventory + +| Entry | Resource touched | Current risk | Decision | +| --- | --- | --- | --- | +| `platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp` UI tile source | LVGL FS path was present in ESP map source | High confusion risk: ESP UI source appeared able to read through LVGL FS | Burned in this cut. ESP UI source is path-only; SD reads remain in the worker `SdMapTileFileSystem`. | +| `platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp` worker tile source | SD through `SdRuntimeFile` | Accepted only in worker domain | Keep. This is the map storage adapter behind the tile async runtime. | +| `platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp` tile decode | LVGL image decoder, CPU/memory work | High when executed from UI event drain | Burned from the UI hot path in this cut. The ESP map worker now publishes already-decoded native image descriptors after releasing the SD bus. | +| `modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp::locale_preview_font` | External font load via `lv_binfont_create` / LVGL FS | High. Locale/settings UI preview could synchronously load SD-backed font packs | Burned in this cut for ESP. Preview uses only already-loaded fonts on ESP. | +| `modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp::activate_locale_internal` | External UI font load | High on ESP if locale UI font is SD-backed | Burned from the ESP UI hot path in this cut. ESP defers unloaded external UI fonts and falls back to the builtin latin UI font chain. | +| `modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp` external pack scan | LVGL FS dir/file reads | Medium. Usually registry/startup path, but still synchronous | Remaining legacy. Needs pack catalog worker or startup preflight outside visible UI frame budget. | +| `modules/ui_shared/src/ui/screens/gps/gps_page_runtime.cpp` route loaders | `ui::fs::read_text_file` | Medium to high when user opens/loads a route | Remaining legacy. Move GPX/CSV parse into route/track runtime worker and return overlay points by event. | +| `platform/esp/arduino_common/src/ui/screens/team/team_ui_store.cpp` | SD exists/open/write/remove/rename | High. A UI screen store owns durable persistence | Remaining legacy. Move team snapshot/key/event/chat/position persistence behind `TEAM_ACTION_RUNTIME_SPEC` storage worker. | +| `platform/esp/arduino_common/src/ui/runtime/pack_repository.cpp` | Flash/LVGL FS open/dir/read/write | Medium. Pack management can block UI actions | Remaining legacy. Keep as explicit pack runtime adapter only after UI callers submit commands; remove direct page calls. | +| `modules/ui_shared/src/ui/widgets/busy_overlay.cpp` | `lv_refr_now` display flush | Medium if called while storage owns display-shared SPI | Burned in this cut. Busy overlay now invalidates and lets the normal UI tick render. | +| startup shells / boot log | `lv_refr_now` / manual `lv_timer_handler` display flush | Medium. Startup could bypass normal UI tick and contend with early SD/display setup | Burned in this cut. Startup paths now invalidate and let the normal UI tick render. | +| `platform/esp/arduino_common/src/LV_Helper_v9.cpp` | LVGL FS adapter to SD/flash | Adapter risk, not business risk | Keep as platform adapter. UI hot paths must not use it for SD-backed files. | +| `platform/esp/boards/src/display/DisplayInterface.cpp` | display SPI lock/flush | Frame-critical adapter | Keep. Other workers must yield to this domain through bounded wait, hold, and cooldown policy. | + +## Burned In This Cut + +### ESP map UI source no longer owns a storage path + +The ESP map file now has two explicit source roles: + +- UI path source: path-only, no `exists`, `isDirectory`, or `readFile` behavior. +- Worker source: SD-backed `SdMapTileFileSystem`, used by the async tile runtime. + +This removes the misleading ESP `LvglMapTileFileSystem` implementation from the +active map source file. Path resolution remains in `FilesystemMapTileSource`; +file reads stay in the worker adapter. + +### Locale preview cannot load external fonts on ESP + +`locale_preview_font()` no longer calls `ensure_font_pack_loaded()` on ESP. +Preview UI can compose with an external font only if it is already loaded. +Otherwise it falls back to the base font and returns immediately. + +This prevents settings/list rendering from repeatedly entering +`lv_binfont_create()` and LVGL FS just because Chinese text appeared in a preview. + +### Locale activation cannot synchronously load external UI fonts on ESP + +`activate_locale_internal()` no longer calls external `lv_binfont_create()` from +ESP activation/startup paths. If the selected locale points at an unloaded +external UI font pack, the locale remains active but the UI font chain falls back +to `builtin-latin-ui` and logs the external font as deferred. + +This prioritizes UI liveness over synchronous Chinese UI font availability. The +proper recovery path is an i18n font-load runtime command that loads the +external font outside the UI owner context and publishes a font-chain update +event. + +### Busy overlay no longer forces immediate display refresh + +`busy_overlay::show/update/set_progress/hide` no longer call `lv_refr_now()`. +They invalidate the overlay or active screen and let the normal UI tick perform +rendering. + +This keeps slow-operation feedback from directly competing with SD or other +display-shared SPI work in the same call stack. + +### Startup paths no longer force display refresh + +Boot log updates and startup shell boot presentation no longer call +`lv_refr_now()` or manually drive `lv_timer_handler()`. They invalidate the +affected layer/object and return. + +This removes startup display refresh re-entry from the same period where SD, +radio, font catalog, and display initialization can still be contending for +shared resources. + +### Map tile decode moved out of UI event drain + +The ESP map active path previously read tile bytes in the worker but decoded the +PNG/JPG payload in `apply_map_tile_event()` on the UI owner context. + +The ESP event sink now decodes the compressed payload before enqueueing the +event. The UI owner receives an already-decoded native image descriptor and only +adopts it into the decoded cache / LVGL image object. + +This removes the active UI call to `lv_image_decoder_open()`. The remaining +technical debt is that the worker-side decoder still uses LVGL's decoder API as +a platform decoder adapter. That is no longer a UI hot-path bypass, but a later +decoder-adapter cut should replace it with a decoder that is explicitly safe to +run outside the UI owner context. + +## Remaining Burn-Down Order + +1. Move i18n deferred external font activation into an event-driven font-load + runtime. Preserve Chinese UI by loading packs asynchronously and applying the + new font chain on a UI-owner event. +2. Move GPS GPX/CSV route loading into a route runtime worker. UI submits a path + and receives parsed/downsampled overlay points. +3. Move team UI persistence out of `platform/esp/.../ui/screens` into a storage + worker owned by the team action runtime. +4. Convert pack repository actions into commands/events. Keep LVGL FS and flash + APIs only in platform adapters. +5. Replace worker-side LVGL image decoding with a platform decoder adapter that + has no dependency on LVGL global decoder state. + +## Verification Gate For Each Follow-Up Cut + +Each follow-up must pass these checks before commit: + +- GitNexus impact analysis for every edited symbol. +- `rg` audit showing the edited UI hot path no longer calls SD, LVGL FS, or + display SPI primitives directly. +- A compile check for one ESP target. +- `gitnexus detect-changes` to confirm the affected surface matches the planned + runtime/worker slice. diff --git a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h index a4fae502..be884ec8 100644 --- a/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h +++ b/modules/ui_map_runtime/include/ui_map_runtime/map_tiles/map_tile_async_runtime.h @@ -44,6 +44,15 @@ struct LoadTileCommand using MapTileEventKind = MapTileAsyncEventKind; +struct MapTileDecodedImage +{ + void* native_handle = nullptr; + void (*release)(void*) = nullptr; + uint16_t width = 0; + uint16_t height = 0; + std::size_t data_size = 0; +}; + class MapTileEvent { public: @@ -53,6 +62,7 @@ class MapTileEvent MapTileRef tile{}; MapTileFormat format = MapTileFormat::Unknown; MapTilePayload payload{}; + MapTileDecodedImage decoded{}; std::size_t payload_size = 0; int32_t error = 0; }; diff --git a/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp index 339ee680..6f0114bd 100644 --- a/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp +++ b/modules/ui_shared/src/ui/i18n/resource_pack_registry.cpp @@ -2340,16 +2340,40 @@ bool activate_locale_internal(LocalePackRecord* locale, FontPackRecord* preserve ? find_pack_by_id(s_ime_packs, locale->ime_pack_id.c_str()) : nullptr; - if (s_active_ui_font_pack != nullptr && !ensure_font_pack_loaded(s_active_ui_font_pack)) + if (s_active_ui_font_pack != nullptr && !is_font_runtime_loaded(*s_active_ui_font_pack)) { - if (locale != nullptr && locale->id != kDefaultLocaleId) +#if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) + if (!s_active_ui_font_pack->builtin) { - LocalePackRecord* fallback = resolve_active_locale(kDefaultLocaleId); - if (fallback && fallback != locale) + std::printf("%s font load deferred id=%s role=active_ui reason=ui_activation active_locale=%s source=%s\n", + kLogTag, + s_active_ui_font_pack->id.c_str(), + s_active_locale ? s_active_locale->id.c_str() : "", + s_active_ui_font_pack->source_path.empty() + ? "" + : s_active_ui_font_pack->source_path.c_str()); + if (FontPackRecord* fallback_ui = find_pack_by_id(s_font_packs, kBuiltinLatinFontPackId)) { - return activate_locale_internal(fallback, preserved_content_pack); + s_active_ui_font_pack = fallback_ui; } } + else if (!ensure_font_pack_loaded(s_active_ui_font_pack)) + { + s_active_ui_font_pack = find_pack_by_id(s_font_packs, kBuiltinLatinFontPackId); + } +#else + if (!ensure_font_pack_loaded(s_active_ui_font_pack)) + { + if (locale != nullptr && locale->id != kDefaultLocaleId) + { + LocalePackRecord* fallback = resolve_active_locale(kDefaultLocaleId); + if (fallback && fallback != locale) + { + return activate_locale_internal(fallback, preserved_content_pack); + } + } + } +#endif } if (can_preserve_content_pack(preserved_content_pack, s_active_ui_font_pack, s_active_content_font_pack)) @@ -2582,11 +2606,23 @@ const lv_font_t* locale_preview_font(const char* locale_id, const lv_font_t* asc } FontPackRecord* ui_pack = find_pack_by_id(s_font_packs, locale->ui_font_pack_id.c_str()); - if (ui_pack == nullptr || !ensure_font_pack_loaded(ui_pack)) + if (ui_pack == nullptr) { return base_font; } +#if defined(ESP_PLATFORM) || defined(ARDUINO_ARCH_ESP32) + if (!is_font_runtime_loaded(*ui_pack)) + { + return base_font; + } +#else + if (!ensure_font_pack_loaded(ui_pack)) + { + return base_font; + } +#endif + return ::ui::fonts::composed_font_with_fallback(base_font, resolved_font(ui_pack), ::ui::fonts::FontScope::Ui); diff --git a/modules/ui_shared/src/ui/startup_shell.cpp b/modules/ui_shared/src/ui/startup_shell.cpp index 484daa33..85b6de36 100644 --- a/modules/ui_shared/src/ui/startup_shell.cpp +++ b/modules/ui_shared/src/ui/startup_shell.cpp @@ -54,8 +54,10 @@ bool resolve_display_time(struct tm* out_tm) void present_boot_overlay_now() { - lv_timer_handler(); - lv_refr_now(nullptr); + if (lv_obj_t* top = lv_layer_top()) + { + lv_obj_invalidate(top); + } } } // namespace diff --git a/modules/ui_shared/src/ui/startup_ui_shell.cpp b/modules/ui_shared/src/ui/startup_ui_shell.cpp index ded982e7..869bf730 100644 --- a/modules/ui_shared/src/ui/startup_ui_shell.cpp +++ b/modules/ui_shared/src/ui/startup_ui_shell.cpp @@ -22,8 +22,10 @@ ui::menu::MenuModel s_ux_menu_model; void present_boot_overlay_now() { - lv_timer_handler(); - lv_refr_now(nullptr); + if (lv_obj_t* top = lv_layer_top()) + { + lv_obj_invalidate(top); + } } bool resolve_display_time(struct tm* out_tm) diff --git a/modules/ui_shared/src/ui/ui_boot.cpp b/modules/ui_shared/src/ui/ui_boot.cpp index c40e6a26..8e382341 100644 --- a/modules/ui_shared/src/ui/ui_boot.cpp +++ b/modules/ui_shared/src/ui/ui_boot.cpp @@ -92,7 +92,7 @@ void refresh_log_label() } lv_label_set_text(s_log_label, s_log_text); - lv_refr_now(nullptr); + lv_obj_invalidate(s_log_label); } } // namespace diff --git a/modules/ui_shared/src/ui/widgets/busy_overlay.cpp b/modules/ui_shared/src/ui/widgets/busy_overlay.cpp index 75f7bd53..ec14797a 100644 --- a/modules/ui_shared/src/ui/widgets/busy_overlay.cpp +++ b/modules/ui_shared/src/ui/widgets/busy_overlay.cpp @@ -33,9 +33,18 @@ lv_obj_t* s_detail_label = nullptr; lv_obj_t* s_bar = nullptr; uint32_t s_depth = 0; -void refresh_now() +void request_refresh() { - lv_refr_now(nullptr); + if (s_root && lv_obj_is_valid(s_root)) + { + lv_obj_invalidate(s_root); + return; + } + + if (lv_obj_t* screen = lv_screen_active()) + { + lv_obj_invalidate(screen); + } } void set_bar_value(void* bar, int32_t value) @@ -243,7 +252,7 @@ void show(const char* title, const char* detail) update_content(title, detail); apply_progress(-1); lv_obj_move_foreground(s_root); - refresh_now(); + request_refresh(); } void update(const char* title, const char* detail) @@ -255,7 +264,7 @@ void update(const char* title, const char* detail) update_content(title, detail); lv_obj_move_foreground(s_root); - refresh_now(); + request_refresh(); } void set_progress(int progress_percent) @@ -266,7 +275,7 @@ void set_progress(int progress_percent) } apply_progress(progress_percent); - refresh_now(); + request_refresh(); } void hide() @@ -284,7 +293,7 @@ void hide() } destroy_overlay(); - refresh_now(); + request_refresh(); } bool visible() diff --git a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp index fc503eb3..e4018c31 100644 --- a/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp +++ b/platform/esp/arduino_common/src/ui/widgets/map/map_tiles.cpp @@ -16,7 +16,6 @@ #include "ui/page/page_profile.h" #include "ui/runtime/memory_profile.h" #include "ui/screens/gps/gps_constants.h" -#include "ui/support/lvgl_fs_utils.h" #include "ui_map_runtime/map_tiles/filesystem_map_tile_source.h" #include "ui_map_runtime/map_tiles/map_tile_async_runtime.h" #include "ui_map_runtime/map_tiles/map_tile_decoder_cache.h" @@ -113,27 +112,19 @@ static uint32_t g_map_tile_runtime_generation = 1; namespace { -class LvglMapTileFileSystem final : public ui::map_tiles::IMapTileFileSystem +class PathOnlyMapTileFileSystem final : public ui::map_tiles::IMapTileFileSystem { public: bool exists(const char* path) const override { -#if defined(ARDUINO) || defined(ARDUINO_ARCH_ESP32) (void)path; return false; -#else - return ui::fs::file_exists(path); -#endif } bool isDirectory(const char* path) const override { -#if defined(ARDUINO) || defined(ARDUINO_ARCH_ESP32) (void)path; return false; -#else - return ui::fs::dir_exists(path); -#endif } bool readFile(const char* path, @@ -142,52 +133,10 @@ class LvglMapTileFileSystem final : public ui::map_tiles::IMapTileFileSystem std::size_t& out_size) const override { out_size = 0; -#if defined(ARDUINO) || defined(ARDUINO_ARCH_ESP32) (void)path; (void)buffer; (void)capacity; return false; -#else - if (!path || !buffer || capacity == 0) - { - return false; - } - - const std::string normalized = ui::fs::normalize_path(path); - if (normalized.empty()) - { - return false; - } - - lv_fs_file_t file; - if (lv_fs_open(&file, normalized.c_str(), LV_FS_MODE_RD) != LV_FS_RES_OK) - { - return false; - } - - uint32_t file_size = 0; - if (lv_fs_seek(&file, 0, LV_FS_SEEK_END) != LV_FS_RES_OK || - lv_fs_tell(&file, &file_size) != LV_FS_RES_OK || - lv_fs_seek(&file, 0, LV_FS_SEEK_SET) != LV_FS_RES_OK || - file_size == 0 || - file_size > capacity) - { - lv_fs_close(&file); - return false; - } - - uint32_t bytes_read = 0; - const lv_fs_res_t res = lv_fs_read(&file, buffer, file_size, &bytes_read); - lv_fs_close(&file); - if (res != LV_FS_RES_OK || bytes_read != file_size) - { - out_size = bytes_read; - return false; - } - - out_size = bytes_read; - return true; -#endif } }; @@ -465,6 +414,30 @@ uint8_t* allocate_tile_payload(std::size_t size) MALLOC_CAP_8BIT)); } +void release_decoded_image_handle(void* handle) +{ + auto* img_dsc = static_cast(handle); + if (img_dsc == nullptr) + { + return; + } + lv_image_cache_drop(img_dsc); + if (img_dsc->data != nullptr) + { + lv_free((void*)img_dsc->data); + } + lv_free(img_dsc); +} + +void release_tile_decoded_image(ui::map_tiles::MapTileDecodedImage& decoded) +{ + if (decoded.native_handle != nullptr && decoded.release != nullptr) + { + decoded.release(decoded.native_handle); + } + decoded = {}; +} + void release_tile_payload(ui::map_tiles::MapTileAsyncEvent& event) { if (event.payload.data != nullptr) @@ -472,9 +445,109 @@ void release_tile_payload(ui::map_tiles::MapTileAsyncEvent& event) heap_caps_free(const_cast(event.payload.data)); } event.payload = {}; + release_tile_decoded_image(event.decoded); event.payload_size = 0; } +lv_image_dsc_t* decode_payload_to_image_desc(const ui::map_tiles::MapTileRef& ref, + const ui::map_tiles::MapTilePayload& payload) +{ + if (payload.data == nullptr || payload.size == 0) + { + return nullptr; + } + + const ui::map_tiles::MapTileFormat payload_format = + payload.format == ui::map_tiles::MapTileFormat::Unknown + ? ui::map_tiles::mapTileFormatForLayer(ref.layer) + : payload.format; + const lv_color_format_t source_format = lvgl_source_format_for_tile(payload_format); + if (source_format == LV_COLOR_FORMAT_UNKNOWN) + { + log_map_tile_decode_failure("unsupported_format", + ref, + payload_format, + payload.size, + static_cast(payload_format)); + return nullptr; + } + + lv_image_dsc_t compressed{}; + compressed.header.magic = LV_IMAGE_HEADER_MAGIC; + compressed.header.cf = source_format; + compressed.header.flags = 0; + compressed.data_size = static_cast(payload.size); + compressed.data = payload.data; + + lv_image_decoder_dsc_t decoder_dsc; + std::memset(&decoder_dsc, 0, sizeof(decoder_dsc)); + + const lv_result_t decode_res = lv_image_decoder_open(&decoder_dsc, &compressed, NULL); + if (decode_res != LV_RESULT_OK || decoder_dsc.decoded == NULL) + { + log_map_tile_decode_failure("open", + ref, + payload_format, + payload.size, + static_cast(decode_res)); + lv_image_decoder_close(&decoder_dsc); + return nullptr; + } + + const lv_draw_buf_t* decoded_buf = decoder_dsc.decoded; + const uint32_t data_size = decoded_buf->data_size; + if (decoded_buf->data == nullptr || data_size == 0 || decoded_buf->header.w == 0 || + decoded_buf->header.h == 0) + { + log_map_tile_decode_failure("decoded_buffer", + ref, + payload_format, + payload.size, + static_cast(data_size)); + lv_image_decoder_close(&decoder_dsc); + return nullptr; + } + + lv_image_dsc_t* img_dsc = static_cast(lv_malloc(sizeof(lv_image_dsc_t))); + if (img_dsc == NULL) + { + log_map_tile_decode_failure("alloc_desc", + ref, + payload_format, + payload.size, + -12); + lv_image_decoder_close(&decoder_dsc); + return nullptr; + } + std::memset(img_dsc, 0, sizeof(lv_image_dsc_t)); + + uint8_t* img_data = static_cast(lv_malloc(data_size)); + if (img_data == NULL) + { + log_map_tile_decode_failure("alloc_pixels", + ref, + payload_format, + data_size, + -12); + lv_free(img_dsc); + lv_image_decoder_close(&decoder_dsc); + return nullptr; + } + + std::memcpy(img_data, decoded_buf->data, data_size); + img_dsc->header.magic = LV_IMAGE_HEADER_MAGIC; + img_dsc->header.w = decoded_buf->header.w; + img_dsc->header.h = decoded_buf->header.h; + img_dsc->header.cf = decoded_buf->header.cf; + img_dsc->header.flags = 0; + img_dsc->header.stride = decoded_buf->header.stride; + img_dsc->data_size = data_size; + img_dsc->data = img_data; + + lv_image_decoder_close(&decoder_dsc); + return img_dsc; +} + bool same_tile_ref(const ui::map_tiles::MapTileRef& lhs, const ui::map_tiles::MapTileRef& rhs) { @@ -739,26 +812,33 @@ class MapTileEventQueue final : public ui::map_tiles::IMapTileEventSink bool publish(const ui::map_tiles::MapTileAsyncEvent& event) override { ui::map_tiles::MapTileAsyncEvent owned = event; - if (owned.kind == ui::map_tiles::MapTileAsyncEventKind::Ready && - event.payload.data != nullptr && - event.payload.size > 0) + if (owned.kind == ui::map_tiles::MapTileAsyncEventKind::Ready) { - uint8_t* payload = allocate_tile_payload(event.payload.size); - if (payload == nullptr) + if (event.payload.data == nullptr || event.payload.size == 0) { - log_map_tile_event_failure("payload_alloc", owned, -12); + log_map_tile_event_failure("payload_missing", owned, -22); owned.kind = ui::map_tiles::MapTileAsyncEventKind::Failed; - owned.error = -12; + owned.error = -22; + owned.payload = {}; + owned.payload_size = 0; + } + else if (lv_image_dsc_t* decoded = decode_payload_to_image_desc(event.tile, event.payload)) + { + owned.decoded.native_handle = decoded; + owned.decoded.release = release_decoded_image_handle; + owned.decoded.width = static_cast(decoded->header.w); + owned.decoded.height = static_cast(decoded->header.h); + owned.decoded.data_size = decoded->data_size; owned.payload = {}; owned.payload_size = 0; } else { - std::memcpy(payload, event.payload.data, event.payload.size); - owned.payload.data = payload; - owned.payload.size = event.payload.size; - owned.payload.format = event.payload.format; - owned.payload.ref = event.payload.ref; + log_map_tile_event_failure("decode_worker", owned, -12); + owned.kind = ui::map_tiles::MapTileAsyncEventKind::Failed; + owned.error = -12; + owned.payload = {}; + owned.payload_size = 0; } } @@ -1194,9 +1274,9 @@ class LvglDecodedTileCache final : public ui::map_tiles::IMapTileDecoderCache bool initialized_ = false; }; -LvglMapTileFileSystem& tile_file_system() +PathOnlyMapTileFileSystem& tile_file_system() { - static LvglMapTileFileSystem fs; + static PathOnlyMapTileFileSystem fs; return fs; } @@ -1218,7 +1298,7 @@ ui::map_tiles::FilesystemMapTileSource& worker_tile_source() static SdMapTileFileSystem fs; static ui::map_tiles::FilesystemMapTileSource source(fs, "/"); #else - static LvglMapTileFileSystem fs; + static PathOnlyMapTileFileSystem fs; static ui::map_tiles::FilesystemMapTileSource source(fs, "A:"); #endif return source; @@ -2294,21 +2374,23 @@ static void mark_missing_base_tile(TileContext& ctx, MapTile& tile) } } -static DecodedTileCache* decode_payload_to_cache(TileContext& ctx, - const ui::map_tiles::MapTileRef& ref, - const ui::map_tiles::MapTilePayload& payload) +static DecodedTileCache* adopt_decoded_image_to_cache( + TileContext& ctx, + const ui::map_tiles::MapTileRef& ref, + ui::map_tiles::MapTileDecodedImage& decoded) { - if (payload.data == nullptr || payload.size == 0) - { - return nullptr; - } - DecodedTileCache* cached = find_cached_tile_ref(ref); if (cached != nullptr && cached->img_dsc != NULL) { return cached; } + auto* img_dsc = static_cast(decoded.native_handle); + if (img_dsc == nullptr) + { + return nullptr; + } + refresh_live_tile_decode_cache_usage(ctx); DecodedTileCache* cache_slot = get_lru_cache_slot(tile_decode_cache_limit(ctx)); if (cache_slot == NULL && evict_invisible_cached_tile_object(ctx)) @@ -2321,94 +2403,7 @@ static DecodedTileCache* decode_payload_to_cache(TileContext& ctx, return nullptr; } - const ui::map_tiles::MapTileFormat payload_format = - payload.format == ui::map_tiles::MapTileFormat::Unknown - ? ui::map_tiles::mapTileFormatForLayer(ref.layer) - : payload.format; - const lv_color_format_t source_format = lvgl_source_format_for_tile(payload_format); - if (source_format == LV_COLOR_FORMAT_UNKNOWN) - { - log_map_tile_decode_failure("unsupported_format", - ref, - payload_format, - payload.size, - static_cast(payload_format)); - return nullptr; - } - - lv_image_dsc_t compressed{}; - compressed.header.magic = LV_IMAGE_HEADER_MAGIC; - compressed.header.cf = source_format; - compressed.header.flags = 0; - compressed.data_size = static_cast(payload.size); - compressed.data = payload.data; - - lv_image_decoder_dsc_t decoder_dsc; - std::memset(&decoder_dsc, 0, sizeof(decoder_dsc)); - - const lv_result_t decode_res = lv_image_decoder_open(&decoder_dsc, &compressed, NULL); - if (decode_res != LV_RESULT_OK || decoder_dsc.decoded == NULL) - { - log_map_tile_decode_failure("open", - ref, - payload_format, - payload.size, - static_cast(decode_res)); - lv_image_decoder_close(&decoder_dsc); - return nullptr; - } - - const lv_draw_buf_t* decoded_buf = decoder_dsc.decoded; - const uint32_t data_size = decoded_buf->data_size; - if (decoded_buf->data == nullptr || data_size == 0 || decoded_buf->header.w == 0 || - decoded_buf->header.h == 0) - { - log_map_tile_decode_failure("decoded_buffer", - ref, - payload_format, - payload.size, - static_cast(data_size)); - lv_image_decoder_close(&decoder_dsc); - return nullptr; - } - - cache_slot->img_dsc = static_cast(lv_malloc(sizeof(lv_image_dsc_t))); - if (cache_slot->img_dsc == NULL) - { - log_map_tile_decode_failure("alloc_desc", - ref, - payload_format, - payload.size, - -12); - lv_image_decoder_close(&decoder_dsc); - return nullptr; - } - std::memset(cache_slot->img_dsc, 0, sizeof(lv_image_dsc_t)); - - uint8_t* img_data = static_cast(lv_malloc(data_size)); - if (img_data == NULL) - { - log_map_tile_decode_failure("alloc_pixels", - ref, - payload_format, - data_size, - -12); - lv_free(cache_slot->img_dsc); - cache_slot->img_dsc = NULL; - lv_image_decoder_close(&decoder_dsc); - return nullptr; - } - - std::memcpy(img_data, decoded_buf->data, data_size); - cache_slot->img_dsc->header.magic = LV_IMAGE_HEADER_MAGIC; - cache_slot->img_dsc->header.w = decoded_buf->header.w; - cache_slot->img_dsc->header.h = decoded_buf->header.h; - cache_slot->img_dsc->header.cf = decoded_buf->header.cf; - cache_slot->img_dsc->header.flags = 0; - cache_slot->img_dsc->header.stride = decoded_buf->header.stride; - cache_slot->img_dsc->data_size = data_size; - cache_slot->img_dsc->data = img_data; - + cache_slot->img_dsc = img_dsc; cache_slot->x = static_cast(ref.x); cache_slot->y = static_cast(ref.y); cache_slot->z = static_cast(ref.z); @@ -2417,7 +2412,7 @@ static DecodedTileCache* decode_payload_to_cache(TileContext& ctx, cache_slot->last_used_ms = sys::millis_now(); cache_slot->lvgl_ref_count = 0; - lv_image_decoder_close(&decoder_dsc); + decoded = {}; return cache_slot; } @@ -2667,11 +2662,11 @@ static bool apply_map_tile_event(TileContext& ctx, ui::map_tiles::MapTileAsyncEv return true; } - DecodedTileCache* cache = decode_payload_to_cache(ctx, event.tile, event.payload); + DecodedTileCache* cache = adopt_decoded_image_to_cache(ctx, event.tile, event.decoded); if (cache == nullptr) { retry_not_before = now_ms + kMapTileLayerCacheBackoffMs; - log_map_tile_event_failure("decode", event, event.error); + log_map_tile_event_failure("decoded_image", event, event.error); release_tile_payload(event); return false; }