From c23e4cdc2b06b6f8581540772c27ab856c797c1f Mon Sep 17 00:00:00 2001 From: gram Date: Sun, 25 Jan 2026 17:00:07 +0100 Subject: [PATCH 01/23] fix formatting for time over 4s --- src/commands/monitor.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commands/monitor.rs b/src/commands/monitor.rs index 802821c..ff8ba8c 100644 --- a/src/commands/monitor.rs +++ b/src/commands/monitor.rs @@ -296,11 +296,18 @@ fn render_log(log: &str) -> anyhow::Result<()> { } fn format_ns(ns: u32) -> String { - if ns > 10_000_000 { - return format!("{:>4} ms", ns / 1_000_000); + const NS: u32 = 1; + const US: u32 = 1000 * NS; + const MS: u32 = 1000 * US; + + if ns == u32::MAX { + return "4+ s".to_string(); + } + if ns > 10 * MS { + return format!("{:>4} ms", ns / MS); } - if ns > 10_000 { - return format!("{:>4} μs", ns / 1_000); + if ns > 10 * US { + return format!("{:>4} μs", ns / US); } format!("{ns:>4} ns") } From e0ea4a6cc8de00cbe8d811613883a36cdc22aaf6 Mon Sep 17 00:00:00 2001 From: Oliver Scherer Date: Mon, 26 Jan 2026 21:35:07 +0100 Subject: [PATCH 02/23] Fix min/max flip in board limits --- src/commands/build.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index dc413c8..13ec3bb 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -342,8 +342,8 @@ fn write_boards(config: &Config) -> anyhow::Result<()> { }; let board = firefly_types::Board { position: board.position.unwrap_or(id), - min: board.max.unwrap_or(i16::MIN), - max: board.min.unwrap_or(i16::MAX), + min: board.min.unwrap_or(i16::MIN), + max: board.max.unwrap_or(i16::MAX), time: board.time, decimals: board.decimals, name: &board.name, From 93ab26cdb67b85eb26c245fd3b2c0e9484247c89 Mon Sep 17 00:00:00 2001 From: gram Date: Thu, 29 Jan 2026 15:16:40 +0100 Subject: [PATCH 03/23] Drop name management --- src/args.rs | 19 ------------------ src/cli.rs | 5 ----- src/commands/mod.rs | 2 -- src/commands/name.rs | 47 -------------------------------------------- 4 files changed, 73 deletions(-) delete mode 100644 src/commands/name.rs diff --git a/src/args.rs b/src/args.rs index ca9f734..1fb8e99 100644 --- a/src/args.rs +++ b/src/args.rs @@ -72,30 +72,11 @@ pub enum Commands { #[clap(alias("shot"), alias("screenshot"), alias("screenshots"))] Shots(ShotsCommands), - /// Set, get, and generate device name. - #[command(subcommand)] - Name(NameCommands), - /// Interact with catalog.fireflyzero.com. #[command(subcommand)] Catalog(CatalogCommands), } -#[derive(Subcommand, Debug)] -pub enum NameCommands { - /// Show the current device name. - #[clap(alias("show"), alias("echo"))] - Get, - - /// Set a new device name. - #[clap(alias("change"))] - Set(NameSetArgs), - - /// Set a new device name. - #[clap(alias("gen"), alias("new"))] - Generate, -} - #[derive(Subcommand, Debug)] pub enum CatalogCommands { /// List all games available in the catalog. diff --git a/src/cli.rs b/src/cli.rs index d587707..4d112c3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,11 +22,6 @@ pub fn run_command(vfs: PathBuf, command: &Commands) -> anyhow::Result<()> { CatalogCommands::List(args) => cmd_catalog_list(args), CatalogCommands::Show(args) => cmd_catalog_show(args), }, - Name(command) => match command { - NameCommands::Get => cmd_name_get(&vfs), - NameCommands::Set(args) => cmd_name_set(&vfs, args), - NameCommands::Generate => cmd_name_generate(&vfs), - }, Runtime(root_args) => match &root_args.command { RuntimeCommands::Launch(args) => cmd_launch(root_args, args), RuntimeCommands::Restart => cmd_restart(root_args), diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ece4d12..2bffbfa 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -9,7 +9,6 @@ mod import; mod inspect; mod logs; mod monitor; -mod name; mod new; mod postinstall; mod repl; @@ -29,7 +28,6 @@ pub use import::cmd_import; pub use inspect::cmd_inspect; pub use logs::cmd_logs; pub use monitor::cmd_monitor; -pub use name::{cmd_name_generate, cmd_name_get, cmd_name_set}; pub use new::cmd_new; pub use postinstall::cmd_postinstall; pub use repl::cmd_repl; diff --git a/src/commands/name.rs b/src/commands/name.rs deleted file mode 100644 index 2dbb1ad..0000000 --- a/src/commands/name.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::args::NameSetArgs; -use crate::vfs::generate_valid_name; -use anyhow::{Context, Result, bail}; -use firefly_types::Encode; -use std::fs; -use std::path::Path; - -pub fn cmd_name_get(vfs: &Path) -> Result<()> { - let name_path = vfs.join("sys").join("name"); - let name = fs::read_to_string(name_path)?; - if let Err(err) = firefly_types::validate_id(&name) { - println!("⚠️ the name is not valid: {err}"); - } - println!("{name}"); - Ok(()) -} - -pub fn cmd_name_set(vfs: &Path, args: &NameSetArgs) -> Result<()> { - let name_path = vfs.join("sys").join("name"); - let old_name = fs::read_to_string(&name_path)?; - println!("old name: {old_name}"); - if let Err(err) = firefly_types::validate_id(&args.name) { - bail!("validate new name: {err}"); - } - write_name(vfs, &args.name)?; - println!("new name: {}", &args.name); - Ok(()) -} - -pub fn cmd_name_generate(vfs: &Path) -> Result<()> { - let name = generate_valid_name(); - write_name(vfs, &name)?; - println!("new name: {name}"); - Ok(()) -} - -fn write_name(vfs: &Path, name: &str) -> Result<()> { - let name_path = vfs.join("sys").join("name"); - fs::write(name_path, name)?; - let settings_path = vfs.join("sys").join("config"); - let raw = fs::read(&settings_path).context("read settings")?; - let mut settings = firefly_types::Settings::decode(&raw[..]).context("parse settings")?; - settings.name = name.to_string(); - let raw = settings.encode_vec().context("encode settings")?; - fs::write(settings_path, raw).context("write settings file")?; - Ok(()) -} From 6bfc4b02369577d5073145ea0c53a6168771907a Mon Sep 17 00:00:00 2001 From: gram Date: Thu, 29 Jan 2026 15:26:11 +0100 Subject: [PATCH 04/23] count months and days from 1 instead of 0 --- src/commands/import.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/commands/import.rs b/src/commands/import.rs index 7d13bee..f1034f7 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -217,11 +217,7 @@ pub(super) fn write_stats(meta: &Meta<'_>, vfs_path: &Path) -> anyhow::Result<() fn copy_stats(default_path: &Path, stats_path: &Path) -> anyhow::Result<()> { let today = chrono::Local::now().date_naive(); #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let today = ( - today.year() as u16, - today.month0() as u8, - today.day0() as u8, - ); + let today = (today.year() as u16, today.month() as u8, today.day() as u8); let default = if default_path.exists() { let raw = fs::read(default_path).context("read default stats file")?; firefly_types::Stats::decode(&raw)? @@ -265,11 +261,7 @@ fn update_stats(default_path: &Path, stats_path: &Path) -> anyhow::Result<()> { let today = chrono::Local::now().date_naive(); #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let today = ( - today.year() as u16, - today.month0() as u8, - today.day0() as u8, - ); + let today = (today.year() as u16, today.month() as u8, today.day() as u8); // The current date might be behind the current date on the device, // and it might be reflected in the dates recorded in the stats. // If that happens, try to stay closer to the device time. From f166e869c5beed1b0b95fad7ba66703eeb3df401 Mon Sep 17 00:00:00 2001 From: gram Date: Fri, 6 Feb 2026 13:48:13 +0100 Subject: [PATCH 05/23] Clean up the list of names --- src/names_noun.txt | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/names_noun.txt b/src/names_noun.txt index bc995ac..140b6c3 100644 --- a/src/names_noun.txt +++ b/src/names_noun.txt @@ -97,7 +97,6 @@ bishop bits bitsy bizzy -blackie blanche blast blaze @@ -125,7 +124,6 @@ boris bosco bosley boss -boy bozley bradley brady @@ -139,7 +137,6 @@ brit brodie brook brooke -brownie bruiser bruno brutus @@ -198,11 +195,8 @@ chester chevy chewie chewy -chic -chico chief chili -china chip chipper chippy @@ -301,7 +295,6 @@ dusty dutches dylan earl -ebony echo eddie eddy @@ -386,17 +379,13 @@ greta gretel gretta griffen -gringo grizzly gromit grover gucci -guido gunner gunther gus -guy -gypsy hailey haley hallie @@ -431,7 +420,6 @@ huey hugh hugo hunter -india indy iris itsy @@ -549,7 +537,6 @@ lucas lucifer lucky mac -macho mack magic major @@ -852,9 +839,7 @@ silver simba simon simone -sissy skeeter -skinny skip skipper skippy @@ -876,7 +861,6 @@ sonny sophia sophie sox -spanky sparkle sparky speed @@ -963,7 +947,6 @@ topaz tori toto tracker -tramp trapper travis trigger @@ -1007,7 +990,6 @@ wesley westie whiskey whispy -whitie whiz wiggles wilber From cf938334ca31bdb95032e5b025ea788b440bbc10 Mon Sep 17 00:00:00 2001 From: gram Date: Fri, 6 Feb 2026 13:56:19 +0100 Subject: [PATCH 06/23] add a few extra names --- src/names_noun.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/names_noun.txt b/src/names_noun.txt index 140b6c3..5c2a7e7 100644 --- a/src/names_noun.txt +++ b/src/names_noun.txt @@ -374,6 +374,7 @@ gordon grace gracie grady +gram greenie greta gretel @@ -472,6 +473,7 @@ judy julius june kali +kalle kallie kane karma @@ -642,9 +644,11 @@ nutmeg oakley obie odie +oli olive oliver olivia +olle ollie onie onyx @@ -770,6 +774,7 @@ rolex rollie roman romeo +ron rosa roscoe rosebud @@ -811,6 +816,7 @@ sarge sasha sassie sassy +sat sawyer schultz scoobie From f48b61bd9a10a583c1418553d5ff8bbd3398720f Mon Sep 17 00:00:00 2001 From: gram Date: Fri, 6 Feb 2026 14:02:58 +0100 Subject: [PATCH 07/23] New settings --- Cargo.lock | 54 +++++++++++++++++++++++++++++++++++++----------------- Cargo.toml | 10 +++++----- src/vfs.rs | 20 +++++++++++++++----- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 325eddc..060aefc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,9 +81,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arbitrary" @@ -193,9 +193,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -203,9 +203,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -215,9 +215,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "firefly-types" -version = "0.7.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7922d445b31f0c96a1712f504d8dfd720df64cd3acc18324109d928e11c00a" +checksum = "4d795a62847d7881dd8fb63892eafd57e0ebe40534c43dc99b9b51ff4d6283d3" dependencies = [ "postcard", "serde", @@ -547,9 +547,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -709,12 +709,13 @@ dependencies = [ [[package]] name = "image" -version = "0.25.6" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", + "moxcms", "num-traits", "png", ] @@ -910,6 +911,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -1000,11 +1011,11 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" -version = "0.17.16" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.0", "crc32fast", "fdeflate", "flate2", @@ -1041,6 +1052,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + [[package]] name = "quote" version = "1.0.38" diff --git a/Cargo.toml b/Cargo.toml index 4a4013a..861747b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ categories = [ [dependencies] # Simpler error handling -anyhow = "1.0.100" +anyhow = "1.0.101" # Get current date and time chrono = { version = "0.4.42", default-features = false, features = ["clock"] } # Framework for parsing CLI args -clap = { version = "4.5.54", features = ["derive"] } +clap = { version = "4.5.57", features = ["derive"] } # Detect message boundaries in serial port output from device cobs = "0.5.0" # Generate PNG images (sign chunks) @@ -33,13 +33,13 @@ crossterm = "0.29.0" # Find the best place to sotre the VFS directories = "6.0.0" # Serialize app config into meta file in the ROM -firefly-types = { version = "0.7.1" } +firefly-types = { version = "0.8.0" } # Read gz archives (to install emulator). -flate2 = "1.1.5" +flate2 = "1.1.9" # Decode wav files hound = "3.5.1" # Parse PNG images -image = { version = "0.25.6", default-features = false, features = ["png"] } +image = { version = "0.25.9", default-features = false, features = ["png"] } # Generate PNG images (compress frame) libflate = "2.2.1" # Random device name generation diff --git a/src/vfs.rs b/src/vfs.rs index 9fec519..108190f 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -32,13 +32,12 @@ pub fn init_vfs(path: &Path) -> anyhow::Result<()> { fs::create_dir_all(path.join("sys").join("priv")).context("create sys/priv directory")?; fs::create_dir_all(path.join("data")).context("create data directory")?; let settings_path = path.join("sys").join("config"); - if !settings_path.exists() { + println!("{}", is_valid_settings(&settings_path)); + if !is_valid_settings(&settings_path) { let mut settings = firefly_types::Settings { - xp: 0, - badges: 0, - lang: [b'e', b'n'], - name: generate_valid_name(), timezone: detect_tz(), + name: generate_valid_name(), + ..Default::default() }; if !settings.timezone.contains('/') { settings.timezone = "Europe/Amsterdam".to_string(); @@ -50,6 +49,17 @@ pub fn init_vfs(path: &Path) -> anyhow::Result<()> { Ok(()) } +fn is_valid_settings(path: &Path) -> bool { + if !path.exists() { + return false; + } + let Ok(data) = std::fs::read(path) else { + return false; + }; + let res = firefly_types::Settings::decode(&data); + res.is_ok() +} + /// Generate a random valid device name. pub fn generate_valid_name() -> String { loop { From b32efe2df0acdc355fe5d9f03f1f4524b6b3b6ad Mon Sep 17 00:00:00 2001 From: gram Date: Fri, 6 Feb 2026 17:42:32 +0100 Subject: [PATCH 08/23] keep wasm debug info for --no-strip without --no-opt closes #78 --- src/langs.rs | 5 +++-- src/wasm.rs | 50 +++++++++++++++++++++++++++++--------------------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/langs.rs b/src/langs.rs index 66c0ac5..82c5a42 100644 --- a/src/langs.rs +++ b/src/langs.rs @@ -38,11 +38,12 @@ pub fn build_bin(config: &Config, args: &BuildArgs) -> anyhow::Result<()> { if !bin_path.is_file() { bail!("the build command haven't produced a binary file"); } - if !args.no_strip { + let strip = !args.no_strip; + if strip { strip_custom(&bin_path)?; } if !args.no_opt { - optimize(&bin_path).context("optimize wasm binary")?; + optimize(&bin_path, strip).context("optimize wasm binary")?; } Ok(()) } diff --git a/src/wasm.rs b/src/wasm.rs index 37ee820..3584242 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -64,7 +64,7 @@ pub fn strip_custom(bin_path: &Path) -> anyhow::Result<()> { /// Run [wasm-opt] on the given wasm binary. /// /// [wasm-opt]: https://github.com/WebAssembly/binaryen -pub fn optimize(bin_path: &Path) -> anyhow::Result<()> { +pub fn optimize(bin_path: &Path, strip: bool) -> anyhow::Result<()> { let Some(bin_path) = bin_path.to_str() else { return Ok(()); }; @@ -76,27 +76,35 @@ pub fn optimize(bin_path: &Path) -> anyhow::Result<()> { } // https://github.com/wasmi-labs/wasmi/?tab=readme-ov-file#webassembly-features + let mut args = vec![ + "-Oz", + "--disable-exception-handling", + "--disable-gc", + "--disable-typed-function-references", + "--enable-bulk-memory", + "--enable-extended-const", + "--enable-memory64", + "--enable-multivalue", + "--enable-mutable-globals", + "--enable-nontrapping-float-to-int", + "--enable-reference-types", + "--enable-relaxed-simd", + "--enable-sign-ext", + "--enable-simd", + "--enable-tail-call", + ]; + if strip { + args.push("--strip-debug"); + args.push("--strip-dwarf"); + args.push("--strip-producers"); + } else { + // https://github.com/firefly-zero/firefly-cli/issues/78 + args.push("--debuginfo"); + } + args.extend_from_slice(&["-o", bin_path, bin_path]); + let output = Command::new("wasm-opt") - .args([ - "-Oz", - "--disable-exception-handling", - "--disable-gc", - "--disable-typed-function-references", - "--enable-bulk-memory", - "--enable-extended-const", - "--enable-memory64", - "--enable-multivalue", - "--enable-mutable-globals", - "--enable-nontrapping-float-to-int", - "--enable-reference-types", - "--enable-relaxed-simd", - "--enable-sign-ext", - "--enable-simd", - "--enable-tail-call", - "-o", - bin_path, - bin_path, - ]) + .args(args) .output() .context("run wasm-opt")?; if !output.status.success() { From 40f7c88c6d337cd82761a44ecfdf1d701375797c Mon Sep 17 00:00:00 2001 From: gram Date: Fri, 6 Feb 2026 18:40:12 +0100 Subject: [PATCH 09/23] auto-generate app version on build --- src/commands/build.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/commands/build.rs b/src/commands/build.rs index 13ec3bb..0104d11 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -21,6 +21,7 @@ use std::ffi::OsString; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; +use std::time::SystemTime; static TIPS: &[&str] = &[ "keep an eye on the binary size: bigger binary often means slower code", @@ -145,7 +146,7 @@ fn write_meta(config: &Config) -> anyhow::Result> { author_name: &config.author_name, launcher: config.launcher, sudo: config.sudo, - version: config.version.unwrap_or(0), + version: get_app_version(config), }; let encoded = meta.encode_vec().context("serialize")?; fs::create_dir_all(&config.rom_path)?; @@ -154,6 +155,29 @@ fn write_meta(config: &Config) -> anyhow::Result> { Ok(meta) } +/// Get or generate the app version. +/// +/// If the app version is not provided explicitly in firefly.toml, +/// we use the build time encoded as the number of minutes since FOSDEM 2026. +#[expect(clippy::cast_possible_truncation)] +fn get_app_version(config: &Config) -> u32 { + /// The date of FOSDEM 2026: 2026-02-01 (Sun) 11:00:00 CET. + /// + /// A semi-arbitrary number to substract from UNIX timestamp + /// when generating app version to avoid Y2038 problem. + const FIREFLY_EPOCH: u64 = 1_769_940_000; + + if let Some(version) = config.version { + return version; + } + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let version = (timestamp - FIREFLY_EPOCH) / 60; + version as u32 +} + fn reset_launcher_cache(vfs_path: &Path) -> anyhow::Result<()> { let cache_path = vfs_path .join("data") From 581f2b11b695c45a076cc44e3efede3536e332e9 Mon Sep 17 00:00:00 2001 From: gram Date: Tue, 10 Feb 2026 13:57:00 +0100 Subject: [PATCH 10/23] update firefly-types --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- src/vfs.rs | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 060aefc..07bf3e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,9 +506,9 @@ dependencies = [ [[package]] name = "firefly-types" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d795a62847d7881dd8fb63892eafd57e0ebe40534c43dc99b9b51ff4d6283d3" +checksum = "7e9d24df2e684ea2ac8338bd0a7131121d8c73e208523b8695b09646b76adc40" dependencies = [ "postcard", "serde", diff --git a/Cargo.toml b/Cargo.toml index 861747b..fb9028f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ crossterm = "0.29.0" # Find the best place to sotre the VFS directories = "6.0.0" # Serialize app config into meta file in the ROM -firefly-types = { version = "0.8.0" } +firefly-types = { version = "0.8.1" } # Read gz archives (to install emulator). flate2 = "1.1.9" # Decode wav files diff --git a/src/vfs.rs b/src/vfs.rs index 108190f..d7c7142 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -34,6 +34,7 @@ pub fn init_vfs(path: &Path) -> anyhow::Result<()> { let settings_path = path.join("sys").join("config"); println!("{}", is_valid_settings(&settings_path)); if !is_valid_settings(&settings_path) { + // TODO(@orsinium): detect country code. let mut settings = firefly_types::Settings { timezone: detect_tz(), name: generate_valid_name(), From 4a84dbf774f8f6ecd9fca87eb8e5b459d2c88ba4 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 15:48:39 +0100 Subject: [PATCH 11/23] new image format --- src/images.rs | 177 +++++++++----------------------------------------- 1 file changed, 29 insertions(+), 148 deletions(-) diff --git a/src/images.rs b/src/images.rs index 39e5504..4d74f52 100644 --- a/src/images.rs +++ b/src/images.rs @@ -1,6 +1,6 @@ use crate::palettes::{Color, Palette}; use anyhow::{Context, Result, bail}; -use image::{Pixel, Rgba, RgbaImage}; +use image::{Pixel, Rgb, Rgba, RgbaImage}; use std::fs::File; use std::io::Write; use std::path::Path; @@ -12,65 +12,41 @@ pub fn convert_image(in_path: &Path, out_path: &Path, sys_pal: &Palette) -> Resu if img.width() % 8 != 0 { bail!("image width must be divisible by 8"); } - let mut img_pal = make_palette(&img, sys_pal).context("detect colors used in the image")?; - let mut out = File::create(out_path).context("create output path")?; - // The magic number. "2"=image, "1"=v1. - write_u8(&mut out, 0x21)?; + let img_pal = make_palette(&img, sys_pal).context("detect colors used in the image")?; + let out = File::create(out_path).context("create output path")?; + let n_colors = img_pal.len(); - if n_colors <= 2 { - if n_colors <= 1 { - println!("⚠️ the image has only one color."); - } - extend_palette(&mut img_pal, sys_pal, 2); - write_image::<1, 8>(out, &img, &img_pal, sys_pal).context("write 1BPP image") - } else if n_colors <= 4 { - extend_palette(&mut img_pal, sys_pal, 4); - write_image::<2, 4>(out, &img, &img_pal, sys_pal).context("write 1BPP image") - } else if n_colors <= 16 { - extend_palette(&mut img_pal, sys_pal, 16); - write_image::<4, 2>(out, &img, &img_pal, sys_pal).context("write 1BPP image") - } else { + if n_colors > 16 { let has_transparency = img_pal.iter().any(Option::is_none); if has_transparency && n_colors == 17 { bail!("cannot use all 16 colors with transparency, remove one color"); } bail!("the image has too many colors"); } + + let transp = pick_transparent(&img_pal, sys_pal)?; + write_image(out, &img, sys_pal, transp).context("write image") } -fn write_image( - mut out: File, - img: &RgbaImage, - img_pal: &[Color], - sys_pal: &Palette, -) -> Result<()> { - write_u8(&mut out, BPP)?; // BPP +fn write_image(mut out: File, img: &RgbaImage, sys_pal: &Palette, transp: u8) -> Result<()> { + const BPP: u8 = 4; + const PPB: usize = 2; + let Ok(width) = u16::try_from(img.width()) else { bail!("the image is too big") }; + write_u8(&mut out, 0x22)?; // magic number write_u16(&mut out, width)?; // image width - let transparent = pick_transparent(img_pal, sys_pal)?; - write_u8(&mut out, transparent)?; // transparent color + write_u8(&mut out, transp)?; // transparent color - // palette swaps - let mut byte = 0; - debug_assert!(img_pal.len() == 2 || img_pal.len() == 4 || img_pal.len() == 16); - for (i, color) in img_pal.iter().enumerate() { - let index = match color { - Some(color) => find_color(sys_pal, Some(*color)), - None => transparent, - }; - byte = (byte << 4) | index; - if i % 2 == 1 { - write_u8(&mut out, byte)?; - } - } - - // image raw packed bytes + // Pixel values. let mut byte: u8 = 0; for (i, pixel) in img.pixels().enumerate() { let color = convert_color(*pixel); - let raw_color = find_color(img_pal, color); + let raw_color = match color { + Some(color) => find_color(sys_pal, color), + None => transp, + }; byte = (byte << BPP) | raw_color; if (i + 1) % PPB == 0 { write_u8(&mut out, byte)?; @@ -95,63 +71,12 @@ fn make_palette(img: &RgbaImage, sys_pal: &Palette) -> Result> { } } palette.sort_by_key(|c| match c { - Some(c) => find_color(sys_pal, Some(*c)), + Some(c) => find_color(sys_pal, *c), None => 20, }); Ok(palette) } -/// Add empty colors at the end of the palette to match the BPP size. -/// -/// If the given image palette is fully contained within the system palette -/// (after being cut to the expected swaps size), place the colors in the -/// image palette in the same positions as they are in the system palette. -/// This will make it possible to read such images without worrying about -/// applying color swaps. -fn extend_palette(img_pal: &mut Vec, sys_pal: &Palette, size: usize) { - if img_pal.len() > size { - return; - } - - let sys_pal_prefix = &sys_pal[..size]; - if !is_subpalette(img_pal, sys_pal_prefix) { - img_pal.extend_from_slice(&sys_pal[img_pal.len()..size]); - return; - } - - // No transparency? Just use the system palette. - let has_transp = img_pal.iter().any(Option::is_none); - if !has_transp { - img_pal.clear(); - img_pal.extend(sys_pal_prefix); - return; - } - - // Has transparency? Then copy the system palette and poke one hole in it. - let mut new_pal: Vec = Vec::new(); - let mut found_transp = false; - for c in sys_pal_prefix { - if found_transp || img_pal.contains(c) { - new_pal.push(*c); - } else { - new_pal.push(None); - found_transp = true; - } - } - img_pal.clear(); - img_pal.extend(new_pal); -} - -/// Check if the image palette is fully contained within the given system palette. -fn is_subpalette(img_pal: &[Color], sys_pal: &[Color]) -> bool { - for c in img_pal { - if c.is_some() && !sys_pal.contains(c) { - return false; - } - } - true -} - fn write_u8(f: &mut File, v: u8) -> std::io::Result<()> { f.write_all(&v.to_le_bytes()) } @@ -161,9 +86,9 @@ fn write_u16(f: &mut File, v: u16) -> std::io::Result<()> { } /// Find the index of the given color in the given palette. -fn find_color(palette: &[Color], c: Color) -> u8 { +fn find_color(palette: &Palette, c: Rgb) -> u8 { for (color, i) in palette.iter().zip(0u8..) { - if *color == c { + if *color == Some(c) { return i; } } @@ -177,27 +102,23 @@ fn format_color(c: Color) -> String { let c = c.0; format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2]) } - None => "ALPHA".to_string(), + None => "TRANSPARENT".to_string(), } } fn convert_color(c: Rgba) -> Color { - if is_transparent(c) { + let alpha = c.0[3]; + let is_transparent = alpha < 128; + if is_transparent { return None; } Some(c.to_rgb()) } -const fn is_transparent(c: Rgba) -> bool { - let alpha = c.0[3]; - alpha < 128 -} - /// Pick the color to be used to represent transparency fn pick_transparent(img_pal: &[Color], sys_pal: &Palette) -> Result { if img_pal.iter().all(Option::is_some) { - // no transparency needed - return Ok(17); + return Ok(0xff); // no transparency needed } for (color, i) in sys_pal.iter().zip(0u8..) { if !img_pal.contains(color) { @@ -208,7 +129,7 @@ fn pick_transparent(img_pal: &[Color], sys_pal: &Palette) -> Result { bail!("the image cannot contain more than 16 colors") } if img_pal.len() == 16 { - bail!("an image cannot contain all 16 colors and transparency") + bail!("the image cannot contain all 16 colors and transparency") } bail!("image contains colors not from the palette") } @@ -232,50 +153,10 @@ mod tests { let c1 = pal[1]; let c2 = pal[2]; let c3 = pal[3]; - assert_eq!(pick_transparent(&[c0, c1], pal).unwrap(), 17); + assert_eq!(pick_transparent(&[c0, c1], pal).unwrap(), 255); assert_eq!(pick_transparent(&[c0, c1, None], pal).unwrap(), 2); assert_eq!(pick_transparent(&[c0, None, c1], pal).unwrap(), 2); assert_eq!(pick_transparent(&[c1, c0, None], pal).unwrap(), 2); assert_eq!(pick_transparent(&[c0, c1, c2, c3, None], pal).unwrap(), 4); } - - #[test] - fn test_extend_palette() { - let pal = SWEETIE16; - let c0 = pal[0]; - let c1 = pal[1]; - let c2 = pal[2]; - let c3 = pal[3]; - let c4 = pal[4]; - - // Already the palette prefix, do nothing. - let mut img_pal = vec![c0, c1]; - extend_palette(&mut img_pal, pal, 2); - assert_eq!(img_pal, vec![c0, c1]); - - // A prefix but in a wrong order. Fix the order. - let mut img_pal = vec![c1, c0]; - extend_palette(&mut img_pal, pal, 2); - assert_eq!(img_pal, vec![c0, c1]); - - // Not a prefix and already full. Keep the given palette. - let mut img_pal = vec![c2, c1]; - extend_palette(&mut img_pal, pal, 2); - assert_eq!(img_pal, vec![c2, c1]); - - // A prefix but too short. Fill the rest. - let mut img_pal = vec![c0, c1]; - extend_palette(&mut img_pal, pal, 4); - assert_eq!(img_pal, vec![c0, c1, c2, c3]); - - // Within the palette prefix. - let mut img_pal = vec![c2, c1]; - extend_palette(&mut img_pal, pal, 4); - assert_eq!(img_pal, vec![c0, c1, c2, c3]); - - // Not a prefix but too short. Don't touch the given, fill the rest. - let mut img_pal = vec![c4, c2]; - extend_palette(&mut img_pal, pal, 4); - assert_eq!(img_pal, vec![c4, c2, c2, c3]); - } } From 7e22ba01c0913ab2828364fe0e4551eaa293fcdb Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 16:19:09 +0100 Subject: [PATCH 12/23] simplify color palettes --- src/palettes.rs | 179 ++++++++++++++++++++++++------------------------ 1 file changed, 91 insertions(+), 88 deletions(-) diff --git a/src/palettes.rs b/src/palettes.rs index d2a5f0d..41e8129 100644 --- a/src/palettes.rs +++ b/src/palettes.rs @@ -2,7 +2,7 @@ use anyhow::{Context, Result, bail}; use image::Rgb; use std::collections::HashMap; -pub type Color = Option>; +pub type Color = Rgb; pub type Palette = [Color; 16]; pub type Palettes = HashMap; type RawPalette = HashMap; @@ -12,110 +12,113 @@ type RawPalette = HashMap; /// /// pub static SWEETIE16: &Palette = &[ - Some(Rgb([0x1a, 0x1c, 0x2c])), // #1a1c2c: black - Some(Rgb([0x5d, 0x27, 0x5d])), // #5d275d: purple - Some(Rgb([0xb1, 0x3e, 0x53])), // #b13e53: red - Some(Rgb([0xef, 0x7d, 0x57])), // #ef7d57: orange - Some(Rgb([0xff, 0xcd, 0x75])), // #ffcd75: yellow - Some(Rgb([0xa7, 0xf0, 0x70])), // #a7f070: light green - Some(Rgb([0x38, 0xb7, 0x64])), // #38b764: green - Some(Rgb([0x25, 0x71, 0x79])), // #257179: dark green - Some(Rgb([0x29, 0x36, 0x6f])), // #29366f: dark blue - Some(Rgb([0x3b, 0x5d, 0xc9])), // #3b5dc9: blue - Some(Rgb([0x41, 0xa6, 0xf6])), // #41a6f6: light blue - Some(Rgb([0x73, 0xef, 0xf7])), // #73eff7: cyan - Some(Rgb([0xf4, 0xf4, 0xf4])), // #f4f4f4: white - Some(Rgb([0x94, 0xb0, 0xc2])), // #94b0c2: light gray - Some(Rgb([0x56, 0x6c, 0x86])), // #566c86: gray - Some(Rgb([0x33, 0x3c, 0x57])), // #333c57: dark gray + Rgb([0x1a, 0x1c, 0x2c]), // #1a1c2c: black + Rgb([0x5d, 0x27, 0x5d]), // #5d275d: purple + Rgb([0xb1, 0x3e, 0x53]), // #b13e53: red + Rgb([0xef, 0x7d, 0x57]), // #ef7d57: orange + Rgb([0xff, 0xcd, 0x75]), // #ffcd75: yellow + Rgb([0xa7, 0xf0, 0x70]), // #a7f070: light green + Rgb([0x38, 0xb7, 0x64]), // #38b764: green + Rgb([0x25, 0x71, 0x79]), // #257179: dark green + Rgb([0x29, 0x36, 0x6f]), // #29366f: dark blue + Rgb([0x3b, 0x5d, 0xc9]), // #3b5dc9: blue + Rgb([0x41, 0xa6, 0xf6]), // #41a6f6: light blue + Rgb([0x73, 0xef, 0xf7]), // #73eff7: cyan + Rgb([0xf4, 0xf4, 0xf4]), // #f4f4f4: white + Rgb([0x94, 0xb0, 0xc2]), // #94b0c2: light gray + Rgb([0x56, 0x6c, 0x86]), // #566c86: gray + Rgb([0x33, 0x3c, 0x57]), // #333c57: dark gray ]; /// The PICO-8 color palette. /// /// static PICO8: &Palette = &[ - Some(Rgb([0x00, 0x00, 0x00])), // #000000: black - Some(Rgb([0x1D, 0x2B, 0x53])), // #1D2B53: dark blue - Some(Rgb([0x7E, 0x25, 0x53])), // #7E2553: dark purple - Some(Rgb([0x00, 0x87, 0x51])), // #008751: dark green - Some(Rgb([0xAB, 0x52, 0x36])), // #AB5236: brown - Some(Rgb([0x5F, 0x57, 0x4F])), // #5F574F: dark gray - Some(Rgb([0xC2, 0xC3, 0xC7])), // #C2C3C7: light gray - Some(Rgb([0xFF, 0xF1, 0xE8])), // #FFF1E8: white - Some(Rgb([0xFF, 0x00, 0x4D])), // #FF004D: red - Some(Rgb([0xFF, 0xA3, 0x00])), // #FFA300: orange - Some(Rgb([0xFF, 0xEC, 0x27])), // #FFEC27: yellow - Some(Rgb([0x00, 0xE4, 0x36])), // #00E436: green - Some(Rgb([0x29, 0xAD, 0xFF])), // #29ADFF: blue - Some(Rgb([0x83, 0x76, 0x9C])), // #83769C: indigo - Some(Rgb([0xFF, 0x77, 0xA8])), // #FF77A8: pink - Some(Rgb([0xFF, 0xCC, 0xAA])), // #FFCCAA: peach + Rgb([0x00, 0x00, 0x00]), // #000000: black + Rgb([0x1D, 0x2B, 0x53]), // #1D2B53: dark blue + Rgb([0x7E, 0x25, 0x53]), // #7E2553: dark purple + Rgb([0x00, 0x87, 0x51]), // #008751: dark green + Rgb([0xAB, 0x52, 0x36]), // #AB5236: brown + Rgb([0x5F, 0x57, 0x4F]), // #5F574F: dark gray + Rgb([0xC2, 0xC3, 0xC7]), // #C2C3C7: light gray + Rgb([0xFF, 0xF1, 0xE8]), // #FFF1E8: white + Rgb([0xFF, 0x00, 0x4D]), // #FF004D: red + Rgb([0xFF, 0xA3, 0x00]), // #FFA300: orange + Rgb([0xFF, 0xEC, 0x27]), // #FFEC27: yellow + Rgb([0x00, 0xE4, 0x36]), // #00E436: green + Rgb([0x29, 0xAD, 0xFF]), // #29ADFF: blue + Rgb([0x83, 0x76, 0x9C]), // #83769C: indigo + Rgb([0xFF, 0x77, 0xA8]), // #FF77A8: pink + Rgb([0xFF, 0xCC, 0xAA]), // #FFCCAA: peach ]; /// SLSO8 color palette. /// /// static SLSO8: &Palette = &[ - Some(Rgb([0x0d, 0x2b, 0x45])), // #0d2b45 - Some(Rgb([0x20, 0x3c, 0x56])), // #203c56 - Some(Rgb([0x54, 0x4e, 0x68])), // #544e68 - Some(Rgb([0x8d, 0x69, 0x7a])), // #8d697a - Some(Rgb([0xd0, 0x81, 0x59])), // #d08159 - Some(Rgb([0xff, 0xaa, 0x5e])), // #ffaa5e - Some(Rgb([0xff, 0xd4, 0xa3])), // #ffd4a3 - Some(Rgb([0xff, 0xec, 0xd6])), // #ffecd6 - None, - None, - None, - None, - None, - None, - None, - None, + Rgb([0x0d, 0x2b, 0x45]), // #0d2b45 + Rgb([0x20, 0x3c, 0x56]), // #203c56 + Rgb([0x54, 0x4e, 0x68]), // #544e68 + Rgb([0x8d, 0x69, 0x7a]), // #8d697a + Rgb([0xd0, 0x81, 0x59]), // #d08159 + Rgb([0xff, 0xaa, 0x5e]), // #ffaa5e + Rgb([0xff, 0xd4, 0xa3]), // #ffd4a3 + Rgb([0xff, 0xec, 0xd6]), // #ffecd6 + // unused + Rgb([0xff, 0xec, 0xd6]), + Rgb([0xff, 0xec, 0xd6]), + Rgb([0xff, 0xec, 0xd6]), + Rgb([0xff, 0xec, 0xd6]), + Rgb([0xff, 0xec, 0xd6]), + Rgb([0xff, 0xec, 0xd6]), + Rgb([0xff, 0xec, 0xd6]), + Rgb([0xff, 0xec, 0xd6]), ]; /// The Kirokaze Gameboy color palette. /// /// static GAMEBOY: &Palette = &[ - Some(Rgb([0x33, 0x2c, 0x50])), // #332c50: purple - Some(Rgb([0x46, 0x87, 0x8f])), // #46878f: blue - Some(Rgb([0x94, 0xe3, 0x44])), // #94e344: green - Some(Rgb([0xe2, 0xf3, 0xe4])), // #e2f3e4: white - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, + Rgb([0x33, 0x2c, 0x50]), // #332c50: purple + Rgb([0x46, 0x87, 0x8f]), // #46878f: blue + Rgb([0x94, 0xe3, 0x44]), // #94e344: green + Rgb([0xe2, 0xf3, 0xe4]), // #e2f3e4: white + // unused + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), + Rgb([0xe2, 0xf3, 0xe4]), ]; /// WASM-4 color palette. /// /// static WASM4: &Palette = &[ - Some(Rgb([0xE0, 0xF8, 0xCF])), // #E0F8CF: white - Some(Rgb([0x86, 0xC0, 0x6C])), // #86C06C: light green - Some(Rgb([0x30, 0x68, 0x50])), // #306850: dark green - Some(Rgb([0x07, 0x18, 0x21])), // #071821: black - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, + Rgb([0xE0, 0xF8, 0xCF]), // #E0F8CF: white + Rgb([0x86, 0xC0, 0x6C]), // #86C06C: light green + Rgb([0x30, 0x68, 0x50]), // #306850: dark green + Rgb([0x07, 0x18, 0x21]), // #071821: black + // unused + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), + Rgb([0x07, 0x18, 0x21]), ]; pub fn parse_palettes(raws: Option<&HashMap>) -> Result { @@ -143,15 +146,15 @@ fn parse_palette(raw: &RawPalette) -> Result { } let len = u16::try_from(len).unwrap(); - let mut palette: Palette = Palette::default(); + let mut palette = Vec::new(); for id in 1u16..=len { let Some(raw_color) = raw.get(&id.to_string()) else { bail!("color IDs must be consecutive but ID {id} is missing"); }; let color = parse_color(*raw_color)?; - let idx = usize::from(id - 1); - palette[idx] = color; + palette.push(color); } + let palette: Palette = palette.try_into().unwrap(); Ok(palette) } @@ -163,7 +166,7 @@ fn parse_color(raw: u32) -> Result { let r = (raw >> 16) as u8; let g = (raw >> 8) as u8; let b = raw as u8; - Ok(Some(Rgb([r, g, b]))) + Ok(Rgb([r, g, b])) } pub fn get_palette<'a>(name: Option<&str>, palettes: &'a Palettes) -> Result<&'a Palette> { @@ -204,9 +207,9 @@ mod tests { let res = parse_palettes(Some(&ps)).unwrap(); assert_eq!(res.len(), 1); let exp: Palette = [ - Some(Rgb([0xff, 0x00, 0x00])), - Some(Rgb([0x00, 0xff, 0x00])), - Some(Rgb([0x00, 0x00, 0xff])), + Rgb([0xff, 0x00, 0x00]), + Rgb([0x00, 0xff, 0x00]), + Rgb([0x00, 0x00, 0xff]), None, None, None, From 520fbc2113fb33cfb095409b2dfa3856a42f7aa0 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 16:19:19 +0100 Subject: [PATCH 13/23] simplify color palettes --- src/images.rs | 90 +++++++++++++++++++------------------------------ src/palettes.rs | 59 +++----------------------------- 2 files changed, 40 insertions(+), 109 deletions(-) diff --git a/src/images.rs b/src/images.rs index 4d74f52..ccbee5f 100644 --- a/src/images.rs +++ b/src/images.rs @@ -12,19 +12,8 @@ pub fn convert_image(in_path: &Path, out_path: &Path, sys_pal: &Palette) -> Resu if img.width() % 8 != 0 { bail!("image width must be divisible by 8"); } - let img_pal = make_palette(&img, sys_pal).context("detect colors used in the image")?; + let transp = find_unused_color(&img, sys_pal).context("detect colors used in the image")?; let out = File::create(out_path).context("create output path")?; - - let n_colors = img_pal.len(); - if n_colors > 16 { - let has_transparency = img_pal.iter().any(Option::is_none); - if has_transparency && n_colors == 17 { - bail!("cannot use all 16 colors with transparency, remove one color"); - } - bail!("the image has too many colors"); - } - - let transp = pick_transparent(&img_pal, sys_pal)?; write_image(out, &img, sys_pal, transp).context("write image") } @@ -55,26 +44,33 @@ fn write_image(mut out: File, img: &RgbaImage, sys_pal: &Palette, transp: u8) -> Ok(()) } -/// Detect all colors used in the image. -fn make_palette(img: &RgbaImage, sys_pal: &Palette) -> Result> { - let mut palette = Vec::new(); +/// Find color from the palette not used on the image. +/// +/// Additionally ensures that the image uses the given color palette. +fn find_unused_color(img: &RgbaImage, sys_pal: &Palette) -> Result { + let mut used_colors: Vec = Vec::new(); + let mut has_transp = false; for (x, y, pixel) in img.enumerate_pixels() { - let color = convert_color(*pixel); - if !palette.contains(&color) { - if color.is_some() && !sys_pal.contains(&color) { - bail!( - "found a color not present in the color palette: {} (at x={x}, y={y})", - format_color(color), - ); - } - palette.push(color); + let Some(color) = convert_color(*pixel) else { + has_transp = true; + continue; + }; + if !sys_pal.contains(&color) { + bail!( + "found a color not present in the color palette: {} (at x={x}, y={y})", + format_color(color), + ); + } + if !used_colors.contains(&color) { + used_colors.push(color); } } - palette.sort_by_key(|c| match c { - Some(c) => find_color(sys_pal, *c), - None => 20, - }); - Ok(palette) + + if has_transp { + pick_transparent(&used_colors, sys_pal) + } else { + Ok(0xff) + } } fn write_u8(f: &mut File, v: u8) -> std::io::Result<()> { @@ -88,7 +84,7 @@ fn write_u16(f: &mut File, v: u16) -> std::io::Result<()> { /// Find the index of the given color in the given palette. fn find_color(palette: &Palette, c: Rgb) -> u8 { for (color, i) in palette.iter().zip(0u8..) { - if *color == Some(c) { + if *color == c { return i; } } @@ -97,16 +93,11 @@ fn find_color(palette: &Palette, c: Rgb) -> u8 { /// Make human-readable hex representation of the color code. fn format_color(c: Color) -> String { - match c { - Some(c) => { - let c = c.0; - format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2]) - } - None => "TRANSPARENT".to_string(), - } + let c = c.0; + format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2]) } -fn convert_color(c: Rgba) -> Color { +fn convert_color(c: Rgba) -> Option { let alpha = c.0[3]; let is_transparent = alpha < 128; if is_transparent { @@ -117,21 +108,13 @@ fn convert_color(c: Rgba) -> Color { /// Pick the color to be used to represent transparency fn pick_transparent(img_pal: &[Color], sys_pal: &Palette) -> Result { - if img_pal.iter().all(Option::is_some) { - return Ok(0xff); // no transparency needed - } + assert!(img_pal.len() <= sys_pal.len()); for (color, i) in sys_pal.iter().zip(0u8..) { if !img_pal.contains(color) { return Ok(i); } } - if img_pal.len() > 16 { - bail!("the image cannot contain more than 16 colors") - } - if img_pal.len() == 16 { - bail!("the image cannot contain all 16 colors and transparency") - } - bail!("image contains colors not from the palette") + bail!("cannot use all 16 colors with transparency, remove one color"); } #[cfg(test)] @@ -142,8 +125,7 @@ mod tests { #[test] fn test_format_color() { - assert_eq!(format_color(None), "ALPHA"); - assert_eq!(format_color(Some(Rgb([0x89, 0xab, 0xcd]))), "#89ABCD"); + assert_eq!(format_color(Rgb([0x89, 0xab, 0xcd])), "#89ABCD"); } #[test] @@ -153,10 +135,8 @@ mod tests { let c1 = pal[1]; let c2 = pal[2]; let c3 = pal[3]; - assert_eq!(pick_transparent(&[c0, c1], pal).unwrap(), 255); - assert_eq!(pick_transparent(&[c0, c1, None], pal).unwrap(), 2); - assert_eq!(pick_transparent(&[c0, None, c1], pal).unwrap(), 2); - assert_eq!(pick_transparent(&[c1, c0, None], pal).unwrap(), 2); - assert_eq!(pick_transparent(&[c0, c1, c2, c3, None], pal).unwrap(), 4); + assert_eq!(pick_transparent(&[c0, c1], pal).unwrap(), 2); + assert_eq!(pick_transparent(&[c1, c0], pal).unwrap(), 2); + assert_eq!(pick_transparent(&[c0, c1, c2, c3], pal).unwrap(), 4); } } diff --git a/src/palettes.rs b/src/palettes.rs index 41e8129..499c283 100644 --- a/src/palettes.rs +++ b/src/palettes.rs @@ -3,8 +3,8 @@ use image::Rgb; use std::collections::HashMap; pub type Color = Rgb; -pub type Palette = [Color; 16]; -pub type Palettes = HashMap; +pub type Palette = [Color]; +pub type Palettes = HashMap>; type RawPalette = HashMap; /// The default color palette (SWEETIE-16). @@ -64,15 +64,6 @@ static SLSO8: &Palette = &[ Rgb([0xff, 0xaa, 0x5e]), // #ffaa5e Rgb([0xff, 0xd4, 0xa3]), // #ffd4a3 Rgb([0xff, 0xec, 0xd6]), // #ffecd6 - // unused - Rgb([0xff, 0xec, 0xd6]), - Rgb([0xff, 0xec, 0xd6]), - Rgb([0xff, 0xec, 0xd6]), - Rgb([0xff, 0xec, 0xd6]), - Rgb([0xff, 0xec, 0xd6]), - Rgb([0xff, 0xec, 0xd6]), - Rgb([0xff, 0xec, 0xd6]), - Rgb([0xff, 0xec, 0xd6]), ]; /// The Kirokaze Gameboy color palette. @@ -83,19 +74,6 @@ static GAMEBOY: &Palette = &[ Rgb([0x46, 0x87, 0x8f]), // #46878f: blue Rgb([0x94, 0xe3, 0x44]), // #94e344: green Rgb([0xe2, 0xf3, 0xe4]), // #e2f3e4: white - // unused - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), - Rgb([0xe2, 0xf3, 0xe4]), ]; /// WASM-4 color palette. @@ -106,19 +84,6 @@ static WASM4: &Palette = &[ Rgb([0x86, 0xC0, 0x6C]), // #86C06C: light green Rgb([0x30, 0x68, 0x50]), // #306850: dark green Rgb([0x07, 0x18, 0x21]), // #071821: black - // unused - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), - Rgb([0x07, 0x18, 0x21]), ]; pub fn parse_palettes(raws: Option<&HashMap>) -> Result { @@ -133,7 +98,7 @@ pub fn parse_palettes(raws: Option<&HashMap>) -> Result Result { +fn parse_palette(raw: &RawPalette) -> Result> { let len = raw.len(); if len > 16 { bail!("too many colors") @@ -154,7 +119,6 @@ fn parse_palette(raw: &RawPalette) -> Result { let color = parse_color(*raw_color)?; palette.push(color); } - let palette: Palette = palette.try_into().unwrap(); Ok(palette) } @@ -206,23 +170,10 @@ mod tests { ps.insert("rgb".to_string(), p); let res = parse_palettes(Some(&ps)).unwrap(); assert_eq!(res.len(), 1); - let exp: Palette = [ + let exp: &Palette = &[ Rgb([0xff, 0x00, 0x00]), Rgb([0x00, 0xff, 0x00]), Rgb([0x00, 0x00, 0xff]), - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, ]; assert_eq!(*res.get("rgb").unwrap(), exp); } @@ -230,7 +181,7 @@ mod tests { #[test] fn test_get_palette() { let mut p = Palettes::new(); - p.insert("sup".to_string(), *SWEETIE16); + p.insert("sup".to_string(), Vec::from(SWEETIE16)); assert_eq!(get_palette(None, &p).unwrap(), SWEETIE16); assert_eq!(get_palette(Some("sup"), &p).unwrap(), SWEETIE16); assert_eq!(get_palette(Some("sweetie16"), &p).unwrap(), SWEETIE16); From 23f9681ddfc2bc68d7afc12d64a1595e3881ee87 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 17:49:14 +0100 Subject: [PATCH 14/23] fix transparency in small palettes --- src/images.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/images.rs b/src/images.rs index ccbee5f..8585622 100644 --- a/src/images.rs +++ b/src/images.rs @@ -109,12 +109,19 @@ fn convert_color(c: Rgba) -> Option { /// Pick the color to be used to represent transparency fn pick_transparent(img_pal: &[Color], sys_pal: &Palette) -> Result { assert!(img_pal.len() <= sys_pal.len()); + assert!(sys_pal.len() <= 16); for (color, i) in sys_pal.iter().zip(0u8..) { if !img_pal.contains(color) { return Ok(i); } } - bail!("cannot use all 16 colors with transparency, remove one color"); + if sys_pal.len() == 16 { + bail!("cannot use all 16 colors with transparency, remove one color"); + } + // If the system palette has less than 16 colors, + // any of the colors outside the palette + // can be used for transparency. We use 15. + Ok(0xf) } #[cfg(test)] From 34ff0b0a889584d7efeb8faa23de82999a5924e1 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 17:59:44 +0100 Subject: [PATCH 15/23] inspect: fix stereo audio --- src/commands/inspect.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/inspect.rs b/src/commands/inspect.rs index 0675151..8faec6a 100644 --- a/src/commands/inspect.rs +++ b/src/commands/inspect.rs @@ -365,11 +365,14 @@ fn inspect_audio(path: &Path) -> Option { let sample_rate = u16::from_le_bytes([audio_bytes[2], audio_bytes[3]]); let audio_bytes = &audio_bytes[4..]; + let samples_per_second = u32::from(channels) * u32::from(sample_rate); #[expect(clippy::cast_precision_loss)] - let mut duration = audio_bytes.len() as f32 / f32::from(u16::from(channels) * sample_rate); + let mut duration = audio_bytes.len() as f64 / f64::from(samples_per_second); if is16 { duration /= 2.0; } + #[expect(clippy::cast_possible_truncation)] + let duration = duration as f32; let name = path.file_name()?; let name: String = name.to_str()?.to_string(); From 205247288761aec5bae6dbd954a5ac15db6f2101 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 18:13:14 +0100 Subject: [PATCH 16/23] inspect: new image format --- src/commands/inspect.rs | 137 ++++++++-------------------------------- 1 file changed, 28 insertions(+), 109 deletions(-) diff --git a/src/commands/inspect.rs b/src/commands/inspect.rs index 8faec6a..7caa133 100644 --- a/src/commands/inspect.rs +++ b/src/commands/inspect.rs @@ -237,18 +237,12 @@ const fn get_section_name(payload: &Payload<'_>) -> &'static str { struct ImageStats { name: String, - bpp: u8, width: u16, height: u16, - swaps: Vec, + uses: Vec, pixels: usize, } -struct Swap { - color: Option, - uses: u32, -} - fn inspect_images(rom_path: &Path) -> anyhow::Result> { let dir = fs::read_dir(rom_path)?; let mut stats = Vec::new(); @@ -266,64 +260,41 @@ fn inspect_image(path: &Path) -> Option { if image_bytes.len() < 8 { return None; } - if image_bytes[0] != 0x21 { + if image_bytes[0] != 0x22 { return None; } - let bpp = image_bytes[1]; - let width = u16::from(image_bytes[2]) | (u16::from(image_bytes[3]) << 8); - let transp = image_bytes[4]; - let image_bytes = &image_bytes[5..]; - let swaps_len = match bpp { - 1 => 1, - 2 => 2, - _ => 8, - }; - let max_colors = match bpp { - 1 => 2, - 2 => 4, - _ => 16, - }; - let Some(swaps) = &image_bytes.get(..swaps_len) else { - return None; - }; - let image_bytes = &image_bytes[swaps_len..]; - let ppb = match bpp { - 1 => 8, - 2 => 4, - _ => 2, - }; - let pixels = image_bytes.len() * ppb; + let width = u16::from(image_bytes[1]) | (u16::from(image_bytes[2]) << 8); + let transp = usize::from(image_bytes[3]); + let image_bytes = &image_bytes[4..]; + let pixels = image_bytes.len() * 2; #[expect(clippy::cast_possible_truncation)] let height = pixels as u16 / width; - let swaps = parse_swaps(transp, swaps); - let mut swaps: Vec<_> = swaps - .map(|color| Swap { color, uses: 0 }) - .into_iter() - .collect(); - let mask = match bpp { - 1 => 0b_0001, - 2 => 0b_0011, - _ => 0b_1111, - }; + let mut uses = vec![0; 16]; + let mask = 0b_1111; + let mut max_colors = 0; for byte in image_bytes { - let mut byte = *byte; - for _ in 0..ppb { - let c = usize::from(byte & mask); - swaps[c].uses += 1; - byte >>= bpp; + let c = usize::from(byte & mask); + if c != transp { + uses[c] += 1; + max_colors = usize::max(max_colors, c); + } + + let c = usize::from((byte >> 4) & mask); + if c != transp { + uses[c] += 1; + max_colors = usize::max(max_colors, c); } } - swaps.truncate(max_colors); + uses.truncate(max_colors + 1); let name = path.file_name()?; let name: String = name.to_str()?.to_string(); Some(ImageStats { name, - bpp, width, height, - swaps, + uses, pixels, }) } @@ -476,26 +447,20 @@ fn print_images_stats(stats: Vec) { fn print_image_stats(stats: ImageStats) { println!(" {}", stats.name.magenta()); - println!(" {}: {}", "bpp".cyan(), stats.bpp); println!(" {}: {}", "width".cyan(), stats.width); println!(" {}: {}", "height".cyan(), stats.height); println!(" {}: {}", "pixels".cyan(), stats.pixels); println!(" {}", "colors".cyan()); - for (i, swap) in stats.swaps.into_iter().enumerate() { - let usage: String = if swap.uses == 0 { + for (uses, i) in stats.uses.into_iter().zip(1u8..) { + let usage: String = if uses == 0 { "unused".blue().to_string() - } else if swap.uses < 10 { - format!("{:>6}", swap.uses).yellow().to_string() + } else if uses < 10 { + format!("{uses:>6}").yellow().to_string() } else { - format!("{:>6}", swap.uses) + format!("{uses:>6}") }; - if let Some(color) = swap.color { - let name = get_color_name(color); - let color = color + 1; - println!(" {i:>2} -> {color:>2} {name} {usage}"); - } else { - println!(" {i:>2} -> 0 transparent {usage}"); - } + let name = get_color_name(i - 1); + println!(" {i:>2} {name} {usage}"); } } @@ -524,52 +489,6 @@ fn print_audio_stats(stats: AudioStats) { println!(" {}: {:0.1}s", "duration".cyan(), stats.duration); } -fn parse_swaps(transp: u8, swaps: &[u8]) -> [Option; 16] { - #[expect(clippy::get_first)] - [ - // 0-4 - parse_color_l(transp, swaps.get(0)), - parse_color_r(transp, swaps.get(0)), - parse_color_l(transp, swaps.get(1)), - parse_color_r(transp, swaps.get(1)), - // 4-8 - parse_color_l(transp, swaps.get(2)), - parse_color_r(transp, swaps.get(2)), - parse_color_l(transp, swaps.get(3)), - parse_color_r(transp, swaps.get(3)), - // 8-12 - parse_color_l(transp, swaps.get(4)), - parse_color_r(transp, swaps.get(4)), - parse_color_l(transp, swaps.get(5)), - parse_color_r(transp, swaps.get(5)), - // 12-16 - parse_color_l(transp, swaps.get(6)), - parse_color_r(transp, swaps.get(6)), - parse_color_l(transp, swaps.get(7)), - parse_color_r(transp, swaps.get(7)), - ] -} - -/// Parse the high bits of a byte as a color. -fn parse_color_r(transp: u8, c: Option<&u8>) -> Option { - let c = c?; - let c = c & 0b1111; - if c == transp { - return None; - } - Some(c) -} - -/// Parse the low bits of a byte as a color. -fn parse_color_l(transp: u8, c: Option<&u8>) -> Option { - let c = c?; - let c = (c >> 4) & 0b1111; - if c == transp { - return None; - } - Some(c) -} - const fn get_color_name(swap: u8) -> &'static str { match swap { 0 => "black #1A1C2C", From 5d7698c29a16ec192cbaa7fffdc12066a4f8f92a Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 18:17:30 +0100 Subject: [PATCH 17/23] inspect: hide unused colors --- src/commands/inspect.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/commands/inspect.rs b/src/commands/inspect.rs index 7caa133..6fb0b8a 100644 --- a/src/commands/inspect.rs +++ b/src/commands/inspect.rs @@ -272,21 +272,17 @@ fn inspect_image(path: &Path) -> Option { let mut uses = vec![0; 16]; let mask = 0b_1111; - let mut max_colors = 0; for byte in image_bytes { let c = usize::from(byte & mask); if c != transp { uses[c] += 1; - max_colors = usize::max(max_colors, c); } let c = usize::from((byte >> 4) & mask); if c != transp { uses[c] += 1; - max_colors = usize::max(max_colors, c); } } - uses.truncate(max_colors + 1); let name = path.file_name()?; let name: String = name.to_str()?.to_string(); @@ -450,11 +446,20 @@ fn print_image_stats(stats: ImageStats) { println!(" {}: {}", "width".cyan(), stats.width); println!(" {}: {}", "height".cyan(), stats.height); println!(" {}: {}", "pixels".cyan(), stats.pixels); - println!(" {}", "colors".cyan()); + + let mut n_colors = 0; + for uses in &stats.uses { + if *uses != 0 { + n_colors += 1; + } + } + println!(" {}: {n_colors}", "colors".cyan()); + for (uses, i) in stats.uses.into_iter().zip(1u8..) { - let usage: String = if uses == 0 { - "unused".blue().to_string() - } else if uses < 10 { + if uses == 0 { + continue; + } + let usage = if uses < 10 { format!("{uses:>6}").yellow().to_string() } else { format!("{uses:>6}") From 66ee7ebf4d84e716ed68986bfe3b85711407b703 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 18:22:25 +0100 Subject: [PATCH 18/23] inspect: highlight top color --- src/commands/inspect.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/commands/inspect.rs b/src/commands/inspect.rs index 6fb0b8a..63c10e1 100644 --- a/src/commands/inspect.rs +++ b/src/commands/inspect.rs @@ -240,6 +240,7 @@ struct ImageStats { width: u16, height: u16, uses: Vec, + n_colors: u32, pixels: usize, } @@ -284,6 +285,13 @@ fn inspect_image(path: &Path) -> Option { } } + let mut n_colors = 0; + for uses in &uses { + if *uses != 0 { + n_colors += 1; + } + } + let name = path.file_name()?; let name: String = name.to_str()?.to_string(); Some(ImageStats { @@ -291,6 +299,7 @@ fn inspect_image(path: &Path) -> Option { width, height, uses, + n_colors, pixels, }) } @@ -446,21 +455,17 @@ fn print_image_stats(stats: ImageStats) { println!(" {}: {}", "width".cyan(), stats.width); println!(" {}: {}", "height".cyan(), stats.height); println!(" {}: {}", "pixels".cyan(), stats.pixels); + println!(" {}: {}", "colors".cyan(), stats.n_colors); - let mut n_colors = 0; - for uses in &stats.uses { - if *uses != 0 { - n_colors += 1; - } - } - println!(" {}: {n_colors}", "colors".cyan()); - + let max_uses = stats.uses.iter().max().copied().unwrap_or_default(); for (uses, i) in stats.uses.into_iter().zip(1u8..) { if uses == 0 { continue; } - let usage = if uses < 10 { + let usage = if uses == max_uses { format!("{uses:>6}").yellow().to_string() + } else if uses < 10 { + format!("{uses:>6}").green().to_string() } else { format!("{uses:>6}") }; From bfa8689e490eea5674dc1fa33c9e63a87a8ee1d7 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 18:23:48 +0100 Subject: [PATCH 19/23] relax required image width --- src/images.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/images.rs b/src/images.rs index 8585622..ac5555f 100644 --- a/src/images.rs +++ b/src/images.rs @@ -9,8 +9,8 @@ pub fn convert_image(in_path: &Path, out_path: &Path, sys_pal: &Palette) -> Resu let file = image::ImageReader::open(in_path).context("open image file")?; let img = file.decode().context("decode image")?; let img = img.to_rgba8(); - if img.width() % 8 != 0 { - bail!("image width must be divisible by 8"); + if img.width() % 2 != 0 { + bail!("image width must be divisible by 2"); } let transp = find_unused_color(&img, sys_pal).context("detect colors used in the image")?; let out = File::create(out_path).context("create output path")?; From eea2fb6bcc735f16494e9589f0ab258d7bedccc4 Mon Sep 17 00:00:00 2001 From: gram Date: Wed, 18 Feb 2026 18:53:38 +0100 Subject: [PATCH 20/23] enable more wasm features for Rust --- src/langs.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/langs.rs b/src/langs.rs index 82c5a42..9c6214c 100644 --- a/src/langs.rs +++ b/src/langs.rs @@ -206,7 +206,14 @@ fn build_rust_inner(config: &Config, example: bool) -> anyhow::Result<()> { let mut cmd = cmd.args(cmd_args).current_dir(in_path); let cargo_config = config.root_path.join(".cargo").join("config.toml"); if !cargo_config.exists() { - cmd = cmd.env("RUSTFLAGS", "-Clink-arg=-zstack-size=4096"); + // https://doc.rust-lang.org/reference/attributes/codegen.html#wasm32-or-wasm64 + // https://github.com/wasmi-labs/wasmi/?tab=readme-ov-file#webassembly-features + // + // TODO: Enable `+relaxed-simd` when it works. + // At the moment of writing, it causes runtime error for Blutti: + // > unexpected SIMD opcode: 0xfd (at offset 0x9f). + let flags = "-Clink-arg=-zstack-size=4096 -Ctarget-feature=+extended-const,+tail-call"; + cmd = cmd.env("RUSTFLAGS", flags); } let output = cmd.output().context("run cargo build")?; check_output(&output)?; From add8791199ce0d7a9b9d95d51f1c166b1123f589 Mon Sep 17 00:00:00 2001 From: gram Date: Thu, 19 Feb 2026 15:24:53 +0100 Subject: [PATCH 21/23] set "--release" flag for MoonBit --- src/langs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/langs.rs b/src/langs.rs index 9c6214c..86aee8a 100644 --- a/src/langs.rs +++ b/src/langs.rs @@ -375,7 +375,7 @@ fn build_zig(config: &Config) -> anyhow::Result<()> { // Build Moon project. fn build_moon(config: &Config) -> anyhow::Result<()> { check_installed("Moon", "moon", "version")?; - let mut cmd_args = vec!["build", "--target", "wasm"]; + let mut cmd_args = vec!["build", "--target", "wasm", "--release"]; if let Some(additional_args) = &config.compile_args { for arg in additional_args { cmd_args.push(arg.as_str()); From b139de22f1d5711a3c002ed2b58bd41f0c330062 Mon Sep 17 00:00:00 2001 From: gram Date: Thu, 19 Feb 2026 20:24:13 +0100 Subject: [PATCH 22/23] upd deps --- Cargo.lock | 119 ++++++++++++++++++++--------------------------------- Cargo.toml | 11 +++-- 2 files changed, 50 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07bf3e4..12301e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,15 +85,6 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "autocfg" version = "1.4.0" @@ -193,9 +184,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" dependencies = [ "clap_builder", "clap_derive", @@ -203,9 +194,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.57" +version = "4.5.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" dependencies = [ "anstream", "anstyle", @@ -227,9 +218,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "clipboard-win" @@ -360,17 +351,6 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" -[[package]] -name = "derive_arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "derive_more" version = "2.0.1" @@ -561,12 +541,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" - [[package]] name = "foldhash" version = "0.2.0" @@ -617,23 +591,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" -dependencies = [ - "foldhash 0.1.4", - "serde", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", + "serde", + "serde_core", ] [[package]] @@ -722,12 +688,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown", "serde", "serde_core", ] @@ -805,7 +771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c" dependencies = [ "core2", - "hashbrown 0.16.0", + "hashbrown", "rle-decode-fast", ] @@ -1036,9 +1002,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] @@ -1108,9 +1074,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -1454,9 +1420,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" dependencies = [ "proc-macro2", "quote", @@ -1553,6 +1519,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + [[package]] name = "typenum" version = "1.17.0" @@ -1594,9 +1566,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "3.1.4" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a" +checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" dependencies = [ "base64", "flate2", @@ -1611,9 +1583,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" dependencies = [ "base64", "http", @@ -1724,9 +1696,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.244.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" dependencies = [ "leb128fmt", "wasmparser", @@ -1734,12 +1706,12 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.244.0" +version = "0.245.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" dependencies = [ "bitflags 2.9.0", - "hashbrown 0.15.2", + "hashbrown", "indexmap", "semver", "serde", @@ -1986,19 +1958,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -2013,14 +1984,14 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zip" -version = "7.0.0" +version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd8a47718a4ee5fe78e07667cd36f3de80e7c2bfe727c7074245ffc7303c037" +checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72" dependencies = [ - "arbitrary", "crc32fast", "indexmap", "memchr", + "typed-path", "zstd", ] @@ -2032,9 +2003,9 @@ checksum = "e6d6085d62852e35540689d1f97ad663e3971fc19cf5eceab364d62c646ea167" [[package]] name = "zstd" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ "zstd-safe", ] diff --git a/Cargo.toml b/Cargo.toml index fb9028f..149cfab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ anyhow = "1.0.101" # Get current date and time chrono = { version = "0.4.42", default-features = false, features = ["clock"] } # Framework for parsing CLI args -clap = { version = "4.5.57", features = ["derive"] } +clap = { version = "4.5.60", features = ["derive"] } # Detect message boundaries in serial port output from device cobs = "0.5.0" # Generate PNG images (sign chunks) @@ -43,7 +43,6 @@ image = { version = "0.25.9", default-features = false, features = ["png"] } # Generate PNG images (compress frame) libflate = "2.2.1" # Random device name generation -# NOTE: Cannot be updated to 0.9.0+ until rsa is updated to 0.10.0+. rand = "0.9.2" rust-embed = { version = "8.9.0", default-features = false, features = [ "debug-embed", @@ -63,10 +62,10 @@ tar = "0.4.44" # Deserialize firefly.toml toml = "0.9.8" # Download remote files (`url` field in `firefly.toml`) -ureq = "3.1.4" +ureq = "3.2.0" # Build together post-processed wasm binaries -wasm-encoder = "0.244.0" +wasm-encoder = "0.245.1" # Parse wasm binaries for post-processing (removing custom sections) -wasmparser = "0.244.0" +wasmparser = "0.245.1" # Work with zip archives (distribution format for ROMs) -zip = { version = "7.0.0", default-features = false, features = ["zstd"] } +zip = { version = "8.1.0", default-features = false, features = ["zstd"] } From 58659d26ae64eac039f33594fb0ed62af6785c22 Mon Sep 17 00:00:00 2001 From: gram Date: Thu, 19 Feb 2026 20:27:27 +0100 Subject: [PATCH 23/23] bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 12301e0..fb6bfa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -496,7 +496,7 @@ dependencies = [ [[package]] name = "firefly_cli" -version = "0.15.8" +version = "0.16.0" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 149cfab..623da1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "firefly_cli" -version = "0.15.8" +version = "0.16.0" rust-version = "1.91.0" edition = "2024" authors = ["Firefly Zero team"]