From 9c2d963ff564510daf3fad07148c35b78e504a54 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Wed, 10 Jun 2026 15:46:16 +0200 Subject: [PATCH] WIP --- render-wasm/src/main.rs | 2 +- render-wasm/src/math/bools.rs | 6 +- render-wasm/src/render.rs | 243 ++++++++++-------- render-wasm/src/render/debug.rs | 4 + render-wasm/src/render/fills.rs | 14 +- render-wasm/src/render/filters.rs | 11 +- render-wasm/src/render/fonts.rs | 10 +- render-wasm/src/render/images.rs | 50 ++-- render-wasm/src/render/options.rs | 2 +- render-wasm/src/render/strokes.rs | 7 +- render-wasm/src/render/surfaces.rs | 53 ++-- render-wasm/src/render/text.rs | 34 ++- render-wasm/src/render/ui.rs | 27 +- .../src/shapes/modifiers/grid_layout.rs | 6 +- render-wasm/src/wasm/fonts.rs | 5 + 15 files changed, 252 insertions(+), 222 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 83b3ad23f8..11feadae61 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -922,7 +922,7 @@ pub extern "C" fn get_shape_extrect(a: u32, b: u32, c: u32, d: u32) -> Result<*m let Some(shape) = state.shapes.get(&id) else { return Err(Error::CriticalError("Shape not found".to_string())); }; - let extrect = get_render_state().get_cached_extrect(shape, &state.shapes, 1.0); + let extrect = shape.extrect(&state.shapes, 1.0); let mut buf = Vec::with_capacity(16); buf.extend_from_slice(&extrect.x().to_le_bytes()); buf.extend_from_slice(&extrect.y().to_le_bytes()); diff --git a/render-wasm/src/math/bools.rs b/render-wasm/src/math/bools.rs index 0f7e01612d..1bfa68182c 100644 --- a/render-wasm/src/math/bools.rs +++ b/render-wasm/src/math/bools.rs @@ -333,11 +333,7 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec { let mut last_end = (first_bezier.end.x as f32, first_bezier.end.y as f32); let mut cur_end = first_bezier.end; - loop { - let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src) - else { - break; - }; + while let Some((next_src, next_bezier)) = find_next_in_pool(&mut pool, cur_end, cur_src) { push_bezier(&mut result, &next_bezier); last_end = (next_bezier.end.x as f32, next_bezier.end.y as f32); cur_end = next_bezier.end; diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9e867eb2a6..f5cf55c9ad 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -24,8 +24,8 @@ pub use surfaces::{SurfaceId, Surfaces}; use crate::error::{Error, Result}; use crate::math; use crate::shapes::{ - all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, - Stroke, StrokeKind, TextContent, Type, + all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Layout, Shadow, Shape, + SolidColor, Stroke, StrokeKind, TextContent, Type, }; use crate::state::{RulerState, ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; @@ -37,7 +37,7 @@ use crate::{get_gpu_state, performance}; pub use fonts::*; pub use images::*; -type ClipStack = Vec<(Rect, Option, Matrix)>; +type ClipStack = Vec<(Rect, Option, Matrix, Matrix)>; #[repr(u8)] pub enum FrameType { @@ -70,22 +70,16 @@ pub struct NodeRenderState { } /// Get simplified children of a container, flattening nested flattened containers -fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec { - let mut result = Vec::new(); - +fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape, result: &mut Vec) { for child_id in shape.children_ids_iter(false) { if let Some(child) = tree.get(child_id) { if child.can_flatten() { - // Child is flattened: recursively get its simplified children - result.extend(get_simplified_children(tree, child)); + get_simplified_children(tree, child, result); } else { - // Child is not flattened: add it directly result.push(*child_id); } } } - - result } impl NodeRenderState { @@ -108,7 +102,7 @@ impl NodeRenderState { /// and need relative positioning adjustments. fn append_clip( clip_stack: Option, - clip: (Rect, Option, Matrix), + clip: (Rect, Option, Matrix, Matrix), ) -> Option { match clip_stack { Some(mut stack) => { @@ -151,7 +145,10 @@ impl NodeRenderState { bounds.inset((clip_inset, clip_inset)); } - Self::append_clip(self.clip_bounds.clone(), (bounds, corners, transform)) + Self::append_clip( + self.clip_bounds.clone(), + (bounds, corners, transform, transform.invert().unwrap_or_default()), + ) } /// Calculates the clip bounds for shadow rendering of a given shape. @@ -205,7 +202,10 @@ impl NodeRenderState { _ => None, }; - Self::append_clip(self.clip_bounds.clone(), (bounds, corners, transform)) + Self::append_clip( + self.clip_bounds.clone(), + (bounds, corners, transform, transform.invert().unwrap_or_default()), + ) } _ => self.clip_bounds.clone(), } @@ -232,14 +232,14 @@ impl NodeRenderState { /// - `is_active()` returns whether the current shape is being rendered in focus. #[derive(Clone)] pub struct FocusMode { - shapes: Vec, + shapes: HashSet, active: bool, } impl FocusMode { pub fn new() -> Self { FocusMode { - shapes: Vec::new(), + shapes: HashSet::new(), active: false, } } @@ -250,7 +250,7 @@ impl FocusMode { } pub fn set_shapes(&mut self, shapes: Vec) { - self.shapes = shapes; + self.shapes = shapes.into_iter().collect(); } /// Returns `true` if the given shape ID should be focused. @@ -289,15 +289,12 @@ impl FocusMode { fn sort_z_index(tree: ShapesPoolRef, element: &Shape, children_ids: Vec) -> Vec { if element.has_layout() { let mut ids = children_ids; - + ids.sort_by_cached_key(|id| { + std::cmp::Reverse(tree.get(id).map(|s| s.z_index()).unwrap_or(0)) + }); if element.is_flex() && !element.is_flex_reverse() { ids.reverse(); } - ids.sort_by(|id1, id2| { - let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0); - let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0); - z2.cmp(&z1) - }); ids } else { children_ids @@ -368,14 +365,17 @@ pub(crate) struct RenderState { // Frames contained in groups must reset this nested_fills stack pushing a new empty vector. pub nested_fills: Vec>, pub nested_blurs: Vec>, // FIXME: why is this an option? + pub cached_layer_blur: Option>, pub nested_shadows: Vec>, pub show_grid: Option, + pub empty_grid_frame_ids: HashSet, pub rulers: RulerState, pub focus_mode: FocusMode, /// Viewer-only whitelist for fixed-scroll layer passes. pub include_filter: Option>, /// Frame id passed as `base_object` for viewer renders; always traversed. pub viewer_render_root: Option, + pub viewer_visible_set: Option>, pub touched_ids: HashSet, /// Temporary flag used for off-screen passes (drop-shadow masks, filter surfaces, etc.) /// where we must render shapes without inheriting ancestor layer blurs. Toggle it through @@ -499,10 +499,6 @@ impl RenderState { return false; } - if !self.backbuffer_crop_cache.contains_key(&node_id) { - return false; - } - // Additionally require this node to be safe to serve from a rectangular backbuffer // crop while moving; otherwise it must be rendered live (e.g. text, overflow frames). return tree @@ -568,12 +564,15 @@ impl RenderState { pending_tiles: PendingTiles::new(), nested_fills: vec![], nested_blurs: vec![], + cached_layer_blur: None, nested_shadows: vec![], show_grid: None, + empty_grid_frame_ids: HashSet::default(), rulers: RulerState::default(), focus_mode: FocusMode::new(), include_filter: None, viewer_render_root: None, + viewer_visible_set: None, touched_ids: HashSet::default(), ignore_nested_blurs: false, preview_mode: false, @@ -593,7 +592,10 @@ impl RenderState { /// 3. Convert the total variance back to a radius with `blur_from_variance`. /// /// This keeps blur math consistent everywhere we need to merge blur sources. - fn combined_layer_blur(&self, shape_blur: Option) -> Option { + fn combined_layer_blur(&mut self, shape_blur: Option) -> Option { + if let Some(ref cached) = self.cached_layer_blur { + return *cached; + } let mut total = 0.; for nested_blur in self.nested_blurs.iter().flatten() { @@ -602,7 +604,9 @@ impl RenderState { total += Self::blur_variance(shape_blur); - Self::blur_from_variance(total) + let result = Self::blur_from_variance(total); + self.cached_layer_blur = Some(result); + result } /// Returns the variance (radius²) for a visible layer blur, or zero if the @@ -1038,6 +1042,11 @@ impl RenderState { .draw_into(SurfaceId::InnerShadows, target, Some(&paint)); } + if self.surfaces.is_dirty(SurfaceId::DropShadows) { + self.surfaces + .draw_into(SurfaceId::DropShadows, target, Some(&paint)); + } + // Build mask of dirty surfaces that need clearing let mut dirty_surfaces_to_clear = 0u32; if self.surfaces.is_dirty(SurfaceId::Strokes) { @@ -1052,6 +1061,9 @@ impl RenderState { if self.surfaces.is_dirty(SurfaceId::TextDropShadows) { dirty_surfaces_to_clear |= SurfaceId::TextDropShadows as u32; } + if self.surfaces.is_dirty(SurfaceId::DropShadows) { + dirty_surfaces_to_clear |= SurfaceId::DropShadows as u32; + } if dirty_surfaces_to_clear != 0 { self.surfaces.apply_mut(dirty_surfaces_to_clear, |s| { @@ -1087,20 +1099,35 @@ impl RenderState { self.surfaces.clear_tile_atlas(); } - /// True when the shape or any descendant is whitelisted. - pub fn shape_visible_in_include_filter(&self, shape_id: &Uuid, tree: ShapesPoolRef) -> bool { + /// Precompute the set of all ancestor ids that are visible for the viewer + /// masked pass, avoiding recursive checks on the hot path. + fn precompute_viewer_visible_set(&mut self, tree: ShapesPoolRef) { + self.viewer_visible_set = None; let Some(ref include) = self.include_filter else { - return true; + return; }; - if include.contains(shape_id) { - return true; + let mut visible = include.clone(); + for id in include.iter() { + let mut current_id = id; + while let Some(raw) = tree.get_raw(current_id) { + match raw.parent_id { + Some(ref parent_id) => { + visible.insert(*parent_id); + current_id = parent_id; + } + None => break, + } + } + } + self.viewer_visible_set = Some(visible); + } + + /// True when the shape or any descendant is whitelisted. + pub fn shape_visible_in_include_filter(&self, shape_id: &Uuid, _tree: ShapesPoolRef) -> bool { + match &self.viewer_visible_set { + Some(visible) => visible.contains(shape_id), + None => true, } - let Some(shape) = tree.get(shape_id) else { - return false; - }; - shape - .children_ids_iter(false) - .any(|child_id| self.shape_visible_in_include_filter(child_id, tree)) } /// When an include whitelist is active, only those ids are painted. @@ -1120,28 +1147,24 @@ impl RenderState { } fn get_inherited_drop_shadows(&self) -> Option> { - let drop_shadows: Vec<&Shadow> = self + let drop_shadows: Vec = self .nested_shadows .iter() .flat_map(|shadows| shadows.iter()) .filter(|shadow| !shadow.hidden() && shadow.style() == crate::shapes::ShadowStyle::Drop) + .map(|shadow| { + let mut paint = skia_safe::Paint::default(); + let filter = shadow.get_drop_shadow_filter(); + paint.set_image_filter(filter); + paint + }) .collect(); if drop_shadows.is_empty() { - return None; + None + } else { + Some(drop_shadows) } - - Some( - drop_shadows - .into_iter() - .map(|shadow| { - let mut paint = skia_safe::Paint::default(); - let filter = shadow.get_drop_shadow_filter(); - paint.set_image_filter(filter); - paint - }) - .collect(), - ) } #[allow(clippy::too_many_arguments)] @@ -1258,7 +1281,7 @@ impl RenderState { // set clipping if let Some(clips) = clip_bounds.as_ref() { let scale = self.get_scale(); - for (mut bounds, corners, transform) in clips.iter() { + for (mut bounds, corners, transform, _inverse_transform) in clips.iter() { self.surfaces.apply_mut(surface_ids, |s| { s.canvas().concat(transform); }); @@ -1307,6 +1330,7 @@ impl RenderState { // We don't want to change the value in the global state let mut shape: Cow = Cow::Borrowed(shape); + let shape = shape.to_mut(); // Remove background blur from the shape so it doesn't get processed // as a layer blur. The actual rendering is done before the save_layer @@ -1320,25 +1344,25 @@ impl RenderState { .blur .is_some_and(|b| !b.hidden && b.blur_type == BlurType::BackgroundBlur) { - shape.to_mut().set_blur(None); + shape.set_blur(None); } - let frame_has_blur = Self::frame_clip_layer_blur(&shape).is_some(); + let frame_has_blur = Self::frame_clip_layer_blur(shape).is_some(); let shape_has_blur = shape.blur.is_some(); if self.ignore_nested_blurs { if frame_has_blur && shape_has_blur { - shape.to_mut().set_blur(None); + shape.set_blur(None); } } else if !frame_has_blur { if let Some(blur) = self.combined_layer_blur(shape.blur) { - shape.to_mut().set_blur(Some(blur)); + shape.set_blur(Some(blur)); } } else if shape_has_blur { - shape.to_mut().set_blur(None); + shape.set_blur(None); } if skip_effects { - shape.to_mut().set_blur(None); + shape.set_blur(None); } // For non-text, non-SVG shapes in the normal rendering path, apply blur @@ -1351,7 +1375,7 @@ impl RenderState { && !matches!(shape.shape_type, Type::SVGRaw(_)) { if let Some(blur) = shape.blur.filter(|b| !b.hidden) { - shape.to_mut().set_blur(None); + shape.set_blur(None); Some(blur.sigma()) } else { None @@ -1388,7 +1412,7 @@ impl RenderState { match dom_result { Ok(dom) => { dom.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id)); - shape.to_mut().set_svg(dom); + shape.set_svg(dom); } Err(e) => { eprintln!("Error parsing SVG. Error: {}", e); @@ -1439,7 +1463,7 @@ impl RenderState { text::render( Some(self), None, - &shape, + shape, &mut paragraph_builders, Some(fills_surface_id), None, @@ -1460,7 +1484,7 @@ impl RenderState { text::render_inner_stroke( Some(self), None, - &shape, + shape, &mut mask_builders, stroke_paragraphs, &mut fill_builders, @@ -1473,7 +1497,7 @@ impl RenderState { text::render_with_bounds_outset( Some(self), None, - &shape, + shape, stroke_paragraphs, Some(strokes_surface_id), None, @@ -1517,7 +1541,7 @@ impl RenderState { text::render( Some(self), None, - &shape, + shape, &mut paragraphs_with_shadows, text_drop_shadows_surface_id.into(), Some(&shadow), @@ -1529,7 +1553,7 @@ impl RenderState { } else { shadows::render_text_shadows( self, - &shape, + shape, &mut paragraphs_with_shadows, &mut stroke_paragraphs_with_shadows_list, text_drop_shadows_surface_id.into(), @@ -1546,7 +1570,7 @@ impl RenderState { text::render( Some(self), None, - &shape, + shape, &mut paragraphs_with_shadows, text_drop_shadows_surface_id.into(), Some(shadow), @@ -1561,7 +1585,7 @@ impl RenderState { text::render( Some(self), None, - &shape, + shape, &mut paragraph_builders, Some(fills_surface_id), None, @@ -1573,7 +1597,7 @@ impl RenderState { // 3. Stroke drop shadows shadows::render_text_shadows( self, - &shape, + shape, &mut paragraphs_with_shadows, &mut stroke_paragraphs_with_shadows_list, text_drop_shadows_surface_id.into(), @@ -1597,7 +1621,7 @@ impl RenderState { text::render_inner_stroke( Some(self), None, - &shape, + shape, &mut mask_builders, stroke_paragraphs, &mut fill_builders, @@ -1610,7 +1634,7 @@ impl RenderState { text::render_with_bounds_outset( Some(self), None, - &shape, + shape, stroke_paragraphs, Some(strokes_surface_id), None, @@ -1625,7 +1649,7 @@ impl RenderState { // 5. Stroke inner shadows shadows::render_text_shadows( self, - &shape, + shape, &mut paragraphs_with_shadows, &mut stroke_paragraphs_with_shadows_list, Some(innershadows_surface_id), @@ -1641,7 +1665,7 @@ impl RenderState { text::render( Some(self), None, - &shape, + shape, &mut paragraphs_with_shadows, Some(innershadows_surface_id), Some(shadow), @@ -1754,12 +1778,12 @@ impl RenderState { }; if self.options.is_debug_visible() { - let shape_selrect_bounds = self.get_shape_selrect_bounds(&shape); + let shape_selrect_bounds = self.get_shape_selrect_bounds(shape); debug::render_debug_shape(self, Some(shape_selrect_bounds), None); } if apply_to_current_surface { - self.draw_shape_surface_stack_into(Some(&shape), target_surface); + self.draw_shape_surface_stack_into(Some(shape), target_surface); } // Only restore if we saved (optimization for simple shapes) @@ -1812,7 +1836,7 @@ impl RenderState { continue; } - let doc_bounds = self.get_cached_extrect(shape, tree, 1.0); + let doc_bounds = shape.extrect(tree, 1.0); if !doc_bounds.intersects(viewport) { continue; } @@ -2133,19 +2157,31 @@ impl RenderState { self.surfaces.gc(); self.pending_nodes.clear(); - if self.pending_nodes.capacity() < tree.len() { - self.pending_nodes - .reserve(tree.len() - self.pending_nodes.capacity()); - } + self.pending_nodes.reserve(tree.len()); // Clear nested state stacks to avoid residual fills/blurs from previous renders // being incorrectly applied to new frames self.nested_fills.clear(); self.nested_blurs.clear(); + self.cached_layer_blur = None; self.nested_shadows.clear(); // reorder by distance to the center. self.current_tile = None; + + self.empty_grid_frame_ids.clear(); + if self.show_grid.is_some() { + for shape in tree.iter() { + if shape.id.is_nil() || !shape.children.is_empty() { + continue; + } + if let Type::Frame(frame) = &shape.shape_type { + if matches!(frame.layout, Some(Layout::GridLayout(_, _))) && !shape.deleted() { + self.empty_grid_frame_ids.insert(shape.id); + } + } + } + } } pub fn start_render_loop( @@ -2156,6 +2192,7 @@ impl RenderState { sync_render: bool, ) -> Result { self.clear(tree); + self.precompute_viewer_visible_set(tree); let _start = performance::begin_timed_log!("start_render_loop"); let scale = self.get_scale(); @@ -2272,7 +2309,7 @@ impl RenderState { let Some(shape) = tree.get(id) else { continue; }; - let r = self.get_cached_extrect(shape, tree, 1.0); + let r = shape.extrect(tree, 1.0); if r.is_empty() { continue; } @@ -2486,7 +2523,7 @@ impl RenderState { .surfaces .get_render_context_translation(self.render_area, scale); - for (bounds, corners, transform) in clips.iter() { + for (bounds, corners, transform, inverse_transform) in clips.iter() { let mut total_matrix = Matrix::new_identity(); if target_surface == SurfaceId::Export { let Some((export_rect, export_scale)) = self.export_context else { @@ -2510,7 +2547,7 @@ impl RenderState { } self.surfaces .canvas(target_surface) - .concat(&total_matrix.invert().unwrap_or_default()); + .concat(inverse_transform); } } @@ -2660,6 +2697,7 @@ impl RenderState { Type::Frame(_) | Type::Group(_) => { self.nested_fills.pop(); self.nested_blurs.pop(); + self.cached_layer_blur = None; self.nested_shadows.pop(); } _ => {} @@ -2750,7 +2788,7 @@ impl RenderState { pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Rect { let scale = self.get_scale(); - let rect = self.get_cached_extrect(shape, tree, scale); + let rect = shape.extrect(tree, scale); self.get_rect_bounds(rect) } @@ -3045,11 +3083,10 @@ impl RenderState { )?; if !matches!(element.shape_type, Type::Bool(_)) { - let shadow_children = if element.is_recursive() { - get_simplified_children(tree, element) - } else { - Vec::new() - }; + let mut shadow_children = Vec::new(); + if element.is_recursive() { + get_simplified_children(tree, element, &mut shadow_children); + } for shadow_shape_id in shadow_children.iter() { let Some(shadow_shape) = tree.get(shadow_shape_id) else { @@ -3166,7 +3203,7 @@ impl RenderState { for id in modifier_ids.iter() { // Current (post-modifier) bounds if let Some(s) = tree.get(id) { - let r = self.get_cached_extrect(s, tree, 1.0); + let r = s.extrect(tree, 1.0); acc = Some(match acc { None => r, Some(mut prev) => { @@ -3180,7 +3217,7 @@ impl RenderState { // shape at its original position are considered "unsafe" even after the shape // has moved away (e.g. dragging a child out of a clipped frame). if let Some(raw) = tree.get_raw(id) { - let r0 = self.get_cached_extrect(raw, tree, 1.0); + let r0 = raw.extrect(tree, 1.0); acc = Some(match acc { None => r0, Some(mut prev) => { @@ -3466,12 +3503,15 @@ impl RenderState { match element.shape_type { Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => { self.nested_blurs.push(None); + self.cached_layer_blur = None; } Type::Group(_) if element.masked_group_layer_blur().is_some() => { self.nested_blurs.push(None); + self.cached_layer_blur = None; } Type::Frame(_) | Type::Group(_) => { self.nested_blurs.push(element.blur); + self.cached_layer_blur = None; } _ => {} } @@ -3501,13 +3541,12 @@ impl RenderState { clip_inset_for_children, ); - let children_ids: Vec<_> = if can_flatten { - // Container was flattened: get simplified children (which skip this level) - get_simplified_children(tree, element) + let mut children_ids: Vec<_> = Vec::new(); + if can_flatten { + get_simplified_children(tree, element, &mut children_ids); } else { - // Container not flattened: use original children - element.children_ids_iter(false).copied().collect() - }; + children_ids = element.children_ids_iter(false).copied().collect(); + } let children_ids = sort_z_index(tree, element, children_ids); @@ -3714,7 +3753,7 @@ impl RenderState { */ pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect { let scale = self.get_scale(); - let extrect = self.get_cached_extrect(shape, tree, scale); + let extrect = shape.extrect(tree, scale); let tile_size = tiles::get_tile_size(scale); let shape_tiles = tiles::get_tiles_for_rect(extrect, tile_size); let interest_rect = &self.tile_viewbox.interest_rect; @@ -4045,10 +4084,6 @@ impl RenderState { self.touched_ids.clear(); } - pub fn get_cached_extrect(&mut self, shape: &Shape, tree: ShapesPoolRef, scale: f32) -> Rect { - shape.extrect(tree, scale) - } - pub fn set_view(&mut self, zoom: f32, x: f32, y: f32) { self.viewbox.set_all(zoom, x, y); } diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 0574e3ab28..893778b4ec 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -163,6 +163,10 @@ pub fn render_debug_shape( shape_selrect: Option, shape_extrect: Option, ) { + if shape_selrect.is_none() && shape_extrect.is_none() { + return; + } + let canvas = render_state.surfaces.canvas(SurfaceId::Debug); let mut paint = skia::Paint::default(); diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 45f8fc3048..fbe88e8025 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -27,13 +27,18 @@ fn draw_image_fill( let mut image_paint = skia::Paint::default(); image_paint.set_anti_alias(antialias); - if let Some(filter) = shape.image_filter(1.) { + let filter = shape.image_filter(1.); + if let Some(ref filter) = filter { image_paint.set_image_filter(filter.clone()); } - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint); - // Save the current canvas state - canvas.save_layer(&layer_rec); + let has_image_filter = filter.is_some(); + if has_image_filter { + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint); + canvas.save_layer(&layer_rec); + } else { + canvas.save(); + } // Set the clipping rectangle to the container bounds match &shape.shape_type { @@ -87,7 +92,6 @@ fn draw_image_fill( paint, ); - // Restore the canvas to remove the clipping canvas.restore(); } diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index 34b33f403d..aca2a27b43 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -119,11 +119,18 @@ where { let canvas = render_state.surfaces.canvas(filter_id); - canvas.clear(skia::Color::TRANSPARENT); canvas.save(); - // Apply scale first, then translate canvas.scale((scale, scale)); canvas.translate((-bounds.left, -bounds.top)); + + let scaled_bounds = Rect::new( + bounds.left, + bounds.top, + bounds.right, + bounds.bottom, + ); + canvas.clip_rect(scaled_bounds, skia::ClipOp::Intersect, false); + canvas.clear(skia::Color::TRANSPARENT); } draw_fn(render_state, filter_id)?; diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 17bdb84290..f321b58a75 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -26,6 +26,7 @@ pub struct FontStore { debug_font: Font, ui_font: Font, fallback_fonts: HashSet, + registered_families: HashSet, } impl FontStore { @@ -55,6 +56,7 @@ impl FontStore { debug_font, ui_font, fallback_fonts: HashSet::new(), + registered_families: HashSet::new(), }) } @@ -105,7 +107,7 @@ impl FontStore { }; self.font_provider.register_typeface(typeface, font_name); - self.font_collection.clear_caches(); + self.registered_families.insert(font_name.to_string()); if is_fallback { self.fallback_fonts.insert(alias); @@ -121,13 +123,17 @@ impl FontStore { } else { alias.as_str() }; - self.font_provider.family_names().any(|x| x == font_name) + self.registered_families.contains(font_name) } pub fn get_fallback(&self) -> &HashSet { &self.fallback_fonts } + pub fn flush_caches(&mut self) { + self.font_collection.clear_caches(); + } + pub fn get_emoji_font(&self, _size: f32) -> Option { None } diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index b7a25388dc..77c74f1013 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -6,6 +6,7 @@ use crate::error::Result; use crate::get_gpu_state; use skia_safe::gpu::{surfaces, Budgeted, DirectContext}; use skia_safe::{self as skia, Codec, ISize}; +use std::cell::RefCell; use std::collections::HashMap; pub type Image = skia::Image; @@ -60,8 +61,8 @@ enum StoredImage { } pub struct ImageStore { - images: HashMap<(Uuid, bool), StoredImage>, - context: Box, + images: RefCell>, + context: RefCell>, } /// Creates a Skia image from an existing WebGL texture. @@ -148,8 +149,8 @@ impl ImageStore { let gpu_state = get_gpu_state(); let context = &gpu_state.context; Self { - images: HashMap::with_capacity(2048), - context: Box::new(context.clone()), + images: RefCell::new(HashMap::new()), + context: RefCell::new(Box::new(context.clone())), } } @@ -161,16 +162,14 @@ impl ImageStore { ) -> crate::error::Result<()> { let key = (id, is_thumbnail); - if self.images.contains_key(&key) { + if self.images.borrow().contains_key(&key) { return Ok(()); } - let raw_data = image_data.to_vec(); - - if let Some(gpu_image) = decode_image(&mut self.context, &raw_data) { - self.images.insert(key, StoredImage::Gpu(gpu_image)); + if let Some(gpu_image) = decode_image(&mut self.context.borrow_mut(), image_data) { + self.images.borrow_mut().insert(key, StoredImage::Gpu(gpu_image)); } else { - self.images.insert(key, StoredImage::Raw(raw_data)); + self.images.borrow_mut().insert(key, StoredImage::Raw(image_data.to_vec())); } Ok(()) } @@ -188,43 +187,38 @@ impl ImageStore { ) -> Result<()> { let key = (id, is_thumbnail); - if self.images.contains_key(&key) { + if self.images.borrow().contains_key(&key) { return Ok(()); } // Create a Skia image from the existing GL texture - let image = create_image_from_gl_texture(&mut self.context, texture_id, width, height)?; - self.images.insert(key, StoredImage::Gpu(image)); + let image = create_image_from_gl_texture(&mut self.context.borrow_mut(), texture_id, width, height)?; + self.images.borrow_mut().insert(key, StoredImage::Gpu(image)); Ok(()) } pub fn contains(&self, id: &Uuid, is_thumbnail: bool) -> bool { - self.images.contains_key(&(*id, is_thumbnail)) + self.images.borrow().contains_key(&(*id, is_thumbnail)) } - pub fn get(&mut self, id: &Uuid) -> Option<&Image> { - // Try to get full image first, fallback to thumbnail - let has_full = self.images.contains_key(&(*id, false)); - if has_full { - self.get_internal(id, false) - } else { - self.get_internal(id, true) - } + pub fn get(&self, id: &Uuid) -> Option<&Image> { + self.get_internal(id, false) + .or_else(|| self.get_internal(id, true)) } - fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> { + fn get_internal(&self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> { let key = (*id, is_thumbnail); - // Use entry API to mutate the HashMap in-place if needed - if let Some(entry) = self.images.get_mut(&key) { + let mut images = self.images.borrow_mut(); + if let Some(entry) = images.get_mut(&key) { match entry { - StoredImage::Gpu(ref img) => Some(img), + StoredImage::Gpu(ref img) => Some(unsafe { &*(img as *const Image) }), StoredImage::Raw(raw_data) => { - let gpu_image = decode_image(&mut self.context, raw_data)?; + let gpu_image = decode_image(&mut self.context.borrow_mut(), raw_data)?; *entry = StoredImage::Gpu(gpu_image); if let StoredImage::Gpu(ref img) = entry { - Some(img) + Some(unsafe { &*(img as *const Image) }) } else { None } diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index 7a6a3a3d39..47edccb493 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -80,7 +80,7 @@ impl RenderOptions { // TODO: this will likely need to change once we have the tile atlas in place self.dpr_viewport_interest_area_threshold = ((self.dpr * self.viewport_interest_area_threshold as f32).ceil() as i32) - .min(MIN_DPR_VIEWPORT_INTEREST_AREA_THRESHOLD); + .max(MIN_DPR_VIEWPORT_INTEREST_AREA_THRESHOLD); } /// Sets the devicePixelRatio. diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 6f31573288..505c9c9117 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -297,13 +297,14 @@ fn handle_stroke_caps( blur: Option<&ImageFilter>, _antialias: bool, ) { - // Closed shapes don't have caps if !is_open { return; } - // When both ends share the same simple line cap, Skia already drew it - // natively via `PaintCap` on the stroke paint, so skip the manual overlay. + if stroke.cap_start.is_none() && stroke.cap_end.is_none() { + return; + } + if stroke.to_skia_linecap().is_some() { return; } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 867b657c7b..20912b799c 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -166,8 +166,10 @@ impl DocAtlas { new_bottom = new_bottom.max(doc_rect.bottom.ceil()); } - // Add padding to reduce realloc frequency. - let pad = tiles::TILE_SIZE; + // Geometric over-allocation: pad by 25% of extent to reduce realloc frequency. + let doc_extent_w = new_right - new_left; + let doc_extent_h = new_bottom - new_top; + let pad = (doc_extent_w.min(doc_extent_h) * 0.25_f32).max(TILE_SIZE as f32); new_left -= pad; new_top -= pad; new_right += pad; @@ -758,7 +760,11 @@ impl Surfaces { if ids & SurfaceId::Export as u32 != 0 { f(self.get_mut(SurfaceId::Export)); } - performance::begin_measure!("apply_mut::flags"); + performance::end_measure!("apply_mut::flags"); + } + + pub fn apply_one(&mut self, surface_id: SurfaceId, mut f: impl FnMut(&mut skia::Surface)) { + f(self.get_mut(surface_id)); } pub fn get_render_context_translation( @@ -789,13 +795,6 @@ impl Surfaces { s.canvas().clear(skia::Color::TRANSPARENT); }); - // Mark all render surfaces as dirty so they get redrawn - self.mark_dirty(SurfaceId::Fills); - self.mark_dirty(SurfaceId::Strokes); - self.mark_dirty(SurfaceId::InnerShadows); - self.mark_dirty(SurfaceId::TextDropShadows); - self.mark_dirty(SurfaceId::DropShadows); - // Update transformations self.apply_mut(surface_ids, |s| { let canvas = s.canvas(); @@ -1172,13 +1171,14 @@ impl Surfaces { self.atlas.tile_doc_rects.insert(*tile, tile_doc_rect); // Draws current tile into tile atlas - let tile_ref = self.tiles.add(tile_viewbox, tile); - self.tile_atlas.canvas().draw_image_rect( - &tile_image, - None, - tile_ref.rect, - &skia::Paint::default(), - ); + if let Some(tile_ref) = self.tiles.add(tile_viewbox, tile) { + self.tile_atlas.canvas().draw_image_rect( + &tile_image, + None, + tile_ref.rect, + &skia::Paint::default(), + ); + } } } @@ -1305,9 +1305,7 @@ impl Surfaces { } pub fn get_tile_image_from_tile_atlas(&mut self, tile: Tile) -> Option { - let Some(tile_ref) = self.tiles.get(tile) else { - panic!("Tile not found {}:{}", tile.0, tile.1); - }; + let tile_ref = self.tiles.get(tile)?; let rect = IRect::from_ltrb( tile_ref.rect.left as i32, @@ -1437,7 +1435,7 @@ impl Surfaces { } if let Some(surface) = self - .shape_strokes + .shape_fills .new_surface_with_dimensions((max_w, max_h)) { self.shape_fills = surface; @@ -1542,8 +1540,9 @@ impl TileTextureCache { fn gc(&mut self) { // Make a real remove - for tile in self.removed.iter() { - if let Some(tile_ref) = self.grid.remove(tile) { + let removed = std::mem::take(&mut self.removed); + for tile in removed { + if let Some(tile_ref) = self.grid.remove(&tile) { self.provider.deallocate(tile_ref); } } @@ -1618,7 +1617,7 @@ impl TileTextureCache { self.grid.contains_key(&tile) && !self.removed.contains(&tile) } - pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> TileAtlasTextureRef { + pub fn add(&mut self, tile_viewbox: &TileViewbox, tile: &Tile) -> Option { if self.grid.len() > TEXTURES_CACHE_CAPACITY { // First we try to remove the obsolete tiles. self.gc(); @@ -1631,9 +1630,7 @@ impl TileTextureCache { self.gc_non_visible(tile_viewbox); } - let Some(tile_ref) = self.provider.allocate() else { - panic!("Tile texture allocation failed {}:{}", tile.0, tile.1); - }; + let tile_ref = self.provider.allocate()?; self.grid.insert(*tile, tile_ref.clone()); @@ -1641,7 +1638,7 @@ impl TileTextureCache { self.removed.remove(tile); } - tile_ref.clone() + Some(tile_ref.clone()) } pub fn get(&mut self, tile: Tile) -> Option<&TileAtlasTextureRef> { diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index ff459481f7..8530e52796 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -28,24 +28,26 @@ pub fn stroke_paragraph_builder_group_from_text( let mut group_layer_opacity: Option = None; for paragraph in text_content.paragraphs() { - let mut stroke_paragraphs_map: std::collections::HashMap = - std::collections::HashMap::new(); + let mut stroke_paragraphs: Vec = Vec::new(); + let (stroke_paints, stroke_layer_opacity) = + get_text_stroke_paints(stroke, bounds, remove_stroke_alpha); + + if group_layer_opacity.is_none() { + group_layer_opacity = stroke_layer_opacity; + } for span in paragraph.children().iter() { - let (stroke_paints, stroke_layer_opacity) = - get_text_stroke_paints(stroke, bounds, remove_stroke_alpha); - - if group_layer_opacity.is_none() { - group_layer_opacity = stroke_layer_opacity; - } - let text: String = span.apply_text_transform(); - for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() { - let builder = stroke_paragraphs_map.entry(paint_idx).or_insert_with(|| { + if stroke_paragraphs.len() < stroke_paints.len() { + for _stroke_paint in stroke_paints.iter() { let paragraph_style = paragraph.paragraph_to_style(); - ParagraphBuilder::new(¶graph_style, fonts) - }); + stroke_paragraphs.push(ParagraphBuilder::new(¶graph_style, fonts)); + } + } + + for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() { + let builder = &mut stroke_paragraphs[paint_idx]; let stroke_paint = stroke_paint.clone(); let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent(); let stroke_style = span.to_stroke_style( @@ -59,10 +61,6 @@ pub fn stroke_paragraph_builder_group_from_text( } } - let stroke_paragraphs: Vec = (0..stroke_paragraphs_map.len()) - .filter_map(|i| stroke_paragraphs_map.remove(&i)) - .collect(); - paragraph_group.push(stroke_paragraphs); } @@ -321,7 +319,7 @@ fn draw_text( let layer_rec = SaveLayerRec::default().paint(&opacity_paint); canvas.save_layer(&layer_rec); } else { - canvas.save_layer(&SaveLayerRec::default()); + canvas.save(); } paint_text(canvas, shape, paragraph_builder_groups); diff --git a/render-wasm/src/render/ui.rs b/render-wasm/src/render/ui.rs index 2d48cdda6d..0e1e18677d 100644 --- a/render-wasm/src/render/ui.rs +++ b/render-wasm/src/render/ui.rs @@ -2,7 +2,6 @@ use skia_safe::{self as skia, Color4f}; use super::{RenderState, ShapesPoolRef, SurfaceId}; use crate::render::{grid_layout, rulers}; -use crate::shapes::{Layout, Type}; pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { let canvas = render_state.surfaces.canvas(SurfaceId::UI); @@ -28,28 +27,13 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { } // Render overlays for empty grid frames - for shape in shapes.iter() { - if shape.id.is_nil() || !shape.children.is_empty() { + let empty_grid_ids: std::collections::HashSet = + std::mem::take(&mut render_state.empty_grid_frame_ids); + for id in &empty_grid_ids { + if show_grid_id == Some(*id) { continue; } - - if show_grid_id == Some(shape.id) { - continue; - } - - let Type::Frame(frame) = &shape.shape_type else { - continue; - }; - - if !matches!(frame.layout, Some(Layout::GridLayout(_, _))) { - continue; - } - - if shape.deleted() { - continue; - } - - if let Some(shape) = shapes.get(&shape.id) { + if let Some(shape) = shapes.get(id) { grid_layout::render_overlay( zoom, render_state.options.antialias_threshold, @@ -59,6 +43,7 @@ pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { ); } } + render_state.empty_grid_frame_ids = empty_grid_ids; let viewbox = render_state.viewbox; let ruler_state = render_state.rulers; diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 7ddc877d91..b0e4ae74cd 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -234,7 +234,7 @@ fn set_auto_multi_span( track.max_size }; - let aloc = new_size - track.size; + let aloc = new_size - track.size; dist -= aloc; track.size += aloc; } @@ -314,9 +314,7 @@ fn set_flex_multi_span( // Distribute the space between flex tracks in proportion to the division for track in tracks[start..end].iter_mut() { if track.track_type == GridTrackType::Flex { - let new_size = alloc.clamp(track.size, track.max_size); - let aloc = new_size - track.size; - dist -= aloc; + let new_size = alloc.clamp(track.size, track.max_size); track.size = new_size; } } diff --git a/render-wasm/src/wasm/fonts.rs b/render-wasm/src/wasm/fonts.rs index 21c4e6a797..e96403dd78 100644 --- a/render-wasm/src/wasm/fonts.rs +++ b/render-wasm/src/wasm/fonts.rs @@ -70,3 +70,8 @@ pub extern "C" fn is_font_uploaded( res } + +#[no_mangle] +pub extern "C" fn flush_font_caches() { + get_render_state().fonts_mut().flush_caches(); +}