mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 10:56:20 +00:00
🔧 Improve performance on shapes with blur
This commit is contained in:
parent
7ef16a2b69
commit
337cfc2d3e
@ -224,8 +224,9 @@
|
|||||||
show-gradient-handlers? (= (count selected) 1)
|
show-gradient-handlers? (= (count selected) 1)
|
||||||
show-grids? (contains? layout :display-guides)
|
show-grids? (contains? layout :display-guides)
|
||||||
|
|
||||||
show-frame-outline? (= transform :move)
|
show-frame-outline? (and (= transform :move) (not panning))
|
||||||
show-outlines? (and (nil? transform)
|
show-outlines? (and (nil? transform)
|
||||||
|
(not panning)
|
||||||
(not edition)
|
(not edition)
|
||||||
(not drawing-obj)
|
(not drawing-obj)
|
||||||
(not (#{:comments :path :curve} drawing-tool)))
|
(not (#{:comments :path :curve} drawing-tool)))
|
||||||
@ -561,7 +562,7 @@
|
|||||||
:shift? @shift?}])
|
:shift? @shift?}])
|
||||||
|
|
||||||
[:> widgets/frame-titles*
|
[:> widgets/frame-titles*
|
||||||
{:objects (with-meta objects-modified nil)
|
{:objects objects-modified
|
||||||
:selected selected
|
:selected selected
|
||||||
:zoom zoom
|
:zoom zoom
|
||||||
:is-show-artboard-names show-artboard-names?
|
:is-show-artboard-names show-artboard-names?
|
||||||
|
|||||||
@ -39,6 +39,7 @@ pub use images::*;
|
|||||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||||
|
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||||
|
|
||||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||||
|
|
||||||
@ -787,6 +788,25 @@ impl RenderState {
|
|||||||
shape.to_mut().set_blur(None);
|
shape.to_mut().set_blur(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For non-text, non-SVG shapes in the normal rendering path, apply blur
|
||||||
|
// via a single save_layer on each render surface
|
||||||
|
// Clip correctness is preserved
|
||||||
|
let blur_sigma_for_layers: Option<f32> = if !fast_mode
|
||||||
|
&& apply_to_current_surface
|
||||||
|
&& fills_surface_id == SurfaceId::Fills
|
||||||
|
&& !matches!(shape.shape_type, Type::Text(_))
|
||||||
|
&& !matches!(shape.shape_type, Type::SVGRaw(_))
|
||||||
|
{
|
||||||
|
if let Some(blur) = shape.blur.filter(|b| !b.hidden) {
|
||||||
|
shape.to_mut().set_blur(None);
|
||||||
|
Some(blur.value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let center = shape.center();
|
let center = shape.center();
|
||||||
let mut matrix = shape.transform;
|
let mut matrix = shape.transform;
|
||||||
matrix.post_translate(center);
|
matrix.post_translate(center);
|
||||||
@ -1001,6 +1021,24 @@ impl RenderState {
|
|||||||
s.canvas().concat(&matrix);
|
s.canvas().concat(&matrix);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Wrap ALL fill/stroke/shadow rendering so a single GPU blur pass calls
|
||||||
|
let blur_filter_for_layers: Option<skia::ImageFilter> = blur_sigma_for_layers
|
||||||
|
.and_then(|sigma| skia::image_filters::blur((sigma, sigma), None, None, None));
|
||||||
|
if let Some(ref filter) = blur_filter_for_layers {
|
||||||
|
let mut layer_paint = skia::Paint::default();
|
||||||
|
layer_paint.set_image_filter(filter.clone());
|
||||||
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&layer_paint);
|
||||||
|
self.surfaces
|
||||||
|
.canvas(fills_surface_id)
|
||||||
|
.save_layer(&layer_rec);
|
||||||
|
self.surfaces
|
||||||
|
.canvas(strokes_surface_id)
|
||||||
|
.save_layer(&layer_rec);
|
||||||
|
self.surfaces
|
||||||
|
.canvas(innershadows_surface_id)
|
||||||
|
.save_layer(&layer_rec);
|
||||||
|
}
|
||||||
|
|
||||||
let shape = &shape;
|
let shape = &shape;
|
||||||
|
|
||||||
if shape.fills.is_empty()
|
if shape.fills.is_empty()
|
||||||
@ -1053,7 +1091,12 @@ impl RenderState {
|
|||||||
innershadows_surface_id,
|
innershadows_surface_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// bools::debug_render_bool_paths(self, shape, shapes, modifiers, structure);
|
|
||||||
|
if blur_filter_for_layers.is_some() {
|
||||||
|
self.surfaces.canvas(innershadows_surface_id).restore();
|
||||||
|
self.surfaces.canvas(strokes_surface_id).restore();
|
||||||
|
self.surfaces.canvas(fills_surface_id).restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1145,6 +1188,11 @@ impl RenderState {
|
|||||||
let _start = performance::begin_timed_log!("render_preview");
|
let _start = performance::begin_timed_log!("render_preview");
|
||||||
performance::begin_measure!("render_preview");
|
performance::begin_measure!("render_preview");
|
||||||
|
|
||||||
|
// Enable fast_mode during preview to skip expensive effects (blur, shadows).
|
||||||
|
// Restore the previous state afterward so the final render is full quality.
|
||||||
|
let current_fast_mode = self.options.is_fast_mode();
|
||||||
|
self.options.set_fast_mode(true);
|
||||||
|
|
||||||
// Skip tile rebuilding during preview - we'll do it at the end
|
// Skip tile rebuilding during preview - we'll do it at the end
|
||||||
// Just rebuild tiles for touched shapes and render synchronously
|
// Just rebuild tiles for touched shapes and render synchronously
|
||||||
self.rebuild_touched_tiles(tree);
|
self.rebuild_touched_tiles(tree);
|
||||||
@ -1152,6 +1200,8 @@ impl RenderState {
|
|||||||
// Use the sync render path
|
// Use the sync render path
|
||||||
self.start_render_loop(None, tree, timestamp, true)?;
|
self.start_render_loop(None, tree, timestamp, true)?;
|
||||||
|
|
||||||
|
self.options.set_fast_mode(current_fast_mode);
|
||||||
|
|
||||||
performance::end_measure!("render_preview");
|
performance::end_measure!("render_preview");
|
||||||
performance::end_timed_log!("render_preview", _start);
|
performance::end_timed_log!("render_preview", _start);
|
||||||
|
|
||||||
@ -1322,11 +1372,16 @@ impl RenderState {
|
|||||||
paint.set_blend_mode(element.blend_mode().into());
|
paint.set_blend_mode(element.blend_mode().into());
|
||||||
paint.set_alpha_f(element.opacity());
|
paint.set_alpha_f(element.opacity());
|
||||||
|
|
||||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
// Skip frame-level blur in fast mode (pan/zoom)
|
||||||
let scale = self.get_scale();
|
if !self.options.is_fast_mode() {
|
||||||
let sigma = frame_blur.value * scale;
|
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||||
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
let scale = self.get_scale();
|
||||||
paint.set_image_filter(filter);
|
let sigma = frame_blur.value * scale;
|
||||||
|
if let Some(filter) =
|
||||||
|
skia::image_filters::blur((sigma, sigma), None, None, None)
|
||||||
|
{
|
||||||
|
paint.set_image_filter(filter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1513,9 +1568,7 @@ impl RenderState {
|
|||||||
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
|
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
|
||||||
let blur_filter = combined_blur
|
let blur_filter = combined_blur
|
||||||
.and_then(|blur| skia::image_filters::blur((blur.value, blur.value), None, None, None));
|
.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();
|
let use_low_zoom_path = scale <= 1.0 && combined_blur.is_none();
|
||||||
|
|
||||||
if use_low_zoom_path {
|
if use_low_zoom_path {
|
||||||
@ -1598,13 +1651,27 @@ impl RenderState {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let filter_result =
|
// Adaptive downscale for large blur values (lossless GPU optimization).
|
||||||
filters::render_into_filter_surface(self, bounds, |state, temp_surface| {
|
// Bounds above were computed from the original sigma so filter surface coverage is correct.
|
||||||
|
// Maximum downscale is 1/BLUR_DOWNSCALE_THRESHOLD (i.e. 8×): beyond that the
|
||||||
|
// filter surface becomes too small and quality degrades noticeably.
|
||||||
|
const MIN_BLUR_DOWNSCALE: f32 = 1.0 / BLUR_DOWNSCALE_THRESHOLD;
|
||||||
|
let blur_downscale = if shadow.blur > BLUR_DOWNSCALE_THRESHOLD {
|
||||||
|
(BLUR_DOWNSCALE_THRESHOLD / shadow.blur).max(MIN_BLUR_DOWNSCALE)
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let filter_result = filters::render_into_filter_surface(
|
||||||
|
self,
|
||||||
|
bounds,
|
||||||
|
blur_downscale,
|
||||||
|
|state, temp_surface| {
|
||||||
{
|
{
|
||||||
let canvas = state.surfaces.canvas(temp_surface);
|
let canvas = state.surfaces.canvas(temp_surface);
|
||||||
|
|
||||||
let mut shadow_paint = skia::Paint::default();
|
let mut shadow_paint = skia::Paint::default();
|
||||||
shadow_paint.set_image_filter(drop_filter.clone());
|
shadow_paint.set_image_filter(drop_filter);
|
||||||
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
|
||||||
|
|
||||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
|
||||||
@ -1629,7 +1696,8 @@ impl RenderState {
|
|||||||
let canvas = state.surfaces.canvas(temp_surface);
|
let canvas = state.surfaces.canvas(temp_surface);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if let Some((mut surface, filter_scale)) = filter_result {
|
if let Some((mut surface, filter_scale)) = filter_result {
|
||||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
@ -1716,6 +1784,7 @@ impl RenderState {
|
|||||||
if shadow_shape.hidden {
|
if shadow_shape.hidden {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nested_clip_bounds =
|
let nested_clip_bounds =
|
||||||
node_render_state.get_nested_shadow_clip_bounds(element, shadow);
|
node_render_state.get_nested_shadow_clip_bounds(element, shadow);
|
||||||
|
|
||||||
@ -1776,7 +1845,6 @@ impl RenderState {
|
|||||||
self.surfaces
|
self.surfaces
|
||||||
.canvas(SurfaceId::DropShadows)
|
.canvas(SurfaceId::DropShadows)
|
||||||
.draw_paint(&paint);
|
.draw_paint(&paint);
|
||||||
|
|
||||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,9 @@ pub fn render_with_filter_surface<F>(
|
|||||||
where
|
where
|
||||||
F: FnOnce(&mut RenderState, SurfaceId),
|
F: FnOnce(&mut RenderState, SurfaceId),
|
||||||
{
|
{
|
||||||
if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
if let Some((mut surface, scale)) =
|
||||||
|
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)
|
||||||
|
{
|
||||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(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 we scaled down, we need to scale the source rect and adjust the destination
|
||||||
@ -69,9 +71,15 @@ where
|
|||||||
/// down so that everything fits; the returned `scale` tells the caller how much the
|
/// down so that everything fits; the returned `scale` tells the caller how much the
|
||||||
/// content was reduced so it can be re-scaled on compositing. The `draw_fn` should
|
/// content was reduced so it can be re-scaled on compositing. The `draw_fn` should
|
||||||
/// render the untransformed shape (i.e. in document coordinates) onto `SurfaceId::Filter`.
|
/// render the untransformed shape (i.e. in document coordinates) onto `SurfaceId::Filter`.
|
||||||
|
///
|
||||||
|
/// `extra_downscale` is an additional scale factor applied on top of the overflow-fit scale.
|
||||||
|
/// Use values < 1.0 to pre-downscale before applying Gaussian blur filters, which dramatically
|
||||||
|
/// reduces GPU kernel work for large blur sigmas (Gaussian blur is scale-equivariant, so the
|
||||||
|
/// caller must also reduce the sigma proportionally). Pass 1.0 for no extra downscale.
|
||||||
pub fn render_into_filter_surface<F>(
|
pub fn render_into_filter_surface<F>(
|
||||||
render_state: &mut RenderState,
|
render_state: &mut RenderState,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
|
extra_downscale: f32,
|
||||||
draw_fn: F,
|
draw_fn: F,
|
||||||
) -> Option<(skia::Surface, f32)>
|
) -> Option<(skia::Surface, f32)>
|
||||||
where
|
where
|
||||||
@ -86,16 +94,28 @@ where
|
|||||||
let bounds_width = bounds.width().ceil().max(1.0) as i32;
|
let bounds_width = bounds.width().ceil().max(1.0) as i32;
|
||||||
let bounds_height = bounds.height().ceil().max(1.0) as i32;
|
let bounds_height = bounds.height().ceil().max(1.0) as i32;
|
||||||
|
|
||||||
|
// Minimum scale floor for fit_scale alone; prevents extreme downscaling when
|
||||||
|
// the shape is much larger than the filter surface.
|
||||||
|
const MIN_FIT_SCALE: f32 = 0.1;
|
||||||
|
// Absolute minimum for the combined scale (fit × extra_downscale). Below this
|
||||||
|
// the offscreen surface would have sub-pixel dimensions and produce artifacts or
|
||||||
|
// crashes. At 0.03 a shape must be at least ~34 px wide to render as a single
|
||||||
|
// pixel, which is a safe lower bound in practice.
|
||||||
|
const MIN_COMBINED_SCALE: f32 = 0.03;
|
||||||
|
|
||||||
// Calculate scale factor if bounds exceed filter surface size
|
// Calculate scale factor if bounds exceed filter surface size
|
||||||
let scale = if bounds_width > filter_width || bounds_height > filter_height {
|
let fit_scale = if bounds_width > filter_width || bounds_height > filter_height {
|
||||||
let scale_x = filter_width as f32 / bounds_width as f32;
|
let scale_x = filter_width as f32 / bounds_width as f32;
|
||||||
let scale_y = filter_height as f32 / bounds_height as f32;
|
let scale_y = filter_height as f32 / bounds_height as f32;
|
||||||
// Use the smaller scale to ensure everything fits
|
// Use the smaller scale to ensure everything fits
|
||||||
scale_x.min(scale_y).max(0.1) // Clamp to minimum 0.1 to avoid extreme scaling
|
scale_x.min(scale_y).max(MIN_FIT_SCALE)
|
||||||
} else {
|
} else {
|
||||||
1.0
|
1.0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Combine overflow-fit scale with caller-requested extra downscale
|
||||||
|
let scale = (fit_scale * extra_downscale).max(MIN_COMBINED_SCALE);
|
||||||
|
|
||||||
{
|
{
|
||||||
let canvas = render_state.surfaces.canvas(filter_id);
|
let canvas = render_state.surfaces.canvas(filter_id);
|
||||||
canvas.clear(skia::Color::TRANSPARENT);
|
canvas.clear(skia::Color::TRANSPARENT);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user