This commit is contained in:
Elena Torro 2026-08-21 12:28:12 +02:00
parent 4cb9f951d2
commit 4a9f55d47d
5 changed files with 149 additions and 28 deletions

View File

@ -126,7 +126,24 @@ pub extern "C" fn render(timestamp: i32, flags: u8) -> Result<FrameType> {
let is_partial = flags & RenderFlag::Partial as u8 == RenderFlag::Partial as u8;
if flags & RenderFlag::SyncTiles as u8 != 0 {
render_state.preserve_target_during_render = true;
// Pan/zoom pause: the viewbox is stable, so render at full quality
// and let the tiles reach the atlas. Fast-mode tiles are dropped by
// `apply_render_to_final_canvas`. `set_view_start` re-arms fast mode
// if the gesture resumes.
if render_state.options.is_fast_mode() {
render_state.options.set_fast_mode(false);
render_state.tile_viewbox.update(&render_state.viewbox);
render_state.rebuild_tile_index(&state.shapes);
}
}
// Gesture in progress with no pause: the viewbox is moving and
// `render_from_cache` owns the screen. Stop the progressive loop rather
// than rasterizing tiles that would be dropped.
if render_state.options.is_fast_mode() && !render_state.options.is_interactive_transform() {
return Ok(FrameType::Full);
}
let frame_type = if is_partial && !render_state.preserve_target_during_render {
state
.continue_render_loop(timestamp)

View File

@ -656,6 +656,11 @@ impl RenderState {
Self::blur_from_variance(total)
}
fn subtract_blur_values(total: Option<Blur>, applied: Option<Blur>) -> Option<Blur> {
let remaining = Self::blur_variance(total) - Self::blur_variance(applied);
Self::blur_from_variance(remaining.max(0.))
}
fn frame_clip_layer_blur(shape: &Shape) -> Option<Blur> {
shape.frame_clip_layer_blur()
}
@ -2339,10 +2344,6 @@ impl RenderState {
let preserve_target = self.preserve_target_during_render;
self.preserve_target_during_render = false;
if preserve_target && self.options.is_fast_mode() {
self.rebuild_tile_index(tree);
}
if self.options.is_interactive_transform() {
// Keep `Target` as the previous frame and overwrite only the tiles
// that changed. This avoids clearing + redrawing an atlas backdrop
@ -3039,6 +3040,8 @@ impl RenderState {
shadow: &Shadow,
scale: f32,
inherited_layer_blur: Option<Blur>,
layer_applied_blur: Option<Blur>,
layer_owns_shadow_blur: bool,
node_render_state: &NodeRenderState,
target_surface: SurfaceId,
) -> Result<()> {
@ -3071,19 +3074,29 @@ impl RenderState {
nested_clip_bounds,
scale,
inherited_layer_blur,
layer_applied_blur,
layer_owns_shadow_blur,
target_surface,
)?;
} else {
let text_extrect = shadow_shape.extrect(tree, scale);
let layer_bounds = shadow
.get_drop_shadow_filter()
.map(|filter| filter.compute_fast_bounds(text_extrect))
.unwrap_or(text_extrect);
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
let layer_rec = skia::canvas::SaveLayerRec::default()
.bounds(&layer_bounds)
.paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
transformed_shadow.to_mut().color = skia::Color::BLACK;
transformed_shadow.to_mut().blur = transformed_shadow.blur;
transformed_shadow.to_mut().spread = transformed_shadow.spread;
if layer_owns_shadow_blur {
transformed_shadow.to_mut().blur = 0.0;
}
let mut new_shadow_paint = skia::Paint::default();
new_shadow_paint.set_image_filter(transformed_shadow.get_drop_shadow_filter());
@ -3124,6 +3137,8 @@ impl RenderState {
clip_bounds: Option<ClipStack>,
scale: f32,
extra_layer_blur: Option<Blur>,
layer_applied_blur: Option<Blur>,
layer_owns_shadow_blur: bool,
target_surface: SurfaceId,
) -> Result<()> {
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
@ -3131,8 +3146,10 @@ impl RenderState {
transformed_shadow.to_mut().color = skia::Color::BLACK;
let mut plain_shape = Cow::Borrowed(shape);
let combined_blur =
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur);
let combined_blur = Self::subtract_blur_values(
Self::combine_blur_values(self.combined_layer_blur(shape.blur), extra_layer_blur),
layer_applied_blur,
);
let blur_filter = combined_blur.and_then(|blur| {
let sigma = blur.sigma();
skia::image_filters::blur((sigma, sigma), None, None, None)
@ -3186,8 +3203,11 @@ impl RenderState {
return Ok(());
}
// blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter).
if scale > 1.0 && shadow.blur <= 0.0 {
if layer_owns_shadow_blur {
transformed_shadow.to_mut().blur = 0.0;
}
if combined_blur.is_none() && transformed_shadow.blur <= 0.0 {
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
drop_canvas.save();
//drop_canvas.scale((scale, scale));
@ -3227,7 +3247,9 @@ impl RenderState {
}
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()
.bounds(&bounds)
.paint(&shadow_paint);
// Low zoom path: use blur filter but apply offset and spread geometrically
if use_low_zoom_path {
@ -3321,11 +3343,11 @@ impl RenderState {
)?;
if let Some((mut surface, filter_scale)) = filter_result {
let cached = shadows::CachedDropShadowFilter::new(
bounds,
filter_scale,
surface.image_snapshot(),
);
let Some(image) = shadows::snapshot_filter_region(&mut surface, &bounds, filter_scale)
else {
return Ok(());
};
let cached = shadows::CachedDropShadowFilter::new(bounds, filter_scale, image);
shadows::blit_cached_drop_shadow_filter(
&mut self.surfaces,
&cached,
@ -3379,8 +3401,36 @@ impl RenderState {
continue;
}
rendered_any = true;
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
let layer_bounds = shadow
.get_drop_shadow_filter()
.map(|filter| filter.compute_fast_bounds(*element_extrect))
.unwrap_or(*element_extrect);
let layer_blur = Self::combine_blur_values(
inherited_layer_blur,
Some(Blur::new(BlurType::LayerBlur, false, shadow.blur)),
);
let layer_sigma = layer_blur.map_or(0., |blur| blur.sigma());
let layer_owns_blur = !use_direct_container_shadow
&& layer_sigma * 3.0 * scale <= self.surfaces.margins().width as f32;
let layer_applied_blur = if layer_owns_blur {
inherited_layer_blur
} else {
None
};
let mut paint = skia::Paint::default();
if layer_owns_blur && layer_sigma > 0. {
paint.set_image_filter(skia::image_filters::blur(
(layer_sigma, layer_sigma),
None,
None,
None,
));
}
let layer_rec = skia::canvas::SaveLayerRec::default()
.bounds(&layer_bounds)
.paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
@ -3402,6 +3452,8 @@ impl RenderState {
clip_bounds.clone(),
scale,
None,
layer_applied_blur,
layer_owns_blur,
target_surface,
)?;
if !element.container_fill_covers_shadow_descendants(tree, scale) {
@ -3411,6 +3463,8 @@ impl RenderState {
shadow,
scale,
inherited_layer_blur,
layer_applied_blur,
layer_owns_blur,
node_render_state,
target_surface,
)?;

View File

@ -26,6 +26,17 @@ pub fn compose_filters(
}
}
pub fn filter_region(bounds: &Rect, scale: f32, max_width: i32, max_height: i32) -> skia::IRect {
const PAD: i32 = 4;
let width = ((bounds.width() * scale).ceil() as i32)
.saturating_add(PAD)
.clamp(1, max_width);
let height = ((bounds.height() * scale).ceil() as i32)
.saturating_add(PAD)
.clamp(1, max_height);
skia::IRect::from_xywh(0, 0, width, height)
}
/// Renders filtered content offscreen and composites it back into the target surface.
///
/// This helper is meant for shapes that rely on blur/filters that should be evaluated
@ -45,6 +56,7 @@ where
if let Some((mut surface, scale)) =
render_into_filter_surface(render_state, bounds, 1.0, draw_fn)?
{
let region = Rect::from(filter_region(&bounds, scale, surface.width(), surface.height()));
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
@ -52,11 +64,13 @@ where
canvas.save();
canvas.scale((1.0 / scale, 1.0 / scale));
canvas.translate((bounds.left * scale, bounds.top * scale));
canvas.clip_rect(region, None, false);
surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None);
canvas.restore();
} else {
canvas.save();
canvas.translate((bounds.left, bounds.top));
canvas.clip_rect(region, None, false);
surface.draw(canvas, (0.0, 0.0), get_resources().sampling_options, None);
canvas.restore();
}
@ -118,10 +132,12 @@ where
// Combine overflow-fit scale with caller-requested extra downscale
let scale = (fit_scale * extra_downscale).max(MIN_COMBINED_SCALE);
let region = filter_region(&bounds, scale, filter_width, filter_height);
{
let canvas = render_state.surfaces.canvas(filter_id);
canvas.clear(skia::Color::TRANSPARENT);
canvas.save();
canvas.clip_irect(region, None);
canvas.clear(skia::Color::TRANSPARENT);
// Apply scale first, then translate
canvas.scale((scale, scale));
canvas.translate((-bounds.left, -bounds.top));

View File

@ -30,7 +30,7 @@ impl GpuState {
context_options.skip_gl_error_checks = Enable::Yes;
// context_options.runtime_program_cache_size = 1024;
// context_options.allow_multiple_glyph_cache_textures = Enable::Yes;
// context_options.allow_path_mask_caching = false;
context_options.allow_path_mask_caching = true;
let context = gpu::direct_contexts::make_gl(interface, Some(&context_options)).ok_or(
Error::CriticalError("Failed to create GL context".to_string()),

View File

@ -142,16 +142,32 @@ fn spread_inset(spread: f32) -> Option<f32> {
Some(-spread).filter(|&s| s > 0.0)
}
fn blur_layer_filter(blur: f32, sigma_scale: f32) -> Option<skia::ImageFilter> {
if blur <= 0.0 {
return None;
}
let sigma = radius_to_sigma(blur) * sigma_scale;
skia::image_filters::blur((sigma, sigma), None, None, None)
}
fn blur_layer_paint(blur: f32, sigma_scale: f32) -> skia::Paint {
let mut paint = skia::Paint::default();
if blur > 0.0 {
let sigma = radius_to_sigma(blur) * sigma_scale;
paint.set_image_filter(skia::image_filters::blur((sigma, sigma), None, None, None));
}
paint.set_image_filter(blur_layer_filter(blur, sigma_scale));
paint.set_blend_mode(skia::BlendMode::SrcOver);
paint
}
fn frame_shadow_layer_bounds(frame: &Shape, shadow: &Shadow, sigma_scale: f32) -> Rect {
let mut rect = frame.selrect;
if shadow.spread > 0.0 {
rect.outset((shadow.spread, shadow.spread));
}
match blur_layer_filter(shadow.blur, sigma_scale) {
Some(filter) => filter.compute_fast_bounds(rect),
None => rect,
}
}
fn draw_frame_shadow_rect(
surfaces: &mut super::Surfaces,
surface_id: SurfaceId,
@ -222,6 +238,15 @@ fn blur_downscale_for_frame_shadow(
}
}
pub(crate) fn snapshot_filter_region(
surface: &mut skia::Surface,
bounds: &Rect,
filter_scale: f32,
) -> Option<skia::Image> {
let region = filters::filter_region(bounds, filter_scale, surface.width(), surface.height());
surface.image_snapshot_with_bounds(region)
}
pub(crate) fn blit_cached_drop_shadow_filter(
surfaces: &mut super::Surfaces,
cached: &CachedDropShadowFilter,
@ -259,13 +284,16 @@ fn render_inline_frame_shadow(
) -> Result<()> {
let antialias = frame_shadow_antialias(state, frame, scale);
let layer_paint = blur_layer_paint(shadow.blur, 1.0);
let layer_bounds = frame_shadow_layer_bounds(frame, shadow, 1.0);
let draw_matrix = frame_shadow_draw_matrix(frame, shadow);
{
let drop_canvas = state.surfaces.canvas(SurfaceId::DropShadows);
drop_canvas.save();
drop_canvas.concat(&draw_matrix);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&layer_paint);
let layer_rec = skia::canvas::SaveLayerRec::default()
.bounds(&layer_bounds)
.paint(&layer_paint);
drop_canvas.save_layer(&layer_rec);
}
@ -315,7 +343,10 @@ fn render_cached_filter_frame_shadow(
state.options.blur_downscale_threshold,
);
let layer_paint = blur_layer_paint(shadow.blur, blur_downscale);
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&layer_paint);
let layer_bounds = frame_shadow_layer_bounds(frame, shadow, blur_downscale);
let layer_rec = skia::canvas::SaveLayerRec::default()
.bounds(&layer_bounds)
.paint(&layer_paint);
let shadow_draw_matrix = frame_shadow_draw_matrix(frame, shadow);
let filter_result = filters::render_into_filter_surface(
@ -340,10 +371,13 @@ fn render_cached_filter_frame_shadow(
)?;
if let Some((mut surface, filter_scale)) = filter_result {
let Some(image) = snapshot_filter_region(&mut surface, &bounds, filter_scale) else {
return Ok(());
};
let cached = CachedDropShadowFilter {
bounds,
filter_scale,
image: surface.image_snapshot(),
image,
};
blit_cached_drop_shadow_filter(&mut state.surfaces, &cached, None);
state.drop_shadow_filter_cache.store(key, cached);