From ca2481502e4373fac74e08fd5101fd6ea22e21eb Mon Sep 17 00:00:00 2001 From: Alex Meyer Date: Tue, 22 Apr 2025 19:32:23 -0500 Subject: [PATCH 1/2] Image rendering ("show" command) demo --- Cargo.lock | 11 +++ Cargo.toml | 1 + crates/kernel/scripts/compile-init.sh | 1 + crates/show/.gitignore | 1 + crates/show/Cargo.toml | 14 +++ crates/show/build.rs | 6 ++ crates/show/build.sh | 25 +++++ crates/show/src/main.rs | 127 ++++++++++++++++++++++++++ 8 files changed, 186 insertions(+) create mode 100644 crates/show/.gitignore create mode 100644 crates/show/Cargo.toml create mode 100644 crates/show/build.rs create mode 100755 crates/show/build.sh create mode 100644 crates/show/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 922be0b6..5963f6e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,17 @@ dependencies = [ "ulib", ] +[[package]] +name = "show" +version = "0.1.0" +dependencies = [ + "bytemuck", + "display-client", + "gfx", + "lz4", + "ulib", +] + [[package]] name = "smallbox" version = "0.8.6" diff --git a/Cargo.toml b/Cargo.toml index 0b2d6ed3..0f92be11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ members = [ "crates/kernel", "crates/lz4", "crates/shell", + "crates/show", "crates/ulib", ] diff --git a/crates/kernel/scripts/compile-init.sh b/crates/kernel/scripts/compile-init.sh index 0121fb55..8288ac90 100755 --- a/crates/kernel/scripts/compile-init.sh +++ b/crates/kernel/scripts/compile-init.sh @@ -8,4 +8,5 @@ FS_PATH="../../disk-image" DESTDIR="$FS_PATH" ../shell/build.sh DESTDIR="$FS_PATH" ../console/build.sh DESTDIR="$FS_PATH" ../display-server/build.sh +DESTDIR="$FS_PATH" ../show/build.sh ../init/build.sh diff --git a/crates/show/.gitignore b/crates/show/.gitignore new file mode 100644 index 00000000..50992530 --- /dev/null +++ b/crates/show/.gitignore @@ -0,0 +1 @@ +*.elf diff --git a/crates/show/Cargo.toml b/crates/show/Cargo.toml new file mode 100644 index 00000000..79fc5b4b --- /dev/null +++ b/crates/show/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "show" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck = { version = "1.19.0", default-features = false } +display-client = { path = "../display-client" } +lz4 = { path = "../lz4" } +ulib = { path = "../ulib", features = ["heap-impl"] } +gfx = { path = "../gfx" } + +[dev-dependencies] +ulib = { path = "../ulib", features = ["test"] } diff --git a/crates/show/build.rs b/crates/show/build.rs new file mode 100644 index 00000000..fe806e18 --- /dev/null +++ b/crates/show/build.rs @@ -0,0 +1,6 @@ +fn main() { + let crate_root = std::env::var("CARGO_MANIFEST_DIR").unwrap(); + println!("cargo::rerun-if-changed=../ulib/script.ld"); + println!("cargo::rustc-link-arg-bins=-T{crate_root}/../ulib/script.ld"); + println!("cargo::rustc-link-arg-bins=-n"); +} diff --git a/crates/show/build.sh b/crates/show/build.sh new file mode 100755 index 00000000..83595802 --- /dev/null +++ b/crates/show/build.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -ex +cd "$(dirname "$0")" + +BIN="show" +TARGET=aarch64-unknown-none-softfloat +PROFILE=${PROFILE-"release"} + +cargo rustc --profile="${PROFILE}" \ + --target="${TARGET}" \ + --bin="${BIN}" \ + -- -C relocation-model=static + +if test "$PROFILE" = "dev" ; then + BINARY=../../target/${TARGET}/debug/${BIN} +else + BINARY=../../target/${TARGET}/${PROFILE}/${BIN} +fi + +cp "${BINARY}" "${BIN}".elf + +if test -n "$DESTDIR" ; then + cp "${BINARY}" "$DESTDIR/" +fi diff --git a/crates/show/src/main.rs b/crates/show/src/main.rs new file mode 100644 index 00000000..adfb9754 --- /dev/null +++ b/crates/show/src/main.rs @@ -0,0 +1,127 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] + +extern crate alloc; +extern crate display_client; + +#[macro_use] +extern crate ulib; + +use display_client::proto; + +use ulib::sys::FileDesc; + +// TODO: stat for file length +fn read_all(fd: FileDesc) -> alloc::vec::Vec { + let mut out = alloc::vec::Vec::new(); + let mut buf = [0u8; 512]; + let mut offset = 0; + loop { + match ulib::sys::pread(fd, &mut buf, offset) { + Ok(0) => break, + Ok(len) => { + out.extend(&buf[..len]); + offset += len as u64; + } + Err(e) => { + println!("Error reading file: {e}"); + ulib::sys::exit(1); + } + } + } + out +} + +#[no_mangle] +fn main(argc: usize, argv: *const *const u8) { + let argv_array = unsafe { core::slice::from_raw_parts(argv, argc) }; + let args = argv_array + .iter() + .copied() + .map(|arg| unsafe { core::ffi::CStr::from_ptr(arg) }.to_bytes()) + .map(|arg| core::str::from_utf8(arg).unwrap()) + .collect::>(); + + let file = args[1]; + + let img_data; + let img_width; + let img_height; + + if file.ends_with("qoi") { + let Ok(file) = ulib::sys::openat(3, file.as_bytes(), 0, 0) else { + println!("Error opening file {}", file); + ulib::sys::exit(1); + }; + + let img = read_all(file); + let (header, data) = gfx::format::qoi::read_qoi_header(&img).unwrap(); + let width = header.width as usize; + let height = header.height as usize; + let mut output = alloc::vec![0u32; width * height]; + gfx::format::qoi::decode_qoi(&header, data, &mut output, width); + + img_width = width; + img_height = height; + img_data = output; + } else { + println!("Unknown file format, exiting."); + ulib::sys::exit(1); + } + + let mut buf = display_client::connect(img_width as u16, img_height as u16); + buf.set_title(alloc::format!("show - {}", file).as_bytes()); + + let (width, height) = ( + buf.video_meta.width as usize, + buf.video_meta.height as usize, + ); + let row_stride = buf.video_meta.row_stride as usize / 4; + + 'outer: loop { + while let Some(ev) = buf.server_to_client_queue().try_recv() { + match ev.kind { + proto::EventKind::INPUT => { + use proto::EventData; + let data = proto::InputEvent::parse(&ev).expect("TODO"); + if data.kind == proto::InputEvent::KIND_KEY { + match proto::ScanCode(data.data2) { + proto::ScanCode::ESCAPE | proto::ScanCode::Q => { + break 'outer; + } + _ => (), + } + } + } + proto::EventKind::REQUEST_CLOSE => { + break 'outer; + } + _ => (), + } + } + + let fb = buf.video_mem(); + gfx::blit_buffer( + fb, width, height, row_stride, 0, 0, &img_data, img_width, img_height, img_width, + ); + + buf.client_to_server_queue() + .try_send(proto::Event { + kind: proto::EventKind::PRESENT, + data: [0; 7], + }) + .ok(); + + // signal(video)? for sync + ulib::sys::sem_down(buf.get_sem_fd(buf.present_sem)).unwrap(); + + unsafe { ulib::sys::sys_sleep_ms(1000) }; + } + + buf.client_to_server_queue() + .try_send(proto::Event { + kind: proto::EventKind::DISCONNECT, + data: [0; 7], + }) + .ok(); +} From ea83d14a00a3195f23eccda526a5ba562b776490 Mon Sep 17 00:00:00 2001 From: Alex Meyer Date: Sun, 27 Apr 2025 17:08:19 -0500 Subject: [PATCH 2/2] Add slideshow mode to 'show' command --- crates/kernel/scripts/run-usb.sh | 2 + crates/show/src/main.rs | 152 +++++++++++++++++++++++++++---- 2 files changed, 136 insertions(+), 18 deletions(-) diff --git a/crates/kernel/scripts/run-usb.sh b/crates/kernel/scripts/run-usb.sh index 8e7c9359..35afe253 100755 --- a/crates/kernel/scripts/run-usb.sh +++ b/crates/kernel/scripts/run-usb.sh @@ -6,6 +6,8 @@ QEMU_DEVICES="-usb -device usb-kbd -device usb-mouse -device usb-net,netdev=net0 -netdev user,id=net0,hostfwd=tcp::2222-:22" \ "$(dirname "$0")/run.sh" +#QEMU_DISPLAY=${QEMU_DISPLAY-"default"} QEMU_DEVICES="-usb -device usb-kbd -device usb-mouse" "$(dirname "$0")/run.sh" +#QEMU_DISPLAY=${QEMU_DISPLAY-"default"} QEMU_DEVICES="-usb -device usb-kbd -device usb-mouse -device usb-net,netdev=net0 -netdev user,id=net0,hostfwd=tcp::2222-:22 -object filter-dump,id=f1,netdev=net0,file=net0.pcap" "$(dirname "$0")/run.sh" # QEMU_DISPLAY=${QEMU_DISPLAY-"default"} QEMU_DEVICES="-usb -device usb-kbd -device usb-mouse -device usb-net,netdev=net0 -netdev tap,id=net0,ifname=tap0,script=no,downscript=no -object filter-dump,id=f1,netdev=net0,file=net0.pcap" "$(dirname "$0")/run.sh" # QEMU_DISPLAY=${QEMU_DISPLAY-"default"} QEMU_DEVICES="-usb -device usb-kbd -device usb-mouse -device usb-net,netdev=net0 -netdev bridge,id=net0,br=br0 -object filter-dump,id=f1,netdev=net0,file=net0.pcap" "$(dirname "$0")/run.sh" diff --git a/crates/show/src/main.rs b/crates/show/src/main.rs index adfb9754..06b78658 100644 --- a/crates/show/src/main.rs +++ b/crates/show/src/main.rs @@ -7,6 +7,9 @@ extern crate display_client; #[macro_use] extern crate ulib; +use alloc::borrow::ToOwned; +use alloc::string::String; +use alloc::vec::Vec; use display_client::proto; use ulib::sys::FileDesc; @@ -32,6 +35,76 @@ fn read_all(fd: FileDesc) -> alloc::vec::Vec { out } +fn load_image(file: u32) -> (usize, usize, Vec) { + let img = read_all(file); + let (header, data) = gfx::format::qoi::read_qoi_header(&img).unwrap(); + let width = header.width as usize; + let height = header.height as usize; + let mut output = alloc::vec![0u32; width * height]; + gfx::format::qoi::decode_qoi(&header, data, &mut output, width); + + (width, height, output) +} + +fn list_dir(dir: u32) -> Vec { + let mut cookie = 0; + let mut data_backing = [0u64; 8192 / 8]; + let data = cast_slice(&mut data_backing); + + fn cast_slice<'a>(s: &'a mut [u64]) -> &'a mut [u8] { + unsafe { + core::slice::from_raw_parts_mut(s.as_mut_ptr().cast::(), s.len() * size_of::()) + } + } + + #[repr(C)] + #[derive(Copy, Clone, Debug)] + pub struct DirEntry { + pub inode: u64, + pub next_entry_cookie: u64, + pub rec_len: u16, + pub name_len: u16, + pub file_type: u8, + pub name: [u8; 3], + // Name is an arbitrary size array; the record is always padded with + // 0 bytes such that rec_len is a multiple of 8 bytes. + } + + let mut filenames = Vec::new(); + + 'outer: loop { + match ulib::sys::pread(dir, data, cookie) { + Err(e) => { + println!("Error reading dir: {e}"); + ulib::sys::exit(1); + } + Ok(0) => break, + Ok(len) => { + let mut i = 0; + while i < len as usize { + let slice = &data[i..]; + assert!(slice.len() >= size_of::()); + let entry = unsafe { *slice.as_ptr().cast::() }; + + let name_off = core::mem::offset_of!(DirEntry, name); + let name = &slice[name_off..][..entry.name_len as usize]; + + let name = core::str::from_utf8(name).unwrap(); + filenames.push(name.to_owned()); + + i += entry.rec_len as usize; + cookie = entry.next_entry_cookie; + } + if cookie == 0 { + break 'outer; + } + } + } + } + + filenames +} + #[no_mangle] fn main(argc: usize, argv: *const *const u8) { let argv_array = unsafe { core::slice::from_raw_parts(argv, argc) }; @@ -44,26 +117,35 @@ fn main(argc: usize, argv: *const *const u8) { let file = args[1]; - let img_data; - let img_width; - let img_height; + let mut dir_fd = 3; + let mut idx = 0; + let mut files = Vec::new(); + + let mut img_data; + let mut img_width; + let mut img_height; + + let mut last_idx = idx; if file.ends_with("qoi") { + files.push(file.to_owned()); let Ok(file) = ulib::sys::openat(3, file.as_bytes(), 0, 0) else { println!("Error opening file {}", file); ulib::sys::exit(1); }; - - let img = read_all(file); - let (header, data) = gfx::format::qoi::read_qoi_header(&img).unwrap(); - let width = header.width as usize; - let height = header.height as usize; - let mut output = alloc::vec![0u32; width * height]; - gfx::format::qoi::decode_qoi(&header, data, &mut output, width); - - img_width = width; - img_height = height; - img_data = output; + (img_width, img_height, img_data) = load_image(file); + } else if let Ok(dir) = ulib::sys::openat(3, alloc::format!("{file}/").as_bytes(), 0, 0) { + files = list_dir(dir); + files.retain(|f| f.ends_with("qoi")); + files.sort(); + println!("Loaded files: {:?}", files); + dir_fd = dir; + + let Ok(file) = ulib::sys::openat(dir_fd, files[0].as_bytes(), 0, 0) else { + println!("Error opening file {}", file); + ulib::sys::exit(1); + }; + (img_width, img_height, img_data) = load_image(file); } else { println!("Unknown file format, exiting."); ulib::sys::exit(1); @@ -84,11 +166,29 @@ fn main(argc: usize, argv: *const *const u8) { proto::EventKind::INPUT => { use proto::EventData; let data = proto::InputEvent::parse(&ev).expect("TODO"); - if data.kind == proto::InputEvent::KIND_KEY { + if data.kind == proto::InputEvent::KIND_KEY && data.data1 == 1 { match proto::ScanCode(data.data2) { proto::ScanCode::ESCAPE | proto::ScanCode::Q => { break 'outer; } + proto::ScanCode::RIGHT => { + idx = (idx + 1) % files.len(); + } + proto::ScanCode::LEFT => { + idx = (idx + files.len() - 1) % files.len(); + } + _ => (), + } + } else if data.kind == proto::InputEvent::KIND_MOUSE && data.data1 == 2 { + match data.data4 { + 1 => { + // Mouse1 down + idx = (idx + 1) % files.len(); + } + 2 => { + // Mouse2 down + idx = (idx + files.len() - 1) % files.len(); + } _ => (), } } @@ -100,9 +200,27 @@ fn main(argc: usize, argv: *const *const u8) { } } + idx = idx % files.len(); + if idx != last_idx { + let start = unsafe { ulib::sys::sys_get_time_ms() }; + if let Ok(file) = ulib::sys::openat(dir_fd, files[idx].as_bytes(), 0, 0) { + drop(img_data); + (img_width, img_height, img_data) = load_image(file); + let end = unsafe { ulib::sys::sys_get_time_ms() }; + println!("Loading image took {}ms", end - start); + last_idx = idx; + } else { + println!("Error opening file {}", file); + idx = last_idx; + } + } + + let x = width.saturating_sub(img_width) / 2; + let y = height.saturating_sub(img_height) / 2; + let fb = buf.video_mem(); gfx::blit_buffer( - fb, width, height, row_stride, 0, 0, &img_data, img_width, img_height, img_width, + fb, width, height, row_stride, x, y, &img_data, img_width, img_height, img_width, ); buf.client_to_server_queue() @@ -114,8 +232,6 @@ fn main(argc: usize, argv: *const *const u8) { // signal(video)? for sync ulib::sys::sem_down(buf.get_sem_fd(buf.present_sem)).unwrap(); - - unsafe { ulib::sys::sys_sleep_ms(1000) }; } buf.client_to_server_queue()