diff --git a/.serena/memories/render-wasm/ffi-rendering-subtleties.md b/.serena/memories/render-wasm/ffi-rendering-subtleties.md index 9f91d27311..64e1b43f83 100644 --- a/.serena/memories/render-wasm/ffi-rendering-subtleties.md +++ b/.serena/memories/render-wasm/ffi-rendering-subtleties.md @@ -23,6 +23,8 @@ - Surfaces allocate with `effective_paint_tile_size` = `min(paint, atlas_slot max)` so DPR 2 on a 4096² atlas paints 512 (not 1024→downscale-to-512). Overpainting the atlas was pure GPU waste on zoom settle. +- 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). - Scales: `get_paint_scale()` matches tile CTM; `get_view_scale()` is `zoom×dpr` for viewport/backbuffer mapping. `get_scale()` is an alias of paint scale (legacy name). - Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index d7010cc2e5..3bcbea93cd 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -56,6 +56,19 @@ 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 +98,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(); }