Compare commits

...

2 Commits

Author SHA1 Message Date
Alejandro Alonso
62df6455fb WIP 2026-02-18 17:31:36 +01:00
Alejandro Alonso
6b44f6e891 🐛 Fix blur 0 artifacts 2026-02-18 17:18:48 +01:00
3 changed files with 108 additions and 5 deletions

View File

@@ -1566,6 +1566,7 @@ impl RenderState {
};
let mut bounds = drop_filter.compute_fast_bounds(shape_bounds);
// Account for the shadow offset so the temporary surface fully contains the shifted blur.
bounds.offset(world_offset);
// Early cull if the shadow bounds are outside the render area.
@@ -1573,6 +1574,76 @@ impl RenderState {
return;
}
// Special case: blur=0 and spread>0 with zoom>100% - render directly to avoid resampling
// artifacts from render_into_filter_surface when spread is large.
if scale > 1.0
// && combined_blur.is_none()
&& shadow.blur <= 0.0
// && shadow.spread <= 0.0
{
// Create filter without dilate first (just offset)
let mut shadow_no_spread = transformed_shadow.clone();
shadow_no_spread.to_mut().spread = 0.0;
let Some(offset_filter) = shadow_no_spread.get_drop_shadow_filter() else {
return;
};
// First, render with offset only
{
let mut shadow_paint = skia::Paint::default();
shadow_paint.set_image_filter(offset_filter);
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);
{
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
drop_canvas.save_layer(&layer_rec);
drop_canvas.scale((scale, scale));
drop_canvas.translate(translation);
}
self.with_nested_blurs_suppressed(|state| {
state.render_shape(
&plain_shape,
clip_bounds,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
false,
Some(shadow.offset),
None,
);
});
{
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
drop_canvas.restore();
}
}
// Then apply dilate filter to expand (in scaled space, so multiply by scale)
let spread_scaled = shadow.spread * scale;
let dilate_filter = skia::image_filters::dilate(
(spread_scaled, spread_scaled),
None,
None,
);
let mut dilate_paint = skia::Paint::default();
dilate_paint.set_image_filter(dilate_filter);
dilate_paint.set_blend_mode(skia::BlendMode::SrcOver);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&dilate_paint);
{
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
drop_canvas.save_layer(&layer_rec);
// No additional transformations needed - dilate applies to existing content
drop_canvas.restore();
}
return;
}
if use_low_zoom_path {
let mut shadow_paint = skia::Paint::default();
shadow_paint.set_image_filter(drop_filter);
@@ -1603,12 +1674,24 @@ impl RenderState {
}
let filter_result =
filters::render_into_filter_surface(self, bounds, |state, temp_surface| {
filters::render_into_filter_surface(self, bounds, |state, temp_surface, filter_scale| {
{
let canvas = state.surfaces.canvas(temp_surface);
// Adjust the filter to compensate for the scaling that render_into_filter_surface
// applies. If filter_scale < 1.0, the canvas is scaled down, so we need to
// scale up the spread to maintain the correct visual size.
let adjusted_filter = if filter_scale < 1.0 && shadow.spread > 0.0 {
// Create a new filter with adjusted spread
let mut adjusted_shadow = transformed_shadow.clone();
adjusted_shadow.to_mut().spread = shadow.spread / filter_scale;
adjusted_shadow.get_drop_shadow_filter().unwrap_or(drop_filter.clone())
} else {
drop_filter.clone()
};
let mut shadow_paint = skia::Paint::default();
shadow_paint.set_image_filter(drop_filter.clone());
shadow_paint.set_image_filter(adjusted_filter);
shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&shadow_paint);

View File

@@ -40,7 +40,9 @@ pub fn render_with_filter_surface<F>(
where
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, |state, surface_id, _filter_scale| {
draw_fn(state, surface_id)
}) {
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
@@ -69,13 +71,15 @@ where
/// 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
/// render the untransformed shape (i.e. in document coordinates) onto `SurfaceId::Filter`.
/// The `draw_fn` receives the `filter_scale` that will be applied, allowing it to
/// adjust filters (like spread) accordingly.
pub fn render_into_filter_surface<F>(
render_state: &mut RenderState,
bounds: Rect,
draw_fn: F,
) -> Option<(skia::Surface, f32)>
where
F: FnOnce(&mut RenderState, SurfaceId),
F: FnOnce(&mut RenderState, SurfaceId, f32),
{
if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 {
return None;
@@ -105,7 +109,7 @@ where
canvas.translate((-bounds.left, -bounds.top));
}
draw_fn(render_state, filter_id);
draw_fn(render_state, filter_id, scale);
render_state.surfaces.canvas(filter_id).restore();

View File

@@ -215,6 +215,22 @@ impl Surfaces {
);
}
/// Draws one surface into another using nearest-neighbor sampling to avoid interpolation blur.
/// Use this for DropShadows when rendering sharp shadows (blur=0, spread=0) at high zoom.
pub fn draw_into_nearest(&mut self, from: SurfaceId, to: SurfaceId, paint: Option<&skia::Paint>) {
let nearest_sampling = skia::SamplingOptions::new(
skia::FilterMode::Nearest,
skia::MipmapMode::None,
);
self.get_mut(from).clone().draw(
self.canvas_and_mark_dirty(to),
(0.0, 0.0),
nearest_sampling,
paint,
);
}
/// Draws the cache surface directly to the target canvas.
/// This avoids creating an intermediate snapshot, reducing GPU stalls.
pub fn draw_cache_to_target(&mut self) {