From f0880cf3f96b8e0418cb1ecc0dc3f8a4c443f3cc Mon Sep 17 00:00:00 2001 From: Miguel de Benito Delgado Date: Tue, 22 Sep 2026 11:09:45 +0200 Subject: [PATCH] :bug: Crop cached drop shadows to their filter bounds (#11801) Snapshot only the scaled shadow bounds instead of the full reusable filter surface. This avoids retaining viewport-sized textures for each small shadow while keeping cached pixels valid when the surface is reused. Use the bounded snapshot for both shape and frame shadow caches. Add regression tests for rounding, scaling, surface limits, and snapshot reuse. AI-assisted-by: gpt-6 --- render-wasm/src/render.rs | 6 +-- render-wasm/src/render/shadows.rs | 61 ++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 8d41e4fb62..4743bd5124 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -3656,11 +3656,7 @@ impl RenderState { )?; if let Some((mut surface, filter_scale)) = filter_result { - let cached = shadows::CachedDropShadowFilter::new( - bounds, - filter_scale, - surface.image_snapshot(), - ); + let cached = shadows::CachedDropShadowFilter::new(bounds, filter_scale, &mut surface); shadows::blit_cached_drop_shadow_filter( &mut self.surfaces, &cached, diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 66400f5f44..6391ba2848 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -43,7 +43,19 @@ pub(crate) struct CachedDropShadowFilter { } impl CachedDropShadowFilter { - pub(crate) fn new(bounds: Rect, filter_scale: f32, image: skia::Image) -> Self { + pub(crate) fn new(bounds: Rect, filter_scale: f32, surface: &mut skia::Surface) -> Self { + // The reusable filter surface can be viewport-sized. Retaining all of it + // for every shadow keeps large, mostly transparent textures alive. + // Filter drawing translates bounds to the origin before applying scale. + let width = (bounds.width() * filter_scale) + .ceil() + .clamp(1.0, surface.width() as f32) as i32; + let height = (bounds.height() * filter_scale) + .ceil() + .clamp(1.0, surface.height() as f32) as i32; + let image = surface + .image_snapshot_with_bounds(skia::IRect::from_wh(width, height)) + .expect("shadow filter bounds must intersect its surface"); Self { bounds, filter_scale, @@ -340,11 +352,7 @@ fn render_cached_filter_frame_shadow( )?; if let Some((mut surface, filter_scale)) = filter_result { - let cached = CachedDropShadowFilter { - bounds, - filter_scale, - image: surface.image_snapshot(), - }; + let cached = CachedDropShadowFilter::new(bounds, filter_scale, &mut surface); blit_cached_drop_shadow_filter(&mut state.surfaces, &cached, None); state.drop_shadow_filter_cache.store(key, cached); } @@ -570,3 +578,44 @@ pub fn render_text_shadows( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn cached_shadow_retains_only_its_scaled_bounds() { + for (bounds, scale, expected) in [ + (Rect::from_xywh(100.0, 200.0, 50.5, 40.5), 1.0, (51, 41)), + (Rect::from_xywh(100.0, 200.0, 50.5, 40.5), 0.5, (26, 21)), + (Rect::from_xywh(0.0, 0.0, 2000.0, 1500.0), 1.0, (1024, 768)), + ] { + let mut surface = skia::surfaces::raster_n32_premul((1024, 768)).unwrap(); + let cached = CachedDropShadowFilter::new(bounds, scale, &mut surface); + + assert_eq!(cached.image.dimensions(), skia::ISize::from(expected)); + } + } + + #[test] + fn cached_shadow_survives_filter_surface_reuse() { + let mut surface = skia::surfaces::raster_n32_premul((1024, 768)).unwrap(); + surface.canvas().clear(skia::Color::TRANSPARENT); + let mut paint = Paint::default(); + paint.set_color(skia::Color::RED); + surface + .canvas() + .draw_rect(Rect::from_xywh(4.0, 5.0, 10.0, 10.0), &paint); + let cached = CachedDropShadowFilter::new( + Rect::from_xywh(100.0, 200.0, 50.5, 40.5), + 1.0, + &mut surface, + ); + + surface.canvas().clear(skia::Color::BLUE); + + let pixels = cached.image.peek_pixels().unwrap(); + assert_eq!(pixels.get_color((5, 6)), skia::Color::RED); + assert_eq!(pixels.get_color((0, 0)), skia::Color::TRANSPARENT); + } +}