From 919744dbec7ac0a501c3a13ad0f52e95269bd81f Mon Sep 17 00:00:00 2001 From: sawyer bristol Date: Sun, 14 Sep 2025 18:40:48 -0600 Subject: [PATCH] atomic fb --- Cargo.lock | 3 +- kernel/Cargo.toml | 3 +- kernel/src/abi.rs | 8 +- kernel/src/display.rs | 25 +-- kernel/src/framebuffer.rs | 432 ++++++++++++++++++++++++++++++++++++++ kernel/src/main.rs | 1 + kernel/src/ui.rs | 9 +- 7 files changed, 453 insertions(+), 28 deletions(-) create mode 100644 kernel/src/framebuffer.rs diff --git a/Cargo.lock b/Cargo.lock index 4db15b7..a4e1b7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,6 +1264,7 @@ dependencies = [ "embassy-usb", "embedded-graphics", "embedded-hal 0.2.7", + "embedded-hal 1.0.0", "embedded-hal-async", "embedded-hal-bus", "embedded-layout", @@ -1991,7 +1992,7 @@ dependencies = [ [[package]] name = "st7365p-lcd" version = "0.11.0" -source = "git+https://github.com/legitcamper/st7365p-lcd-rs?rev=2a484aaab5f6b9824cc813fe4ae087250c9e39c1#2a484aaab5f6b9824cc813fe4ae087250c9e39c1" +source = "git+https://github.com/legitcamper/st7365p-lcd-rs?rev=1d15123929fa7ef73d5d6aead7faf1bba50ce915#1d15123929fa7ef73d5d6aead7faf1bba50ce915" dependencies = [ "bitvec", "embedded-graphics-core", diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 327cc02..a7fe247 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -60,6 +60,7 @@ cyw43-pio = { version = "0.3.0", optional = true } embedded-hal-bus = { version = "0.3.0", features = ["async"] } embedded-hal = "0.2.7" +embedded-hal_2 = { package = "embedded-hal", version = "1.0.0" } embedded-hal-async = "1.0.0" cortex-m = { version = "0.7.7" } cortex-m-rt = "0.7.5" @@ -71,7 +72,7 @@ defmt = { version = "0.3", optional = true } defmt-rtt = "0.4.2" embedded-sdmmc = { version = "0.9", default-features = false } -st7365p-lcd = { git = "https://github.com/legitcamper/st7365p-lcd-rs", rev = "2a484aaab5f6b9824cc813fe4ae087250c9e39c1" } # async branch +st7365p-lcd = { git = "https://github.com/legitcamper/st7365p-lcd-rs", rev = "1d15123929fa7ef73d5d6aead7faf1bba50ce915" } # async branch embedded-graphics = { version = "0.8.1" } embedded-text = "0.7.2" embedded-layout = "0.4.2" diff --git a/kernel/src/abi.rs b/kernel/src/abi.rs index 8be0c82..453d2e9 100644 --- a/kernel/src/abi.rs +++ b/kernel/src/abi.rs @@ -37,12 +37,8 @@ pub extern "Rust" fn sleep(ticks: u64) { // TODO: maybe return result pub extern "Rust" fn draw_iter(pixels: &[Pixel]) { loop { - let fb = FRAMEBUFFER.get().try_lock(); - if let Ok(mut fb) = fb { - fb.draw_iter(pixels.iter().copied()).unwrap(); - return; - } - sleep(1) + unsafe { FRAMEBUFFER.draw_iter(pixels.iter().copied()).unwrap() } + return; } } diff --git a/kernel/src/display.rs b/kernel/src/display.rs index c362bd7..ff18142 100644 --- a/kernel/src/display.rs +++ b/kernel/src/display.rs @@ -1,19 +1,17 @@ +use crate::framebuffer::AtomicFrameBuffer; use embassy_rp::{ Peri, gpio::{Level, Output}, peripherals::{PIN_13, PIN_14, PIN_15, SPI1}, spi::{Async, Spi}, }; -use embassy_sync::{ - blocking_mutex::raw::CriticalSectionRawMutex, lazy_lock::LazyLock, mutex::Mutex, -}; use embassy_time::{Delay, Timer}; use embedded_graphics::{ draw_target::DrawTarget, pixelcolor::{Rgb565, RgbColor}, }; use embedded_hal_bus::spi::ExclusiveDevice; -use st7365p_lcd::{FrameBuffer, ST7365P}; +use st7365p_lcd::ST7365P; type DISPLAY = ST7365P< ExclusiveDevice, Output<'static>, Delay>, @@ -25,9 +23,7 @@ type DISPLAY = ST7365P< pub const SCREEN_WIDTH: usize = 320; pub const SCREEN_HEIGHT: usize = 320; -type FB = FrameBuffer; -pub static FRAMEBUFFER: LazyLock> = - LazyLock::new(|| Mutex::new(FrameBuffer::new())); +pub static mut FRAMEBUFFER: AtomicFrameBuffer = AtomicFrameBuffer::new(); pub async fn init_display( spi: Spi<'static, SPI1, Async>, @@ -44,26 +40,25 @@ pub async fn init_display( true, Delay, ); - let mut fb = FRAMEBUFFER.get().lock().await; display.init().await.unwrap(); display.set_custom_orientation(0x40).await.unwrap(); - display.draw(&mut fb).await.unwrap(); + unsafe { FRAMEBUFFER.draw(&mut display).await.unwrap() } display.set_on().await.unwrap(); display } pub async fn clear_fb() { - let mut fb = FRAMEBUFFER.get().lock().await; - let fb = &mut *fb; - fb.clear(Rgb565::BLACK).unwrap(); + unsafe { FRAMEBUFFER.clear(Rgb565::BLACK).unwrap() } } pub async fn display_handler(mut display: DISPLAY) { loop { - { - let mut fb = FRAMEBUFFER.get().lock().await; - display.partial_draw_batched(&mut fb).await.unwrap(); + unsafe { + FRAMEBUFFER + .partial_draw_batched(&mut display) + .await + .unwrap() } Timer::after_millis(32).await; // 30 fps diff --git a/kernel/src/framebuffer.rs b/kernel/src/framebuffer.rs new file mode 100644 index 0000000..11a832e --- /dev/null +++ b/kernel/src/framebuffer.rs @@ -0,0 +1,432 @@ +use crate::display::{SCREEN_HEIGHT, SCREEN_WIDTH}; +use core::sync::atomic::{AtomicBool, Ordering}; +use embassy_sync::lazy_lock::LazyLock; +use embedded_graphics::{ + draw_target::DrawTarget, + pixelcolor::{ + Rgb565, + raw::{RawData, RawU16}, + }, + prelude::*, + primitives::Rectangle, +}; +use embedded_hal_2::digital::OutputPin; +use embedded_hal_async::{delay::DelayNs, spi::SpiDevice}; +use heapless::Vec; +use st7365p_lcd::{FrameBuffer, ST7365P}; + +pub const TILE_SIZE: usize = 16; // 16x16 tile +pub const TILE_COUNT: usize = (SCREEN_WIDTH / TILE_SIZE) * (SCREEN_HEIGHT / TILE_SIZE); // 400 tiles + +// Group of tiles for batching +pub const MAX_META_TILES: usize = SCREEN_WIDTH / TILE_SIZE; // max number of meta tiles in buffer +type MetaTileVec = heapless::Vec; + +const SIZE: usize = SCREEN_HEIGHT * SCREEN_WIDTH; + +static mut BUFFER: [u16; SIZE] = [0; SIZE]; + +static mut DIRTY_TILES: LazyLock> = LazyLock::new(|| { + let mut tiles = Vec::new(); + for _ in 0..TILE_COUNT { + tiles.push(AtomicBool::new(true)); + } + tiles +}); + +#[allow(dead_code)] +pub struct AtomicFrameBuffer; + +impl AtomicFrameBuffer { + pub const fn new() -> Self { + Self + } + + fn mark_tiles_dirty(&mut self, rect: Rectangle) { + let tiles_x = (SCREEN_WIDTH + TILE_SIZE - 1) / TILE_SIZE; + let start_tx = (rect.top_left.x as usize) / TILE_SIZE; + let end_tx = ((rect.top_left.x + rect.size.width as i32 - 1) as usize) / TILE_SIZE; + let start_ty = (rect.top_left.y as usize) / TILE_SIZE; + let end_ty = ((rect.top_left.y + rect.size.height as i32 - 1) as usize) / TILE_SIZE; + + for ty in start_ty..=end_ty { + for tx in start_tx..=end_tx { + let tile_idx = ty * tiles_x + tx; + unsafe { DIRTY_TILES.get_mut()[tile_idx].store(true, Ordering::Relaxed) }; + } + } + } + + fn set_pixel(&mut self, x: u16, y: u16, color: u16) -> Result<(), ()> { + unsafe { BUFFER[(y as usize * SCREEN_WIDTH) + x as usize] = color }; + + Ok(()) + } + + fn set_pixels_buffered>( + &mut self, + sx: u16, + sy: u16, + ex: u16, + ey: u16, + colors: P, + ) -> Result<(), ()> { + if sx >= self.size().width as u16 - 1 + || ex >= self.size().width as u16 - 1 + || sy >= self.size().height as u16 - 1 + || ey >= self.size().height as u16 - 1 + { + return Err(()); // Bounds check + } + + let mut color_iter = colors.into_iter(); + + for y in sy..=ey { + for x in sx..=ex { + if let Some(color) = color_iter.next() { + unsafe { BUFFER[(y as usize * SCREEN_WIDTH) + x as usize] = color }; + } else { + return Err(()); // Not enough data + } + } + } + + // Optional: check that we consumed *exactly* the right amount + if color_iter.next().is_some() { + return Err(()); // Too much data + } + + Ok(()) + } + + // walk the dirty tiles and mark groups of tiles(meta-tiles) for batched updates + fn find_meta_tiles(&mut self, tiles_x: usize, tiles_y: usize) -> MetaTileVec { + let mut meta_tiles: MetaTileVec = Vec::new(); + + for ty in 0..tiles_y { + let mut tx = 0; + while tx < tiles_x { + let idx = ty * tiles_x + tx; + if !unsafe { DIRTY_TILES.get()[idx].load(Ordering::Acquire) } { + tx += 1; + continue; + } + + // Start meta-tile at this tile + let mut width_tiles = 1; + let height_tiles = 1; + + // Grow horizontally, but keep under MAX_TILES_PER_METATILE + while tx + width_tiles < tiles_x + && unsafe { + DIRTY_TILES.get()[ty * tiles_x + tx + width_tiles].load(Ordering::Relaxed) + } + && (width_tiles + height_tiles) <= MAX_META_TILES + { + width_tiles += 1; + } + + // TODO: for simplicity, skipped vertical growth + + for x_off in 0..width_tiles { + unsafe { + DIRTY_TILES.get()[ty * tiles_x + tx + x_off] + .store(false, Ordering::Release); + }; + } + + // new meta-tile pos + let rect = Rectangle::new( + Point::new((tx * TILE_SIZE) as i32, (ty * TILE_SIZE) as i32), + Size::new( + (width_tiles * TILE_SIZE) as u32, + (height_tiles * TILE_SIZE) as u32, + ), + ); + + if meta_tiles.push(rect).is_err() { + return meta_tiles; + }; + + tx += width_tiles; + } + } + + meta_tiles + } + + /// Sends the entire framebuffer to the display + pub async fn draw( + &mut self, + display: &mut ST7365P, + ) -> Result<(), ()> + where + SPI: SpiDevice, + DC: OutputPin, + RST: OutputPin, + { + display + .set_pixels_buffered( + 0, + 0, + self.size().width as u16 - 1, + self.size().height as u16 - 1, + unsafe { &BUFFER }, + ) + .await?; + + unsafe { + for tile in DIRTY_TILES.get_mut().iter() { + tile.store(false, Ordering::Release); + } + }; + + Ok(()) + } + + /// Sends only dirty tiles (16x16px) individually to the display without batching + pub async fn partial_draw( + &mut self, + display: &mut ST7365P, + ) -> Result<(), ()> + where + SPI: SpiDevice, + DC: OutputPin, + RST: OutputPin, + { + if unsafe { DIRTY_TILES.get().iter().any(|p| p.load(Ordering::Acquire)) } { + let tiles_x = (SCREEN_WIDTH + TILE_SIZE - 1) / TILE_SIZE; + let tiles_y = (SCREEN_HEIGHT + TILE_SIZE - 1) / TILE_SIZE; + + let mut tile_buffer = [0u16; TILE_SIZE * TILE_SIZE]; + + for ty in 0..tiles_y { + for tx in 0..tiles_x { + if unsafe { !DIRTY_TILES.get()[ty * tiles_x + tx].load(Ordering::Acquire) } { + continue; + } + + let x = tx * TILE_SIZE; + let y = ty * TILE_SIZE; + + // Copy pixels for the tile into tile_buffer + for row in 0..TILE_SIZE { + for col in 0..TILE_SIZE { + let actual_x = x + col; + let actual_y = y + row; + + if actual_x < SCREEN_WIDTH && actual_y < SCREEN_HEIGHT { + let idx = actual_y * SCREEN_WIDTH + actual_x; + tile_buffer[row * TILE_SIZE + col] = unsafe { BUFFER[idx] }; + } else { + // Out of bounds, fill with zero (or background) + tile_buffer[row * TILE_SIZE + col] = 0; + } + } + } + + // Send the tile's pixel data to the display + display + .set_pixels_buffered( + x as u16, + y as u16, + (x + TILE_SIZE - 1).min(SCREEN_WIDTH - 1) as u16, + (y + TILE_SIZE - 1).min(SCREEN_HEIGHT - 1) as u16, + &tile_buffer, + ) + .await?; + + // Mark tile as clean + unsafe { + DIRTY_TILES.get_mut()[ty * tiles_x + tx].store(false, Ordering::Release) + }; + } + } + } + + Ok(()) + } + + /// Sends only dirty tiles (16x16px) in batches to the display + pub async fn partial_draw_batched( + &mut self, + display: &mut ST7365P, + ) -> Result<(), ()> + where + SPI: SpiDevice, + DC: OutputPin, + RST: OutputPin, + DELAY: DelayNs, + { + if unsafe { DIRTY_TILES.get().iter().any(|p| p.load(Ordering::Acquire)) } { + let tiles_x = (SCREEN_WIDTH + TILE_SIZE - 1) / TILE_SIZE; + let tiles_y = (SCREEN_HEIGHT + TILE_SIZE - 1) / TILE_SIZE; + + let meta_tiles = self.find_meta_tiles(tiles_x, tiles_y); + + // buffer for copying meta tiles before sending to display + let mut pixel_buffer: heapless::Vec = + Vec::new(); + + for rect in meta_tiles { + let rect_width = rect.size.width as usize; + let rect_height = rect.size.height as usize; + let rect_x = rect.top_left.x as usize; + let rect_y = rect.top_left.y as usize; + + pixel_buffer.clear(); + + for row in 0..rect_height { + let y = rect_y + row; + let start = y * SCREEN_WIDTH + rect_x; + let end = start + rect_width; + + // Safe: we guarantee buffer will not exceed MAX_META_TILE_PIXELS + pixel_buffer + .extend_from_slice(unsafe { &BUFFER[start..end] }) + .unwrap(); + } + + display + .set_pixels_buffered( + rect_x as u16, + rect_y as u16, + (rect_x + rect_width - 1) as u16, + (rect_y + rect_height - 1) as u16, + &pixel_buffer, + ) + .await?; + + // walk the meta-tile and set as clean + let start_tx = rect_x / TILE_SIZE; + let start_ty = rect_y / TILE_SIZE; + let end_tx = (rect_x + rect_width - 1) / TILE_SIZE; + let end_ty = (rect_y + rect_height - 1) / TILE_SIZE; + + for ty in start_ty..=end_ty { + for tx in start_tx..=end_tx { + let tile_idx = ty * tiles_x + tx; + unsafe { DIRTY_TILES.get_mut()[tile_idx].store(false, Ordering::Release) }; + } + } + } + } + + Ok(()) + } +} + +impl DrawTarget for AtomicFrameBuffer { + type Error = (); + type Color = Rgb565; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + let mut dirty_rect: Option = None; + + for Pixel(coord, color) in pixels { + if coord.x >= 0 && coord.y >= 0 { + let x = coord.x as i32; + let y = coord.y as i32; + + if (x as usize) < SCREEN_WIDTH && (y as usize) < SCREEN_HEIGHT { + unsafe { + BUFFER[(y as usize) * SCREEN_WIDTH + (x as usize)] = + RawU16::from(color).into_inner() + }; + + if let Some(ref mut rect) = dirty_rect { + rect.top_left.x = rect.top_left.x.min(x); + rect.top_left.y = rect.top_left.y.min(y); + let max_x = (rect.top_left.x + rect.size.width as i32 - 1).max(x); + let max_y = (rect.top_left.y + rect.size.height as i32 - 1).max(y); + rect.size.width = (max_x - rect.top_left.x + 1) as u32; + rect.size.height = (max_y - rect.top_left.y + 1) as u32; + } else { + dirty_rect = Some(Rectangle::new(Point::new(x, y), Size::new(1, 1))); + } + } + } + } + + if let Some(rect) = dirty_rect { + self.mark_tiles_dirty(rect); + } + + Ok(()) + } + + fn fill_contiguous(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error> + where + I: IntoIterator, + { + let drawable_area = area.intersection(&Rectangle::new(Point::zero(), self.size())); + + if drawable_area.size != Size::zero() { + // We assume that `colors` iterator is in row-major order for the original `area` + // So we must skip rows/pixels that are clipped + let area_width = area.size.width; + let area_height = area.size.height; + let mut colors = colors.into_iter(); + + for y in 0..area_height { + for x in 0..area_width { + let p = area.top_left + Point::new(x as i32, y as i32); + + if drawable_area.contains(p) { + if let Some(color) = colors.next() { + self.set_pixel( + p.x as u16, + p.y as u16, + RawU16::from(color).into_inner(), + )?; + } else { + break; + } + } else { + // Still need to consume the color even if not used! + let _ = colors.next(); + } + } + } + + self.mark_tiles_dirty(*area); + } + + Ok(()) + } + + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + self.fill_contiguous( + area, + core::iter::repeat(color).take((self.size().width * self.size().height) as usize), + ) + } + + fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> { + self.set_pixels_buffered( + 0, + 0, + self.size().width as u16 - 1, + self.size().height as u16 - 1, + core::iter::repeat(RawU16::from(color).into_inner()) + .take((self.size().width * self.size().height) as usize), + )?; + + unsafe { + for tile in DIRTY_TILES.get_mut().iter() { + tile.store(true, Ordering::Relaxed); + } + } + + Ok(()) + } +} + +impl OriginDimensions for AtomicFrameBuffer { + fn size(&self) -> Size { + Size::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32) + } +} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 84c2ae9..73150bb 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -10,6 +10,7 @@ extern crate alloc; mod abi; mod display; mod elf; +mod framebuffer; mod peripherals; mod scsi; mod storage; diff --git a/kernel/src/ui.rs b/kernel/src/ui.rs index 481de65..03122c1 100644 --- a/kernel/src/ui.rs +++ b/kernel/src/ui.rs @@ -74,9 +74,8 @@ async fn draw_selection() { guard.selections.clone() }; - let mut fb = FRAMEBUFFER.get().lock().await; let text_style = MonoTextStyle::new(&FONT_9X15, Rgb565::WHITE); - let display_area = fb.bounding_box(); + let display_area = unsafe { FRAMEBUFFER.bounding_box() }; const NO_BINS: &str = "No Programs found on SD Card. Ensure programs end with '.bin', and are located in the root directory"; let no_bins = String::from_str(NO_BINS).unwrap(); @@ -90,13 +89,13 @@ async fn draw_selection() { ), text_style, ) - .draw(&mut *fb) + .draw(unsafe { &mut FRAMEBUFFER }) .unwrap(); } else { let mut file_names = file_names.iter(); let Some(first) = file_names.next() else { Text::new(NO_BINS, Point::zero(), text_style) - .draw(&mut *fb) + .draw(unsafe { &mut FRAMEBUFFER }) .unwrap(); return; @@ -116,7 +115,7 @@ async fn draw_selection() { .with_alignment(horizontal::Center) .arrange() .align_to(&display_area, horizontal::Center, vertical::Center) - .draw(&mut *fb) + .draw(unsafe { &mut FRAMEBUFFER }) .unwrap(); } }