diff --git a/userspace/Cargo.lock b/userspace/Cargo.lock index 728e6928..861931bc 100644 --- a/userspace/Cargo.lock +++ b/userspace/Cargo.lock @@ -36,6 +36,12 @@ version = "0.1.0" name = "abi-serde" version = "0.1.0" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -583,6 +589,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -994,6 +1009,15 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "ff" version = "0.13.1" @@ -1010,6 +1034,16 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "flate2" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30f4148e3c9b7dbe0cc7e842ad5a61b28f9025f201d78149383e778a08bc9215" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1416,6 +1450,22 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "iv" +version = "0.1.0" +dependencies = [ + "bytemuck", + "clap", + "cross", + "libcolors", + "log", + "logsink", + "pixie", + "png", + "thiserror 1.0.69", + "toml", +] + [[package]] name = "jiff" version = "0.2.15" @@ -1644,6 +1694,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.11" @@ -2207,6 +2267,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pixie" +version = "0.1.0" +dependencies = [ + "bytemuck", + "log", + "thiserror 1.0.69", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -2234,6 +2303,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.9.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.8.0" @@ -2852,6 +2934,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "siphasher" version = "1.0.1" diff --git a/userspace/Cargo.toml b/userspace/Cargo.toml index d69cc63d..96f0c8e7 100644 --- a/userspace/Cargo.toml +++ b/userspace/Cargo.toml @@ -3,6 +3,7 @@ resolver = "1" members = [ "dyn-loader", "graphics/colors", + "graphics/iv", "graphics/term", "lib/cross", "lib/cryptic", @@ -11,6 +12,7 @@ members = [ "lib/libpsf", "lib/libterm", "lib/logsink", + "lib/pixie", "lib/runtime", "lib/stuff", "lib/uipc", @@ -85,6 +87,7 @@ logsink.path = "lib/logsink" libutil.path = "../lib/libutil" cryptic.path = "lib/cryptic" hclient.path = "lib/hclient" +pixie.path = "lib/pixie" stuff.path = "lib/stuff" [workspace.lints.rust] diff --git a/userspace/etc/test.jpeg b/userspace/etc/test.jpeg new file mode 100644 index 00000000..94754c05 Binary files /dev/null and b/userspace/etc/test.jpeg differ diff --git a/userspace/etc/test.png b/userspace/etc/test.png new file mode 100644 index 00000000..62fef9ce Binary files /dev/null and b/userspace/etc/test.png differ diff --git a/userspace/graphics/iv/Cargo.toml b/userspace/graphics/iv/Cargo.toml new file mode 100644 index 00000000..5d64908a --- /dev/null +++ b/userspace/graphics/iv/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "iv" +version = "0.1.0" +edition = "2021" + +[dependencies] +libcolors = { workspace = true, features = ["client"] } +pixie.workspace = true +logsink.workspace = true +cross.workspace = true + +bytemuck.workspace = true +log.workspace = true +thiserror.workspace = true +clap.workspace = true +toml.workspace = true + +# TODO write own library +png = "0.18.0" + +[lints] +workspace = true diff --git a/userspace/graphics/iv/src/error.rs b/userspace/graphics/iv/src/error.rs new file mode 100644 index 00000000..afaed77f --- /dev/null +++ b/userspace/graphics/iv/src/error.rs @@ -0,0 +1,18 @@ +use std::io; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{0}")] + Io(#[from] io::Error), + #[error("Application error: {0}")] + Colors(#[from] libcolors::error::Error), + #[error("Unrecognized/unsupported image format")] + UnknownFormat, + + #[error("PNG decode error: {0}")] + PngDecode(#[from] png::DecodingError), + #[error("JPEG decode error: {0}")] + JpegDecode(#[from] pixie::jpeg::Error), + #[error("Corrupt image")] + CorruptImage, +} diff --git a/userspace/graphics/iv/src/format/mod.rs b/userspace/graphics/iv/src/format/mod.rs new file mode 100644 index 00000000..1ca3fe52 --- /dev/null +++ b/userspace/graphics/iv/src/format/mod.rs @@ -0,0 +1,262 @@ +use std::{ + fs::File, + io::{BufReader, Read}, + path::Path, +}; + +use libcolors::surface::{Surface, VecSurface}; + +use crate::error::Error; + +pub struct ImageData { + pub data: Vec, + pub pixel_format: PixelFormat, + pub bit_depth: BitDepth, + pub stride: usize, + pub width: u32, + pub height: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PixelFormat { + Rgb, + Rgba, + #[allow(unused)] + Bgr, + Bgra, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BitDepth { + Eight, +} + +struct ImageLoader { + #[allow(unused)] + name: &'static str, + probe: fn(&Path) -> Result, + load: fn(&Path) -> Result, +} + +static IMAGE_LOADERS: &[ImageLoader] = &[ + ImageLoader { + name: "png", + probe: png_probe, + load: png_load, + }, + ImageLoader { + name: "jpeg", + probe: jpeg_probe, + load: jpeg_load, + }, +]; + +fn png_probe(path: &Path) -> Result { + let file = File::open(path).map(BufReader::new)?; + let decoder = png::Decoder::new(file); + Ok(decoder.read_info().is_ok()) +} +fn jpeg_probe(path: &Path) -> Result { + let mut file = File::open(path).map(BufReader::new)?; + let mut magic = [0; 4]; + let len = file.read(&mut magic)?; + if len != 4 || magic != [0xFF, 0xD8, 0xFF, 0xE0] { + Ok(false) + } else { + Ok(true) + } +} + +fn png_load(path: &Path) -> Result { + let file = File::open(path).map(BufReader::new)?; + let decoder = png::Decoder::new(file); + let mut info = decoder.read_info()?; + let output_buffer_size = info.output_buffer_size().ok_or(Error::CorruptImage)?; + let mut buffer = vec![0; output_buffer_size]; + let frame = info.next_frame(&mut buffer)?; + buffer.drain(frame.buffer_size()..); + let pixel_format = match frame.color_type { + png::ColorType::Rgb => PixelFormat::Rgb, + png::ColorType::Rgba => PixelFormat::Rgba, + t => todo!("Unsupported color type: {t:?}"), + }; + let bit_depth = match frame.bit_depth { + png::BitDepth::Eight => BitDepth::Eight, + t => todo!("Unsupported bit depth: {t:?}"), + }; + Ok(ImageData { + data: buffer, + pixel_format, + bit_depth, + stride: frame.line_size, + width: frame.width, + height: frame.height, + }) +} +fn jpeg_load(path: &Path) -> Result { + let file = File::open(path).map(BufReader::new)?; + let mut decoder = Box::new(pixie::JpegDecoder::new(file)); + let rgb = decoder.decode_rgb()?; + + Ok(ImageData { + data: rgb.data, + pixel_format: PixelFormat::Rgb, + bit_depth: BitDepth::Eight, + stride: rgb.stride, + width: rgb.width as u32, + height: rgb.height as u32, + }) +} + +impl ImageData { + pub fn load_image>(path: P) -> Result { + let path = path.as_ref(); + let mut loader = None; + for l in IMAGE_LOADERS { + if (l.probe)(path)? { + loader = Some(l); + break; + } + } + let loader = loader.ok_or(Error::UnknownFormat)?; + (loader.load)(path) + } + + #[inline] + fn convert_pixel( + source: &[u8], + source_format: PixelFormat, + source_depth: BitDepth, + output: &mut [u8], + output_format: PixelFormat, + output_depth: BitDepth, + ) { + let (r, g, b, a) = match (source_format, source_depth) { + (PixelFormat::Rgb, BitDepth::Eight) => (source[0], source[1], source[2], 255), + (PixelFormat::Rgba, BitDepth::Eight) => (source[0], source[1], source[2], source[3]), + (PixelFormat::Bgr, BitDepth::Eight) => (source[2], source[1], source[0], 255), + (PixelFormat::Bgra, BitDepth::Eight) => (source[2], source[1], source[0], source[3]), + }; + + match (output_format, output_depth) { + (PixelFormat::Rgb, BitDepth::Eight) => { + output[0] = r; + output[1] = g; + output[2] = b; + } + (PixelFormat::Rgba, BitDepth::Eight) => { + output[0] = r; + output[1] = g; + output[2] = b; + output[3] = a; + } + (PixelFormat::Bgr, BitDepth::Eight) => { + output[0] = b; + output[1] = g; + output[2] = r; + } + (PixelFormat::Bgra, BitDepth::Eight) => { + output[0] = b; + output[1] = g; + output[2] = r; + output[3] = a; + } + } + } + + // TODO optimized format conversion + pub fn convert_color( + &self, + target_pixel_format: PixelFormat, + target_bit_depth: BitDepth, + ) -> ImageData { + // TODO less than 8bpp + let source_bytes_per_pixel = match (self.pixel_format, self.bit_depth) { + (PixelFormat::Rgb | PixelFormat::Bgr, BitDepth::Eight) => 3, + (PixelFormat::Rgba | PixelFormat::Bgra, BitDepth::Eight) => 4, + }; + let output_bytes_per_pixel = match (target_pixel_format, target_bit_depth) { + (PixelFormat::Rgb | PixelFormat::Bgr, BitDepth::Eight) => 3, + (PixelFormat::Rgba | PixelFormat::Bgra, BitDepth::Eight) => 4, + }; + let output_stride = (self.width as usize * output_bytes_per_pixel + 3) & !3; + let output_buffer_size = output_stride * self.height as usize; + let mut output_buffer = vec![0u8; output_buffer_size]; + + for y in 0..self.height { + let source_row_i = y as usize * self.stride; + let source_row = &self.data[source_row_i..source_row_i + self.stride]; + let output_row_i = y as usize * output_stride; + let output_row = &mut output_buffer[output_row_i..output_row_i + output_stride]; + + // TODO SIMD impl + for x in 0..self.width { + let source_pixel_i = x as usize * source_bytes_per_pixel; + let source_pixel = + &source_row[source_pixel_i..source_pixel_i + source_bytes_per_pixel]; + let output_pixel_i = x as usize * output_bytes_per_pixel; + let output_pixel = + &mut output_row[output_pixel_i..output_pixel_i + output_bytes_per_pixel]; + + Self::convert_pixel( + source_pixel, + self.pixel_format, + self.bit_depth, + output_pixel, + target_pixel_format, + target_bit_depth, + ); + } + } + + ImageData { + data: output_buffer, + pixel_format: target_pixel_format, + bit_depth: target_bit_depth, + stride: output_stride, + width: self.width, + height: self.height, + } + } + + pub fn create_surface(&self) -> VecSurface { + if self.pixel_format == PixelFormat::Bgra && self.bit_depth == BitDepth::Eight { + let mut surface = VecSurface::new(self.width, self.height); + assert_eq!(surface.stride() * 4, self.stride); + // TODO misaligned + surface.copy_from_slice(bytemuck::cast_slice(&self.data[..])); + surface + } else { + self.convert_color(PixelFormat::Bgra, BitDepth::Eight) + .create_surface() + } + } +} + +// TODO optimized scaling +pub fn scale_surface(source: &VecSurface, destination: &mut VecSurface) { + let scale_x = destination.width() as f32 / source.width() as f32; + let scale_y = destination.height() as f32 / source.height() as f32; + + let (source_scale_x, destination_scale_x, w) = if destination.width() > source.width() { + (1.0 / scale_x, 1.0, destination.width()) + } else { + (1.0, scale_x, source.width()) + }; + let (source_scale_y, destination_scale_y, h) = if destination.height() > source.height() { + (1.0 / scale_y, 1.0, destination.height()) + } else { + (1.0, scale_y, source.height()) + }; + + for y in 0..h { + for x in 0..w { + let sx = (source_scale_x * x as f32) as u32; + let sy = (source_scale_y * y as f32) as u32; + let dx = (destination_scale_x * x as f32) as u32; + let dy = (destination_scale_y * y as f32) as u32; + + destination.set_pixel(dx, dy, source.pixel(sx, sy)); + } + } +} diff --git a/userspace/graphics/iv/src/main.rs b/userspace/graphics/iv/src/main.rs new file mode 100644 index 00000000..630eb67c --- /dev/null +++ b/userspace/graphics/iv/src/main.rs @@ -0,0 +1,192 @@ +use std::{ + path::{Path, PathBuf}, + process::ExitCode, + sync::{Arc, RwLock}, +}; + +use clap::Parser; +use libcolors::{ + application::{ + window::{EventOutcome, Window}, + Application, + }, + event::KeyEvent, + geometry::{Point, Rectangle}, + input::Key, + surface::{MappedSurface, Surface, VecSurface}, +}; + +use crate::{error::Error, format::ImageData}; + +mod error; +mod format; + +#[derive(Debug, Parser)] +struct Args { + filename: PathBuf, +} + +struct State { + image_surface: VecSurface, + target_surface: VecSurface, + + scale: ScaleMethod, +} + +#[derive(Debug, Clone, Copy)] +pub enum ScaleMethod { + Fit, + Identity, + Maximize(u32), // x scaling + Minimize(u32), // 1/x scaling +} + +impl ScaleMethod { + pub fn bigger(self) -> ScaleMethod { + match self { + Self::Fit => ScaleMethod::Maximize(1), + Self::Identity => ScaleMethod::Maximize(2), + Self::Maximize(n) if n < 8 => Self::Maximize(n * 2), + Self::Minimize(2) => Self::Identity, + Self::Minimize(n) => Self::Minimize(n / 2), + _ => self, + } + } + + pub fn smaller(self) -> ScaleMethod { + match self { + Self::Fit => ScaleMethod::Identity, + Self::Identity => Self::Minimize(2), + Self::Maximize(2) => Self::Identity, + Self::Maximize(n) => Self::Maximize(n / 2), + Self::Minimize(n) if n < 8 => Self::Minimize(n * 2), + _ => self, + } + } +} + +impl State { + fn load_image>(path: P) -> Result { + let image = ImageData::load_image(path)?; + let image_surface = image.create_surface(); + let target_surface = image_surface.clone(); + Ok(Self { + image_surface, + target_surface, + scale: ScaleMethod::Fit, + }) + } + + fn handle_key(&mut self, key: KeyEvent) -> EventOutcome { + match key.key { + Key::Up => { + self.scale = self.scale.bigger(); + EventOutcome::Redraw + } + Key::Down => { + self.scale = self.scale.smaller(); + EventOutcome::Redraw + } + _ => EventOutcome::None, + } + } + + fn scale_to_aspect(&mut self, target_width: u32, target_height: u32) { + let target_aspect = self.image_surface.width() as f32 / self.image_surface.height() as f32; + + let w; + let h; + if (target_height as f32 * target_aspect) as u32 > target_width { + w = target_width; + h = (target_width as f32 / target_aspect) as u32; + } else { + w = (target_height as f32 * target_aspect) as u32; + h = target_height; + } + + if self.target_surface.width() == w && self.target_surface.height() == h { + return; + } + + let mut destination = VecSurface::new(w, h); + format::scale_surface(&self.image_surface, &mut destination); + self.target_surface = destination; + } + + fn redraw(&mut self, surface: &mut MappedSurface) { + // Scale to match aspect + let (target_width, target_height) = match self.scale { + ScaleMethod::Fit => (surface.width(), surface.height()), + ScaleMethod::Identity => (self.image_surface.width(), self.image_surface.height()), + ScaleMethod::Maximize(n) => ( + self.image_surface.width() * n, + self.image_surface.height() * n, + ), + ScaleMethod::Minimize(n) => ( + self.image_surface.width() / n, + self.image_surface.height() / n, + ), + }; + self.scale_to_aspect(target_width, target_height); + + let iw = self.target_surface.width(); + let ih = self.target_surface.height(); + let sw = surface.width(); + let sh = surface.height(); + + let center = surface.center(); + + let (off_x, w) = if iw > sw { + ((iw - sw) / 2, sw) + } else { + (0, iw) + }; + let (off_y, h) = if ih > sh { + ((ih - sh) / 2, sh) + } else { + (0, ih) + }; + + let origin = center - Point::new(w / 2, h / 2); + let rect = Rectangle { + x: off_x, + y: off_y, + w, + h, + }; + + surface.fill(0); + surface.copy_rect(&self.target_surface, rect, origin); + } +} + +fn run(args: Args) -> Result { + let state = State::load_image(args.filename)?; + let mut application = Application::new()?; + let mut window = Window::new(&application)?; + let state = Arc::new(RwLock::new(state)); + + let s = state.clone(); + window.set_on_redraw_requested(move |surface| { + s.write().unwrap().redraw(surface); + }); + let s = state.clone(); + window.set_on_key_pressed(move |key| s.write().unwrap().handle_key(key)); + + application.add_window(window); + + Ok(application.run()) +} + +fn main() -> ExitCode { + logsink::setup_logging(true); + let args = Args::parse(); + + match run(args) { + Ok(code) => code, + Err(error) => { + eprintln!("{error}"); + ExitCode::FAILURE + } + } +} diff --git a/userspace/lib/libcolors/src/application/mod.rs b/userspace/lib/libcolors/src/application/mod.rs index 67fab3a4..67146fa1 100644 --- a/userspace/lib/libcolors/src/application/mod.rs +++ b/userspace/lib/libcolors/src/application/mod.rs @@ -10,6 +10,7 @@ use std::{ use cross::signal::set_sigint_handler; use crate::{ + application::window::EventOutcome, error::Error, event::{Event, EventData, WindowEvent, WindowManagementEvent}, message::ClientMessage, @@ -68,6 +69,10 @@ impl Application { self.windows.insert(window.id(), window); } + pub fn remove_window(&mut self, window_id: u32) { + self.windows.remove(&window_id); + } + pub fn is_running(&self) -> bool { !EXIT_SIGNAL.load(Ordering::Acquire) } @@ -88,7 +93,9 @@ impl Application { } EventData::WindowEvent(window_id, ev) => { if let Some(window) = self.windows.get_mut(&window_id) { - window.handle_event(ev)?; + if window.handle_event(ev)? == EventOutcome::Destroy { + self.remove_window(window_id); + } } else { log::warn!("Unknown window ID received: {window_id}"); } diff --git a/userspace/lib/libcolors/src/application/window.rs b/userspace/lib/libcolors/src/application/window.rs index bc36446a..539ca15b 100644 --- a/userspace/lib/libcolors/src/application/window.rs +++ b/userspace/lib/libcolors/src/application/window.rs @@ -192,3 +192,13 @@ impl Window { Ok(()) } } + +impl Drop for Window { + fn drop(&mut self) { + self.connection + .lock() + .unwrap() + .send(&ClientMessage::DestroyWindow(self.window_id)) + .ok(); + } +} diff --git a/userspace/lib/libcolors/src/geometry.rs b/userspace/lib/libcolors/src/geometry.rs index 6c0ef2e8..0609381f 100644 --- a/userspace/lib/libcolors/src/geometry.rs +++ b/userspace/lib/libcolors/src/geometry.rs @@ -7,6 +7,7 @@ pub trait Cast { fn cast(self) -> T; } +#[derive(Debug)] pub struct Rectangle { pub x: T, pub y: T, @@ -14,6 +15,7 @@ pub struct Rectangle { pub h: T, } +#[derive(Debug, Clone, Copy)] pub struct Point { pub x: T, pub y: T, @@ -34,6 +36,17 @@ impl> Cast> for Point { } } +impl> Sub> for Point { + type Output = Point; + + fn sub(self, rhs: Point) -> Self::Output { + Point { + x: self.x - rhs.x, + y: self.y - rhs.y, + } + } +} + impl Rectangle where T: Copy, @@ -141,6 +154,7 @@ impl_primitive_cast!( u32 => u16, u32 => u64, u32 => usize, + u32 => i64, u64 => u8, u64 => u16, diff --git a/userspace/lib/libcolors/src/surface.rs b/userspace/lib/libcolors/src/surface.rs index 38864b89..7a8f17f8 100644 --- a/userspace/lib/libcolors/src/surface.rs +++ b/userspace/lib/libcolors/src/surface.rs @@ -19,6 +19,19 @@ pub trait Surface: DerefMut { fn height(&self) -> u32; fn resize(&mut self, width: u32, height: u32) -> Result<(), Error>; + fn rectangle(&self) -> Rectangle { + Rectangle { + x: 0, + y: 0, + w: self.width(), + h: self.height(), + } + } + + fn center(&self) -> Point { + Point::new(self.width() / 2, self.height() / 2) + } + fn clip_rectangle(&self, input: Rectangle) -> Option> { input.intersection(&Rectangle { x: 0, @@ -54,20 +67,20 @@ pub trait Surface: DerefMut { source_rectangle.h, ))?; let source_rectangle = foreground.clip_rectangle(source_rectangle)?; - let clip_rectangle = source_rectangle.intersection(&destination_rectangle)?; + let clip_w = source_rectangle.w.min(destination_rectangle.w); + let clip_h = source_rectangle.h.min(destination_rectangle.h); - for y in 0..clip_rectangle.h { + for y in 0..clip_h { let source_i = (y + source_rectangle.y) as usize * source_stride + source_rectangle.x as usize; let destination_i = (y + destination_rectangle.y) as usize * destination_stride - + destination_rectangle.y as usize; - let source_row = &foreground[source_i..source_i + clip_rectangle.w as usize]; - let destination_row = - &mut self[destination_i..destination_i + clip_rectangle.w as usize]; + + destination_rectangle.x as usize; + let source_row = &foreground[source_i..source_i + clip_w as usize]; + let destination_row = &mut self[destination_i..destination_i + clip_w as usize]; blend_row(destination_row, source_row); } - Some(clip_rectangle) + Some(Rectangle::from_origin_size(destination, clip_w, clip_h)) } fn fill_rect(&mut self, rectangle: Rectangle, color: u32) { @@ -86,6 +99,11 @@ pub trait Surface: DerefMut { let i = y as usize * self.stride() + x as usize; self[i] = color; } + + fn pixel(&self, x: u32, y: u32) -> u32 { + let i = y as usize * self.stride() + x as usize; + self[i] + } } pub struct MappedSurface { @@ -96,6 +114,7 @@ pub struct MappedSurface { len: usize, } +#[derive(Clone)] pub struct VecSurface { data: Vec, width: u32, diff --git a/userspace/lib/pixie/Cargo.toml b/userspace/lib/pixie/Cargo.toml new file mode 100644 index 00000000..303f1b0d --- /dev/null +++ b/userspace/lib/pixie/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pixie" +version = "0.1.0" +edition = "2021" + +[dependencies] +bytemuck.workspace = true +thiserror.workspace = true +log.workspace = true + +[lints] +workspace = true diff --git a/userspace/lib/pixie/src/image.rs b/userspace/lib/pixie/src/image.rs new file mode 100644 index 00000000..f26889a3 --- /dev/null +++ b/userspace/lib/pixie/src/image.rs @@ -0,0 +1,75 @@ +pub struct RgbImage { + pub data: Vec, + pub stride: usize, + pub width: usize, + pub height: usize, +} + +impl RgbImage { + pub fn get(&self, x: usize, y: usize) -> Option<(u8, u8, u8)> { + if x >= self.width || y >= self.height { + return None; + } + let i = y * self.stride + x * 3; + let rgb = &self.data[i..]; + Some((rgb[0], rgb[1], rgb[2])) + } +} + +pub struct YCbCrImage { + pub luma: Vec, + pub chroma_b: Vec, + pub chroma_r: Vec, + pub width: usize, + pub height: usize, +} + +impl YCbCrImage { + pub fn to_rgb(&self) -> RgbImage { + ycbcr_to_rgb(self) + } +} + +pub fn ycbcr_to_rgb(ycbcr: &YCbCrImage) -> RgbImage { + let mut rgb = vec![]; + for i in 0..ycbcr.luma.len() { + let y = ycbcr.luma[i] as f64; + let cb = ycbcr.chroma_b[i] as f64 - 128.0; + let cr = ycbcr.chroma_r[i] as f64 - 128.0; + + let r = y + 1.40200 * cr; + let g = y - 0.34414 * cb - 0.71414 * cr; + let b = y + 1.77200 * cb; + + rgb.push(r.clamp(0.0, 255.0) as u8); + rgb.push(g.clamp(0.0, 255.0) as u8); + rgb.push(b.clamp(0.0, 255.0) as u8); + } + + RgbImage { + data: rgb, + stride: 3 * ycbcr.width, + width: ycbcr.width, + height: ycbcr.height, + } +} + +#[cfg(test)] +mod tests { + use crate::{rgb_to_ycbcr, RgbImageRef}; + + #[test] + fn test_rgb_to_ycbcr() { + let rgb = [86, 86, 0]; + let rgb = RgbImageRef { + data: &rgb, + stride: 3, + width: 1, + height: 1, + }; + let ycbcr = rgb_to_ycbcr(&rgb); + assert_eq!(&ycbcr.luma, &[76]); + assert_eq!(&ycbcr.chroma_b, &[85]); + assert_eq!(&ycbcr.chroma_r, &[134]); + } +} diff --git a/userspace/lib/pixie/src/jpeg/data/block.rs b/userspace/lib/pixie/src/jpeg/data/block.rs new file mode 100644 index 00000000..566e3dd0 --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/data/block.rs @@ -0,0 +1,84 @@ +use std::fmt; + +use crate::jpeg::data::dct; + +#[repr(align(16))] +#[derive(Clone, Copy)] +struct BlockAlign([u8; 0]); + +#[derive(Clone, Copy)] +pub struct Block8x8 { + pub data: [T; 64], + _align: BlockAlign, +} + +impl Block8x8 { + pub const fn new(data: [T; 64]) -> Self { + Self { + data, + _align: BlockAlign([]), + } + } + + pub fn map U>(self, f: F) -> Block8x8 { + Block8x8::new(self.data.map(f)) + } +} + +impl Block8x8 { + pub fn splat(v: T) -> Self { + Self { + data: [v; 64], + _align: BlockAlign([]), + } + } + + #[inline] + pub fn get(&self, x: usize, y: usize) -> T { + self.data[y * 8 + x] + } + + pub fn unzigzag(from: &[T; 64]) -> Self { + let mut output = [from[0]; 64]; + super::unzigzag64(from, &mut output); + Self::new(output) + } +} + +impl From> for Vec { + fn from(value: Block8x8) -> Self { + value.data.into() + } +} + +impl Block8x8 { + #[inline] + pub fn idct(&self) -> Block8x8 { + dct::idct_8x8(self) + } +} + +impl fmt::Debug for Block8x8 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut outer = f.debug_list(); + for y in 0..8 { + outer.entry(&&self.data[y * 8..y * 8 + 8]); + } + outer.finish() + } +} + +impl fmt::Display for Block8x8 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for y in 0..8 { + for x in 0..8 { + if x != 0 { + write!(f, " ")?; + } + fmt::Display::fmt(&self.data[y * 8 + x], f)?; + } + writeln!(f)?; + } + Ok(()) + } +} diff --git a/userspace/lib/pixie/src/jpeg/data/copy.rs b/userspace/lib/pixie/src/jpeg/data/copy.rs new file mode 100644 index 00000000..d96ab127 --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/data/copy.rs @@ -0,0 +1,73 @@ +use crate::jpeg::data::Block8x8; + +pub fn write_block_1x1( + output: &mut [T], + output_stride: usize, + dx: usize, + dy: usize, + block: &Block8x8, +) { + for by in 0..8 { + let block_row = &block.data[by * 8..by * 8 + 8]; + let output_offset = (dy + by) * output_stride + dx; + if output_offset + 8 > output.len() { + break; + } + output[output_offset..output_offset + 8].copy_from_slice(block_row); + } +} + +pub fn write_block_1x2( + output: &mut [T], + output_stride: usize, + dx: usize, + dy: usize, + block: &Block8x8, +) { + for by in 0..16 { + let block_row = &block.data[by / 2 * 8..by / 2 * 8 + 8]; + let output_offset = (dy + by) * output_stride + dx; + if output_offset + 8 > output.len() { + break; + } + output[output_offset..output_offset + 8].copy_from_slice(block_row); + } +} + +pub fn write_block_2x1( + output: &mut [T], + output_stride: usize, + dx: usize, + dy: usize, + block: &Block8x8, +) { + for by in 0..8 { + let block_row = &block.data[by * 8..by * 8 + 8]; + for bx in 0..16 { + let output_offset = (dy + by) * output_stride + (dx + bx); + if output_offset >= output.len() { + break; + } + output[output_offset] = block_row[bx / 2]; + } + } +} + +pub fn write_block_2x2( + output: &mut [T], + output_stride: usize, + dx: usize, + dy: usize, + block: &Block8x8, +) { + for by in 0..16 { + let block_row = &block.data[by / 2 * 8..by / 2 * 8 + 8]; + for bx in 0..16 { + let output_offset = (dy + by) * output_stride + (dx + bx); + if output_offset >= output.len() { + break; + } + output[output_offset] = block_row[bx / 2]; + } + } +} diff --git a/userspace/lib/pixie/src/jpeg/data/dct.rs b/userspace/lib/pixie/src/jpeg/data/dct.rs new file mode 100644 index 00000000..a496c3cb --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/data/dct.rs @@ -0,0 +1,48 @@ +use crate::jpeg::data::Block8x8; + +// Table of precomputed cosine coefficients for DCT/IDCT +#[rustfmt::skip] +#[allow(clippy::excessive_precision, clippy::approx_constant)] +const COSINE_TABLE: [f64; 8 * 8] = [ + 1.0000000000000000, 0.9807852804032304, 0.9238795325112867, 0.8314696123025452, 0.7071067811865476, 0.5555702330196023, 0.3826834323650898, 0.1950903220161283, + 1.0000000000000000, 0.8314696123025452, 0.3826834323650898, -0.1950903220161282, -0.7071067811865475, -0.9807852804032304, -0.9238795325112868, -0.5555702330196022, + 1.0000000000000000, 0.5555702330196023, -0.3826834323650897, -0.9807852804032304, -0.7071067811865477, 0.1950903220161283, 0.9238795325112865, 0.8314696123025455, + 1.0000000000000000, 0.1950903220161283, -0.9238795325112867, -0.5555702330196022, 0.7071067811865474, 0.8314696123025455, -0.3826834323650899, -0.9807852804032307, + 1.0000000000000000, -0.1950903220161282, -0.9238795325112868, 0.5555702330196018, 0.7071067811865477, -0.8314696123025451, -0.3826834323650906, 0.9807852804032304, + 1.0000000000000000, -0.5555702330196020, -0.3826834323650903, 0.9807852804032304, -0.7071067811865467, -0.1950903220161280, 0.9238795325112867, -0.8314696123025450, + 1.0000000000000000, -0.8314696123025453, 0.3826834323650900, 0.1950903220161288, -0.7071067811865471, 0.9807852804032307, -0.9238795325112864, 0.5555702330196015, + 1.0000000000000000, -0.9807852804032304, 0.9238795325112865, -0.8314696123025451, 0.7071067811865466, -0.5555702330196015, 0.3826834323650896, -0.1950903220161286, +]; + +#[inline] +fn c_uv(u: usize, v: usize) -> f64 { + if u + v == 0 { + 0.5 + } else { + 1.0 + } +} + +// TODO optimize IDCT +fn partial_idct_8x8(block: &Block8x8, x: usize, y: usize) -> f64 { + let mut sum = 0.0; + for v in 0..8 { + for u in 0..8 { + let c_uv = c_uv(u, v); + let f_uv = block.get(u, v) as f64; + sum += c_uv * f_uv * COSINE_TABLE[x * 8 + u] * COSINE_TABLE[y * 8 + v]; + } + } + sum * 0.25 +} + +pub fn idct_8x8(input: &Block8x8) -> Block8x8 { + let mut block = Block8x8::splat(0.0); + for y in 0..8 { + for x in 0..8 { + let f = partial_idct_8x8(input, x, y); + block.data[y * 8 + x] = f + 128.0; + } + } + block +} diff --git a/userspace/lib/pixie/src/jpeg/data/mod.rs b/userspace/lib/pixie/src/jpeg/data/mod.rs new file mode 100644 index 00000000..60050a51 --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/data/mod.rs @@ -0,0 +1,8 @@ +mod block; +mod copy; +mod dct; +mod zigzag; + +pub use block::Block8x8; +pub use copy::{write_block_1x1, write_block_1x2, write_block_2x1, write_block_2x2}; +pub use zigzag::unzigzag64; diff --git a/userspace/lib/pixie/src/jpeg/data/zigzag.rs b/userspace/lib/pixie/src/jpeg/data/zigzag.rs new file mode 100644 index 00000000..2b95dd1c --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/data/zigzag.rs @@ -0,0 +1,41 @@ +#[rustfmt::skip] +const UNZIGZAG_TABLE: [usize; 64] = [ + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63, +]; + +// TODO optimize +pub fn unzigzag64(input: &[T; 64], output: &mut [T; 64]) { + for i in 0..64 { + output[i] = input[UNZIGZAG_TABLE[i]]; + } +} + +#[cfg(test)] +mod tests { + use crate::data::unzigzag64; + + #[test] + fn test_unzigzag64() { + let mut input = [0; 64]; + let mut output = [0; 64]; + for (i, input) in input.iter_mut().enumerate() { + *input = i + 1; + } + unzigzag64(&input, &mut output); + assert_eq!( + &output[..], + &[ + 1, 2, 6, 7, 15, 16, 28, 29, 3, 5, 8, 14, 17, 27, 30, 43, 4, 9, 13, 18, 26, 31, 42, + 44, 10, 12, 19, 25, 32, 41, 45, 54, 11, 20, 24, 33, 40, 46, 53, 55, 21, 23, 34, 39, + 47, 52, 56, 61, 22, 35, 38, 48, 51, 57, 60, 62, 36, 37, 49, 50, 58, 59, 63, 64 + ] + ); + } +} diff --git a/userspace/lib/pixie/src/jpeg/decoder.rs b/userspace/lib/pixie/src/jpeg/decoder.rs new file mode 100644 index 00000000..76baabd1 --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/decoder.rs @@ -0,0 +1,576 @@ +use std::{collections::HashMap, io::Read}; + +use bytemuck::Pod; + +use crate::jpeg::{ + data::{self, Block8x8}, + error::Error, + header::App0Header, + huffman::{HuffmanDecoder, HuffmanTable}, +}; +use crate::{RgbImage, YCbCrImage}; + +pub const MAX_COMPONENTS: usize = 4; +pub const COMPONENT_NAMES: &[&str] = &["luma", "chroma_b", "chroma_r", ""]; + +#[derive(Debug, Default)] +struct JpegState { + frame_info: Option, + quantization_tables: [Option>; MAX_COMPONENTS], + scan_info: Option, + dc_huffman_tables: HashMap, + ac_huffman_tables: HashMap, + headers_parsed: bool, + end_of_image: bool, + is_progressive: bool, +} + +pub struct JpegDecoder { + reader: R, + state: JpegState, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SamplingMode { + Sample1x1, + Sample1x2, + Sample2x1, + Sample2x2, +} + +#[derive(Debug, Clone, Copy)] +pub struct FrameComponent { + pub sampling: SamplingMode, + pub qt_index: usize, +} + +#[derive(Debug)] +pub struct FrameInfo { + pub precision: u8, + pub components: [Option; MAX_COMPONENTS], + pub lines: usize, + pub samples_per_line: usize, +} + +#[derive(Debug)] +pub struct ScanInfo { + pub dc_entropy_selectors: [u8; MAX_COMPONENTS], + pub ac_entropy_selectors: [u8; MAX_COMPONENTS], + pub spectral_predict_start: u8, + pub spectral_predict_end: u8, + pub successive_approximation: u8, + pub component_mask: u8, +} + +impl SamplingMode { + pub fn vertical(&self) -> usize { + match self { + Self::Sample1x1 | Self::Sample2x1 => 1, + _ => 2, + } + } + + pub fn horizontal(&self) -> usize { + match self { + Self::Sample1x1 | Self::Sample1x2 => 1, + _ => 2, + } + } + + pub fn sample_count(&self) -> usize { + match self { + Self::Sample1x1 => 1, + Self::Sample1x2 | Self::Sample2x1 => 2, + Self::Sample2x2 => 4, + } + } +} + +impl JpegDecoder { + pub fn new(reader: R) -> Self { + Self { + reader, + state: JpegState::default(), + } + } + + pub fn decode_headers(&mut self) -> Result<(), Error> { + self.state.decode_headers(&mut self.reader) + } + + pub fn decode_ycbcr(&mut self) -> Result { + self.state.decode_frame(&mut self.reader) + } + + pub fn decode_rgb(&mut self) -> Result { + let ycbcr = self.decode_ycbcr()?; + Ok(ycbcr.to_rgb()) + } +} + +impl JpegState { + fn do_decode_headers(&mut self, reader: &mut R) -> Result<(), Error> { + // SOI marker + let soi_marker = read_u16be(reader)?; + if soi_marker != 0xFFD8 { + return Err(Error::MalformedHeader); + } + + let mut last_byte = 0; + + loop { + let mut m = read_u8(reader)?; + if last_byte == 0xFF && (m == 0xFF || m == 0x00) { + while m == 0xFF || m == 0x00 { + last_byte = m; + m = read_u8(reader)?; + } + } + if last_byte == 0xFF { + self.consume_marker(reader, m)?; + + if self.scan_info.is_some() || self.end_of_image { + break; + } + } + last_byte = m; + } + + Ok(()) + } + + fn decode_headers(&mut self, reader: &mut R) -> Result<(), Error> { + if self.headers_parsed { + return Ok(()); + } + self.do_decode_headers(reader) + } + + fn decode_frame(&mut self, reader: &mut R) -> Result { + self.decode_headers(reader)?; + if self.is_progressive { + Err(Error::UnimplementedFeature("Progressive image decoding")) + } else { + decode_ycbcr_baseline(reader, self) + } + } + + fn consume_marker(&mut self, reader: &mut R, m: u8) -> Result<(), Error> { + match m { + 0xC0..=0xC3 => self.consume_sof(reader, m - 0xC0), + 0xC4 => self.consume_dht(reader), + 0xE0 => self.consume_app0(reader), + 0xD9 => { + self.end_of_image = true; + Ok(()) + } + 0xDA => self.consume_sos(reader), + 0xDB => self.consume_dqt(reader), + _ => Err(Error::InvalidMarker(m)), + } + } + + fn consume_sof(&mut self, reader: &mut R, i: u8) -> Result<(), Error> { + self.is_progressive = i >= 2; + if self.frame_info.is_some() { + return Err(Error::MultipleSofMarkers); + } + let length = read_u16be(reader)?; + let sample_precision = read_u8(reader)?; + let line_count = read_u16be(reader)?; + let samples_per_line = read_u16be(reader)?; + let component_count = read_u8(reader)?; + // TODO parse non-8bpp images + if sample_precision != 8 { + return Err(Error::UnimplementedSamplePrecision(sample_precision)); + } + if length as usize != 8 + 3 * component_count as usize { + return Err(Error::MalformedHeader); + } + if component_count != 3 { + return Err(Error::UnimplementedComponentCount(component_count)); + } + log::debug!("SOF {sample_precision}bpp {samples_per_line}x{line_count}"); + let mut frame_info = FrameInfo { + precision: sample_precision, + samples_per_line: samples_per_line as usize, + lines: line_count as usize, + components: [const { None }; MAX_COMPONENTS], + }; + for _ in 0..component_count { + let component_id = read_u8(reader)? as usize; + if component_id > MAX_COMPONENTS || component_id == 0 { + return Err(Error::InvalidComponentIndex(component_id)); + } + let component_id = component_id - 1; + if frame_info.components[component_id].is_some() { + return Err(Error::DuplicateComponent(component_id)); + } + let sampling_factor = read_u8(reader)?; + let sampling = match sampling_factor { + 0x11 => SamplingMode::Sample1x1, + 0x21 => SamplingMode::Sample2x1, + 0x12 => SamplingMode::Sample1x2, + 0x22 => SamplingMode::Sample2x2, + _ => { + let hsampling = sampling_factor >> 4; + let vsampling = sampling_factor & 0xF; + return Err(Error::UnimplementedSamplingFactor( + COMPONENT_NAMES[component_id], + hsampling, + vsampling, + )); + } + }; + let qt_index = read_u8(reader)? as usize; + frame_info.components[component_id] = Some(FrameComponent { sampling, qt_index }); + log::debug!(" [{component_id}] {sampling:?}, qt {qt_index}"); + } + self.frame_info = Some(frame_info); + Ok(()) + } + + fn consume_dht(&mut self, reader: &mut R) -> Result<(), Error> { + let mut length = read_u16be(reader)?.saturating_sub(2) as usize; + let mut huffman_code_counts = [0; 16]; + let mut huffman_code_values = [0; 256]; + + while length > 16 { + let ht_info = read_u8(reader)?; + let dc_or_ac = ht_info >> 4; + let index = ht_info & 0x0F; + reader.read_exact(&mut huffman_code_counts)?; + let total_value_count: usize = + huffman_code_counts.into_iter().map(|c| c as usize).sum(); + length -= 17; + if total_value_count > 256 || total_value_count > length { + return Err(Error::InvalidHuffmanTable); + } + reader.read_exact(&mut huffman_code_values[..total_value_count])?; + length -= total_value_count; + match dc_or_ac { + 0 => { + log::debug!( + "DC[{index}] {huffman_code_counts:?}, {:?}", + &huffman_code_values[..total_value_count] + ); + let table = HuffmanTable::new( + huffman_code_counts, + huffman_code_values, + true, + self.is_progressive, + )?; + self.dc_huffman_tables.insert(index, table); + } + 1 => { + log::debug!( + "AC[{index}] {huffman_code_counts:?}, {:?}", + &huffman_code_values[..total_value_count] + ); + let table = HuffmanTable::new( + huffman_code_counts, + huffman_code_values, + false, + self.is_progressive, + )?; + self.ac_huffman_tables.insert(index, table); + } + _ => { + return Err(Error::InvalidHuffmanTable); + } + } + } + + if length > 0 { + return Err(Error::InvalidHuffmanTable); + } + Ok(()) + } + + fn consume_dqt(&mut self, reader: &mut R) -> Result<(), Error> { + let mut length = read_u16be(reader)?.saturating_sub(2) as usize; + while length != 0 { + let qt_param = read_u8(reader)?; + let qt_precision = (qt_param >> 4) as usize; + let qt_index = (qt_param & 0x0F) as usize; + if qt_index >= MAX_COMPONENTS { + return Err(Error::InvalidQtIndex(qt_index)); + } + if self.quantization_tables[qt_index].is_some() { + return Err(Error::DuplicateQt(qt_index)); + } + let qt_bytes_per_element = 1 << qt_precision; + if qt_bytes_per_element * 64 + 1 > length { + return Err(Error::MalformedHeader); + } + // Read 64 elements of the quantization table + let qt_block = match qt_bytes_per_element { + 1 => { + let mut qt_values = [0; 64]; + reader.read_exact(&mut qt_values)?; + Block8x8::new(qt_values).map(|x| x as u16) + } + 2 => { + let mut qt_values = [0u16; 64]; + reader.read_exact(bytemuck::cast_slice_mut(&mut qt_values))?; + Block8x8::new(qt_values) + } + _ => return Err(Error::InvalidQtPrecision(qt_bytes_per_element)), + }; + + log::debug!("QT[{qt_index}]: {qt_bytes_per_element}Bpe\n{qt_block:3}"); + + self.quantization_tables[qt_index] = Some(qt_block); + length -= 1 + qt_bytes_per_element * 64; + } + Ok(()) + } + + fn consume_app0(&mut self, reader: &mut R) -> Result<(), Error> { + let app0: App0Header = read_struct(reader)?; + if app0.identifier != *b"JFIF\x00" { + return Err(Error::MalformedHeader); + } + assert_eq!(app0.xthumbnail, 0); + assert_eq!(app0.ythumbnail, 0); + assert_eq!(app0.length.read() as usize, size_of::()); + + // TODO skip thumbnail bytes + Ok(()) + } + + fn consume_sos(&mut self, reader: &mut R) -> Result<(), Error> { + let _length = read_u16be(reader)?.saturating_sub(2) as usize; + let mut scan = ScanInfo { + dc_entropy_selectors: [0; MAX_COMPONENTS], + ac_entropy_selectors: [0; MAX_COMPONENTS], + spectral_predict_start: 0, + spectral_predict_end: 0, + successive_approximation: 0, + component_mask: 0, + }; + let component_count = read_u8(reader)? as usize; + for _ in 0..component_count { + let component_id = read_u8(reader)? as usize; + let entropy_dst_selector = read_u8(reader)?; + if component_id > MAX_COMPONENTS || component_id == 0 { + return Err(Error::InvalidComponentIndex(component_id)); + } + let component_id = component_id - 1; + let dc_selector = entropy_dst_selector >> 4; + let ac_selector = entropy_dst_selector & 0x0F; + scan.dc_entropy_selectors[component_id] = dc_selector; + scan.ac_entropy_selectors[component_id] = ac_selector; + scan.component_mask |= 1 << component_id; + } + scan.spectral_predict_start = read_u8(reader)?; + scan.spectral_predict_end = read_u8(reader)?; + scan.successive_approximation = read_u8(reader)?; + self.scan_info = Some(scan); + self.headers_parsed = true; + Ok(()) + } +} + +pub fn read_u8(reader: &mut R) -> Result { + let mut buf = [0; 1]; + reader.read_exact(&mut buf)?; + Ok(buf[0]) +} + +pub fn read_u16be(reader: &mut R) -> Result { + let mut buf = [0; 2]; + reader.read_exact(&mut buf)?; + Ok(u16::from_be_bytes(buf)) +} + +pub fn read_struct(reader: &mut R) -> Result { + let mut value = T::zeroed(); + reader.read_exact(bytemuck::bytes_of_mut(&mut value))?; + Ok(value) +} + +fn expand_number(code: u8, bits: u16) -> i16 { + if code == 0 { + return 0; + } + let l = 1 << (code - 1); + if bits >= l { + bits as i16 + } else { + bits as i16 - ((1i16 << code) - 1) + } +} + +fn decode_block( + reader: &mut R, + huffman: &mut HuffmanDecoder, + dc_table: &HuffmanTable, + ac_table: &HuffmanTable, + quant_table: &Block8x8, + dc_predictor: &mut i16, +) -> Result, Error> { + let mut block_data = [0; 64]; + + let mut l = 1; + + // Read the DC coefficient + let code = huffman.decode_symbol(reader, dc_table)?; + let bits = huffman.get_bits(reader, code & 0x0F)?; + let dccoeff = expand_number(code, bits).wrapping_add(*dc_predictor); + *dc_predictor = dccoeff; + block_data[0] = dccoeff.wrapping_mul(quant_table.data[0] as i16); + + while l < 64 { + let code = huffman.decode_symbol(reader, ac_table)?; + if code == 0 { + break; + } + + let code = if code > 15 { + l += (code >> 4) as usize; + code & 0x0F + } else { + code + }; + + let bits = huffman.get_bits(reader, code)?; + + if l < 64 { + let coeff = expand_number(code, bits); + block_data[l] = coeff.wrapping_mul(quant_table.data[l] as i16); + l += 1; + } + } + + let block = Block8x8::unzigzag(&block_data); + let block = block.idct(); + let block = block.map(|f| f as u8); + Ok(block) +} + +fn decode_ycbcr_baseline( + reader: &mut R, + state: &mut JpegState, +) -> Result { + let frame_info = state.frame_info.as_ref().unwrap(); + let scan_info = state.scan_info.as_ref().unwrap(); + let component_count = scan_info.component_mask.count_ones() as usize; + + let components = [ + frame_info.components[0].as_ref().unwrap(), + frame_info.components[1].as_ref().unwrap(), + frame_info.components[2].as_ref().unwrap(), + ]; + + let max_hsample = components + .iter() + .map(|c| c.sampling.horizontal()) + .max() + .unwrap(); + let max_vsample = components + .iter() + .map(|c| c.sampling.vertical()) + .max() + .unwrap(); + + let component_len = frame_info.samples_per_line * frame_info.lines; + let mut component_data = [ + vec![0; component_len], + vec![0; component_len], + vec![0; component_len], + ]; + + let mut huffman = HuffmanDecoder::default(); + + let mut dc_predictors = [0; MAX_COMPONENTS]; + + let mcu_width_pixels = 8 * max_hsample; + let mcu_height_pixels = 8 * max_vsample; + + let mcu_columns = frame_info.samples_per_line.div_ceil(mcu_width_pixels); + let mcu_rows = frame_info.lines.div_ceil(mcu_height_pixels); + + 'outer: for mcu_y in 0..mcu_rows { + for mcu_x in 0..mcu_columns { + for component_id in 0..component_count { + // TODO move this to block decode + if let Some(marker) = huffman.marker { + match marker { + 0xD9 => { + break 'outer; + } + _ => todo!(), + } + } + + let hsamples = components[component_id].sampling.horizontal(); + let vsamples = components[component_id].sampling.vertical(); + let dc_index = scan_info.dc_entropy_selectors[component_id]; + let ac_index = scan_info.ac_entropy_selectors[component_id]; + let qt_index = frame_info.components[component_id] + .as_ref() + .unwrap() + .qt_index; + let dc_table = state + .dc_huffman_tables + .get(&dc_index) + .ok_or(Error::MissingDCHT(dc_index))?; + let ac_table = state + .ac_huffman_tables + .get(&ac_index) + .ok_or(Error::MissingACHT(ac_index))?; + let quant_table = state.quantization_tables[qt_index] + .as_ref() + .ok_or(Error::MissingQT(qt_index))?; + + let upsample_y = max_vsample / vsamples; + let upsample_x = max_hsample / hsamples; + let output = &mut component_data[component_id]; + + for vsample in 0..vsamples { + for hsample in 0..hsamples { + let block = decode_block( + reader, + &mut huffman, + dc_table, + ac_table, + quant_table, + &mut dc_predictors[component_id], + )?; + let dst_block_x = (mcu_x * max_hsample + hsample) * 8; + let dst_block_y = (mcu_y * max_vsample + vsample) * 8; + + let write_fn = match (upsample_x, upsample_y) { + (1, 1) => data::write_block_1x1, + (1, 2) => data::write_block_1x2, + (2, 1) => data::write_block_2x1, + (2, 2) => data::write_block_2x2, + _ => unreachable!(), + }; + + write_fn( + output, + frame_info.samples_per_line, + dst_block_x, + dst_block_y, + &block, + ); + } + } + } + } + } + + let [y, cb, cr] = component_data; + + let ycbcr = YCbCrImage { + luma: y, + chroma_b: cb, + chroma_r: cr, + width: frame_info.samples_per_line, + height: frame_info.lines, + }; + + Ok(ycbcr) +} diff --git a/userspace/lib/pixie/src/jpeg/error.rs b/userspace/lib/pixie/src/jpeg/error.rs new file mode 100644 index 00000000..2741e937 --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/error.rs @@ -0,0 +1,44 @@ +use std::io; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{0}")] + Io(#[from] io::Error), + #[error("Unhandled marker byte: {0:#02x}")] + InvalidMarker(u8), + #[error("Malformed JPEG/JFIF header")] + MalformedHeader, + #[error("Multiple SOF markers within one image")] + MultipleSofMarkers, + #[error("Duplicate quantization table [{0}]")] + DuplicateQt(usize), + #[error("Invalid quantization table index [{0}]")] + InvalidQtIndex(usize), + #[error("Duplicate component [{0}]")] + DuplicateComponent(usize), + #[error("Invalid component index [{0}]")] + InvalidComponentIndex(usize), + #[error("Malformed Huffman bit sequence: {0:016b}")] + MalformedHuffmanCode(u16), + #[error("Invalid huffman table")] + InvalidHuffmanTable, + #[error("Invalid quantization table precision: {0}-byte")] + InvalidQtPrecision(usize), + + #[error("Missing DC Huffman table [{0}]")] + MissingDCHT(u8), + #[error("Missing AC Huffman table [{0}]")] + MissingACHT(u8), + #[error("Missing quantization table [{0}]")] + MissingQT(usize), + + // Unimplemented features + #[error("Unsupported sample precision: {0} bits/sample")] + UnimplementedSamplePrecision(u8), + #[error("Unsupported {0} sampling factor: {1}x{2}")] + UnimplementedSamplingFactor(&'static str, u8, u8), + #[error("Unsupported component count: {0}")] + UnimplementedComponentCount(u8), + #[error("{0} is not supported")] + UnimplementedFeature(&'static str), +} diff --git a/userspace/lib/pixie/src/jpeg/header.rs b/userspace/lib/pixie/src/jpeg/header.rs new file mode 100644 index 00000000..fb0ff6c1 --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/header.rs @@ -0,0 +1,36 @@ +use std::marker::PhantomData; + +use bytemuck::{Pod, Zeroable}; + +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(transparent)] +pub struct BigEndian([u8; size_of::()], PhantomData) +where + [u8; size_of::()]: Sized + Zeroable + Pod; + +#[derive(Debug, Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct App0Header { + pub length: BigEndian, + pub identifier: [u8; 5], + pub version: BigEndian, + pub units: u8, + pub xdensity: BigEndian, + pub ydensity: BigEndian, + pub xthumbnail: u8, + pub ythumbnail: u8, +} + +macro_rules! impl_big_endian { + ($($ty:ty),+) => { + $( + impl BigEndian<$ty> { + pub fn read(&self) -> $ty { + <$ty>::from_be_bytes(self.0) + } + } + )+ + }; +} + +impl_big_endian!(u16, u32); diff --git a/userspace/lib/pixie/src/jpeg/huffman.rs b/userspace/lib/pixie/src/jpeg/huffman.rs new file mode 100644 index 00000000..00611997 --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/huffman.rs @@ -0,0 +1,253 @@ +use std::{io::Read, iter}; + +use crate::{jpeg::decoder::read_u8, jpeg::error::Error}; + +pub const LUT_BITS: u8 = 9; + +#[derive(Debug, Clone, Copy)] +pub struct LutEntry { + pub symbol: u8, + pub length: u8, +} + +#[derive(Debug)] +struct Lut(Box<[LutEntry; 1 << BITS]>) +where + [LutEntry; 1 << BITS]: Sized; + +#[derive(Debug)] +pub struct HuffmanTable { + pub(crate) bits: [u8; 16], + pub(crate) huffval: [u8; 256], + + lut: Lut<{ LUT_BITS as usize }>, +} + +impl Lut +where + [Option; 1 << BITS]: Sized, +{ + pub fn new() -> Self { + Self(Box::new( + [LutEntry { + length: 0, + symbol: 0, + }; 1 << BITS], + )) + } +} + +#[derive(Default)] +pub struct HuffmanDecoder { + buffer: u64, + bit_count: usize, + pub marker: Option, +} + +impl HuffmanTable { + pub fn new( + counts: [u8; 16], + values: [u8; 256], + is_dc: bool, + is_progressive: bool, + ) -> Result { + // TODO handle progressive decoding + _ = is_dc; + _ = is_progressive; + + // Build huffman code table + let (huffcode, huffsize) = Self::derive_huffman_codes(&counts)?; + let mut lut = Lut::new(); + + for (i, &size) in huffsize + .iter() + .enumerate() + .filter(|&(_, &size)| size <= LUT_BITS) + { + let extra_bits = LUT_BITS - size; + let start = (huffcode[i] as usize) << extra_bits; + + for j in 0..1 << extra_bits { + lut.0[start + j] = LutEntry { + symbol: values[i], + length: size, + }; + } + } + + let mut p = 0; + let mut si = huffsize[0]; + let mut code = 0; + let mut maxcode = [0; 18]; + while p < huffsize.len() && huffsize[p] != 0 { + while p < huffsize.len() && huffsize[p] == si { + code += 1; + p += 1; + } + maxcode[si as usize] = code << (16 - si); + + code <<= 1; + si += 1; + } + + p = 0; + let mut offset = [0; 18]; + for l in 0..16 { + if counts[l] == 0 { + maxcode[l] = -1; + } else { + offset[l] = (p as i32) - (huffcode[p] as i32); + p += usize::from(counts[l]); + } + } + + offset[17] = 0; + offset[17] = 0x000F_FFFF; + + Ok(Self { + lut, + bits: counts, + huffval: values, + }) + } + + #[inline] + pub fn lookup(&self, index: usize) -> LutEntry { + self.lut.0[index] + } + + fn derive_huffman_codes(counts: &[u8; 16]) -> Result<(Vec, Vec), Error> { + let huffsize = counts + .iter() + .enumerate() + .fold(Vec::new(), |mut acc, (i, &value)| { + acc.extend(iter::repeat_n((i + 1) as u8, value as usize)); + acc + }); + let mut huffcode = vec![0u16; huffsize.len()]; + let mut code_size = huffsize[0]; + let mut code = 0; + + for (i, &size) in huffsize.iter().enumerate() { + while code_size < size { + code <<= 1; + code_size += 1; + } + + if code >= 1 << size { + return Err(Error::InvalidHuffmanTable); + } + + huffcode[i] = code as u16; + code += 1; + } + + Ok((huffcode, huffsize)) + } +} + +impl HuffmanDecoder { + pub fn decode_symbol( + &mut self, + reader: &mut R, + table: &HuffmanTable, + ) -> Result { + if self.bit_count < 16 { + self.read_bits(reader)?; + } + + let bits = self.peek_bits(LUT_BITS); + let lut_entry = table.lookup(bits as usize); + if lut_entry.length != 0 { + self.consume_bits(lut_entry.length); + return Ok(lut_entry.symbol); + } + + let mut code = 0; + let mut first = 0; + let mut index = 0; + for len in 1..=16 { + code = (code << 1) | self.get_bits(reader, 1)?; + let count = table.bits[len - 1] as usize; + if code as usize >= first && code as usize - first < count { + let symbol = table.huffval[index + (code as usize - first)]; + return Ok(symbol); + } + index += count; + first = (first + count) << 1; + } + + Err(Error::MalformedHuffmanCode(code)) + } + + fn consume_bits(&mut self, count: u8) { + self.buffer <<= count as usize; + self.bit_count -= count as usize; + } + + pub fn get_bits(&mut self, reader: &mut R, count: u8) -> Result { + if self.bit_count < 16 { + self.read_bits(reader)?; + } + let bits = self.peek_bits(count); + self.consume_bits(count); + Ok(bits) + } + + fn peek_bits(&mut self, count: u8) -> u16 { + assert!(count <= 16); + assert!(self.bit_count >= count as usize); + ((self.buffer.wrapping_shr(64 - count as u32)) & ((1 << count) - 1)) as u16 + } + + fn read_bits(&mut self, reader: &mut R) -> Result<(), Error> { + while self.bit_count <= 56 { + let byte = match self.marker { + Some(_) => 0, + None => read_u8(reader)?, + }; + + if byte == 0xFF { + let mut next_byte = read_u8(reader)?; + + if next_byte != 0x00 { + while next_byte == 0xFF { + next_byte = read_u8(reader)?; + } + + match next_byte { + 0x00 => todo!(), + _ => self.marker = Some(next_byte), + } + + continue; + } + } + + self.buffer |= (byte as u64) << (56 - self.bit_count); + self.bit_count += 8; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::jpeg::huffman::HuffmanDecoder; + + #[test] + fn test_huffman_bit_read() { + let mut huffman = HuffmanDecoder::default(); + let bitstring = b"\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78"; + let mut reader = &bitstring[..]; + let bits = huffman.get_bits(&mut reader, 3).unwrap(); + assert_eq!(bits, 0); + let bits = huffman.get_bits(&mut reader, 2).unwrap(); + assert_eq!(bits, 0b10); + let bits = huffman.get_bits(&mut reader, 2).unwrap(); + assert_eq!(bits, 0b01); + let bits = huffman.get_bits(&mut reader, 1).unwrap(); + assert_eq!(bits, 0); + } +} diff --git a/userspace/lib/pixie/src/jpeg/mod.rs b/userspace/lib/pixie/src/jpeg/mod.rs new file mode 100644 index 00000000..de2547ef --- /dev/null +++ b/userspace/lib/pixie/src/jpeg/mod.rs @@ -0,0 +1,7 @@ +pub mod data; +pub mod decoder; +pub mod error; +pub mod header; +pub mod huffman; + +pub use error::Error; diff --git a/userspace/lib/pixie/src/lib.rs b/userspace/lib/pixie/src/lib.rs new file mode 100644 index 00000000..861c6385 --- /dev/null +++ b/userspace/lib/pixie/src/lib.rs @@ -0,0 +1,8 @@ +#![feature(generic_const_exprs)] +#![allow(incomplete_features)] + +pub mod image; +pub mod jpeg; + +pub use image::{RgbImage, YCbCrImage}; +pub use jpeg::decoder::JpegDecoder; diff --git a/xtask/src/build/userspace.rs b/xtask/src/build/userspace.rs index 216d20a5..dc221aa5 100644 --- a/xtask/src/build/userspace.rs +++ b/xtask/src/build/userspace.rs @@ -76,6 +76,7 @@ const PROGRAMS: &[(&str, &str)] = &[ ("colors", "bin/colors"), ("colors-bar", "bin/colors-bar"), ("term", "bin/term"), + ("iv", "bin/iv"), // red ("red", "bin/red"), // rdb