diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index c5bff2105f..e0c60a75d9 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -126,7 +126,24 @@ pub extern "C" fn render(timestamp: i32, flags: u8) -> Result { 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) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 1854a2d5ba..35e2a520fe 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -656,6 +656,11 @@ impl RenderState { Self::blur_from_variance(total) } + fn subtract_blur_values(total: Option, applied: Option) -> Option { + 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 { 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, + layer_applied_blur: Option, + 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 = 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, scale: f32, extra_layer_blur: Option, + layer_applied_blur: Option, + layer_owns_shadow_blur: bool, target_surface: SurfaceId, ) -> Result<()> { let mut transformed_shadow: Cow = 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, )?; diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index b4313a043d..9f1892c46a 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -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)); diff --git a/render-wasm/src/render/gpu_state.rs b/render-wasm/src/render/gpu_state.rs index 7a158b5fc6..76e9842cc8 100644 --- a/render-wasm/src/render/gpu_state.rs +++ b/render-wasm/src/render/gpu_state.rs @@ -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()), diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 19f90542cf..b5db531388 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -142,16 +142,32 @@ fn spread_inset(spread: f32) -> Option { Some(-spread).filter(|&s| s > 0.0) } +fn blur_layer_filter(blur: f32, sigma_scale: f32) -> Option { + 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 { + 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);