mirror of
https://github.com/penpot/penpot.git
synced 2026-08-17 10:18:59 +00:00
⚡ Expand direct shape painting and skip empty drop-shadow blits (#11100)
* ♻️ Extract apply_clip_stack_to_surfaces helper Share the layered-path clip loop so the Current-surface direct path can reuse the same hard-clip stack without duplication. * ⚡ Expand direct shape painting onto Current Allow clip stacks, frames, non-identity transforms, and SrcOver opacity on the Current-surface fast path; skip empty non-masked groups. Avoids Fills/Strokes blits for common shapes. * ⚡ Skip empty drop-shadow blits; warm DropShadows once Early-out drop-shadow composite when a shape has no visible shadows, and touch DropShadows→Current once per tile instead of per shape to keep flush_and_submit cheap.
This commit is contained in:
parent
10a2c19f92
commit
11fc090bc4
@ -413,6 +413,10 @@ pub(crate) struct RenderState {
|
|||||||
/// a tile before its text glyph uploads complete (blank first/center tile).
|
/// a tile before its text glyph uploads complete (blank first/center tile).
|
||||||
/// One explicit flush warms the submit path for the rest of the pass.
|
/// One explicit flush warms the submit path for the rest of the pass.
|
||||||
pub tile_atlas_flushed: bool,
|
pub tile_atlas_flushed: bool,
|
||||||
|
/// DropShadows→Current touch once per tile when no shape composites a real
|
||||||
|
/// shadow. A full skip made flush_and_submit very slow (Skia ops-task
|
||||||
|
/// ordering); doing it per shape was wasted GPU work.
|
||||||
|
pub drop_shadows_ops_warmed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InteractiveDragCrop {
|
pub struct InteractiveDragCrop {
|
||||||
@ -596,6 +600,7 @@ impl RenderState {
|
|||||||
preserve_target_during_render: false,
|
preserve_target_during_render: false,
|
||||||
backbuffer_crop_cache: HashMap::default(),
|
backbuffer_crop_cache: HashMap::default(),
|
||||||
tile_atlas_flushed: false,
|
tile_atlas_flushed: false,
|
||||||
|
drop_shadows_ops_warmed: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1248,6 +1253,66 @@ impl RenderState {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply frame clip stack in document space on the given surface bitmask.
|
||||||
|
/// Caller must already have those surfaces in doc transform (Fills-style
|
||||||
|
/// scale + tile translation, or Current after the same). Hard (non-AA)
|
||||||
|
/// clips avoid alpha seams on semi-transparent overflow.
|
||||||
|
fn apply_clip_stack_to_surfaces(
|
||||||
|
&mut self,
|
||||||
|
clips: &ClipStack,
|
||||||
|
surface_ids: u32,
|
||||||
|
scale: f32,
|
||||||
|
debug_fill_surface: Option<SurfaceId>,
|
||||||
|
) {
|
||||||
|
for (mut bounds, corners, transform) in clips.iter() {
|
||||||
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
|
s.canvas().concat(transform);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Outset clip by ~0.5 to include edge pixels that
|
||||||
|
// aliased clip misclassifies as outside (causing artifacts).
|
||||||
|
let outset = 0.5 / scale;
|
||||||
|
bounds.outset((outset, outset));
|
||||||
|
|
||||||
|
// Hard clip edge (antialias = false) to avoid alpha seam when clipping
|
||||||
|
// semi-transparent content larger than the frame.
|
||||||
|
if let Some(corners) = corners {
|
||||||
|
let rrect = RRect::new_rect_radii(bounds, corners);
|
||||||
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
|
s.canvas().clip_rrect(rrect, skia::ClipOp::Intersect, false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
|
s.canvas().clip_rect(bounds, skia::ClipOp::Intersect, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This renders a red line around clipped
|
||||||
|
// shapes (frames).
|
||||||
|
if self.options.is_debug_visible() {
|
||||||
|
if let Some(fills_surface_id) = debug_fill_surface {
|
||||||
|
let mut paint = skia::Paint::default();
|
||||||
|
paint.set_style(skia::PaintStyle::Stroke);
|
||||||
|
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
|
||||||
|
paint.set_stroke_width(4.);
|
||||||
|
self.surfaces
|
||||||
|
.canvas(fills_surface_id)
|
||||||
|
.draw_rect(bounds, &paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment to debug the render_position_data
|
||||||
|
// if let Type::Text(text_content) = &shape.shape_type {
|
||||||
|
// text::render_position_data(self, fills_surface_id, &shape, text_content);
|
||||||
|
// }
|
||||||
|
|
||||||
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
|
s.canvas()
|
||||||
|
.concat(&transform.invert().unwrap_or(Matrix::default()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn render_shape(
|
pub fn render_shape(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -1271,16 +1336,6 @@ impl RenderState {
|
|||||||
| innershadows_surface_id as u32
|
| innershadows_surface_id as u32
|
||||||
| text_drop_shadows_surface_id as u32;
|
| text_drop_shadows_surface_id as u32;
|
||||||
|
|
||||||
// Only save canvas state if we have clipping or transforms
|
|
||||||
// For simple shapes without clipping, skip expensive save/restore
|
|
||||||
let needs_save =
|
|
||||||
clip_bounds.is_some() || offset.is_some() || !shape.transform.is_identity();
|
|
||||||
|
|
||||||
if needs_save {
|
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
|
||||||
s.canvas().save();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let fast_mode = self.options.is_fast_mode();
|
let fast_mode = self.options.is_fast_mode();
|
||||||
// Skip anti-aliasing entirely during fast_mode (interactive
|
// Skip anti-aliasing entirely during fast_mode (interactive
|
||||||
// gestures + pan/zoom). AA edge sampling is per-pixel and adds
|
// gestures + pan/zoom). AA edge sampling is per-pixel and adds
|
||||||
@ -1297,19 +1352,39 @@ impl RenderState {
|
|||||||
&& self.nested_blurs.iter().flatten().any(|blur| {
|
&& self.nested_blurs.iter().flatten().any(|blur| {
|
||||||
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
|
!blur.hidden && blur.blur_type == BlurType::LayerBlur && blur.value > 0.0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Empty non-masked groups paint nothing here (children are separate walker
|
||||||
|
// nodes). Skip the layered Fills/Strokes path entirely.
|
||||||
|
if matches!(shape.shape_type, Type::Group(g) if !g.masked)
|
||||||
|
&& shape.fills.is_empty()
|
||||||
|
&& !shape.has_visible_strokes()
|
||||||
|
&& shape.shadows.is_empty()
|
||||||
|
&& shape.blur.is_none()
|
||||||
|
&& shape.background_blur.is_none()
|
||||||
|
&& !has_inherited_blur
|
||||||
|
&& parent_shadows.is_none()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip is allowed: we apply the same stack on Current after scale+translate.
|
||||||
|
// Opacity < 1 with SrcOver is OK: render_shape_enter already opened a
|
||||||
|
// save_layer on Current; painting fills/strokes into that layer matches
|
||||||
|
// the layered path without Fills/Strokes blits.
|
||||||
|
// Non-SrcOver blend, frame clip blur, and masked groups stay layered.
|
||||||
let can_render_directly = apply_to_current_surface
|
let can_render_directly = apply_to_current_surface
|
||||||
&& clip_bounds.is_none()
|
|
||||||
&& offset.is_none()
|
&& offset.is_none()
|
||||||
&& parent_shadows.is_none()
|
&& parent_shadows.is_none()
|
||||||
&& !shape.needs_layer()
|
&& shape.blend_mode().0 == skia::BlendMode::SrcOver
|
||||||
|
&& !shape.has_frame_clip_layer_blur()
|
||||||
|
&& !matches!(shape.shape_type, Type::Group(g) if g.masked)
|
||||||
&& shape.blur.is_none()
|
&& shape.blur.is_none()
|
||||||
&& shape.background_blur.is_none()
|
&& shape.background_blur.is_none()
|
||||||
&& !has_inherited_blur
|
&& !has_inherited_blur
|
||||||
&& shape.shadows.is_empty()
|
&& shape.shadows.is_empty()
|
||||||
&& shape.transform.is_identity()
|
|
||||||
&& matches!(
|
&& matches!(
|
||||||
shape.shape_type,
|
shape.shape_type,
|
||||||
Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_)
|
Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_) | Type::Frame(_)
|
||||||
)
|
)
|
||||||
&& !(shape.fills.is_empty() && has_nested_fills)
|
&& !(shape.fills.is_empty() && has_nested_fills)
|
||||||
&& !shape
|
&& !shape
|
||||||
@ -1331,17 +1406,36 @@ impl RenderState {
|
|||||||
canvas.translate(translation);
|
canvas.translate(translation);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(clips) = clip_bounds.as_ref() {
|
||||||
|
self.apply_clip_stack_to_surfaces(clips, target_surface as u32, scale, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shape.transform.is_identity() {
|
||||||
|
let center = shape.center();
|
||||||
|
let mut matrix = shape.transform;
|
||||||
|
matrix.post_translate(center);
|
||||||
|
matrix.pre_translate(-center);
|
||||||
|
self.surfaces.apply_mut(target_surface as u32, |s| {
|
||||||
|
s.canvas().concat(&matrix);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fills::render(self, shape, &shape.fills, antialias, target_surface, None)?;
|
fills::render(self, shape, &shape.fills, antialias, target_surface, None)?;
|
||||||
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
|
||||||
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
// Clipped frames draw strokes in render_shape_exit over children.
|
||||||
strokes::render(
|
let skip_strokes = matches!(shape.shape_type, Type::Frame(_)) && shape.clip_content;
|
||||||
self,
|
if !skip_strokes {
|
||||||
shape,
|
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
||||||
&visible_strokes,
|
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
||||||
Some(target_surface),
|
strokes::render(
|
||||||
antialias,
|
self,
|
||||||
outset,
|
shape,
|
||||||
)?;
|
&visible_strokes,
|
||||||
|
Some(target_surface),
|
||||||
|
antialias,
|
||||||
|
outset,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
self.surfaces.apply_mut(target_surface as u32, |s| {
|
self.surfaces.apply_mut(target_surface as u32, |s| {
|
||||||
s.canvas().restore();
|
s.canvas().restore();
|
||||||
@ -1352,62 +1446,24 @@ impl RenderState {
|
|||||||
debug::render_debug_shape(self, Some(shape_selrect_bounds), None);
|
debug::render_debug_shape(self, Some(shape_selrect_bounds), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
if needs_save {
|
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
|
||||||
s.canvas().restore();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only save canvas state if we have clipping or transforms
|
||||||
|
// For simple shapes without clipping, skip expensive save/restore
|
||||||
|
let needs_save =
|
||||||
|
clip_bounds.is_some() || offset.is_some() || !shape.transform.is_identity();
|
||||||
|
|
||||||
|
if needs_save {
|
||||||
|
self.surfaces.apply_mut(surface_ids, |s| {
|
||||||
|
s.canvas().save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// set clipping
|
// set clipping
|
||||||
if let Some(clips) = clip_bounds.as_ref() {
|
if let Some(clips) = clip_bounds.as_ref() {
|
||||||
let scale = self.get_scale();
|
let scale = self.get_scale();
|
||||||
for (mut bounds, corners, transform) in clips.iter() {
|
self.apply_clip_stack_to_surfaces(clips, surface_ids, scale, Some(fills_surface_id));
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
|
||||||
s.canvas().concat(transform);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Outset clip by ~0.5 to include edge pixels that
|
|
||||||
// aliased clip misclassifies as outside (causing artifacts).
|
|
||||||
let outset = 0.5 / scale;
|
|
||||||
bounds.outset((outset, outset));
|
|
||||||
|
|
||||||
// Hard clip edge (antialias = false) to avoid alpha seam when clipping
|
|
||||||
// semi-transparent content larger than the frame.
|
|
||||||
if let Some(corners) = corners {
|
|
||||||
let rrect = RRect::new_rect_radii(bounds, corners);
|
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
|
||||||
s.canvas().clip_rrect(rrect, skia::ClipOp::Intersect, false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
|
||||||
s.canvas().clip_rect(bounds, skia::ClipOp::Intersect, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This renders a red line around clipped
|
|
||||||
// shapes (frames).
|
|
||||||
if self.options.is_debug_visible() {
|
|
||||||
let mut paint = skia::Paint::default();
|
|
||||||
paint.set_style(skia::PaintStyle::Stroke);
|
|
||||||
paint.set_color(skia::Color::from_argb(255, 255, 0, 0));
|
|
||||||
paint.set_stroke_width(4.);
|
|
||||||
self.surfaces
|
|
||||||
.canvas(fills_surface_id)
|
|
||||||
.draw_rect(bounds, &paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncomment to debug the render_position_data
|
|
||||||
// if let Type::Text(text_content) = &shape.shape_type {
|
|
||||||
// text::render_position_data(self, fills_surface_id, &shape, text_content);
|
|
||||||
// }
|
|
||||||
|
|
||||||
self.surfaces.apply_mut(surface_ids, |s| {
|
|
||||||
s.canvas()
|
|
||||||
.concat(&transform.invert().unwrap_or(Matrix::default()));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't want to change the value in the global state
|
// We don't want to change the value in the global state
|
||||||
@ -3117,6 +3173,7 @@ impl RenderState {
|
|||||||
|
|
||||||
/// Renders element drop shadows to DropShadows surface and composites to Current.
|
/// Renders element drop shadows to DropShadows surface and composites to Current.
|
||||||
/// Used for both normal shadow rendering and pre-layer rendering (frame_clip_layer_blur).
|
/// Used for both normal shadow rendering and pre-layer rendering (frame_clip_layer_blur).
|
||||||
|
/// Returns `true` when at least one visible drop shadow was composited.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn render_element_drop_shadows_and_composite(
|
fn render_element_drop_shadows_and_composite(
|
||||||
&mut self,
|
&mut self,
|
||||||
@ -3127,7 +3184,14 @@ impl RenderState {
|
|||||||
scale: f32,
|
scale: f32,
|
||||||
node_render_state: &NodeRenderState,
|
node_render_state: &NodeRenderState,
|
||||||
target_surface: SurfaceId,
|
target_surface: SurfaceId,
|
||||||
) -> Result<()> {
|
) -> Result<bool> {
|
||||||
|
// Avoid a blank DropShadows→Current blit + clear on every shape without
|
||||||
|
// shadows. Callers must still touch DropShadows once per tile when this
|
||||||
|
// returns false (see `drop_shadows_ops_warmed`).
|
||||||
|
if element.drop_shadows_visible().next().is_none() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
||||||
let inherited_layer_blur = match element.shape_type {
|
let inherited_layer_blur = match element.shape_type {
|
||||||
Type::Frame(_) | Type::Group(_) => element.blur,
|
Type::Frame(_) | Type::Group(_) => element.blur,
|
||||||
@ -3242,7 +3306,7 @@ impl RenderState {
|
|||||||
self.surfaces
|
self.surfaces
|
||||||
.canvas(SurfaceId::DropShadows)
|
.canvas(SurfaceId::DropShadows)
|
||||||
.clear(skia::Color::TRANSPARENT);
|
.clear(skia::Color::TRANSPARENT);
|
||||||
Ok(())
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_shape_tree_partial_uncached(
|
pub fn render_shape_tree_partial_uncached(
|
||||||
@ -3482,8 +3546,8 @@ impl RenderState {
|
|||||||
&& Self::frame_clip_layer_blur(element).is_some()
|
&& Self::frame_clip_layer_blur(element).is_some()
|
||||||
&& element.drop_shadows_visible().next().is_some();
|
&& element.drop_shadows_visible().next().is_some();
|
||||||
|
|
||||||
if shadow_before_layer {
|
if shadow_before_layer
|
||||||
self.render_element_drop_shadows_and_composite(
|
&& self.render_element_drop_shadows_and_composite(
|
||||||
element,
|
element,
|
||||||
tree,
|
tree,
|
||||||
&mut extrect,
|
&mut extrect,
|
||||||
@ -3491,7 +3555,9 @@ impl RenderState {
|
|||||||
scale,
|
scale,
|
||||||
&node_render_state,
|
&node_render_state,
|
||||||
target_surface,
|
target_surface,
|
||||||
)?;
|
)?
|
||||||
|
{
|
||||||
|
self.drop_shadows_ops_warmed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render background blur BEFORE save_layer so it modifies
|
// Render background blur BEFORE save_layer so it modifies
|
||||||
@ -3514,8 +3580,7 @@ impl RenderState {
|
|||||||
if !skip_shadows
|
if !skip_shadows
|
||||||
&& !shadows_already_rendered
|
&& !shadows_already_rendered
|
||||||
&& !matches!(element.shape_type, Type::Text(_))
|
&& !matches!(element.shape_type, Type::Text(_))
|
||||||
{
|
&& self.render_element_drop_shadows_and_composite(
|
||||||
self.render_element_drop_shadows_and_composite(
|
|
||||||
element,
|
element,
|
||||||
tree,
|
tree,
|
||||||
&mut extrect,
|
&mut extrect,
|
||||||
@ -3523,11 +3588,26 @@ impl RenderState {
|
|||||||
scale,
|
scale,
|
||||||
&node_render_state,
|
&node_render_state,
|
||||||
target_surface,
|
target_surface,
|
||||||
)?;
|
)?
|
||||||
} else {
|
{
|
||||||
// This is necessary or the later flush_and_submit will be very slow
|
// Real shadow composite already clears DropShadows.
|
||||||
|
self.drop_shadows_ops_warmed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.drop_shadows_ops_warmed {
|
||||||
|
// Touch DropShadows→Current once per tile when no shape has
|
||||||
|
// composited real shadows yet. Omitting this entirely made
|
||||||
|
// flush_and_submit very slow (ops-task ordering); repeating
|
||||||
|
// it per shape was waste.
|
||||||
|
self.surfaces.draw_into(
|
||||||
|
SurfaceId::DropShadows,
|
||||||
|
target_surface,
|
||||||
|
Some(&skia::Paint::default()),
|
||||||
|
);
|
||||||
self.surfaces
|
self.surfaces
|
||||||
.draw_into(SurfaceId::DropShadows, target_surface, None);
|
.canvas(SurfaceId::DropShadows)
|
||||||
|
.clear(skia::Color::TRANSPARENT);
|
||||||
|
self.drop_shadows_ops_warmed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For frames without clip_content, inner strokes must render after children in
|
// For frames without clip_content, inner strokes must render after children in
|
||||||
@ -3729,6 +3809,7 @@ impl RenderState {
|
|||||||
// empty tile.
|
// empty tile.
|
||||||
self.current_tile_had_shapes = false;
|
self.current_tile_had_shapes = false;
|
||||||
self.tile_atlas_flushed = false;
|
self.tile_atlas_flushed = false;
|
||||||
|
self.drop_shadows_ops_warmed = false;
|
||||||
|
|
||||||
let viewer_masked_pass = self.viewer_masked_pass();
|
let viewer_masked_pass = self.viewer_masked_pass();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user