From 8ff29caa6fd260750c7ca51fb822c60d829d647b Mon Sep 17 00:00:00 2001 From: Victoria Fischer Date: Fri, 27 Mar 2026 22:38:49 +0100 Subject: [PATCH] graphics: rewrite the DisplayControls to use a write-ack mechanism between the renderer and ui tasks --- src/animation.rs | 4 +- src/bin/main.rs | 6 ++- src/graphics/display.rs | 114 +++++++++++++++++++++++----------------- src/graphics/ssd1306.rs | 6 +-- src/tasks/oled.rs | 4 +- src/tasks/render.rs | 11 ++-- src/tasks/safetyui.rs | 6 +-- 7 files changed, 85 insertions(+), 66 deletions(-) diff --git a/src/animation.rs b/src/animation.rs index 2a88e35..15bcbac 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -29,9 +29,9 @@ impl AnimationActor for S { } #[derive(Debug)] -pub struct AnimDisplay<'a>(pub &'a mut DisplayControls); +pub struct AnimDisplay<'a, 'b>(pub &'a mut DisplayControls<'b>); -impl<'a> AnimationActor for AnimDisplay<'a> { +impl<'a> AnimationActor for AnimDisplay<'a, '_> { fn get_value(&self) -> Fract8 { self.0.brightness() } diff --git a/src/bin/main.rs b/src/bin/main.rs index 0c3779b..00cdf2b 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -82,8 +82,10 @@ async fn main(spawner: Spawner) { let mut surfaces = UiSurfacePool::default(); let ui = Ui::new(&mut surfaces); - let display_controls = DisplayControls::default(); - let oled_controls = DisplayControls::default(); + static MAIN_DISPLAY_SWITCH: ConstStaticCell = ConstStaticCell::new(DisplayControlResources::new()); + static OLED_DISPLAY_SWITCH: ConstStaticCell = ConstStaticCell::new(DisplayControlResources::new()); + let display_controls = DisplayControls::new(MAIN_DISPLAY_SWITCH.take()); + let oled_controls = DisplayControls::new(OLED_DISPLAY_SWITCH.take()); let mut oled_surfaces = OledUiSurfacePool::default(); let oled_uniforms = Default::default(); diff --git a/src/graphics/display.rs b/src/graphics/display.rs index 9ba061f..d333ad9 100644 --- a/src/graphics/display.rs +++ b/src/graphics/display.rs @@ -1,7 +1,7 @@ use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal, watch::{Receiver, Watch}}; use figments::{liber8tion::interpolate::Fract8, prelude::*}; use portable_atomic::AtomicU32; -use core::{fmt::Debug, ops::Mul, sync::atomic::{AtomicBool, AtomicU8}}; +use core::{cell::RefCell, fmt::Debug, ops::Mul, sync::atomic::{AtomicBool, AtomicU8}}; use alloc::sync::Arc; use figments_render::{ @@ -14,7 +14,7 @@ pub const NUM_PIXELS: usize = 178; pub struct BikeOutput { pixbuf: [Color; NUM_PIXELS], writer: PowerManagedWriter, - controls: DisplayControls + controls: DisplayControls<'static> } impl GammaCorrected for BikeOutput { @@ -24,7 +24,7 @@ impl GammaCorrected for BikeOutput { } impl BikeOutput { - pub fn new(target: T, controls: DisplayControls) -> Self { + pub fn new(target: T, controls: DisplayControls<'static>) -> Self { Self { pixbuf: [Default::default(); NUM_PIXELS], writer: PowerManagedWriter::new(target, controls.max_power()), @@ -40,7 +40,7 @@ impl BikeOutput { impl<'a, T: SmartLedsWrite + 'a> Output<'a, SegmentSpace> for BikeOutput where T::Color: AsMilliwatts + Copy + Mul + WithGamma + Default + Debug + 'a + 'static { type Error = T::Error; - type Controls = DisplayControls; + type Controls = DisplayControls<'static>; fn commit(&mut self) -> Result<(), Self::Error> { self.writer.controls().set_brightness(self.controls.brightness()); @@ -66,7 +66,7 @@ impl<'a, T: SmartLedsWriteAsync + 'a> OutputAsync<'a, SegmentSpace> for BikeOutp } type Error = T::Error; - type Controls = DisplayControls; + type Controls = DisplayControls<'static>; fn controls(&mut self) -> Option<&mut Self::Controls> { Some(&mut self.controls) @@ -184,27 +184,56 @@ impl Default for ControlData { } } -// A watch that indicates whether or not the rendering engine is running. If the display is off or sleeping, this will be false. -static RENDER_IS_RUNNING: Watch = Watch::new(); - -// TODO: Implement something similar for a system-wide sleep mechanism -pub struct DisplayControls { - data: Arc, - display_is_on: Arc>, - render_run_receiver: Receiver<'static, CriticalSectionRawMutex, bool, 7> +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RenderState { + On, + Off } -impl Clone for DisplayControls { +pub struct DisplayControlResources { + state_rx: Watch, + state_ack: Watch +} + +impl DisplayControlResources { + pub const fn new() -> Self { + Self { + state_rx: Watch::new_with(RenderState::On), + state_ack: Watch::new_with(RenderState::On) + } + } +} + +// TODO: Implement something similar for a system-wide sleep mechanism +pub struct DisplayControls<'a> { + data: Arc, + render_run_receiver: Receiver<'a, CriticalSectionRawMutex, RenderState, 7>, + render_ack_receiver: Receiver<'a, CriticalSectionRawMutex, RenderState, 7>, + resources: &'a DisplayControlResources, +} + +impl Clone for DisplayControls<'_> { fn clone(&self) -> Self { Self { data: Arc::clone(&self.data), - display_is_on: Arc::clone(&self.display_is_on), - render_run_receiver: RENDER_IS_RUNNING.receiver().expect("Could not create enough render running receivers") + render_run_receiver: self.resources.state_rx.receiver().expect("Could not create enough render running receivers"), + resources: self.resources, + render_ack_receiver: self.resources.state_ack.receiver().expect("Could not create enough render ack receivers"), } } } -impl DisplayControls { + +impl<'a> DisplayControls<'a> { + pub fn new(resources: &'a DisplayControlResources) -> Self { + Self { + data: Default::default(), + render_run_receiver: resources.state_rx.receiver().expect("Could not create enough render running receivers"), + render_ack_receiver: resources.state_ack.receiver().expect("Could not create enough render ack receivers"), + resources + } + } + pub fn is_on(&self) -> bool { self.data.on.load(core::sync::atomic::Ordering::Relaxed) } @@ -221,21 +250,6 @@ impl DisplayControls { 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!"); - } - - pub fn notify_render_is_running(&mut self, value: bool) { - log::trace!("render is running!"); - RENDER_IS_RUNNING.sender().send(value); - } - - pub async fn wait_until_render_is_running(&mut self) { - while !self.render_run_receiver.changed().await { log::info!("wait for render run") } - log::trace!("render says run!"); - } - pub fn set_max_power(&mut self, mw: Milliwatts) { self.data.max_power_mw.store(mw.0, core::sync::atomic::Ordering::Relaxed); } @@ -243,44 +257,48 @@ impl DisplayControls { pub fn max_power(&self) -> Milliwatts { Milliwatts(self.data.max_power_mw.load(core::sync::atomic::Ordering::Relaxed)) } + + pub async fn wait_for_state(&mut self, state: RenderState) { + self.render_run_receiver.get_and(|cur| { *cur == state }).await; + } + + pub async fn wait_for_ack(&mut self, state: RenderState) { + self.render_ack_receiver.get_and(|cur| { *cur == state }).await; + } + + /// Indicates the current state has been applied to the hardware, which can wake up tasks waiting for the power to turn on/off. + pub async fn ack(&mut self) { + let next_state = self.render_run_receiver.get().await; + let is_on = next_state == RenderState::On; + self.data.on.store(is_on, core::sync::atomic::Ordering::Relaxed); + self.resources.state_ack.sender().send(if self.is_on() { RenderState::On } else { RenderState::Off }); + } } -impl GammaCorrected for DisplayControls { +impl GammaCorrected for DisplayControls<'_> { fn set_gamma(&mut self, _gamma: GammaCurve) { todo!() } } -impl Brightness for DisplayControls { +impl Brightness for DisplayControls<'_> { fn set_brightness(&mut self, brightness: Fract8) { self.data.brightness.store(brightness.to_raw(), core::sync::atomic::Ordering::Relaxed); } fn set_on(&mut self, is_on: bool) { - self.data.on.store(is_on, core::sync::atomic::Ordering::Relaxed); + self.resources.state_rx.sender().send(if is_on { RenderState::On } else { RenderState::Off }); log::trace!("display is on {is_on}"); - self.display_is_on.signal(is_on); } } -impl core::fmt::Debug for DisplayControls { +impl core::fmt::Debug for DisplayControls<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { 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()) .field("max_power_mw", &self.data.max_power_mw) .finish() } -} - -impl Default for DisplayControls { - fn default() -> Self { - Self { - data: Default::default(), - display_is_on: Default::default(), - render_run_receiver: RENDER_IS_RUNNING.receiver().unwrap() - } - } } \ No newline at end of file diff --git a/src/graphics/ssd1306.rs b/src/graphics/ssd1306.rs index bb62d15..da05688 100644 --- a/src/graphics/ssd1306.rs +++ b/src/graphics/ssd1306.rs @@ -106,7 +106,7 @@ impl<'a> Iterator for SsdSampler<'a> { pub struct SsdOutput { pixbuf: [u8; 128 * 64 / 8], target: ssd1306::Ssd1306Async>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>, - controls: DisplayControls, + controls: DisplayControls<'static>, is_on: bool, last_brightness: Fract8, reset_pin: Output<'static> @@ -124,7 +124,7 @@ impl SsdOutput { Some(SsdPixel { byte: pixref, bit, coords }) } - pub async fn new(i2c: I2c<'static, Async>, reset_pin: Output<'static>, controls: DisplayControls) -> Self { + pub async fn new(i2c: I2c<'static, Async>, reset_pin: Output<'static>, controls: DisplayControls<'static>) -> Self { let interface = I2CDisplayInterface::new(i2c); let target = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0); @@ -221,7 +221,7 @@ impl OriginDimensions for SsdOutput { impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput { type Error = DisplayError; - type Controls = DisplayControls; + type Controls = DisplayControls<'static>; async fn commit_async(&mut self) -> Result<(), Self::Error> { let new_brightness = self.controls.brightness(); diff --git a/src/tasks/oled.rs b/src/tasks/oled.rs index da88751..5caf83f 100644 --- a/src/tasks/oled.rs +++ b/src/tasks/oled.rs @@ -20,7 +20,7 @@ pub type LockedUniforms = Arc>; pub struct OledUI { overlay: S, - controls: DisplayControls, + controls: DisplayControls<'static>, uniforms: LockedUniforms } @@ -32,7 +32,7 @@ impl Shader for OverlayShader { } impl> OledUI { - pub fn new>(surfaces: &mut SS, controls: DisplayControls, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug { + pub fn new>(surfaces: &mut SS, controls: DisplayControls<'static>, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug { Self { overlay: SurfaceBuilder::build(surfaces) .rect(Rectangle::everything()) diff --git a/src/tasks/render.rs b/src/tasks/render.rs index 6a92d8f..ec02485 100644 --- a/src/tasks/render.rs +++ b/src/tasks/render.rs @@ -9,13 +9,13 @@ use figments_render::gamma::GammaCurve; use figments_render::output::{GammaCorrected, OutputAsync}; use log::*; -use crate::graphics::display::NUM_PIXELS; +use crate::graphics::display::{NUM_PIXELS, RenderState}; use crate::{graphics::display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool}; static SPI_BUFFERS: static_cell::ConstStaticCell> = static_cell::ConstStaticCell::new(DmaBuffers::new(0)); #[embassy_executor::task] -pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: AnyPin<'static>, mut surfaces: UiSurfacePool, mut safety_surfaces: UiSurfacePool, mut controls: DisplayControls, mut wdt: Wdt>) { +pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: AnyPin<'static>, mut surfaces: UiSurfacePool, mut safety_surfaces: UiSurfacePool, mut controls: DisplayControls<'static>, mut wdt: Wdt>) { info!("Starting rendering task"); let buffers = SPI_BUFFERS.take(); @@ -41,7 +41,6 @@ pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: An output.set_gamma(GammaCurve::new(1.3)); info!("Rendering started! {}ms since boot", Instant::now().as_millis()); - controls.notify_render_is_running(true); let mut requested_fps= controls.fps() as u64; let mut render_budget = Duration::from_millis(1000 / requested_fps); @@ -74,15 +73,15 @@ pub async fn render(spi: AnySpi<'static>, dma: AnyGdmaChannel<'static>, gpio: An 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_turned_on().await; + controls.ack().await; + controls.wait_for_state(RenderState::On).await; + controls.ack().await; wdt.feed(); wdt.enable(); warn!("Renderer is awake !!!!"); - controls.notify_render_is_running(true); } // Apply the FPS cap where we sleep if we are rendering fast enough diff --git a/src/tasks/safetyui.rs b/src/tasks/safetyui.rs index ca0bfcd..d2a62a9 100644 --- a/src/tasks/safetyui.rs +++ b/src/tasks/safetyui.rs @@ -16,11 +16,11 @@ pub struct SafetyUi { // The overlay covers everything and is used to implement a power-on and power-off animation. overlay: AnimatedSurface, - display: DisplayControls + display: DisplayControls<'static> } impl>> SafetyUi { - pub fn new>(surfaces: &mut SS, display: DisplayControls) -> Self where SS::Error: Debug { + pub fn new>(surfaces: &mut SS, display: DisplayControls<'static>) -> Self where SS::Error: Debug { let ret = Self { overlay: SurfaceBuilder::build(surfaces) .rect(Rectangle::everything()) @@ -68,7 +68,7 @@ impl