5 Commits

Author SHA1 Message Date
aa4cc58b42 expand doom memory for pimoroni 2025-10-31 17:59:48 -06:00
85e888193d Merge branch 'main' into doom 2025-10-31 17:51:18 -06:00
beee2392ae Merge branch 'main' into doom 2025-10-31 14:15:53 -06:00
46bfa89e18 doom compiles but is BIG 2025-10-31 13:39:16 -06:00
970d021a9a layout 2025-10-29 17:49:14 -06:00
37 changed files with 1223 additions and 1640 deletions

2
.gitignore vendored
View File

@@ -2,5 +2,3 @@
*.uf2
abi_sys.h
assets/gif/bad_apple.gif
picolibc
user-apps/gboy/Peanut-GB

9
.gitmodules vendored
View File

@@ -1,6 +1,3 @@
[submodule "picolibc"]
path = picolibc
url = https://github.com/picolibc/picolibc
[submodule "user-apps/gboy/Peanut-GB"]
path = user-apps/gboy/Peanut-GB
url = https://github.com/deltabeard/Peanut-GB
[submodule "user-apps/doom/doomgeneric"]
path = user-apps/doom/doomgeneric
url = https://github.com/ozkl/doomgeneric

575
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,12 +4,11 @@ members = [
"kernel",
"abi_sys",
"abi",
"selection_ui",
"user-apps/calculator",
"user-apps/snake",
"user-apps/gallery",
"user-apps/gif",
"user-apps/wav_player",
"user-apps/doom",
]
[profile.release]
@@ -24,8 +23,8 @@ codegen-units = 1
lto = true
debug = false
opt-level = "z"
panic = "unwind"
panic = "abort"
[profile.dev]
lto = true
opt-level = "s"
opt-level = "z"

View File

@@ -4,7 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
abi_sys = { path = "../abi_sys" }
embedded-sdmmc = { version = "0.9.0", default-features = false }
embedded-graphics = "0.8.1"
abi_sys = { path = "../abi_sys" }
once_cell = { version = "1", default-features = false }
rand_core = "0.9.3"

View File

