diff --git a/Cargo.lock b/Cargo.lock index 19c302d..07f323a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,6 @@ version = "0.1.0" dependencies = [ "abi_sys", "embedded-graphics", - "embedded-sdmmc", "once_cell", "rand_core 0.9.3", ] @@ -31,7 +30,6 @@ dependencies = [ "cbindgen", "defmt 0.3.100", "embedded-graphics", - "embedded-sdmmc", "strum", ] @@ -183,12 +181,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bit_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - [[package]] name = "bitfield" version = "0.13.2" @@ -1024,18 +1016,6 @@ dependencies = [ "nb 1.1.0", ] -[[package]] -name = "embedded-iconoir" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52b9899b636b56d4e66834f7a90766d0bc6600c0f067d91ed0711b11fa3f5c8" -dependencies = [ - "bit_field", - "embedded-graphics", - "paste", - "static_assertions", -] - [[package]] name = "embedded-io" version = "0.6.1" @@ -1202,12 +1182,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "funty" version = "2.0.0" @@ -1355,6 +1329,7 @@ version = "0.1.0" dependencies = [ "abi", "embedded-graphics", + "selection_ui", "tinygif", ] @@ -1421,7 +1396,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" dependencies = [ "hash32", - "serde", "stable_deref_trait", ] @@ -1575,7 +1549,6 @@ dependencies = [ "embedded-text", "goblin", "heapless", - "kolibri-embedded-gui", "num_enum 0.7.4", "once_cell", "panic-probe", @@ -1589,18 +1562,6 @@ dependencies = [ "trouble-host", ] -[[package]] -name = "kolibri-embedded-gui" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011f8f415e8c2f03e4ad752afcf1bb156a18926250401b1fe29d8feda644c140" -dependencies = [ - "embedded-graphics", - "embedded-iconoir", - "foldhash", - "heapless", -] - [[package]] name = "lalrpop" version = "0.19.12" @@ -2305,6 +2266,16 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +[[package]] +name = "selection_ui" +version = "0.1.0" +dependencies = [ + "abi", + "embedded-graphics", + "embedded-layout", + "embedded-text", +] + [[package]] name = "semver" version = "0.9.0" @@ -2463,12 +2434,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "static_cell" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index e370f22..49d0de7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "kernel", "abi_sys", "abi", + "selection_ui", "user-apps/calculator", "user-apps/snake", "user-apps/gallery", @@ -27,4 +28,4 @@ panic = "abort" [profile.dev] lto = true -opt-level = "z" +opt-level = "s" diff --git a/abi/Cargo.toml b/abi/Cargo.toml index a197d01..591ddaa 100644 --- a/abi/Cargo.toml +++ b/abi/Cargo.toml @@ -4,8 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -embedded-sdmmc = { version = "0.9.0", default-features = false } -embedded-graphics = "0.8.1" abi_sys = { path = "../abi_sys" } +embedded-graphics = "0.8.1" once_cell = { version = "1", default-features = false } rand_core = "0.9.3" diff --git a/abi/src/lib.rs b/abi/src/lib.rs index 3d0778e..366059c 100644 --- a/abi/src/lib.rs +++ b/abi/src/lib.rs @@ -46,7 +46,6 @@ pub fn get_key() -> KeyEvent { pub mod display { use abi_sys::CPixel; - use alloc::{vec, vec::Vec}; use embedded_graphics::{ Pixel, geometry::{Dimensions, Point}, @@ -54,7 +53,6 @@ pub mod display { prelude::{DrawTarget, Size}, primitives::Rectangle, }; - use once_cell::unsync::Lazy; pub const SCREEN_WIDTH: usize = 320; pub const SCREEN_HEIGHT: usize = 320; @@ -142,7 +140,9 @@ impl RngCore for Rng { } pub mod fs { - use embedded_sdmmc::DirEntry; + use core::fmt::Display; + + use alloc::{format, vec::Vec}; pub fn read_file(file: &str, read_from: usize, buf: &mut [u8]) -> usize { abi_sys::read_file( @@ -154,8 +154,85 @@ pub mod fs { ) } - pub fn list_dir(path: &str, files: &mut [Option]) -> usize { - abi_sys::list_dir(path.as_ptr(), path.len(), files.as_mut_ptr(), files.len()) + pub struct FileName<'a> { + full: &'a str, + base: &'a str, + ext: Option<&'a str>, + } + + impl<'a> FileName<'a> { + pub fn full_name(&self) -> &str { + self.full + } + + pub fn base(&self) -> &str { + self.base + } + + pub fn extension(&self) -> Option<&str> { + self.ext + } + } + + impl<'a> Display for FileName<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.full_name()) + } + } + + impl<'a> From<&'a str> for FileName<'a> { + fn from(s: &'a str) -> FileName<'a> { + let full = s; + + // Split on last dot for extension + let (base, ext) = match s.rfind('.') { + Some(idx) => (&s[..idx], Some(&s[idx + 1..])), + None => (s, None), + }; + + FileName { full, base, ext } + } + } + + const MAX_ENTRY_NAME_LEN: usize = 25; + const MAX_ENTRIES: usize = 25; + + #[derive(Clone, Copy)] + pub struct Entries([[u8; MAX_ENTRY_NAME_LEN]; MAX_ENTRIES]); + + impl Entries { + pub fn new() -> Self { + Self([[0; MAX_ENTRY_NAME_LEN]; MAX_ENTRIES]) + } + + /// Get list of file names after listing + pub fn entries<'a>(&'a self) -> Vec> { + self.0 + .iter() + .filter_map(|buf| { + let nul_pos = buf.iter().position(|&b| b == 0).unwrap_or(buf.len()); + Some(core::str::from_utf8(&buf[..nul_pos]).ok()?.into()) + }) + .collect() + } + + fn as_ptrs(&mut self) -> [*mut u8; MAX_ENTRIES] { + let mut ptrs: [*mut u8; MAX_ENTRIES] = [core::ptr::null_mut(); MAX_ENTRIES]; + for (i, buf) in self.0.iter_mut().enumerate() { + ptrs[i] = buf.as_mut_ptr(); + } + ptrs + } + } + + pub fn list_dir(path: &str, entries: &mut Entries) -> usize { + abi_sys::list_dir( + path.as_ptr(), + path.len(), + entries.as_ptrs().as_mut_ptr(), + MAX_ENTRIES, + MAX_ENTRY_NAME_LEN, + ) } pub fn file_len(str: &str) -> usize { diff --git a/abi_sys/Cargo.toml b/abi_sys/Cargo.toml index e30198a..5a83d0b 100644 --- a/abi_sys/Cargo.toml +++ b/abi_sys/Cargo.toml @@ -12,7 +12,6 @@ defmt = ["dep:defmt"] strum = { version = "0.27.2", default-features = false, features = ["derive"] } bitflags = "2.9.4" embedded-graphics = "0.8.1" -embedded-sdmmc = { version = "0.9.0", default-features = false } defmt = { version = "0.3", optional = true } [build-dependencies] diff --git a/abi_sys/src/lib.rs b/abi_sys/src/lib.rs index ed9e9f6..3243771 100644 --- a/abi_sys/src/lib.rs +++ b/abi_sys/src/lib.rs @@ -1,14 +1,13 @@ #![no_std] - #[cfg(feature = "alloc")] use core::alloc::Layout; +use core::ffi::c_char; use embedded_graphics::{ Pixel, pixelcolor::{Rgb565, raw::RawU16}, prelude::{IntoStorage, Point}, }; -use embedded_sdmmc::DirEntry; use strum::{EnumCount, EnumIter}; pub type EntryFn = fn(); @@ -396,21 +395,23 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) { pub type ListDir = extern "C" fn( str: *const u8, len: usize, - files: *mut Option, + entries: *mut *mut c_char, file_len: usize, + max_entry_str_len: usize, ) -> usize; #[unsafe(no_mangle)] pub extern "C" fn list_dir( str: *const u8, len: usize, - files: *mut Option, - file_len: usize, + entries: *mut *mut c_char, + entry_count: usize, + max_entry_str_len: usize, ) -> usize { unsafe { let ptr = CALL_ABI_TABLE[CallTable::ListDir as usize]; let f: ListDir = core::mem::transmute(ptr); - f(str, len, files, file_len) + f(str, len, entries, entry_count, max_entry_str_len) } } diff --git a/justfile b/justfile index fee1f63..8ddc020 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ kernel-dev board: - cargo run --bin kernel --features {{board}} + cargo run --bin kernel --features {{board}} --features fps kernel-release-probe board: - cargo run --bin kernel --profile release --features {{board}} + cargo run --bin kernel --profile release --features {{board}} --features fps kernel-release board: cargo build --bin kernel --release --no-default-features --features {{board}} elf2uf2-rs -d target/thumbv8m.main-none-eabihf/release/kernel diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index d890cba..943a1b0 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -12,10 +12,12 @@ bench = false [features] default = ["rp235x", "defmt"] -pimoroni2w = ["rp235x"] -rp2040 = ["embassy-rp/rp2040"] +pimoroni2w = ["rp235x", "psram"] +# rp2040 = ["embassy-rp/rp2040"] # unsupported, ram too small for fb rp235x = ["embassy-rp/rp235xb"] trouble = ["dep:bt-hci", "dep:cyw43", "dep:cyw43-pio", "dep:trouble-host"] +psram = ["dep:embedded-alloc"] +fps = [] defmt = [ "dep:defmt", "panic-probe/print-defmt", @@ -78,7 +80,6 @@ st7365p-lcd = { git = "https://github.com/legitcamper/st7365p-lcd-rs", rev = "a7 embedded-graphics = { version = "0.8.1" } embedded-text = "0.7.2" embedded-layout = "0.4.2" -kolibri-embedded-gui = "0.1.0" strum = { version = "0.27.2", default-features = false } rand = { version = "0.9.0", default-features = false } @@ -90,7 +91,9 @@ spin = "0.10.0" num_enum = { version = "0.7.4", default-features = false } goblin = { version = "0.10.1", default-features = false, features = ["elf32"] } talc = "4.4.3" -embedded-alloc = { version = "0.6.0", features = ["allocator_api"] } +embedded-alloc = { version = "0.6.0", features = [ + "allocator_api", +], optional = true } bumpalo = "3.19.0" abi_sys = { path = "../abi_sys" } diff --git a/kernel/src/abi.rs b/kernel/src/abi.rs index 9f64223..4d8c52d 100644 --- a/kernel/src/abi.rs +++ b/kernel/src/abi.rs @@ -3,13 +3,16 @@ use abi_sys::{ PrintAbi, ReadFile, RngRequest, SleepMsAbi, keyboard::*, }; use alloc::{string::ToString, vec::Vec}; -use core::{alloc::GlobalAlloc, sync::atomic::Ordering}; +use core::{alloc::GlobalAlloc, ffi::c_char, ptr, sync::atomic::Ordering}; use embassy_rp::clocks::{RoscRng, clk_sys_freq}; use embassy_time::Instant; use embedded_graphics::draw_target::DrawTarget; -use embedded_sdmmc::{DirEntry, LfnBuffer}; +use embedded_sdmmc::LfnBuffer; use heapless::spsc::Queue; +#[cfg(feature = "psram")] +use crate::heap::HEAP; + use crate::{ display::FRAMEBUFFER, framebuffer::FB_PAUSED, @@ -20,10 +23,14 @@ const _: AllocAbi = alloc; pub extern "C" fn alloc(layout: CLayout) -> *mut u8 { // SAFETY: caller guarantees layout is valid unsafe { - if cfg!(feature = "pimoroni2w") { - crate::heap::HEAP.alloc(layout.into()) - } else { - alloc::alloc::alloc(layout.into()) + #[cfg(feature = "psram")] + { + return HEAP.alloc(layout.into()); + } + + #[cfg(not(feature = "psram"))] + { + return alloc::alloc::alloc(layout.into()); } } } @@ -31,12 +38,14 @@ pub extern "C" fn alloc(layout: CLayout) -> *mut u8 { const _: DeallocAbi = dealloc; pub extern "C" fn dealloc(ptr: *mut u8, layout: CLayout) { // SAFETY: caller guarantees ptr and layout are valid - unsafe { - if cfg!(feature = "pimoroni2w") { - crate::heap::HEAP.dealloc(ptr, layout.into()) - } else { - alloc::alloc::dealloc(ptr, layout.into()) - } + #[cfg(feature = "psram")] + { + unsafe { HEAP.dealloc(ptr, layout.into()) } + } + + #[cfg(not(feature = "psram"))] + { + unsafe { alloc::alloc::dealloc(ptr, layout.into()) } } } @@ -116,11 +125,27 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) { } } -fn get_dir_entries(dir: &Dir, files: &mut [Option]) -> usize { +unsafe fn copy_entry_to_user_buf(name: &[u8], dest: *mut c_char, max_str_len: usize) { + if !dest.is_null() { + let len = name.len().min(max_str_len - 1); + unsafe { + ptr::copy_nonoverlapping(name.as_ptr(), dest as *mut u8, len); + *dest.add(len) = 0; // nul terminator + } + } +} + +unsafe fn get_dir_entries(dir: &Dir, entries: &mut [*mut c_char], max_str_len: usize) -> usize { + let mut b = [0; 25]; + let mut buf = LfnBuffer::new(&mut b); let mut i = 0; - dir.iterate_dir(|entry| { - if i < files.len() { - files[i] = Some(entry.clone()); + dir.iterate_dir_lfn(&mut buf, |entry, lfn_name| { + if i < entries.len() { + if let Some(name) = lfn_name { + unsafe { copy_entry_to_user_buf(name.as_bytes(), entries[i], max_str_len) }; + } else { + unsafe { copy_entry_to_user_buf(entry.name.base_name(), entries[i], max_str_len) }; + } i += 1; } }) @@ -128,24 +153,30 @@ fn get_dir_entries(dir: &Dir, files: &mut [Option]) -> usize { i } -fn recurse_dir(dir: &Dir, dirs: &[&str], files: &mut [Option]) -> usize { +unsafe fn recurse_dir( + dir: &Dir, + dirs: &[&str], + entries: &mut [*mut c_char], + max_str_len: usize, +) -> usize { if dirs.is_empty() { - return get_dir_entries(dir, files); + return unsafe { get_dir_entries(dir, entries, max_str_len) }; } let dir = dir.open_dir(dirs[0]).unwrap(); - recurse_dir(&dir, &dirs[1..], files) + unsafe { recurse_dir(&dir, &dirs[1..], entries, max_str_len) } } const _: ListDir = list_dir; pub extern "C" fn list_dir( dir: *const u8, len: usize, - files: *mut Option, + entries: *mut *mut c_char, files_len: usize, + max_entry_str_len: usize, ) -> usize { // SAFETY: caller guarantees `ptr` is valid for `len` bytes - let files = unsafe { core::slice::from_raw_parts_mut(files, files_len) }; + let files = unsafe { core::slice::from_raw_parts_mut(entries, files_len) }; // SAFETY: caller guarantees `ptr` is valid for `len` bytes let dir = unsafe { core::str::from_raw_parts(dir, len) }; let dirs: Vec<&str> = dir.split('/').collect(); @@ -156,10 +187,12 @@ pub extern "C" fn list_dir( let mut wrote = 0; sd.access_root_dir(|root| { if dirs[0] == "" && dirs.len() >= 2 { - if dir == "/" { - wrote = get_dir_entries(&root, files); - } else { - wrote = recurse_dir(&root, &dirs[1..], files); + unsafe { + if dir == "/" { + wrote = get_dir_entries(&root, files, max_entry_str_len); + } else { + wrote = recurse_dir(&root, &dirs[1..], files, max_entry_str_len); + } } } }); diff --git a/kernel/src/display.rs b/kernel/src/display.rs index c576c06..2fdd9f2 100644 --- a/kernel/src/display.rs +++ b/kernel/src/display.rs @@ -15,6 +15,12 @@ use embedded_graphics::{ use embedded_hal_bus::spi::ExclusiveDevice; use st7365p_lcd::ST7365P; +#[cfg(feature = "psram")] +use crate::heap::HEAP; + +#[cfg(feature = "fps")] +pub use framebuffer::fps::{FPS_CANVAS, FPS_COUNTER}; + type DISPLAY = ST7365P< ExclusiveDevice, Output<'static>, Delay>, Output<'static>, @@ -29,18 +35,21 @@ pub static mut FRAMEBUFFER: Option = None; fn init_fb() { unsafe { - FRAMEBUFFER = Some(if cfg!(not(feature = "pimoroni2w")) { - static mut BUF: [u16; framebuffer::SIZE] = [0; framebuffer::SIZE]; - AtomicFrameBuffer::new(&mut BUF) - } else { - let slab = crate::heap::HEAP.alloc(Layout::array::(framebuffer::SIZE).unwrap()) - as *mut u16; + #[cfg(feature = "psram")] + { + let slab = HEAP.alloc(Layout::array::(framebuffer::SIZE).unwrap()) as *mut u16; let buf = core::slice::from_raw_parts_mut(slab, framebuffer::SIZE); let mut fb = AtomicFrameBuffer::new(buf); fb.clear(Rgb565::BLACK).unwrap(); - fb - }); + FRAMEBUFFER = Some(fb); + } + + #[cfg(not(feature = "psram"))] + { + static mut BUF: [u16; framebuffer::SIZE] = [0; framebuffer::SIZE]; + FRAMEBUFFER = Some(AtomicFrameBuffer::new(&mut BUF)); + } } } @@ -79,18 +88,26 @@ pub async fn init_display( #[embassy_executor::task] pub async fn display_handler(mut display: DISPLAY) { loop { + // renders fps text to canvas + #[cfg(feature = "fps")] + unsafe { + if FPS_COUNTER.should_draw() { + FPS_CANVAS.draw_fps().await; + } + } + if !FB_PAUSED.load(Ordering::Acquire) { unsafe { FRAMEBUFFER .as_mut() .unwrap() - .safe_draw(&mut display) + .partial_draw(&mut display) .await .unwrap() }; } // small yield to allow other tasks to run - Timer::after_nanos(500).await; + Timer::after_millis(10).await; } } diff --git a/kernel/src/framebuffer.rs b/kernel/src/framebuffer.rs index 918857f..994b4b4 100644 --- a/kernel/src/framebuffer.rs +++ b/kernel/src/framebuffer.rs @@ -1,6 +1,5 @@ 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::{ @@ -14,32 +13,36 @@ use embedded_hal_2::digital::OutputPin; use embedded_hal_async::{delay::DelayNs, spi::SpiDevice}; use st7365p_lcd::ST7365P; -pub const TILE_SIZE: usize = 16; // 16x16 tile -pub const TILE_COUNT: usize = (SCREEN_WIDTH / TILE_SIZE) * (SCREEN_HEIGHT / TILE_SIZE); // 400 tiles +#[cfg(feature = "fps")] +use fps::{FPS_CANVAS, FPS_CANVAS_HEIGHT, FPS_CANVAS_WIDTH, FPS_CANVAS_X, FPS_CANVAS_Y}; -// Group of tiles for batching -pub const MAX_META_TILES: usize = (SCREEN_WIDTH / TILE_SIZE) * 2; // max number of meta tiles in buffer -type MetaTileVec = heapless::Vec; +const TILE_SIZE: usize = 16; // 16x16 tile +const TILE_COUNT: usize = (SCREEN_WIDTH / TILE_SIZE) * (SCREEN_HEIGHT / TILE_SIZE); // 400 tiles +const NUM_TILE_ROWS: usize = SCREEN_WIDTH / TILE_SIZE; +const NUM_TILE_COLS: usize = SCREEN_WIDTH / TILE_SIZE; + +const MAX_BATCH_TILES: usize = (SCREEN_WIDTH / TILE_SIZE) * 2; +type BatchTileBuf = [u16; MAX_BATCH_TILES * TILE_SIZE * TILE_SIZE]; pub const SIZE: usize = SCREEN_HEIGHT * SCREEN_WIDTH; pub static FB_PAUSED: AtomicBool = AtomicBool::new(false); -static mut DIRTY_TILES: LazyLock> = LazyLock::new(|| { - let mut tiles = heapless::Vec::new(); - for _ in 0..TILE_COUNT { - tiles.push(AtomicBool::new(true)).unwrap(); - } - tiles -}); - #[allow(dead_code)] -pub struct AtomicFrameBuffer<'a>(&'a mut [u16]); +pub struct AtomicFrameBuffer<'a> { + fb: &'a mut [u16], + dirty_tiles: [AtomicBool; TILE_COUNT], + batch_tile_buf: BatchTileBuf, +} impl<'a> AtomicFrameBuffer<'a> { pub fn new(buffer: &'a mut [u16]) -> Self { assert!(buffer.len() == SIZE); - Self(buffer) + Self { + fb: buffer, + dirty_tiles: core::array::from_fn(|_| AtomicBool::new(true)), + batch_tile_buf: [0; MAX_BATCH_TILES * TILE_SIZE * TILE_SIZE], + } } fn mark_tiles_dirty(&mut self, rect: Rectangle) { @@ -52,7 +55,7 @@ impl<'a> AtomicFrameBuffer<'a> { 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::Release) }; + self.dirty_tiles[tile_idx].store(true, Ordering::Release); } } } @@ -78,7 +81,7 @@ impl<'a> AtomicFrameBuffer<'a> { for y in sy..=ey { for x in sx..=ex { if let Some(color) = color_iter.next() { - self.0[(y as usize * SCREEN_WIDTH) + x as usize] = color; + self.fb[(y as usize * SCREEN_WIDTH) + x as usize] = color; } else { return Err(()); // Not enough data } @@ -93,60 +96,17 @@ impl<'a> AtomicFrameBuffer<'a> { 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 = heapless::Vec::new(); + // Checks if a full draw would be faster than individual tile batches + fn should_full_draw(&self) -> bool { + let threshold_pixels = SIZE * 80 / 100; + let mut dirty_pixels = 0; - 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::Acquire) - } - && (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; + self.dirty_tiles.iter().any(|tile| { + if tile.load(Ordering::Acquire) { + dirty_pixels += TILE_SIZE * TILE_SIZE; } - } - - meta_tiles + dirty_pixels >= threshold_pixels + }) } /// Sends the entire framebuffer to the display @@ -165,71 +125,104 @@ impl<'a> AtomicFrameBuffer<'a> { 0, self.size().width as u16 - 1, self.size().height as u16 - 1, - &self.0[..], + &self.fb[..], ) .await?; + for tile in self.dirty_tiles.iter() { + tile.store(false, Ordering::Release); + } + + #[cfg(feature = "fps")] unsafe { - for tile in DIRTY_TILES.get_mut().iter() { - tile.store(false, Ordering::Release); - } - }; - - Ok(()) - } - - pub async fn safe_draw( - &mut self, - display: &mut ST7365P, - ) -> Result<(), ()> - where - SPI: SpiDevice, - DC: OutputPin, - RST: OutputPin, - DELAY: DelayNs, - { - let tiles_x = SCREEN_WIDTH / TILE_SIZE; - let _tiles_y = SCREEN_HEIGHT / TILE_SIZE; - - let tiles = unsafe { DIRTY_TILES.get_mut() }; - let mut pixel_buffer: heapless::Vec = heapless::Vec::new(); - - for tile_idx in 0..TILE_COUNT { - if tiles[tile_idx].swap(false, Ordering::AcqRel) { - let tx = tile_idx % tiles_x; - let ty = tile_idx / tiles_x; - - let x_start = tx * TILE_SIZE; - let y_start = ty * TILE_SIZE; - - let x_end = (x_start + TILE_SIZE).min(SCREEN_WIDTH); - let y_end = (y_start + TILE_SIZE).min(SCREEN_HEIGHT); - - pixel_buffer.clear(); - - for y in y_start..y_end { - let start = y * SCREEN_WIDTH + x_start; - let end = y * SCREEN_WIDTH + x_end; - pixel_buffer.extend_from_slice(&self.0[start..end]).unwrap(); - } - - display - .set_pixels_buffered( - x_start as u16, - y_start as u16, - (x_end - 1) as u16, - (y_end - 1) as u16, - &pixel_buffer, - ) - .await - .unwrap(); - } + crate::display::FPS_COUNTER.measure() } Ok(()) } - /// Sends only dirty tiles (16x16px) in batches to the display + // used when doing a full screen refresh fps must be drawn into fb + // unfortunately it is not garenteed to not be drawn over before + // being pushed to the display + #[cfg(feature = "fps")] + pub fn draw_fps_into_fb(&mut self) { + unsafe { + let canvas = &FPS_CANVAS.canvas; + + for y in 0..FPS_CANVAS_HEIGHT { + let fb_y = FPS_CANVAS_Y + y; + let fb_row_start = fb_y * SCREEN_WIDTH + FPS_CANVAS_X; + let canvas_row_start = y * FPS_CANVAS_WIDTH; + + self.fb[fb_row_start..fb_row_start + FPS_CANVAS_WIDTH].copy_from_slice( + &canvas[canvas_row_start..canvas_row_start + FPS_CANVAS_WIDTH], + ); + } + } + } + + // copy N tiles horizontally to the right into batch tile buf + fn append_tiles_to_batch( + &mut self, + tile_x: u16, + tile_y: u16, + total_tiles: u16, // number of tiles being written to buf + ) { + debug_assert!(total_tiles as usize <= NUM_TILE_COLS); + for batch_row_num in 0..TILE_SIZE { + let batch_row_offset = batch_row_num * total_tiles as usize * TILE_SIZE; + let batch_row = &mut self.batch_tile_buf + [batch_row_offset..batch_row_offset + (total_tiles as usize * TILE_SIZE)]; + + let fb_row_offset = (tile_y as usize * TILE_SIZE + batch_row_num) * SCREEN_WIDTH + + tile_x as usize * TILE_SIZE; + let fb_row = + &self.fb[fb_row_offset..fb_row_offset + (total_tiles as usize * TILE_SIZE)]; + + batch_row.copy_from_slice(fb_row); + + // override fps pixel region with fps + // avoids writing to fps, and having it overridden before draw + #[cfg(feature = "fps")] + { + let global_y = tile_y as usize * TILE_SIZE + batch_row_num; + + if global_y >= FPS_CANVAS_Y && global_y < FPS_CANVAS_Y + FPS_CANVAS_HEIGHT { + let start_x = tile_x as usize * TILE_SIZE; + let end_x = start_x + (total_tiles as usize * TILE_SIZE); + + // horizontal overlap check + let fps_x0 = FPS_CANVAS_X; + let fps_x1 = FPS_CANVAS_X + FPS_CANVAS_WIDTH; + + let x0 = start_x.max(fps_x0); + let x1 = end_x.min(fps_x1); + + if x1 > x0 { + let row_in_fps = global_y - FPS_CANVAS_Y; + let fps_off = row_in_fps + .checked_mul(FPS_CANVAS_WIDTH) + .and_then(|v| v.checked_add(x0 - fps_x0)); + let batch_off = x0 - start_x; + let len = x1 - x0; + + if let Some(fps_off) = fps_off { + let fps_len_ok = fps_off + len <= unsafe { FPS_CANVAS.canvas.len() }; + let batch_len_ok = batch_off + len <= batch_row.len(); + + if fps_len_ok && batch_len_ok { + batch_row[batch_off..batch_off + len].copy_from_slice(unsafe { + &FPS_CANVAS.canvas[fps_off..fps_off + len] + }); + } + } + } + } + } + } + } + + // Pushes tiles to the display in batches to avoid full frame pushes (unless needed) pub async fn partial_draw( &mut self, display: &mut ST7365P, @@ -240,58 +233,77 @@ impl<'a> AtomicFrameBuffer<'a> { 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; + if self.should_full_draw() { + #[cfg(feature = "fps")] + self.draw_fps_into_fb(); + return self.draw(display).await; + } - let meta_tiles = self.find_meta_tiles(tiles_x, tiles_y); + #[cfg(feature = "fps")] + { + let fps_tile_x = FPS_CANVAS_X / TILE_SIZE; + let fps_tile_y = FPS_CANVAS_Y / TILE_SIZE; + let fps_tile_w = (FPS_CANVAS_WIDTH + TILE_SIZE - 1) / TILE_SIZE; + let fps_tile_h = (FPS_CANVAS_HEIGHT + TILE_SIZE - 1) / TILE_SIZE; - // buffer for copying meta tiles before sending to display - let mut pixel_buffer: heapless::Vec = - heapless::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(&self.0[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) }; - } + for ty in fps_tile_y..fps_tile_y + fps_tile_h { + for tx in fps_tile_x..fps_tile_x + fps_tile_w { + self.dirty_tiles[ty * NUM_TILE_COLS + tx].store(true, Ordering::Release); } } } + for tile_row in 0..NUM_TILE_ROWS { + let row_start_idx = tile_row * NUM_TILE_COLS; + let mut col = 0; + + while col < NUM_TILE_COLS { + // Check for dirty tile + if self.dirty_tiles[row_start_idx + col].swap(false, Ordering::Acquire) { + let run_start = col; + let mut run_len = 1; + + // Extend run while contiguous dirty tiles and within MAX_BATCH_TILES + while col + 1 < NUM_TILE_COLS + && self.dirty_tiles[row_start_idx + col + 1].load(Ordering::Acquire) + && run_len < MAX_BATCH_TILES + { + col += 1; + run_len += 1; + } + + // Copy the whole horizontal run into the batch buffer in one call + let tile_x = run_start; + let tile_y = tile_row; + self.append_tiles_to_batch(tile_x as u16, tile_y as u16, run_len as u16); + + // Compute coordinates for display write + let start_x = tile_x * TILE_SIZE; + let end_x = start_x + run_len * TILE_SIZE - 1; + let start_y = tile_y * TILE_SIZE; + let end_y = start_y + TILE_SIZE - 1; + + // Send batch to display + display + .set_pixels_buffered( + start_x as u16, + start_y as u16, + end_x as u16, + end_y as u16, + &self.batch_tile_buf[..run_len * TILE_SIZE * TILE_SIZE], + ) + .await?; + } + + col += 1; + } + } + + #[cfg(feature = "fps")] + unsafe { + crate::display::FPS_COUNTER.measure() + } + Ok(()) } } @@ -309,14 +321,14 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> { 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; + let x = coord.x; + let y = coord.y; if (x as usize) < SCREEN_WIDTH && (y as usize) < SCREEN_HEIGHT { let idx = (y as usize) * SCREEN_WIDTH + (x as usize); let raw_color = RawU16::from(color).into_inner(); - if self.0[idx] != raw_color { - self.0[idx] = raw_color; + if self.fb[idx] != raw_color { + self.fb[idx] = raw_color; changed = true; } @@ -365,8 +377,8 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> { if let Some(color) = colors.next() { let idx = (p.y as usize * SCREEN_WIDTH) + (p.x as usize); let raw_color = RawU16::from(color).into_inner(); - if self.0[idx] != raw_color { - self.0[idx] = raw_color; + if self.fb[idx] != raw_color { + self.fb[idx] = raw_color; changed = true; } } else { @@ -404,7 +416,7 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> { .take((self.size().width * self.size().height) as usize), )?; - for tile in unsafe { DIRTY_TILES.get_mut() }.iter() { + for tile in self.dirty_tiles.iter() { tile.store(true, Ordering::Release); } @@ -417,3 +429,146 @@ impl<'a> OriginDimensions for AtomicFrameBuffer<'a> { Size::new(SCREEN_WIDTH as u32, SCREEN_HEIGHT as u32) } } + +#[cfg(feature = "fps")] +pub mod fps { + use crate::display::SCREEN_WIDTH; + use core::fmt::Write; + use embassy_time::{Duration, Instant}; + use embedded_graphics::{ + Drawable, Pixel, + draw_target::DrawTarget, + geometry::Point, + mono_font::{MonoTextStyle, ascii::FONT_8X13}, + pixelcolor::Rgb565, + prelude::{IntoStorage, OriginDimensions, RgbColor, Size}, + text::{Alignment, Text}, + }; + + pub static mut FPS_COUNTER: FpsCounter = FpsCounter::new(); + pub static mut FPS_CANVAS: FpsCanvas = FpsCanvas::new(); + + // "FPS: 120" = 8 len + const FPS_LEN: usize = 8; + pub const FPS_CANVAS_WIDTH: usize = (FONT_8X13.character_size.width + 4) as usize * FPS_LEN; + pub const FPS_CANVAS_HEIGHT: usize = FONT_8X13.character_size.height as usize; + + // puts canvas in the top right of the display + // top left point of canvas + pub const FPS_CANVAS_X: usize = SCREEN_WIDTH - FPS_CANVAS_WIDTH; + pub const FPS_CANVAS_Y: usize = 0; + + pub struct FpsCanvas { + pub canvas: [u16; FPS_CANVAS_HEIGHT * FPS_CANVAS_WIDTH], + } + + impl FpsCanvas { + const fn new() -> Self { + Self { + canvas: [0; FPS_CANVAS_HEIGHT * FPS_CANVAS_WIDTH], + } + } + + fn clear(&mut self) { + for p in &mut self.canvas { + *p = 0; + } + } + + pub async fn draw_fps(&mut self) { + let mut buf: heapless::String = heapless::String::new(); + let fps = unsafe { FPS_COUNTER.smoothed }; + let _ = write!(buf, "FPS: {}", fps as u8); + + self.clear(); + let text_style = MonoTextStyle::new(&FONT_8X13, Rgb565::WHITE); + Text::with_alignment( + buf.as_str(), + Point::new( + FPS_CANVAS_WIDTH as i32 / 2, + (FPS_CANVAS_HEIGHT as i32 + 8) / 2, + ), + text_style, + Alignment::Center, + ) + .draw(self) + .unwrap(); + } + } + + impl DrawTarget for FpsCanvas { + type Error = (); + type Color = Rgb565; + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for Pixel(point, color) in pixels { + if point.x < 0 + || point.x >= FPS_CANVAS_WIDTH as i32 + || point.y < 0 + || point.y >= FPS_CANVAS_HEIGHT as i32 + { + continue; + } + + let index = (point.y as usize) * FPS_CANVAS_WIDTH + point.x as usize; + self.canvas[index] = color.into_storage(); + } + Ok(()) + } + } + + impl OriginDimensions for FpsCanvas { + fn size(&self) -> Size { + Size::new(FPS_CANVAS_WIDTH as u32, FPS_CANVAS_HEIGHT as u32) + } + } + + pub struct FpsCounter { + last_frame: Option, + smoothed: f32, + last_draw: Option, + } + + impl FpsCounter { + pub const fn new() -> Self { + Self { + last_frame: None, + smoothed: 0.0, + last_draw: None, + } + } + + // Is called once per frame or partial frame to update FPS + pub fn measure(&mut self) { + let now = Instant::now(); + + if let Some(last) = self.last_frame { + let dt_us = (now - last).as_micros() as f32; + if dt_us > 0.0 { + let current = 1_000_000.0 / dt_us; + self.smoothed = if self.smoothed == 0.0 { + current + } else { + 0.9 * self.smoothed + 0.1 * current + }; + } + } + + self.last_frame = Some(now); + } + + pub fn should_draw(&mut self) -> bool { + let now = Instant::now(); + match self.last_draw { + Some(last) if now - last < Duration::from_millis(200) => false, + _ => { + self.last_draw = Some(now); + true + } + } + } + } +} diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 20f36f8..9d864fc 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -12,17 +12,22 @@ mod abi; mod display; mod elf; mod framebuffer; -mod heap; mod peripherals; -mod psram; mod scsi; mod storage; mod ui; mod usb; mod utils; -#[cfg(feature = "pimoroni2w")] -use crate::{heap::init_qmi_psram_heap, psram::init_psram_qmi}; +#[cfg(feature = "psram")] +#[allow(unused)] +mod heap; +#[cfg(feature = "psram")] +#[allow(unused)] +mod psram; + +#[cfg(feature = "psram")] +use crate::{heap::HEAP, heap::init_qmi_psram_heap, psram::init_psram, psram::init_psram_qmi}; use crate::{ abi::{KEY_CACHE, MS_SINCE_LAUNCH}, @@ -31,7 +36,6 @@ use crate::{ conf_peripherals, keyboard::{KeyState, read_keyboard_fifo}, }, - psram::init_psram, scsi::MSC_SHUTDOWN, storage::{SDCARD, SdCard}, ui::{SELECTIONS, clear_selection, ui_handler}, @@ -225,6 +229,7 @@ struct Sd { cs: Peri<'static, PIN_17>, det: Peri<'static, PIN_22>, } +#[allow(dead_code)] struct Psram { pio: Peri<'static, PIO0>, sclk: Peri<'static, PIN_21>, @@ -266,22 +271,22 @@ async fn setup_display(display: Display, spawner: Spawner) { // psram is kind of useless on the pico calc // ive opted to use the pimoroni with on onboard xip psram instead -async fn setup_psram(psram: Psram) { - let psram = init_psram( - psram.pio, psram.sclk, psram.mosi, psram.miso, psram.cs, psram.dma1, psram.dma2, - ) - .await; +// async fn setup_psram(psram: Psram) { +// let psram = init_psram( +// psram.pio, psram.sclk, psram.mosi, psram.miso, psram.cs, psram.dma1, psram.dma2, +// ) +// .await; - #[cfg(feature = "defmt")] - defmt::info!("psram size: {}", psram.size); +// #[cfg(feature = "defmt")] +// defmt::info!("psram size: {}", psram.size); - if psram.size == 0 { - #[cfg(feature = "defmt")] - defmt::info!("\u{1b}[1mExternal PSRAM was NOT found!\u{1b}[0m"); - } -} +// if psram.size == 0 { +// #[cfg(feature = "defmt")] +// defmt::info!("\u{1b}[1mExternal PSRAM was NOT found!\u{1b}[0m"); +// } +// } -#[cfg(feature = "pimoroni2w")] +#[cfg(feature = "psram")] async fn setup_qmi_psram() { Timer::after_millis(250).await; let psram_qmi_size = init_psram_qmi(&embassy_rp::pac::QMI, &embassy_rp::pac::XIP_CTRL); @@ -318,7 +323,7 @@ async fn kernel_task( watchdog: Peri<'static, WATCHDOG>, display: Display, sd: Sd, - psram: Psram, + _psram: Psram, mcu: Mcu, usb: Peri<'static, USB>, ) { @@ -328,10 +333,15 @@ async fn kernel_task( setup_mcu(mcu).await; + #[cfg(feature = "defmt")] + defmt::info!("setting up psram"); + Timer::after_millis(100).await; + // setup_psram(psram).await; - #[cfg(feature = "pimoroni2w")] + #[cfg(feature = "psram")] setup_qmi_psram().await; + Timer::after_millis(100).await; setup_display(display, spawner).await; setup_sd(sd).await; diff --git a/kernel/src/psram.rs b/kernel/src/psram.rs index 32ed6fd..91d447a 100644 --- a/kernel/src/psram.rs +++ b/kernel/src/psram.rs @@ -549,13 +549,13 @@ pub fn init_psram_qmi( let min_deselect: u32 = ((18 * 1_000_000 + (clock_period_fs - 1)) / clock_period_fs - u64::from(divisor + 1) / 2) as u32; - #[cfg(feature = "defmt")] - defmt::info!( - "clock_period_fs={} max_select={} min_deselect={}", - clock_period_fs, - max_select, - min_deselect - ); + // #[cfg(feature = "defmt")] + // defmt::info!( + // "clock_period_fs={} max_select={} min_deselect={}", + // clock_period_fs, + // max_select, + // min_deselect + // ); qmi.direct_csr().write(|w| { w.set_clkdiv(10); diff --git a/selection_ui/Cargo.toml b/selection_ui/Cargo.toml new file mode 100644 index 0000000..263d6e3 --- /dev/null +++ b/selection_ui/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "selection_ui" +version = "0.1.0" +edition = "2024" + +[dependencies] +abi = { path = "../abi" } +embedded-graphics = "0.8.1" +embedded-layout = "0.4.2" +embedded-text = "0.7.3" diff --git a/selection_ui/src/lib.rs b/selection_ui/src/lib.rs new file mode 100644 index 0000000..6a58b3a --- /dev/null +++ b/selection_ui/src/lib.rs @@ -0,0 +1,119 @@ +#![no_std] + +extern crate alloc; + +use abi::{ + display::Display, + get_key, + keyboard::{KeyCode, KeyState}, + print, sleep, +}; +use alloc::vec::Vec; +use embedded_graphics::{ + Drawable, + mono_font::{MonoTextStyle, ascii::FONT_10X20}, + pixelcolor::Rgb565, + prelude::{Dimensions, Point, Primitive, RgbColor, Size}, + primitives::{PrimitiveStyle, Rectangle}, + text::Text, +}; +use embedded_layout::{ + align::{horizontal, vertical}, + layout::linear::{FixedMargin, LinearLayout}, + prelude::*, +}; +use embedded_text::TextBox; + +pub struct SelectionUi<'a> { + selection: usize, + items: &'a [&'a str], + error: &'a str, + last_bounds: Option, +} + +impl<'a> SelectionUi<'a> { + pub fn new(items: &'a [&'a str], error: &'a str) -> Self { + Self { + selection: 0, + items, + error, + last_bounds: None, + } + } + + pub fn run_selection_ui(&mut self, display: &mut Display) -> Result, ()> { + self.draw(display)?; + let selection; + loop { + let key = get_key(); + if key.state == KeyState::Pressed { + print!("Got Key press: {:?}", key.key); + if let Some(s) = self.update(display, key.key)? { + selection = Some(s); + break; + } + } + } + Ok(selection) + } + + /// updates the display with a new keypress. + /// returns selection idx if selected + pub fn update(&mut self, display: &mut Display, key: KeyCode) -> Result, ()> { + match key { + KeyCode::JoyUp => { + self.selection = self.selection.saturating_sub(1); + } + KeyCode::JoyDown => { + self.selection = self.selection.saturating_add(1); + } + KeyCode::Enter | KeyCode::JoyRight => return Ok(Some(self.selection)), + _ => return Ok(Some(self.selection)), + }; + self.draw(display)?; + Ok(None) + } + + fn draw(&mut self, display: &mut Display) -> Result<(), ()> { + let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE); + let display_area = display.bounding_box(); + + if self.items.is_empty() { + TextBox::new( + &self.error, + Rectangle::new( + Point::new(25, 25), + Size::new(display_area.size.width - 50, display_area.size.width - 50), + ), + text_style, + ) + .draw(display) + .unwrap(); + } + + let mut views: Vec>> = Vec::new(); + + for i in self.items { + views.push(Text::new(i, Point::zero(), text_style)); + } + + let views_group = Views::new(views.as_mut_slice()); + + let layout = LinearLayout::vertical(views_group) + .with_alignment(horizontal::Center) + .with_spacing(FixedMargin(5)) + .arrange() + .align_to(&display_area, horizontal::Center, vertical::Center); + + // draw selected box + let selected_bounds = layout.inner().get(self.selection).ok_or(())?.bounding_box(); + Rectangle::new(selected_bounds.top_left, selected_bounds.size) + .into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1)) + .draw(display)?; + + self.last_bounds = Some(layout.bounds()); + + layout.draw(display)?; + Ok(()) + } +} diff --git a/user-apps/gallery/src/main.rs b/user-apps/gallery/src/main.rs index 631ac37..eb8ca48 100644 --- a/user-apps/gallery/src/main.rs +++ b/user-apps/gallery/src/main.rs @@ -5,12 +5,12 @@ extern crate alloc; use abi::{ display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH}, - fs::{list_dir, read_file}, + fs::{Entries, list_dir, read_file}, get_key, keyboard::{KeyCode, KeyState}, print, }; -use alloc::{format, string::ToString, vec}; +use alloc::{format, vec}; use core::panic::PanicInfo; use embedded_graphics::{ Drawable, image::Image, mono_font::MonoTextStyle, mono_font::ascii::FONT_6X10, @@ -34,7 +34,6 @@ pub fn main() { let mut bmp_buf = vec![0_u8; 100_000]; let mut display = Display; - // Grid parameters let grid_cols = 3; let grid_rows = 3; let cell_width = SCREEN_WIDTH as i32 / grid_cols; @@ -42,50 +41,44 @@ pub fn main() { let mut images_drawn = 0; - let mut files = [const { None }; 18]; - let files_num = list_dir("/images", &mut files); + let mut entries = Entries::new(); + let files_num = list_dir("/images", &mut entries); - for file in &files[2..files_num] { + for file in &entries.entries()[2..files_num] { if images_drawn >= grid_cols * grid_rows { break; // only draw 3x3 } - if let Some(f) = file { - print!("file: {}", f.name); - if f.name.extension() == b"bmp" || f.name.extension() == b"BMP" { - let file = format!("/images/{}", f.name); + print!("file: {}", file); + if file.extension().unwrap_or("") == "bmp" || file.extension().unwrap_or("") == "BMP" { + let file_path = format!("/images/{}", file); - let read = read_file(&file, 0, &mut &mut bmp_buf[..]); - if read > 0 { - let bmp = Bmp::from_slice(&bmp_buf).expect("failed to parse bmp"); + let read = read_file(&file_path, 0, &mut &mut bmp_buf[..]); + if read > 0 { + let bmp = Bmp::from_slice(&bmp_buf).expect("failed to parse bmp"); - let row = images_drawn / grid_cols; - let col = images_drawn % grid_cols; - let cell_x = col * cell_width; - let cell_y = row * cell_height; + let row = images_drawn / grid_cols; + let col = images_drawn % grid_cols; + let cell_x = col * cell_width; + let cell_y = row * cell_height; - // Center image inside cell - let bmp_w = bmp.size().width as i32; - let bmp_h = bmp.size().height as i32; - let x = cell_x + (cell_width - bmp_w) / 2; - let y = cell_y + 5; // 5px top margin + // Center image inside cell + let bmp_w = bmp.size().width as i32; + let bmp_h = bmp.size().height as i32; + let x = cell_x + (cell_width - bmp_w) / 2; + let y = cell_y + 5; // 5px top margin - Image::new(&bmp, Point::new(x, y)) - .draw(&mut display) - .unwrap(); - - let text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); - let text_y = y + bmp_h + 2; // 2px gap under image - Text::new( - f.name.to_string().as_str(), - Point::new(cell_x + 2, text_y), - text_style, - ) + Image::new(&bmp, Point::new(x, y)) .draw(&mut display) .unwrap(); - images_drawn += 1; - } + let text_style = MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE); + let text_y = y + bmp_h + 2; // 2px gap under image + Text::new(&file.base(), Point::new(cell_x + 2, text_y), text_style) + .draw(&mut display) + .unwrap(); + + images_drawn += 1; } } } diff --git a/user-apps/gif/Cargo.toml b/user-apps/gif/Cargo.toml index 6f76ceb..1c5ecd8 100644 --- a/user-apps/gif/Cargo.toml +++ b/user-apps/gif/Cargo.toml @@ -6,4 +6,5 @@ edition = "2024" [dependencies] abi = { path = "../../abi" } embedded-graphics = "0.8.1" +selection_ui = { path = "../../selection_ui" } tinygif = { git = "https://github.com/LegitCamper/tinygif" } diff --git a/user-apps/gif/src/main.rs b/user-apps/gif/src/main.rs index b1e249d..81a52d1 100644 --- a/user-apps/gif/src/main.rs +++ b/user-apps/gif/src/main.rs @@ -4,16 +4,17 @@ extern crate alloc; use abi::{ display::Display, - fs::{file_len, read_file}, + fs::{Entries, file_len, list_dir, read_file}, get_key, get_ms, keyboard::{KeyCode, KeyState}, print, sleep, }; -use alloc::vec; +use alloc::{format, vec, vec::Vec}; use core::panic::PanicInfo; use embedded_graphics::{ image::ImageDrawable, pixelcolor::Rgb565, prelude::Point, transform::Transform, }; +use selection_ui::SelectionUi; use tinygif::Gif; #[panic_handler] @@ -31,13 +32,27 @@ pub fn main() { print!("Starting Gif app"); let mut display = Display; - let size = file_len("/gifs/bad_apple.gif"); + let mut entries = Entries::new(); + list_dir("/gifs", &mut entries); + + let mut files = entries.entries(); + files.retain(|e| e.extension().unwrap_or("") == "gif"); + let gifs = &files.iter().map(|e| e.full_name()).collect::>(); + + let mut selection_ui = SelectionUi::new(&gifs, "No Gif files found in /gifs"); + let selection = selection_ui + .run_selection_ui(&mut display) + .expect("failed to draw") + .expect("Failed to get user selection"); + + let file_name = format!("/gifs/{}", gifs[selection]); + let size = file_len(&file_name); let mut buf = vec![0_u8; size]; - let read = read_file("/gifs/bad_apple.gif", 0, &mut buf); + let read = read_file(&file_name, 0, &mut buf); print!("read: {}, file size: {}", read, size); assert!(read == size); - let gif = Gif::::from_slice(&buf).unwrap(); + let gif = Gif::::from_slice(&buf).expect("Failed to parse gif"); let height = gif.height(); let mut frame_num = 0; @@ -51,11 +66,14 @@ pub fn main() { .unwrap(); frame_num += 1; - if frame_num % 100 == 0 { + if frame_num % 5 == 0 { let event = get_key(); if event.state != KeyState::Idle { match event.key { - KeyCode::Esc => return, + KeyCode::Esc => { + drop(buf); + return; + } _ => (), }; };