From f7403935c817c60556ec51c39df3ca37dc64c5c2 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 3 Feb 2026 08:47:43 +0100 Subject: [PATCH 1/3] :tada: Improve shadows rendering performance --- render-wasm/src/render.rs | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 76eedf0288..9b783e76b8 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1512,6 +1512,16 @@ impl RenderState { Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur); let blur_filter = combined_blur .and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None)); + // Legacy path is only stable up to 1.0 zoom: the canvas is scaled and the shadow + // filter is evaluated in that scaled space, so for scale > 1 it over-inflates blur/spread. + // We also disable it when combined layer blur is present to avoid incorrect composition. + let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none(); + + if use_low_zoom_path { + // Match pre-commit behavior: scale blur/spread with zoom for low zoom levels. + transformed_shadow.to_mut().blur = shadow.blur * scale; + transformed_shadow.to_mut().spread = shadow.spread * scale; + } let mut transform_matrix = shape.transform; let center = shape.center(); @@ -1556,6 +1566,39 @@ impl RenderState { let mut bounds = drop_filter.compute_fast_bounds(shape_bounds); // Account for the shadow offset so the temporary surface fully contains the shifted blur. bounds.offset(world_offset); + // Early cull if the shadow bounds are outside the render area. + if !bounds.intersects(self.render_area) { + return; + } + + if use_low_zoom_path { + let mut shadow_paint = skia::Paint::default(); + shadow_paint.set_image_filter(drop_filter); + shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); + + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint); + let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); + drop_canvas.save_layer(&layer_rec); + drop_canvas.scale((scale, scale)); + drop_canvas.translate(translation); + + self.with_nested_blurs_suppressed(|state| { + state.render_shape( + &plain_shape, + clip_bounds, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + SurfaceId::DropShadows, + false, + Some(shadow.offset), + None, + ); + }); + + self.surfaces.canvas(SurfaceId::DropShadows).restore(); + return; + } let filter_result = filters::render_into_filter_surface(self, bounds, |state, temp_surface| { From 5be887f10bb755deb2e8b5605bd18227fb838560 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 3 Feb 2026 11:21:01 +0100 Subject: [PATCH 2/3] :tada: Improve plain shape calculation --- render-wasm/src/render.rs | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9b783e76b8..b1968e7e99 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -22,7 +22,7 @@ pub use surfaces::{SurfaceId, Surfaces}; use crate::performance; use crate::shapes::{ - all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, Type, + all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Type, }; use crate::state::{ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; @@ -1536,28 +1536,20 @@ impl RenderState { let world_offset = (mapped.x, mapped.y); // The opacity of fills and strokes shouldn't affect the shadow, - // so we paint everything black with the same opacity - plain_shape.to_mut().clear_fills(); + // so we paint everything black with the same opacity. + let plain_shape_mut = plain_shape.to_mut(); + plain_shape_mut.clear_fills(); if shape.has_fills() { - plain_shape - .to_mut() - .add_fill(Fill::Solid(SolidColor(skia::Color::BLACK))); + plain_shape_mut.add_fill(Fill::Solid(SolidColor(skia::Color::BLACK))); } - plain_shape.to_mut().clear_strokes(); - for stroke in shape.strokes.iter() { - plain_shape.to_mut().add_stroke(Stroke { - fill: Fill::Solid(SolidColor(skia::Color::BLACK)), - width: stroke.width, - style: stroke.style, - cap_end: stroke.cap_end, - cap_start: stroke.cap_start, - kind: stroke.kind, - }); + // Reuse existing strokes and only override their fill color. + for stroke in plain_shape_mut.strokes.iter_mut() { + stroke.fill = Fill::Solid(SolidColor(skia::Color::BLACK)); } - plain_shape.to_mut().clear_shadows(); - plain_shape.to_mut().blur = None; + plain_shape_mut.clear_shadows(); + plain_shape_mut.blur = None; let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else { return; From 25aff100cfc96fcbd28ef69a044b7841f4119706 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Tue, 3 Feb 2026 12:42:24 +0100 Subject: [PATCH 3/3] :tada: Add shadows playground for render wasm --- .../resources/wasm-playground/shadows.html | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 frontend/resources/wasm-playground/shadows.html diff --git a/frontend/resources/wasm-playground/shadows.html b/frontend/resources/wasm-playground/shadows.html new file mode 100644 index 0000000000..ea6b206633 --- /dev/null +++ b/frontend/resources/wasm-playground/shadows.html @@ -0,0 +1,90 @@ + + + + + WASM + WebGL2 Canvas + + + + + + +