Prefer direct painting when effects are imperceptible

Skip the Fills/Strokes layered path when drop/inner shadows would
not paint at the current scale, and allow stroke-only shapes
(fills_none) on the direct path. Apply the same footprint LOD to
inner-shadow painting. Raise the render budget slightly during
multi-tile paint regions.
This commit is contained in:
Alejandro Alonso 2026-08-03 11:27:07 +02:00
parent fa23668001
commit 18381f3e09
2 changed files with 57 additions and 29 deletions

View File

@ -1540,11 +1540,29 @@ impl RenderState {
return Ok(());
}
// Only perceptible shadows need the layered Fills/Strokes path. Use the
// same footprint LOD as when painting drop and inner shadows.
let scale = self.get_scale();
let shadows_need_layered = !skip_drop_shadows
&& (shape
.drop_shadows_visible()
.any(|s| s.is_perceptible_at_scale_for(scale, shape.is_recursive()))
|| shape
.inner_shadows_visible()
.any(|s| s.is_perceptible_at_scale_for(scale, shape.is_recursive())));
// 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.
// Stroke-only (fills_none) can go direct: empty fills are a no-op and
// strokes paint into Current.
let type_ok = matches!(
shape.shape_type,
Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_) | Type::Frame(_)
);
let needs_nested_fills = shape.fills.is_empty() && has_nested_fills;
let can_render_directly = apply_to_current_surface
&& offset.is_none()
&& parent_shadows.is_none()
@ -1554,20 +1572,12 @@ impl RenderState {
&& shape.blur.is_none()
&& shape.background_blur.is_none()
&& !has_inherited_blur
&& shape.shadows.is_empty()
&& matches!(
shape.shape_type,
Type::Rect(_) | Type::Circle | Type::Path(_) | Type::Bool(_) | Type::Frame(_)
)
&& !(shape.fills.is_empty() && has_nested_fills)
&& !shape
.svg_attrs
.as_ref()
.is_some_and(|attrs| attrs.fill_none)
&& !shadows_need_layered
&& type_ok
&& !needs_nested_fills
&& target_surface != SurfaceId::Export;
if can_render_directly {
let scale = self.get_scale();
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
@ -2976,7 +2986,13 @@ impl RenderState {
if iteration % self.options.node_batch_threshold != 0 {
return false;
}
if performance::get_time() - timestamp <= self.options.max_blocking_time_ms {
// Multi-tile paint regions need fewer yields to finish visible HQ work.
let budget = if self.paint_region.is_some() {
self.options.max_blocking_time_ms.max(48)
} else {
self.options.max_blocking_time_ms
};
if performance::get_time() - timestamp <= budget {
return false;
}
@ -2995,11 +3011,11 @@ impl RenderState {
true
}
/// Skip all drop shadows in fast mode, or when even a large design-space
/// Skip all drop/inner shadows in fast mode, or when even a large design-space
/// shadow would be subpixel. Otherwise filter per shadow via
/// [`Shadow::is_perceptible_at_scale_for`] (stricter for recursive shapes).
#[inline]
fn should_skip_drop_shadows(&self) -> bool {
pub(crate) fn should_skip_drop_shadows(&self) -> bool {
if self.options.is_fast_mode() {
return true;
}

View File

@ -13,10 +13,16 @@ pub fn render_fill_inner_shadows(
antialias: bool,
surface_id: SurfaceId,
) {
if shape.has_fills() {
for shadow in shape.inner_shadows_visible() {
render_fill_inner_shadow(render_state, shape, shadow, antialias, surface_id);
if !shape.has_fills() || render_state.should_skip_drop_shadows() {
return;
}
let scale = render_state.get_scale();
let recursive = shape.is_recursive();
for shadow in shape.inner_shadows_visible() {
if !shadow.is_perceptible_at_scale_for(scale, recursive) {
continue;
}
render_fill_inner_shadow(render_state, shape, shadow, antialias, surface_id);
}
}
@ -38,19 +44,25 @@ pub fn render_stroke_inner_shadows(
antialias: bool,
surface_id: SurfaceId,
) -> Result<()> {
if !shape.has_fills() {
for shadow in shape.inner_shadows_visible() {
let filter = shadow.get_inner_shadow_filter();
strokes::render_single(
render_state,
shape,
stroke,
Some(surface_id),
filter.as_ref(),
antialias,
None, // Inner shadows don't use spread
)?;
if shape.has_fills() || render_state.should_skip_drop_shadows() {
return Ok(());
}
let scale = render_state.get_scale();
let recursive = shape.is_recursive();
for shadow in shape.inner_shadows_visible() {
if !shadow.is_perceptible_at_scale_for(scale, recursive) {
continue;
}
let filter = shadow.get_inner_shadow_filter();
strokes::render_single(
render_state,
shape,
stroke,
Some(surface_id),
filter.as_ref(),
antialias,
None, // Inner shadows don't use spread
)?;
}
Ok(())
}