graphics: rewrite the DisplayControls to use a write-ack mechanism between the renderer and ui tasks

This commit is contained in:
2026-03-27 22:38:49 +01:00
parent 3fff4fdfd1
commit 8ff29caa6f
7 changed files with 85 additions and 66 deletions

View File

@@ -29,9 +29,9 @@ impl<S: Surface> AnimationActor<Fract8> 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<Fract8> for AnimDisplay<'a> {
impl<'a> AnimationActor<Fract8> for AnimDisplay<'a, '_> {
fn get_value(&self) -> Fract8 {
self.0.brightness()
}

View File

@@ -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<DisplayControlResources> = ConstStaticCell::new(DisplayControlResources::new());
static OLED_DISPLAY_SWITCH: ConstStaticCell<DisplayControlResources> = 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();

View File

@@ -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<T, Color> {
pixbuf: [Color; NUM_PIXELS],
writer: PowerManagedWriter<T>,
controls: DisplayControls
controls: DisplayControls<'static>
}
impl<T, Color> GammaCorrected for BikeOutput<T, Color> {
@@ -24,7 +24,7 @@ impl<T, Color> GammaCorrected for BikeOutput<T, Color> {
}
impl<T, Color: Default + Copy> BikeOutput<T, Color> {
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<T, Color: Default + Copy> BikeOutput<T, Color> {
impl<'a, T: SmartLedsWrite + 'a> Output<'a, SegmentSpace> for BikeOutput<T, T::Color> where T::Color: AsMilliwatts + Copy + Mul<Fract8, Output = T::Color> + 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<CriticalSectionRawMutex, bool, 7> = Watch::new();
// TODO: Implement something similar for a system-wide sleep mechanism
pub struct DisplayControls {
data: Arc<ControlData>,
display_is_on: Arc<Signal<CriticalSectionRawMutex, bool>>,
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<CriticalSectionRawMutex, RenderState, 7>,
state_ack: Watch<CriticalSectionRawMutex, RenderState, 7>
}
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<ControlData>,
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()
}
}
}

View File

@@ -106,7 +106,7 @@ impl<'a> Iterator for SsdSampler<'a> {
pub struct SsdOutput {
pixbuf: [u8; 128 * 64 / 8],
target: ssd1306::Ssd1306Async<ssd1306::prelude::I2CInterface<esp_hal::i2c::master::I2c<'static, esp_hal::Async>>, 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();

View File

@@ -20,7 +20,7 @@ pub type LockedUniforms = Arc<Mutex<CriticalSectionRawMutex, OledUniforms>>;
pub struct OledUI<S: Surface + core::fmt::Debug> {
overlay: S,
controls: DisplayControls,
controls: DisplayControls<'static>,
uniforms: LockedUniforms
}
@@ -32,7 +32,7 @@ impl Shader<OledUniforms, Matrix2DSpace, BinaryColor> for OverlayShader {
}
impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = BinaryColor, Uniforms = OledUniforms>> OledUI<S> {
pub fn new<SS: Surfaces<Surface = S>>(surfaces: &mut SS, controls: DisplayControls, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug {
pub fn new<SS: Surfaces<Surface = S>>(surfaces: &mut SS, controls: DisplayControls<'static>, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug {
Self {
overlay: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything())

View File

@@ -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<DmaBuffers<u8, {NUM_PIXELS * 12 + 140}>> = 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<esp_hal::peripherals::TIMG0<'static>>) {
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<esp_hal::peripherals::TIMG0<'static>>) {
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

View File

@@ -16,11 +16,11 @@ pub struct SafetyUi<S: Surface> {
// The overlay covers everything and is used to implement a power-on and power-off animation.
overlay: AnimatedSurface<S>,
display: DisplayControls
display: DisplayControls<'static>
}
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> SafetyUi<S> {
pub fn new<SS: Surfaces<Surface = S>>(surfaces: &mut SS, display: DisplayControls) -> Self where SS::Error: Debug {
pub fn new<SS: Surfaces<Surface = S>>(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<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
self.display.set_brightness(Fract8::MIN);
self.display.set_on(true);
// Wait for the renderer to start running again
self.display.wait_until_render_is_running().await;
self.display.wait_for_ack(RenderState::On).await;
trace!("Fading in brightness with overlay={:?}", self.overlay);
self.overlay.set_opacity(Fract8::MAX);