From a6508d4e67a35dfe1a2c41e85c020c253eb86576 Mon Sep 17 00:00:00 2001 From: Alan Morgan Date: Tue, 28 Jan 2025 18:56:34 -0800 Subject: [PATCH 1/6] Got image parsing working on p8.png files. Need to finish integrating into pico8. And need to expand functionality. --- drive/carts/serial.p8 | 20 ++++++ src/image.rs | 160 +++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 20 ++++++ src/p8util.rs | 4 +- testcarts/test1.p8.png | Bin 0 -> 1780 bytes 6 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 src/image.rs create mode 100644 testcarts/test1.p8.png diff --git a/drive/carts/serial.p8 b/drive/carts/serial.p8 index a4fac84..ee46c94 100644 --- a/drive/carts/serial.p8 +++ b/drive/carts/serial.p8 @@ -88,6 +88,26 @@ function serial_spawn_pico8() serial_writeline('spawn_pico8:') end +-- load an image serial-in +-- image is loaded in the pico-8 4bit image format. +function serial_load_image(filename, buffer_len) + -- buffer_len guaranteed to align to a byte and be up to 1 screen in size. + local buffer_len = mid(1, ceil(buffer_len), 128*128/2) + serial_writeline('load_image:'..ceil(buffer_len)..","..filename) +end + +-- buffer_len: how big the image is +-- step_len: how many bytes to read with each step +-- location: where to write data to (eg: 0x8000) +function serial_read_image(buffer_len, step_len, location) + local offset = 0 + while offset < buffer_len do + serial(stdin, location+offset, step_len) + offset += step_len + yield() + end +end + function serial_spawn_splore() serial_writeline('spawn_splore:') end diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 0000000..5ac830b --- /dev/null +++ b/src/image.rs @@ -0,0 +1,160 @@ +// This function is able to parse a variety of files and returns a byte array that represents a pico-8 readable format. +// Supported formats are: +// - .p8 +// - .p8.png +// - .png: assumes dimensions divisible by 128 (screenshot) + +// load_image:w,h,frames,mode +// w = number between 1 and 128 +// w = number between 1 and 128 +// mode: # In the future there will be different drawing modes that allow you to display the image with scanlines. +// Because it is technically possible to screenshot up to 32 colors on the screen at one time. +// - default: default palette mode. all colors will be converted/mapped to the default palette. +// - custom: custom palette mode, only 1 palette allowed. +// - cust32: outputs screen orientation (horizontal/vertical). then palette mappings for all rows. then each row. +// + +// a 64x64 image can load at 60 fps if on max cpu. 30 fps if on lower cpu. + +// 1 second gif at 60 fps can be loaded in 4 seconds without any compression. though scan lines could be cycled and cached to guarantee max load speed. + +// - for 32 color gifs, up to 12 frames can be loaded a second (on high cpu), 6 frames a second to be safe. +// - frames can in 4 frames on near max cpu (15 fps). 8 frames with half cpu (7.5 fps). +// - palette can change each frame, so each frame is loading a separate image. if loading scanlines, there could be 2 bits before each scanline. 1 bit: palette, 2 bit: same line +// - if switching rotation, not same as previous frame. + +// - gif playback could be sped up by checking scan lines that changed from the previous frame. but do i really want to do that? +// gifs files only support 100fps, 50fps, 33.3fps, 25fps, and slower. + +// frames can be loaded 10 frames can be loaded in 1 se + +use image::{DynamicImage, GenericImageView, ImageReader}; +use lazy_static::lazy_static; +use ndarray::{arr1, arr2, Array1, Array2}; +use std::cmp::Ordering; + +type Pico8Screen = [u8; 8192]; + +lazy_static! { + static ref DEFAULT_PALETTE: Array2 = arr2(&[ + [0, 0, 0], + [29, 43, 83], + [126, 37, 83], + [0, 135, 81], + [171, 82, 54], + [95, 87, 79], + [194, 195, 199], + [255, 241, 232], + [255, 0, 77], + [255, 163, 0], + [255, 236, 39], + [0, 228, 54], + [41, 173, 255], + [131, 118, 156], + [255, 119, 168], + [255, 204, 170] + ]); + static ref EXTENDED_PALETTE: Array2 = arr2(&[ + [41, 24, 20], + [17, 29, 53], + [66, 33, 54], + [18, 83, 89], + [116, 47, 41], + [73, 51, 59], + [162, 136, 121], + [243, 239, 125], + [190, 18, 80], + [255, 108, 36], + [168, 231, 46], + [0, 181, 67], + [6, 90, 181], + [117, 70, 101], + [255, 110, 89], + [255, 157, 129] + ]); +} + +fn rgba_to_pico8(palette: &Array2, img: &DynamicImage, x: u32, y: u32) -> usize { + let pixel: Array1 = arr1(&img.get_pixel(x, y).0); + + let dist = palette + .rows() + .into_iter() + .map(|row| { + row.iter() + .zip(pixel.iter()) + .map(|(&c, &p)| (c as f64 - p as f64).powi(2)) + .sum::() + }) + .collect::>(); + + // Find the index of the minimum distance + let min_index = dist + .iter() + .enumerate() + .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Equal)) + .map(|(idx, _)| idx) + .unwrap(); + + // Return the min index. + // This is a number between 0-15 if using a single palette. + // could be between 0-31 if combining both palettes. + min_index +} + +// coords for where the label image starts. (16, 24) +// dimensions to verify p8.png file: 160 x 205 +pub fn process(filename: &str) -> Pico8Screen { + match process_(filename) { + Err(_) => [0; 8192], + Ok(value) => value, + } +} + +// intermediate function +fn process_(filename: &str) -> anyhow::Result { + let img_path = ImageReader::open(filename)?; + let _img_format = img_path.format(); + let img = img_path.decode()?; + let mut imgdata = [0; 8192]; // this is the default value + + // assume these exact dimensions mean the image is a pico8 cartridge. + if img.width() == 160 && img.height() == 205 { + for y in 0..128 { + for x in 0..64 { + let p1 = rgba_to_pico8(&DEFAULT_PALETTE, &img, 16 + x * 2, y + 24); + let p2 = rgba_to_pico8(&DEFAULT_PALETTE, &img, 16 + x * 2 + 1, y + 24); + imgdata[(y * 64 + x) as usize] = (p1 << 4 | p2) as u8; + } + } + } + + Ok(imgdata) + + // let mut pixels = Vec::new(); + + // if Path::new(filename).extension() == Some(std::ffi::OsStr::new("p8.png")) { + // let img = image::open(filename).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Failed to open image"))?; + // let (width, height) = img.dimensions(); + + // // p8.png files are always 160x205 + // if width == 160 && height == 205 { + // for (_, _, pixel) in img.pixels() { + // pixels.push(pixel[0]); + // } + // } + // } + // Ok(pixels) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_printdata() -> anyhow::Result<()> { + let _cart = process("./testcarts/test1.p8.png"); + assert_eq!(_cart, [17; 8192]); // 17 means 0b00010001 or 0x11, so dark blue (1) in every pixel, which is what this test cart is + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0a47884..272eb22 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,3 +5,4 @@ pub mod db; pub mod exe; pub mod hal; pub mod p8util; +pub mod image; diff --git a/src/main.rs b/src/main.rs index a28cb93..e18219d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,6 +28,7 @@ use picolauncher::{ db, exe::ExeMeta, hal::*, + image, p8util::{self, *}, }; use serde_json::{Map, Value}; @@ -392,6 +393,25 @@ async fn main() { "sys" => { // Get system information like operating system, etc }, + + // PARAMS: filename, img_num, frame_num, bytes_per_frame + // filename: which file to load + // img_num: which image in a gif to load. not used for non-gifs. + // frame_num: which frame this is for loading the image. + // bytes_per_frame: how many bytes you are loading each frame. + "load_image" => { + let mut split = data.splitn(4, ","); + let filename = split.next().unwrap_or_default(); + let img_num = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let frame_num = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let bytes_per_frame = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let image_data = image::process(filename); + let mut in_pipe = open_in_pipe().expect("failed to open pipe"); + in_pipe.write( + &image_data[((frame_num * bytes_per_frame) as usize) + ..(((frame_num + 1) * bytes_per_frame) as usize)], + ); + }, "pushcart" => { // when loading a new cart, can push the current cart and use as breadcrumb cartstack.push(data.into()); diff --git a/src/p8util.rs b/src/p8util.rs index f7887bf..c3e2cf8 100644 --- a/src/p8util.rs +++ b/src/p8util.rs @@ -8,8 +8,8 @@ use std::{ path::Path, }; -use anyhow::{anyhow, Result}; -use image::{GenericImageView, ImageReader, Pixel, Pixels}; +use anyhow::anyhow; +use image::{GenericImageView, ImageReader}; use lazy_static::lazy_static; use ndarray::{arr1, arr2, Array1, Array2}; use pino_deref::{Deref, DerefMut}; diff --git a/testcarts/test1.p8.png b/testcarts/test1.p8.png new file mode 100644 index 0000000000000000000000000000000000000000..f702edc654337d452d5a74f9f132fee6690d09c3 GIT binary patch literal 1780 zcmbtVX;ji#6bBR|HATy1OpQsh#&T4$Y{50j^bOVrr=EDaI5-O4xC9 zMoG!6Oe@hwb3rR>l){t>ffCITTyfO-+jQp4e3%b&`knLMyZ7Due)q#WzkAQ^!TK!I zwa^8DK+F7mz4roVF|fK|I>7E;N-GC}bhv)rp24Z$5spJrU)!oP?vjX8jap0ewDk0K z0YD%ch^BC?Gq$Y0F6(jj5?x(`6*}6mY55<YOlg$1t6zR3%ZKO0e1aZHf*NnE=D|KZH7qtG}^qpeAtXbi`b2d#(=dQf#R zH~86Rj75xI!d@eMZr${Fwd##+0Z$V+kx}F@ugC6~B8%Pdqsy$;Y6m4d?|$zzYcY=9 z#!d+WgLV4pU^oB}7yukTd6~$1`Z30zUW~Lhv?)fqZg(R<4SpoMu;QA2PxbUz1V|ge zR~r9^K)*5IxGk00PMqpX(S~Ga%%9Jb`=Jenim+vADd`&ILB{bon6I?2t2AXy5g6Ch z&njECaI6Fnt({ZoJIsbG0z)aLeL# zp^wcXe|<+u!D2W<=G0~yZqQ5(n@3XHi#8^yCakGC7mN5sVn7uR2?Aai4T_!_&&X(|A6)W?zVu zPb=F~Eu3??)H8UUVOUXg)YqinC7`<-=v$tgBQ>v?v#Sr#c9sP05hSDHDSA4uOc!PK zU%JS`ofBJpn46uVv?>$|L$QqZxw{nBkQ(lZWpzq*W4KWXlFH04=1t}1*+qY#o1RqKi8q_vC-H99 zXPUK_XV7e;d%R2*S`PAY_jj3)nCSSB8$F5$d?2r4bIzeJ0z?;f`NEwoF3~lm%vg#E zJo>r4dGXS2c&QnJ6L6(D(Hol6G|3r(a!o*#;p`v0zyF-H7{Be#JB}vEa#=eaQjj7y>ZXKqyBi YCr1>@NxJ`D6A%OWVX)p+UeWYF0SgWO&j0`b literal 0 HcmV?d00001 From 1deccc2fd266c597f4d7af0011794ecee0510500 Mon Sep 17 00:00:00 2001 From: Alan Morgan Date: Mon, 3 Feb 2025 20:40:56 -0800 Subject: [PATCH 2/6] More progress. Also trying a refactor to use stdin/stdout. Stdin should be done, but not stdout. Refactor is because I think pico8 is behaving weirdly. With sending/receiving data. I expect it to block on serial reads but it doesn't and I expect a serial read to always return the specified amount of data, but it doesn't. --- drive/carts/pexsplore_bbs.p8 | 10 ++- drive/carts/serial.p8 | 44 +++++++----- drive/carts/tween.lua | 5 +- src/hal/linux.rs | 9 ++- src/image.rs | 31 ++++---- src/main.rs | 134 ++++++++++++----------------------- 6 files changed, 103 insertions(+), 130 deletions(-) diff --git a/drive/carts/pexsplore_bbs.p8 b/drive/carts/pexsplore_bbs.p8 index 0d63199..c35db6b 100644 --- a/drive/carts/pexsplore_bbs.p8 +++ b/drive/carts/pexsplore_bbs.p8 @@ -16,7 +16,7 @@ bar_color_1=12 bar_color_2=-4 cart_dir='games' -label_dir='labels' +label_dir='bbs' -- TODO: currently bbs is a symlink because I want to test p8.png loading. This should not be a symlink and instead games should have p8.png files when possible since those have more information. loaded_carts={} -- list of all carts that can be displayed in the menu carts={} -- menu for pexsplore ui @@ -188,10 +188,16 @@ function make_cart_swipe_tween(dir) cart_swipe_tween:register_step_callback(function(pos) cart_x_swipe=pos end) + + cart_swipe_tween:register_step_callback(function(_, frame) + serial_load_image("bbs/"..carts:cur().filename, 0x0000, 128, 128, frame) + end) + cart_swipe_tween:register_finished_callback(function(tween) cart_x_swipe=64-1*dir*128 tween:remove() - load_label(carts:cur(), 0) + + -- load_label(carts:cur(), 0) make_cart_swipe_tween_2(dir) end) cart_swipe_tween:restart() diff --git a/drive/carts/serial.p8 b/drive/carts/serial.p8 index ee46c94..4ded3ea 100644 --- a/drive/carts/serial.p8 +++ b/drive/carts/serial.p8 @@ -88,23 +88,35 @@ function serial_spawn_pico8() serial_writeline('spawn_pico8:') end --- load an image serial-in +-- load an image in parts through serial-in. image is loaded row-by-row. -- image is loaded in the pico-8 4bit image format. -function serial_load_image(filename, buffer_len) - -- buffer_len guaranteed to align to a byte and be up to 1 screen in size. - local buffer_len = mid(1, ceil(buffer_len), 128*128/2) - serial_writeline('load_image:'..ceil(buffer_len)..","..filename) -end +-- return true means the function is done at that frame and doesn't need to be called anymore. +-- load modes: +-- - "top_bot" -- load the image from the top to the bottom. +POSSIBLE_LOAD_MODES = {top_bot=true} +function serial_load_image(filename, location, scale_width, scale_height, frame, mode, bytes_per_frame) + filename = filename..".p8.png" + mode = POSSIBLE_LOAD_MODES[mode] and mode or "top_bot" -- load the image top to bottom. + bytes_per_frame = bytes_per_frame or 1024 -- 1024 bytes is about 50% cpu on 60 fps. + scale_width = mid(1, scale_width, 128)\1 -- scale_width, scaled down images load can faster + scale_height = mid(1, scale_height, 128)\1 + frame = max(1, frame\1) -- which frame this is. used to determine which part of the image to load. + + local buffer_len = ceil(scale_width*scale_height/2) + if bytes_per_frame*(frame-1) >= buffer_len then + return true + end + + serial_writeline('load_image:'..filename..","..scale_width..","..scale_height..","..bytes_per_frame..","..frame..","..mode) + local size = serial(stdin, location+bytes_per_frame*(frame-1), bytes_per_frame) -- TODO: make it read less for the last frame + printh(" size "..size.." "..t()) + local size2 = serial(stdin, 0x0000, 10) + printh(" size2 "..size2.." "..t()) + -- printh(" "..location+bytes_per_frame*(frame-1)) + -- printh(" "..bytes_per_frame) --- buffer_len: how big the image is --- step_len: how many bytes to read with each step --- location: where to write data to (eg: 0x8000) -function serial_read_image(buffer_len, step_len, location) - local offset = 0 - while offset < buffer_len do - serial(stdin, location+offset, step_len) - offset += step_len - yield() + if bytes_per_frame*(frame-1) >= buffer_len then + return true end end @@ -183,7 +195,7 @@ end function serial_writeline(buf) -- TODO check length of buf to avoid overfloe -- TODO not super efficient - printh('output len '..#buf .. ' content ' .. buf) + -- printh('output len '..#buf .. ' content ' .. buf) for i=1,#buf do b = ord(sub(buf, i, i)) -- printh('copy: '..b) diff --git a/drive/carts/tween.lua b/drive/carts/tween.lua index f853304..d4e1ccc 100644 --- a/drive/carts/tween.lua +++ b/drive/carts/tween.lua @@ -491,6 +491,7 @@ function tween_machine:add_tween(instance) start_time = 0, duration = 0, elapsed = 0, + frame = 0, finished = false, --- Callbacks @@ -583,6 +584,7 @@ end function __tween:restart() self:init() self.elapsed = 0 + self.frame = 0 self.finished = false end @@ -604,6 +606,7 @@ function __tween:update() if (self.finished or self.func == nil) return self.elapsed = time() - self.start_time + self.frame += 1 -- frame just increments by 1 each frame. if (self.elapsed > self.duration) self.elapsed = self.duration self.value = self.func( self.elapsed, @@ -614,7 +617,7 @@ function __tween:update() if #self.step_callbacks > 0 then for v in all(self.step_callbacks) do - v(self.value) + v(self.value, self.frame) end end diff --git a/src/hal/linux.rs b/src/hal/linux.rs index 39ce11a..8b53197 100644 --- a/src/hal/linux.rs +++ b/src/hal/linux.rs @@ -1,7 +1,5 @@ use std::{ - fs::{File, OpenOptions}, - path::{Path, PathBuf}, - time::Duration, + fs::{File, OpenOptions}, path::{Path, PathBuf}, process::Stdio, time::Duration }; use anyhow::anyhow; @@ -52,7 +50,7 @@ pub fn open_in_pipe() -> anyhow::Result { } pub fn open_out_pipe() -> anyhow::Result { - create_pipe(&PathBuf::from(IN_PIPE))?; + create_pipe(&PathBuf::from(OUT_PIPE))?; let out_pipe = OpenOptions::new().read(true).open(&*OUT_PIPE)?; @@ -157,7 +155,8 @@ pub fn launch_pico8_binary(bin_names: &Vec, args: Vec<&str>) -> anyhow:: for bin_name in bin_names { let pico8_process = Command::new(bin_name.clone()) .args(args.clone()) - // .stdout(Stdio::piped()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) .spawn(); match pico8_process { diff --git a/src/image.rs b/src/image.rs index 5ac830b..0bce8fb 100644 --- a/src/image.rs +++ b/src/image.rs @@ -32,6 +32,8 @@ use image::{DynamicImage, GenericImageView, ImageReader}; use lazy_static::lazy_static; use ndarray::{arr1, arr2, Array1, Array2}; use std::cmp::Ordering; +use std::env; +use std::path::Path; type Pico8Screen = [u8; 8192]; @@ -104,22 +106,26 @@ fn rgba_to_pico8(palette: &Array2, img: &DynamicImage, x: u32, y: u32) -> us // coords for where the label image starts. (16, 24) // dimensions to verify p8.png file: 160 x 205 -pub fn process(filename: &str) -> Pico8Screen { - match process_(filename) { +pub fn process(filepath: &Path) -> Pico8Screen { + match process_(filepath) { Err(_) => [0; 8192], Ok(value) => value, } } // intermediate function -fn process_(filename: &str) -> anyhow::Result { - let img_path = ImageReader::open(filename)?; +fn process_(filepath: &Path) -> anyhow::Result { + let current_dir = env::current_dir()?; + println!("{}", current_dir.display()); + println!("1 {}", filepath.display()); + let img_path = ImageReader::open(filepath)?; let _img_format = img_path.format(); let img = img_path.decode()?; let mut imgdata = [0; 8192]; // this is the default value // assume these exact dimensions mean the image is a pico8 cartridge. if img.width() == 160 && img.height() == 205 { + println!("Ya Yes"); for y in 0..128 { for x in 0..64 { let p1 = rgba_to_pico8(&DEFAULT_PALETTE, &img, 16 + x * 2, y + 24); @@ -127,24 +133,11 @@ fn process_(filename: &str) -> anyhow::Result { imgdata[(y * 64 + x) as usize] = (p1 << 4 | p2) as u8; } } + } else { + println!("Nope no"); } Ok(imgdata) - - // let mut pixels = Vec::new(); - - // if Path::new(filename).extension() == Some(std::ffi::OsStr::new("p8.png")) { - // let img = image::open(filename).map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Failed to open image"))?; - // let (width, height) = img.dimensions(); - - // // p8.png files are always 160x205 - // if width == 160 && height == 205 { - // for (_, _, pixel) in img.pixels() { - // pixels.push(pixel[0]); - // } - // } - // } - // Ok(pixels) } #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index e18219d..a5ff727 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,6 @@ // TODO maybe switch to async use std::{ - collections::HashMap, - ffi::OsStr, - fs::{create_dir_all, read_dir, read_to_string, File, OpenOptions}, - io::{BufRead, BufReader, Read, Write}, - ops::ControlFlow, - path::{Path, PathBuf}, - ptr, - sync::Arc, - thread, - time::{Duration, Instant}, + collections::HashMap, ffi::OsStr, fs::{create_dir_all, read_dir, read_to_string, File}, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::Arc, thread, time::{Duration, Instant} }; use anyhow::anyhow; @@ -32,7 +23,7 @@ use picolauncher::{ p8util::{self, *}, }; use serde_json::{Map, Value}; -use tokio::{process::Command, runtime::Runtime, sync::Mutex}; +use tokio::{io::AsyncWriteExt, process::{Child, Command}, runtime::Runtime, sync::Mutex}; use crate::db::{schema::CartId, Cart, DB}; @@ -124,17 +115,13 @@ async fn main() { DRIVE_DIR, "-run", &format!("drive/carts/{init_cart}"), - "-i", - "in_pipe", - "-o", - "out_pipe", ], ) .expect("failed to spawn pico8 process"); // need to drop the in_pipe (for some reason) for the pico8 process to start up - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - drop(in_pipe); + // let mut in_pipe = open_in_pipe().expect("failed to open pipe"); + // drop(in_pipe); let mut out_pipe = open_out_pipe().expect("failed to open pipe"); let mut reader = BufReader::new(out_pipe); @@ -321,11 +308,9 @@ async fn main() { exes.push(meta_string); } } - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); let exes_joined = exes.join(","); debug!("exes_joined {exes_joined}"); - writeln!(in_pipe, "{}", exes_joined).expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, exes_joined).await; }, "bbs" => { // Query the bbs @@ -374,9 +359,7 @@ async fn main() { .collect::>() .join(","); - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{}", cartdatas_encoded).expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, cartdatas_encoded).await; }, "download" => { // Download a cart from the bbs @@ -400,17 +383,28 @@ async fn main() { // frame_num: which frame this is for loading the image. // bytes_per_frame: how many bytes you are loading each frame. "load_image" => { - let mut split = data.splitn(4, ","); - let filename = split.next().unwrap_or_default(); - let img_num = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here - let frame_num = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let mut split = data.splitn(6, ","); + // filename..","..scale_width..","..scale_height..","..bytes_per_frame..","..frame..","..mode) + let filename = CART_DIR.join(split.next().unwrap_or_default()); + let scale_width = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let scale_height = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here let bytes_per_frame = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here - let image_data = image::process(filename); - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - in_pipe.write( - &image_data[((frame_num * bytes_per_frame) as usize) - ..(((frame_num + 1) * bytes_per_frame) as usize)], - ); + let frame = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let mode = split.next().unwrap_or_default(); + + if frame*bytes_per_frame < scale_height*scale_width/2 { + let image_data = image::process(&filename.as_path()); + stdin_writeraw( + &mut pico8_process, + &image_data[((frame * bytes_per_frame) as usize) + ..(((frame + 1) * bytes_per_frame) as usize)] + ).await; + } else { + stdin_writeraw( + &mut pico8_process, + &vec![3u8; bytes_per_frame as usize] + ).await; + } }, "pushcart" => { // when loading a new cart, can push the current cart and use as breadcrumb @@ -422,9 +416,7 @@ async fn main() { let topcart = cartstack.last().cloned().unwrap_or_default(); debug!("popcart topcart is {topcart}"); - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{}", topcart).expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, topcart).await; }, "wifi_list" => { // scan for networks @@ -437,9 +429,7 @@ async fn main() { // TODO save this to global state println!("found networks {}", networks.join(",")); - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{}", networks.join(",")).expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, networks.join(",")).await; }, "wifi_connect" => { // Grab password and connect to wifi, returning success or failure info @@ -454,27 +444,19 @@ async fn main() { println!("wifi connection result {res:?}"); let status = impl_wifi_status(&nm); - - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{}", status).expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, status).await; }, "wifi_disconnect" => { let res = impl_wifi_disconnect(&nm); println!("wifi disconnection result {res:?}"); let status = impl_wifi_status(&nm); - - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{}", status).expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, status).await; }, "wifi_status" => { // Get if wifi is connected or not, the current network, and the strength of connection let status = impl_wifi_status(&nm); - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{}", status).expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, status).await; }, "bt_start" => { println!("HELLO"); @@ -493,16 +475,8 @@ async fn main() { }, "bt_status" => { - let mut bt_status_guard = bt_status.lock().await; - - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!( - in_pipe, - "{}", - bt_status_guard.get_status_table(&adapter).await.unwrap() - ) - .expect("failed to write to pipe"); - drop(in_pipe); + let bt_status_guard = bt_status.lock().await; + stdin_writeln(&mut pico8_process, bt_status_guard.get_status_table(&adapter).await.unwrap()).await; }, "set_favorite" => { let mut split = data.splitn(2, ","); @@ -511,10 +485,7 @@ async fn main() { // TODO better error handling db.set_favorite(cart_id, is_favorite).unwrap(); - - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{cart_id},{is_favorite}").expect("failed to write to pipe"); - drop(in_pipe); + stdin_writeln(&mut pico8_process, format!("{},{}", cart_id, is_favorite)).await; }, "list_favorite" => {}, "bt_connect" => {}, @@ -525,18 +496,18 @@ async fn main() { if let Err(ref e) = res { warn!("download_music failed {e:?}"); } - write_to_pico8(format!("{}", res.is_ok())).await; + stdin_writeln(&mut pico8_process, format!("{}", res.is_ok())).await; }, "gyro_read" => { if imu.is_some() { let imu = Arc::clone(&imu.clone().unwrap()); let (pitch, roll) = imu.get_tilt().await; debug!("got imu data {},{}", pitch, roll); - write_to_pico8(format!("{pitch},{roll}")).await; + stdin_writeln(&mut pico8_process, format!("{pitch},{roll}")).await; } else { - write_to_pico8(format!("0,0")).await; + stdin_writeln(&mut pico8_process, format!("0,0")).await; } - write_to_pico8(format!("0,0")).await; + stdin_writeln(&mut pico8_process, format!("0,0")).await; }, "shutdown" => { // shutdown() call in pico8 only escapes to the pico8 shell, so implement special command that kills pico8 process @@ -614,21 +585,6 @@ async fn postprocess_cart( .map_err(|e| anyhow!("failed to convert cart to p8 from file {path:?}: {e:?}"))?; } - // generate label file - let mut label_path = LABEL_DIR.join(filestem); - label_path.set_extension("64.p8"); - if !label_path.exists() { - let label_cart = cart2label(&dest_path) - .map_err(|_| anyhow!("failed to generate label cart from {dest_path:?}"))?; - - let mut label_file = File::create(label_path.clone()) - .map_err(|e| anyhow!("failed to create label file {label_path:?}: {e:?}"))?; - - label_cart - .write(&mut label_file) - .map_err(|e| anyhow!("failed to write label file {label_path:?}: {e:?}"))?; - } - // generate metadata file /* let mut metadata_path = METADATA_DIR.clone().join(filestem); @@ -920,8 +876,12 @@ async fn impl_download_music(db: &mut DB, cart_id: CartId) -> anyhow::Result<()> Ok(()) } -async fn write_to_pico8(msg: String) { - let mut in_pipe = open_in_pipe().expect("failed to open pipe"); - writeln!(in_pipe, "{msg}",).expect("failed to write to pipe"); - drop(in_pipe); +async fn stdin_writeln(pico8_process: &mut Child, msg: String) { + let mut stdin = pico8_process.stdin.take().unwrap(); + stdin.write_all(format!("{}\n", msg).as_bytes()).await.unwrap(); +} + +async fn stdin_writeraw(pico8_process: &mut Child, msg: &[u8]) { + let mut stdin = pico8_process.stdin.take().unwrap(); + stdin.write_all(msg).await.unwrap(); } From 0c845f6b26841cf09b39950d9dd16dccd7d48c71 Mon Sep 17 00:00:00 2001 From: Alan Morgan Date: Mon, 3 Feb 2025 20:54:58 -0800 Subject: [PATCH 3/6] Refactored Rust to work with stdout. Next up is lua. --- src/main.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index a5ff727..ec98318 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ // TODO maybe switch to async use std::{ - collections::HashMap, ffi::OsStr, fs::{create_dir_all, read_dir, read_to_string, File}, io::{BufRead, BufReader}, path::{Path, PathBuf}, sync::Arc, thread, time::{Duration, Instant} + collections::HashMap, ffi::OsStr, fs::{create_dir_all, read_dir, read_to_string, File}, path::{Path, PathBuf}, sync::Arc, thread, time::{Duration, Instant} }; use anyhow::anyhow; @@ -23,7 +23,7 @@ use picolauncher::{ p8util::{self, *}, }; use serde_json::{Map, Value}; -use tokio::{io::AsyncWriteExt, process::{Child, Command}, runtime::Runtime, sync::Mutex}; +use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::{Child, Command}, runtime::Runtime, sync::Mutex}; use crate::db::{schema::CartId, Cart, DB}; @@ -123,8 +123,8 @@ async fn main() { // let mut in_pipe = open_in_pipe().expect("failed to open pipe"); // drop(in_pipe); - let mut out_pipe = open_out_pipe().expect("failed to open pipe"); - let mut reader = BufReader::new(out_pipe); + let pico8_stdout = pico8_process.stdout.take().expect("child did not have a handle to stdout"); + let mut reader = BufReader::new(pico8_stdout).lines(); // TODO don't crash if browser fails to launch - just disable bbs functionality? // spawn browser and create tab for crawling @@ -202,18 +202,9 @@ async fn main() { break; } - let mut line = String::new(); - reader - .read_line(&mut line) - .expect("failed to read line from pipe"); + let mut line = reader.next_line().await.unwrap().unwrap(); // TODO: better error handling. unwrap for await then line. line = line.trim().to_string(); - // TODO this busy loops? - if line.len() == 0 { - continue; - } - //println!("received [{}] {}", line.len(), line); - // spawn process command let mut split = line.splitn(2, ':'); let cmd = split.next().unwrap_or(""); From 9ae9c462c7057029c3d906adffece7609b73a811 Mon Sep 17 00:00:00 2001 From: Alan Morgan Date: Sat, 8 Feb 2025 19:41:48 -0800 Subject: [PATCH 4/6] Got serial bugs ironed out. --- drive/carts/bbs | 1 + drive/carts/pexsplore_bbs.p8 | 3 +- drive/carts/serial.p8 | 55 ++++++++++++------------------------ src/main.rs | 55 ++++++++++++++++++++---------------- 4 files changed, 51 insertions(+), 63 deletions(-) create mode 120000 drive/carts/bbs diff --git a/drive/carts/bbs b/drive/carts/bbs new file mode 120000 index 0000000..1949128 --- /dev/null +++ b/drive/carts/bbs @@ -0,0 +1 @@ +../bbs/carts \ No newline at end of file diff --git a/drive/carts/pexsplore_bbs.p8 b/drive/carts/pexsplore_bbs.p8 index c35db6b..4cd4cb8 100644 --- a/drive/carts/pexsplore_bbs.p8 +++ b/drive/carts/pexsplore_bbs.p8 @@ -75,6 +75,7 @@ function draw_label(x, y, w, slot) sspr(i*64, slot*32 + j, 64, 1, x-w/2, y-w/2+j*2+i) end end + -- sspr(0, 0, 128, 128, x-w/2, y-w/2+j*2+i, 64, 64) end end @@ -511,7 +512,7 @@ function draw_carts_menu() else local str="❎ load more carts" print(str, cart_x_swipe-#str*2, 64, 7) - end + end else draw_cart(cart_x_swipe, 64.5+cart_y_ease+cart_y_bob, 0) local str="❎view" diff --git a/drive/carts/serial.p8 b/drive/carts/serial.p8 index 4ded3ea..5960572 100644 --- a/drive/carts/serial.p8 +++ b/drive/carts/serial.p8 @@ -46,8 +46,8 @@ end -- serial interface with the underlying operating system -stdin=0x806 -stdout=0x807 +stdin=0x804 +stdout=0x805 chan_buf=0x4300 chan_buf_size=0x1000 @@ -108,10 +108,13 @@ function serial_load_image(filename, location, scale_width, scale_height, frame, end serial_writeline('load_image:'..filename..","..scale_width..","..scale_height..","..bytes_per_frame..","..frame..","..mode) + + -- printh("about to read "..bytes_per_frame.." bytes") local size = serial(stdin, location+bytes_per_frame*(frame-1), bytes_per_frame) -- TODO: make it read less for the last frame - printh(" size "..size.." "..t()) - local size2 = serial(stdin, 0x0000, 10) - printh(" size2 "..size2.." "..t()) + -- printh("finished reading "..bytes_per_frame.." bytes") + + -- local size2 = serial(stdin, 0x0000, 10) + -- printh(" size2 "..size2.." "..t()) -- printh(" "..location+bytes_per_frame*(frame-1)) -- printh(" "..bytes_per_frame) @@ -168,45 +171,24 @@ end -- read from input file until a newline is reached -- TODO this can be refactored into a coroutine? function serial_readline() + printh("about to read") local result='' - local got_newline=false while true do -- also use the argument space to receive the result - size = serial(stdin, chan_buf, chan_buf_size) - if (size == 0) then return result end - -- printh('size: ' .. size) - for i=0,size do - b = peek(chan_buf+i) - -- printh('byte: '..b) - if b == 0x0a then - got_newline=true - break - end - result = result..chr(b) - end - end - if not got_newline then - printh('warning: newline was not received') + serial(stdin, chan_buf, 1) + local byte = @chan_buf + if byte == 0x0a then break + else result ..= chr(byte) end end - printh('result '..result) + + printh("finished reading") + return result end +-- Yep a write is just a printh to stdout. function serial_writeline(buf) - -- TODO check length of buf to avoid overfloe - -- TODO not super efficient - -- printh('output len '..#buf .. ' content ' .. buf) - for i=1,#buf do - b = ord(sub(buf, i, i)) - -- printh('copy: '..b) - poke(chan_buf + i - 1, b) - end - -- write a newline character - poke(chan_buf+#buf, ord('\n')) - - -- TODO currently this means that newlines are not allowed in messages - serial(stdout, chan_buf, #buf+1) - flip() + printh(buf) end function serial_fetch_carts() @@ -231,7 +213,6 @@ function os_load(path, breadcrumb, param) if param == nil then param = '' end serial_writeline('pushcart:'..path..','..breadcrumb..','..param) - serial_readline() -- empty response load(path, breadcrumb, param) end diff --git a/src/main.rs b/src/main.rs index ec98318..7dea936 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ use picolauncher::{ p8util::{self, *}, }; use serde_json::{Map, Value}; -use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::{Child, Command}, runtime::Runtime, sync::Mutex}; +use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}, process::{Child, ChildStdin, Command}, runtime::Runtime, sync::Mutex}; use crate::db::{schema::CartId, Cart, DB}; @@ -124,7 +124,9 @@ async fn main() { // drop(in_pipe); let pico8_stdout = pico8_process.stdout.take().expect("child did not have a handle to stdout"); - let mut reader = BufReader::new(pico8_stdout).lines(); + let pico8_stdin = pico8_process.stdin.take().expect("child did not have a handle to stdin"); + let mut pico8_writer = BufWriter::new(pico8_stdin); + let mut pico8_reader = BufReader::new(pico8_stdout).lines(); // TODO don't crash if browser fails to launch - just disable bbs functionality? // spawn browser and create tab for crawling @@ -202,14 +204,15 @@ async fn main() { break; } - let mut line = reader.next_line().await.unwrap().unwrap(); // TODO: better error handling. unwrap for await then line. + debug!("Reading input now"); + let mut line = pico8_reader.next_line().await.unwrap().unwrap(); // TODO: better error handling. unwrap for await then line. line = line.trim().to_string(); + println!("INPUT: {}", line); // spawn process command let mut split = line.splitn(2, ':'); let cmd = split.next().unwrap_or(""); let data = split.next().unwrap_or(""); - debug!("received cmd:{cmd} data:{data}"); match cmd { // TODO disable until we port this to windows and support launching external binaries @@ -301,7 +304,7 @@ async fn main() { } let exes_joined = exes.join(","); debug!("exes_joined {exes_joined}"); - stdin_writeln(&mut pico8_process, exes_joined).await; + stdin_writeln(&mut pico8_writer, exes_joined).await; }, "bbs" => { // Query the bbs @@ -350,7 +353,7 @@ async fn main() { .collect::>() .join(","); - stdin_writeln(&mut pico8_process, cartdatas_encoded).await; + stdin_writeln(&mut pico8_writer, cartdatas_encoded).await; }, "download" => { // Download a cart from the bbs @@ -386,13 +389,13 @@ async fn main() { if frame*bytes_per_frame < scale_height*scale_width/2 { let image_data = image::process(&filename.as_path()); stdin_writeraw( - &mut pico8_process, + &mut pico8_writer, &image_data[((frame * bytes_per_frame) as usize) ..(((frame + 1) * bytes_per_frame) as usize)] ).await; } else { stdin_writeraw( - &mut pico8_process, + &mut pico8_writer, &vec![3u8; bytes_per_frame as usize] ).await; } @@ -407,7 +410,7 @@ async fn main() { let topcart = cartstack.last().cloned().unwrap_or_default(); debug!("popcart topcart is {topcart}"); - stdin_writeln(&mut pico8_process, topcart).await; + stdin_writeln(&mut pico8_writer, topcart).await; }, "wifi_list" => { // scan for networks @@ -420,7 +423,7 @@ async fn main() { // TODO save this to global state println!("found networks {}", networks.join(",")); - stdin_writeln(&mut pico8_process, networks.join(",")).await; + stdin_writeln(&mut pico8_writer, networks.join(",")).await; }, "wifi_connect" => { // Grab password and connect to wifi, returning success or failure info @@ -435,19 +438,19 @@ async fn main() { println!("wifi connection result {res:?}"); let status = impl_wifi_status(&nm); - stdin_writeln(&mut pico8_process, status).await; + stdin_writeln(&mut pico8_writer, status).await; }, "wifi_disconnect" => { let res = impl_wifi_disconnect(&nm); println!("wifi disconnection result {res:?}"); let status = impl_wifi_status(&nm); - stdin_writeln(&mut pico8_process, status).await; + stdin_writeln(&mut pico8_writer, status).await; }, "wifi_status" => { // Get if wifi is connected or not, the current network, and the strength of connection let status = impl_wifi_status(&nm); - stdin_writeln(&mut pico8_process, status).await; + stdin_writeln(&mut pico8_writer, status).await; }, "bt_start" => { println!("HELLO"); @@ -467,7 +470,7 @@ async fn main() { "bt_status" => { let bt_status_guard = bt_status.lock().await; - stdin_writeln(&mut pico8_process, bt_status_guard.get_status_table(&adapter).await.unwrap()).await; + stdin_writeln(&mut pico8_writer, bt_status_guard.get_status_table(&adapter).await.unwrap()).await; }, "set_favorite" => { let mut split = data.splitn(2, ","); @@ -476,7 +479,7 @@ async fn main() { // TODO better error handling db.set_favorite(cart_id, is_favorite).unwrap(); - stdin_writeln(&mut pico8_process, format!("{},{}", cart_id, is_favorite)).await; + stdin_writeln(&mut pico8_writer, format!("{},{}", cart_id, is_favorite)).await; }, "list_favorite" => {}, "bt_connect" => {}, @@ -487,18 +490,18 @@ async fn main() { if let Err(ref e) = res { warn!("download_music failed {e:?}"); } - stdin_writeln(&mut pico8_process, format!("{}", res.is_ok())).await; + stdin_writeln(&mut pico8_writer, format!("{}", res.is_ok())).await; }, "gyro_read" => { if imu.is_some() { let imu = Arc::clone(&imu.clone().unwrap()); let (pitch, roll) = imu.get_tilt().await; debug!("got imu data {},{}", pitch, roll); - stdin_writeln(&mut pico8_process, format!("{pitch},{roll}")).await; + stdin_writeln(&mut pico8_writer, format!("{pitch},{roll}")).await; } else { - stdin_writeln(&mut pico8_process, format!("0,0")).await; + stdin_writeln(&mut pico8_writer, format!("0,0")).await; } - stdin_writeln(&mut pico8_process, format!("0,0")).await; + stdin_writeln(&mut pico8_writer, format!("0,0")).await; }, "shutdown" => { // shutdown() call in pico8 only escapes to the pico8 shell, so implement special command that kills pico8 process @@ -867,12 +870,14 @@ async fn impl_download_music(db: &mut DB, cart_id: CartId) -> anyhow::Result<()> Ok(()) } -async fn stdin_writeln(pico8_process: &mut Child, msg: String) { - let mut stdin = pico8_process.stdin.take().unwrap(); - stdin.write_all(format!("{}\n", msg).as_bytes()).await.unwrap(); +async fn stdin_writeln(pico8_writer: &mut BufWriter, msg: String) { + let msg = format!("{}\n", msg); + debug!("writing line: {}", &msg); //&msg[..msg.len().min(10)]); + let _ = pico8_writer.write_all(msg.as_bytes()).await; + let _ = pico8_writer.flush().await; } -async fn stdin_writeraw(pico8_process: &mut Child, msg: &[u8]) { - let mut stdin = pico8_process.stdin.take().unwrap(); - stdin.write_all(msg).await.unwrap(); +async fn stdin_writeraw(pico8_writer: &mut BufWriter, msg: &[u8]) { + let _ = pico8_writer.write_all(msg).await; + let _ = pico8_writer.flush().await; } From 31b457b9fcd137a9337d17c0f3c28cd01a5d23fa Mon Sep 17 00:00:00 2001 From: Alan Morgan Date: Mon, 10 Feb 2025 17:50:52 -0800 Subject: [PATCH 5/6] Oh boy. Lots of stuff. Got a basic implementation that streams images from rust to pico8 working. --- Cargo.lock | 300 +++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + drive/carts/pexsplore_bbs.p8 | 4 +- drive/carts/serial.p8 | 13 +- drive/carts/tween.lua | 2 +- drive/config.txt | 2 +- src/image.rs | 49 +++--- src/main.rs | 3 +- 8 files changed, 330 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11e8a19..7287e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -150,6 +150,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "auto_generate_cdp" version = "0.4.4" @@ -429,6 +440,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils 0.8.20", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -895,6 +915,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "exr" version = "1.72.0" @@ -1141,6 +1182,19 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generator" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1420,7 +1474,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -1718,6 +1772,19 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if 1.0.0", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "loop9" version = "0.1.5" @@ -1762,6 +1829,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "matrixmultiply" version = "0.3.9" @@ -1871,6 +1947,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "moka" +version = "0.12.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926" +dependencies = [ + "async-lock", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils 0.8.20", + "event-listener", + "futures-util", + "loom", + "parking_lot", + "portable-atomic", + "rustc_version", + "smallvec", + "tagptr", + "thiserror", + "uuid", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -2028,6 +2126,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -2172,6 +2280,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2315,6 +2435,7 @@ dependencies = [ "lazy_static", "linux-embedded-hal", "log", + "moka", "ndarray", "network-manager", "nix 0.29.0", @@ -2606,8 +2727,17 @@ checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -2618,9 +2748,15 @@ checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.4", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.4" @@ -2711,6 +2847,15 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.34" @@ -2795,6 +2940,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -2859,6 +3010,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "semver" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" + [[package]] name = "serde" version = "1.0.209" @@ -2950,6 +3107,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3190,6 +3356,12 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -3240,6 +3412,16 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + [[package]] name = "tiff" version = "0.9.1" @@ -3430,9 +3612,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", @@ -3440,11 +3622,41 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ + "log", "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -3586,6 +3798,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -3764,6 +3982,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -3773,6 +4001,60 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index f34eb29..01318dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ headless_chrome = "1" urlencoding = "2.1" scraper = "0.19" futures = "0.3" +moka = { version = "0.12.10", features = ["future"] } [target.'cfg(target_os = "linux")'.dependencies] nix = { version = "0.29", features = ["fs", "process", "signal"] } diff --git a/drive/carts/pexsplore_bbs.p8 b/drive/carts/pexsplore_bbs.p8 index 4cd4cb8..fa7962f 100644 --- a/drive/carts/pexsplore_bbs.p8 +++ b/drive/carts/pexsplore_bbs.p8 @@ -191,7 +191,7 @@ function make_cart_swipe_tween(dir) end) cart_swipe_tween:register_step_callback(function(_, frame) - serial_load_image("bbs/"..carts:cur().filename, 0x0000, 128, 128, frame) + serial_load_image("bbs/"..carts:cur().filename, 0x0000, 64, 64, frame) end) cart_swipe_tween:register_finished_callback(function(tween) @@ -467,7 +467,7 @@ function build_new_cart_menu(resp) item.menuitem = menuitem.cart add(new_menuitems, item) end - + -- TODO would like to disable this option for local categories, but also need to make sure we don't have an empty menu add(new_menuitems, {menuitem=menuitem.load}) diff --git a/drive/carts/serial.p8 b/drive/carts/serial.p8 index 5960572..4e6b7b8 100644 --- a/drive/carts/serial.p8 +++ b/drive/carts/serial.p8 @@ -100,7 +100,7 @@ function serial_load_image(filename, location, scale_width, scale_height, frame, bytes_per_frame = bytes_per_frame or 1024 -- 1024 bytes is about 50% cpu on 60 fps. scale_width = mid(1, scale_width, 128)\1 -- scale_width, scaled down images load can faster scale_height = mid(1, scale_height, 128)\1 - frame = max(1, frame\1) -- which frame this is. used to determine which part of the image to load. + frame = max(0, frame\1) -- which frame this is. used to determine which part of the image to load. local buffer_len = ceil(scale_width*scale_height/2) if bytes_per_frame*(frame-1) >= buffer_len then @@ -109,14 +109,9 @@ function serial_load_image(filename, location, scale_width, scale_height, frame, serial_writeline('load_image:'..filename..","..scale_width..","..scale_height..","..bytes_per_frame..","..frame..","..mode) - -- printh("about to read "..bytes_per_frame.." bytes") - local size = serial(stdin, location+bytes_per_frame*(frame-1), bytes_per_frame) -- TODO: make it read less for the last frame - -- printh("finished reading "..bytes_per_frame.." bytes") - - -- local size2 = serial(stdin, 0x0000, 10) - -- printh(" size2 "..size2.." "..t()) - -- printh(" "..location+bytes_per_frame*(frame-1)) - -- printh(" "..bytes_per_frame) + printh("about to read "..bytes_per_frame.." bytes") + local size = serial(stdin, location+bytes_per_frame*frame, bytes_per_frame) -- TODO: make it read less for the last frame + printh("finished reading "..bytes_per_frame.." bytes size "..size.." | "..(location+bytes_per_frame*frame)) if bytes_per_frame*(frame-1) >= buffer_len then return true diff --git a/drive/carts/tween.lua b/drive/carts/tween.lua index d4e1ccc..c56a158 100644 --- a/drive/carts/tween.lua +++ b/drive/carts/tween.lua @@ -617,7 +617,7 @@ function __tween:update() if #self.step_callbacks > 0 then for v in all(self.step_callbacks) do - v(self.value, self.frame) + v(self.value, self.frame-1) end end diff --git a/drive/config.txt b/drive/config.txt index 2a11067..7b707ef 100644 --- a/drive/config.txt +++ b/drive/config.txt @@ -30,7 +30,7 @@ foreground_sleep_ms 2 // number of milliseconds to sleep each frame. Try 10 to c background_sleep_ms 10 // number of milliseconds to sleep each frame when running in the background -sessions 700 // number of times program has been run +sessions 712 // number of times program has been run // (scancode) hold this key down and left-click to simulate right-click rmb_key 0 // 0 for none 226 for LALT diff --git a/src/image.rs b/src/image.rs index 0bce8fb..43ae650 100644 --- a/src/image.rs +++ b/src/image.rs @@ -30,13 +30,12 @@ use image::{DynamicImage, GenericImageView, ImageReader}; use lazy_static::lazy_static; +use log::debug; use ndarray::{arr1, arr2, Array1, Array2}; use std::cmp::Ordering; use std::env; use std::path::Path; -type Pico8Screen = [u8; 8192]; - lazy_static! { static ref DEFAULT_PALETTE: Array2 = arr2(&[ [0, 0, 0], @@ -106,35 +105,36 @@ fn rgba_to_pico8(palette: &Array2, img: &DynamicImage, x: u32, y: u32) -> us // coords for where the label image starts. (16, 24) // dimensions to verify p8.png file: 160 x 205 -pub fn process(filepath: &Path) -> Pico8Screen { - match process_(filepath) { - Err(_) => [0; 8192], +pub fn process(filepath: &Path, width: u32, height: u32) -> Vec { + let width = width + width%2; // Width is changed because each row must be even since there are 2 pixels per row. + match process_(filepath, width, height) { + Err(_) => vec![0; ((width/2)*height) as usize], Ok(value) => value, } } -// intermediate function -fn process_(filepath: &Path) -> anyhow::Result { - let current_dir = env::current_dir()?; - println!("{}", current_dir.display()); - println!("1 {}", filepath.display()); +// intermediate function. width must be even +fn process_(filepath: &Path, width: u32, height: u32) -> anyhow::Result> { let img_path = ImageReader::open(filepath)?; let _img_format = img_path.format(); let img = img_path.decode()?; - let mut imgdata = [0; 8192]; // this is the default value + let mut imgdata = vec![0; ((width/2)*height) as usize]; // this is the default value // assume these exact dimensions mean the image is a pico8 cartridge. if img.width() == 160 && img.height() == 205 { - println!("Ya Yes"); - for y in 0..128 { - for x in 0..64 { - let p1 = rgba_to_pico8(&DEFAULT_PALETTE, &img, 16 + x * 2, y + 24); - let p2 = rgba_to_pico8(&DEFAULT_PALETTE, &img, 16 + x * 2 + 1, y + 24); - imgdata[(y * 64 + x) as usize] = (p1 << 4 | p2) as u8; + let halfwidth = width/2; + for y in 0..height { + let img_y = (128.0/(height as f64)*(y as f64)) as u32; + for x in 0..halfwidth { + let img_x1 = (128.0/(width as f64)*((x*2+0) as f64)) as u32; + let img_x2 = (128.0/(width as f64)*((x*2+1) as f64)) as u32; + // println!("imgy is {} | x is {} | img_x1 is {} | img_x2 is {}", img_y, x, img_x1, img_x2); + + let p1 = rgba_to_pico8(&DEFAULT_PALETTE, &img, 16+img_x1, img_y+24); + let p2 = rgba_to_pico8(&DEFAULT_PALETTE, &img, 16+img_x2, img_y+24); + imgdata[(y * halfwidth + x) as usize] = (p2 << 4 | p1) as u8; } } - } else { - println!("Nope no"); } Ok(imgdata) @@ -142,12 +142,21 @@ fn process_(filepath: &Path) -> anyhow::Result { #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; #[test] fn test_printdata() -> anyhow::Result<()> { - let _cart = process("./testcarts/test1.p8.png"); + let _cart = process(PathBuf::from("./testcarts/test1.p8.png").as_path(), 128, 128); assert_eq!(_cart, [17; 8192]); // 17 means 0b00010001 or 0x11, so dark blue (1) in every pixel, which is what this test cart is Ok(()) } + + #[test] + fn test_printdata_64() -> anyhow::Result<()> { + let _cart = process(PathBuf::from("./testcarts/test1.p8.png").as_path(), 64, 64); + assert_eq!(_cart, [17; 32*64]); // 17 means 0b00010001 or 0x11, so dark blue (1) in every pixel, which is what this test cart is + Ok(()) + } } diff --git a/src/main.rs b/src/main.rs index 7dea936..abec4de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -204,7 +204,6 @@ async fn main() { break; } - debug!("Reading input now"); let mut line = pico8_reader.next_line().await.unwrap().unwrap(); // TODO: better error handling. unwrap for await then line. line = line.trim().to_string(); println!("INPUT: {}", line); @@ -387,7 +386,7 @@ async fn main() { let mode = split.next().unwrap_or_default(); if frame*bytes_per_frame < scale_height*scale_width/2 { - let image_data = image::process(&filename.as_path()); + let image_data = image::process(&filename.as_path(), scale_width, scale_height); stdin_writeraw( &mut pico8_writer, &image_data[((frame * bytes_per_frame) as usize) From 8620588e97e84d9e559fe6c9722e5c3b333a7bce Mon Sep 17 00:00:00 2001 From: Alan Morgan Date: Tue, 18 Feb 2025 17:47:45 -0800 Subject: [PATCH 6/6] Finally starting implementation since the protocol is specccccced out. --- src/image.rs | 66 ++++++++++++++++++++++++++++++++++---------- src/main.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 17 deletions(-) diff --git a/src/image.rs b/src/image.rs index 43ae650..2e7f5dc 100644 --- a/src/image.rs +++ b/src/image.rs @@ -28,10 +28,10 @@ // frames can be loaded 10 frames can be loaded in 1 se -use image::{DynamicImage, GenericImageView, ImageReader}; +use image::{DynamicImage, RgbaImage}; use lazy_static::lazy_static; -use log::debug; use ndarray::{arr1, arr2, Array1, Array2}; +use tokio::{fs, task}; use std::cmp::Ordering; use std::env; use std::path::Path; @@ -75,7 +75,22 @@ lazy_static! { ]); } -fn rgba_to_pico8(palette: &Array2, img: &DynamicImage, x: u32, y: u32) -> usize { +type P8Screen = [u8; 8192]; + +#[derive(PartialEq, Eq, Debug)] +pub struct ImageCacheData { + data: P8Screen, + state: ImageCacheState, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum ImageCacheState { + NotCached, + Caching, + Cached, +} + +fn rgba_to_pico8(palette: &Array2, img: &RgbaImage, x: u32, y: u32) -> usize { let pixel: Array1 = arr1(&img.get_pixel(x, y).0); let dist = palette @@ -105,20 +120,37 @@ fn rgba_to_pico8(palette: &Array2, img: &DynamicImage, x: u32, y: u32) -> us // coords for where the label image starts. (16, 24) // dimensions to verify p8.png file: 160 x 205 -pub fn process(filepath: &Path, width: u32, height: u32) -> Vec { - let width = width + width%2; // Width is changed because each row must be even since there are 2 pixels per row. - match process_(filepath, width, height) { - Err(_) => vec![0; ((width/2)*height) as usize], - Ok(value) => value, +pub async fn process(filepath: &Path) -> ImageCacheData { + match process_(filepath).await { + Err(_) => ImageCacheData { + data: [0; 8192], + state: ImageCacheState::Cached, + }, + Ok(value) => ImageCacheData { + data: value, + state: ImageCacheState::Cached, + }, } } +// TODO: this should take a Cursor? and do guessed format from that. on io error, an image that is black should be saved into the cache. + // intermediate function. width must be even -fn process_(filepath: &Path, width: u32, height: u32) -> anyhow::Result> { - let img_path = ImageReader::open(filepath)?; - let _img_format = img_path.format(); - let img = img_path.decode()?; - let mut imgdata = vec![0; ((width/2)*height) as usize]; // this is the default value +async fn process_(filepath: &Path) -> anyhow::Result { + let width = 128; + let height = 128; + + let image_bytes = fs::read(filepath).await?; + + let raw_image: DynamicImage = task::spawn_blocking(move || { + image::load_from_memory(&image_bytes) + }) + .await??; // TODO error handling. + + // Optionally, convert the image to a specific color format, + // such as RGBA8 (each pixel has 4 u8 values) + let img = raw_image.to_rgba8(); + let mut imgdata = [0; 8192 as usize]; // this is the default value // assume these exact dimensions mean the image is a pico8 cartridge. if img.width() == 160 && img.height() == 205 { @@ -148,8 +180,11 @@ mod tests { #[test] fn test_printdata() -> anyhow::Result<()> { - let _cart = process(PathBuf::from("./testcarts/test1.p8.png").as_path(), 128, 128); - assert_eq!(_cart, [17; 8192]); // 17 means 0b00010001 or 0x11, so dark blue (1) in every pixel, which is what this test cart is + let _cart = process(PathBuf::from("./testcarts/test1.p8.png").as_path()).await?; + assert_eq!(_cart, ImageCacheData { + state: ImageCacheState::Cached, + data: [17; 8192] + }); // 17 means 0b00010001 or 0x11, so dark blue (1) in every pixel, which is what this test cart is Ok(()) } @@ -160,3 +195,4 @@ mod tests { Ok(()) } } + diff --git a/src/main.rs b/src/main.rs index abec4de..dd07337 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use std::{ collections::HashMap, ffi::OsStr, fs::{create_dir_all, read_dir, read_to_string, File}, path::{Path, PathBuf}, sync::Arc, thread, time::{Duration, Instant} }; +use moka::future::Cache; use anyhow::anyhow; use futures::future::join_all; use headless_chrome::{Browser, LaunchOptions, Tab}; @@ -39,6 +40,7 @@ fn create_dirs() -> anyhow::Result<()> { create_dir_all(SCREENSHOT_PATH)?; Ok(()) } + #[tokio::main] async fn main() { // set up logger @@ -53,6 +55,9 @@ async fn main() { screenshot_watcher(); }); + // A cache where key=file_string, value=128x128 image (128*128/2 = 8192 bytes). + let pico8_image_cache: Cache> = Cache::new(1_000); // 1000 is arbitrary and probably good enough. + // set up dbus connection and network manager // TODO linux specific currently // start network manager if not started @@ -370,12 +375,82 @@ async fn main() { // Get system information like operating system, etc }, + "image_cache" => { // the total image cache can hold 128 frames/images. This equates to 1024KB of image data in memory at any point in time. + // image_cache:filename,framestart,framelen // start an async thread to cache an image. if the image is a gif/video, framelen frames from framestart are loaded. + // if the image is already cached, nothing happens. + // resets the frames count also with each call. + // returns: empty + let mut split = data.splitn(3, ","); + let filename = CART_DIR.join(split.next().unwrap_or_default()); + let frame_start = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let frame_len = split.next().unwrap().parse::().unwrap(); // TODO better error handlng here + let imcache = pico8_image_cache.clone(); + tokio::spawn(async move { + // // Insert 64 entries. (NUM_KEYS_PER_TASK = 64) + // for key in start..end { + // // insert() is an async method, so await it. + // my_cache.insert(key, value(key)).await; + // // get() returns Option, a clone of the stored value. + // assert_eq!(my_cache.get(&key).await, Some(value(key))); + // } + + // // Invalidate every 4 element of the inserted entries. + // for key in (start..end).step_by(4) { + // // invalidate() is an async method, so await it. + // my_cache.invalidate(&key).await; + // } + }); + }, + + "image_checkcache" => { // :filename,frame // check if the frame of a file is cached or not. + // returns: 0=image is not cached. 1=image is caching. 2=image is cached. + }, + + "image_frames" => { // :filename // How many frames are in the image. 1 for png files. >1 for images/vids. returns 1 by default if the image is not cached. + // returns: 2 bytes: framecount. 1-16384 (14 bits, so there are 2 extra bits...) + }, + + // TODO: implement a frame info for more complex images. + // "image_frameinfo" => { + // // image_info:filename,frame // Gets image palettes/scanline orientation for the image, or frame of a video/gif. + // // returns: 1 byte: 1bit vertical/horizontal scan lines. 1 bit gradient or scanline. 4 bits gradient color. then 16 bytes for pal1. then 16 bytes for pal2. + // // and need to represent gradient... + // // then 16 bytes specifying the palette for each line, 1 bit per scan line + // }, + + // Caches an image in the background. + "cache_image" => { + let mut split = data.splitn(6, ","); + let filename = CART_DIR.join(split.next().unwrap_or_default()); + tokio::spawn(async move { + let image_data = image::process(&filename.as_path(), scale_width, scale_height); + + // Insert 64 entries. (NUM_KEYS_PER_TASK = 64) + for key in start..end { + // insert() is an async method, so await it. + my_cache.insert(key, value(key)).await; + // get() returns Option, a clone of the stored value. + assert_eq!(my_cache.get(&key).await, Some(value(key))); + } + + // Invalidate every 4 element of the inserted entries. + for key in (start..end).step_by(4) { + // invalidate() is an async method, so await it. + my_cache.invalidate(&key).await; + } + }); + + + + }, + + // will return a black stream if the image is not cached. // PARAMS: filename, img_num, frame_num, bytes_per_frame // filename: which file to load // img_num: which image in a gif to load. not used for non-gifs. // frame_num: which frame this is for loading the image. // bytes_per_frame: how many bytes you are loading each frame. - "load_image" => { + "image_load" => { let mut split = data.splitn(6, ","); // filename..","..scale_width..","..scale_height..","..bytes_per_frame..","..frame..","..mode) let filename = CART_DIR.join(split.next().unwrap_or_default()); @@ -386,7 +461,6 @@ async fn main() { let mode = split.next().unwrap_or_default(); if frame*bytes_per_frame < scale_height*scale_width/2 { - let image_data = image::process(&filename.as_path(), scale_width, scale_height); stdin_writeraw( &mut pico8_writer, &image_data[((frame * bytes_per_frame) as usize)