This commit is contained in:
2025-11-13 12:31:40 -07:00
parent 35d9cd092d
commit b1a0351399
17 changed files with 293 additions and 76 deletions

1
Cargo.lock generated
View File

@@ -1291,6 +1291,7 @@ dependencies = [
"bindgen",
"cc",
"embedded-graphics",
"selection_ui",
]
[[package]]

View File

@@ -3,8 +3,8 @@
extern crate alloc;
pub use abi_sys::{self, keyboard};
use abi_sys::{RngRequest, alloc, dealloc, keyboard::KeyEvent};
pub use abi_sys::{keyboard, print};
pub use alloc::format;
use core::alloc::{GlobalAlloc, Layout};
use rand_core::RngCore;
@@ -28,7 +28,7 @@ unsafe impl GlobalAlloc for Alloc {
macro_rules! print {
($($arg:tt)*) => {{
let s = $crate::format!($($arg)*);
$crate::abi_sys::print(s.as_ptr(), s.len());
$crate::print(s.as_ptr(), s.len());
}};
}
@@ -61,10 +61,8 @@ pub mod display {
pub type Pixel565 = Pixel<Rgb565>;
const BUF_SIZE: usize = 15 * 1024; // tune this for performance
const BUF_SIZE: usize = 1024;
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);
@@ -160,20 +158,30 @@ impl RngCore for Rng {
}
pub mod fs {
use alloc::vec::Vec;
use core::fmt::Display;
use alloc::{format, vec::Vec};
pub fn read_file(file: &str, read_from: usize, buf: &mut [u8]) -> usize {
pub fn read_file(file: &str, start_from: usize, buf: &mut [u8]) -> usize {
abi_sys::read_file(
file.as_ptr(),
file.len(),
read_from,
start_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,
@@ -217,7 +225,7 @@ pub mod fs {
const MAX_ENTRY_NAME_LEN: usize = 25;
const MAX_ENTRIES: usize = 25;
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub struct Entries([[u8; MAX_ENTRY_NAME_LEN]; MAX_ENTRIES]);
impl Entries {

View File

@@ -12,7 +12,7 @@ use strum::{EnumCount, EnumIter};
pub type EntryFn = fn();
pub const ABI_CALL_TABLE_COUNT: usize = 11;
pub const ABI_CALL_TABLE_COUNT: usize = 12;
const _: () = assert!(ABI_CALL_TABLE_COUNT == CallTable::COUNT);
#[derive(Clone, Copy, EnumIter, EnumCount)]
@@ -28,7 +28,8 @@ pub enum CallTable {
GenRand = 7,
ListDir = 8,
ReadFile = 9,
FileLen = 10,
WriteFile = 10,
FileLen = 11,
}
#[unsafe(no_mangle)]
@@ -438,6 +439,24 @@ 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)]

View File

@@ -1,6 +1,6 @@
use abi_sys::{
AllocAbi, CLayout, CPixel, DeallocAbi, DrawIterAbi, FileLen, GenRand, GetMsAbi, ListDir,
PrintAbi, ReadFile, RngRequest, SleepMsAbi, keyboard::*,
PrintAbi, ReadFile, RngRequest, SleepMsAbi, WriteFile, keyboard::*,
};
use alloc::{string::ToString, vec::Vec};
use core::{ffi::c_char, ptr, sync::atomic::Ordering};
@@ -207,6 +207,7 @@ fn recurse_file<T>(
dirs: &[&str],
mut access: impl FnMut(&mut File) -> T,
) -> Result<T, ()> {
defmt::info!("dir: {}, dirs: {}", dir, dirs);
if dirs.len() == 1 {
let mut b = [0_u8; 50];
let mut buf = LfnBuffer::new(&mut b);
@@ -218,7 +219,8 @@ fn recurse_file<T>(
}
}
})
.unwrap();
.expect("Failed to iterate dir");
if let Some(name) = short_name {
let mut file = dir
.open_file_in_dir(name, embedded_sdmmc::Mode::ReadWriteAppend)
@@ -242,7 +244,17 @@ 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 file: Vec<&str> = file.split('/').collect();
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 `ptr` is valid for `len` bytes
let mut buf = unsafe { core::slice::from_raw_parts_mut(buf, buf_len) };
@@ -252,8 +264,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, &file[1..], |file| {
file.seek_from_start(start_from as u32).unwrap();
if let Ok(result) = recurse_file(&root, &components[1..count], |file| {
file.seek_from_start(start_from as u32).unwrap_or(());
file.read(&mut buf).unwrap()
}) {
read = result
@@ -263,9 +275,46 @@ 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 `ptr` is valid for `len` bytes
// SAFETY: caller guarantees str ptr is valid for `len` bytes
let file = unsafe { core::str::from_raw_parts(str, len) };
let file: Vec<&str> = file.split('/').collect();

View File

@@ -87,8 +87,14 @@ 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 {
// renders fps text to canvas
let start = Instant::now();
#[cfg(feature = "fps")]
unsafe {
if FPS_COUNTER.should_draw() {
@@ -103,11 +109,15 @@ pub async fn display_handler(mut display: DISPLAY) {
.unwrap()
.partial_draw(&mut display)
.await
.unwrap()
};
.unwrap();
}
}
// small yield to allow other tasks to run
Timer::after_millis(10).await;
let elapsed = start.elapsed().as_millis() as u64;
if elapsed < FRAME_TIME_MS {
Timer::after_millis(FRAME_TIME_MS - elapsed).await;
} else {
Timer::after_millis(1).await;
}
}
}

View File

@@ -206,6 +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,
};
unsafe {

View File

@@ -122,7 +122,7 @@ static UI_CHANGE: Signal<CriticalSectionRawMutex, ()> = Signal::new();
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = if cfg!(feature = "overclock") {
let clocks = ClockConfig::system_freq(192_000_000).unwrap();
let clocks = ClockConfig::system_freq(300_000_000).unwrap();
let config = Config::new(clocks);
embassy_rp::init(config)
} else {
@@ -339,6 +339,9 @@ 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")]
@@ -372,7 +375,8 @@ async fn prog_search_handler() {
let mut guard = SDCARD.get().lock().await;
let sd = guard.as_mut().unwrap();
let files = sd.list_files_by_extension(".bin").unwrap();
let mut files = sd.list_files_by_extension(".bin").unwrap();
files.sort();
let mut select = SELECTIONS.lock().await;
if *select.selections() != files {
@@ -387,10 +391,8 @@ async fn prog_search_handler() {
async fn key_handler() {
loop {
if let Some(event) = read_keyboard_fifo().await {
if let KeyState::Pressed = event.state {
unsafe {
let _ = KEY_CACHE.enqueue(event);
}
unsafe {
let _ = KEY_CACHE.enqueue(event);
}
}
Timer::after_millis(50).await;

View File

@@ -35,12 +35,24 @@ impl TimeSource for DummyTimeSource {
}
}
#[derive(Clone, PartialEq)]
#[derive(Clone, PartialEq, Eq)]
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));
}

View File

@@ -55,6 +55,9 @@ impl<'a> SelectionUi<'a> {
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;
}
}
@@ -69,7 +72,6 @@ impl<'a> SelectionUi<'a> {
display: &mut Display,
key: KeyCode,
) -> Result<Option<usize>, SelectionUiError<<Display as DrawTarget>::Error>> {
print!("Got Key: {:?}", key);
match key {
KeyCode::Down => {
self.selection = (self.selection + 1).min(self.items.len() - 1);
@@ -80,7 +82,6 @@ impl<'a> SelectionUi<'a> {
KeyCode::Enter | KeyCode::Right => return Ok(Some(self.selection)),
_ => return Ok(None),
};
print!("new selection: {:?}", self.selection);
self.draw(display)?;
Ok(None)
}

View File

@@ -104,7 +104,7 @@ pub fn main() {
}
let event = get_key();
if event.state != KeyState::Idle {
if event.state == KeyState::Released {
match event.key {
KeyCode::Char(ch) => {
input.push(ch);

View File

@@ -9,4 +9,5 @@ cc = "1.2.44"
[dependencies]
abi = { path = "../../abi" }
selection_ui = { path = "../../selection_ui" }
embedded-graphics = "0.8.1"

View File

@@ -45,6 +45,7 @@ fn bindgen() {
.expect("Couldn't write bindings!");
cc::Build::new()
.define("PEANUT_GB_IS_LITTLE_ENDIAN", None)
.file("peanut_gb_stub.c")
.include("Peanut-GB")
// optimization flags

View File

@@ -5,24 +5,34 @@
extern crate alloc;
use abi::{
display::Display,
fs::{file_len, read_file},
format,
fs::{Entries, file_len, list_dir, read_file, write_file},
get_key,
keyboard::{KeyCode, KeyState},
print,
};
use alloc::{vec, vec::Vec};
use core::{ffi::c_void, mem::MaybeUninit, panic::PanicInfo};
use alloc::{string::String, vec, vec::Vec};
use core::{cell::LazyCell, ffi::c_void, mem::MaybeUninit, panic::PanicInfo};
use embedded_graphics::{
mono_font::{MonoTextStyle, ascii::FONT_6X10},
pixelcolor::Rgb565,
prelude::RgbColor,
};
use selection_ui::{SelectionUi, SelectionUiError, draw_text_center};
mod peanut;
use peanut::gb_run_frame;
use crate::peanut::{
JOYPAD_A, JOYPAD_B, JOYPAD_DOWN, JOYPAD_LEFT, JOYPAD_RIGHT, JOYPAD_SELECT, JOYPAD_START,
JOYPAD_UP, gb_cart_ram_read, gb_cart_ram_write, gb_error, gb_init, gb_init_lcd, gb_rom_read,
gb_s, lcd_draw_line,
JOYPAD_UP, gb_cart_ram_read, gb_cart_ram_write, gb_error, gb_get_rom_name, gb_get_save_size,
gb_init, gb_init_lcd, gb_rom_read, gb_s, lcd_draw_line,
};
static mut DISPLAY: Display = Display;
static mut DISPLAY: LazyCell<Display> = LazyCell::new(|| Display::take().unwrap());
const RAM_SIZE: usize = 32 * 1024; // largest ram size is 32k
static mut RAM: [u8; RAM_SIZE] = [0; RAM_SIZE];
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
@@ -35,7 +45,7 @@ pub extern "Rust" fn _start() {
main()
}
const GAME: &'static str = "/games/gameboy/zelda.gb";
const GAME_PATH: &'static str = "/games/gameboy";
static mut GAME_ROM: Option<Vec<u8>> = None;
@@ -45,9 +55,43 @@ struct Priv {}
pub fn main() {
print!("Starting Gameboy app");
let size = file_len(GAME);
let mut entries = Entries::new();
list_dir(GAME_PATH, &mut entries);
let mut files = entries.entries();
files.retain(|e| {
let ext = e.extension().unwrap_or("");
ext == "gb" || ext == "GB"
});
let mut roms = files.iter().map(|e| e.full_name()).collect::<Vec<&str>>();
roms.sort();
let selection = {
let display = unsafe { &mut *DISPLAY };
let mut selection_ui = SelectionUi::new(&roms);
match selection_ui.run_selection_ui(display) {
Ok(maybe_sel) => maybe_sel,
Err(e) => match e {
SelectionUiError::SelectionListEmpty => {
draw_text_center(
display,
&format!("No Roms were found in {}", GAME_PATH),
MonoTextStyle::new(&FONT_6X10, Rgb565::RED),
)
.expect("Display Error");
None
}
SelectionUiError::DisplayError(_) => panic!("Display Error"),
},
}
};
assert!(selection.is_some());
let file_name = format!("{}/{}", GAME_PATH, roms[selection.unwrap()]);
let size = file_len(&file_name);
unsafe { GAME_ROM = Some(vec![0_u8; size]) };
let read = read_file(GAME, 0, unsafe { GAME_ROM.as_mut().unwrap() });
let read = read_file(&file_name, 0, unsafe { GAME_ROM.as_mut().unwrap() });
assert!(size == read);
print!("Rom size: {}", read);
@@ -66,40 +110,92 @@ pub fn main() {
};
print!("gb init status: {}", init_status);
unsafe {
load_save(&mut gb.assume_init());
}
unsafe {
gb_init_lcd(gb.as_mut_ptr(), Some(lcd_draw_line));
// enable frame skip
gb.assume_init().direct.set_frame_skip(true);
gb.assume_init().direct.set_frame_skip(!true); // active low
};
loop {
let event = get_key();
let keycode = match event.key {
KeyCode::Esc => break,
KeyCode::Tab => Some(JOYPAD_START),
KeyCode::Del => Some(JOYPAD_SELECT),
KeyCode::Enter => Some(JOYPAD_A),
KeyCode::Backspace => Some(JOYPAD_B),
KeyCode::JoyUp => Some(JOYPAD_UP),
KeyCode::JoyDown => Some(JOYPAD_DOWN),
KeyCode::JoyLeft => Some(JOYPAD_LEFT),
KeyCode::JoyRight => Some(JOYPAD_RIGHT),
_ => None,
let button = match event.key {
KeyCode::Esc => {
unsafe { write_save(&mut gb.assume_init()) };
break;
}
KeyCode::Tab => JOYPAD_START as u8,
KeyCode::Del => JOYPAD_SELECT as u8,
KeyCode::Enter => JOYPAD_A as u8,
KeyCode::Backspace => JOYPAD_B as u8,
KeyCode::Up => JOYPAD_UP as u8,
KeyCode::Down => JOYPAD_DOWN as u8,
KeyCode::Left => JOYPAD_LEFT as u8,
KeyCode::Right => JOYPAD_RIGHT as u8,
_ => 0,
};
if let Some(keycode) = keycode {
if button != 0 {
let mut joypad = unsafe { (*gb.as_mut_ptr()).direct.__bindgen_anon_1.joypad };
match event.state {
KeyState::Pressed => unsafe {
(*gb.as_mut_ptr()).direct.__bindgen_anon_1.joypad &= !keycode as u8
},
KeyState::Released => unsafe {
(*gb.as_mut_ptr()).direct.__bindgen_anon_1.joypad |= keycode as u8
},
_ => (),
KeyState::Pressed => joypad &= !button,
KeyState::Released => joypad |= button,
_ => {}
}
print!("joypad now: {:#010b}\n", joypad);
}
unsafe { gb_run_frame(gb.as_mut_ptr()) };
unsafe {
gb_run_frame(gb.as_mut_ptr());
}
}
}
unsafe fn load_save(gb: &mut gb_s) {
let mut buf = [0; 16];
unsafe {
gb_get_rom_name(gb, buf.as_mut_ptr());
let save_size = gb_get_save_size(gb);
if save_size > 0 {
read_file(
&format!(
"{}/saves/{}.sav",
GAME_PATH,
str::from_utf8(&buf).expect("bad rom name")
),
0,
&mut RAM,
);
}
}
}
unsafe fn write_save(gb: &mut gb_s) {
let mut buf = [0; 16];
unsafe {
gb_get_rom_name(gb, buf.as_mut_ptr());
let save_size = gb_get_save_size(gb);
if save_size > 0 {
write_file(
&format!(
"{}/saves/{}.sav",
GAME_PATH,
str::from_utf8(&buf).expect("bad rom name")
),
0,
&mut RAM,
);
}
}
}

View File

@@ -2,27 +2,42 @@
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use crate::{DISPLAY, GAME_ROM};
use crate::{DISPLAY, GAME_ROM, RAM};
#[allow(unused)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
use abi::{display::Pixel565, fs::read_file};
use abi::{display::Pixel565, print};
use embedded_graphics::{Drawable, pixelcolor::Rgb565, prelude::Point};
pub const GBOY_WIDTH: usize = 160;
pub const GBOY_HEIGHT: usize = 144;
pub unsafe extern "C" fn gb_rom_read(gb: *mut gb_s, addr: u32) -> u8 {
pub unsafe extern "C" fn gb_rom_read(_gb: *mut gb_s, addr: u32) -> u8 {
unsafe { GAME_ROM.as_ref().unwrap()[addr as usize] }
}
pub unsafe extern "C" fn gb_cart_ram_read(gb: *mut gb_s, addr: u32) -> u8 {
0
pub unsafe extern "C" fn gb_cart_ram_read(_gb: *mut gb_s, addr: u32) -> u8 {
unsafe { RAM[addr as usize] }
}
pub unsafe extern "C" fn gb_cart_ram_write(gb: *mut gb_s, addr: u32, val: u8) {}
pub unsafe extern "C" fn gb_cart_ram_write(_gb: *mut gb_s, addr: u32, val: u8) {
unsafe { RAM[addr as usize] = val }
}
pub unsafe extern "C" fn gb_error(gb: *mut gb_s, err: gb_error_e, addr: u16) {}
pub unsafe extern "C" fn gb_error(_gb: *mut gb_s, err: gb_error_e, addr: u16) {
let e = match err {
0 => "UNKNOWN ERROR",
1 => "INVALID OPCODE",
2 => "INVALID READ",
3 => "INVALID WRITE",
4 => "HALT FOREVER",
5 => "INVALID MAX",
_ => unreachable!(),
};
print!("PeanutGB error: {}, addr: {}", e, addr);
}
const NUM_PALETTES: usize = 3;
const SHADES_PER_PALETTE: usize = 4;
@@ -82,6 +97,6 @@ fn draw_color(color: Rgb565, x: u16, y: u16) {
pixel.1 = color;
unsafe {
pixel.draw(&mut DISPLAY).unwrap();
pixel.draw(&mut *DISPLAY).unwrap();
}
}

View File

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

View File

@@ -41,9 +41,10 @@ pub fn main() {
let mut files = entries.entries();
files.retain(|e| e.extension().unwrap_or("") == "gif");
let gifs = &files.iter().map(|e| e.full_name()).collect::<Vec<&str>>();
let mut gifs = files.iter().map(|e| e.full_name()).collect::<Vec<&str>>();
gifs.sort();
let mut selection_ui = SelectionUi::new(&gifs);
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 {