diff --git a/Cargo.lock b/Cargo.lock index 03adeee3..ebb564db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1060,6 +1060,7 @@ dependencies = [ "png", "rayon", "smallvec", + "tiff 0.10.0", "wide", ] @@ -1555,7 +1556,7 @@ dependencies = [ "ravif", "rayon", "rgb", - "tiff", + "tiff 0.9.1", "zune-core", "zune-jpeg", ] @@ -3378,6 +3379,19 @@ dependencies = [ "weezl", ] +[[package]] +name = "tiff" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd35f8bd40ae1e2b51e86f7a665f81160dc82b785f5de1f193a52f1298a76586" +dependencies = [ + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "tiny-skia" version = "0.11.4" diff --git a/canvas/src/color.rs b/canvas/src/color.rs index ba07f904..b59fd87c 100644 --- a/canvas/src/color.rs +++ b/canvas/src/color.rs @@ -6,7 +6,8 @@ use hsluv::*; #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] pub enum ColorFormat { Rgba, - Hsluv + Hsluv, + Cmyka } /// @@ -15,7 +16,8 @@ pub enum ColorFormat { #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum Color { Rgba(f32, f32, f32, f32), - Hsluv(f32, f32, f32, f32) + Hsluv(f32, f32, f32, f32), + Cmyka(f32, f32, f32, f32, f32) } impl PartialEq for Color { @@ -40,11 +42,15 @@ impl Color { pub fn to_rgba_components(&self) -> (f32, f32, f32, f32) { match self { &Color::Rgba(r, g, b, a) => (r, g, b, a), - + &Color::Hsluv(h, s, l, a) => { let (r, g, b) = hsluv_to_rgb((h as f64, s as f64, l as f64)); (r as f32, g as f32, b as f32, a) } + &Color::Cmyka(c, m, y, k, a) => { + todo!("to_rgba_components"); + (c, m, y, a) + } } } @@ -60,6 +66,24 @@ impl Color { let s = if l <= 0.0 { 100.0 } else { s }; (h as f32, s as f32, l as f32, a) } + &Color::Cmyka(c, m, y, k, a) => { + todo!("to_hsluv_components"); + (c, m, y, a) + } + } + } + + + /// + /// Returns this colour as CMYKA components + /// + pub fn to_cmyka_components(&self) -> (f32, f32, f32, f32, f32) { + match self { + &Color::Rgba(r, g, b, a) => { todo!("to_cmyka_components") } + &Color::Hsluv(h, s, l, a) => { todo!("to_cmyka_components") } + &Color::Cmyka(c, m, y, k, a) => { + (c, m, y, k, a) + } } } @@ -70,6 +94,7 @@ impl Color { match self { Color::Hsluv(_, _, _, a) => *a, Color::Rgba(_, _, _, a) => *a, + Color::Cmyka(_, _, _, _, a) => *a, } } @@ -87,6 +112,11 @@ impl Color { let s = if l <= 0.0 { 100.0 } else { s }; Color::Hsluv(h as f32, s as f32, l as f32, a) } + ColorFormat::Cmyka => match self { + &Color::Rgba(x, y, z, a) => Color::Cmyka(x, y, z, 0.0, a), + &Color::Hsluv(x, y, z, a) => Color::Cmyka(x, y, z, 0.0, a), + &Color::Cmyka(_, _, _, _, _) => self.clone() + } } } @@ -96,7 +126,8 @@ impl Color { pub fn with_alpha(&self, new_alpha: f32) -> Color { match self { &Color::Rgba(r, g, b, _) => Color::Rgba(r, g, b, new_alpha), - &Color::Hsluv(h, s, l, _) => Color::Hsluv(h, s, l, new_alpha) + &Color::Hsluv(h, s, l, _) => Color::Hsluv(h, s, l, new_alpha), + &Color::Cmyka(c, m, y, k, _) => Color::Cmyka(c, m, y, k, new_alpha) } } } diff --git a/render_software/Cargo.toml b/render_software/Cargo.toml index eccca7a9..139973b9 100644 --- a/render_software/Cargo.toml +++ b/render_software/Cargo.toml @@ -30,3 +30,4 @@ rayon = { version = "1.7", optional = true } [dev-dependencies] flo_canvas = { version = "0.4", features = ["image-loading", "outline-fonts"] } futures = "0.3" +tiff = "0.10" diff --git a/render_software/examples/cmyk_software_filters.rs b/render_software/examples/cmyk_software_filters.rs new file mode 100644 index 00000000..aaafd3a8 --- /dev/null +++ b/render_software/examples/cmyk_software_filters.rs @@ -0,0 +1,150 @@ +use flo_render_software::canvas::*; +use flo_render_software::render::*; + +use flo_render_software::draw::{CanvasDrawing, CanvasDrawingRegionRenderer}; +use flo_render_software::pixel::{F32CmykaPixel, F32LinearPixel, Pixel, U8RgbaPremultipliedPixel}; +use flo_render_software::scanplan::ShardScanPlanner; +use std::f32; +use std::f32::consts::PI; +use std::fs::File; +use std::ops::{Add, Div, Mul}; +use tiff::encoder::TiffEncoder; + +fn draw_circles, const N: usize>(drawing: &mut CanvasDrawing, circles: Vec<(f32, Color)>) { + + let mut sprite = vec![]; + sprite.sprite(SpriteId(1)); + for (rotate, color) in circles.iter() { + let x = rotate.mul(PI).cos().mul(200.0); + let y = rotate.mul(PI).sin().mul(200.0); + sprite.new_path(); + sprite.circle(x, y, 250.0); + sprite.fill_color(color.clone()); + sprite.fill(); + } + sprite.layer(LayerId(0)); + + // Create drawing instructions for the png + let mut canvas = vec![]; + + // Clear the canvas and set up the coordinates + canvas.canvas_height(1000.0); + canvas.center_region(0.0, 0.0, 2000.0, 1000.0); + + canvas.layer(LayerId(0)); + canvas.clear_layer(); + + drawing.draw(canvas); + + drawing.draw(sprite.iter().cloned()); + + for part in 0..2 { + let max_scale = 0.5; + let min_scale = 0.1; + let iters = 100; + let div = iters as f32; + let cycles = 1.66; + let radius = 200.0; + for i in 0..iters { + let mut draw = vec![]; + draw.layer(LayerId(i + 1)); + let i = i as f32; + let scale = max_scale - (max_scale - min_scale).mul(i.div(div).powi(2)); + let r = 200.0 + (radius / div) * i; + let rot = PI.div(div).mul(cycles).mul(i).mul(2.0); + let x = 500.0 + 1000.0 * part as f32 + rot.cos().mul(r); + let y = 500.0 + rot.sin().mul(r); + // println!("{scale} | {rot} | {r} | {x}, {y} "); + draw.sprite_transform(SpriteTransform::Identity); + draw.sprite_transform(SpriteTransform::Scale(scale, scale)); + draw.sprite_transform(SpriteTransform::Rotate(rot.mul(1.66).to_degrees())); + draw.sprite_transform(SpriteTransform::Translate(x, y)); + match part { + 0 => draw.draw_sprite(SpriteId(1)), + _ => draw.draw_sprite_with_filters(SpriteId(1), vec![TextureFilter::GaussianBlur(20.0 + 30.0 * (1.0 - i / div).powi(2))]), + }; + drawing.draw(draw); + } + + } + +} + +/// +/// Draws a bunch of circles in CMYK colorspace +/// +pub fn main() { + + let alpha = 0.7; + let width = 2000; + let height = 1000; + + { + + // Draw circles on a CMYK tiff + + let mut canvas_drawing = CanvasDrawing::::empty(); + canvas_drawing.draw(vec![Draw::ClearCanvas(Color::Cmyka(0.0, 0.0, 0.0, 0.0, 0.0))]); + + let circles = vec![ + (0.0, Color::Cmyka(1.0, 0.0, 0.0, 0.0, alpha)), + (0.5, Color::Cmyka(0.0, 1.0, 0.0, 0.0, alpha)), + (1.0, Color::Cmyka(0.0, 0.0, 1.0, 0.0, alpha)), + (1.5, Color::Cmyka(0.0, 0.0, 0.0, 1.0, alpha)), + ]; + draw_circles(&mut canvas_drawing, circles); + + let renderer = CanvasDrawingRegionRenderer::new( + ShardScanPlanner::default(), ScanlineRenderer::new(canvas_drawing.program_runner(height as _)), + height + ); + + let renderer = F32CmykaFrameRenderer::new(renderer); + let frame_size = GammaFrameSize { width, height, gamma: 2.2 }; + let mut pixel_data = vec![F32CmykaPixel::default(); width * height]; + + renderer.render(&frame_size, &canvas_drawing, pixel_data.as_mut_slice()); + + let file = File::create("circles_cmyk.tiff").unwrap(); + let mut tiff_enc = TiffEncoder::new_big(file).unwrap(); + let mut img_enc = tiff_enc.new_image::(width as u32, height as u32).unwrap(); + let es: &[u8] = &[2u8]; + img_enc.encoder().write_tag(tiff::tags::Tag::ExtraSamples, es).unwrap(); + + let pixel_data = pixel_data.iter().map(|p| p.to_u8()).flatten().collect::>(); + img_enc.write_data(&pixel_data).unwrap(); + + } + + #[cfg(feature="render_png")] + { + + // Draw same? circles in RGB for comparison + + let mut canvas_drawing = CanvasDrawing::::empty(); + canvas_drawing.draw(vec![Draw::ClearCanvas(Color::Rgba(0.0, 0.0, 0.0, 0.0))]); + + let circles = vec![ + (0.0, Color::Rgba(0.0, 1.0, 1.0, alpha)), + (0.5, Color::Rgba(1.0, 0.0, 1.0, alpha)), + (1.0, Color::Rgba(1.0, 1.0, 0.0, alpha)), + (1.5, Color::Rgba(0.0, 0.0, 0.0, alpha)), + ]; + draw_circles(&mut canvas_drawing, circles); + + let renderer = CanvasDrawingRegionRenderer::new( + ShardScanPlanner::default(), ScanlineRenderer::new(canvas_drawing.program_runner(height as _)), + height + ); + + let mut png_data: Vec = vec![]; + { + let mut png_render = PngRenderTarget::from_stream(&mut png_data, width, height, 2.2); + png_render.render(renderer, &canvas_drawing); + } + + std::fs::write("circles_rgb.png", png_data).unwrap(); + + } + +} diff --git a/render_software/examples/cmyk_software_gradient.rs b/render_software/examples/cmyk_software_gradient.rs new file mode 100644 index 00000000..401fae7f --- /dev/null +++ b/render_software/examples/cmyk_software_gradient.rs @@ -0,0 +1,96 @@ +use flo_render_software::render::*; +use flo_render_software::canvas::*; + +use std::f32; +use std::fs::File; +use std::path::PathBuf; +use flo_render_software::draw::{CanvasDrawing, CanvasDrawingRegionRenderer}; +use flo_render_software::pixel::{F32CmykaPixel, F32LinearPixel, Pixel, ToRgbaPremultipliedPixels}; +use flo_render_software::scanplan::ShardScanPlanner; +use tiff::encoder::TiffEncoder; + +/// +/// Draws a simple linear gradient +/// +pub fn main() { + // Create drawing instructions for the png + let mut canvas = vec![]; + + let angle = (30.0 / 360.0) * (2.0 * f32::consts::PI); + + // Clear the canvas and set up the coordinates + canvas.clear_canvas(Color::Cmyka(0.0, 0.0, 0.0, 0.0, 1.0)); + canvas.canvas_height(1000.0); + canvas.center_region(0.0, 0.0, 1000.0, 1000.0); + + canvas.layer(LayerId(0)); + canvas.clear_layer(); + + // Set up the canvas + canvas.canvas_height(1000.0); + canvas.center_region(0.0, 0.0, 1000.0, 1000.0); + + // Set up a gradient + canvas.create_gradient(GradientId(1), Color::Cmyka(0.8, 0.0, 0.0, 0.0, 1.0)); + canvas.gradient_stop(GradientId(1), 0.33, Color::Cmyka(0.3, 0.8, 0.0, 0.0, 1.0)); + canvas.gradient_stop(GradientId(1), 0.66, Color::Cmyka(0.0, 0.3, 0.8, 0.9, 1.0)); + canvas.gradient_stop(GradientId(1), 1.0, Color::Cmyka(0.6, 0.3, 0.9, 0.0, 1.0)); + + let x1 = 500.0 - 300.0 * f32::cos(angle); + let y1 = 500.0 - 300.0 * f32::sin(angle); + let x2 = 500.0 + 300.0 * f32::cos(angle); + let y2 = 500.0 + 300.0 * f32::sin(angle); + + // Draw a circle using the gradient + canvas.new_path(); + canvas.circle(500.0, 500.0, 250.0); + canvas.fill_gradient(GradientId(1), x1, y1, x2, y2); + canvas.fill(); + + canvas.line_width(4.0); + canvas.stroke_color(Color::Cmyka(0.0, 0.0, 0.0, 1.0, 1.0)); + canvas.stroke(); + + // Draw indicators where the gradient is moving between + canvas.line_width(1.0); + + canvas.new_path(); + canvas.circle(x1, y1, 8.0); + canvas.stroke(); + + canvas.new_path(); + canvas.circle(x2, y2, 8.0); + canvas.stroke(); + + // Render to the terminal window + // render_drawing(&mut TerminalRenderTarget::new(1920, 1080), canvas.iter().cloned()); + + let mut canvas_drawing = CanvasDrawing::::empty(); + canvas_drawing.draw(canvas); + + let width = 1920; + let height = 1080; + + let renderer = CanvasDrawingRegionRenderer::new(ShardScanPlanner::default(), ScanlineRenderer::new(canvas_drawing.program_runner(height as _)), height); + + let renderer = F32CmykaFrameRenderer::new(renderer); + let frame_size = GammaFrameSize { width, height, gamma: 2.2 }; + let mut pixel_data = vec![F32CmykaPixel::default(); width * height]; + + renderer.render(&frame_size, &canvas_drawing, pixel_data.as_mut_slice()); + println!("wat {}", pixel_data.iter().flat_map(|c| c.to_cmyk()).reduce(f32::max).unwrap()); + + let file = File::create("cmyk_software_gradient.tiff").unwrap(); + let mut tiff_enc = TiffEncoder::new_big(file).unwrap(); + let mut img_enc = tiff_enc.new_image::(width as u32, height as u32).unwrap(); + let es: &[u8] = &[2u8]; + img_enc.encoder().write_tag(tiff::tags::Tag::ExtraSamples, es).unwrap(); + + let pixel_data = pixel_data.iter().map(|p| p.to_u8()).flatten().collect::>(); + img_enc.write_data(&pixel_data).unwrap(); + + // render_drawing(&mut renderer, canvas.iter().clone()) + + // Send the buffer to the png file + // self.writer.write_image_data(&pixel_data).unwrap(); +} diff --git a/render_software/src/pixel/f32_cmyka.rs b/render_software/src/pixel/f32_cmyka.rs new file mode 100644 index 00000000..00f989d2 --- /dev/null +++ b/render_software/src/pixel/f32_cmyka.rs @@ -0,0 +1,234 @@ +use super::alpha_blend_trait::*; +use super::to_gamma_colorspace_trait::*; +use super::to_linear_colorspace_trait::*; +use super::pixel_trait::*; +use super::u8_rgba::*; +use super::u16_rgba::*; +use super::u32_argb::*; +use super::gamma_lut::*; + +use flo_canvas as canvas; + +use wide::*; +use once_cell::sync::{Lazy}; + +use std::cell::{RefCell}; +use std::collections::{HashMap}; +use std::ops::*; +use std::sync::*; + +/// +/// A pixel using linear floating-point components, with the alpha value pre-multiplied +/// +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct F32CmykaPixel(f32x4, f32); + +impl F32CmykaPixel { + #[inline] + pub fn to_cmyk(&self) -> [f32; 4] { + self.0.to_array() + } + #[inline] + pub fn to_u8(&self) -> [u8; 5] { + let a = self.1; + let [c, m, y, k] = *self.0.div(a).as_array_ref(); + [c, m, y, k, a].map(|f| (f * 255.0).round() as u8) + } +} + +impl Default for F32CmykaPixel { + #[inline] + fn default() -> Self { + F32CmykaPixel(f32x4::splat(0.0), 0.0) + } +} + +impl Pixel<5> for F32CmykaPixel { + #[inline] + fn black() -> F32CmykaPixel { + F32CmykaPixel(f32x4::new([0.0, 0.0, 0.0, 1.0]), 1.0) + } + + #[inline] + fn white() -> F32CmykaPixel { + F32CmykaPixel(f32x4::new([0.0, 0.0, 0.0, 0.0]), 1.0) + } + + #[inline] + fn from_components(components: [Self::Component; 5]) -> Self { + let [c, m, y, k, a] = components; + F32CmykaPixel(f32x4::new([c, m, y, k]), a) + } + + #[inline] + fn to_components(&self) -> [Self::Component; 5] { + let mut arr = [0.0; 5]; + arr[0..4].copy_from_slice(self.0.as_array_ref()); + arr[4] = self.1; + arr + } + + #[inline] + fn get(&self, component: usize) -> Self::Component { + match component { + 4 => self.1, + _ => self.0.as_array_ref()[component] + } + } + + #[inline] + fn from_color(color: canvas::Color, _gamma: f64) -> Self { + let (c, m, y, k, a) = color.to_cmyka_components(); + let cmyk = f32x4::new([c, m, y, k]); + F32CmykaPixel(cmyk * a, a) + } + + #[inline] + fn to_color(&self, _gamma: f64) -> canvas::Color { + let a = self.1; + let [c, m, y, k] = *self.0.div(a).as_array_ref(); + canvas::Color::Cmyka(c, m, y, k, a) + } +} + +// Lookup tables for gamma values +// static GAMMA_LUT: Lazy>>> = Lazy::new(|| Mutex::new(HashMap::new())); + +impl ToGammaColorSpace for F32CmykaPixel { + fn to_gamma_colorspace(input_pixels: &[F32CmykaPixel], output_pixels: &mut [U8RgbaPremultipliedPixel], gamma: f64) { + todo!("idk") + } +} + +impl ToGammaColorSpace for F32CmykaPixel { + fn to_gamma_colorspace(input_pixels: &[F32CmykaPixel], output_pixels: &mut [U32ArgbPremultipliedPixel], gamma: f64) { + todo!("idk") + } +} + +impl ToLinearColorSpace for F32CmykaPixel { + fn to_linear_colorspace(input_pixels: &[Self], output_pixels: &mut [U16LinearPixel]) { + todo!("idk") + } +} + +impl AlphaBlend for F32CmykaPixel { + type Component = f32; + + #[inline] + fn alpha_blend_with_function(self, dest: Self, source_alpha_fn: AlphaFunction, dest_alpha_fn: AlphaFunction) -> Self { + let src_alpha = self.alpha_component(); + let dst_alpha = dest.alpha_component(); + + source_alpha_fn.apply(self, src_alpha, dst_alpha) + dest_alpha_fn.apply(dest, src_alpha, dst_alpha) + } + + #[inline] + fn alpha_component(&self) -> Self::Component { + self.1 + } + + #[inline] + fn multiply_alpha(self, factor: f64) -> Self { + self * (factor as f32) + } + + #[inline] fn source_over(self, dest: Self) -> Self { let src_alpha = self.1; self + dest * (1.0-src_alpha) } + #[inline] fn dest_over(self, dest: Self) -> Self { let dst_alpha = dest.1; self * (1.0-dst_alpha) + dest } + #[inline] fn source_in(self, dest: Self) -> Self { let dst_alpha = dest.1; self * dst_alpha } + #[inline] fn dest_in(self, dest: Self) -> Self { let src_alpha = self.1; dest * src_alpha } + #[inline] fn source_held_out(self, dest: Self) -> Self { let dst_alpha = dest.1; self*(1.0-dst_alpha) } + #[inline] fn dest_held_out(self, dest: Self) -> Self { let src_alpha = self.1; dest*(1.0-src_alpha) } + #[inline] fn source_atop(self, dest: Self) -> Self { self.alpha_blend(dest, AlphaOperation::SourceAtop) } + #[inline] fn dest_atop(self, dest: Self) -> Self { self.alpha_blend(dest, AlphaOperation::DestAtop) } +} + +impl Add for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn add(self, val: F32CmykaPixel) -> F32CmykaPixel { + F32CmykaPixel(self.0 + val.0, self.1 + val.1) + } +} + +impl Add for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn add(self, val: f32) -> F32CmykaPixel { + F32CmykaPixel(self.0 + val, self.1 + val) + } +} + +impl Sub for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn sub(self, val: F32CmykaPixel) -> F32CmykaPixel { + F32CmykaPixel(self.0 - val.0, self.1 - val.1) + } +} + +impl Sub for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn sub(self, val: f32) -> F32CmykaPixel { + F32CmykaPixel(self.0 - val, self.1 - val) + } +} + +impl Mul for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn mul(self, val: F32CmykaPixel) -> F32CmykaPixel { + F32CmykaPixel(self.0 * val.0, self.1 * val.1) + } +} + +impl Mul for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn mul(self, val: f32) -> F32CmykaPixel { + F32CmykaPixel(self.0 * val, self.1 * val) + } +} + +impl Div for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn div(self, val: F32CmykaPixel) -> F32CmykaPixel { + F32CmykaPixel(self.0 / val.0, self.1 / val.1) + } +} + +impl Div for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn div(self, val: f32) -> F32CmykaPixel { + F32CmykaPixel(self.0 / val, self.1 / val) + } +} + +impl Neg for F32CmykaPixel { + type Output= F32CmykaPixel; + + #[inline] + fn neg(self) -> F32CmykaPixel { + F32CmykaPixel(-self.0, -self.1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + } +} diff --git a/render_software/src/pixel/f32_cmyka_texture_reader.rs b/render_software/src/pixel/f32_cmyka_texture_reader.rs new file mode 100644 index 00000000..1af0022e --- /dev/null +++ b/render_software/src/pixel/f32_cmyka_texture_reader.rs @@ -0,0 +1,89 @@ +use super::Pixel; +use super::f32_cmyka::*; +use super::rgba_texture::*; +use super::texture_reader::*; +use super::u16_linear_texture::*; + +use once_cell::sync::{Lazy}; + +/// +/// Table that maps values with the 8 upper bits representing the alpha value and the 8 lower bits representing the colour value +/// to their premultiplied-alpha equivalents +/// +static TO_PREMULTIPLIED_LINEAR_WITH_ALPHA: Lazy<[f32; 65536]> = Lazy::new(|| { + let mut table = [0.0; 65536]; + + for a in 0..256 { + // Convert the alpha value to f64 (these are always linear) + let alpha = (a as f64)/255.0; + + for c in 0..256 { + // Gamma correct the value and pre-multiply it + let val = (c as f64)/255.0; + let val = val.powf(2.2); + let val = val * alpha; + + // Store in the table + let table_pos = (a<<8) | c; + table[table_pos] = val as f32; + } + } + + table +}); + +impl TextureReader for F32CmykaPixel { + #[inline] + fn texture_size(texture: &RgbaTexture) -> (f64, f64) { + (texture.width() as _, texture.height() as _) + } + + #[inline] + fn read_pixels(texture: &RgbaTexture, positions: &[(f64, f64)]) -> Vec { + // Pre-allocate the space for the pixels + let mut pixels = Vec::with_capacity(positions.len()); + + // Read the pixel from the texture + let u8pixels = texture.read_pixels(positions.iter().map(|(x, y)| (*x as i64, *y as i64))); + + // Convert to F32CmykaPixels + pixels.extend(u8pixels.map(|[r, g, b, a]| { + // Use the 2.2 gamma conversion table to convert to a F32 pixel (we assume non-premultiplied RGBA pixels with a gamma of 2.2) + let alpha = (*a as usize) << 8; + let ri = (*r as usize) | alpha; + let gi = (*g as usize) | alpha; + let bi = (*b as usize) | alpha; + + let rf = unsafe { *(*TO_PREMULTIPLIED_LINEAR_WITH_ALPHA).get_unchecked(ri) }; + let gf = unsafe { *(*TO_PREMULTIPLIED_LINEAR_WITH_ALPHA).get_unchecked(gi) }; + let bf = unsafe { *(*TO_PREMULTIPLIED_LINEAR_WITH_ALPHA).get_unchecked(bi) }; + let af = (*a as f32) / 255.0; + + // We should always have enough space to write the pixels here + F32CmykaPixel::from_components([rf, gf, bf, 0.0, af]) + })); + + pixels + } +} + +impl TextureReader for F32CmykaPixel { + #[inline] + fn texture_size(texture: &U16LinearTexture) -> (f64, f64) { + (texture.width() as _, texture.height() as _) + } + + #[inline] + fn read_pixels(texture: &U16LinearTexture, positions: &[(f64, f64)]) -> Vec { + // Pre-allocate the space for the pixels + let mut pixels = Vec::with_capacity(positions.len()); + + // Read the pixel from the texture + let u16pixels = texture.read_pixels(positions.iter().map(|(x, y)| (*x as i64, *y as i64))); + + // Convert to F32CmykaPixels + pixels.extend(u16pixels.map(|[r, g, b, a]| F32CmykaPixel::from_components([*r as f32/65535.0, *g as f32/65535.0, *b as f32/65535.0, 0.0, *a as f32/65535.0]))); + + pixels + } +} diff --git a/render_software/src/pixel/mod.rs b/render_software/src/pixel/mod.rs index 76e08729..7b612536 100644 --- a/render_software/src/pixel/mod.rs +++ b/render_software/src/pixel/mod.rs @@ -9,6 +9,8 @@ mod u16_rgba; mod u32_argb; mod f32_linear; mod f32_linear_texture_reader; +mod f32_cmyka; +mod f32_cmyka_texture_reader; mod u32_linear; mod u32_linear_texture_reader; mod pixel_program; @@ -27,6 +29,7 @@ pub use to_linear_colorspace_trait::*; pub use u8_rgba::*; pub use u32_argb::*; pub use f32_linear::*; +pub use f32_cmyka::*; pub use u32_linear::*; pub use pixel_program::*; pub use pixel_program_cache::*; diff --git a/render_software/src/render/f32_cmyka_frame_renderer.rs b/render_software/src/render/f32_cmyka_frame_renderer.rs new file mode 100644 index 00000000..790af083 --- /dev/null +++ b/render_software/src/render/f32_cmyka_frame_renderer.rs @@ -0,0 +1,135 @@ +use super::renderer::*; +use super::render_slice::*; +use super::frame_size::*; + +use crate::pixel::*; + +use std::marker::{PhantomData}; +use std::sync::*; + +type TPixel = F32CmykaPixel; + +/// +/// Renders a whole frame of pixels to a RGBA U8 buffer (using TPixel as the intermediate format) +/// +pub struct F32CmykaFrameRenderer +where + // TPixel: Sized + Send + Default + F32CmykaPixel, // ToGammaColorSpace, + TRegionRenderer: Renderer, +{ + region_renderer: TRegionRenderer, + pixel: PhantomData>, +} + +impl F32CmykaFrameRenderer +where + // TPixel: Sized + Send + Clone + Default + ToGammaColorSpace, + TRegionRenderer: Renderer, +{ + /// + /// Creates a new frame renderer + /// + /// Use a gamma value of 2.2 for most rendering tasks (this is the default used by most operating systems) + /// + pub fn new(region_renderer: TRegionRenderer) -> Self { + Self { + region_renderer: region_renderer, + pixel: PhantomData, + } + } +} + +#[cfg(not(feature="multithreading"))] +impl<'a, TPixel, TRegionRenderer> Renderer for F32CmykaFrameRenderer +where + TPixel: Sized + Send + Clone + Default + ToGammaColorSpace, + TRegionRenderer: Renderer, +{ + type Region = GammaFrameSize; + type Source = TRegionRenderer::Source; + type Dest = [U8RgbaPremultipliedPixel]; + + fn render(&self, region: &GammaFrameSize, source: &TRegionRenderer::Source, dest: &mut [U8RgbaPremultipliedPixel]) { + const LINES_AT_ONCE: usize = 8; + + // Rendering fails if there are insufficient lines to complete + if dest.len() < region.width * region.height { + panic!("Cannot render: needed an output buffer large enough to fit {} lines but found {} lines", region.height, dest.len()/region.width); + } + + // Cut the destination into chunks to form the lines + let chunks = dest.chunks_mut(region.width*LINES_AT_ONCE); + let renderer = &self.region_renderer; + + // Render in chunks of LINES_AT_ONCE lines + let mut render_slice = RenderSlice { width: region.width, y_positions: vec![] }; + let mut buffer = vec![TPixel::default(); region.width*LINES_AT_ONCE]; + + chunks.enumerate().map(|(chunk_idx, chunk)| { + let start_y = chunk_idx * LINES_AT_ONCE; + let end_y = if start_y + LINES_AT_ONCE > region.height { region.height } else { start_y + LINES_AT_ONCE }; + + (start_y..end_y, chunk_idx, chunk) + }).for_each(|(y_positions, _chunk_idx, chunk)| { + // Write the y positions + render_slice.y_positions.clear(); + render_slice.y_positions.extend(y_positions.map(|idx| idx as f64)); + + // Render these lines + renderer.render(&render_slice, source, &mut buffer); + + // Convert to the final pixel format + TPixel::to_gamma_colorspace(&buffer, chunk, region.gamma); + }); + } +} + +#[cfg(feature="multithreading")] +impl<'a, TRegionRenderer> Renderer for F32CmykaFrameRenderer +where + // TPixel: Sized + Send + Clone + Default + ToGammaColorSpace, + TRegionRenderer: Renderer, +{ + type Region = GammaFrameSize; + type Source = TRegionRenderer::Source; + type Dest = [F32CmykaPixel]; + + fn render(&self, region: &GammaFrameSize, source: &TRegionRenderer::Source, dest: &mut [F32CmykaPixel]) { + const LINES_AT_ONCE: usize = 8; + + use rayon::prelude::*; + + // Rendering fails if there are insufficient lines to complete + if dest.len() < region.width * region.height { + panic!("Cannot render: needed an output buffer large enough to fit {} lines but found {} lines", region.height, dest.len()/region.width); + } + + // Cut the destination into chunks to form the lines + let chunks = dest.par_chunks_mut(region.width*LINES_AT_ONCE); + let renderer = &self.region_renderer; + + // Render in chunks of LINES_AT_ONCE lines + chunks.enumerate().map(|(chunk_idx, chunk)| { + let start_y = chunk_idx * LINES_AT_ONCE; + let end_y = if start_y + LINES_AT_ONCE > region.height { region.height } else { start_y + LINES_AT_ONCE }; + + (start_y..end_y, chunk_idx, chunk) + }).for_each_init(|| { + let render_slice = RenderSlice { width: region.width, y_positions: vec![] }; + let buffer = vec![TPixel::default(); region.width*LINES_AT_ONCE]; + + (render_slice, buffer) + }, + |(ref mut render_slice, ref mut buffer), (y_positions, _chunk_idx, chunk)| { + // Write the y positions + render_slice.y_positions.clear(); + render_slice.y_positions.extend(y_positions.map(|idx| idx as f64)); + + // Render these lines + renderer.render(render_slice, source, chunk); + + // Convert to the final pixel format + // TPixel::to_gamma_colorspace(&buffer, chunk, region.gamma); + }); + } +} diff --git a/render_software/src/render/mod.rs b/render_software/src/render/mod.rs index c5b08ca3..8139bdde 100644 --- a/render_software/src/render/mod.rs +++ b/render_software/src/render/mod.rs @@ -7,6 +7,7 @@ mod frame_size; mod edgeplan_region_renderer; mod edge_plan; mod u8_frame_renderer; +mod f32_cmyka_frame_renderer; mod u32_frame_renderer; mod u16_linear_frame_renderer; mod rgba_frame; @@ -24,6 +25,7 @@ pub use edgeplan_region_renderer::*; pub use u8_frame_renderer::*; pub use u32_frame_renderer::*; pub use u16_linear_frame_renderer::*; +pub use f32_cmyka_frame_renderer::*; pub use rgba_frame::*; pub use render_frame::*; pub use image_render::*;