From b892cc9b1485112b1cc40caac1dc9b95ba661f11 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Thu, 12 Feb 2026 16:14:09 +0100 Subject: [PATCH] :wrench: Refactor shape base props to use transmute --- render-wasm/src/wasm/shapes/base_props.rs | 352 ++++++++++++++-------- 1 file changed, 219 insertions(+), 133 deletions(-) diff --git a/render-wasm/src/wasm/shapes/base_props.rs b/render-wasm/src/wasm/shapes/base_props.rs index 428c5e18fa..265e4f7841 100644 --- a/render-wasm/src/wasm/shapes/base_props.rs +++ b/render-wasm/src/wasm/shapes/base_props.rs @@ -8,166 +8,252 @@ use crate::{with_state_mut, STATE}; use super::RawShapeType; -/// Binary layout for batched shape base properties: -/// -/// | Offset | Size | Field | Type | -/// |--------|------|--------------|-----------------------------------| -/// | 0 | 16 | id | UUID (4 × u32 LE) | -/// | 16 | 16 | parent_id | UUID (4 × u32 LE) | -/// | 32 | 1 | shape_type | u8 | -/// | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) | -/// | 34 | 1 | blend_mode | u8 | -/// | 35 | 1 | constraint_h | u8 (0xFF = None) | -/// | 36 | 1 | constraint_v | u8 (0xFF = None) | -/// | 37 | 3 | padding | - | -/// | 40 | 4 | opacity | f32 LE | -/// | 44 | 4 | rotation | f32 LE | -/// | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) | -/// | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) | -/// | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) | -/// |--------|------|--------------|-----------------------------------| -/// | Total | 104 | | | -pub const BASE_PROPS_SIZE: usize = 104; - const FLAG_CLIP_CONTENT: u8 = 0b0000_0001; const FLAG_HIDDEN: u8 = 0b0000_0010; const CONSTRAINT_NONE: u8 = 0xFF; -/// Reads a f32 from a byte slice at the given offset (little-endian) -#[inline] -fn read_f32_le(bytes: &[u8], offset: usize) -> f32 { - f32::from_le_bytes([ - bytes[offset], - bytes[offset + 1], - bytes[offset + 2], - bytes[offset + 3], - ]) +const RAW_BASE_PROPS_SIZE: usize = std::mem::size_of::(); + +/// Binary layout for batched shape base properties. +/// +/// The struct fields directly mirror the binary protocol — the layout +/// documentation lives in the struct definition itself via `#[repr(C)]`. +#[repr(C)] +#[repr(align(4))] +#[derive(Debug, Clone, Copy)] +pub struct RawBasePropsData { + // UUID id (16 bytes) + id_a: u32, + id_b: u32, + id_c: u32, + id_d: u32, + // UUID parent_id (16 bytes) + parent_a: u32, + parent_b: u32, + parent_c: u32, + parent_d: u32, + // Single-byte fields + shape_type: u8, + flags: u8, + blend_mode: u8, + constraint_h: u8, + constraint_v: u8, + padding: [u8; 3], + // f32 fields + opacity: f32, + rotation: f32, + // Transform matrix (a, b, c, d, e, f) + transform_a: f32, + transform_b: f32, + transform_c: f32, + transform_d: f32, + transform_e: f32, + transform_f: f32, + // Selrect (x1, y1, x2, y2) + selrect_x1: f32, + selrect_y1: f32, + selrect_x2: f32, + selrect_y2: f32, + // Corners (r1, r2, r3, r4) + corner_r1: f32, + corner_r2: f32, + corner_r3: f32, + corner_r4: f32, } -/// Reads a u32 from a byte slice at the given offset (little-endian) -#[inline] -fn read_u32_le(bytes: &[u8], offset: usize) -> u32 { - u32::from_le_bytes([ - bytes[offset], - bytes[offset + 1], - bytes[offset + 2], - bytes[offset + 3], - ]) +impl RawBasePropsData { + fn id(&self) -> Uuid { + uuid_from_u32_quartet(self.id_a, self.id_b, self.id_c, self.id_d) + } + + fn parent_id(&self) -> Uuid { + uuid_from_u32_quartet(self.parent_a, self.parent_b, self.parent_c, self.parent_d) + } + + fn clip_content(&self) -> bool { + (self.flags & FLAG_CLIP_CONTENT) != 0 + } + + fn hidden(&self) -> bool { + (self.flags & FLAG_HIDDEN) != 0 + } + + fn blend_mode(&self) -> BlendMode { + RawBlendMode::from(self.blend_mode).into() + } + + fn constraint_h(&self) -> Option { + if self.constraint_h == CONSTRAINT_NONE { + None + } else { + Some(RawConstraintH::from(self.constraint_h).into()) + } + } + + fn constraint_v(&self) -> Option { + if self.constraint_v == CONSTRAINT_NONE { + None + } else { + Some(RawConstraintV::from(self.constraint_v).into()) + } + } } -/// Parses UUID from bytes at given offset -#[inline] -fn read_uuid(bytes: &[u8], offset: usize) -> Uuid { - uuid_from_u32_quartet( - read_u32_le(bytes, offset), - read_u32_le(bytes, offset + 4), - read_u32_le(bytes, offset + 8), - read_u32_le(bytes, offset + 12), - ) +impl From<[u8; RAW_BASE_PROPS_SIZE]> for RawBasePropsData { + fn from(bytes: [u8; RAW_BASE_PROPS_SIZE]) -> Self { + unsafe { std::mem::transmute(bytes) } + } } #[no_mangle] pub extern "C" fn set_shape_base_props() { let bytes = mem::bytes(); - if bytes.len() < BASE_PROPS_SIZE { + if bytes.len() < RAW_BASE_PROPS_SIZE { return; } - // Parse all fields from the buffer - let id = read_uuid(&bytes, 0); - let parent_id = read_uuid(&bytes, 16); - let shape_type = bytes[32]; - let flags = bytes[33]; - let blend_mode = bytes[34]; - let constraint_h = bytes[35]; - let constraint_v = bytes[36]; - // bytes[37..40] are padding + let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE].try_into().unwrap(); + let raw = RawBasePropsData::from(data); - let opacity = read_f32_le(&bytes, 40); - let rotation = read_f32_le(&bytes, 44); - - // Transform matrix (a, b, c, d, e, f) - let transform_a = read_f32_le(&bytes, 48); - let transform_b = read_f32_le(&bytes, 52); - let transform_c = read_f32_le(&bytes, 56); - let transform_d = read_f32_le(&bytes, 60); - let transform_e = read_f32_le(&bytes, 64); - let transform_f = read_f32_le(&bytes, 68); - - // Selrect (x1, y1, x2, y2) - let selrect_x1 = read_f32_le(&bytes, 72); - let selrect_y1 = read_f32_le(&bytes, 76); - let selrect_x2 = read_f32_le(&bytes, 80); - let selrect_y2 = read_f32_le(&bytes, 84); - - // Corners (r1, r2, r3, r4) - let corner_r1 = read_f32_le(&bytes, 88); - let corner_r2 = read_f32_le(&bytes, 92); - let corner_r3 = read_f32_le(&bytes, 96); - let corner_r4 = read_f32_le(&bytes, 100); - - // Decode flags - let clip_content = (flags & FLAG_CLIP_CONTENT) != 0; - let hidden = (flags & FLAG_HIDDEN) != 0; - - // Convert raw enum values - let shape_type_enum = RawShapeType::from(shape_type); - let blend_mode_enum: BlendMode = RawBlendMode::from(blend_mode).into(); - - let constraint_h_opt: Option = if constraint_h == CONSTRAINT_NONE { - None - } else { - Some(RawConstraintH::from(constraint_h).into()) - }; - - let constraint_v_opt: Option = if constraint_v == CONSTRAINT_NONE { - None - } else { - Some(RawConstraintV::from(constraint_v).into()) - }; + let id = raw.id(); + let parent_id = raw.parent_id(); + let shape_type = RawShapeType::from(raw.shape_type); with_state_mut!(state, { - // Select/create the shape state.use_shape(id); - - // Set parent relationship state.set_parent_for_current_shape(parent_id); - - // Mark shape as touched state.touch_current(); - // Apply all properties to the current shape if let Some(shape) = state.current_shape_mut() { - // Type - shape.set_shape_type(shape_type_enum.into()); - - // Boolean flags - shape.set_clip(clip_content); - shape.set_hidden(hidden); - - // Blend mode and opacity - shape.set_blend_mode(blend_mode_enum); - shape.set_opacity(opacity); - - // Constraints - shape.set_constraint_h(constraint_h_opt); - shape.set_constraint_v(constraint_v_opt); - - // Transform - shape.set_rotation(rotation); + shape.set_shape_type(shape_type.into()); + shape.set_clip(raw.clip_content()); + shape.set_hidden(raw.hidden()); + shape.set_blend_mode(raw.blend_mode()); + shape.set_opacity(raw.opacity); + shape.set_constraint_h(raw.constraint_h()); + shape.set_constraint_v(raw.constraint_v()); + shape.set_rotation(raw.rotation); shape.set_transform( - transform_a, - transform_b, - transform_c, - transform_d, - transform_e, - transform_f, + raw.transform_a, + raw.transform_b, + raw.transform_c, + raw.transform_d, + raw.transform_e, + raw.transform_f, ); - - // Geometry - shape.set_selrect(selrect_x1, selrect_y1, selrect_x2, selrect_y2); - shape.set_corners((corner_r1, corner_r2, corner_r3, corner_r4)); + shape.set_selrect( + raw.selrect_x1, + raw.selrect_y1, + raw.selrect_x2, + raw.selrect_y2, + ); + shape.set_corners((raw.corner_r1, raw.corner_r2, raw.corner_r3, raw.corner_r4)); } }); } + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper: builds a 104-byte buffer with all zeros, then lets the + /// caller poke specific offsets before transmuting. + fn make_bytes() -> [u8; RAW_BASE_PROPS_SIZE] { + [0u8; RAW_BASE_PROPS_SIZE] + } + + fn raw_from(bytes: &[u8; RAW_BASE_PROPS_SIZE]) -> RawBasePropsData { + RawBasePropsData::from(*bytes) + } + + #[test] + fn test_raw_base_props_layout() { + assert_eq!(RAW_BASE_PROPS_SIZE, 104); + assert_eq!(std::mem::align_of::(), 4); + } + + #[test] + fn test_field_offsets_match_binary_protocol() { + // Verify that key struct fields sit at the documented byte offsets. + assert_eq!(std::mem::offset_of!(RawBasePropsData, id_a), 0); + assert_eq!(std::mem::offset_of!(RawBasePropsData, parent_a), 16); + assert_eq!(std::mem::offset_of!(RawBasePropsData, shape_type), 32); + assert_eq!(std::mem::offset_of!(RawBasePropsData, flags), 33); + assert_eq!(std::mem::offset_of!(RawBasePropsData, blend_mode), 34); + assert_eq!(std::mem::offset_of!(RawBasePropsData, constraint_h), 35); + assert_eq!(std::mem::offset_of!(RawBasePropsData, constraint_v), 36); + assert_eq!(std::mem::offset_of!(RawBasePropsData, padding), 37); + assert_eq!(std::mem::offset_of!(RawBasePropsData, opacity), 40); + assert_eq!(std::mem::offset_of!(RawBasePropsData, rotation), 44); + assert_eq!(std::mem::offset_of!(RawBasePropsData, transform_a), 48); + assert_eq!(std::mem::offset_of!(RawBasePropsData, selrect_x1), 72); + assert_eq!(std::mem::offset_of!(RawBasePropsData, corner_r1), 88); + } + + #[test] + fn test_full_deserialization() { + let mut bytes = make_bytes(); + // id + bytes[0..4].copy_from_slice(&1_u32.to_le_bytes()); + bytes[4..8].copy_from_slice(&2_u32.to_le_bytes()); + bytes[8..12].copy_from_slice(&3_u32.to_le_bytes()); + bytes[12..16].copy_from_slice(&4_u32.to_le_bytes()); + // parent_id + bytes[16..20].copy_from_slice(&5_u32.to_le_bytes()); + bytes[20..24].copy_from_slice(&6_u32.to_le_bytes()); + bytes[24..28].copy_from_slice(&7_u32.to_le_bytes()); + bytes[28..32].copy_from_slice(&8_u32.to_le_bytes()); + // shape_type = Rect (3) + bytes[32] = 3; + // flags = clip + hidden + bytes[33] = FLAG_CLIP_CONTENT | FLAG_HIDDEN; + // blend_mode = Overlay (15) + bytes[34] = 15; + // constraint_h = Center (3) + bytes[35] = 3; + // constraint_v = Scale (4) + bytes[36] = 4; + // opacity + bytes[40..44].copy_from_slice(&0.5_f32.to_le_bytes()); + // rotation + bytes[44..48].copy_from_slice(&90.0_f32.to_le_bytes()); + // transform (a=2, b=0, c=0, d=2, e=50, f=60) + bytes[48..52].copy_from_slice(&2.0_f32.to_le_bytes()); + bytes[52..56].copy_from_slice(&0.0_f32.to_le_bytes()); + bytes[56..60].copy_from_slice(&0.0_f32.to_le_bytes()); + bytes[60..64].copy_from_slice(&2.0_f32.to_le_bytes()); + bytes[64..68].copy_from_slice(&50.0_f32.to_le_bytes()); + bytes[68..72].copy_from_slice(&60.0_f32.to_le_bytes()); + // selrect + bytes[72..76].copy_from_slice(&0.0_f32.to_le_bytes()); + bytes[76..80].copy_from_slice(&0.0_f32.to_le_bytes()); + bytes[80..84].copy_from_slice(&100.0_f32.to_le_bytes()); + bytes[84..88].copy_from_slice(&200.0_f32.to_le_bytes()); + // corners + bytes[88..92].copy_from_slice(&4.0_f32.to_le_bytes()); + bytes[92..96].copy_from_slice(&8.0_f32.to_le_bytes()); + bytes[96..100].copy_from_slice(&12.0_f32.to_le_bytes()); + bytes[100..104].copy_from_slice(&16.0_f32.to_le_bytes()); + + let raw = raw_from(&bytes); + + assert_eq!(raw.id(), uuid_from_u32_quartet(1, 2, 3, 4)); + assert_eq!(raw.parent_id(), uuid_from_u32_quartet(5, 6, 7, 8)); + assert_eq!(raw.shape_type, 3); // Rect + assert!(raw.clip_content()); + assert!(raw.hidden()); + assert_eq!(raw.blend_mode(), BlendMode(skia_safe::BlendMode::Overlay)); + assert_eq!(raw.constraint_h(), Some(ConstraintH::Center)); + assert_eq!(raw.constraint_v(), Some(ConstraintV::Scale)); + assert_eq!(raw.opacity, 0.5); + assert_eq!(raw.rotation, 90.0); + assert_eq!(raw.transform_a, 2.0); + assert_eq!(raw.transform_e, 50.0); + assert_eq!(raw.transform_f, 60.0); + assert_eq!(raw.selrect_x1, 0.0); + assert_eq!(raw.selrect_y2, 200.0); + assert_eq!(raw.corner_r1, 4.0); + assert_eq!(raw.corner_r4, 16.0); + } +}