Skip to content

gui, menu: one LVGL lock, shared by the launcher and the pause menu - #8

Merged
finger563 merged 1 commit into
mainfrom
fix/lvgl-lock
Oct 24, 2026
Merged

finger563 merged 1 commit into
mainfrom
fix/lvgl-lock

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Fixes a crash when quitting a game after its pause menu was used (seen with TPM): a Load access fault in lv_event_mark_deleted during Menu::deinit_ui().

Cause

LVGL is built without LV_USE_OS, so it isn't thread-safe. The launcher (Gui) and the pause menu (Menu) each had their own mutex, so they didn't exclude each other.

  1. Changing the volume or mute in the pause menu publishes volume_changed.
  2. Gui::on_volume then updates the launcher's widgets on the event-manager task.
  3. Meanwhile, the pause menu's timer is still dispatching the slider's event.

The two event pushes interleave and leave LVGL's global event list (event_head) pointing at a dead stack frame. The next object delete walks that list, which here was the pause menu's teardown on quit.

Fix

  • Both menus lock one shared Tab5Emu::lvgl_mutex().
  • pause() takes that lock after stopping its timer, so a callback already inside lv_timer_handler() finishes before the caller goes on. esp_timer_stop doesn't wait for a running callback.

Testing (Tab5)

A temporary stress hook (local, not part of this PR) opens the pause menu every 4 s and toggles volume and mute 150 times from another thread, while the cart cycles through every game.

  • Before: it crashed after about 8.5 minutes, in LVGL, called from Gui::on_volume on the event task.
  • After: it ran 20 minutes, 206 stress rounds and 24 launches (TPM, JK, MGS, SNES, GBA, Genesis) with no crash.

🤖 Generated with Claude Code

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) <noreply@anthropic.com>
@finger563
finger563 merged commit bda0992 into main Oct 24, 2026
1 check passed
@finger563
finger563 deleted the fix/lvgl-lock branch October 24, 2026 07:16
@github-actions

Copy link
Copy Markdown

ESP-IDF Size Report for 'Tab5 Emu'

Metric Base PR Delta
FLASH 0 bytes (0.00%) 0 bytes (0.00%) 0 bytes (0.00%)
DRAM 267,166 bytes (60.54%) 267,182 bytes (60.54%) 🔺 +16 bytes (0.00%)
IRAM 0 bytes 0 bytes 0 bytes
RAM (DRAM+IRAM) 267,166 bytes 267,182 bytes 🔺 +16 bytes (0.00%)

FLASH uses app .bin size or json2 flash sum. RAM sums DRAM+IRAM via idf_size. Percentages shown when totals are available.
DRAM/IRAM usage does not include memory used by the heap allocator at runtime.
This report was generated by esp-idf-size-delta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant