mirror of
https://github.com/penpot/penpot.git
synced 2026-09-10 05:58:47 +00:00
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:
commit
0499cd6162
@ -952,14 +952,14 @@
|
|||||||
(= result 1)))
|
(= result 1)))
|
||||||
|
|
||||||
(def render-finish
|
(def render-finish
|
||||||
(letfn [(do-render [ts]
|
(letfn [(do-render []
|
||||||
;; Check if context is still initialized before executing
|
;; Check if context is still initialized before executing
|
||||||
;; to prevent errors when navigating quickly
|
;; to prevent errors when navigating quickly
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(perf/begin-measure "render-finish")
|
(perf/begin-measure "render-finish")
|
||||||
(h/call wasm/internal-module "_set_view_end")
|
(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)))
|
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
|
||||||
|
|
||||||
(def render-pan
|
(def render-pan
|
||||||
|
|||||||
@ -295,43 +295,33 @@ pub extern "C" fn set_view_start() -> Result<()> {
|
|||||||
Ok(())
|
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]
|
#[no_mangle]
|
||||||
#[wasm_error]
|
#[wasm_error]
|
||||||
pub extern "C" fn set_view_end() -> Result<()> {
|
pub extern "C" fn set_view_end() -> Result<()> {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
let _end_start = performance::begin_timed_log!("set_view_end");
|
|
||||||
performance::begin_measure!("set_view_end");
|
performance::begin_measure!("set_view_end");
|
||||||
state.render_state.options.set_fast_mode(false);
|
state.render_state.options.set_fast_mode(false);
|
||||||
state.render_state.cancel_animation_frame();
|
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();
|
let scale = state.render_state.get_scale();
|
||||||
state
|
state
|
||||||
.render_state
|
.render_state
|
||||||
.tile_viewbox
|
.tile_viewbox
|
||||||
.update(state.render_state.viewbox, scale);
|
.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() {
|
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||||
state.rebuild_tiles();
|
state.rebuild_tiles();
|
||||||
} else {
|
} 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();
|
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_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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2675,24 +2675,20 @@ impl RenderState {
|
|||||||
self.surfaces.remove_cached_tile_surface(tile);
|
self.surfaces.remove_cached_tile_surface(tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
/// Rebuild the tile index (shape→tile mapping) for all top-level shapes.
|
||||||
performance::begin_measure!("rebuild_tiles_shallow");
|
/// This does NOT invalidate the tile texture cache — cached tile images
|
||||||
|
/// survive so that fast-mode renders during pan still show shadows/blur.
|
||||||
// Check if zoom changed - if so, we need full cache invalidation
|
pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) {
|
||||||
// because tiles are rendered at specific zoom levels
|
|
||||||
let zoom_changed = self.zoom_changed();
|
let zoom_changed = self.zoom_changed();
|
||||||
|
|
||||||
let mut tiles_to_invalidate = HashSet::<tiles::Tile>::new();
|
|
||||||
let mut nodes = vec![Uuid::nil()];
|
let mut nodes = vec![Uuid::nil()];
|
||||||
while let Some(shape_id) = nodes.pop() {
|
while let Some(shape_id) = nodes.pop() {
|
||||||
if let Some(shape) = tree.get(&shape_id) {
|
if let Some(shape) = tree.get(&shape_id) {
|
||||||
if shape_id != Uuid::nil() {
|
if shape_id != Uuid::nil() {
|
||||||
if zoom_changed {
|
if zoom_changed {
|
||||||
// Zoom changed: use full update that tracks all affected tiles
|
let _ = self.update_shape_tiles(shape, tree);
|
||||||
tiles_to_invalidate.extend(self.update_shape_tiles(shape, tree));
|
|
||||||
} else {
|
} else {
|
||||||
// Pan only: use incremental update that preserves valid cached tiles
|
let _ = self.update_shape_tiles_incremental(shape, tree);
|
||||||
self.update_shape_tiles_incremental(shape, tree);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// We only need to rebuild tiles from the first level.
|
// 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
|
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
||||||
self.surfaces.remove_cached_tiles(self.background_color);
|
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");
|
performance::end_measure!("rebuild_tiles_shallow");
|
||||||
}
|
}
|
||||||
@ -2826,10 +2830,6 @@ impl RenderState {
|
|||||||
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
|
(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) {
|
pub fn mark_touched(&mut self, uuid: Uuid) {
|
||||||
self.touched_ids.insert(uuid);
|
self.touched_ids.insert(uuid);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
self.cache.canvas().clear(color);
|
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) {
|
pub fn gc(&mut self) {
|
||||||
self.tiles.gc();
|
self.tiles.gc();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -102,14 +102,14 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
||||||
// If zoom changed, we MUST rebuild the tile index before using it.
|
// If zoom changed (e.g. interrupted zoom render followed by pan), the
|
||||||
// Otherwise, the index will have tiles from the old zoom level, causing visible
|
// tile index may be stale for the new viewport position. Rebuild the
|
||||||
// tiles to appear empty. This can happen if start_render_loop() is called before
|
// index so shapes are mapped to the correct tiles. We use
|
||||||
// set_view_end() finishes rebuilding the index, or if set_view_end() hasn't been
|
// rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile
|
||||||
// called yet.
|
// texture cache — otherwise cached tiles with shadows/blur would be
|
||||||
let zoom_changed = self.render_state.zoom_changed();
|
// cleared and re-rendered in fast mode without effects.
|
||||||
if zoom_changed {
|
if self.render_state.zoom_changed() {
|
||||||
self.rebuild_tiles_shallow();
|
self.render_state.rebuild_tile_index(&self.shapes);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.render_state
|
self.render_state
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user