From de04896266362522ff24fb7b1ab7f61df3ae7c47 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Mon, 16 Mar 2026 13:05:05 +0100 Subject: [PATCH] :wrench: Preserve cache canvas during tile rebuild for smooth zoom preview --- frontend/src/app/render_wasm/api.cljs | 6 ++--- render-wasm/src/main.rs | 22 +++++------------- render-wasm/src/render.rs | 32 +++++++++++++-------------- render-wasm/src/render/surfaces.rs | 11 +++++++++ render-wasm/src/state.rs | 16 +++++++------- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index e4d097cc06..91097d6e66 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -951,14 +951,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 diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 017540a11b..bdb7a6ba88 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -296,43 +296,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(()) } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index bb0427ac78..ed117bcb60 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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::::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); } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 857ccfda6d..7337409923 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -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(); } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 3509b529aa..a976ef331f 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -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