Merge pull request #8654 from penpot/elenatorro-13282-perf-tiles

🔧 Preserve cache canvas during tile rebuild for smooth zoom preview
This commit is contained in:
Alejandro Alonso
2026-03-19 15:20:08 +01:00
committed by GitHub
5 changed files with 44 additions and 43 deletions

View File

@@ -952,14 +952,14 @@
(= result 1)))
(def render-finish
(letfn [(do-render [ts]
(letfn [(do-render []
;; Check if context is still initialized before executing
;; to prevent errors when navigating quickly
(when wasm/context-initialized?
(perf/begin-measure "render-finish")
(h/call wasm/internal-module "_set_view_end")
(render ts)
(perf/end-measure "render-finish")))]
(perf/end-measure "render-finish")
(render (js/performance.now))))]
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
(def render-pan

View File

@@ -295,43 +295,33 @@ pub extern "C" fn set_view_start() -> Result<()> {
Ok(())
}
/// Finishes a view interaction (zoom or pan). Rebuilds the tile index
/// and invalidates the tile texture cache so the subsequent render
/// re-draws all tiles at full quality (fast_mode is off at this point).
#[no_mangle]
#[wasm_error]
pub extern "C" fn set_view_end() -> Result<()> {
with_state_mut!(state, {
let _end_start = performance::begin_timed_log!("set_view_end");
performance::begin_measure!("set_view_end");
state.render_state.options.set_fast_mode(false);
state.render_state.cancel_animation_frame();
// Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area
// This is critical because we limit tiles to the interest area for optimization
let scale = state.render_state.get_scale();
state
.render_state
.tile_viewbox
.update(state.render_state.viewbox, scale);
// We rebuild the tile index on both pan and zoom because `get_tiles_for_shape`
// clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent).
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
performance::begin_measure!("set_view_end::rebuild_tiles");
if state.render_state.options.is_profile_rebuild_tiles() {
state.rebuild_tiles();
} else {
// Rebuild tile index + invalidate tile texture cache.
// Cache canvas is preserved so render_from_cache can still
// show a scaled preview during zoom.
state.rebuild_tiles_shallow();
}
performance::end_measure!("set_view_end::rebuild_tiles");
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
state.render_state.sync_cached_viewbox();
performance::end_measure!("set_view_end");
performance::end_timed_log!("set_view_end", _end_start);
#[cfg(feature = "profile-macros")]
{
let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START };
performance::console_log!("[PERF] view_interaction: {}ms", total_time);
}
});
Ok(())
}

View File

@@ -2675,24 +2675,20 @@ impl RenderState {
self.surfaces.remove_cached_tile_surface(tile);
}
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
performance::begin_measure!("rebuild_tiles_shallow");
// Check if zoom changed - if so, we need full cache invalidation
// because tiles are rendered at specific zoom levels
/// Rebuild the tile index (shape→tile mapping) for all top-level shapes.
/// This does NOT invalidate the tile texture cache — cached tile images
/// survive so that fast-mode renders during pan still show shadows/blur.
pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) {
let zoom_changed = self.zoom_changed();
let mut tiles_to_invalidate = HashSet::<tiles::Tile>::new();
let mut nodes = vec![Uuid::nil()];
while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) {
if shape_id != Uuid::nil() {
if zoom_changed {
// Zoom changed: use full update that tracks all affected tiles
tiles_to_invalidate.extend(self.update_shape_tiles(shape, tree));
let _ = self.update_shape_tiles(shape, tree);
} else {
// Pan only: use incremental update that preserves valid cached tiles
self.update_shape_tiles_incremental(shape, tree);
let _ = self.update_shape_tiles_incremental(shape, tree);
}
} else {
// We only need to rebuild tiles from the first level.
@@ -2702,9 +2698,17 @@ impl RenderState {
}
}
}
}
// Invalidate changed tiles - old content stays visible until new tiles render
self.surfaces.remove_cached_tiles(self.background_color);
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
performance::begin_measure!("rebuild_tiles_shallow");
self.rebuild_tile_index(tree);
// Invalidate the tile texture cache so all tiles are re-rendered, but
// preserve the cache canvas so render_from_cache can still show a scaled
// preview of old content while new tiles load progressively.
self.surfaces.invalidate_tile_cache();
performance::end_measure!("rebuild_tiles_shallow");
}
@@ -2826,10 +2830,6 @@ impl RenderState {
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
}
pub fn sync_cached_viewbox(&mut self) {
self.cached_viewbox = self.viewbox;
}
pub fn mark_touched(&mut self, uuid: Uuid) {
self.touched_ids.insert(uuid);
}

View File

@@ -608,11 +608,22 @@ impl Surfaces {
);
}
/// Full cache reset: clears both the tile texture cache and the cache canvas.
/// Used by `rebuild_tiles` (full rebuild). For shallow rebuilds that preserve
/// the cache canvas for scaled previews, use `invalidate_tile_cache` instead.
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
self.tiles.clear();
self.cache.canvas().clear(color);
}
/// Invalidate the tile texture cache without clearing the cache canvas.
/// This forces all tiles to be re-rendered, but preserves the cache canvas
/// so that `render_from_cache` can still show a scaled preview of the old
/// content while new tiles are being rendered.
pub fn invalidate_tile_cache(&mut self) {
self.tiles.clear();
}
pub fn gc(&mut self) {
self.tiles.gc();
}

View File

@@ -102,14 +102,14 @@ impl State {
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
// If zoom changed, we MUST rebuild the tile index before using it.
// Otherwise, the index will have tiles from the old zoom level, causing visible
// tiles to appear empty. This can happen if start_render_loop() is called before
// set_view_end() finishes rebuilding the index, or if set_view_end() hasn't been
// called yet.
let zoom_changed = self.render_state.zoom_changed();
if zoom_changed {
self.rebuild_tiles_shallow();
// If zoom changed (e.g. interrupted zoom render followed by pan), the
// tile index may be stale for the new viewport position. Rebuild the
// index so shapes are mapped to the correct tiles. We use
// rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile
// texture cache — otherwise cached tiles with shadows/blur would be
// cleared and re-rendered in fast mode without effects.
if self.render_state.zoom_changed() {
self.render_state.rebuild_tile_index(&self.shapes);
}
self.render_state