From ee766e85a0b49847c1adf7c21c3722169dadd52a Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 12 Jan 2026 12:03:03 +0100 Subject: [PATCH] :tada: Wasm render dirty surfaces --- render-wasm/src/render.rs | 67 ++++++++++++------ render-wasm/src/render/fills.rs | 2 +- render-wasm/src/render/filters.rs | 2 +- render-wasm/src/render/shadows.rs | 2 +- render-wasm/src/render/strokes.rs | 6 +- render-wasm/src/render/surfaces.rs | 105 +++++++++++++++++++++++------ render-wasm/src/render/text.rs | 6 +- 7 files changed, 141 insertions(+), 49 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index d3ac74b422..9e0fddeec3 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -540,38 +540,59 @@ impl RenderState { let paint = skia::Paint::default(); - self.surfaces - .draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint)); + // Only draw surfaces that have content (dirty flag optimization) + if self.surfaces.is_dirty(SurfaceId::TextDropShadows) { + self.surfaces + .draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint)); + } - self.surfaces - .draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint)); + if self.surfaces.is_dirty(SurfaceId::Fills) { + self.surfaces + .draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint)); + } let mut render_overlay_below_strokes = false; if let Some(shape) = shape { render_overlay_below_strokes = shape.has_fills(); } - if render_overlay_below_strokes { + if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) { self.surfaces .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); } - self.surfaces - .draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint)); + if self.surfaces.is_dirty(SurfaceId::Strokes) { + self.surfaces + .draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint)); + } - if !render_overlay_below_strokes { + if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) { self.surfaces .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); } - let surface_ids = SurfaceId::Strokes as u32 - | SurfaceId::Fills as u32 - | SurfaceId::InnerShadows as u32 - | SurfaceId::TextDropShadows as u32; + // Build mask of dirty surfaces that need clearing + let mut dirty_surfaces_to_clear = 0u32; + if self.surfaces.is_dirty(SurfaceId::Strokes) { + dirty_surfaces_to_clear |= SurfaceId::Strokes as u32; + } + if self.surfaces.is_dirty(SurfaceId::Fills) { + dirty_surfaces_to_clear |= SurfaceId::Fills as u32; + } + if self.surfaces.is_dirty(SurfaceId::InnerShadows) { + dirty_surfaces_to_clear |= SurfaceId::InnerShadows as u32; + } + if self.surfaces.is_dirty(SurfaceId::TextDropShadows) { + dirty_surfaces_to_clear |= SurfaceId::TextDropShadows as u32; + } - self.surfaces.apply_mut(surface_ids, |s| { - s.canvas().clear(skia::Color::TRANSPARENT); - }); + if dirty_surfaces_to_clear != 0 { + self.surfaces.apply_mut(dirty_surfaces_to_clear, |s| { + s.canvas().clear(skia::Color::TRANSPARENT); + }); + // Clear dirty flags for surfaces we just cleared + self.surfaces.clear_dirty(dirty_surfaces_to_clear); + } } pub fn clear_focus_mode(&mut self) { @@ -710,16 +731,18 @@ impl RenderState { matrix.pre_concat(&svg_transform); } - self.surfaces.canvas(fills_surface_id).concat(&matrix); + self.surfaces + .canvas_and_mark_dirty(fills_surface_id) + .concat(&matrix); if let Some(svg) = shape.svg.as_ref() { - svg.render(self.surfaces.canvas(fills_surface_id)) + svg.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id)); } else { let font_manager = skia::FontMgr::from(self.fonts().font_provider().clone()); let dom_result = skia::svg::Dom::from_str(&sr.content, font_manager); match dom_result { Ok(dom) => { - dom.render(self.surfaces.canvas(fills_surface_id)); + dom.render(self.surfaces.canvas_and_mark_dirty(fills_surface_id)); shape.to_mut().set_svg(dom); } Err(e) => { @@ -1914,7 +1937,9 @@ impl RenderState { }; // Check if cache is valid (same root_ids) - let cache_valid = self.cached_root_ids.as_ref() + let cache_valid = self + .cached_root_ids + .as_ref() .map(|cached| cached.as_slice() == root_ids.as_slice()) .unwrap_or(false); @@ -1928,10 +1953,10 @@ impl RenderState { .enumerate() .map(|(i, id)| (*id, i)) .collect(); - + self.cached_root_ids = Some(root_ids.clone()); self.cached_root_ids_map = Some(root_ids_map.clone()); - + root_ids_map } }; diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index a2a6e748a6..1d8ad98084 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -18,7 +18,7 @@ fn draw_image_fill( } let size = image.unwrap().dimensions(); - let canvas = render_state.surfaces.canvas(surface_id); + let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id); let container = &shape.selrect; let path_transform = shape.to_path_transform(); diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index bddd5ab26a..832fc32d88 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -41,7 +41,7 @@ where F: FnOnce(&mut RenderState, SurfaceId), { if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) { - let canvas = render_state.surfaces.canvas(target_surface); + let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); // If we scaled down, we need to scale the source rect and adjust the destination if scale < 1.0 { diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index f5f4fbfccf..64a6d7533a 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -135,7 +135,7 @@ pub fn render_text_shadows( let canvas = render_state .surfaces - .canvas(surface_id.unwrap_or(SurfaceId::TextDropShadows)); + .canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::TextDropShadows)); for shadow in shadows { let shadow_layer = SaveLayerRec::default().paint(shadow); diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 5e4f02c8e3..103831013a 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -387,7 +387,7 @@ fn draw_image_stroke_in_container( } let size = image.unwrap().dimensions(); - let canvas = render_state.surfaces.canvas(surface_id); + let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id); let container = &shape.selrect; let path_transform = shape.to_path_transform(); let svg_attrs = shape.svg_attrs.as_ref(); @@ -606,7 +606,7 @@ fn render_internal( let scale = render_state.get_scale(); let target_surface = surface_id.unwrap_or(SurfaceId::Strokes); - let canvas = render_state.surfaces.canvas(target_surface); + let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); let selrect = shape.selrect; let path_transform = shape.to_path_transform(); let svg_attrs = shape.svg_attrs.as_ref(); @@ -688,7 +688,7 @@ pub fn render_text_paths( let scale = render_state.get_scale(); let canvas = render_state .surfaces - .canvas(surface_id.unwrap_or(SurfaceId::Strokes)); + .canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes)); let selrect = &shape.selrect; let svg_attrs = shape.svg_attrs.as_ref(); let mut paint: skia_safe::Handle<_> = diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index fe0edbb455..00792109d8 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -55,6 +55,8 @@ pub struct Surfaces { tiles: TileTextureCache, sampling_options: skia::SamplingOptions, margins: skia::ISize, + // Tracks which surfaces have content (dirty flag bitmask) + dirty_surfaces: u32, } #[allow(dead_code)] @@ -105,6 +107,7 @@ impl Surfaces { tiles, sampling_options, margins, + dirty_surfaces: 0, } } @@ -147,10 +150,51 @@ impl Surfaces { None } + /// Returns a mutable reference to the canvas and automatically marks + /// render surfaces as dirty when accessed. This tracks which surfaces + /// have content for optimization purposes. + pub fn canvas_and_mark_dirty(&mut self, id: SurfaceId) -> &skia::Canvas { + // Automatically mark render surfaces as dirty when accessed + // This tracks which surfaces have content for optimization + match id { + SurfaceId::Fills + | SurfaceId::Strokes + | SurfaceId::InnerShadows + | SurfaceId::TextDropShadows => { + self.mark_dirty(id); + } + _ => {} + } + self.canvas(id) + } + + /// Returns a mutable reference to the canvas without any side effects. + /// Use this when you only need to read or manipulate the canvas state + /// without marking the surface as dirty. pub fn canvas(&mut self, id: SurfaceId) -> &skia::Canvas { self.get_mut(id).canvas() } + /// Marks a surface as having content (dirty) + pub fn mark_dirty(&mut self, id: SurfaceId) { + self.dirty_surfaces |= id as u32; + } + + /// Checks if a surface has content + pub fn is_dirty(&self, id: SurfaceId) -> bool { + (self.dirty_surfaces & id as u32) != 0 + } + + /// Clears the dirty flag for a surface or set of surfaces + pub fn clear_dirty(&mut self, ids: u32) { + self.dirty_surfaces &= !ids; + } + + /// Clears all dirty flags + pub fn clear_all_dirty(&mut self) { + self.dirty_surfaces = 0; + } + pub fn flush_and_submit(&mut self, gpu_state: &mut GpuState, id: SurfaceId) { let surface = self.get_mut(id); gpu_state.context.flush_and_submit_surface(surface, None); @@ -159,9 +203,12 @@ impl Surfaces { pub fn draw_into(&mut self, from: SurfaceId, to: SurfaceId, paint: Option<&skia::Paint>) { let sampling_options = self.sampling_options; - self.get_mut(from) - .clone() - .draw(self.canvas(to), (0.0, 0.0), sampling_options, paint); + self.get_mut(from).clone().draw( + self.canvas_and_mark_dirty(to), + (0.0, 0.0), + sampling_options, + paint, + ); } pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) { @@ -212,18 +259,33 @@ impl Surfaces { pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) { let translation = self.get_render_context_translation(render_area, scale); - self.apply_mut( - SurfaceId::Fills as u32 - | SurfaceId::Strokes as u32 - | SurfaceId::InnerShadows as u32 - | SurfaceId::TextDropShadows as u32, - |s| { - let canvas = s.canvas(); - canvas.reset_matrix(); - canvas.scale((scale, scale)); - canvas.translate(translation); - }, - ); + + // When context changes (zoom/pan/tile), clear all render surfaces first + // to remove any residual content from previous tiles, then mark as dirty + // so they get redrawn with new transformations + let surface_ids = SurfaceId::Fills as u32 + | SurfaceId::Strokes as u32 + | SurfaceId::InnerShadows as u32 + | SurfaceId::TextDropShadows as u32; + + // Clear surfaces before updating transformations to remove residual content + self.apply_mut(surface_ids, |s| { + 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); + + // Update transformations + self.apply_mut(surface_ids, |s| { + let canvas = s.canvas(); + canvas.reset_matrix(); + canvas.scale((scale, scale)); + canvas.translate(translation); + }); } #[inline] @@ -264,19 +326,21 @@ impl Surfaces { pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { if let Some(corners) = shape.shape_type.corners() { let rrect = RRect::new_rect_radii(shape.selrect, &corners); - self.canvas(id).draw_rrect(rrect, paint); + self.canvas_and_mark_dirty(id).draw_rrect(rrect, paint); } else { - self.canvas(id).draw_rect(shape.selrect, paint); + self.canvas_and_mark_dirty(id) + .draw_rect(shape.selrect, paint); } } pub fn draw_circle_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { - self.canvas(id).draw_oval(shape.selrect, paint); + self.canvas_and_mark_dirty(id) + .draw_oval(shape.selrect, paint); } pub fn draw_path_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { if let Some(path) = shape.get_skia_path() { - self.canvas(id).draw_path(&path, paint); + self.canvas_and_mark_dirty(id).draw_path(&path, paint); } } @@ -304,6 +368,9 @@ impl Surfaces { self.canvas(SurfaceId::UI) .clear(skia::Color::TRANSPARENT) .reset_matrix(); + + // Clear all dirty flags after reset + self.clear_all_dirty(); } pub fn cache_current_tile_texture( diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 58f10cbc6c..ba14112921 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -192,7 +192,7 @@ pub fn render( } } - let canvas = render_state.surfaces.canvas(target_surface); + let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur); return; } @@ -371,7 +371,7 @@ pub fn render_as_path( ) { let canvas = render_state .surfaces - .canvas(surface_id.unwrap_or(SurfaceId::Fills)); + .canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Fills)); for (path, paint) in paths { // Note: path can be empty @@ -397,7 +397,7 @@ pub fn render_position_data( let rect = Rect::from_xywh(pd.x, pd.y, pd.width, pd.height); render_state .surfaces - .canvas(surface_id) + .canvas_and_mark_dirty(surface_id) .draw_rect(rect, &paint); } }