From f043fb01b2a83fca0bed9b1eee4f3c712c0d3e89 Mon Sep 17 00:00:00 2001 From: jajarom82 Date: Wed, 30 Sep 2026 09:20:33 +0200 Subject: [PATCH] Adaptive main loop: sleep on real events when idle The main loop polls at a fixed TARGET_FPS (20Hz) forever via SDL_PollEvent + a fixed-rate delay, even when the app is fully idle on a static page with nothing held or animating. Rendering is already change-driven, but the wake cadence was not, so the CPU (and battery) paid for 20 wakeups a second regardless of whether anything was happening. Adds View::on_tick()/wants_continuous_render() (a view opts in to mean "keep ticking at full rate even with no input", for a future animated view such as auto-scroll - unused by any current view, so no behavior change yet) and HeldKeyTracker::any_held(). When nothing is held and the active view does not need continuous rendering, the loop blocks on the next real input event instead of polling, using a small local poll_event_with_timeout() rather than SDL_WaitEventTimeout itself, since that symbol is not declared in this toolchain's SDL1.2 headers. No behavior change while input is active or a view opts into continuous rendering; the only difference is fewer wakeups while a book just sits on screen. Verified this still compiles and links against the real Miyoo Mini cross-compile toolchain. Co-Authored-By: Claude Sonnet 5 --- src/reader/config.h | 5 ++++ src/reader/main.cpp | 48 +++++++++++++++++++++++++++++++++-- src/reader/view.h | 10 ++++++++ src/reader/view_stack.cpp | 8 ++++++ src/reader/view_stack.h | 1 + src/util/held_key_tracker.cpp | 15 +++++++++++ src/util/held_key_tracker.h | 4 +++ 7 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/reader/config.h b/src/reader/config.h index fc92f19..f9e969d 100644 --- a/src/reader/config.h +++ b/src/reader/config.h @@ -3,6 +3,11 @@ #define TARGET_FPS 20 +// How long the main loop may block waiting for input when there's nothing +// held/animating, instead of polling at TARGET_FPS. Bounds CPU/battery use +// while idle without meaningfully affecting perceived input latency. +#define IDLE_POLL_TIMEOUT_MS 500 + #define IDLE_SAVE_TIME_SEC 60 #define FONT_DIR "resources/fonts" diff --git a/src/reader/main.cpp b/src/reader/main.cpp index fe7ff34..6b2db79 100644 --- a/src/reader/main.cpp +++ b/src/reader/main.cpp @@ -151,6 +151,29 @@ class SystemKeyChordTracker } }; +// Portable substitute for SDL_WaitEventTimeout, which isn't declared in +// every SDL1.2 build (notably absent from the Miyoo Mini cross-compile +// toolchain's headers). Polls in a short sleep loop instead of a single +// blocking call - still lets the CPU idle between polls, just with +// POLL_STEP_MS granularity instead of an instant wake. +bool poll_event_with_timeout(SDL_Event *event, uint32_t timeout_ms) +{ + constexpr uint32_t POLL_STEP_MS = 10; + uint32_t start = SDL_GetTicks(); + while (true) + { + if (SDL_PollEvent(event)) + { + return true; + } + if (SDL_GetTicks() - start >= timeout_ms) + { + return false; + } + SDL_Delay(POLL_STEP_MS); + } +} + bool quit = false; void signal_handler(int) @@ -283,6 +306,7 @@ int main(int argc, char **argv) // Timing Timer idle_timer; + Timer tick_timer; FPSLimiter limit_fps(TARGET_FPS); const uint32_t avg_loop_time = 1000 / TARGET_FPS; @@ -295,8 +319,25 @@ int main(int argc, char **argv) { bool ran_user_code = task_queue.drain(); + { + uint32_t elapsed_ms = tick_timer.elapsed_ms(); + tick_timer.reset(); + view_stack.on_tick(elapsed_ms); + } + + // Only stay on a tight poll+sleep cadence while something is held or + // animating; otherwise block until the next real event to let the + // CPU idle between key presses. + bool need_fast_ticks = held_key_tracker.any_held() || ( + view_stack.top_view() && view_stack.top_view()->wants_continuous_render() + ); + SDL_Event event; - while (SDL_PollEvent(&event)) + bool got_event = (!ran_user_code && !need_fast_ticks) + ? poll_event_with_timeout(&event, IDLE_POLL_TIMEOUT_MS) + : SDL_PollEvent(&event) != 0; + + while (got_event) { switch (event.type) { @@ -343,12 +384,15 @@ int main(int argc, char **argv) default: break; } + + got_event = SDL_PollEvent(&event) != 0; } quit = quit || chord_tracker.exit_requested(); held_key_tracker.accumulate(avg_loop_time); // Pretend perfect loop timing for event firing consistency ran_user_code = held_key_tracker.for_longest_held(key_held_callback) || ran_user_code; + ran_user_code = ran_user_code || need_fast_ticks; if (ran_user_code) { @@ -366,7 +410,7 @@ int main(int argc, char **argv) } } - if (!quit) + if (!quit && need_fast_ticks) { limit_fps(); } diff --git a/src/reader/view.h b/src/reader/view.h index e116a18..2496d74 100644 --- a/src/reader/view.h +++ b/src/reader/view.h @@ -22,6 +22,16 @@ class View // Pass key and held time in ms. virtual void on_keyheld(SDLKey, uint32_t) {} + // Called once per main loop iteration regardless of input, with elapsed + // time since the last call. Used for animations that aren't driven by a + // held key (e.g. auto-scroll). + virtual void on_tick(uint32_t) {} + + // If true, the main loop will keep ticking/rendering at full rate even + // with no input, instead of sleeping between events. Used by views that + // animate on their own (e.g. auto-scroll). + virtual bool wants_continuous_render() const { return false; } + // This view has been popped from the stack (now defunct). virtual void on_pop() {} diff --git a/src/reader/view_stack.cpp b/src/reader/view_stack.cpp index 4f2990a..8617eca 100644 --- a/src/reader/view_stack.cpp +++ b/src/reader/view_stack.cpp @@ -67,6 +67,14 @@ void ViewStack::on_keyheld(SDLKey key, uint32_t hold_time_ms) } } +void ViewStack::on_tick(uint32_t elapsed_ms) +{ + if (!views.empty()) + { + views.back()->on_tick(elapsed_ms); + } +} + bool ViewStack::pop_completed_views() { bool changed_focus = false; diff --git a/src/reader/view_stack.h b/src/reader/view_stack.h index 0d17ffb..fb83df6 100644 --- a/src/reader/view_stack.h +++ b/src/reader/view_stack.h @@ -19,6 +19,7 @@ class ViewStack: public View void on_keypress(SDLKey key) override; void on_keyheld(SDLKey key, uint32_t hold_time_ms) override; + void on_tick(uint32_t elapsed_ms) override; // Pop views that report as done. Return true if focus changed. bool pop_completed_views(); diff --git a/src/util/held_key_tracker.cpp b/src/util/held_key_tracker.cpp index f0d4295..6e7bae9 100644 --- a/src/util/held_key_tracker.cpp +++ b/src/util/held_key_tracker.cpp @@ -63,3 +63,18 @@ bool HeldKeyTracker::for_longest_held(const std::function &callback); + + // Return true if any tracked key is currently held, checked directly + // against live keyboard state (independent of accumulate()'s timing). + bool any_held() const; }; #endif