From f0d7968843093b1cda9fb475cea18c2b38af2f0f Mon Sep 17 00:00:00 2001 From: Victoria Fischer Date: Mon, 9 Mar 2026 10:16:02 +0100 Subject: [PATCH] graphics: display: implement dynamic target fps --- src/graphics/display.rs | 20 +++++++++++++++++--- src/tasks/render.rs | 20 ++++++++++++-------- src/tasks/safetyui.rs | 3 +++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/graphics/display.rs b/src/graphics/display.rs index f4f2b7e..d2fcb15 100644 --- a/src/graphics/display.rs +++ b/src/graphics/display.rs @@ -159,16 +159,21 @@ pub struct Uniforms { pub primary_color: Hsv } +pub const DEFAULT_FPS: u8 = 60; +pub const LOW_POWER_FPS: u8 = 16; + struct ControlData { on: AtomicBool, - brightness: AtomicU8 + brightness: AtomicU8, + fps: AtomicU8 } impl Default for ControlData { fn default() -> Self { Self { on: AtomicBool::new(true), - brightness: AtomicU8::new(255) + brightness: AtomicU8::new(255), + fps: AtomicU8::new(DEFAULT_FPS) } } } @@ -202,7 +207,15 @@ impl DisplayControls { Fract8::from_raw(self.data.brightness.load(core::sync::atomic::Ordering::Relaxed)) } - pub async fn wait_until_display_is_on(&self) { + pub fn fps(&self) -> u8 { + self.data.fps.load(core::sync::atomic::Ordering::Relaxed) + } + + pub fn set_fps(&self, value: u8) { + self.data.fps.store(value, core::sync::atomic::Ordering::Relaxed); + } + + pub async fn wait_until_display_is_turned_on(&self) { while !self.display_is_on.wait().await { log::info!("wait for display") } log::trace!("display says on!"); } @@ -241,6 +254,7 @@ impl core::fmt::Debug for DisplayControls { f.debug_struct("DisplayControls") .field("on", &self.data.on) .field("brightness", &self.data.brightness) + .field("fps", &self.data.fps) .field("render_pause_signaled", &self.display_is_on.signaled()) .finish() } diff --git a/src/tasks/render.rs b/src/tasks/render.rs index 3ed1468..627be30 100644 --- a/src/tasks/render.rs +++ b/src/tasks/render.rs @@ -46,15 +46,13 @@ pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: An info!("Rendering started! {}ms since boot", Instant::now().as_millis()); controls.notify_render_is_running(true); - // TODO: The prediction engine should be able to scale the display FPS down for power saving sometimes - const FPS: u64 = 30; - const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS); + let mut requested_fps= controls.fps() as u64; + let mut render_budget = Duration::from_millis(1000 / requested_fps); const ANIMATION_TPS: u64 = 120; const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS); loop { - // FIXME: need to put the rendering loop into a deep sleep when the display is off let start = Instant::now(); output.blank(); @@ -70,13 +68,20 @@ pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: An let render_duration = Instant::now() - start; + let next_fps = controls.fps() as u64; + if next_fps != requested_fps { + requested_fps = next_fps; + render_budget = Duration::from_millis(1000 / requested_fps); + info!("FPS changed to {requested_fps}"); + } + if !controls.is_on() { warn!("Renderer is sleeping zzzz"); controls.notify_render_is_running(false); output.blank(); output.commit_async().await.expect("Failed to commit low power frame"); wdt.disable(); - controls.wait_until_display_is_on().await; + controls.wait_until_display_is_turned_on().await; wdt.feed(); wdt.enable(); warn!("Renderer is awake !!!!"); @@ -84,12 +89,11 @@ pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: An } // Apply the FPS cap where we sleep if we are rendering fast enough - if render_duration < RENDER_BUDGET { - let remaining_budget = RENDER_BUDGET - render_duration; + if render_duration < render_budget { + let remaining_budget = render_budget - render_duration; Timer::after(remaining_budget).await; } else { // Otherwise, we have a problem - // TODO: Automatically scale FPS back when this happens warn!("Render stall! Frame took {}ms", render_duration.as_millis()); // Kick the scheduler since it took so long Timer::after_ticks(1).await; diff --git a/src/tasks/safetyui.rs b/src/tasks/safetyui.rs index bef47f0..7f4e000 100644 --- a/src/tasks/safetyui.rs +++ b/src/tasks/safetyui.rs @@ -94,6 +94,7 @@ impl { // FIXME: These should be a Off/Low/High enum, so the stopping brake looks different from the dayrunning brake. + self.display.set_fps(DEFAULT_FPS); warn!("Active personality: Turning on safety lights"); TURN_ON_FAST.apply([ &mut self.brakelight, @@ -106,6 +107,7 @@ impl { warn!("Sleeping personality: Safety UI is going to sleep"); @@ -113,6 +115,7 @@ impl { warn!("Waking personality: Waking up safety UI"); + self.display.set_fps(DEFAULT_FPS); self.wake().await; }, } }