diff --git a/frontend/resources/wasm-playground/js/lib.js b/frontend/resources/wasm-playground/js/lib.js index 124b86a3c..f7d534946 100644 --- a/frontend/resources/wasm-playground/js/lib.js +++ b/frontend/resources/wasm-playground/js/lib.js @@ -79,6 +79,19 @@ function getHeapU32() { return Module.HEAPU32; } +export function clearShapeFills() { + Module._clear_shape_fills(); +} + +export function addShapeSolidFill(argb) { + const ptr = allocBytes(176); + const heap = getHeapU32(); + const dv = new DataView(heap.buffer); + dv.setUint8(ptr, 0x00, true); + dv.setUint32(ptr + 4, argb, true); + Module._add_shape_fill(); +} + export function setShapeChildren(shapeIds) { const offset = allocBytes(shapeIds.length * 16); const heap = getHeapU32(); diff --git a/frontend/resources/wasm-playground/rects.html b/frontend/resources/wasm-playground/rects.html index 120ee688b..e63eb6938 100644 --- a/frontend/resources/wasm-playground/rects.html +++ b/frontend/resources/wasm-playground/rects.html @@ -25,43 +25,50 @@ diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 9a53c3637..ef28d0a0f 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -842,9 +842,12 @@ (defn initialize [base-objects zoom vbox background] - (let [rgba (sr-clr/hex->u32argb background 1)] + (let [rgba (sr-clr/hex->u32argb background 1) + shapes (into [] (vals base-objects)) + total-shapes (count shapes)] (h/call wasm/internal-module "_set_canvas_background" rgba) (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) + (h/call wasm/internal-module "_init_shapes_pool" total-shapes) (set-objects base-objects))) (def ^:private canvas-options diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index d2adcbba0..1c22f4eb9 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -150,6 +150,13 @@ pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) { }); } +#[no_mangle] +pub extern "C" fn init_shapes_pool(capacity: usize) { + with_state!(state, { + state.init_shapes_pool(capacity); + }); +} + #[no_mangle] pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) { with_state!(state, { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index a9427400c..e3ab497b6 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -434,7 +434,7 @@ impl RenderState { pub fn start_render_loop( &mut self, - tree: &mut HashMap, + tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, timestamp: i32, @@ -501,7 +501,7 @@ impl RenderState { pub fn process_animation_frame( &mut self, - tree: &mut HashMap, + tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, timestamp: i32, @@ -600,7 +600,7 @@ impl RenderState { pub fn render_shape_tree( &mut self, - tree: &mut HashMap, + tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, timestamp: i32, @@ -855,7 +855,7 @@ impl RenderState { pub fn rebuild_tiles_shallow( &mut self, - tree: &mut HashMap, + tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, ) { @@ -864,7 +864,7 @@ impl RenderState { self.surfaces.remove_cached_tiles(); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { - if let Some(shape) = tree.get(&shape_id) { + if let Some(shape) = tree.get_mut(&shape_id) { let mut shape = shape.clone(); if shape_id != Uuid::nil() { if let Some(modifier) = modifiers.get(&shape_id) { @@ -885,7 +885,7 @@ impl RenderState { pub fn rebuild_tiles( &mut self, - tree: &mut HashMap, + tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, ) { @@ -894,7 +894,7 @@ impl RenderState { self.surfaces.remove_cached_tiles(); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { - if let Some(shape) = tree.get(&shape_id) { + if let Some(shape) = tree.get_mut(&shape_id) { let mut shape = shape.clone(); if shape_id != Uuid::nil() { if let Some(modifier) = modifiers.get(&shape_id) { @@ -914,11 +914,11 @@ impl RenderState { pub fn rebuild_modifier_tiles( &mut self, - tree: &mut HashMap, + tree: &mut HashMap, modifiers: &HashMap, ) { for (uuid, matrix) in modifiers { - if let Some(shape) = tree.get(uuid) { + if let Some(shape) = tree.get_mut(uuid) { let mut shape: Shape = shape.clone(); shape.apply_transform(matrix); self.update_tile_for(&shape); diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 491b5ba7e..1b0560e96 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -16,7 +16,7 @@ use crate::uuid::Uuid; fn propagate_children( shape: &Shape, - shapes: &HashMap, + shapes: &HashMap, parent_bounds_before: &Bounds, parent_bounds_after: &Bounds, transform: Matrix, @@ -83,7 +83,7 @@ fn propagate_children( fn calculate_group_bounds( shape: &Shape, - shapes: &HashMap, + shapes: &HashMap, bounds: &HashMap, structure: &HashMap>, ) -> Option { @@ -303,19 +303,20 @@ mod tests { #[test] fn test_propagate_shape() { - let mut shapes = HashMap::::new(); + let mut shapes = HashMap::::new(); let child_id = Uuid::new_v4(); let mut child = Shape::new(child_id); child.set_selrect(3.0, 3.0, 2.0, 2.0); - shapes.insert(child_id, child); + shapes.insert(child_id, &mut child); let parent_id = Uuid::new_v4(); let mut parent = Shape::new(parent_id); parent.set_shape_type(Type::Group(Group::default())); parent.add_child(child_id); parent.set_selrect(1.0, 1.0, 5.0, 5.0); - shapes.insert(parent_id, parent.clone()); + let mut parent_clone = parent.clone(); + shapes.insert(parent_id, &mut parent_clone); let mut transform = Matrix::scale((2.0, 1.5)); let x = parent.selrect.x(); @@ -341,17 +342,17 @@ mod tests { #[test] fn test_group_bounds() { - let mut shapes = HashMap::::new(); + let mut shapes = HashMap::::new(); let child1_id = Uuid::new_v4(); let mut child1 = Shape::new(child1_id); child1.set_selrect(3.0, 3.0, 2.0, 2.0); - shapes.insert(child1_id, child1); + shapes.insert(child1_id, &mut child1); let child2_id = Uuid::new_v4(); let mut child2 = Shape::new(child2_id); child2.set_selrect(0.0, 0.0, 1.0, 1.0); - shapes.insert(child2_id, child2); + shapes.insert(child2_id, &mut child2); let parent_id = Uuid::new_v4(); let mut parent = Shape::new(parent_id); @@ -359,7 +360,8 @@ mod tests { parent.add_child(child1_id); parent.add_child(child2_id); parent.set_selrect(0.0, 0.0, 3.0, 3.0); - shapes.insert(parent_id, parent.clone()); + let mut parent_clone = parent.clone(); + shapes.insert(parent_id, &mut parent_clone); let bounds = calculate_group_bounds(&parent, &shapes, &HashMap::new(), &HashMap::new()).unwrap(); diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 51022a35f..93a34cb0a 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -178,7 +178,7 @@ fn initialize_tracks( layout_bounds: &Bounds, layout_axis: &LayoutAxis, flex_data: &FlexData, - shapes: &HashMap, + shapes: &HashMap, bounds: &HashMap, structure: &HashMap>, ) -> Vec { @@ -420,7 +420,7 @@ fn calculate_track_data( layout_data: &LayoutData, flex_data: &FlexData, layout_bounds: &Bounds, - shapes: &HashMap, + shapes: &HashMap, bounds: &HashMap, structure: &HashMap>, ) -> Vec { @@ -551,7 +551,7 @@ pub fn reflow_flex_layout( shape: &Shape, layout_data: &LayoutData, flex_data: &FlexData, - shapes: &HashMap, + shapes: &HashMap, bounds: &mut HashMap, structure: &HashMap>, ) -> VecDeque { diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 66933b836..496d038e2 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -40,7 +40,7 @@ fn calculate_tracks( grid_data: &GridData, layout_bounds: &Bounds, cells: &Vec, - shapes: &HashMap, + shapes: &HashMap, bounds: &HashMap, ) -> Vec { let layout_size = if is_column { @@ -105,7 +105,7 @@ fn set_auto_base_size( column: bool, tracks: &mut Vec, cells: &Vec, - shapes: &HashMap, + shapes: &HashMap, bounds: &HashMap, ) { for cell in cells { @@ -156,7 +156,7 @@ fn set_auto_multi_span( column: bool, tracks: &mut Vec, cells: &Vec, - shapes: &HashMap, + shapes: &HashMap, bounds: &HashMap, ) { // Remove groups with flex (will be set in flex_multi_span) @@ -230,7 +230,7 @@ fn set_flex_multi_span( column: bool, tracks: &mut Vec, cells: &Vec, - shapes: &HashMap, + shapes: &HashMap, bounds: &HashMap, ) { // Remove groups without flex @@ -509,7 +509,7 @@ fn cell_bounds( fn create_cell_data<'a>( layout_bounds: &Bounds, children: &IndexSet, - shapes: &'a HashMap, + shapes: &'a HashMap, cells: &Vec, column_tracks: &Vec, row_tracks: &Vec, @@ -618,7 +618,7 @@ pub fn reflow_grid_layout<'a>( shape: &Shape, layout_data: &LayoutData, grid_data: &GridData, - shapes: &'a HashMap, + shapes: &'a HashMap, bounds: &mut HashMap, structure: &HashMap>, ) -> VecDeque { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 67f9129c1..dac8915a7 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -7,6 +7,54 @@ use crate::shapes::Shape; use crate::shapes::StructureEntry; use crate::uuid::Uuid; +/// A pool allocator for `Shape` objects that attempts to minimize memory reallocations. +/// +/// `ShapesPool` pre-allocates a contiguous vector of boxed `Shape` instances, +/// which can be reused and indexed efficiently. This design helps avoid +/// memory reallocation overhead by reserving enough space in advance. +/// +/// # Memory Layout +/// +/// Shapes are stored in a `Vec>`, which keeps the `Box` pointers +/// in a contiguous memory block. The actual `Shape` instances are heap-allocated, +/// and this approach ensures that pushing new shapes does not invalidate +/// previously returned mutable references. +/// +/// This is especially important because references to `Shape` are also held in the +/// state shapes attribute +pub(crate) struct ShapesPool { + // We need a box so that pushing here doesn't invalidate state.shapes references + shapes: Vec>, + counter: usize, +} + +impl ShapesPool { + pub fn new() -> Self { + ShapesPool { + shapes: vec![], + counter: 0, + } + } + + pub fn initialize(&mut self, capacity: usize) { + self.counter = 0; + self.shapes = Vec::with_capacity(capacity); + for _ in 0..capacity { + self.shapes.push(Box::new(Shape::new(Uuid::nil()))); + } + } + + pub fn add_shape(&mut self, id: Uuid) -> &mut Shape { + if self.counter >= self.shapes.len() { + self.shapes.push(Box::new(Shape::new(Uuid::nil()))); + } + let new_shape = &mut self.shapes[self.counter]; + new_shape.id = id; + self.counter += 1; + new_shape + } +} + /// This struct holds the state of the Rust application between JS calls. /// /// It is created by [init] and passed to the other exported functions. @@ -16,9 +64,10 @@ pub(crate) struct State<'a> { pub render_state: RenderState, pub current_id: Option, pub current_shape: Option<&'a mut Shape>, - pub shapes: HashMap, + pub shapes: HashMap, pub modifiers: HashMap, pub structure: HashMap>, + pub shapes_pool: ShapesPool, } impl<'a> State<'a> { @@ -30,6 +79,7 @@ impl<'a> State<'a> { shapes: HashMap::with_capacity(capacity), modifiers: HashMap::new(), structure: HashMap::new(), + shapes_pool: ShapesPool::new(), } } @@ -61,13 +111,17 @@ impl<'a> State<'a> { Ok(()) } + pub fn init_shapes_pool(&mut self, capacity: usize) { + self.shapes_pool.initialize(capacity); + } + pub fn use_shape(&'a mut self, id: Uuid) { if !self.shapes.contains_key(&id) { - let new_shape = Shape::new(id); + let new_shape = self.shapes_pool.add_shape(id); self.shapes.insert(id, new_shape); } self.current_id = Some(id); - self.current_shape = self.shapes.get_mut(&id); + self.current_shape = self.shapes.get_mut(&id).map(|r| &mut **r); } pub fn delete_shape(&mut self, id: Uuid) {