diff --git a/kernel/src/elf.rs b/kernel/src/elf.rs index 37e2676..1e26734 100644 --- a/kernel/src/elf.rs +++ b/kernel/src/elf.rs @@ -16,7 +16,7 @@ use goblin::{ elf32::{reloc::Rel, section_header::SectionHeader, sym::Sym}, }; use strum::IntoEnumIterator; -use userlib_sys::{EntryFn, SyscallTable}; +use userlib_sys::{EntryFn, SYS_CALL_TABLE_COUNT, SyscallTable}; const ELF32_HDR_SIZE: usize = 52; @@ -28,6 +28,7 @@ pub enum LoadError { ElfIsNotPie, UnknownRelocationType, SyscallTableNotFound, + SyscallTableSizeMismatch, } pub async unsafe fn load_binary(name: &ShortFileName) -> Result<(EntryFn, Bump), LoadError> { @@ -212,6 +213,15 @@ fn patch_syscalls( let symbol_name = core::str::from_utf8(&name).expect("symbol was not utf8"); if symbol_name == stringify!(SYS_CALL_TABLE) { + // The binary was linked against a different SyscallTable + // than this kernel (e.g. built before a syscall was + // added) -- writing the full table would overrun its + // `.syscall_table` array and corrupt whatever follows it. + let expected_size = SYS_CALL_TABLE_COUNT * size_of::(); + if sym.st_size as usize != expected_size { + return Err(LoadError::SyscallTableSizeMismatch); + } + let table_base = unsafe { base.add((sym.st_value as usize) - min_vaddr as usize) } as *mut usize; @@ -235,6 +245,8 @@ fn patch_syscalls( } SyscallTable::AudioBufferReady => syscalls::audio_buffer_ready as usize, SyscallTable::SendAudioBuffer => syscalls::send_audio_buffer as usize, + SyscallTable::FillRect => syscalls::fill_rect as usize, + SyscallTable::Blit => syscalls::blit as usize, }; unsafe { table_base.add(idx).write(ptr); diff --git a/kernel/src/framebuffer.rs b/kernel/src/framebuffer.rs index 64469d3..8df6c01 100644 --- a/kernel/src/framebuffer.rs +++ b/kernel/src/framebuffer.rs @@ -329,7 +329,10 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> { 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(); + // Stored pre-swapped so a full/partial draw can hand the + // buffer straight to the display as raw bytes, see + // `ST7365P::write_words_buffered`. + let raw_color = RawU16::from(color).into_inner().swap_bytes(); if self.fb[idx] != raw_color { self.fb[idx] = raw_color; changed = true; @@ -377,7 +380,7 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> { if drawable_area.contains(p) { 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(); + let raw_color = RawU16::from(color).into_inner().swap_bytes(); if self.fb[idx] != raw_color { self.fb[idx] = raw_color; changed = true; @@ -414,7 +417,7 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> { self.size().width as u16 - 1, self.size().height as u16 - 1, core::iter::repeat_n( - RawU16::from(color).into_inner(), + RawU16::from(color).into_inner().swap_bytes(), (self.size().width * self.size().height) as usize, ), )?; @@ -517,7 +520,9 @@ pub mod fps { } let index = (point.y as usize) * FPS_CANVAS_WIDTH + point.x as usize; - self.canvas[index] = color.into_storage(); + // Pre-swapped to match the main framebuffer's storage, since + // this canvas is copied directly into it (see draw_fps_into_fb). + self.canvas[index] = color.into_storage().swap_bytes(); } Ok(()) } diff --git a/kernel/src/syscalls.rs b/kernel/src/syscalls.rs index 17f8fd4..78f20b9 100644 --- a/kernel/src/syscalls.rs +++ b/kernel/src/syscalls.rs @@ -2,13 +2,19 @@ use alloc::{string::ToString, vec::Vec}; use core::{ffi::c_char, ptr, slice, sync::atomic::Ordering}; use embassy_rp::clocks::{RoscRng, clk_sys_freq}; use embassy_time::Instant; -use embedded_graphics::{Pixel, draw_target::DrawTarget, pixelcolor::Rgb565}; +use embedded_graphics::{ + Pixel, + draw_target::DrawTarget, + geometry::{Point, Size}, + pixelcolor::{Rgb565, raw::RawU16}, + primitives::Rectangle, +}; use embedded_sdmmc::LfnBuffer; use heapless::spsc::Queue; use userlib_sys::{ - AUDIO_BUFFER_SAMPLES, Alloc, AudioBufferReady, CLayout, CPixel, Dealloc, DrawIter, FileLen, - GenRand, GetMs, ListDir, Print, ReadFile, ReconfigureAudioSampleRate, RngRequest, - SendAudioBuffer, SleepMs, WriteFile, keyboard::*, + AUDIO_BUFFER_SAMPLES, Alloc, AudioBufferReady, Blit, CLayout, CPixel, Dealloc, DrawIter, + FileLen, FillRect, GenRand, GetMs, ListDir, Print, ReadFile, ReconfigureAudioSampleRate, + RngRequest, SendAudioBuffer, SleepMs, WriteFile, keyboard::*, }; #[cfg(feature = "psram")] @@ -102,6 +108,44 @@ pub extern "C" fn draw_iter(cpixels: *const CPixel, len: usize) { FB_PAUSED.store(false, Ordering::Release); } +const _: FillRect = fill_rect; +pub extern "C" fn fill_rect(x: u16, y: u16, w: u16, h: u16, color: u16) { + let area = Rectangle::new( + Point::new(x as i32, y as i32), + Size::new(w as u32, h as u32), + ); + let color: Rgb565 = RawU16::new(color).into(); + + FB_PAUSED.store(true, Ordering::Release); + unsafe { + FRAMEBUFFER + .as_mut() + .unwrap() + .fill_solid(&area, color) + .unwrap() + } + FB_PAUSED.store(false, Ordering::Release); +} + +const _: Blit = blit; +pub extern "C" fn blit(x: u16, y: u16, w: u16, h: u16, colors: *const u16, len: usize) { + let area = Rectangle::new( + Point::new(x as i32, y as i32), + Size::new(w as u32, h as u32), + ); + let raw: &[u16] = unsafe { slice::from_raw_parts(colors, len) }; + + FB_PAUSED.store(true, Ordering::Release); + unsafe { + FRAMEBUFFER + .as_mut() + .unwrap() + .fill_contiguous(&area, raw.iter().map(|&c| RawU16::new(c).into())) + .unwrap() + } + FB_PAUSED.store(false, Ordering::Release); +} + pub static mut KEY_CACHE: Queue = Queue::new(); const _: GetKey = get_key; diff --git a/userlib/src/lib.rs b/userlib/src/lib.rs index 6db95e9..14e0a66 100644 --- a/userlib/src/lib.rs +++ b/userlib/src/lib.rs @@ -50,7 +50,10 @@ pub mod display { use embedded_graphics::{ Pixel, geometry::{Dimensions, Point}, - pixelcolor::Rgb565, + pixelcolor::{ + Rgb565, + raw::{RawData, RawU16}, + }, prelude::{DrawTarget, Size}, primitives::Rectangle, }; @@ -121,6 +124,111 @@ pub mod display { Ok(()) } + + // One `fill_rect` syscall instead of decomposing the area into + // individual pixels through `draw_iter` (the default impl this + // overrides), e.g. for game backgrounds and UI panels. + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + let drawable = area.intersection(&self.bounding_box()); + if drawable.size.width == 0 || drawable.size.height == 0 { + return Ok(()); + } + + userlib_sys::fill_rect( + drawable.top_left.x as u16, + drawable.top_left.y as u16, + drawable.size.width as u16, + drawable.size.height as u16, + RawU16::from(color).into_inner(), + ); + + Ok(()) + } + + // Batches contiguous rows into `blit` calls instead of decomposing + // the area into individual pixels through `draw_iter` (the default + // impl this overrides), e.g. for images and animation frames. Every + // row within the screen-clipped area shares the same horizontal + // clip, so runs of rows are batched into a single rectangular blit. + fn fill_contiguous(&mut self, area: &Rectangle, colors: I) -> Result<(), Self::Error> + where + I: IntoIterator, + { + let drawable = area.intersection(&self.bounding_box()); + if drawable.size.width == 0 || drawable.size.height == 0 { + return Ok(()); + } + + let area_width = area.size.width; + let area_height = area.size.height; + let clip_w = drawable.size.width as usize; + let clip_x0 = drawable.top_left.x; + let clip_y0 = drawable.top_left.y; + let clip_y1 = clip_y0 + drawable.size.height as i32; + + const CHUNK_LEN: usize = 4096; + static mut CHUNK: [u16; CHUNK_LEN] = [0; CHUNK_LEN]; + let rows_per_chunk = (CHUNK_LEN / clip_w).max(1); + + let mut colors = colors.into_iter(); + let mut chunk_count = 0usize; + let mut chunk_row0 = clip_y0; + + for y in 0..area_height { + let py = area.top_left.y + y as i32; + let row_in_bounds = py >= clip_y0 && py < clip_y1; + + for x in 0..area_width { + let px = area.top_left.x + x as i32; + let in_bounds = row_in_bounds && px >= clip_x0 && px < clip_x0 + clip_w as i32; + + let Some(color) = colors.next() else { + break; + }; + + if in_bounds { + if chunk_count == 0 { + chunk_row0 = py; + } + unsafe { CHUNK[chunk_count] = RawU16::from(color).into_inner() }; + chunk_count += 1; + } + } + + if row_in_bounds { + let rows_buffered = chunk_count / clip_w; + if rows_buffered >= rows_per_chunk { + unsafe { + userlib_sys::blit( + clip_x0 as u16, + chunk_row0 as u16, + clip_w as u16, + rows_buffered as u16, + CHUNK.as_ptr(), + chunk_count, + ); + } + chunk_count = 0; + } + } + } + + if chunk_count > 0 { + let rows_buffered = chunk_count / clip_w; + unsafe { + userlib_sys::blit( + clip_x0 as u16, + chunk_row0 as u16, + clip_w as u16, + rows_buffered as u16, + CHUNK.as_ptr(), + chunk_count, + ); + } + } + + Ok(()) + } } } diff --git a/userlib_sys/src/lib.rs b/userlib_sys/src/lib.rs index 97ab22d..7821d09 100644 --- a/userlib_sys/src/lib.rs +++ b/userlib_sys/src/lib.rs @@ -12,7 +12,7 @@ use strum::{EnumCount, EnumIter}; pub type EntryFn = fn(); -pub const SYS_CALL_TABLE_COUNT: usize = 15; +pub const SYS_CALL_TABLE_COUNT: usize = 17; const _: () = assert!(SYS_CALL_TABLE_COUNT == SyscallTable::COUNT); #[derive(Clone, Copy, EnumIter, EnumCount)] @@ -33,6 +33,8 @@ pub enum SyscallTable { ReconfigureAudioSampleRate = 12, AudioBufferReady = 13, SendAudioBuffer = 14, + FillRect = 15, + Blit = 16, } #[unsafe(no_mangle)] @@ -152,6 +154,27 @@ pub extern "C" fn draw_iter(ptr: *const CPixel, len: usize) { f(ptr, len); } +/// Fills an axis-aligned rectangle with a single color in one call, instead +/// of marshaling it as a stream of individual `CPixel`s through `draw_iter`. +pub type FillRect = extern "C" fn(x: u16, y: u16, w: u16, h: u16, color: u16); + +#[unsafe(no_mangle)] +pub extern "C" fn fill_rect(x: u16, y: u16, w: u16, h: u16, color: u16) { + let f: FillRect = + unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::FillRect as usize]) }; + f(x, y, w, h, color); +} + +/// Writes a row-major block of raw RGB565 colors into a rectangle in one +/// call, instead of marshaling it pixel-by-pixel through `draw_iter`. +pub type Blit = extern "C" fn(x: u16, y: u16, w: u16, h: u16, colors: *const u16, len: usize); + +#[unsafe(no_mangle)] +pub extern "C" fn blit(x: u16, y: u16, w: u16, h: u16, colors: *const u16, len: usize) { + let f: Blit = unsafe { core::mem::transmute(SYS_CALL_TABLE[SyscallTable::Blit as usize]) }; + f(x, y, w, h, colors, len); +} + pub mod keyboard { use crate::{SYS_CALL_TABLE, SyscallTable};