diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index f009946a21..e80f029adc 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -264,7 +264,6 @@ pub(crate) struct RenderState { pub fonts: FontStore, pub viewbox: Viewbox, pub cached_viewbox: Viewbox, - pub cached_target_snapshot: Option, pub images: ImageStore, pub background_color: skia::Color, // Identifier of the current requestAnimationFrame call, if any. @@ -345,7 +344,6 @@ impl RenderState { fonts, viewbox, cached_viewbox: Viewbox::new(0., 0.), - cached_target_snapshot: None, images: ImageStore::new(gpu_state.context.clone()), background_color: skia::Color::TRANSPARENT, render_request_id: None, @@ -1094,15 +1092,12 @@ impl RenderState { let _start = performance::begin_timed_log!("render_from_cache"); performance::begin_measure!("render_from_cache"); let scale = self.get_cached_scale(); - if let Some(snapshot) = &self.cached_target_snapshot { - let canvas = self.surfaces.canvas(SurfaceId::Target); - canvas.save(); + // Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache) + if self.cached_viewbox.area.width() > 0.0 { // Scale and translate the target according to the cached data let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom; - canvas.scale((navigate_zoom, navigate_zoom)); - let TileRect(start_tile_x, start_tile_y, _, _) = tiles::get_tiles_for_viewbox_with_interest( self.cached_viewbox, @@ -1111,15 +1106,24 @@ impl RenderState { ); let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr(); let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr(); + let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x; + let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y; + let bg_color = self.background_color; - canvas.translate(( - (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x, - (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y, - )); + // Setup canvas transform + { + let canvas = self.surfaces.canvas(SurfaceId::Target); + canvas.save(); + canvas.scale((navigate_zoom, navigate_zoom)); + canvas.translate((translate_x, translate_y)); + canvas.clear(bg_color); + } - canvas.clear(self.background_color); - canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default())); - canvas.restore(); + // Draw directly from cache surface, avoiding snapshot overhead + self.surfaces.draw_cache_to_target(); + + // Restore canvas state + self.surfaces.canvas(SurfaceId::Target).restore(); if self.options.is_debug_visible() { debug::render(self); @@ -1587,7 +1591,7 @@ impl RenderState { } }); - if let Some((image, filter_scale)) = filter_result { + if let Some((mut surface, filter_scale)) = filter_result { let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); drop_canvas.save(); drop_canvas.scale((scale, scale)); @@ -1597,34 +1601,26 @@ impl RenderState { // If we scaled down in the filter surface, we need to scale back up if filter_scale < 1.0 { - let scaled_width = bounds.width() * filter_scale; - let scaled_height = bounds.height() * filter_scale; - let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height); - drop_canvas.save(); drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale)); - drop_canvas.draw_image_rect_with_sampling_options( - image, - Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), - skia::Rect::from_xywh( - bounds.left * filter_scale, - bounds.top * filter_scale, - scaled_width, - scaled_height, - ), + drop_canvas.translate((bounds.left * filter_scale, bounds.top * filter_scale)); + surface.draw( + drop_canvas, + (0.0, 0.0), self.sampling_options, - &drop_paint, + Some(&drop_paint), ); drop_canvas.restore(); } else { - let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height()); - drop_canvas.draw_image_rect_with_sampling_options( - image, - Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), - bounds, + drop_canvas.save(); + drop_canvas.translate((bounds.left, bounds.top)); + surface.draw( + drop_canvas, + (0.0, 0.0), self.sampling_options, - &drop_paint, + Some(&drop_paint), ); + drop_canvas.restore(); } drop_canvas.restore(); } @@ -2097,11 +2093,9 @@ impl RenderState { self.surfaces.gc(); - // Cache target surface in a texture + // Mark cache as valid for render_from_cache self.cached_viewbox = self.viewbox; - self.cached_target_snapshot = Some(self.surfaces.snapshot(SurfaceId::Cache)); - if self.options.is_debug_visible() { debug::render(self); } diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index 832fc32d88..557f92bb75 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -40,41 +40,21 @@ pub fn render_with_filter_surface( where F: FnOnce(&mut RenderState, SurfaceId), { - if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) { + if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) { 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 if scale < 1.0 { - // The image was rendered at a smaller scale, so we need to scale it back up - let scaled_width = bounds.width() * scale; - let scaled_height = bounds.height() * scale; - let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height); - canvas.save(); canvas.scale((1.0 / scale, 1.0 / scale)); - canvas.draw_image_rect_with_sampling_options( - image, - Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), - skia::Rect::from_xywh( - bounds.left * scale, - bounds.top * scale, - scaled_width, - scaled_height, - ), - render_state.sampling_options, - &skia::Paint::default(), - ); + canvas.translate((bounds.left * scale, bounds.top * scale)); + surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None); canvas.restore(); } else { - // No scaling needed, draw normally - let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height()); - canvas.draw_image_rect_with_sampling_options( - image, - Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), - bounds, - render_state.sampling_options, - &skia::Paint::default(), - ); + canvas.save(); + canvas.translate((bounds.left, bounds.top)); + surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None); + canvas.restore(); } true } else { @@ -93,7 +73,7 @@ pub fn render_into_filter_surface( render_state: &mut RenderState, bounds: Rect, draw_fn: F, -) -> Option<(skia::Image, f32)> +) -> Option<(skia::Surface, f32)> where F: FnOnce(&mut RenderState, SurfaceId), { @@ -129,5 +109,6 @@ where render_state.surfaces.canvas(filter_id).restore(); - Some((render_state.surfaces.snapshot(filter_id), scale)) + let filter_surface = render_state.surfaces.surface_clone(filter_id); + Some((filter_surface, scale)) } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 8719b0373a..86a0f0422e 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -175,6 +175,10 @@ impl Surfaces { self.get_mut(id).canvas() } + pub fn surface_clone(&self, id: SurfaceId) -> skia::Surface { + self.get(id).clone() + } + /// Marks a surface as having content (dirty) pub fn mark_dirty(&mut self, id: SurfaceId) { self.dirty_surfaces |= id as u32; @@ -211,6 +215,18 @@ impl Surfaces { ); } + /// 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) { + let sampling_options = self.sampling_options; + self.cache.clone().draw( + self.target.canvas(), + (0.0, 0.0), + sampling_options, + Some(&skia::Paint::default()), + ); + } + pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) { performance::begin_measure!("apply_mut::flags"); if ids & SurfaceId::Target as u32 != 0 { @@ -305,6 +321,22 @@ impl Surfaces { } } + fn get(&self, id: SurfaceId) -> &skia::Surface { + match id { + SurfaceId::Target => &self.target, + SurfaceId::Filter => &self.filter, + SurfaceId::Cache => &self.cache, + SurfaceId::Current => &self.current, + SurfaceId::DropShadows => &self.drop_shadows, + SurfaceId::InnerShadows => &self.inner_shadows, + SurfaceId::TextDropShadows => &self.text_drop_shadows, + SurfaceId::Fills => &self.shape_fills, + SurfaceId::Strokes => &self.shape_strokes, + SurfaceId::Debug => &self.debug, + SurfaceId::UI => &self.ui, + } + } + fn reset_from_target(&mut self, target: skia::Surface) { let dim = (target.width(), target.height()); self.target = target; @@ -386,14 +418,22 @@ impl Surfaces { self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height, ); - if let Some(snapshot) = self.current.image_snapshot_with_bounds(rect) { - self.tiles.add(tile_viewbox, tile, snapshot.clone()); + let snapshot = self.current.image_snapshot(); + let mut direct_context = self.current.direct_context(); + let tile_image_opt = snapshot + .make_subset(direct_context.as_mut(), rect) + .or_else(|| self.current.image_snapshot_with_bounds(rect)); + + if let Some(tile_image) = tile_image_opt { + // Draw to cache first (takes reference), then move to tile cache self.cache.canvas().draw_image_rect( - snapshot.clone(), + &tile_image, None, tile_rect, &skia::Paint::default(), ); + + self.tiles.add(tile_viewbox, tile, tile_image); } } @@ -409,16 +449,57 @@ impl Surfaces { } pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) { - let image = self.tiles.get(tile).unwrap(); + if let Some(image) = self.tiles.get(tile) { + let mut paint = skia::Paint::default(); + paint.set_color(color); + self.target.canvas().draw_rect(rect, &paint); + + self.target + .canvas() + .draw_image_rect(&image, None, rect, &skia::Paint::default()); + } + } + + /// Draws the current tile directly to the target and cache surfaces without + /// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't + /// populate the tile texture cache (suitable for one-shot renders like tests). + pub fn draw_current_tile_direct(&mut self, tile_rect: &skia::Rect, color: skia::Color) { + let sampling_options = self.sampling_options; + let src_rect = IRect::from_xywh( + self.margins.width, + self.margins.height, + self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width, + self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height, + ); + let src_rect_f = skia::Rect::from(src_rect); + + // Draw background let mut paint = skia::Paint::default(); paint.set_color(color); + self.target.canvas().draw_rect(tile_rect, &paint); - self.target.canvas().draw_rect(rect, &paint); + // Draw current surface directly to target (no snapshot) + self.current.clone().draw( + self.target.canvas(), + ( + tile_rect.left - src_rect_f.left, + tile_rect.top - src_rect_f.top, + ), + sampling_options, + None, + ); - self.target - .canvas() - .draw_image_rect(&image, None, rect, &skia::Paint::default()); + // Also draw to cache for render_from_cache + self.current.clone().draw( + self.cache.canvas(), + ( + tile_rect.left - src_rect_f.left, + tile_rect.top - src_rect_f.top, + ), + sampling_options, + None, + ); } pub fn remove_cached_tiles(&mut self, color: skia::Color) { @@ -491,9 +572,11 @@ impl TileTextureCache { } } - pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Image, String> { - let image = self.grid.get_mut(&tile).unwrap(); - Ok(image) + pub fn get(&mut self, tile: Tile) -> Option<&mut skia::Image> { + if self.removed.contains(&tile) { + return None; + } + self.grid.get_mut(&tile) } pub fn remove(&mut self, tile: Tile) {