Merge pull request #6370 from penpot/superalex-improve-zoom-in-zoom-out-performance-2

🎉 Improve zoom in/out performance
This commit is contained in:
Aitor Moreno 2025-05-12 11:22:57 +02:00 committed by GitHub
commit 9c24d3a521
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 144 additions and 14 deletions

View file

@ -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);

View file

@ -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<skia::Image>,
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<tiles::TileWithDistance>,
}
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<Uuid, &mut Shape>,
@ -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<Uuid, &mut Shape>,
@ -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);
}

View file

@ -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,

View file

@ -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(),
);
}
}