From bc8ca5f2a7234549491073a0fa052c67f341eab5 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 22 Apr 2026 17:51:53 +0200 Subject: [PATCH] WIP a --- render-wasm/src/main.rs | 2 + render-wasm/src/render.rs | 160 ++++++++++++++++++++++++--- render-wasm/src/state/shapes_pool.rs | 14 +++ 3 files changed, 162 insertions(+), 14 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 9f298c3900..ca3d2b10b8 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -456,6 +456,7 @@ pub extern "C" fn set_modifiers_start() -> Result<()> { let opts = &mut state.render_state.options; opts.set_fast_mode(true); opts.set_interactive_transform(true); + state.render_state.clear_picture_cache(); performance::end_measure!("set_modifiers_start"); }); Ok(()) @@ -473,6 +474,7 @@ pub extern "C" fn set_modifiers_end() -> Result<()> { let opts = &mut state.render_state.options; opts.set_fast_mode(false); opts.set_interactive_transform(false); + state.render_state.clear_picture_cache(); state.render_state.cancel_animation_frame(); performance::end_measure!("set_modifiers_end"); }); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 6272d8d9a3..e09ab4fcd0 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -15,7 +15,7 @@ mod ui; use skia_safe::{self as skia, Matrix, RRect, Rect}; use std::borrow::Cow; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use gpu_state::GpuState; @@ -334,6 +334,9 @@ pub(crate) struct RenderState { /// Cleared at the beginning of a render pass; set to true after we clear Cache the first /// time we are about to blit a tile into Cache for this pass. pub cache_cleared_this_render: bool, + /// Per-gesture cache of recorded draw commands for leaf shapes. + /// Used to speed up interactive transforms by replaying commands with a modifier matrix. + picture_cache: HashMap, } pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize { @@ -407,9 +410,14 @@ impl RenderState { preview_mode: false, export_context: None, cache_cleared_this_render: false, + picture_cache: HashMap::new(), }) } + pub fn clear_picture_cache(&mut self) { + self.picture_cache.clear(); + } + /// Combines every visible layer blur currently active (ancestors + shape) /// into a single equivalent blur. Layer blur radii compound by adding their /// variances (σ² = radius²), so we: @@ -1674,6 +1682,15 @@ impl RenderState { self.cache_cleared_this_render = false; self.reset_canvas(); + // `picture_cache` is per-gesture and is cleared from the WASM exports + // (`set_modifiers_start`/`set_modifiers_end`). Keep this method side-effect free. + + // Compute and set document-space bounds (1 unit == 1 doc px @ 100% zoom) + // to clamp atlas updates. This prevents zoom-out tiles from forcing atlas + // growth far beyond real content. + let doc_bounds = self.compute_document_bounds(base_object, tree); + self.surfaces.set_atlas_doc_bounds(doc_bounds); + // During an interactive shape transform (drag/resize/rotate) the // Target is repainted tile-by-tile. If only a subset of the // invalidated tiles finishes in this rAF the remaining area @@ -1767,6 +1784,64 @@ impl RenderState { Ok(()) } + fn get_or_record_simple_rect_picture( + &mut self, + shape_id: &Uuid, + base_shape: &Shape, + fill: &Fill, + antialias: bool, + ) -> Option { + if let Some(picture) = self.picture_cache.get(shape_id) { + return Some(picture.clone()); + } + + let bounds = base_shape.selrect(); + if bounds.is_empty() { + return None; + } + + let mut recorder = skia::PictureRecorder::new(); + let canvas = recorder.begin_recording(bounds, false); + let mut paint = fill.to_paint(&bounds, antialias); + canvas.draw_rect(bounds, &paint); + drop(paint); + let picture = recorder.finish_recording_as_picture(None)?; + + self.picture_cache.insert(*shape_id, picture.clone()); + Some(picture) + } + + fn compute_document_bounds( + &mut self, + base_object: Option<&Uuid>, + tree: ShapesPoolRef, + ) -> Option { + let ids: Vec = if let Some(id) = base_object { + vec![*id] + } else { + let root = tree.get(&Uuid::nil())?; + root.children_ids(false) + }; + + let mut acc: Option = None; + for id in ids.iter() { + let Some(shape) = tree.get(id) else { + continue; + }; + let r = self.get_cached_extrect(shape, tree, 1.0); + if r.is_empty() { + continue; + } + acc = Some(if let Some(mut a) = acc { + a.join(r); + a + } else { + r + }); + } + acc + } + pub fn process_animation_frame( &mut self, base_object: Option<&Uuid>, @@ -2741,19 +2816,76 @@ impl RenderState { .draw_into(SurfaceId::DropShadows, target_surface, None); } - self.render_shape( - element, - clip_bounds.clone(), - SurfaceId::Fills, - SurfaceId::Strokes, - SurfaceId::InnerShadows, - SurfaceId::TextDropShadows, - true, - None, - None, - None, - target_surface, - )?; + // Interactive-transform fast path: replay a recorded Picture for very simple + // leaf rects, applying only the modifier matrix per frame. + let mut rendered_via_picture = false; + if self.options.is_interactive_transform() + && !element.is_recursive() + && clip_bounds.is_none() + && element.transform.is_identity() + && element.opacity() == 1.0 + && element.shadows.is_empty() + && element.blur.is_none() + && element.strokes.is_empty() + && matches!(element.shape_type, Type::Rect(_)) + && element.fills.len() == 1 + && matches!(element.fills[0], Fill::Solid(_)) + && target_surface != SurfaceId::Export + { + if let (Some(modifier), Some(base_shape)) = + (tree.get_modifier(&node_id), tree.get_base(&node_id)) + { + if base_shape.transform.is_identity() + && base_shape.fills.len() == 1 + && matches!(base_shape.fills[0], Fill::Solid(_)) + { + let antialias = base_shape.should_use_antialias( + self.get_scale(), + self.options.antialias_threshold, + ); + if let Some(picture) = self.get_or_record_simple_rect_picture( + &node_id, + base_shape, + &base_shape.fills[0], + antialias, + ) { + println!("draw picture"); + let picture = picture.clone(); + let scale = self.get_scale(); + let translation = self + .surfaces + .get_render_context_translation(self.render_area, scale); + self.surfaces.apply_mut(target_surface as u32, |s| { + let canvas = s.canvas(); + canvas.save(); + canvas.scale((scale, scale)); + canvas.translate(translation); + canvas.concat(modifier); + canvas.draw_picture(&picture, None, None); + canvas.restore(); + }); + rendered_via_picture = true; + } + } + } + } + + if !rendered_via_picture { + println!("render_shape"); + self.render_shape( + element, + clip_bounds.clone(), + SurfaceId::Fills, + SurfaceId::Strokes, + SurfaceId::InnerShadows, + SurfaceId::TextDropShadows, + true, + None, + None, + None, + target_surface, + )?; + } self.surfaces .canvas(SurfaceId::DropShadows) diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index 7e03befa01..63b849b2a9 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -177,6 +177,20 @@ impl ShapesPoolImpl { } } + /// Returns the base (unmodified) shape as stored in the pool. + /// + /// Unlike `get`, this does **not** apply transient modifiers/structure/scale_content. + pub fn get_base(&self, id: &Uuid) -> Option<&Shape> { + let idx = *self.uuid_to_idx.get(id)?; + Some(&self.shapes[idx]) + } + + /// Returns the current transient modifier matrix for `id` if present. + pub fn get_modifier(&self, id: &Uuid) -> Option<&skia::Matrix> { + let idx = *self.uuid_to_idx.get(id)?; + self.modifiers.get(&idx) + } + // Given an id, returns the depth in the tree-shaped structure // of shapes. pub fn get_depth(&self, id: &Uuid) -> usize {