From 4b30a1d260a5949d54fe9d960b5d24d8fdd54b67 Mon Sep 17 00:00:00 2001 From: kilyabin <65072190+kilyabin@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:29:20 +0400 Subject: [PATCH] perf: replace busy-loop Poll with WaitUntil frame limiter Screensaver was consuming ~8% CPU due to ControlFlow::Poll rendering as fast as possible. Switch to WaitUntil with configurable FPS cap. Blank and image screensavers auto-cap at 2fps since they have no animation, reducing CPU usage to near zero. --- config.toml.example | 5 +++-- src/app.rs | 17 ++++++++++++++--- src/config.rs | 4 +++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/config.toml.example b/config.toml.example index f30dd1c..138aa90 100644 --- a/config.toml.example +++ b/config.toml.example @@ -30,9 +30,10 @@ type = "blank" # pure black (default, OLED-optimal) # ── Display settings ───────────────────────────────────────────────────────── [display] -monitor = 0 # monitor index (0 = primary). See: scr33ny monitors -burn_shift = 2 # pixel shift for OLED burn-in prevention +monitor = 0 # monitor index (0 = primary). See: scr33ny monitors +burn_shift = 2 # pixel shift for OLED burn-in prevention shift_interval = 120 # seconds between shifts +fps = 30 # max frames per second (blank/image auto-cap at 2) # ── Idle daemon ─────────────────────────────────────────────────────────────── diff --git a/src/app.rs b/src/app.rs index b543d00..3bd56a5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ use std::num::NonZeroU32; use std::rc::Rc; -use std::time::Instant; +use std::time::{Duration, Instant}; use anyhow::Result; use winit::{ @@ -55,8 +55,16 @@ pub fn run(config: Config, monitor_idx: usize) -> Result<()> { let mut screensaver: Box = build_screensaver(&config, font.clone())?; let mut widgets: Vec> = build_widgets(&config, font)?; + // Adaptive FPS: blank/image need only 1–2 fps, animations up to config.display.fps + let target_fps = match &config.screensaver { + ScreensaverConfig::Blank | ScreensaverConfig::Image { .. } => { + config.display.fps.min(2) + } + _ => config.display.fps, + }.max(1); + let frame_time = Duration::from_millis(1000 / target_fps as u64); + let event_loop = EventLoop::new()?; - event_loop.set_control_flow(ControlFlow::Poll); let monitors: Vec<_> = event_loop.available_monitors().collect(); let monitor = monitors.get(monitor_idx).or_else(|| monitors.first()).cloned(); @@ -131,7 +139,10 @@ pub fn run(config: Config, monitor_idx: usize) -> Result<()> { } } - Event::AboutToWait => window.request_redraw(), + Event::AboutToWait => { + elwt.set_control_flow(ControlFlow::WaitUntil(Instant::now() + frame_time)); + window.request_redraw(); + } _ => {} } diff --git a/src/config.rs b/src/config.rs index 2e78546..9e8ba1d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -147,11 +147,13 @@ pub struct DisplayConfig { /// Anti-burn-in pixel shift in pixels, applied every shift_interval seconds pub burn_shift: u32, pub shift_interval: u64, + /// Target frames per second. Lower = less CPU. Blank/image need only 1–2. + pub fps: u32, } impl Default for DisplayConfig { fn default() -> Self { - Self { monitor: 0, burn_shift: 2, shift_interval: 120 } + Self { monitor: 0, burn_shift: 2, shift_interval: 120, fps: 30 } } }