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/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 0d63199..fa7962f 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 @@ -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 @@ -188,10 +189,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, 64, 64, 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() @@ -460,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}) @@ -505,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 a4fac84..4e6b7b8 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 @@ -88,6 +88,36 @@ function serial_spawn_pico8() serial_writeline('spawn_pico8:') end +-- load an image in parts through serial-in. image is loaded row-by-row. +-- image is loaded in the pico-8 4bit image format. +-- 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(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 + return true + 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, 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 + end +end + function serial_spawn_splore() serial_writeline('spawn_splore:') end @@ -136,45 +166,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 + serial(stdin, chan_buf, 1) + local byte = @chan_buf + if byte == 0x0a then break + else result ..= chr(byte) end end - if not got_newline then - printh('warning: newline was not received') - 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() @@ -199,7 +208,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/drive/carts/tween.lua b/drive/carts/tween.lua index f853304..c56a158 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-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/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 new file mode 100644 index 0000000..2e7f5dc --- /dev/null +++ b/src/image.rs @@ -0,0 +1,198 @@ +// 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, RgbaImage}; +use lazy_static::lazy_static; +use ndarray::{arr1, arr2, Array1, Array2}; +use tokio::{fs, task}; +use std::cmp::Ordering; +use std::env; +use std::path::Path; + +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] + ]); +} + +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 + .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 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 +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 { + 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; + } + } + } + + Ok(imgdata) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + #[test] + fn test_printdata() -> anyhow::Result<()> { + 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(()) + } + + #[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/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..dd07337 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,9 @@ // 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}, 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}; @@ -28,10 +20,11 @@ use picolauncher::{ db, exe::ExeMeta, hal::*, + image, p8util::{self, *}, }; use serde_json::{Map, Value}; -use tokio::{process::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}; @@ -47,6 +40,7 @@ fn create_dirs() -> anyhow::Result<()> { create_dir_all(SCREENSHOT_PATH)?; Ok(()) } + #[tokio::main] async fn main() { // set up logger @@ -61,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 @@ -123,20 +120,18 @@ 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); + let pico8_stdout = pico8_process.stdout.take().expect("child did not have a handle to stdout"); + 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 @@ -214,23 +209,14 @@ async fn main() { break; } - let mut line = String::new(); - reader - .read_line(&mut line) - .expect("failed to read line from pipe"); + let mut line = pico8_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); + 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 @@ -320,11 +306,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_writer, exes_joined).await; }, "bbs" => { // Query the bbs @@ -373,9 +357,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_writer, cartdatas_encoded).await; }, "download" => { // Download a cart from the bbs @@ -392,6 +374,105 @@ async fn main() { "sys" => { // 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. + "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()); + 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 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 { + stdin_writeraw( + &mut pico8_writer, + &image_data[((frame * bytes_per_frame) as usize) + ..(((frame + 1) * bytes_per_frame) as usize)] + ).await; + } else { + stdin_writeraw( + &mut pico8_writer, + &vec![3u8; bytes_per_frame as usize] + ).await; + } + }, "pushcart" => { // when loading a new cart, can push the current cart and use as breadcrumb cartstack.push(data.into()); @@ -402,9 +483,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_writer, topcart).await; }, "wifi_list" => { // scan for networks @@ -417,9 +496,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_writer, networks.join(",")).await; }, "wifi_connect" => { // Grab password and connect to wifi, returning success or failure info @@ -434,27 +511,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_writer, 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_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); - 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_writer, status).await; }, "bt_start" => { println!("HELLO"); @@ -473,16 +542,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_writer, bt_status_guard.get_status_table(&adapter).await.unwrap()).await; }, "set_favorite" => { let mut split = data.splitn(2, ","); @@ -491,10 +552,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_writer, format!("{},{}", cart_id, is_favorite)).await; }, "list_favorite" => {}, "bt_connect" => {}, @@ -505,18 +563,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_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); - write_to_pico8(format!("{pitch},{roll}")).await; + stdin_writeln(&mut pico8_writer, format!("{pitch},{roll}")).await; } else { - write_to_pico8(format!("0,0")).await; + stdin_writeln(&mut pico8_writer, format!("0,0")).await; } - write_to_pico8(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 @@ -594,21 +652,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); @@ -900,8 +943,14 @@ 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_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_writer: &mut BufWriter, msg: &[u8]) { + let _ = pico8_writer.write_all(msg).await; + let _ = pico8_writer.flush().await; } 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 0000000..f702edc Binary files /dev/null and b/testcarts/test1.p8.png differ