From a48c25fbb6f618afc3494a64f1ee99510068706b Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 23 Oct 2026 19:35:04 -0700 Subject: [PATCH] gui, menu: one LVGL lock, shared by the launcher and the pause menu LVGL is built without LV_USE_OS, so it is not thread-safe, but the launcher (Gui) and the pause menu (Menu) each had their own mutex. Changing the volume or mute in the pause menu publishes volume_changed; Gui::on_volume then updated the launcher's widgets on the event-manager task while the pause menu's timer was still dispatching the slider's event. The two event pushes interleaved and left LVGL's global event list (event_head) pointing at a dead stack frame, and the next object delete walked it: the pause menu's teardown on quit faulted in lv_event_mark_deleted (Load access fault, seen after TPM's menu was used). Both now lock Tab5Emu::lvgl_mutex(), and pause() takes the lock after stopping its timer, so a callback already in lv_timer_handler() finishes before the caller goes on (esp_timer_stop does not wait for it). Co-Authored-By: Claude Opus 5.5 (1M context) --- components/gui/include/gui.hpp | 2 +- components/gui/src/gui.cpp | 2 ++ components/menu/include/menu.hpp | 2 +- components/menu/src/menu.cpp | 2 ++ components/tab5-emu/include/tab5-emu.hpp | 7 +++++++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/components/gui/include/gui.hpp b/components/gui/include/gui.hpp index e6a53ee..c36a2f3 100644 --- a/components/gui/include/gui.hpp +++ b/components/gui/include/gui.hpp @@ -94,5 +94,5 @@ class Gui { .callback = std::bind(&Gui::update, this), }}; espp::Logger logger_; - mutable std::recursive_mutex mutex_; + std::recursive_mutex &mutex_{Tab5Emu::lvgl_mutex()}; // shared: LVGL is not thread-safe }; diff --git a/components/gui/src/gui.cpp b/components/gui/src/gui.cpp index 4a4157e..37e1235 100644 --- a/components/gui/src/gui.cpp +++ b/components/gui/src/gui.cpp @@ -39,6 +39,8 @@ Gui::~Gui() { void Gui::pause() { paused_ = true; task_.stop(); + // wait out a timer callback already in lv_timer_handler() (stop() does not) + std::lock_guard lk(mutex_); } void Gui::resume() { diff --git a/components/menu/include/menu.hpp b/components/menu/include/menu.hpp index d47df65..2585cc3 100644 --- a/components/menu/include/menu.hpp +++ b/components/menu/include/menu.hpp @@ -92,5 +92,5 @@ class Menu { .callback = std::bind(&Menu::update, this), }}; espp::Logger logger_; - std::recursive_mutex mutex_; + std::recursive_mutex &mutex_{Tab5Emu::lvgl_mutex()}; // shared: LVGL is not thread-safe }; diff --git a/components/menu/src/menu.cpp b/components/menu/src/menu.cpp index 0b65b84..048b868 100644 --- a/components/menu/src/menu.cpp +++ b/components/menu/src/menu.cpp @@ -36,6 +36,8 @@ void Menu::select_slot(int slot) { void Menu::pause() { paused_ = true; task_.stop(); + // wait out a timer callback already in lv_timer_handler() (stop() does not) + std::lock_guard lk(mutex_); } void Menu::resume() { diff --git a/components/tab5-emu/include/tab5-emu.hpp b/components/tab5-emu/include/tab5-emu.hpp index 14f2744..0a3c274 100644 --- a/components/tab5-emu/include/tab5-emu.hpp +++ b/components/tab5-emu/include/tab5-emu.hpp @@ -194,6 +194,13 @@ class Tab5Emu : public espp::BaseComponent { /// focused slider / dropdown), A = enter, B = escape. Menus attach their /// lv_group to it (lv_indev_set_group) when shown. lv_indev_t *keypad_indev() const { return keypad_indev_; } + /// The one lock for LVGL (built without LV_USE_OS, so it is not + /// thread-safe): the launcher's and the pause menu's timers and their + /// event-manager callbacks (volume) all touch it from different tasks. + static std::recursive_mutex &lvgl_mutex() { + static std::recursive_mutex m; + return m; + } /// Stop the USB host library (keyboard / mouse stop working). The USB /// controller is shared with device mode (see initialize_usb_msc()). void deinitialize_usb_host();