From 895bbbc633b29f6ec14ed4df29c55668aa60d815 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Fri, 22 May 2026 12:00:32 +0200 Subject: [PATCH] joystick UI: Coalesce joystick scroll input and throttle OLED redraws so fast list navigation doesn't stall on one I2C blit per key. --- .../helpers/ui-joystick/joystick_ui_task.cpp | 146 +++++++++++------- .../helpers/ui-joystick/joystick_ui_task.h | 6 +- 2 files changed, 93 insertions(+), 59 deletions(-) diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp index d7ba17e..0da5292 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.cpp +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.cpp @@ -453,7 +453,8 @@ JoystickUITask::JoystickUITask() _doom(nullptr), #endif _repeater_admin(nullptr), _curr(nullptr), - _next_refresh(0), _screen_off_ms(AUTO_OFF_MILLIS), _was_display_on(false), + _next_refresh(0), _last_blt_ms(0), _pending_render(false), _render_immediate(false), + _screen_off_ms(AUTO_OFF_MILLIS), _was_display_on(false), _cached_batt_mv(0), _battery_display_mode(0), _brightness(100), _wake_on_msg(true), _ble_connected(false), _ble_enabled(true), _noise_floor(-120), @@ -766,76 +767,105 @@ void JoystickUITask::loop() processPendingRetries(); processPendingChannelFeedback(); - /* Dequeue key events */ - char key = 0; - if (k_msgq_get(&_key_queue, &key, K_NO_WAIT) == 0) { + /* Drain all pending keys — apply navigation in one pass, render once. */ + char keys[JOYSTICK_KEY_QUEUE_DEPTH]; + int nkeys = 0; + + while (nkeys < JOYSTICK_KEY_QUEUE_DEPTH && + k_msgq_get(&_key_queue, &keys[nkeys], K_NO_WAIT) == 0) { + nkeys++; + } + + for (int i = 0; i < nkeys; i++) { + char key = keys[i]; + if (!_display.isOn()) { - /* Wake display, consume the key press. Invalidate the battery - * cache so the upcoming render samples a fresh value — matters - * for the lock screen, which the user is about to see. */ + /* Wake display; discard this press (same as single-key path). */ ui_invalidate_battery_cache(); - _display.turnOn(); /* mc_display_on() already calls mc_display_reset_auto_off() */ - onDisplayStateChanged(); /* dispatch onDisplayOn now, no one-frame delay */ - /* Push the lock deadline forward so a wake-press at the tail end - * of the lock window doesn't get re-locked immediately. */ - if (!_locked) scheduleLockTimer(); - _next_refresh = 0; - goto do_render; + _display.turnOn(); + onDisplayStateChanged(); + if (!_locked) { + scheduleLockTimer(); + } + _render_immediate = true; + _pending_render = true; + continue; } - /* Reset auto-off (display.c) on any keypress */ mc_display_reset_auto_off(); if (_locked) { handleLockInput(key, now); - } else { - scheduleLockTimer(); /* push lock deadline forward */ - bool consumed = false; - if (key == KEY_BUZZ_TOGGLE) { - toggleBuzzer(); - consumed = true; - } else if (key == KEY_GPS_TOGGLE) { - toggleGPS(); - consumed = true; - } else if (key == KEY_LED_TOGGLE) { - toggleLeds(); - consumed = true; - } - if (!consumed && _curr) { - _curr->handleInput(key); - } - _next_refresh = now; + _render_immediate = true; + _pending_render = true; + continue; } + + scheduleLockTimer(); + bool consumed = false; + if (key == KEY_BUZZ_TOGGLE) { + toggleBuzzer(); + consumed = true; + } else if (key == KEY_GPS_TOGGLE) { + toggleGPS(); + consumed = true; + } else if (key == KEY_LED_TOGGLE) { + toggleLeds(); + consumed = true; + } + if (!consumed && _curr) { + _curr->handleInput(key); + } + _pending_render = true; } do_render: - /* Render if due */ - if (_display.isOn() && now >= _next_refresh && _curr) { -#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM - if (doom_game_is_running()) { - _next_refresh = now + 500; - if (s_schedule_render_fn) s_schedule_render_fn(500); - } else -#endif - { - ui_refresh_battery(); - _display.startFrame(); - int delay_ms; - if (_locked) { - renderLockOverlay(); - delay_ms = 200; - } else { - delay_ms = _curr->render(_display); - if (now < _alert_expiry) { - renderAlertOverlay(); - } - if (delay_ms <= 0) delay_ms = 100; - } - _next_refresh = now + (uint32_t)delay_ms; - if (s_schedule_render_fn) s_schedule_render_fn((uint32_t)delay_ms); - _display.endFrame(); - } + if (!_display.isOn() || !_curr) { + return; } + + const bool due_timer = (now >= _next_refresh); + if (!_pending_render && !due_timer) { + return; + } + + /* Throttle input-driven blits; timer/notify wakes (due_timer) stay immediate. */ + if (_pending_render && !due_timer && !_render_immediate && + (now - _last_blt_ms < JOYSTICK_RENDER_MIN_MS)) { + uint32_t wait = JOYSTICK_RENDER_MIN_MS - (now - _last_blt_ms); + if (s_schedule_render_fn) { + s_schedule_render_fn(wait); + } + _next_refresh = now + wait; + return; + } + +#ifdef CONFIG_ZEPHCORE_EASTER_EGG_DOOM + if (doom_game_is_running()) { + _next_refresh = now + 500; + if (s_schedule_render_fn) s_schedule_render_fn(500); + return; + } +#endif + ui_refresh_battery(); + _display.startFrame(); + int delay_ms; + if (_locked) { + renderLockOverlay(); + delay_ms = 200; + } else { + delay_ms = _curr->render(_display); + if (now < _alert_expiry) { + renderAlertOverlay(); + } + if (delay_ms <= 0) delay_ms = 100; + } + _next_refresh = now + (uint32_t)delay_ms; + if (s_schedule_render_fn) s_schedule_render_fn((uint32_t)delay_ms); + _display.endFrame(); + _last_blt_ms = now; + _pending_render = false; + _render_immediate = false; } /* ===== Buzzer / GPS helpers ===== */ diff --git a/zephcore/helpers/ui-joystick/joystick_ui_task.h b/zephcore/helpers/ui-joystick/joystick_ui_task.h index ad29e91..4a0f33f 100644 --- a/zephcore/helpers/ui-joystick/joystick_ui_task.h +++ b/zephcore/helpers/ui-joystick/joystick_ui_task.h @@ -35,7 +35,7 @@ #endif /* Key event queue depth */ -#define JOYSTICK_KEY_QUEUE_DEPTH 8 +#define JOYSTICK_KEY_QUEUE_DEPTH 24 class JoystickUITask { public: @@ -229,6 +229,10 @@ private: /* Timing */ uint32_t _next_refresh; + static constexpr uint32_t JOYSTICK_RENDER_MIN_MS = 40; /* min gap between input blits */ + uint32_t _last_blt_ms; + bool _pending_render; + bool _render_immediate; uint32_t _screen_off_ms; /* user setting; display.c owns the auto-off timer */ struct k_timer _lock_timer; /* one-shot, fires LOCK_AFTER_MS after last activity */ static void lockTimerCb(struct k_timer *t);