From 8e18a0880e8a77a3957d9571f481092c67af0b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 29 Apr 2025 14:53:49 +0200 Subject: [PATCH 01/13] :recycle: Use a single byte to store gradient stop count (wasm) --- frontend/src/app/render_wasm/serializers/fills.cljs | 2 +- render-wasm/src/shapes/fills.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index f4f395b66e..bef0c35220 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -25,7 +25,7 @@ (.setFloat32 dview (+ offset 12) end-y true) (.setFloat32 dview (+ offset 16) opacity true) (.setFloat32 dview (+ offset 20) width true) - (.setUint32 dview (+ offset 24) (count stops) true) + (.setUint8 dview (+ offset 24) (count stops) true) (loop [stops (seq stops) offset (+ offset GRADIENT-BASE-SIZE)] (if (empty? stops) offset diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index f85ba75068..3d75f53f1f 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -17,7 +17,8 @@ pub struct RawGradientData { end_y: f32, opacity: f32, width: f32, - stop_count: u32, + stop_count: u8, + _padding: [u8; 3], stops: [RawStopData; MAX_GRADIENT_STOPS], } @@ -30,7 +31,8 @@ impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { end_y: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), opacity: f32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), width: f32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), - stop_count: u32::from_le_bytes([bytes[24], bytes[25], bytes[26], bytes[27]]), + stop_count: bytes[24], + _padding: [0; 3], // FIXME: 2025-04-22: use `array_chunks` once the next release is out // and we update our devenv. // See https://github.com/rust-lang/rust/issues/74985 From 875e019d4f283f03d2f606bf9bd30dcedde6f2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 29 Apr 2025 15:03:19 +0200 Subject: [PATCH 02/13] :recycle: Refactor raw gradient data into wasm module --- render-wasm/src/shapes/fills.rs | 164 +++++--------------------------- render-wasm/src/wasm/fills.rs | 134 ++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 141 deletions(-) diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 3d75f53f1f..9497a1f902 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -3,118 +3,37 @@ use skia_safe::{self as skia, Rect}; use super::Color; use crate::uuid::Uuid; -const MAX_GRADIENT_STOPS: usize = 16; -const BASE_GRADIENT_DATA_SIZE: usize = 28; -const RAW_GRADIENT_DATA_SIZE: usize = - BASE_GRADIENT_DATA_SIZE + RAW_STOP_DATA_SIZE * MAX_GRADIENT_STOPS; - -#[derive(Debug)] -#[repr(C)] -pub struct RawGradientData { - start_x: f32, - start_y: f32, - end_x: f32, - end_y: f32, - opacity: f32, - width: f32, - stop_count: u8, - _padding: [u8; 3], - stops: [RawStopData; MAX_GRADIENT_STOPS], -} - -impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { - fn from(bytes: [u8; RAW_GRADIENT_DATA_SIZE]) -> Self { - Self { - start_x: f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - start_y: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), - end_x: f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), - end_y: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), - opacity: f32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), - width: f32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), - stop_count: bytes[24], - _padding: [0; 3], - // FIXME: 2025-04-22: use `array_chunks` once the next release is out - // and we update our devenv. - // See https://github.com/rust-lang/rust/issues/74985 - stops: bytes[28..] - .chunks_exact(RAW_STOP_DATA_SIZE) - .map(|chunk| RawStopData::try_from(chunk).unwrap()) - .collect::>() - .try_into() - .unwrap(), - } - } -} - -impl RawGradientData { - pub fn start(&self) -> (f32, f32) { - (self.start_x, self.start_y) - } - - pub fn end(&self) -> (f32, f32) { - (self.end_x, self.end_y) - } - - pub fn opacity(&self) -> f32 { - self.opacity - } - - pub fn width(&self) -> f32 { - self.width - } -} - -pub const RAW_STOP_DATA_SIZE: usize = 8; - -#[derive(Debug)] -#[repr(C)] -pub struct RawStopData { - color: u32, - offset: f32, -} - -impl RawStopData { - pub fn color(&self) -> skia::Color { - skia::Color::from(self.color) - } - - pub fn offset(&self) -> f32 { - self.offset - } -} - -impl From<[u8; RAW_STOP_DATA_SIZE]> for RawStopData { - fn from(bytes: [u8; RAW_STOP_DATA_SIZE]) -> Self { - Self { - color: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - offset: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), - } - } -} - -// FIXME: We won't need this once we use `array_chunks`. See comment above. -impl TryFrom<&[u8]> for RawStopData { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let data: [u8; RAW_STOP_DATA_SIZE] = bytes - .try_into() - .map_err(|_| "Invalid stop data".to_string())?; - Ok(RawStopData::from(data)) - } -} - #[derive(Debug, Clone, PartialEq)] pub struct Gradient { - colors: Vec, - offsets: Vec, - opacity: f32, start: (f32, f32), end: (f32, f32), + opacity: f32, width: f32, + colors: Vec, + offsets: Vec, } impl Gradient { + pub fn new( + start: (f32, f32), + end: (f32, f32), + opacity: f32, + width: f32, + stops: &[(Color, f32)], + ) -> Self { + let mut gradient = Gradient { + start, + end, + opacity, + colors: vec![], + offsets: vec![], + width, + }; + + gradient.add_stops(stops); + gradient + } + fn add_stops(&mut self, stops: &[(Color, f32)]) { let colors = stops.iter().map(|(color, _)| *color); let offsets = stops.iter().map(|(_, offset)| *offset); @@ -175,43 +94,6 @@ impl Gradient { } } -impl From for Gradient { - fn from(raw_gradient: RawGradientData) -> Self { - let stops = raw_gradient - .stops - .iter() - .take(raw_gradient.stop_count as usize) - .map(|stop| (stop.color(), stop.offset())) - .collect::>(); - - let mut gradient = Gradient { - start: raw_gradient.start(), - end: raw_gradient.end(), - opacity: raw_gradient.opacity(), - colors: vec![], - offsets: vec![], - width: raw_gradient.width(), - }; - - gradient.add_stops(&stops); - - gradient - } -} - -impl TryFrom<&[u8]> for Gradient { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let raw_gradient_bytes: [u8; RAW_GRADIENT_DATA_SIZE] = bytes[0..RAW_GRADIENT_DATA_SIZE] - .try_into() - .map_err(|_| "Invalid gradient data".to_string())?; - let gradient = RawGradientData::from(raw_gradient_bytes).into(); - - Ok(gradient) - } -} - #[derive(Debug, Clone, PartialEq)] pub struct ImageFill { id: Uuid, diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index a6c47fdf1c..663941fdd4 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -2,6 +2,7 @@ use skia_safe as skia; use crate::mem; use crate::shapes; +use crate::shapes::Gradient; use crate::utils::uuid_from_u32_quartet; use crate::with_current_shape; use crate::STATE; @@ -58,3 +59,136 @@ pub extern "C" fn clear_shape_fills() { shape.clear_fills(); }); } + +const MAX_GRADIENT_STOPS: usize = 16; +const BASE_GRADIENT_DATA_SIZE: usize = 28; +const RAW_GRADIENT_DATA_SIZE: usize = + BASE_GRADIENT_DATA_SIZE + RAW_STOP_DATA_SIZE * MAX_GRADIENT_STOPS; + +#[derive(Debug)] +#[repr(C)] +pub struct RawGradientData { + start_x: f32, + start_y: f32, + end_x: f32, + end_y: f32, + opacity: f32, + width: f32, + stop_count: u8, + _pad: [u8; 3], + stops: [RawStopData; MAX_GRADIENT_STOPS], +} + +impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { + fn from(bytes: [u8; RAW_GRADIENT_DATA_SIZE]) -> Self { + Self { + start_x: f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + start_y: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + end_x: f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), + end_y: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), + opacity: f32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), + width: f32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), + stop_count: bytes[24], + _pad: [0; 3], + // FIXME: 2025-04-22: use `array_chunks` once the next release is out + // and we update our devenv. + // See https://github.com/rust-lang/rust/issues/74985 + stops: bytes[28..] + .chunks_exact(RAW_STOP_DATA_SIZE) + .map(|chunk| RawStopData::try_from(chunk).unwrap()) + .collect::>() + .try_into() + .unwrap(), + } + } +} + +impl RawGradientData { + pub fn start(&self) -> (f32, f32) { + (self.start_x, self.start_y) + } + + pub fn end(&self) -> (f32, f32) { + (self.end_x, self.end_y) + } + + pub fn opacity(&self) -> f32 { + self.opacity + } + + pub fn width(&self) -> f32 { + self.width + } +} + +pub const RAW_STOP_DATA_SIZE: usize = 8; + +#[derive(Debug)] +#[repr(C)] +pub struct RawStopData { + color: u32, + offset: f32, +} + +impl RawStopData { + pub fn color(&self) -> skia::Color { + skia::Color::from(self.color) + } + + pub fn offset(&self) -> f32 { + self.offset + } +} + +impl From<[u8; RAW_STOP_DATA_SIZE]> for RawStopData { + fn from(bytes: [u8; RAW_STOP_DATA_SIZE]) -> Self { + Self { + color: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + offset: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + } + } +} + +// FIXME: We won't need this once we use `array_chunks`. See comment above. +impl TryFrom<&[u8]> for RawStopData { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; RAW_STOP_DATA_SIZE] = bytes + .try_into() + .map_err(|_| "Invalid stop data".to_string())?; + Ok(RawStopData::from(data)) + } +} + +impl From for Gradient { + fn from(raw_gradient: RawGradientData) -> Self { + let stops = raw_gradient + .stops + .iter() + .take(raw_gradient.stop_count as usize) + .map(|stop| (stop.color(), stop.offset())) + .collect::>(); + + Gradient::new( + raw_gradient.start(), + raw_gradient.end(), + raw_gradient.opacity(), + raw_gradient.width(), + &stops, + ) + } +} + +impl TryFrom<&[u8]> for Gradient { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let raw_gradient_bytes: [u8; RAW_GRADIENT_DATA_SIZE] = bytes[0..RAW_GRADIENT_DATA_SIZE] + .try_into() + .map_err(|_| "Invalid gradient data".to_string())?; + let gradient = RawGradientData::from(raw_gradient_bytes).into(); + + Ok(gradient) + } +} From 81f18ad7f4c2b1db895ca2191edb9f641653cb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 29 Apr 2025 15:10:05 +0200 Subject: [PATCH 03/13] :recycle: Normalize opacity in fills to u8 --- render-wasm/src/shapes/fills.rs | 8 ++++---- render-wasm/src/wasm/fills.rs | 16 ++++------------ 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 9497a1f902..fd87f08042 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -7,7 +7,7 @@ use crate::uuid::Uuid; pub struct Gradient { start: (f32, f32), end: (f32, f32), - opacity: f32, + opacity: u8, width: f32, colors: Vec, offsets: Vec, @@ -17,7 +17,7 @@ impl Gradient { pub fn new( start: (f32, f32), end: (f32, f32), - opacity: f32, + opacity: u8, width: f32, stops: &[(Color, f32)], ) -> Self { @@ -143,7 +143,7 @@ impl Fill { Self::LinearGradient(gradient) => { let mut p = skia::Paint::default(); p.set_shader(gradient.to_linear_shader(rect)); - p.set_alpha((gradient.opacity * 255.) as u8); + p.set_alpha(gradient.opacity); p.set_style(skia::PaintStyle::Fill); p.set_anti_alias(anti_alias); p.set_blend_mode(skia::BlendMode::SrcOver); @@ -152,7 +152,7 @@ impl Fill { Self::RadialGradient(gradient) => { let mut p = skia::Paint::default(); p.set_shader(gradient.to_radial_shader(rect)); - p.set_alpha((gradient.opacity * 255.) as u8); + p.set_alpha(gradient.opacity); p.set_style(skia::PaintStyle::Fill); p.set_anti_alias(anti_alias); p.set_blend_mode(skia::BlendMode::SrcOver); diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 663941fdd4..a2642bfc16 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -67,7 +67,7 @@ const RAW_GRADIENT_DATA_SIZE: usize = #[derive(Debug)] #[repr(C)] -pub struct RawGradientData { +struct RawGradientData { start_x: f32, start_y: f32, end_x: f32, @@ -111,21 +111,13 @@ impl RawGradientData { pub fn end(&self) -> (f32, f32) { (self.end_x, self.end_y) } - - pub fn opacity(&self) -> f32 { - self.opacity - } - - pub fn width(&self) -> f32 { - self.width - } } pub const RAW_STOP_DATA_SIZE: usize = 8; #[derive(Debug)] #[repr(C)] -pub struct RawStopData { +struct RawStopData { color: u32, offset: f32, } @@ -173,8 +165,8 @@ impl From for Gradient { Gradient::new( raw_gradient.start(), raw_gradient.end(), - raw_gradient.opacity(), - raw_gradient.width(), + (raw_gradient.opacity * 255.) as u8, + raw_gradient.width, &stops, ) } From 093fa18839c0936d6da380ca311851c768753cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 29 Apr 2025 16:20:45 +0200 Subject: [PATCH 04/13] :sparkles: Serialize solid fills as bytes (wasm) --- frontend/src/app/render_wasm/api.cljs | 8 +++- .../app/render_wasm/serializers/fills.cljs | 12 +++++- render-wasm/src/shapes.rs | 7 +++- render-wasm/src/shapes/fills.rs | 9 ++-- render-wasm/src/shapes/strokes.rs | 17 ++++---- render-wasm/src/wasm/fills.rs | 42 +++++++++++++++++-- render-wasm/src/wasm/strokes.rs | 2 +- 7 files changed, 73 insertions(+), 24 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index caff9874e4..1cc55da7ad 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -217,8 +217,12 @@ image (:fill-image fill)] (cond (some? color) - (let [rgba (sr-clr/hex->u32argb color opacity)] - (h/call wasm/internal-module "_add_shape_solid_fill" rgba)) + (let [size sr-fills/SOLID-BYTE-SIZE + offset (mem/alloc-bytes size) + heap (mem/get-heap-u32) + argb (sr-clr/hex->u32argb color opacity)] + (sr-fills/write-solid-fill! offset heap argb) + (h/call wasm/internal-module "_add_shape_solid_fill")) (some? gradient) (let [size sr-fills/GRADIENT-BYTE-SIZE diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index bef0c35220..685030e558 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -2,6 +2,14 @@ (:require [app.render-wasm.serializers.color :as clr])) +(def SOLID-BYTE-SIZE 4) + +(defn write-solid-fill! + [offset heap-u32 argb] + (let [dview (js/DataView. (.-buffer heap-u32))] + (.setUint32 dview offset argb true) + (+ offset 4))) + (def ^:private GRADIENT-STOP-SIZE 8) (def ^:private GRADIENT-BASE-SIZE 28) ;; TODO: Define in shape model @@ -11,8 +19,8 @@ (+ GRADIENT-BASE-SIZE (* MAX-GRADIENT-STOPS GRADIENT-STOP-SIZE))) (defn write-gradient-fill! - [offset heap gradient opacity] - (let [dview (js/DataView. (.-buffer heap)) + [offset heap-u32 gradient opacity] + (let [dview (js/DataView. (.-buffer heap-u32)) start-x (:start-x gradient) start-y (:start-y gradient) end-x (:end-x gradient) diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index caf9056c82..3931eca380 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -860,8 +860,11 @@ mod tests { let mut shape = any_shape(); assert_eq!(shape.fills.len(), 0); - shape.add_fill(Fill::Solid(Color::TRANSPARENT)); - assert_eq!(shape.fills.get(0), Some(&Fill::Solid(Color::TRANSPARENT))) + shape.add_fill(Fill::Solid(SolidColor(Color::TRANSPARENT))); + assert_eq!( + shape.fills.get(0), + Some(&Fill::Solid(SolidColor(Color::TRANSPARENT))) + ) } #[test] diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index fd87f08042..948b962728 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -1,6 +1,6 @@ use skia_safe::{self as skia, Rect}; -use super::Color; +pub use super::Color; use crate::uuid::Uuid; #[derive(Debug, Clone, PartialEq)] @@ -112,9 +112,12 @@ impl ImageFill { } } +#[derive(Debug, Clone, PartialEq, Copy)] +pub struct SolidColor(pub Color); + #[derive(Debug, Clone, PartialEq)] pub enum Fill { - Solid(Color), + Solid(SolidColor), LinearGradient(Gradient), RadialGradient(Gradient), Image(ImageFill), @@ -132,7 +135,7 @@ impl Fill { pub fn to_paint(&self, rect: &Rect, anti_alias: bool) -> skia::Paint { match self { - Self::Solid(color) => { + Self::Solid(SolidColor(color)) => { let mut p = skia::Paint::default(); p.set_color(*color); p.set_style(skia::PaintStyle::Fill); diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index c1372d224c..ffa1e40621 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -1,4 +1,4 @@ -use crate::shapes::fills::Fill; +use crate::shapes::fills::{Fill, SolidColor}; use skia_safe::{self as skia, Rect}; use std::collections::HashMap; @@ -78,10 +78,9 @@ impl Stroke { } pub fn new_center_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) -> Self { - let transparent = skia::Color::from_argb(0, 0, 0, 0); Stroke { - fill: Fill::Solid(transparent), - width: width, + fill: Fill::Solid(SolidColor(skia::Color::TRANSPARENT)), + width, style: StrokeStyle::from(style), cap_end: StrokeCap::from(cap_end), cap_start: StrokeCap::from(cap_start), @@ -90,10 +89,9 @@ impl Stroke { } pub fn new_inner_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) -> Self { - let transparent = skia::Color::from_argb(0, 0, 0, 0); Stroke { - fill: Fill::Solid(transparent), - width: width, + fill: Fill::Solid(SolidColor(skia::Color::TRANSPARENT)), + width, style: StrokeStyle::from(style), cap_end: StrokeCap::from(cap_end), cap_start: StrokeCap::from(cap_start), @@ -102,10 +100,9 @@ impl Stroke { } pub fn new_outer_stroke(width: f32, style: u8, cap_start: u8, cap_end: u8) -> Self { - let transparent = skia::Color::from_argb(0, 0, 0, 0); Stroke { - fill: Fill::Solid(transparent), - width: width, + fill: Fill::Solid(SolidColor(skia::Color::TRANSPARENT)), + width, style: StrokeStyle::from(style), cap_end: StrokeCap::from(cap_end), cap_start: StrokeCap::from(cap_start), diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index a2642bfc16..50521d6ca3 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -2,16 +2,18 @@ use skia_safe as skia; use crate::mem; use crate::shapes; -use crate::shapes::Gradient; +use crate::shapes::{Gradient, SolidColor}; use crate::utils::uuid_from_u32_quartet; use crate::with_current_shape; use crate::STATE; #[no_mangle] -pub extern "C" fn add_shape_solid_fill(raw_color: u32) { +pub extern "C" fn add_shape_solid_fill() { with_current_shape!(state, |shape: &mut Shape| { - let color = skia::Color::new(raw_color); - shape.add_fill(shapes::Fill::Solid(color)); + let bytes = mem::bytes(); + let solid_color = SolidColor::try_from(&bytes[..]).expect("Invalid solid color data"); + + shape.add_fill(shapes::Fill::Solid(solid_color)); }); } @@ -60,6 +62,38 @@ pub extern "C" fn clear_shape_fills() { }); } +#[repr(C)] +pub struct RawSolidData { + color: u32, +} + +impl From<[u8; 4]> for RawSolidData { + fn from(value: [u8; 4]) -> Self { + Self { + color: u32::from_le_bytes(value), + } + } +} + +impl From for SolidColor { + fn from(value: RawSolidData) -> Self { + Self(skia::Color::new(value.color)) + } +} + +impl TryFrom<&[u8]> for SolidColor { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let raw_solid_bytes: [u8; 4] = bytes[0..4] + .try_into() + .map_err(|_| "Invalid solid fill data".to_string())?; + let color = RawSolidData::from(raw_solid_bytes).into(); + + Ok(color) + } +} + const MAX_GRADIENT_STOPS: usize = 16; const BASE_GRADIENT_DATA_SIZE: usize = 28; const RAW_GRADIENT_DATA_SIZE: usize = diff --git a/render-wasm/src/wasm/strokes.rs b/render-wasm/src/wasm/strokes.rs index 5aea834cfb..2912b9a37e 100644 --- a/render-wasm/src/wasm/strokes.rs +++ b/render-wasm/src/wasm/strokes.rs @@ -38,7 +38,7 @@ pub extern "C" fn add_shape_stroke_solid_fill(raw_color: u32) { with_current_shape!(state, |shape: &mut Shape| { let color = skia::Color::new(raw_color); shape - .set_stroke_fill(shapes::Fill::Solid(color)) + .set_stroke_fill(shapes::Fill::Solid(shapes::SolidColor(color))) .expect("could not add stroke solid fill"); }); } From 5ae125db9473f3fb0409a48180c32515be58938f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 29 Apr 2025 16:26:20 +0200 Subject: [PATCH 05/13] :sparkles: Serialize stroke solid fills as bytes (wasm) --- frontend/src/app/render_wasm/api.cljs | 8 ++++++-- render-wasm/src/wasm/strokes.rs | 8 +++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 1cc55da7ad..9ed04e556e 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -297,8 +297,12 @@ (store-image id))) (some? color) - (let [rgba (sr-clr/hex->u32argb color opacity)] - (h/call wasm/internal-module "_add_shape_stroke_solid_fill" rgba))))) + (let [size sr-fills/SOLID-BYTE-SIZE + offset (mem/alloc-bytes size) + heap (mem/get-heap-u32) + argb (sr-clr/hex->u32argb color opacity)] + (sr-fills/write-solid-fill! offset heap argb) + (h/call wasm/internal-module "_add_shape_stroke_solid_fill"))))) strokes)) (defn set-shape-path-attrs diff --git a/render-wasm/src/wasm/strokes.rs b/render-wasm/src/wasm/strokes.rs index 2912b9a37e..ef4a1f61d8 100644 --- a/render-wasm/src/wasm/strokes.rs +++ b/render-wasm/src/wasm/strokes.rs @@ -34,11 +34,13 @@ pub extern "C" fn add_shape_outer_stroke(width: f32, style: u8, cap_start: u8, c } #[no_mangle] -pub extern "C" fn add_shape_stroke_solid_fill(raw_color: u32) { +pub extern "C" fn add_shape_stroke_solid_fill() { with_current_shape!(state, |shape: &mut Shape| { - let color = skia::Color::new(raw_color); + let bytes = mem::bytes(); + let solid_color = + shapes::SolidColor::try_from(&bytes[..]).expect("Invalid solid color data"); shape - .set_stroke_fill(shapes::Fill::Solid(shapes::SolidColor(color))) + .set_stroke_fill(shapes::Fill::Solid(solid_color)) .expect("could not add stroke solid fill"); }); } From f30441626eee6bce5c0aaadd7620635cbbff6fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 29 Apr 2025 16:46:27 +0200 Subject: [PATCH 06/13] :recycle: Refactor fills DTOs into separate submodules --- render-wasm/src/wasm/fills.rs | 164 +------------------------ render-wasm/src/wasm/fills/gradient.rs | 126 +++++++++++++++++++ render-wasm/src/wasm/fills/solid.rs | 33 +++++ render-wasm/src/wasm/strokes.rs | 2 - 4 files changed, 163 insertions(+), 162 deletions(-) create mode 100644 render-wasm/src/wasm/fills/gradient.rs create mode 100644 render-wasm/src/wasm/fills/solid.rs diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 50521d6ca3..c5dbf19e04 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -1,8 +1,8 @@ -use skia_safe as skia; +mod gradient; +mod solid; use crate::mem; use crate::shapes; -use crate::shapes::{Gradient, SolidColor}; use crate::utils::uuid_from_u32_quartet; use crate::with_current_shape; use crate::STATE; @@ -11,7 +11,8 @@ use crate::STATE; pub extern "C" fn add_shape_solid_fill() { with_current_shape!(state, |shape: &mut Shape| { let bytes = mem::bytes(); - let solid_color = SolidColor::try_from(&bytes[..]).expect("Invalid solid color data"); + let solid_color = + shapes::SolidColor::try_from(&bytes[..]).expect("Invalid solid color data"); shape.add_fill(shapes::Fill::Solid(solid_color)); }); @@ -61,160 +62,3 @@ pub extern "C" fn clear_shape_fills() { shape.clear_fills(); }); } - -#[repr(C)] -pub struct RawSolidData { - color: u32, -} - -impl From<[u8; 4]> for RawSolidData { - fn from(value: [u8; 4]) -> Self { - Self { - color: u32::from_le_bytes(value), - } - } -} - -impl From for SolidColor { - fn from(value: RawSolidData) -> Self { - Self(skia::Color::new(value.color)) - } -} - -impl TryFrom<&[u8]> for SolidColor { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let raw_solid_bytes: [u8; 4] = bytes[0..4] - .try_into() - .map_err(|_| "Invalid solid fill data".to_string())?; - let color = RawSolidData::from(raw_solid_bytes).into(); - - Ok(color) - } -} - -const MAX_GRADIENT_STOPS: usize = 16; -const BASE_GRADIENT_DATA_SIZE: usize = 28; -const RAW_GRADIENT_DATA_SIZE: usize = - BASE_GRADIENT_DATA_SIZE + RAW_STOP_DATA_SIZE * MAX_GRADIENT_STOPS; - -#[derive(Debug)] -#[repr(C)] -struct RawGradientData { - start_x: f32, - start_y: f32, - end_x: f32, - end_y: f32, - opacity: f32, - width: f32, - stop_count: u8, - _pad: [u8; 3], - stops: [RawStopData; MAX_GRADIENT_STOPS], -} - -impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { - fn from(bytes: [u8; RAW_GRADIENT_DATA_SIZE]) -> Self { - Self { - start_x: f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - start_y: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), - end_x: f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), - end_y: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), - opacity: f32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), - width: f32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), - stop_count: bytes[24], - _pad: [0; 3], - // FIXME: 2025-04-22: use `array_chunks` once the next release is out - // and we update our devenv. - // See https://github.com/rust-lang/rust/issues/74985 - stops: bytes[28..] - .chunks_exact(RAW_STOP_DATA_SIZE) - .map(|chunk| RawStopData::try_from(chunk).unwrap()) - .collect::>() - .try_into() - .unwrap(), - } - } -} - -impl RawGradientData { - pub fn start(&self) -> (f32, f32) { - (self.start_x, self.start_y) - } - - pub fn end(&self) -> (f32, f32) { - (self.end_x, self.end_y) - } -} - -pub const RAW_STOP_DATA_SIZE: usize = 8; - -#[derive(Debug)] -#[repr(C)] -struct RawStopData { - color: u32, - offset: f32, -} - -impl RawStopData { - pub fn color(&self) -> skia::Color { - skia::Color::from(self.color) - } - - pub fn offset(&self) -> f32 { - self.offset - } -} - -impl From<[u8; RAW_STOP_DATA_SIZE]> for RawStopData { - fn from(bytes: [u8; RAW_STOP_DATA_SIZE]) -> Self { - Self { - color: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - offset: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), - } - } -} - -// FIXME: We won't need this once we use `array_chunks`. See comment above. -impl TryFrom<&[u8]> for RawStopData { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let data: [u8; RAW_STOP_DATA_SIZE] = bytes - .try_into() - .map_err(|_| "Invalid stop data".to_string())?; - Ok(RawStopData::from(data)) - } -} - -impl From for Gradient { - fn from(raw_gradient: RawGradientData) -> Self { - let stops = raw_gradient - .stops - .iter() - .take(raw_gradient.stop_count as usize) - .map(|stop| (stop.color(), stop.offset())) - .collect::>(); - - Gradient::new( - raw_gradient.start(), - raw_gradient.end(), - (raw_gradient.opacity * 255.) as u8, - raw_gradient.width, - &stops, - ) - } -} - -impl TryFrom<&[u8]> for Gradient { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let raw_gradient_bytes: [u8; RAW_GRADIENT_DATA_SIZE] = bytes[0..RAW_GRADIENT_DATA_SIZE] - .try_into() - .map_err(|_| "Invalid gradient data".to_string())?; - let gradient = RawGradientData::from(raw_gradient_bytes).into(); - - Ok(gradient) - } -} diff --git a/render-wasm/src/wasm/fills/gradient.rs b/render-wasm/src/wasm/fills/gradient.rs new file mode 100644 index 0000000000..a2f593879e --- /dev/null +++ b/render-wasm/src/wasm/fills/gradient.rs @@ -0,0 +1,126 @@ +use crate::shapes::{Color, Gradient}; + +const MAX_GRADIENT_STOPS: usize = 16; +const BASE_GRADIENT_DATA_SIZE: usize = 28; +const RAW_GRADIENT_DATA_SIZE: usize = + BASE_GRADIENT_DATA_SIZE + RAW_STOP_DATA_SIZE * MAX_GRADIENT_STOPS; + +#[derive(Debug)] +#[repr(C)] +struct RawGradientData { + start_x: f32, + start_y: f32, + end_x: f32, + end_y: f32, + opacity: f32, + width: f32, + stop_count: u8, + _pad: [u8; 3], + stops: [RawStopData; MAX_GRADIENT_STOPS], +} + +impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { + fn from(bytes: [u8; RAW_GRADIENT_DATA_SIZE]) -> Self { + Self { + start_x: f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + start_y: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + end_x: f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), + end_y: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), + opacity: f32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), + width: f32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), + stop_count: bytes[24], + _pad: [0; 3], + // FIXME: 2025-04-22: use `array_chunks` once the next release is out + // and we update our devenv. + // See https://github.com/rust-lang/rust/issues/74985 + stops: bytes[28..] + .chunks_exact(RAW_STOP_DATA_SIZE) + .map(|chunk| RawStopData::try_from(chunk).unwrap()) + .collect::>() + .try_into() + .unwrap(), + } + } +} + +impl RawGradientData { + pub fn start(&self) -> (f32, f32) { + (self.start_x, self.start_y) + } + + pub fn end(&self) -> (f32, f32) { + (self.end_x, self.end_y) + } +} + +pub const RAW_STOP_DATA_SIZE: usize = 8; + +#[derive(Debug)] +#[repr(C)] +struct RawStopData { + color: u32, + offset: f32, +} + +impl RawStopData { + pub fn color(&self) -> Color { + Color::from(self.color) + } + + pub fn offset(&self) -> f32 { + self.offset + } +} + +impl From<[u8; RAW_STOP_DATA_SIZE]> for RawStopData { + fn from(bytes: [u8; RAW_STOP_DATA_SIZE]) -> Self { + Self { + color: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + offset: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + } + } +} + +// FIXME: We won't need this once we use `array_chunks`. See comment above. +impl TryFrom<&[u8]> for RawStopData { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; RAW_STOP_DATA_SIZE] = bytes + .try_into() + .map_err(|_| "Invalid stop data".to_string())?; + Ok(RawStopData::from(data)) + } +} + +impl From for Gradient { + fn from(raw_gradient: RawGradientData) -> Self { + let stops = raw_gradient + .stops + .iter() + .take(raw_gradient.stop_count as usize) + .map(|stop| (stop.color(), stop.offset())) + .collect::>(); + + Gradient::new( + raw_gradient.start(), + raw_gradient.end(), + (raw_gradient.opacity * 255.) as u8, + raw_gradient.width, + &stops, + ) + } +} + +impl TryFrom<&[u8]> for Gradient { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let raw_gradient_bytes: [u8; RAW_GRADIENT_DATA_SIZE] = bytes[0..RAW_GRADIENT_DATA_SIZE] + .try_into() + .map_err(|_| "Invalid gradient data".to_string())?; + let gradient = RawGradientData::from(raw_gradient_bytes).into(); + + Ok(gradient) + } +} diff --git a/render-wasm/src/wasm/fills/solid.rs b/render-wasm/src/wasm/fills/solid.rs new file mode 100644 index 0000000000..f91f46640a --- /dev/null +++ b/render-wasm/src/wasm/fills/solid.rs @@ -0,0 +1,33 @@ +use crate::shapes::{Color, SolidColor}; + +#[repr(C)] +pub struct RawSolidData { + color: u32, +} + +impl From<[u8; 4]> for RawSolidData { + fn from(value: [u8; 4]) -> Self { + Self { + color: u32::from_le_bytes(value), + } + } +} + +impl From for SolidColor { + fn from(value: RawSolidData) -> Self { + Self(Color::new(value.color)) + } +} + +impl TryFrom<&[u8]> for SolidColor { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let raw_solid_bytes: [u8; 4] = bytes[0..4] + .try_into() + .map_err(|_| "Invalid solid fill data".to_string())?; + let color = RawSolidData::from(raw_solid_bytes).into(); + + Ok(color) + } +} diff --git a/render-wasm/src/wasm/strokes.rs b/render-wasm/src/wasm/strokes.rs index ef4a1f61d8..575890d0f3 100644 --- a/render-wasm/src/wasm/strokes.rs +++ b/render-wasm/src/wasm/strokes.rs @@ -1,5 +1,3 @@ -use skia_safe as skia; - use crate::mem; use crate::shapes; use crate::utils::uuid_from_u32_quartet; From 173d6c23b0253049053ce911e51b9a95e3f20007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 29 Apr 2025 17:30:17 +0200 Subject: [PATCH 07/13] :sparkles: Serialize image fills in binary --- frontend/src/app/render_wasm/api.cljs | 14 ++-- .../app/render_wasm/serializers/fills.cljs | 17 +++++ render-wasm/src/shapes/fills.rs | 20 +++--- render-wasm/src/wasm/fills.rs | 22 ++----- render-wasm/src/wasm/fills/image.rs | 66 +++++++++++++++++++ render-wasm/src/wasm/strokes.rs | 21 ++---- 6 files changed, 109 insertions(+), 51 deletions(-) create mode 100644 render-wasm/src/wasm/fills/image.rs diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 9ed04e556e..caf29baf3e 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -236,17 +236,13 @@ (h/call wasm/internal-module "_add_shape_radial_fill"))) (some? image) - (let [id (dm/get-prop image :id) + (let [heap (mem/get-heap-u32) + offset (mem/alloc-bytes sr-fills/IMAGE-BYTE-SIZE) + id (dm/get-prop image :id) buffer (uuid/get-u32 id) cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] - (h/call wasm/internal-module "_add_shape_image_fill" - (aget buffer 0) - (aget buffer 1) - (aget buffer 2) - (aget buffer 3) - opacity - (dm/get-prop image :width) - (dm/get-prop image :height)) + (sr-fills/write-image-fill! offset heap id opacity (dm/get-prop image :width) (dm/get-prop image :height)) + (h/call wasm/internal-module "_add_shape_image_fill") (when (== cached-image? 0) (store-image id)))))) fills)) diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index 685030e558..32ee70887a 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -1,5 +1,6 @@ (ns app.render-wasm.serializers.fills (:require + [app.common.uuid :as uuid] [app.render-wasm.serializers.color :as clr])) (def SOLID-BYTE-SIZE 4) @@ -10,6 +11,22 @@ (.setUint32 dview offset argb true) (+ offset 4))) +(def IMAGE-BYTE-SIZE 28) + +(defn write-image-fill! + [offset heap-u32 id opacity width height] + (js/console.log "write-image-fill!" (str id) opacity width height) + (let [dview (js/DataView. (.-buffer heap-u32)) + uuid-buffer (uuid/get-u32 id)] + (.setUint32 dview offset (aget uuid-buffer 0) true) + (.setUint32 dview (+ offset 4) (aget uuid-buffer 1) true) + (.setUint32 dview (+ offset 8) (aget uuid-buffer 2) true) + (.setUint32 dview (+ offset 12) (aget uuid-buffer 3) true) + (.setFloat32 dview (+ offset 16) opacity true) + (.setInt32 dview (+ offset 20) width true) + (.setInt32 dview (+ offset 24) height true) + (+ offset 28))) + (def ^:private GRADIENT-STOP-SIZE 8) (def ^:private GRADIENT-BASE-SIZE 28) ;; TODO: Define in shape model diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 948b962728..0263bdd94f 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -98,11 +98,20 @@ impl Gradient { pub struct ImageFill { id: Uuid, opacity: u8, - height: i32, width: i32, + height: i32, } impl ImageFill { + pub fn new(id: Uuid, opacity: u8, width: i32, height: i32) -> Self { + Self { + id, + opacity, + width, + height, + } + } + pub fn size(&self) -> (i32, i32) { (self.width, self.height) } @@ -124,15 +133,6 @@ pub enum Fill { } impl Fill { - pub fn new_image_fill(id: Uuid, opacity: u8, (width, height): (i32, i32)) -> Self { - Self::Image(ImageFill { - id, - opacity, - height, - width, - }) - } - pub fn to_paint(&self, rect: &Rect, anti_alias: bool) -> skia::Paint { match self { Self::Solid(SolidColor(color)) => { diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index c5dbf19e04..9d74a85797 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -1,9 +1,9 @@ mod gradient; +mod image; mod solid; use crate::mem; use crate::shapes; -use crate::utils::uuid_from_u32_quartet; use crate::with_current_shape; use crate::STATE; @@ -37,22 +37,12 @@ pub extern "C" fn add_shape_radial_fill() { } #[no_mangle] -pub extern "C" fn add_shape_image_fill( - a: u32, - b: u32, - c: u32, - d: u32, - alpha: f32, - width: i32, - height: i32, -) { +pub extern "C" fn add_shape_image_fill() { with_current_shape!(state, |shape: &mut Shape| { - let id = uuid_from_u32_quartet(a, b, c, d); - shape.add_fill(shapes::Fill::new_image_fill( - id, - (alpha * 0xff as f32).floor() as u8, - (width, height), - )); + let bytes = mem::bytes(); + let image_fill = shapes::ImageFill::try_from(&bytes[..]).expect("Invalid image fill data"); + + shape.add_fill(shapes::Fill::Image(image_fill)); }); } diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs new file mode 100644 index 0000000000..a233b817d4 --- /dev/null +++ b/render-wasm/src/wasm/fills/image.rs @@ -0,0 +1,66 @@ +use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet}; + +const RAW_IMAGE_DATA_SIZE: usize = 28; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(C)] +pub struct RawImageFillData { + a: u32, + b: u32, + c: u32, + d: u32, + opacity: f32, + width: i32, + height: i32, +} + +impl From for ImageFill { + fn from(value: RawImageFillData) -> Self { + let id = uuid_from_u32_quartet(value.a, value.b, value.c, value.d); + let opacity = (value.opacity * 255.).floor() as u8; + + Self::new(id, opacity, value.width, value.height) + } +} + +impl From<[u8; RAW_IMAGE_DATA_SIZE]> for RawImageFillData { + fn from(value: [u8; RAW_IMAGE_DATA_SIZE]) -> Self { + let a = u32::from_le_bytes([value[0], value[1], value[2], value[3]]); + let b = u32::from_le_bytes([value[4], value[5], value[6], value[7]]); + let c = u32::from_le_bytes([value[8], value[9], value[10], value[11]]); + let d = u32::from_le_bytes([value[12], value[13], value[14], value[15]]); + let opacity = f32::from_le_bytes([value[16], value[17], value[18], value[19]]); + let width = i32::from_le_bytes([value[20], value[21], value[22], value[23]]); + let height = i32::from_le_bytes([value[24], value[25], value[26], value[27]]); + + Self { + a, + b, + c, + d, + opacity, + width, + height, + } + } +} + +impl TryFrom<&[u8]> for RawImageFillData { + type Error = String; + + fn try_from(value: &[u8]) -> Result { + let data: [u8; RAW_IMAGE_DATA_SIZE] = value + .try_into() + .map_err(|_| "Invalid image fill data".to_string())?; + Ok(Self::from(data)) + } +} + +impl TryFrom<&[u8]> for ImageFill { + type Error = String; + + fn try_from(value: &[u8]) -> Result { + let raw_image_data = RawImageFillData::try_from(value)?; + Ok(raw_image_data.into()) + } +} diff --git a/render-wasm/src/wasm/strokes.rs b/render-wasm/src/wasm/strokes.rs index 575890d0f3..72b6ea43d5 100644 --- a/render-wasm/src/wasm/strokes.rs +++ b/render-wasm/src/wasm/strokes.rs @@ -1,6 +1,5 @@ use crate::mem; use crate::shapes; -use crate::utils::uuid_from_u32_quartet; use crate::with_current_shape; use crate::STATE; @@ -68,23 +67,13 @@ pub extern "C" fn add_shape_stroke_radial_fill() { } #[no_mangle] -pub extern "C" fn add_shape_image_stroke( - a: u32, - b: u32, - c: u32, - d: u32, - alpha: f32, - width: i32, - height: i32, -) { +pub extern "C" fn add_shape_image_stroke() { with_current_shape!(state, |shape: &mut Shape| { - let id = uuid_from_u32_quartet(a, b, c, d); + let bytes = mem::bytes(); + let image_fill = shapes::ImageFill::try_from(&bytes[..]).expect("Invalid image fill data"); + shape - .set_stroke_fill(shapes::Fill::new_image_fill( - id, - (alpha * 0xff as f32).floor() as u8, - (width, height), - )) + .set_stroke_fill(shapes::Fill::Image(image_fill)) .expect("could not add stroke image fill"); }); } From 784aecd1a184b4bcd224acf280c9f22177694c49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Mon, 5 May 2025 16:55:00 +0200 Subject: [PATCH 08/13] :tada: Add a DTO that handles all fill types --- render-wasm/src/wasm/fills.rs | 90 ++++++++++++++++++++++++++ render-wasm/src/wasm/fills/gradient.rs | 17 ++++- render-wasm/src/wasm/fills/solid.rs | 15 ++++- 3 files changed, 118 insertions(+), 4 deletions(-) diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 9d74a85797..71d9a4286f 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -7,6 +7,67 @@ use crate::shapes; use crate::with_current_shape; use crate::STATE; +#[repr(C)] +#[repr(align(4))] +#[repr(u8)] +#[derive(Debug, PartialEq, Clone, Copy)] +enum RawFillData { + Solid(solid::RawSolidData) = 0x00, + Linear(gradient::RawGradientData) = 0x01, + Radial(gradient::RawGradientData) = 0x02, + Image(image::RawImageFillData) = 0x03, +} + +impl From for shapes::Fill { + fn from(fill_data: RawFillData) -> Self { + match fill_data { + RawFillData::Solid(solid_fill_data) => shapes::Fill::Solid(solid_fill_data.into()), + RawFillData::Linear(linear_fill_data) => { + shapes::Fill::LinearGradient(linear_fill_data.into()) + } + RawFillData::Radial(radial_fill_data) => { + shapes::Fill::RadialGradient(radial_fill_data.into()) + } + RawFillData::Image(image_fill_data) => shapes::Fill::Image(image_fill_data.into()), + } + } +} + +impl TryFrom<&[u8]> for RawFillData { + type Error = String; + fn try_from(bytes: &[u8]) -> Result { + if bytes.len() < std::mem::size_of::() { + return Err("Invalid fill data".to_string()); + } + + let fill_type = bytes[0]; + match fill_type { + 0x00 => Ok(RawFillData::Solid(solid::RawSolidData::try_from( + &bytes[1..], + )?)), + 0x01 => Ok(RawFillData::Linear(gradient::RawGradientData::try_from( + &bytes[1..], + )?)), + 0x02 => Ok(RawFillData::Radial(gradient::RawGradientData::try_from( + &bytes[1..], + )?)), + 0x03 => Ok(RawFillData::Image(image::RawImageFillData::try_from( + &bytes[1..], + )?)), + _ => Err("Invalid fill type".to_string()), + } + } +} + +#[no_mangle] +pub extern "C" fn add_shape_fill() { + with_current_shape!(state, |shape: &mut Shape| { + let bytes = mem::bytes(); + let raw_fill = RawFillData::try_from(&bytes[..]).expect("Invalid fill data"); + shape.add_fill(raw_fill.into()); + }); +} + #[no_mangle] pub extern "C" fn add_shape_solid_fill() { with_current_shape!(state, |shape: &mut Shape| { @@ -52,3 +113,32 @@ pub extern "C" fn clear_shape_fills() { shape.clear_fills(); }); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_raw_fill_data_layout() { + assert_eq!( + std::mem::size_of::(), + 4 + std::mem::size_of::() + ); + assert_eq!(std::mem::align_of::(), 4); + } + + #[test] + fn test_raw_fill_data_from_bytes_to_solid_fill() { + let mut bytes = vec![0x00; std::mem::size_of::()]; + bytes[0] = 0x00; + bytes[1..=4].copy_from_slice(&0xfffabada_u32.to_le_bytes()); + + let raw_fill = RawFillData::try_from(&bytes[..]); + + assert!(raw_fill.is_ok()); + assert_eq!( + raw_fill.unwrap(), + RawFillData::Solid(solid::RawSolidData { color: 0xfffabada }) + ); + } +} diff --git a/render-wasm/src/wasm/fills/gradient.rs b/render-wasm/src/wasm/fills/gradient.rs index a2f593879e..cd16c0c857 100644 --- a/render-wasm/src/wasm/fills/gradient.rs +++ b/render-wasm/src/wasm/fills/gradient.rs @@ -5,9 +5,9 @@ const BASE_GRADIENT_DATA_SIZE: usize = 28; const RAW_GRADIENT_DATA_SIZE: usize = BASE_GRADIENT_DATA_SIZE + RAW_STOP_DATA_SIZE * MAX_GRADIENT_STOPS; -#[derive(Debug)] +#[derive(Debug, PartialEq, Clone, Copy)] #[repr(C)] -struct RawGradientData { +pub struct RawGradientData { start_x: f32, start_y: f32, end_x: f32, @@ -43,6 +43,17 @@ impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { } } +impl TryFrom<&[u8]> for RawGradientData { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; RAW_GRADIENT_DATA_SIZE] = bytes + .try_into() + .map_err(|_| "Invalid gradient data".to_string())?; + Ok(RawGradientData::from(data)) + } +} + impl RawGradientData { pub fn start(&self) -> (f32, f32) { (self.start_x, self.start_y) @@ -55,7 +66,7 @@ impl RawGradientData { pub const RAW_STOP_DATA_SIZE: usize = 8; -#[derive(Debug)] +#[derive(Debug, PartialEq, Clone, Copy)] #[repr(C)] struct RawStopData { color: u32, diff --git a/render-wasm/src/wasm/fills/solid.rs b/render-wasm/src/wasm/fills/solid.rs index f91f46640a..124262b736 100644 --- a/render-wasm/src/wasm/fills/solid.rs +++ b/render-wasm/src/wasm/fills/solid.rs @@ -1,8 +1,9 @@ use crate::shapes::{Color, SolidColor}; #[repr(C)] +#[derive(Debug, PartialEq, Clone, Copy)] pub struct RawSolidData { - color: u32, + pub color: u32, } impl From<[u8; 4]> for RawSolidData { @@ -13,6 +14,18 @@ impl From<[u8; 4]> for RawSolidData { } } +impl TryFrom<&[u8]> for RawSolidData { + type Error = String; + + fn try_from(bytes: &[u8]) -> Result { + let data: [u8; 4] = bytes + .get(0..4) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid solid fill data".to_string())?; + Ok(RawSolidData::from(data)) + } +} + impl From for SolidColor { fn from(value: RawSolidData) -> Self { Self(Color::new(value.color)) From cba65972ddfd13154c1161e5773cf9e6d4618405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 6 May 2025 12:33:14 +0200 Subject: [PATCH 09/13] :sparkles: Use same wasm function to add all types of fills --- frontend/src/app/render_wasm/api.cljs | 54 +++++------- .../app/render_wasm/serializers/fills.cljs | 86 +++++++++++-------- render-wasm/src/wasm/fills.rs | 49 ++--------- render-wasm/src/wasm/fills/gradient.rs | 5 +- render-wasm/src/wasm/fills/image.rs | 5 +- render-wasm/src/wasm/fills/solid.rs | 8 +- render-wasm/src/wasm/strokes.rs | 45 +--------- 7 files changed, 89 insertions(+), 163 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index caf29baf3e..56a38d7b6f 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -214,35 +214,30 @@ (let [opacity (or (:fill-opacity fill) 1.0) color (:fill-color fill) gradient (:fill-color-gradient fill) - image (:fill-image fill)] + image (:fill-image fill) + offset (mem/alloc-bytes sr-fills/FILL-BYTE-SIZE) + heap (mem/get-heap-u32)] (cond (some? color) - (let [size sr-fills/SOLID-BYTE-SIZE - offset (mem/alloc-bytes size) - heap (mem/get-heap-u32) - argb (sr-clr/hex->u32argb color opacity)] + (let [argb (sr-clr/hex->u32argb color opacity)] (sr-fills/write-solid-fill! offset heap argb) - (h/call wasm/internal-module "_add_shape_solid_fill")) + (h/call wasm/internal-module "_add_shape_fill")) (some? gradient) - (let [size sr-fills/GRADIENT-BYTE-SIZE - offset (mem/alloc-bytes size) - heap (mem/get-heap-u32)] + (let [_ nil] (sr-fills/write-gradient-fill! offset heap gradient opacity) (case (:type gradient) :linear - (h/call wasm/internal-module "_add_shape_linear_fill") + (h/call wasm/internal-module "_add_shape_fill") :radial - (h/call wasm/internal-module "_add_shape_radial_fill"))) + (h/call wasm/internal-module "_add_shape_fill"))) (some? image) - (let [heap (mem/get-heap-u32) - offset (mem/alloc-bytes sr-fills/IMAGE-BYTE-SIZE) - id (dm/get-prop image :id) + (let [id (dm/get-prop image :id) buffer (uuid/get-u32 id) cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] (sr-fills/write-image-fill! offset heap id opacity (dm/get-prop image :width) (dm/get-prop image :height)) - (h/call wasm/internal-module "_add_shape_image_fill") + (h/call wasm/internal-module "_add_shape_fill") (when (== cached-image? 0) (store-image id)))))) fills)) @@ -259,7 +254,9 @@ align (:stroke-alignment stroke) style (-> stroke :stroke-style sr/translate-stroke-style) cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap) - cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap)] + cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap) + offset (mem/alloc-bytes sr-fills/FILL-BYTE-SIZE) + heap (mem/get-heap-u32)] (case align :inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end) :outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end) @@ -267,38 +264,27 @@ (cond (some? gradient) - (let [size sr-fills/GRADIENT-BYTE-SIZE - offset (mem/alloc-bytes size) - heap (mem/get-heap-u32)] + (let [_ nil] (sr-fills/write-gradient-fill! offset heap gradient opacity) (case (:type gradient) :linear - (h/call wasm/internal-module "_add_shape_stroke_linear_fill") + (h/call wasm/internal-module "_add_shape_stroke_fill") :radial - (h/call wasm/internal-module "_add_shape_stroke_radial_fill"))) + (h/call wasm/internal-module "_add_shape_stroke_fill"))) (some? image) (let [id (dm/get-prop image :id) buffer (uuid/get-u32 id) cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] - (h/call wasm/internal-module "_add_shape_image_stroke" - (aget buffer 0) - (aget buffer 1) - (aget buffer 2) - (aget buffer 3) - opacity - (dm/get-prop image :width) - (dm/get-prop image :height)) + (sr-fills/write-image-fill! offset heap id opacity (dm/get-prop image :width) (dm/get-prop image :height)) + (h/call wasm/internal-module "_add_shape_stroke_fill") (when (== cached-image? 0) (store-image id))) (some? color) - (let [size sr-fills/SOLID-BYTE-SIZE - offset (mem/alloc-bytes size) - heap (mem/get-heap-u32) - argb (sr-clr/hex->u32argb color opacity)] + (let [argb (sr-clr/hex->u32argb color opacity)] (sr-fills/write-solid-fill! offset heap argb) - (h/call wasm/internal-module "_add_shape_stroke_solid_fill"))))) + (h/call wasm/internal-module "_add_shape_stroke_fill"))))) strokes)) (defn set-shape-path-attrs diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index 32ee70887a..c557bca2d8 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -3,30 +3,6 @@ [app.common.uuid :as uuid] [app.render-wasm.serializers.color :as clr])) -(def SOLID-BYTE-SIZE 4) - -(defn write-solid-fill! - [offset heap-u32 argb] - (let [dview (js/DataView. (.-buffer heap-u32))] - (.setUint32 dview offset argb true) - (+ offset 4))) - -(def IMAGE-BYTE-SIZE 28) - -(defn write-image-fill! - [offset heap-u32 id opacity width height] - (js/console.log "write-image-fill!" (str id) opacity width height) - (let [dview (js/DataView. (.-buffer heap-u32)) - uuid-buffer (uuid/get-u32 id)] - (.setUint32 dview offset (aget uuid-buffer 0) true) - (.setUint32 dview (+ offset 4) (aget uuid-buffer 1) true) - (.setUint32 dview (+ offset 8) (aget uuid-buffer 2) true) - (.setUint32 dview (+ offset 12) (aget uuid-buffer 3) true) - (.setFloat32 dview (+ offset 16) opacity true) - (.setInt32 dview (+ offset 20) width true) - (.setInt32 dview (+ offset 24) height true) - (+ offset 28))) - (def ^:private GRADIENT-STOP-SIZE 8) (def ^:private GRADIENT-BASE-SIZE 28) ;; TODO: Define in shape model @@ -35,6 +11,40 @@ (def GRADIENT-BYTE-SIZE (+ GRADIENT-BASE-SIZE (* MAX-GRADIENT-STOPS GRADIENT-STOP-SIZE))) +(def SOLID-BYTE-SIZE 4) +(def IMAGE-BYTE-SIZE 28) + +;; FIXME: get it from the wasm module +(def FILL-BYTE-SIZE (+ 4 (max GRADIENT-BYTE-SIZE IMAGE-BYTE-SIZE SOLID-BYTE-SIZE))) + +;; (defn write-fill! [offset heap-u32 fill] +;; (let [dview (js/DataView. (.-buffer heap-u32))] +;; offset)) + + +(defn write-solid-fill! + [offset heap-u32 argb] + (let [dview (js/DataView. (.-buffer heap-u32))] + (.setUint8 dview offset 0x00 true) + (.setUint32 dview (+ offset 4) argb true) + (+ offset FILL-BYTE-SIZE))) + +(defn write-image-fill! + [offset heap-u32 id opacity width height] + (let [dview (js/DataView. (.-buffer heap-u32)) + uuid-buffer (uuid/get-u32 id)] + (.setUint8 dview offset 0x03 true) + (.setUint32 dview (+ offset 4) (aget uuid-buffer 0) true) + (.setUint32 dview (+ offset 8) (aget uuid-buffer 1) true) + (.setUint32 dview (+ offset 12) (aget uuid-buffer 2) true) + (.setUint32 dview (+ offset 16) (aget uuid-buffer 3) true) + (.setFloat32 dview (+ offset 20) opacity true) + (.setInt32 dview (+ offset 24) width true) + (.setInt32 dview (+ offset 28) height true) + (+ offset FILL-BYTE-SIZE))) + + + (defn write-gradient-fill! [offset heap-u32 gradient opacity] (let [dview (js/DataView. (.-buffer heap-u32)) @@ -43,22 +53,24 @@ end-x (:end-x gradient) end-y (:end-y gradient) width (or (:width gradient) 0) - stops (take MAX-GRADIENT-STOPS (:stops gradient))] - (.setFloat32 dview offset start-x true) - (.setFloat32 dview (+ offset 4) start-y true) - (.setFloat32 dview (+ offset 8) end-x true) - (.setFloat32 dview (+ offset 12) end-y true) - (.setFloat32 dview (+ offset 16) opacity true) - (.setFloat32 dview (+ offset 20) width true) - (.setUint8 dview (+ offset 24) (count stops) true) - (loop [stops (seq stops) offset (+ offset GRADIENT-BASE-SIZE)] + stops (take MAX-GRADIENT-STOPS (:stops gradient)) + type (if (= (:type gradient) :linear) 0x01 0x02)] + (.setUint8 dview offset type true) + (.setFloat32 dview (+ offset 4) start-x true) + (.setFloat32 dview (+ offset 8) start-y true) + (.setFloat32 dview (+ offset 12) end-x true) + (.setFloat32 dview (+ offset 16) end-y true) + (.setFloat32 dview (+ offset 20) opacity true) + (.setFloat32 dview (+ offset 24) width true) + (.setUint8 dview (+ offset 28) (count stops) true) + (loop [stops (seq stops) loop-offset (+ offset 32)] (if (empty? stops) - offset + (+ offset FILL-BYTE-SIZE) (let [stop (first stops) hex-color (:color stop) opacity (:opacity stop) argb (clr/hex->u32argb hex-color opacity) stop-offset (:offset stop)] - (.setUint32 dview offset argb true) - (.setFloat32 dview (+ offset 4) stop-offset true) - (recur (rest stops) (+ offset GRADIENT-STOP-SIZE))))))) \ No newline at end of file + (.setUint32 dview loop-offset argb true) + (.setFloat32 dview (+ loop-offset 4) stop-offset true) + (recur (rest stops) (+ loop-offset GRADIENT-STOP-SIZE))))))) \ No newline at end of file diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 71d9a4286f..4c54fb187b 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -11,7 +11,7 @@ use crate::STATE; #[repr(align(4))] #[repr(u8)] #[derive(Debug, PartialEq, Clone, Copy)] -enum RawFillData { +pub enum RawFillData { Solid(solid::RawSolidData) = 0x00, Linear(gradient::RawGradientData) = 0x01, Radial(gradient::RawGradientData) = 0x02, @@ -43,16 +43,16 @@ impl TryFrom<&[u8]> for RawFillData { let fill_type = bytes[0]; match fill_type { 0x00 => Ok(RawFillData::Solid(solid::RawSolidData::try_from( - &bytes[1..], + &bytes[4..], )?)), 0x01 => Ok(RawFillData::Linear(gradient::RawGradientData::try_from( - &bytes[1..], + &bytes[4..], )?)), 0x02 => Ok(RawFillData::Radial(gradient::RawGradientData::try_from( - &bytes[1..], + &bytes[4..], )?)), 0x03 => Ok(RawFillData::Image(image::RawImageFillData::try_from( - &bytes[1..], + &bytes[4..], )?)), _ => Err("Invalid fill type".to_string()), } @@ -68,45 +68,6 @@ pub extern "C" fn add_shape_fill() { }); } -#[no_mangle] -pub extern "C" fn add_shape_solid_fill() { - with_current_shape!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let solid_color = - shapes::SolidColor::try_from(&bytes[..]).expect("Invalid solid color data"); - - shape.add_fill(shapes::Fill::Solid(solid_color)); - }); -} - -#[no_mangle] -pub extern "C" fn add_shape_linear_fill() { - with_current_shape!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let gradient = shapes::Gradient::try_from(&bytes[..]).expect("Invalid gradient data"); - shape.add_fill(shapes::Fill::LinearGradient(gradient)); - }); -} - -#[no_mangle] -pub extern "C" fn add_shape_radial_fill() { - with_current_shape!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let gradient = shapes::Gradient::try_from(&bytes[..]).expect("Invalid gradient data"); - shape.add_fill(shapes::Fill::RadialGradient(gradient)); - }); -} - -#[no_mangle] -pub extern "C" fn add_shape_image_fill() { - with_current_shape!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let image_fill = shapes::ImageFill::try_from(&bytes[..]).expect("Invalid image fill data"); - - shape.add_fill(shapes::Fill::Image(image_fill)); - }); -} - #[no_mangle] pub extern "C" fn clear_shape_fills() { with_current_shape!(state, |shape: &mut Shape| { diff --git a/render-wasm/src/wasm/fills/gradient.rs b/render-wasm/src/wasm/fills/gradient.rs index cd16c0c857..060ca010a3 100644 --- a/render-wasm/src/wasm/fills/gradient.rs +++ b/render-wasm/src/wasm/fills/gradient.rs @@ -48,8 +48,9 @@ impl TryFrom<&[u8]> for RawGradientData { fn try_from(bytes: &[u8]) -> Result { let data: [u8; RAW_GRADIENT_DATA_SIZE] = bytes - .try_into() - .map_err(|_| "Invalid gradient data".to_string())?; + .get(0..RAW_GRADIENT_DATA_SIZE) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid gradient fill data".to_string())?; Ok(RawGradientData::from(data)) } } diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index a233b817d4..e8714b06b6 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -50,8 +50,9 @@ impl TryFrom<&[u8]> for RawImageFillData { fn try_from(value: &[u8]) -> Result { let data: [u8; RAW_IMAGE_DATA_SIZE] = value - .try_into() - .map_err(|_| "Invalid image fill data".to_string())?; + .get(0..RAW_IMAGE_DATA_SIZE) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid image fill data".to_string())?; Ok(Self::from(data)) } } diff --git a/render-wasm/src/wasm/fills/solid.rs b/render-wasm/src/wasm/fills/solid.rs index 124262b736..bd4219b4b1 100644 --- a/render-wasm/src/wasm/fills/solid.rs +++ b/render-wasm/src/wasm/fills/solid.rs @@ -1,5 +1,7 @@ use crate::shapes::{Color, SolidColor}; +const RAW_SOLID_DATA_SIZE: usize = 4; + #[repr(C)] #[derive(Debug, PartialEq, Clone, Copy)] pub struct RawSolidData { @@ -7,7 +9,7 @@ pub struct RawSolidData { } impl From<[u8; 4]> for RawSolidData { - fn from(value: [u8; 4]) -> Self { + fn from(value: [u8; RAW_SOLID_DATA_SIZE]) -> Self { Self { color: u32::from_le_bytes(value), } @@ -18,8 +20,8 @@ impl TryFrom<&[u8]> for RawSolidData { type Error = String; fn try_from(bytes: &[u8]) -> Result { - let data: [u8; 4] = bytes - .get(0..4) + let data: [u8; RAW_SOLID_DATA_SIZE] = bytes + .get(0..RAW_SOLID_DATA_SIZE) .and_then(|slice| slice.try_into().ok()) .ok_or("Invalid solid fill data".to_string())?; Ok(RawSolidData::from(data)) diff --git a/render-wasm/src/wasm/strokes.rs b/render-wasm/src/wasm/strokes.rs index 72b6ea43d5..4f9905daac 100644 --- a/render-wasm/src/wasm/strokes.rs +++ b/render-wasm/src/wasm/strokes.rs @@ -31,50 +31,13 @@ pub extern "C" fn add_shape_outer_stroke(width: f32, style: u8, cap_start: u8, c } #[no_mangle] -pub extern "C" fn add_shape_stroke_solid_fill() { +pub extern "C" fn add_shape_stroke_fill() { with_current_shape!(state, |shape: &mut Shape| { let bytes = mem::bytes(); - let solid_color = - shapes::SolidColor::try_from(&bytes[..]).expect("Invalid solid color data"); + let raw_fill = super::fills::RawFillData::try_from(&bytes[..]).expect("Invalid fill data"); shape - .set_stroke_fill(shapes::Fill::Solid(solid_color)) - .expect("could not add stroke solid fill"); - }); -} - -#[no_mangle] -pub extern "C" fn add_shape_stroke_linear_fill() { - with_current_shape!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let gradient = shapes::Gradient::try_from(&bytes[..]).expect("Invalid gradient data"); - - shape - .set_stroke_fill(shapes::Fill::LinearGradient(gradient)) - .expect("could not add stroke linear gradient fill"); - }); -} - -#[no_mangle] -pub extern "C" fn add_shape_stroke_radial_fill() { - with_current_shape!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let gradient = shapes::Gradient::try_from(&bytes[..]).expect("Invalid gradient data"); - - shape - .set_stroke_fill(shapes::Fill::RadialGradient(gradient)) - .expect("could not add stroke radial gradient fill"); - }); -} - -#[no_mangle] -pub extern "C" fn add_shape_image_stroke() { - with_current_shape!(state, |shape: &mut Shape| { - let bytes = mem::bytes(); - let image_fill = shapes::ImageFill::try_from(&bytes[..]).expect("Invalid image fill data"); - - shape - .set_stroke_fill(shapes::Fill::Image(image_fill)) - .expect("could not add stroke image fill"); + .set_stroke_fill(raw_fill.into()) + .expect("could not add stroke fill"); }); } From ec73bd640cb658d437d3c5e3c8e7db500417071c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 6 May 2025 12:38:30 +0200 Subject: [PATCH 10/13] :sparkles: Use mem::transmute to deserialize raw fill data --- render-wasm/src/wasm/fills/gradient.rs | 22 ++-------------------- render-wasm/src/wasm/fills/image.rs | 19 ++----------------- render-wasm/src/wasm/fills/solid.rs | 5 ++--- 3 files changed, 6 insertions(+), 40 deletions(-) diff --git a/render-wasm/src/wasm/fills/gradient.rs b/render-wasm/src/wasm/fills/gradient.rs index 060ca010a3..8b045f62c1 100644 --- a/render-wasm/src/wasm/fills/gradient.rs +++ b/render-wasm/src/wasm/fills/gradient.rs @@ -7,6 +7,7 @@ const RAW_GRADIENT_DATA_SIZE: usize = #[derive(Debug, PartialEq, Clone, Copy)] #[repr(C)] +#[repr(align(4))] pub struct RawGradientData { start_x: f32, start_y: f32, @@ -15,31 +16,12 @@ pub struct RawGradientData { opacity: f32, width: f32, stop_count: u8, - _pad: [u8; 3], stops: [RawStopData; MAX_GRADIENT_STOPS], } impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { fn from(bytes: [u8; RAW_GRADIENT_DATA_SIZE]) -> Self { - Self { - start_x: f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - start_y: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), - end_x: f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), - end_y: f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), - opacity: f32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]), - width: f32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]), - stop_count: bytes[24], - _pad: [0; 3], - // FIXME: 2025-04-22: use `array_chunks` once the next release is out - // and we update our devenv. - // See https://github.com/rust-lang/rust/issues/74985 - stops: bytes[28..] - .chunks_exact(RAW_STOP_DATA_SIZE) - .map(|chunk| RawStopData::try_from(chunk).unwrap()) - .collect::>() - .try_into() - .unwrap(), - } + unsafe { std::mem::transmute(bytes) } } } diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index e8714b06b6..7540c3184e 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -4,6 +4,7 @@ const RAW_IMAGE_DATA_SIZE: usize = 28; #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] +#[repr(align(4))] pub struct RawImageFillData { a: u32, b: u32, @@ -25,23 +26,7 @@ impl From for ImageFill { impl From<[u8; RAW_IMAGE_DATA_SIZE]> for RawImageFillData { fn from(value: [u8; RAW_IMAGE_DATA_SIZE]) -> Self { - let a = u32::from_le_bytes([value[0], value[1], value[2], value[3]]); - let b = u32::from_le_bytes([value[4], value[5], value[6], value[7]]); - let c = u32::from_le_bytes([value[8], value[9], value[10], value[11]]); - let d = u32::from_le_bytes([value[12], value[13], value[14], value[15]]); - let opacity = f32::from_le_bytes([value[16], value[17], value[18], value[19]]); - let width = i32::from_le_bytes([value[20], value[21], value[22], value[23]]); - let height = i32::from_le_bytes([value[24], value[25], value[26], value[27]]); - - Self { - a, - b, - c, - d, - opacity, - width, - height, - } + unsafe { std::mem::transmute(value) } } } diff --git a/render-wasm/src/wasm/fills/solid.rs b/render-wasm/src/wasm/fills/solid.rs index bd4219b4b1..5b9d97286e 100644 --- a/render-wasm/src/wasm/fills/solid.rs +++ b/render-wasm/src/wasm/fills/solid.rs @@ -3,6 +3,7 @@ use crate::shapes::{Color, SolidColor}; const RAW_SOLID_DATA_SIZE: usize = 4; #[repr(C)] +#[repr(align(4))] #[derive(Debug, PartialEq, Clone, Copy)] pub struct RawSolidData { pub color: u32, @@ -10,9 +11,7 @@ pub struct RawSolidData { impl From<[u8; 4]> for RawSolidData { fn from(value: [u8; RAW_SOLID_DATA_SIZE]) -> Self { - Self { - color: u32::from_le_bytes(value), - } + unsafe { std::mem::transmute(value) } } } From 8609db21824f8d8d0f4785b990946254832a3979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 6 May 2025 12:49:49 +0200 Subject: [PATCH 11/13] :recycle: Remove unused deserialization code --- .../app/render_wasm/serializers/fills.cljs | 5 -- render-wasm/src/wasm/fills.rs | 36 +++++------- render-wasm/src/wasm/fills/gradient.rs | 57 ------------------- render-wasm/src/wasm/fills/image.rs | 29 ---------- render-wasm/src/wasm/fills/solid.rs | 33 ----------- 5 files changed, 15 insertions(+), 145 deletions(-) diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index c557bca2d8..4b21af4f4b 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -17,11 +17,6 @@ ;; FIXME: get it from the wasm module (def FILL-BYTE-SIZE (+ 4 (max GRADIENT-BYTE-SIZE IMAGE-BYTE-SIZE SOLID-BYTE-SIZE))) -;; (defn write-fill! [offset heap-u32 fill] -;; (let [dview (js/DataView. (.-buffer heap-u32))] -;; offset)) - - (defn write-solid-fill! [offset heap-u32 argb] (let [dview (js/DataView. (.-buffer heap-u32))] diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 4c54fb187b..7be256d776 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -7,10 +7,13 @@ use crate::shapes; use crate::with_current_shape; use crate::STATE; +const RAW_FILL_DATA_SIZE: usize = std::mem::size_of::(); + #[repr(C)] #[repr(align(4))] #[repr(u8)] #[derive(Debug, PartialEq, Clone, Copy)] +#[allow(dead_code)] pub enum RawFillData { Solid(solid::RawSolidData) = 0x00, Linear(gradient::RawGradientData) = 0x01, @@ -33,29 +36,20 @@ impl From for shapes::Fill { } } +impl From<[u8; RAW_FILL_DATA_SIZE]> for RawFillData { + fn from(bytes: [u8; RAW_FILL_DATA_SIZE]) -> Self { + unsafe { std::mem::transmute(bytes) } + } +} + impl TryFrom<&[u8]> for RawFillData { type Error = String; fn try_from(bytes: &[u8]) -> Result { - if bytes.len() < std::mem::size_of::() { - return Err("Invalid fill data".to_string()); - } - - let fill_type = bytes[0]; - match fill_type { - 0x00 => Ok(RawFillData::Solid(solid::RawSolidData::try_from( - &bytes[4..], - )?)), - 0x01 => Ok(RawFillData::Linear(gradient::RawGradientData::try_from( - &bytes[4..], - )?)), - 0x02 => Ok(RawFillData::Radial(gradient::RawGradientData::try_from( - &bytes[4..], - )?)), - 0x03 => Ok(RawFillData::Image(image::RawImageFillData::try_from( - &bytes[4..], - )?)), - _ => Err("Invalid fill type".to_string()), - } + let data: [u8; RAW_FILL_DATA_SIZE] = bytes + .get(0..RAW_FILL_DATA_SIZE) + .and_then(|slice| slice.try_into().ok()) + .ok_or("Invalid fill data".to_string())?; + Ok(RawFillData::from(data)) } } @@ -92,7 +86,7 @@ mod tests { fn test_raw_fill_data_from_bytes_to_solid_fill() { let mut bytes = vec![0x00; std::mem::size_of::()]; bytes[0] = 0x00; - bytes[1..=4].copy_from_slice(&0xfffabada_u32.to_le_bytes()); + bytes[4..8].copy_from_slice(&0xfffabada_u32.to_le_bytes()); let raw_fill = RawFillData::try_from(&bytes[..]); diff --git a/render-wasm/src/wasm/fills/gradient.rs b/render-wasm/src/wasm/fills/gradient.rs index 8b045f62c1..dcb2a81fa9 100644 --- a/render-wasm/src/wasm/fills/gradient.rs +++ b/render-wasm/src/wasm/fills/gradient.rs @@ -1,9 +1,6 @@ use crate::shapes::{Color, Gradient}; const MAX_GRADIENT_STOPS: usize = 16; -const BASE_GRADIENT_DATA_SIZE: usize = 28; -const RAW_GRADIENT_DATA_SIZE: usize = - BASE_GRADIENT_DATA_SIZE + RAW_STOP_DATA_SIZE * MAX_GRADIENT_STOPS; #[derive(Debug, PartialEq, Clone, Copy)] #[repr(C)] @@ -19,24 +16,6 @@ pub struct RawGradientData { stops: [RawStopData; MAX_GRADIENT_STOPS], } -impl From<[u8; RAW_GRADIENT_DATA_SIZE]> for RawGradientData { - fn from(bytes: [u8; RAW_GRADIENT_DATA_SIZE]) -> Self { - unsafe { std::mem::transmute(bytes) } - } -} - -impl TryFrom<&[u8]> for RawGradientData { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let data: [u8; RAW_GRADIENT_DATA_SIZE] = bytes - .get(0..RAW_GRADIENT_DATA_SIZE) - .and_then(|slice| slice.try_into().ok()) - .ok_or("Invalid gradient fill data".to_string())?; - Ok(RawGradientData::from(data)) - } -} - impl RawGradientData { pub fn start(&self) -> (f32, f32) { (self.start_x, self.start_y) @@ -47,8 +26,6 @@ impl RawGradientData { } } -pub const RAW_STOP_DATA_SIZE: usize = 8; - #[derive(Debug, PartialEq, Clone, Copy)] #[repr(C)] struct RawStopData { @@ -66,27 +43,6 @@ impl RawStopData { } } -impl From<[u8; RAW_STOP_DATA_SIZE]> for RawStopData { - fn from(bytes: [u8; RAW_STOP_DATA_SIZE]) -> Self { - Self { - color: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), - offset: f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), - } - } -} - -// FIXME: We won't need this once we use `array_chunks`. See comment above. -impl TryFrom<&[u8]> for RawStopData { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let data: [u8; RAW_STOP_DATA_SIZE] = bytes - .try_into() - .map_err(|_| "Invalid stop data".to_string())?; - Ok(RawStopData::from(data)) - } -} - impl From for Gradient { fn from(raw_gradient: RawGradientData) -> Self { let stops = raw_gradient @@ -105,16 +61,3 @@ impl From for Gradient { ) } } - -impl TryFrom<&[u8]> for Gradient { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let raw_gradient_bytes: [u8; RAW_GRADIENT_DATA_SIZE] = bytes[0..RAW_GRADIENT_DATA_SIZE] - .try_into() - .map_err(|_| "Invalid gradient data".to_string())?; - let gradient = RawGradientData::from(raw_gradient_bytes).into(); - - Ok(gradient) - } -} diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index 7540c3184e..b669ad2b32 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -1,7 +1,5 @@ use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet}; -const RAW_IMAGE_DATA_SIZE: usize = 28; - #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] #[repr(align(4))] @@ -23,30 +21,3 @@ impl From for ImageFill { Self::new(id, opacity, value.width, value.height) } } - -impl From<[u8; RAW_IMAGE_DATA_SIZE]> for RawImageFillData { - fn from(value: [u8; RAW_IMAGE_DATA_SIZE]) -> Self { - unsafe { std::mem::transmute(value) } - } -} - -impl TryFrom<&[u8]> for RawImageFillData { - type Error = String; - - fn try_from(value: &[u8]) -> Result { - let data: [u8; RAW_IMAGE_DATA_SIZE] = value - .get(0..RAW_IMAGE_DATA_SIZE) - .and_then(|slice| slice.try_into().ok()) - .ok_or("Invalid image fill data".to_string())?; - Ok(Self::from(data)) - } -} - -impl TryFrom<&[u8]> for ImageFill { - type Error = String; - - fn try_from(value: &[u8]) -> Result { - let raw_image_data = RawImageFillData::try_from(value)?; - Ok(raw_image_data.into()) - } -} diff --git a/render-wasm/src/wasm/fills/solid.rs b/render-wasm/src/wasm/fills/solid.rs index 5b9d97286e..164f4282e5 100644 --- a/render-wasm/src/wasm/fills/solid.rs +++ b/render-wasm/src/wasm/fills/solid.rs @@ -1,7 +1,5 @@ use crate::shapes::{Color, SolidColor}; -const RAW_SOLID_DATA_SIZE: usize = 4; - #[repr(C)] #[repr(align(4))] #[derive(Debug, PartialEq, Clone, Copy)] @@ -9,39 +7,8 @@ pub struct RawSolidData { pub color: u32, } -impl From<[u8; 4]> for RawSolidData { - fn from(value: [u8; RAW_SOLID_DATA_SIZE]) -> Self { - unsafe { std::mem::transmute(value) } - } -} - -impl TryFrom<&[u8]> for RawSolidData { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let data: [u8; RAW_SOLID_DATA_SIZE] = bytes - .get(0..RAW_SOLID_DATA_SIZE) - .and_then(|slice| slice.try_into().ok()) - .ok_or("Invalid solid fill data".to_string())?; - Ok(RawSolidData::from(data)) - } -} - impl From for SolidColor { fn from(value: RawSolidData) -> Self { Self(Color::new(value.color)) } } - -impl TryFrom<&[u8]> for SolidColor { - type Error = String; - - fn try_from(bytes: &[u8]) -> Result { - let raw_solid_bytes: [u8; 4] = bytes[0..4] - .try_into() - .map_err(|_| "Invalid solid fill data".to_string())?; - let color = RawSolidData::from(raw_solid_bytes).into(); - - Ok(color) - } -} From 6323031b403a649772b7bb1c217adecd700d4537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 6 May 2025 14:41:40 +0200 Subject: [PATCH 12/13] :books: Add serialization docs for fills --- render-wasm/docs/serialization.md | 73 +++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/render-wasm/docs/serialization.md b/render-wasm/docs/serialization.md index 7318c62a25..36cbc5833c 100644 --- a/render-wasm/docs/serialization.md +++ b/render-wasm/docs/serialization.md @@ -67,21 +67,60 @@ Paths are made of segments of **28 bytes** each. The layout (assuming positions **Flags** is not being used at the moment. -## Gradient stops +## Fills -Gradient stops are serialized in a `Uint8Array`, each stop taking **5 bytes**. +All fills take `160` bytes, but depending on the fill type, not all bytes are actually used. + +### Solid color fills + +| Offset | Length (bytes) | Data Type | Field | +| ------ | -------------- | --------- | ---------- | +| 0 | 1 | `0x00` | Fill type | +| 1 | 3 | ? | Reserved | +| 4 | 4 | `u32` | ARGB color | + +### Image fills + +| Offset | Length (bytes) | Data Type | Field | +| ------ | -------------- | --------- | --------- | +| 0 | 1 | `0x03` | Fill type | +| 1 | 3 | ? | Reserved | +| 4 | 4 | `u32` | `a` (ID) | +| 8 | 4 | `u32` | `b` (ID) | +| 12 | 4 | `u32` | `c` (ID) | +| 16 | 4 | `u32` | `d` (ID) | +| 20 | 4 | `f32` | Opacity | +| 24 | 4 | `width` | Opacity | +| 29 | 4 | `height` | Opacity | + +### Gradient fills + +| Offset | Length (bytes) | Data Type | Field | +| ------ | -------------- | ----------- | ----------- | +| 0 | 1 | `0x03` | Fill type\* | +| 1 | 3 | ? | Reserved | +| 4 | 4 | `f32` | Start `x` | +| 8 | 4 | `f32` | Start `y` | +| 12 | 4 | `f32` | End `x` | +| 16 | 4 | `f32` | End `y` | +| 20 | 4 | `f32` | Opacity | +| 24 | 4 | `f32` | Width\*\* | +| 28 | 4 | `u8` | Stop count | +| 29 | 3 | ? | Reserved | +| 32 | 128 | _See below_ | Stop data | + +\*: **Fill type** is `0x01` for linear gradients and `0x02` for radial gradients. + +\*\*: **Width** is unused in linear gradients. + +#### Gradient stop data + +Gradient stops are serialized as a sequence of `16` chunks with the following layout: | Offset | Length (bytes) | Data Type | Field | | ------ | -------------- | --------- | ----------- | -| 0 | 1 | `u8` | Red | -| 1 | 1 | `u8` | Green | -| 2 | 1 | `u8` | Blue | -| 3 | 1 | `u8` | Alpha | -| 4 | 1 | `u8` | Stop Offset | - -**Red**, **Green**, **Blue** and **Alpha** are the RGBA components of the stop. - -**Stop offset** is the offset, being integer values ranging from `0` to `100` (both inclusive). +| 0 | 4 | `u32` | ARGB Color | +| 4 | 4 | `f32` | Stop offset | ## Stroke Caps @@ -154,11 +193,11 @@ Shadow styles are serialized as `u8`: ### Grid Direction -| Value | Field | -| ----- | ------------- | -| 0 | Row | -| 1 | Column | -| \_ | error | +| Value | Field | +| ----- | ------ | +| 0 | Row | +| 1 | Column | +| \_ | error | ### Align Items @@ -265,8 +304,6 @@ Shadow styles are serialized as `u8`: | 3 | Fixed | | \_ | error | - - ## Font ### Style From 8f9298fac8f687f6148a7981ba47013c3810de79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 7 May 2025 14:55:54 +0200 Subject: [PATCH 13/13] :recycle: Remove redundant calls to add_shape_fill --- frontend/src/app/render_wasm/api.cljs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 56a38d7b6f..7a14839618 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -224,13 +224,9 @@ (h/call wasm/internal-module "_add_shape_fill")) (some? gradient) - (let [_ nil] + (do (sr-fills/write-gradient-fill! offset heap gradient opacity) - (case (:type gradient) - :linear - (h/call wasm/internal-module "_add_shape_fill") - :radial - (h/call wasm/internal-module "_add_shape_fill"))) + (h/call wasm/internal-module "_add_shape_fill")) (some? image) (let [id (dm/get-prop image :id) @@ -266,11 +262,7 @@ (some? gradient) (let [_ nil] (sr-fills/write-gradient-fill! offset heap gradient opacity) - (case (:type gradient) - :linear - (h/call wasm/internal-module "_add_shape_stroke_fill") - :radial - (h/call wasm/internal-module "_add_shape_stroke_fill"))) + (h/call wasm/internal-module "_add_shape_stroke_fill")) (some? image) (let [id (dm/get-prop image :id)