@@ -3,8 +3,8 @@
extern crate alloc;
use abi_sys::{RngRequest, keyboard::KeyEvent};
pub use abi_sys::{keyboard, print};
pub use abi_sys::{self, keyboard};
use abi_sys::{RngRequest, alloc, dealloc, keyboard::KeyEvent};
pub use alloc::format;
use core::alloc::{GlobalAlloc, Layout};
use rand_core::RngCore;
@@ -16,19 +16,19 @@ struct Alloc;
unsafe impl GlobalAlloc for Alloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
abi_sys::alloc(layout.into())
alloc(layout.into())
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
abi_sys::dealloc(ptr, layout.into());
dealloc(ptr, layout.into());
}
}
#[macro_export]
macro_rules! println {
macro_rules! print {
($($arg:tt)*) => {{
let s = $crate::format!($($arg)*);
$crate::print(s.as_ptr(), s.len());
$crate::abi_sys::print(s.as_ptr(), s.len());
}};
}
@@ -45,8 +45,6 @@ pub fn get_key() -> KeyEvent {
}
pub mod display {
use core::sync::atomic::{AtomicBool, Ordering};
use abi_sys::CPixel;
use embedded_graphics::{
Pixel,
@@ -61,28 +59,12 @@ pub mod display {
pub type Pixel565 = Pixel<Rgb565>;
const BUF_SIZE: usize = 1024;
const BUF_SIZE: usize = 15 * 1024; // tune this for performance
static mut BUF: [CPixel; BUF_SIZE] = [CPixel::new(); BUF_SIZE];
// const BUF_SIZE: usize = 250 * 1024; // tune this for performance
// static mut BUF: Lazy<Vec<CPixel>> = Lazy::new(|| vec![const { CPixel::new() }; BUF_SIZE]);
static DISPLAY_TAKEN: AtomicBool = AtomicBool::new(false);
pub struct Display {
_private: (),
}
impl Display {
/// Only one instance of Display can be taken
pub fn take() -> Option<Display> {
if DISPLAY_TAKEN
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_ok()
{
Some(Self { _private: () })
} else {
None
}
}
}
pub struct Display;
impl Dimensions for Display {
fn bounding_box(&self) -> Rectangle {
@@ -158,120 +140,23 @@ impl RngCore for Rng {
}
pub mod fs {
use alloc::vec::Vec;
use core::fmt::Display;
use embedded_sdmmc::DirEntry;
pub fn read_file(file: &str, start_from: usize, buf: &mut [u8]) -> usize {
pub fn read_file(file: &str, read_from: usize, buf: &mut [u8]) -> usize {
abi_sys::read_file(
file.as_ptr(),
file.len(),
start_from,
read_from,
buf.as_mut_ptr(),
buf.len(),
)
}
pub fn write_file(file: &str, start_from: usize, buf: &[u8]) {
abi_sys::write_file(
file.as_ptr(),
file.len(),
start_from,
buf.as_ptr(),
buf.len(),
)
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
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, Debug)]
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<FileName<'a>> {
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 list_dir(path: &str, files: &mut [Option<DirEntry>]) -> usize {
abi_sys::list_dir(path.as_ptr(), path.len(), files.as_mut_ptr(), files.len())
}
pub fn file_len(str: &str) -> usize {
abi_sys::file_len(str.as_ptr(), str.len())
}
}
pub mod audio {
pub use abi_sys::{AUDIO_BUFFER_LEN, AUDIO_BUFFER_SAMPLES, audio_buffer_ready};
pub fn send_audio_buffer(buf: &[u8]) {
abi_sys::send_audio_buffer(buf.as_ptr(), buf.len())
}
}

View File

@@ -12,6 +12,7 @@ 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]

View File

@@ -1,18 +1,19 @@
#![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();
pub const ABI_CALL_TABLE_COUNT: usize = 14;
pub const ABI_CALL_TABLE_COUNT: usize = 11;
const _: () = assert!(ABI_CALL_TABLE_COUNT == CallTable::COUNT);
#[derive(Clone, Copy, EnumIter, EnumCount)]
@@ -28,10 +29,7 @@ pub enum CallTable {
GenRand = 7,
ListDir = 8,
ReadFile = 9,
WriteFile = 10,
FileLen = 11,
AudioBufferReady = 12,
SendAudioBuffer = 13,
FileLen = 10,
}
#[unsafe(no_mangle)]
@@ -398,23 +396,21 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) {
pub type ListDir = extern "C" fn(
str: *const u8,
len: usize,
entries: *mut *mut c_char,
files: *mut Option<DirEntry>,
file_len: usize,
max_entry_str_len: usize,
) -> usize;
#[unsafe(no_mangle)]
pub extern "C" fn list_dir(
str: *const u8,
len: usize,
entries: *mut *mut c_char,
entry_count: usize,
max_entry_str_len: usize,
files: *mut Option<DirEntry>,
file_len: usize,
) -> usize {
unsafe {
let ptr = CALL_ABI_TABLE[CallTable::ListDir as usize];
let f: ListDir = core::mem::transmute(ptr);
f(str, len, entries, entry_count, max_entry_str_len)
f(str, len, files, file_len)
}
}
@@ -441,24 +437,6 @@ pub extern "C" fn read_file(
}
}
pub type WriteFile =
extern "C" fn(str: *const u8, len: usize, write_from: usize, buf: *const u8, buf_len: usize);
#[unsafe(no_mangle)]
pub extern "C" fn write_file(
str: *const u8,
len: usize,
write_from: usize,
buf: *const u8,
buf_len: usize,
) {
unsafe {
let ptr = CALL_ABI_TABLE[CallTable::WriteFile as usize];
let f: WriteFile = core::mem::transmute(ptr);
f(str, len, write_from, buf, buf_len)
}
}
pub type FileLen = extern "C" fn(str: *const u8, len: usize) -> usize;
#[unsafe(no_mangle)]
@@ -469,28 +447,3 @@ pub extern "C" fn file_len(str: *const u8, len: usize) -> usize {
f(str, len)
}
}
pub type AudioBufferReady = extern "C" fn() -> bool;
#[allow(unused)]
pub fn audio_buffer_ready() -> bool {
unsafe {
let ptr = CALL_ABI_TABLE[CallTable::AudioBufferReady as usize];
let f: AudioBufferReady = core::mem::transmute(ptr);
f()
}
}
pub const AUDIO_BUFFER_SAMPLES: usize = 1024;
pub const AUDIO_BUFFER_LEN: usize = AUDIO_BUFFER_SAMPLES * 2;
pub type SendAudioBuffer = extern "C" fn(ptr: *const u8, len: usize);
#[allow(unused)]
pub fn send_audio_buffer(buf: *const u8, len: usize) {
unsafe {
let ptr = CALL_ABI_TABLE[CallTable::SendAudioBuffer as usize];
let f: SendAudioBuffer = core::mem::transmute(ptr);
f(buf, len)
}
}

View File

@@ -1,7 +1,5 @@
kernel-dev board:
cargo run --bin kernel --features {{board}} --features fps
kernel-release-probe board:
cargo run --bin kernel --profile release --features {{board}} --features fps
cargo run --bin kernel --features {{board}}
kernel-release board:
cargo build --bin kernel --release --no-default-features --features {{board}}
elf2uf2-rs -d target/thumbv8m.main-none-eabihf/release/kernel
@@ -11,26 +9,6 @@ binary-args := "RUSTFLAGS=\"-C link-arg=-pie -C relocation-model=pic\""
cbindgen:
cbindgen abi_sys --output abi_sys.h -q
newlib:
#!/bin/bash
cd picolibc
mkdir build
cd build
CONFIG_PICOLIBC=true ../scripts/do-configure thumbv8m_main_fp-none-eabi \
--buildtype=minsize \
-Dtests=true \
-Dtinystdio=false \
-Dsingle-thread=true \
-Db_pie=true \
-Ddefault_library=static \
-Dtinystdio=false \
-Dnewlib-nano-malloc=true \
-Dmultilib=false \
-Dpicolib=true \
"$@"
DESTDIR=./install meson install
ninja
userapp app:
{{binary-args}} cargo build --bin {{app}} --profile release-binary
@@ -39,20 +17,4 @@ userapps: cbindgen
just userapp snake
just userapp gallery
just userapp gif
just userapp wav_player
copy-userapp app:
cp ./target/thumbv8m.main-none-eabihf/release-binary/{{app}} /run/media/$(whoami)/PICOCALC/{{app}}.bin
copy-userapps:
#!/bin/bash
just userapps
just copy-userapp calculator
just copy-userapp snake
just copy-userapp gallery
just copy-userapp gif
just copy-userapp wav_player
DEV=$(lsblk -o LABEL,NAME -nr | awk -v L="PICOCALC" '$1==L {print "/dev/" $2}')
udisksctl unmount -b "$DEV"
udisksctl power-off -b "$DEV"
just userapp doom

View File

@@ -12,13 +12,10 @@ bench = false
[features]
default = ["rp235x", "defmt"]
pimoroni2w = ["rp235x", "psram"]
# rp2040 = ["embassy-rp/rp2040"] # unsupported, ram too small for fb
pimoroni2w = ["rp235x"]
rp2040 = ["embassy-rp/rp2040"]
rp235x = ["embassy-rp/rp235xb"]
trouble = ["dep:bt-hci", "dep:cyw43", "dep:cyw43-pio", "dep:trouble-host"]
psram = ["dep:embedded-alloc"]
overclock = []
fps = []
defmt = [
"dep:defmt",
"panic-probe/print-defmt",
@@ -73,17 +70,16 @@ panic-probe = "0.3"
portable-atomic = { version = "1.11", features = ["critical-section"] }
assign-resources = "0.5.0"
defmt = { version = "1.0.1", optional = true }
defmt-rtt = "1.1.0"
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 = "a784b9e6df0769371dfc522528e770cf8fc6403a" } # async branch
embedded-graphics = { version = "0.8.1" }
embedded-text = "0.7.2"
embedded-layout = "0.4.2"
kolibri-embedded-gui = "0.1.0"
micromath = "2.1.0"
fixed = "1.29.0"
strum = { version = "0.27.2", default-features = false }
rand = { version = "0.9.0", default-features = false }
once_cell = { version = "1.21.3", default-features = false }
@@ -94,9 +90,7 @@ 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",
], optional = true }
embedded-alloc = { version = "0.6.0", features = ["allocator_api"] }
bumpalo = "3.19.0"
abi_sys = { path = "../abi_sys" }

View File

@@ -1,24 +1,16 @@
use abi_sys::{
AUDIO_BUFFER_SAMPLES, AllocAbi, AudioBufferReady, CLayout, CPixel, DeallocAbi, DrawIterAbi,
FileLen, GenRand, GetMsAbi, ListDir, PrintAbi, ReadFile, RngRequest, SendAudioBuffer,
SleepMsAbi, WriteFile, keyboard::*,
AllocAbi, CLayout, CPixel, DeallocAbi, DrawIterAbi, FileLen, GenRand, GetMsAbi, ListDir,
PrintAbi, ReadFile, RngRequest, SleepMsAbi, keyboard::*,
};
use alloc::{string::ToString, vec::Vec};
use core::{ffi::c_char, ptr, sync::atomic::Ordering};
use core::{alloc::GlobalAlloc, sync::atomic::Ordering};
use embassy_rp::clocks::{RoscRng, clk_sys_freq};
use embassy_time::Instant;
use embedded_graphics::draw_target::DrawTarget;
use embedded_sdmmc::LfnBuffer;
use embedded_sdmmc::{DirEntry, LfnBuffer};
use heapless::spsc::Queue;
#[cfg(feature = "psram")]
use crate::heap::HEAP;
#[cfg(feature = "psram")]
use core::alloc::GlobalAlloc;
use crate::{
audio::{AUDIO_BUFFER, AUDIO_BUFFER_READY},
display::FRAMEBUFFER,
framebuffer::FB_PAUSED,
storage::{Dir, File, SDCARD},
@@ -28,14 +20,10 @@ const _: AllocAbi = alloc;
pub extern "C" fn alloc(layout: CLayout) -> *mut u8 {
// SAFETY: caller guarantees layout is valid
unsafe {
#[cfg(feature = "psram")]
{
return HEAP.alloc(layout.into());
}
#[cfg(not(feature = "psram"))]
{
return alloc::alloc::alloc(layout.into());
if cfg!(feature = "pimoroni2w") {
crate::heap::HEAP.alloc(layout.into())
} else {
alloc::alloc::alloc(layout.into())
}
}
}
@@ -43,14 +31,12 @@ 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
#[cfg(feature = "psram")]
{
unsafe { HEAP.dealloc(ptr, layout.into()) }
}
#[cfg(not(feature = "psram"))]
{
unsafe { alloc::alloc::dealloc(ptr, layout.into()) }
unsafe {
if cfg!(feature = "pimoroni2w") {
crate::heap::HEAP.dealloc(ptr, layout.into())
} else {
alloc::alloc::dealloc(ptr, layout.into())
}
}
}
@@ -130,27 +116,11 @@ pub extern "C" fn gen_rand(req: &mut RngRequest) {
}
}
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);
fn get_dir_entries(dir: &Dir, files: &mut [Option<DirEntry>]) -> usize {
let mut i = 0;
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) };
}
dir.iterate_dir(|entry| {
if i < files.len() {
files[i] = Some(entry.clone());
i += 1;
}
})
@@ -158,30 +128,24 @@ unsafe fn get_dir_entries(dir: &Dir, entries: &mut [*mut c_char], max_str_len: u
i
}
unsafe fn recurse_dir(
dir: &Dir,
dirs: &[&str],
entries: &mut [*mut c_char],
max_str_len: usize,
) -> usize {
fn recurse_dir(dir: &Dir, dirs: &[&str], files: &mut [Option<DirEntry>]) -> usize {
if dirs.is_empty() {
return unsafe { get_dir_entries(dir, entries, max_str_len) };
return get_dir_entries(dir, files);
}
let dir = dir.open_dir(dirs[0]).unwrap();
unsafe { recurse_dir(&dir, &dirs[1..], entries, max_str_len) }
recurse_dir(&dir, &dirs[1..], files)
}
const _: ListDir = list_dir;
pub extern "C" fn list_dir(
dir: *const u8,
len: usize,
entries: *mut *mut c_char,
files: *mut Option<DirEntry>,
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(entries, files_len) };
let files = unsafe { core::slice::from_raw_parts_mut(files, 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();
@@ -192,12 +156,10 @@ pub extern "C" fn list_dir(
let mut wrote = 0;
sd.access_root_dir(|root| {
if dirs[0] == "" && dirs.len() >= 2 {
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);
}
if dir == "/" {
wrote = get_dir_entries(&root, files);
} else {
wrote = recurse_dir(&root, &dirs[1..], files);
}
}
});
@@ -220,8 +182,7 @@ fn recurse_file<T>(
}
}
})
.expect("Failed to iterate dir");
.unwrap();
if let Some(name) = short_name {
let mut file = dir
.open_file_in_dir(name, embedded_sdmmc::Mode::ReadWriteAppend)
@@ -245,17 +206,7 @@ pub extern "C" fn read_file(
) -> usize {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let file = unsafe { core::str::from_raw_parts(str, len) };
let mut components: [&str; 8] = [""; 8];
let mut count = 0;
for part in file.split('/') {
if count >= components.len() {
break;
}
components[count] = part;
count += 1;
}
let file: Vec<&str> = file.split('/').collect();
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let mut buf = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
@@ -265,8 +216,8 @@ pub extern "C" fn read_file(
let sd = guard.as_mut().unwrap();
if !file.is_empty() {
sd.access_root_dir(|root| {
if let Ok(result) = recurse_file(&root, &components[1..count], |file| {
file.seek_from_start(start_from as u32).unwrap_or(());
if let Ok(result) = recurse_file(&root, &file[1..], |file| {
file.seek_from_start(start_from as u32).unwrap();
file.read(&mut buf).unwrap()
}) {
read = result
@@ -276,46 +227,9 @@ pub extern "C" fn read_file(
read
}
const _: WriteFile = write_file;
pub extern "C" fn write_file(
str: *const u8,
len: usize,
start_from: usize,
buf: *const u8,
buf_len: usize,
) {
// SAFETY: caller guarantees str ptr is valid for `len` bytes
let file = unsafe { core::str::from_raw_parts(str, len) };
let mut components: [&str; 8] = [""; 8];
let mut count = 0;
for part in file.split('/') {
if count >= components.len() {
break;
}
components[count] = part;
count += 1;
}
// SAFETY: caller guarantees buf ptr is valid for `buf_len` bytes
let buf = unsafe { core::slice::from_raw_parts(buf, buf_len) };
let mut guard = SDCARD.get().try_lock().expect("Failed to get sdcard");
let sd = guard.as_mut().unwrap();
if !file.is_empty() {
sd.access_root_dir(|root| {
recurse_file(&root, &components[1..count], |file| {
file.seek_from_start(start_from as u32).unwrap();
file.write(&buf).unwrap()
})
.unwrap_or(())
});
};
}
const _: FileLen = file_len;
pub extern "C" fn file_len(str: *const u8, len: usize) -> usize {
// SAFETY: caller guarantees str ptr is valid for `len` bytes
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let file = unsafe { core::str::from_raw_parts(str, len) };
let file: Vec<&str> = file.split('/').collect();
@@ -332,28 +246,3 @@ pub extern "C" fn file_len(str: *const u8, len: usize) -> usize {
}
len as usize
}
const _: AudioBufferReady = audio_buffer_ready;
pub extern "C" fn audio_buffer_ready() -> bool {
AUDIO_BUFFER_READY.load(Ordering::Acquire)
}
const _: SendAudioBuffer = send_audio_buffer;
pub extern "C" fn send_audio_buffer(ptr: *const u8, len: usize) {
// SAFETY: caller guarantees `ptr` is valid for `len` bytes
let buf = unsafe { core::slice::from_raw_parts(ptr, len) };
while !AUDIO_BUFFER_READY.load(Ordering::Acquire) {}
if buf.len() == AUDIO_BUFFER_SAMPLES * 2 {
AUDIO_BUFFER_READY.store(false, Ordering::Release);
unsafe { AUDIO_BUFFER.copy_from_slice(buf) };
} else {
#[cfg(feature = "defmt")]
defmt::warn!(
"user audio stream was wrong size: {} should be {}",
buf.len(),
AUDIO_BUFFER_SAMPLES * 2
)
}
}

View File

@@ -1,149 +0,0 @@
use crate::Audio;
use core::sync::atomic::{AtomicBool, Ordering};
use embassy_futures::join::join;
use embassy_rp::{
Peri,
clocks::clk_sys_freq,
dma::{AnyChannel, Channel},
gpio::Level,
pio::{
Common, Config, Direction, FifoJoin, Instance, LoadedProgram, PioPin, ShiftConfig,
StateMachine, program::pio_asm,
},
};
use fixed::traits::ToFixed;
pub const SAMPLE_RATE_HZ: u32 = 22_050;
const AUDIO_BUFFER_SAMPLES: usize = 1024;
const _: () = assert!(AUDIO_BUFFER_SAMPLES == abi_sys::AUDIO_BUFFER_SAMPLES);
// 8bit stereo interleaved PCM audio buffers
pub static mut AUDIO_BUFFER: [u8; AUDIO_BUFFER_SAMPLES * 2] = [0; AUDIO_BUFFER_SAMPLES * 2];
static mut AUDIO_BUFFER_1: [u8; AUDIO_BUFFER_SAMPLES * 2] = [0; AUDIO_BUFFER_SAMPLES * 2];
pub static AUDIO_BUFFER_READY: AtomicBool = AtomicBool::new(true);
#[embassy_executor::task]
pub async fn audio_handler(mut audio: Audio) {
const SILENCE_VALUE: u8 = u8::MAX / 2;
let prg = PioPwmAudioProgram8Bit::new(&mut audio.pio);
let mut pwm_pio_left =
PioPwmAudio::new(audio.dma0, &mut audio.pio, audio.sm0, audio.left, &prg);
let mut pwm_pio_right =
PioPwmAudio::new(audio.dma1, &mut audio.pio, audio.sm1, audio.right, &prg);
loop {
write_samples(&mut pwm_pio_left, &mut pwm_pio_right, unsafe {
&AUDIO_BUFFER_1
})
.await;
unsafe { &mut AUDIO_BUFFER_1 }.fill(SILENCE_VALUE);
unsafe { core::mem::swap(&mut AUDIO_BUFFER, &mut AUDIO_BUFFER_1) };
AUDIO_BUFFER_READY.store(true, Ordering::Release)
}
}
async fn write_samples<PIO: Instance>(
left: &mut PioPwmAudio<'static, PIO, 0>,
right: &mut PioPwmAudio<'static, PIO, 1>,
buf: &[u8],
) {
// pack two samples per word
let mut packed_buf_left: [u32; AUDIO_BUFFER_SAMPLES / 2] = [0; AUDIO_BUFFER_SAMPLES / 2];
let mut packed_buf_right: [u32; AUDIO_BUFFER_SAMPLES / 2] = [0; AUDIO_BUFFER_SAMPLES / 2];
for ((pl, pr), sample) in packed_buf_left
.iter_mut()
.zip(packed_buf_right.iter_mut())
.zip(buf.chunks(4))
{
*pl = pack_u8_samples(sample[0], sample[2]);
*pr = pack_u8_samples(sample[1], sample[3]);
}
let left_fut = left
.sm
.tx()
.dma_push(left.dma.reborrow(), &packed_buf_left, false);
let right_fut = right
.sm
.tx()
.dma_push(right.dma.reborrow(), &packed_buf_right, false);
join(left_fut, right_fut).await;
}
struct PioPwmAudioProgram8Bit<'d, PIO: Instance>(LoadedProgram<'d, PIO>);
/// Writes one sample to pwm as high and low time
impl<'d, PIO: Instance> PioPwmAudioProgram8Bit<'d, PIO> {
fn new(common: &mut Common<'d, PIO>) -> Self {
// only uses 16 bits top for high, bottom for low
// allows two samples per word
let prg = pio_asm!(
"out x, 8", // pwm high time
"out y, 8", // pwm low time
"loop_high:",
"set pins, 1", // keep pin high
"jmp x-- loop_high", // decrement X until 0
"loop_low:",
"set pins, 0", // keep pin low
"jmp y-- loop_low", // decrement Y until 0
);
let prg = common.load_program(&prg.program);
Self(prg)
}
}
struct PioPwmAudio<'d, PIO: Instance, const SM: usize> {
dma: Peri<'d, AnyChannel>,
sm: StateMachine<'d, PIO, SM>,
}
impl<'d, PIO: Instance, const SM: usize> PioPwmAudio<'d, PIO, SM> {
fn new(
dma: Peri<'d, impl Channel>,
pio: &mut Common<'d, PIO>,
mut sm: StateMachine<'d, PIO, SM>,
pin: Peri<'d, impl PioPin>,
prg: &PioPwmAudioProgram8Bit<'d, PIO>,
) -> Self {
let pin = pio.make_pio_pin(pin);
sm.set_pins(Level::High, &[&pin]);
sm.set_pin_dirs(Direction::Out, &[&pin]);
let mut cfg = Config::default();
cfg.set_set_pins(&[&pin]);
cfg.fifo_join = FifoJoin::TxOnly;
let mut shift_cfg = ShiftConfig::default();
shift_cfg.auto_fill = true;
cfg.shift_out = shift_cfg;
cfg.use_program(&prg.0, &[]);
sm.set_config(&cfg);
let target_clock = (u8::MAX as u32 + 1) * SAMPLE_RATE_HZ;
let divider = (clk_sys_freq() / (target_clock * 2)).to_fixed();
sm.set_clock_divider(divider);
sm.set_enable(true);
Self {
dma: dma.into(),
sm,
}
}
}
/// packs two u8 samples into 32bit word
fn pack_u8_samples(sample1: u8, sample2: u8) -> u32 {
(u8_pcm_to_pwm(sample1) as u32) << 16 | u8_pcm_to_pwm(sample2) as u32
}
fn u8_pcm_to_pwm(sample: u8) -> u16 {
((sample as u16) << 8) | ((u8::MAX - sample) as u16)
}

View File

@@ -1,24 +1,20 @@
use crate::framebuffer::{self, AtomicFrameBuffer, FB_PAUSED};
use core::alloc::{GlobalAlloc, Layout};
use core::sync::atomic::Ordering;
use embassy_futures::yield_now;
use embassy_rp::{
Peri,
gpio::{Level, Output},
peripherals::{PIN_13, PIN_14, PIN_15, SPI1},
spi::{Async, Spi},
};
use embassy_time::Delay;
use embedded_graphics::{draw_target::DrawTarget, pixelcolor::Rgb565, prelude::RgbColor};
use embassy_time::{Delay, Timer};
use embedded_graphics::{
pixelcolor::Rgb565,
prelude::{DrawTarget, RgbColor},
};
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<Spi<'static, SPI1, Async>, Output<'static>, Delay>,
Output<'static>,
@@ -33,21 +29,18 @@ pub static mut FRAMEBUFFER: Option<AtomicFrameBuffer> = None;
fn init_fb() {
unsafe {
#[cfg(feature = "psram")]
{
let slab = HEAP.alloc(Layout::array::<u16>(framebuffer::SIZE).unwrap()) as *mut u16;
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::<u16>(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();
FRAMEBUFFER = Some(fb);
}
#[cfg(not(feature = "psram"))]
{
static mut BUF: [u16; framebuffer::SIZE] = [0; framebuffer::SIZE];
FRAMEBUFFER = Some(AtomicFrameBuffer::new(&mut BUF));
}
fb
});
}
}
@@ -85,37 +78,19 @@ pub async fn init_display(
#[embassy_executor::task]
pub async fn display_handler(mut display: DISPLAY) {
use embassy_time::{Instant, Timer};
// Target ~60 Hz refresh (≈16.67 ms per frame)
const FRAME_TIME_MS: u64 = 1000 / 60;
loop {
let start = Instant::now();
#[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()
.partial_draw(&mut display)
.safe_draw(&mut display)
.await
.unwrap();
}
.unwrap()
};
}
let elapsed = start.elapsed().as_millis() as u64;
if elapsed < FRAME_TIME_MS {
Timer::after_millis(FRAME_TIME_MS - elapsed).await;
} else {
yield_now().await;
}
// small yield to allow other tasks to run
Timer::after_nanos(500).await;
}
}

View File

@@ -2,7 +2,8 @@ use crate::{
abi,
storage::{File, SDCARD},
};
use abi_sys::{CallTable, EntryFn};
use abi_sys::CallTable;
use abi_sys::EntryFn;
use alloc::{vec, vec::Vec};
use bumpalo::Bump;
use core::ptr;
@@ -205,10 +206,7 @@ fn patch_abi(
CallTable::GenRand => abi::gen_rand as usize,
CallTable::ListDir => abi::list_dir as usize,
CallTable::ReadFile => abi::read_file as usize,
CallTable::WriteFile => abi::write_file as usize,
CallTable::FileLen => abi::file_len as usize,
CallTable::AudioBufferReady => abi::audio_buffer_ready as usize,
CallTable::SendAudioBuffer => abi::send_audio_buffer as usize,
};
unsafe {
table_base.add(idx as usize).write(ptr);

View File

@@ -1,5 +1,6 @@
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::{
@@ -13,36 +14,32 @@ use embedded_hal_2::digital::OutputPin;
use embedded_hal_async::{delay::DelayNs, spi::SpiDevice};
use st7365p_lcd::ST7365P;
#[cfg(feature = "fps")]
use fps::{FPS_CANVAS, FPS_CANVAS_HEIGHT, FPS_CANVAS_WIDTH, FPS_CANVAS_X, FPS_CANVAS_Y};
pub const TILE_SIZE: usize = 16; // 16x16 tile
pub const TILE_COUNT: usize = (SCREEN_WIDTH / TILE_SIZE) * (SCREEN_HEIGHT / TILE_SIZE); // 400 tiles
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];
// 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<Rectangle, { TILE_COUNT / MAX_META_TILES }>;
pub const SIZE: usize = SCREEN_HEIGHT * SCREEN_WIDTH;
pub static FB_PAUSED: AtomicBool = AtomicBool::new(false);
static mut DIRTY_TILES: LazyLock<heapless::Vec<AtomicBool, TILE_COUNT>> = 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> {
fb: &'a mut [u16],
dirty_tiles: [AtomicBool; TILE_COUNT],
batch_tile_buf: BatchTileBuf,
}
pub struct AtomicFrameBuffer<'a>(&'a mut [u16]);
impl<'a> AtomicFrameBuffer<'a> {
pub fn new(buffer: &'a mut [u16]) -> Self {
assert!(buffer.len() == SIZE);
Self {
fb: buffer,
dirty_tiles: core::array::from_fn(|_| AtomicBool::new(true)),
batch_tile_buf: [0; MAX_BATCH_TILES * TILE_SIZE * TILE_SIZE],
}
Self(buffer)
}
fn mark_tiles_dirty(&mut self, rect: Rectangle) {
@@ -55,7 +52,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;
self.dirty_tiles[tile_idx].store(true, Ordering::Release);
unsafe { DIRTY_TILES.get_mut()[tile_idx].store(true, Ordering::Release) };
}
}
}
@@ -81,7 +78,7 @@ impl<'a> AtomicFrameBuffer<'a> {
for y in sy..=ey {
for x in sx..=ex {
if let Some(color) = color_iter.next() {
self.fb[(y as usize * SCREEN_WIDTH) + x as usize] = color;
self.0[(y as usize * SCREEN_WIDTH) + x as usize] = color;
} else {
return Err(()); // Not enough data
}
@@ -96,17 +93,60 @@ impl<'a> AtomicFrameBuffer<'a> {
Ok(())
}
// 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;
// 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();
self.dirty_tiles.iter().any(|tile| {
if tile.load(Ordering::Acquire) {
dirty_pixels += TILE_SIZE * TILE_SIZE;
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;
}
dirty_pixels >= threshold_pixels
})
}
meta_tiles
}
/// Sends the entire framebuffer to the display
@@ -125,104 +165,71 @@ impl<'a> AtomicFrameBuffer<'a> {
0,
self.size().width as u16 - 1,
self.size().height as u16 - 1,
&self.fb[..],
&self.0[..],
)
.await?;
for tile in self.dirty_tiles.iter() {
tile.store(false, Ordering::Release);
}
#[cfg(feature = "fps")]
unsafe {
crate::display::FPS_COUNTER.measure()
for tile in DIRTY_TILES.get_mut().iter() {
tile.store(false, Ordering::Release);
}
};
Ok(())
}
pub async fn safe_draw<SPI, DC, RST, DELAY>(
&mut self,
display: &mut ST7365P<SPI, DC, RST, DELAY>,
) -> 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<u16, { TILE_SIZE * TILE_SIZE }> = 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();
}
}
Ok(())
}
// 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)
/// Sends only dirty tiles (16x16px) in batches to the display
pub async fn partial_draw<SPI, DC, RST, DELAY>(
&mut self,
display: &mut ST7365P<SPI, DC, RST, DELAY>,
@@ -233,77 +240,58 @@ impl<'a> AtomicFrameBuffer<'a> {
RST: OutputPin,
DELAY: DelayNs,
{
if self.should_full_draw() {
#[cfg(feature = "fps")]
self.draw_fps_into_fb();
return self.draw(display).await;
}
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;
#[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;
let meta_tiles = self.find_meta_tiles(tiles_x, tiles_y);
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);
// buffer for copying meta tiles before sending to display
let mut pixel_buffer: heapless::Vec<u16, { MAX_META_TILES * TILE_SIZE * TILE_SIZE }> =
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();
}
}
}
for tile_row in 0..NUM_TILE_ROWS {
let row_start_idx = tile_row * NUM_TILE_COLS;
let mut col = 0;
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?;
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;
// 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;
// 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;
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) };
}
// 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(())
}
}
@@ -321,14 +309,14 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> {
for Pixel(coord, color) in pixels {
if coord.x >= 0 && coord.y >= 0 {
let x = coord.x;
let y = coord.y;
let x = coord.x as i32;
let y = coord.y as i32;
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.fb[idx] != raw_color {
self.fb[idx] = raw_color;
if self.0[idx] != raw_color {
self.0[idx] = raw_color;
changed = true;
}
@@ -377,8 +365,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.fb[idx] != raw_color {
self.fb[idx] = raw_color;
if self.0[idx] != raw_color {
self.0[idx] = raw_color;
changed = true;
}
} else {
@@ -416,7 +404,7 @@ impl<'a> DrawTarget for AtomicFrameBuffer<'a> {
.take((self.size().width * self.size().height) as usize),
)?;
for tile in self.dirty_tiles.iter() {
for tile in unsafe { DIRTY_TILES.get_mut() }.iter() {
tile.store(true, Ordering::Release);
}
@@ -429,146 +417,3 @@ 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<FPS_LEN> = 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<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
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<Instant>,
smoothed: f32,
last_draw: Option<Instant>,
}
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
}
}
}
}
}

