🎉 Improve zoom in/out performance

This commit is contained in:
Alejandro Alonso 2025-04-25 12:22:01 +02:00
parent e0e381bdfc
commit 480e0887e3
4 changed files with 144 additions and 14 deletions

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