diff --git a/.serena/memories/render-wasm/ffi-rendering-subtleties.md b/.serena/memories/render-wasm/ffi-rendering-subtleties.md index f57856a17d..d739cd54e7 100644 --- a/.serena/memories/render-wasm/ffi-rendering-subtleties.md +++ b/.serena/memories/render-wasm/ffi-rendering-subtleties.md @@ -17,6 +17,8 @@ ## Tile/render behavior +- Raster `Fill::Image`: skip `save_layer` unless the shape has an image filter; plain + Rect/Frame (no corners) also skip the container clip (`draw_image_fill` in fills.rs). - Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame. - During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately. - `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render. diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index d7010cc2e5..bebd20f7c6 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -56,6 +56,15 @@ fn clip_to_shape( } } +/// Axis-aligned rect/frame with no corner radii: `dest` fills `selrect`, so a +/// clip to the container is a no-op before `draw_image_rect`. +fn is_axis_aligned_image_rect(shape: &Shape) -> bool { + matches!( + &shape.shape_type, + Type::Rect(Rect { corners: None }) | Type::Frame(Frame { corners: None, .. }) + ) +} + fn draw_image_fill( render_state: &mut RenderState, shape: &Shape, @@ -85,29 +94,53 @@ fn draw_image_fill( let src_rect = get_source_rect(size, container, image_fill); let dest_rect = container; + let sampling = get_resources().sampling_options; - let mut image_paint = skia::Paint::default(); - image_paint.set_anti_alias(antialias); + // `save_layer` is only required when a shape-level image filter (blur) must + // run over the clipped image. Otherwise a plain save/clip (or no clip for + // axis-aligned rects) avoids an offscreen buffer per fill — the hot path + // for photo-heavy boards during tile walks. if let Some(filter) = shape.image_filter(1.) { - image_paint.set_image_filter(filter.clone()); + let mut layer_paint = skia::Paint::default(); + layer_paint.set_anti_alias(antialias); + layer_paint.set_image_filter(filter); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&layer_paint); + canvas.save_layer(&layer_rec); + clip_to_shape(canvas, shape, container, antialias); + canvas.draw_image_rect_with_sampling_options( + image, + Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), + dest_rect, + sampling, + paint, + ); + canvas.restore(); + return; } - let layer_rec = skia::canvas::SaveLayerRec::default().paint(&image_paint); - // Save the current canvas state - canvas.save_layer(&layer_rec); + let mut draw_paint = paint.clone(); + draw_paint.set_anti_alias(antialias); + if is_axis_aligned_image_rect(shape) { + canvas.draw_image_rect_with_sampling_options( + image, + Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), + dest_rect, + sampling, + &draw_paint, + ); + return; + } + + canvas.save(); clip_to_shape(canvas, shape, container, antialias); - - // Draw the image with the calculated destination rectangle canvas.draw_image_rect_with_sampling_options( image, Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), dest_rect, - get_resources().sampling_options, - paint, + sampling, + &draw_paint, ); - - // Restore the canvas to remove the clipping canvas.restore(); }