View File

@@ -9,35 +9,29 @@
extern crate alloc;
mod abi;
mod audio;
mod display;
mod elf;
mod framebuffer;
mod heap;
mod peripherals;
mod psram;
mod scsi;
mod storage;
mod ui;
mod usb;
mod utils;
#[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};
#[cfg(feature = "pimoroni2w")]
use crate::{heap::init_qmi_psram_heap, psram::init_psram_qmi};
use crate::{
abi::{KEY_CACHE, MS_SINCE_LAUNCH},
audio::audio_handler,
display::{FRAMEBUFFER, display_handler, init_display},
peripherals::{
conf_peripherals,
keyboard::{KeyState, read_keyboard_fifo},
},
psram::init_psram,
scsi::MSC_SHUTDOWN,
storage::{SDCARD, SdCard},
ui::{SELECTIONS, clear_selection, ui_handler},
@@ -49,17 +43,15 @@ use embassy_executor::{Executor, Spawner};
use embassy_futures::{join::join, select::select};
use embassy_rp::{
Peri,
clocks::ClockConfig,
config::Config,
gpio::{Input, Level, Output, Pull},
i2c::{self, I2c},
multicore::{Stack, spawn_core1},
peripherals::{
DMA_CH0, DMA_CH1, DMA_CH3, DMA_CH4, I2C1, PIN_2, PIN_3, PIN_6, PIN_7, PIN_10, PIN_11,
PIN_12, PIN_13, PIN_14, PIN_15, PIN_16, PIN_17, PIN_18, PIN_19, PIN_20, PIN_21, PIN_22,
PIN_26, PIN_27, PIO0, SPI0, SPI1, USB, WATCHDOG,
PIO0, SPI0, SPI1, USB, WATCHDOG,
},
pio::{self, Common, Pio, StateMachine},
pio,
spi::{self, Spi},
usb as embassy_rp_usb,
watchdog::{ResetReason, Watchdog},
@@ -121,17 +113,9 @@ async fn watchdog_task(mut watchdog: Watchdog) {
static ENABLE_UI: AtomicBool = AtomicBool::new(true);
static UI_CHANGE: Signal<CriticalSectionRawMutex, ()> = Signal::new();
const OVERCLOCK: u32 = 300_000_000;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = if cfg!(feature = "overclock") {
let clocks = ClockConfig::system_freq(OVERCLOCK).unwrap();
let config = Config::new(clocks);
embassy_rp::init(config)
} else {
embassy_rp::init(Default::default())
};
let p = embassy_rp::init(Default::default());
spawn_core1(
p.CORE1,
@@ -153,19 +137,6 @@ async fn main(_spawner: Spawner) {
data: p.PIN_14,
reset: p.PIN_15,
};
let Pio {
common, sm0, sm1, ..
} = Pio::new(p.PIO0, Irqs);
let audio = Audio {
pio: common,
sm0,
dma0: p.DMA_CH3,
left: p.PIN_26,
sm1,
dma1: p.DMA_CH4,
right: p.PIN_27,
};
let sd = Sd {
spi: p.SPI0,
clk: p.PIN_18,
@@ -174,15 +145,15 @@ async fn main(_spawner: Spawner) {
cs: p.PIN_17,
det: p.PIN_22,
};
// let psram = Psram {
// pio: p.PIO0,
// sclk: p.PIN_21,
// mosi: p.PIN_2,
// miso: p.PIN_3,
// cs: p.PIN_20,
// dma1: p.DMA_CH3,
// dma2: p.DMA_CH4,
// };
let psram = Psram {
pio: p.PIO0,
sclk: p.PIN_21,
mosi: p.PIN_2,
miso: p.PIN_3,
cs: p.PIN_20,
dma1: p.DMA_CH3,
dma2: p.DMA_CH4,
};
let mcu = Mcu {
i2c: p.I2C1,
clk: p.PIN_7,
@@ -192,7 +163,7 @@ async fn main(_spawner: Spawner) {
executor0.run(|spawner| {
spawner
.spawn(kernel_task(
spawner, p.WATCHDOG, display, audio, sd, mcu, p.USB,
spawner, p.WATCHDOG, display, sd, psram, mcu, p.USB,
))
.unwrap()
});
@@ -246,15 +217,6 @@ struct Display {
data: Peri<'static, PIN_14>,
reset: Peri<'static, PIN_15>,
}
struct Audio {
pio: Common<'static, PIO0>,
dma0: Peri<'static, DMA_CH3>,
sm0: StateMachine<'static, PIO0, 0>,
left: Peri<'static, PIN_26>,
dma1: Peri<'static, DMA_CH4>,
sm1: StateMachine<'static, PIO0, 1>,
right: Peri<'static, PIN_27>,
}
struct Sd {
spi: Peri<'static, SPI0>,
clk: Peri<'static, PIN_18>,
@@ -263,7 +225,6 @@ 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>,
@@ -289,7 +250,7 @@ async fn setup_mcu(mcu: Mcu) {
async fn setup_display(display: Display, spawner: Spawner) {
let mut config = spi::Config::default();
config.frequency = 192_000_000;
config.frequency = 64_000_000;
let spi = Spi::new(
display.spi,
display.clk,
@@ -305,22 +266,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 = "psram")]
#[cfg(feature = "pimoroni2w")]
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);
@@ -356,9 +317,8 @@ async fn kernel_task(
spawner: Spawner,
watchdog: Peri<'static, WATCHDOG>,
display: Display,
audio: Audio,
sd: Sd,
// _psram: Psram,
psram: Psram,
mcu: Mcu,
usb: Peri<'static, USB>,
) {
@@ -366,25 +326,15 @@ async fn kernel_task(
.spawn(watchdog_task(Watchdog::new(watchdog)))
.unwrap();
#[cfg(feature = "debug")]
defmt::info!("Clock: {}", embassy_rp::clocks::clk_sys_freq());
setup_mcu(mcu).await;
#[cfg(feature = "defmt")]
defmt::info!("setting up psram");
Timer::after_millis(100).await;
// setup_psram(psram).await;
#[cfg(feature = "psram")]
#[cfg(feature = "pimoroni2w")]
setup_qmi_psram().await;
Timer::after_millis(100).await;
setup_display(display, spawner).await;
setup_sd(sd).await;
spawner.spawn(audio_handler(audio)).unwrap();
let _usb = embassy_rp_usb::Driver::new(usb, Irqs);
// spawner.spawn(usb_handler(usb)).unwrap();
@@ -404,8 +354,7 @@ async fn prog_search_handler() {
let mut guard = SDCARD.get().lock().await;
let sd = guard.as_mut().unwrap();
let mut files = sd.list_files_by_extension(".bin").unwrap();
files.sort();
let files = sd.list_files_by_extension(".bin").unwrap();
let mut select = SELECTIONS.lock().await;
if *select.selections() != files {
@@ -420,8 +369,10 @@ async fn prog_search_handler() {
async fn key_handler() {
loop {
if let Some(event) = read_keyboard_fifo().await {
unsafe {
let _ = KEY_CACHE.enqueue(event);
if let KeyState::Pressed = event.state {
unsafe {
let _ = KEY_CACHE.enqueue(event);
}
}
}
Timer::after_millis(50).await;

View File

@@ -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);

View File

@@ -35,24 +35,12 @@ impl TimeSource for DummyTimeSource {
}
}
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq)]
pub struct FileName {
pub long_name: String,
pub short_name: ShortFileName,
}
impl PartialOrd for FileName {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.long_name.cmp(&other.long_name))
}
}
impl Ord for FileName {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.long_name.cmp(&other.long_name)
}
}
pub struct SdCard {
det: Input<'static>,
volume_mgr: VolMgr,

View File

@@ -75,7 +75,7 @@ pub async fn clear_selection() {
async fn draw_selection() {
let mut guard = SELECTIONS.lock().await;
let file_names = guard.selections.clone();
let file_names = &guard.selections.clone();
let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let display_area = unsafe { FRAMEBUFFER.as_mut().unwrap().bounding_box() };
@@ -99,7 +99,7 @@ async fn draw_selection() {
} else {
let mut views: alloc::vec::Vec<Text<MonoTextStyle<Rgb565>>> = Vec::new();
for i in &file_names {
for i in file_names {
views.push(Text::new(&i.long_name, Point::zero(), text_style));
}

Submodule picolibc deleted from d664643068

View File

@@ -1,10 +0,0 @@
[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"

View File

@@ -1,148 +0,0 @@
#![no_std]
extern crate alloc;
use abi::{
display::Display,
get_key,
keyboard::{KeyCode, KeyState},
};
use alloc::vec::Vec;
use embedded_graphics::{
Drawable,
mono_font::{MonoTextStyle, ascii::FONT_10X20},
pixelcolor::Rgb565,
prelude::{Dimensions, DrawTarget, Point, Primitive, RgbColor},
primitives::{PrimitiveStyle, Rectangle},
text::{Alignment, Text, renderer::TextRenderer},
};
use embedded_layout::{
align::{horizontal, vertical},
layout::linear::{FixedMargin, LinearLayout},
prelude::*,
};
#[derive(Debug)]
pub enum SelectionUiError<DisplayError> {
SelectionListEmpty,
DisplayError(DisplayError),
}
pub struct SelectionUi<'a> {
selection: usize,
items: &'a [&'a str],
last_bounds: Option<Rectangle>,
}
impl<'a> SelectionUi<'a> {
pub fn new(items: &'a [&'a str]) -> Self {
Self {
selection: 0,
items,
last_bounds: None,
}
}
pub fn run_selection_ui(
&mut self,
display: &mut Display,
) -> Result<Option<usize>, SelectionUiError<<Display as DrawTarget>::Error>> {
self.draw(display)?;
let selection;
loop {
let key = get_key();
if key.state == KeyState::Pressed {
if let Some(s) = self.update(display, key.key)? {
selection = Some(s);
display
.clear(Rgb565::BLACK)
.map_err(|e| SelectionUiError::DisplayError(e))?;
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<Option<usize>, SelectionUiError<<Display as DrawTarget>::Error>> {
match key {
KeyCode::Down => {
self.selection = (self.selection + 1).min(self.items.len() - 1);
}
KeyCode::Up => {
self.selection = self.selection.saturating_sub(1);
}
KeyCode::Enter | KeyCode::Right => return Ok(Some(self.selection)),
_ => return Ok(None),
};
self.draw(display)?;
Ok(None)
}
fn draw(
&mut self,
display: &mut Display,
) -> Result<(), SelectionUiError<<Display as DrawTarget>::Error>> {
let text_style = MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE);
let display_area = display.bounding_box();
if self.items.is_empty() {
return Err(SelectionUiError::SelectionListEmpty);
}
if let Some(bounds) = self.last_bounds {
Rectangle::new(bounds.top_left, bounds.size)
.into_styled(PrimitiveStyle::with_fill(Rgb565::BLACK))
.draw(display)
.map_err(|e| SelectionUiError::DisplayError(e))?;
}
let mut views: Vec<Text<MonoTextStyle<Rgb565>>> = 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);
layout
.draw(display)
.map_err(|e| SelectionUiError::DisplayError(e))?;
// draw selected box
if let Some(selected_bounds) = layout.inner().get(self.selection) {
let selected_bounds = selected_bounds.bounding_box();
Rectangle::new(selected_bounds.top_left, selected_bounds.size)
.into_styled(PrimitiveStyle::with_stroke(Rgb565::WHITE, 1))
.draw(display)
.map_err(|e| SelectionUiError::DisplayError(e))?;
self.last_bounds = Some(selected_bounds);
}
Ok(())
}
}
pub fn draw_text_center<'a, S>(
display: &mut Display,
text: &'a str,
style: S,
) -> Result<Point, <Display as DrawTarget>::Error>
where
S: TextRenderer<Color = <Display as DrawTarget>::Color>,
{
Text::with_alignment(text, Point::zero(), style, Alignment::Center).draw(display)
}

View File

@@ -6,7 +6,7 @@ use abi::{
display::Display,
get_key,
keyboard::{KeyCode, KeyState},
println,
print,
};
use alloc::{format, string::String, vec, vec::Vec};
use core::panic::PanicInfo;
@@ -28,7 +28,7 @@ use embedded_layout::{
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
@@ -38,8 +38,8 @@ pub extern "Rust" fn _start() {
}
pub fn main() {
println!("Starting Calculator app");
let mut display = Display::take().unwrap();
print!("Starting Calculator app");
let mut display = Display;
let mut input = vec!['e', 'x', 'p', 'r', ':', ' '];
let input_min = input.len();
@@ -104,7 +104,7 @@ pub fn main() {
}
let event = get_key();
if event.state == KeyState::Released {
if event.state != KeyState::Idle {
match event.key {
KeyCode::Char(ch) => {
input.push(ch);

12
user-apps/doom/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "doom"
version = "0.1.0"
edition = "2024"
[dependencies]
abi = { path = "../../abi" }
embedded-graphics = "0.8.1"
[build-dependencies]
bindgen = "0.71.0"
cc = "1.0"

126
user-apps/doom/build.rs Normal file
View File

@@ -0,0 +1,126 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
build_newlib();
build_doom();
// Put `memory.x` in our output directory and ensure it's
// on the linker search path.
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
println!("cargo:rustc-link-arg-bins=-Tmemory.x");
}
// TODO: use target to change newlib build - only workds for rp2350 right now
fn build_newlib() {
let out_dir = std::env::var("OUT_DIR").unwrap();
let picolibc_dir = format!("{}/picolibc", out_dir);
if !Path::new(&picolibc_dir).exists() {
let status = Command::new("git")
.args(&[
"clone",
"https://github.com/picolibc/picolibc",
&picolibc_dir,
])
.status()
.expect("Failed to clone picolibc");
assert!(status.success());
}
let build_dir = format!("{}/build", picolibc_dir);
std::fs::create_dir_all(&build_dir).unwrap();
let status = Command::new(&format!("{picolibc_dir}/scripts/do-configure"))
.current_dir(&build_dir)
.args(&[
"thumbv8m_main_fp-none-eabi",
"--buildtype=minsize",
"-Dtests=true",
"-Dtinystdio=false",
"-Dsingle-thread=true",
"-Db_pie=true",
"-Ddefault_library=static",
"-Dtinystdio=false",
"-Dmultilib=false",
".",
])
.status()
.expect("Failed to run Meson configure");
assert!(status.success());
let status = Command::new("ninja")
.current_dir(&build_dir)
.status()
.expect("Failed to run Ninja build");
assert!(status.success());
println!("cargo:rustc-link-search={}/newlib", build_dir);
println!("cargo:rustc-link-lib=c");
println!("cargo:rustc-link-lib=m");
}
fn build_doom() {
let ref dg_src_dir = std::path::PathBuf::from("doomgeneric/doomgeneric");
let mut dg_c_paths = vec![];
let mut dg_h_paths = vec![];
// Find most c and h files
for entry in std::fs::read_dir(dg_src_dir).unwrap() {
let entry = entry.unwrap();
if let Some(filename) = entry.file_name().to_str() {
if filename.starts_with("doomgeneric")
|| filename.contains("_allegro")
|| filename.contains("_sdl")
|| filename.contains("net_")
|| filename == "i_main.c"
{
continue;
}
if filename.ends_with(".h") {
dg_h_paths.push(dg_src_dir.join(filename));
} else if filename.ends_with(".c") {
dg_c_paths.push(dg_src_dir.join(filename));
}
}
}
dg_c_paths
.iter()
.chain(dg_h_paths.iter())
.for_each(|path| println!("cargo:rerun-if-changed={}", path.to_str().unwrap()));
cc::Build::new()
.compiler("arm-none-eabi-gcc")
.flag("-w") // silence warnings
.flag("-Os") // optimize for size
.flag("-ffunction-sections")
.flag("-fdata-sections")
.define("CMAP256", None)
.define("DOOMGENERIC_RESX", Some("320"))
.define("DOOMGENERIC_RESY", Some("200"))
.flag_if_supported("-std=gnu99")
.define("_POSIX_C_SOURCE", Some("200809L"))
.files(dg_c_paths)
.compile("doomgeneric");
}

31
user-apps/doom/memory.x Normal file
View File

@@ -0,0 +1,31 @@
MEMORY
{
RAM : ORIGIN = 0x0, LENGTH = 400K
}
SECTIONS
{
.text : ALIGN(4)
{
*(.text .text.*);
*(.rodata .rodata.*);
} > RAM
.data : ALIGN(4)
{
*(.data .data.*);
} > RAM
.bss : ALIGN(4)
{
*(.bss .bss.*);
*(COMMON);
} > RAM
.syscall_table (NOLOAD) : ALIGN(4)
{
__user_reloc_start = .;
KEEP(*(.user_reloc*));
__user_reloc_end = .;
} > RAM
}

112
user-apps/doom/src/doom.rs Normal file
View File

@@ -0,0 +1,112 @@
use abi::{
abi_sys::{get_ms, sleep},
display::Display,
};
use core::ffi::{c_int, c_uchar};
use embedded_graphics::{
Drawable, Pixel,
draw_target::DrawTarget,
pixelcolor::Rgb565,
prelude::{Point, RgbColor},
};
#[unsafe(no_mangle)]
#[allow(non_snake_case)]
static mut DG_ScreenBuffer: *const u8 = core::ptr::null();
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct RGBA {
pub b: u8,
pub g: u8,
pub r: u8,
pub a: u8,
}
unsafe extern "C" {
fn D_DoomMain();
fn doomgeneric_Tick();
fn M_FindResponseFile();
pub static colors: [RGBA; 256];
}
pub fn tick() {
unsafe { doomgeneric_Tick() };
}
pub struct ScreenBuffer<const RESX: usize, const RESY: usize, const SIZE: usize>(pub [u8; SIZE]);
impl<const RESX: usize, const RESY: usize, const SIZE: usize> ScreenBuffer<RESX, RESY, SIZE> {
#[allow(dead_code)]
const NA: () = assert!(SIZE == RESX * RESY);
pub const fn new() -> Self {
Self([0_u8; SIZE])
}
}
#[unsafe(no_mangle)]
extern "C" fn DG_SetWindowTitle() {}
pub fn create<const RESX: usize, const RESY: usize, const SIZE: usize>(
screenbuffer: &ScreenBuffer<RESX, RESY, SIZE>,
) {
unsafe {
M_FindResponseFile();
DG_ScreenBuffer = screenbuffer.0.as_ptr();
D_DoomMain();
}
}
const X: usize = 320;
const Y: usize = 200;
const SIZE: usize = X * Y;
pub static mut DISPLAY: Option<Display> = None;
pub static mut SCREEN_BUFFER: ScreenBuffer<X, Y, SIZE> = ScreenBuffer::new();
#[unsafe(no_mangle)]
extern "C" fn DG_DrawFrame() {
let palette565: [Rgb565; 256] = unsafe {
colors.map(|c| {
Rgb565::new(
((c.r as u16 * 31) / 255) as u8, // red 5 bits
((c.g as u16 * 63) / 255) as u8, // green 6 bits
((c.b as u16 * 31) / 255) as u8, // blue 5 bits
)
})
};
let buf = unsafe { &SCREEN_BUFFER.0 };
let display = unsafe { DISPLAY.as_mut().unwrap() };
display.clear(Rgb565::BLACK).unwrap();
for y in 0..Y {
for x in 0..X {
let idx = y * X + x;
let color = palette565[buf[idx] as usize];
Pixel(Point::new(x as i32, y as i32), color)
.draw(display)
.unwrap();
}
}
}
#[unsafe(no_mangle)]
extern "C" fn DG_GetTicksMs() -> u32 {
get_ms() as u32
}
#[unsafe(no_mangle)]
extern "C" fn DG_GetKey(pressed: *mut c_int, key: *mut c_uchar) -> c_int {
0
}
#[unsafe(no_mangle)]
extern "C" fn DG_SleepMs(ms: u32) {
sleep(ms.into());
}

118
user-apps/doom/src/libc.rs Normal file
View File

@@ -0,0 +1,118 @@
use core::ffi::{c_char, c_int, c_uchar, c_void};
use core::ptr;
#[unsafe(no_mangle)]
pub static _ctype_: [c_uchar; 256] = [0u8; 256];
#[repr(C)]
pub struct Reent {
pub errno: c_int,
_reserved: [u8; 32],
}
#[unsafe(no_mangle)]
pub static mut _reent_data: Reent = Reent {
errno: 0,
_reserved: [0; 32],
};
#[unsafe(no_mangle)]
pub static mut _impure_ptr: *mut Reent = unsafe { &mut _reent_data as *mut Reent };
#[unsafe(no_mangle)]
pub extern "C" fn __errno() -> *mut c_int {
unsafe { &mut (*_impure_ptr).errno as *mut c_int }
}
#[unsafe(no_mangle)]
pub extern "C" fn exit(_status: i32) -> ! {
loop {}
}
#[unsafe(no_mangle)]
pub extern "C" fn close(_file: i32) -> i32 {
-1
}
#[repr(C)]
pub struct Stat {
st_mode: u32,
}
#[unsafe(no_mangle)]
pub extern "C" fn fstat(_file: i32, st: *mut Stat) -> i32 {
unsafe {
if !st.is_null() {
(*st).st_mode = 0x2000; // S_IFCHR
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn isatty(_file: i32) -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn lseek(_file: i32, _ptr: i32, _dir: i32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn lseek64(_fd: c_int, _offset: i64, _whence: c_int) -> i64 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn open(_name: *const u8, _flags: i32, _mode: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn read(_file: i32, _ptr: *mut u8, len: usize) -> i32 {
len as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn write(_file: i32, _ptr: *const u8, len: usize) -> i32 {
len as i32
}
#[unsafe(no_mangle)]
pub extern "C" fn rename(_old: *const c_char, _new: *const c_char) -> c_int {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn kill(_pid: i32, _sig: i32) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn getpid() -> i32 {
1
}
#[unsafe(no_mangle)]
pub extern "C" fn link(_old: *const u8, _new: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn unlink(_name: *const u8) -> i32 {
-1
}
#[unsafe(no_mangle)]
pub extern "C" fn mkdir(_path: *const u8, _mode: u32) -> i32 {
0
}
#[unsafe(no_mangle)]
pub extern "C" fn sbrk(incr: isize) -> *mut c_void {
core::ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn fini() {}

View File

@@ -0,0 +1,43 @@
#![no_std]
#![no_main]
#![allow(static_mut_refs)]
#![feature(c_variadic)]
extern crate alloc;
use abi::{
display::Display,
fs::{file_len, read_file},
get_key, get_ms,
keyboard::{KeyCode, KeyState},
print, sleep,
};
use alloc::vec;
use core::panic::PanicInfo;
use embedded_graphics::{pixelcolor::Rgb565, prelude::Point};
mod doom;
use crate::doom::{DISPLAY, SCREEN_BUFFER, create, tick};
mod libc;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
print!("Starting Doom app");
let display = Display;
unsafe { DISPLAY = Some(display) };
unsafe { create(&SCREEN_BUFFER) };
loop {
tick();
}
}

View File

@@ -5,12 +5,12 @@
extern crate alloc;
use abi::{
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
fs::{Entries, list_dir, read_file},
fs::{list_dir, read_file},
get_key,
keyboard::{KeyCode, KeyState},
println,
print,
};
use alloc::{format, vec};
use alloc::{format, string::ToString, vec};
use core::panic::PanicInfo;
use embedded_graphics::{
Drawable, image::Image, mono_font::MonoTextStyle, mono_font::ascii::FONT_6X10,
@@ -20,7 +20,7 @@ use tinybmp::Bmp;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location());
print!("user panic: {} @ {:?}", info.message(), info.location());
loop {}
}
@@ -30,10 +30,11 @@ pub extern "Rust" fn _start() {
}
pub fn main() {
println!("Starting Gallery app");
print!("Starting Gallery app");
let mut bmp_buf = vec![0_u8; 100_000];
let mut display = Display::take().unwrap();
let mut display = Display;
// Grid parameters
let grid_cols = 3;
let grid_rows = 3;
let cell_width = SCREEN_WIDTH as i32 / grid_cols;
@@ -41,44 +42,50 @@ pub fn main() {
let mut images_drawn = 0;
let mut entries = Entries::new();
let files_num = list_dir("/images", &mut entries);
let mut files = [const { None }; 18];
let files_num = list_dir("/images", &mut files);
for file in &entries.entries()[2..files_num] {
for file in &files[2..files_num] {
if images_drawn >= grid_cols * grid_rows {
break; // only draw 3x3
}
println!("file: {}", file);
if file.extension().unwrap_or("") == "bmp" || file.extension().unwrap_or("") == "BMP" {
let file_path = format!("/images/{}", file);
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);
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 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 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))
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,
)
.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(&file.base(), Point::new(cell_x + 2, text_y), text_style)
.draw(&mut display)
.unwrap();
images_drawn += 1;
images_drawn += 1;
}
}
}
}

View File

@@ -5,6 +5,5 @@ edition = "2024"
[dependencies]
abi = { path = "../../abi" }
selection_ui = { path = "../../selection_ui" }
embedded-graphics = "0.8.1"
tinygif = { git = "https://github.com/LegitCamper/tinygif" }

View File

@@ -3,27 +3,22 @@
extern crate alloc;
use abi::{
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
fs::{Entries, file_len, list_dir, read_file},
display::Display,
fs::{file_len, read_file},
get_key, get_ms,
keyboard::{KeyCode, KeyState},
println, sleep,
print, sleep,
};
use alloc::{format, vec, vec::Vec};
use alloc::vec;
use core::panic::PanicInfo;
use embedded_graphics::{
image::ImageDrawable,
mono_font::{MonoTextStyle, ascii::FONT_6X10},
pixelcolor::Rgb565,
prelude::{Point, RgbColor},
transform::Transform,
image::ImageDrawable, pixelcolor::Rgb565, prelude::Point, transform::Transform,
};
use selection_ui::{SelectionUi, SelectionUiError, draw_text_center};
use tinygif::Gif;
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
@@ -33,66 +28,34 @@ pub extern "Rust" fn _start() {
}
pub fn main() {
println!("Starting Gif app");
let mut display = Display::take().unwrap();
print!("Starting Gif app");
let mut display = Display;
let mut entries = Entries::new();
list_dir("/gifs", &mut entries);
let mut files = entries.entries();
files.retain(|e| e.extension().unwrap_or("") == "gif");
let mut gifs = files.iter().map(|e| e.full_name()).collect::<Vec<&str>>();
gifs.sort();
let mut selection_ui = SelectionUi::new(&mut gifs);
let selection = match selection_ui.run_selection_ui(&mut display) {
Ok(maybe_sel) => maybe_sel,
Err(e) => match e {
SelectionUiError::SelectionListEmpty => {
draw_text_center(
&mut display,
"No Gifs were found in /gifs",
MonoTextStyle::new(&FONT_6X10, Rgb565::RED),
)
.expect("Display Error");
None
}
SelectionUiError::DisplayError(_) => panic!("Display Error"),
},
};
assert!(selection.is_some());
let file_name = format!("/gifs/{}", gifs[selection.unwrap()]);
let size = file_len(&file_name);
let size = file_len("/gifs/bad_apple.gif");
let mut buf = vec![0_u8; size];
let read = read_file(&file_name, 0, &mut buf);
println!("read: {}, file size: {}", read, size);
let read = read_file("/gifs/bad_apple.gif", 0, &mut buf);
print!("read: {}, file size: {}", read, size);
assert!(read == size);
let gif = Gif::<Rgb565>::from_slice(&buf).expect("Failed to parse gif");
let translation = Point::new(
(SCREEN_WIDTH as i32 - gif.width() as i32) / 2,
(SCREEN_HEIGHT as i32 - gif.height() as i32) / 2,
);
let gif = Gif::<Rgb565>::from_slice(&buf).unwrap();
let height = gif.height();
let mut frame_num = 0;
loop {
for mut frame in gif.frames() {
let start = get_ms();
frame.translate_mut(translation).draw(&mut display).unwrap();
frame
.translate_mut(Point::new(0, (320 - height as i32) / 2))
.draw(&mut display)
.unwrap();
frame_num += 1;
if frame_num % 5 == 0 {
if frame_num % 100 == 0 {
let event = get_key();
if event.state != KeyState::Idle {
match event.key {
KeyCode::Esc => {
drop(buf);
return;
}
KeyCode::Esc => return,
_ => (),
};
};

View File

@@ -7,7 +7,7 @@ use abi::{
display::{Display, SCREEN_HEIGHT, SCREEN_WIDTH},
get_key,
keyboard::{KeyCode, KeyState},
println, sleep,
print, sleep,
};
use core::panic::PanicInfo;
use embedded_graphics::{pixelcolor::Rgb565, prelude::RgbColor};
@@ -15,7 +15,7 @@ use embedded_snake::{Direction, SnakeGame};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
print!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
@@ -27,8 +27,8 @@ pub extern "Rust" fn _start() {
const CELL_SIZE: usize = 8;
pub fn main() {
println!("Starting Snake app");
let mut display = Display::take().unwrap();
print!("Starting Snake app");
let mut display = Display;
let mut game = SnakeGame::<100, Rgb565, Rng>::new(
SCREEN_WIDTH as u16,

View File

@@ -1,11 +0,0 @@
[package]
name = "wav_player"
version = "0.1.0"
edition = "2024"
[dependencies]
abi = { path = "../../abi" }
selection_ui = { path = "../../selection_ui" }
embedded-graphics = "0.8.1"
rand = { version = "0.9.0", default-features = false }
embedded-audio = { git = "https://github.com/LegitCamper/embedded-audio" }

View File

@@ -1,28 +0,0 @@
//! This build script copies the `memory.x` file from the crate root into
//! a directory where the linker can always find it at build time.
//! For many projects this is optional, as the linker always searches the
//! project root directory -- wherever `Cargo.toml` is. However, if you
//! are using a workspace or have a more complicated build setup, this
//! build script becomes required. Additionally, by requesting that
//! Cargo re-run the build script whenever `memory.x` is changed,
//! updating `memory.x` ensures a rebuild of the application with the
//! new memory settings.
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
// Put `memory.x` in our output directory and ensure it's
// on the linker search path.
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.unwrap()
.write_all(include_bytes!("../memory.x"))
.unwrap();
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
println!("cargo:rustc-link-arg-bins=-Tmemory.x");
}

View File

@@ -1,139 +0,0 @@
#![no_std]
#![no_main]
extern crate alloc;
use abi::{
audio::{AUDIO_BUFFER_LEN, audio_buffer_ready, send_audio_buffer},
display::Display,
format,
fs::{Entries, file_len, list_dir, read_file},
get_key,
keyboard::{KeyCode, KeyState},
println,
};
use alloc::{string::String, vec::Vec};
use core::panic::PanicInfo;
use embedded_audio::{AudioFile, PlatformFile, PlatformFileError, wav::Wav};
use embedded_graphics::{
mono_font::{MonoTextStyle, ascii::FONT_6X10},
pixelcolor::Rgb565,
prelude::RgbColor,
};
use selection_ui::{SelectionUi, SelectionUiError, draw_text_center};
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("user panic: {} @ {:?}", info.message(), info.location(),);
loop {}
}
#[unsafe(no_mangle)]
pub extern "Rust" fn _start() {
main()
}
pub fn main() {
println!("Starting Wav player app");
let mut display = Display::take().unwrap();
loop {
let mut entries = Entries::new();
list_dir("/music", &mut entries);
let mut files = entries.entries();
files.retain(|e| e.extension().unwrap_or("") == "wav");
let mut wavs = files.iter().map(|e| e.full_name()).collect::<Vec<&str>>();
wavs.sort();
let mut selection_ui = SelectionUi::new(&mut wavs);
let selection = match selection_ui.run_selection_ui(&mut display) {
Ok(maybe_sel) => maybe_sel,
Err(e) => match e {
SelectionUiError::SelectionListEmpty => {
draw_text_center(
&mut display,
"No Wavs were found in /gifs",
MonoTextStyle::new(&FONT_6X10, Rgb565::RED),
)
.expect("Display Error");
None
}
SelectionUiError::DisplayError(_) => panic!("Display Error"),
},
};
assert!(selection.is_some());
let file_name = format!("/music/{}", wavs[selection.unwrap()]);
let file = File::new(String::from(file_name));
let mut wav = Wav::new(file).unwrap();
println!("sample rate: {}", wav.sample_rate());
println!("channels: {:?}", wav.channels() as u8);
let mut buf = [0_u8; AUDIO_BUFFER_LEN];
loop {
if audio_buffer_ready() {
if wav.is_eof() {
break;
}
let _read = wav.read(&mut buf).unwrap();
send_audio_buffer(&buf);
}
let event = get_key();
if event.state == KeyState::Released {
match event.key {
KeyCode::Esc => return,
_ => (),
}
}
}
}
}
struct File {
current_pos: usize,
file: String,
}
impl File {
fn new(file: String) -> Self {
Self {
current_pos: 0,
file,
}
}
}
impl PlatformFile for File {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, PlatformFileError> {
let read = read_file(&self.file, self.current_pos, buf);
self.current_pos += read;
Ok(read)
}
fn seek_from_current(&mut self, offset: i64) -> Result<(), PlatformFileError> {
if offset.is_positive() {
self.current_pos += offset as usize;
} else {
self.current_pos -= offset as usize;
}
Ok(())
}
fn seek_from_start(&mut self, offset: usize) -> Result<(), PlatformFileError> {
self.current_pos = offset;
Ok(())
}
fn seek_from_end(&mut self, offset: usize) -> Result<(), PlatformFileError> {
self.current_pos = self.length() - offset;
Ok(())
}
fn length(&mut self) -> usize {
file_len(&self.file)
}
}