From 480e0887e3a58e32c1a1084e8bd53dcbce8dda64 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 25 Apr 2025 12:22:01 +0200 Subject: [PATCH] :tada: Improve zoom in/out performance --- frontend/resources/wasm-playground/rects.html | 6 +- render-wasm/src/render.rs | 96 ++++++++++++++++++- render-wasm/src/render/debug.rs | 11 +++ render-wasm/src/render/surfaces.rs | 45 +++++++-- 4 files changed, 144 insertions(+), 14 deletions(-) diff --git a/frontend/resources/wasm-playground/rects.html b/frontend/resources/wasm-playground/rects.html index e63eb6938..322cadd64 100644 --- a/frontend/resources/wasm-playground/rects.html +++ b/frontend/resources/wasm-playground/rects.html @@ -32,7 +32,7 @@ const canvas = document.getElementById("canvas"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; - const shapes = 1_000; + const shapes = 100; initWasmModule().then(Module => { init(Module); @@ -50,8 +50,8 @@ useShape(uuid); Module._set_parent(0, 0, 0, 0); Module._set_shape_type(3); - const x1 = getRandomInt(0, canvas.width*3); - const y1 = getRandomInt(0, canvas.height*3); + const x1 = getRandomInt(0, canvas.width); + const y1 = getRandomInt(0, canvas.height); const width = getRandomInt(20, 100); const height = getRandomInt(20, 100); Module._set_shape_selrect(x1, y1, x1 + width, y1 + height); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index e3ab497b6..1fbc29473 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -81,6 +81,8 @@ pub(crate) struct RenderState { pub surfaces: Surfaces, 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. @@ -96,6 +98,22 @@ pub(crate) struct RenderState { pub pending_tiles: Vec, } +pub fn get_cache_size(viewbox: Viewbox) -> skia::ISize { + // First we retrieve the extended area of the viewport that we could render. + let (isx, isy, iex, iey) = + tiles::get_tiles_for_viewbox_with_interest(viewbox, VIEWPORT_INTEREST_AREA_THRESHOLD); + + let dx = if isx.signum() != iex.signum() { 1 } else { 0 }; + let dy = if isy.signum() != iey.signum() { 1 } else { 0 }; + + let tile_size = tiles::TILE_SIZE; + ( + ((iex - isx).abs() + dx) * tile_size as i32, + ((iey - isy).abs() + dy) * tile_size as i32, + ) + .into() +} + impl RenderState { pub fn new(width: i32, height: i32) -> RenderState { // This needs to be done once per WebGL context. @@ -122,6 +140,8 @@ impl RenderState { surfaces, fonts, viewbox: Viewbox::new(width as f32, height as f32), + cached_viewbox: Viewbox::new(0., 0.), + cached_target_snapshot: None, images: ImageStore::new(), background_color: skia::Color::TRANSPARENT, render_request_id: None, @@ -172,7 +192,6 @@ impl RenderState { pub fn resize(&mut self, width: i32, height: i32) { let dpr_width = (width as f32 * self.options.dpr()).floor() as i32; let dpr_height = (height as f32 * self.options.dpr()).floor() as i32; - self.surfaces .resize(&mut self.gpu_state, dpr_width, dpr_height); self.viewbox.set_wh(width as f32, height as f32); @@ -191,7 +210,8 @@ impl RenderState { let x = self.current_tile.unwrap().0; let y = self.current_tile.unwrap().1; - self.surfaces.cache_current_tile_texture((x, y)); + let tile_rect = self.get_current_aligned_tile_bounds(); + self.surfaces.cache_current_tile_texture((x, y), tile_rect); self.surfaces .draw_cached_tile_surface(self.current_tile.unwrap(), rect); @@ -432,6 +452,34 @@ impl RenderState { .update_render_context(self.render_area, self.viewbox); } + fn render_from_cache(&mut self) { + if let Some(snapshot) = &self.cached_target_snapshot { + let canvas = self.surfaces.canvas(SurfaceId::Target); + canvas.save(); + + // 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 (start_tile_x, start_tile_y, _, _) = tiles::get_tiles_for_viewbox_with_interest( + self.cached_viewbox, + VIEWPORT_INTEREST_AREA_THRESHOLD, + ); + let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom; + let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom; + + canvas.translate(( + (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x, + (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y, + )); + + canvas.clear(self.background_color); + canvas.draw_image(&snapshot.clone(), (0, 0), Some(&skia::Paint::default())); + canvas.restore(); + } + } + pub fn start_render_loop( &mut self, tree: &mut HashMap, @@ -444,8 +492,13 @@ impl RenderState { wapi::cancel_animation_frame!(frame_id); } } + performance::begin_measure!("render"); performance::begin_measure!("start_render_loop"); + + // If we have cached data let's do a fast render from it + self.render_from_cache(); + let scale = self.get_scale(); self.reset_canvas(); self.surfaces.apply_mut( @@ -465,6 +518,17 @@ impl RenderState { self.viewbox, VIEWPORT_INTEREST_AREA_THRESHOLD, ); + + let viewbox_cache_size = get_cache_size(self.viewbox); + let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox); + if viewbox_cache_size != cached_viewbox_cache_size { + self.surfaces.resize_cache( + &mut self.gpu_state, + viewbox_cache_size, + VIEWPORT_INTEREST_AREA_THRESHOLD, + ); + } + // Then we get the real amount of tiles rendered for the current viewbox. let (sx, sy, ex, ey) = tiles::get_tiles_for_viewbox(self.viewbox); debug::render_debug_tiles_for_viewbox(self, isx, isy, iex, iey); @@ -598,6 +662,29 @@ impl RenderState { ) } + // Returns the bounds of the current tile relative to the viewbox, + // aligned to the nearest tile grid origin. + // + // Unlike `get_current_tile_bounds`, which calculates bounds using the exact + // scaled offset of the viewbox, this method snaps the origin to the nearest + // lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned + // with the global tile grid, which is useful for rendering tiles in a + /// consistent and predictable layout. + pub fn get_current_aligned_tile_bounds(&mut self) -> Rect { + let (tile_x, tile_y) = self.current_tile.unwrap(); + let scale = self.get_scale(); + let start_tile_x = + (self.viewbox.area.left * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; + let start_tile_y = + (self.viewbox.area.top * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE; + Rect::from_xywh( + (tile_x as f32 * tiles::TILE_SIZE) - start_tile_x, + (tile_y as f32 * tiles::TILE_SIZE) - start_tile_y, + tiles::TILE_SIZE, + tiles::TILE_SIZE, + ) + } + pub fn render_shape_tree( &mut self, tree: &mut HashMap, @@ -814,6 +901,11 @@ impl RenderState { } } self.render_in_progress = false; + + // Cache target surface in a texture + self.cached_viewbox = self.viewbox.clone(); + 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/debug.rs b/render-wasm/src/render/debug.rs index 230464139..42962274c 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -30,6 +30,16 @@ fn render_debug_view(render_state: &mut RenderState) { .draw_rect(rect, &paint); } +pub fn render_debug_cache_surface(render_state: &mut RenderState) { + let canvas = render_state.surfaces.canvas(SurfaceId::Debug); + canvas.save(); + canvas.scale((0.1, 0.1)); + render_state + .surfaces + .draw_into(SurfaceId::Cache, SurfaceId::Debug, None); + render_state.surfaces.canvas(SurfaceId::Debug).restore(); +} + pub fn render_wasm_label(render_state: &mut RenderState) { let canvas = render_state.surfaces.canvas(SurfaceId::Debug); let skia::ISize { width, height } = canvas.base_layer_size(); @@ -164,6 +174,7 @@ pub fn render(render_state: &mut RenderState) { render_debug_view(render_state); render_debug_viewbox_tiles(render_state); render_debug_tiles(render_state); + render_debug_cache_surface(render_state); render_state.surfaces.draw_into( SurfaceId::Debug, SurfaceId::Target, diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 09fad9ef9..383c0378d 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -2,7 +2,7 @@ use crate::shapes::Shape; use crate::view::Viewbox; use skia_safe::{self as skia, IRect, Paint, RRect}; -use super::{gpu_state::GpuState, tiles::Tile}; +use super::{gpu_state::GpuState, tiles::Tile, tiles::TILE_SIZE}; use base64::{engine::general_purpose, Engine as _}; use std::collections::HashMap; @@ -16,6 +16,7 @@ const TILE_SIZE_MULTIPLIER: i32 = 2; #[derive(Debug, PartialEq, Clone, Copy)] pub enum SurfaceId { Target, + Cache, Current, Fills, Strokes, @@ -27,6 +28,7 @@ pub enum SurfaceId { pub struct Surfaces { // is the final destination surface, the one that it is represented in the canvas element. target: skia::Surface, + cache: skia::Surface, // keeps the current render current: skia::Surface, // keeps the current shape's fills @@ -60,7 +62,7 @@ impl Surfaces { let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4); let target = gpu_state.create_target_surface(width, height); - + let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height); let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims); let drop_shadows = gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims); @@ -75,6 +77,7 @@ impl Surfaces { let tiles = TileTextureCache::new(); Surfaces { target, + cache, current, drop_shadows, inner_shadows, @@ -91,6 +94,11 @@ impl Surfaces { self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)); } + pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image { + let surface = self.get_mut(id); + surface.image_snapshot() + } + pub fn base64_snapshot(&mut self, id: SurfaceId) -> String { let surface = self.get_mut(id); let image = surface.image_snapshot(); @@ -160,6 +168,7 @@ impl Surfaces { fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface { match id { SurfaceId::Target => &mut self.target, + SurfaceId::Cache => &mut self.cache, SurfaceId::Current => &mut self.current, SurfaceId::DropShadows => &mut self.drop_shadows, SurfaceId::InnerShadows => &mut self.inner_shadows, @@ -176,6 +185,20 @@ impl Surfaces { // The rest are tile size surfaces } + pub fn resize_cache( + &mut self, + gpu_state: &mut GpuState, + cache_dims: skia::ISize, + interest_area_threshold: i32, + ) { + self.cache = gpu_state.create_surface_with_isize("cache".to_string(), cache_dims); + self.cache.canvas().reset_matrix(); + self.cache.canvas().translate(( + (interest_area_threshold as f32 * TILE_SIZE), + (interest_area_threshold as f32 * TILE_SIZE), + )); + } + pub fn draw_rect_to(&mut self, id: SurfaceId, shape: &Shape, paint: &Paint) { if let Some(corners) = shape.shape_type.corners() { let rrect = RRect::new_rect_radii(shape.selrect, &corners); @@ -227,18 +250,22 @@ impl Surfaces { self.tiles.visit(tile); } - pub fn cache_current_tile_texture(&mut self, tile: Tile) { - let snapshot = self.current.image_snapshot(); + pub fn cache_current_tile_texture(&mut self, tile: Tile, tile_rect: skia::Rect) { let rect = IRect::from_xywh( self.margins.width, self.margins.height, - snapshot.width() - TILE_SIZE_MULTIPLIER * self.margins.width, - snapshot.height() - TILE_SIZE_MULTIPLIER * self.margins.height, + self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width, + self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height, ); - let mut context = self.current.direct_context(); - if let Some(snapshot) = snapshot.make_subset(&mut context, &rect) { - self.tiles.add(tile, snapshot); + if let Some(snapshot) = self.current.image_snapshot_with_bounds(&rect) { + self.tiles.add(tile, snapshot.clone()); + self.cache.canvas().draw_image_rect( + &snapshot.clone(), + None, + tile_rect, + &skia::Paint::default(), + ); } }