mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🔧 Improve zoom and pan performance
This commit is contained in:
parent
47eadab82e
commit
6b3d5d930f
@ -1148,23 +1148,21 @@
|
||||
[shapes start-index thumbnails-acc full-acc]
|
||||
(let [total (count shapes)
|
||||
deadline (+ (js/performance.now) CHUNK_TIME_BUDGET_MS)]
|
||||
(let [result
|
||||
(loop [index start-index
|
||||
t-acc (transient thumbnails-acc)
|
||||
f-acc (transient full-acc)]
|
||||
(if (and (< index total)
|
||||
;; Check performance.now every 8 shapes to reduce overhead
|
||||
(or (pos? (bit-and (- index start-index) 7))
|
||||
(<= (js/performance.now) deadline)))
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(reduce conj! t-acc thumbnails)
|
||||
(reduce conj! f-acc full)))
|
||||
{:thumbnails (persistent! t-acc)
|
||||
:full (persistent! f-acc)
|
||||
:next-index index}))]
|
||||
result)))
|
||||
(loop [index start-index
|
||||
t-acc (transient thumbnails-acc)
|
||||
f-acc (transient full-acc)]
|
||||
(if (and (< index total)
|
||||
;; Check performance.now every 8 shapes to reduce overhead
|
||||
(or (pos? (bit-and (- index start-index) 7))
|
||||
(<= (js/performance.now) deadline)))
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(reduce conj! t-acc thumbnails)
|
||||
(reduce conj! f-acc full)))
|
||||
{:thumbnails (persistent! t-acc)
|
||||
:full (persistent! f-acc)
|
||||
:next-index index}))))
|
||||
|
||||
(defn- set-objects-async
|
||||
"Asynchronously process shapes in time-budgeted chunks, yielding to the
|
||||
@ -1194,6 +1192,10 @@
|
||||
(h/call wasm/internal-module "_end_loading")
|
||||
(end-shapes-loading!)
|
||||
|
||||
;; Rebuild the tile index so _render knows which shapes
|
||||
;; map to which tiles after a page switch.
|
||||
(h/call wasm/internal-module "_set_view_end")
|
||||
|
||||
;; Text layouts must run after _end_loading (they
|
||||
;; depend on state that is only correct when loading
|
||||
;; is false). Each call touch_shape → touched_ids.
|
||||
@ -1247,6 +1249,9 @@
|
||||
{:thumbnails (persistent! thumbnails-acc) :full (persistent! full-acc)}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(when on-shapes-ready (on-shapes-ready))
|
||||
;; Rebuild the tile index so _render knows which shapes
|
||||
;; map to which tiles after a page switch.
|
||||
(h/call wasm/internal-module "_set_view_end")
|
||||
(process-pending shapes thumbnails full
|
||||
(fn []
|
||||
(if render-callback
|
||||
|
||||
@ -220,7 +220,13 @@ pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.render_state.cancel_animation_frame();
|
||||
// Don't cancel the animation frame — let the async render
|
||||
// continue populating the tile HashMap in the background.
|
||||
// process_animation_frame skips flush_and_submit in fast
|
||||
// mode so it won't present stale Target content. The
|
||||
// tile HashMap is position-independent, so tiles rendered
|
||||
// for the old viewport can be reused by the next full
|
||||
// render at the new viewport position.
|
||||
state.render_from_cache();
|
||||
});
|
||||
Ok(())
|
||||
|
||||
@ -40,6 +40,7 @@ pub use images::*;
|
||||
// This is the extra area used for tile rendering (tiles beyond viewport).
|
||||
// Higher values pre-render more tiles, reducing empty squares during pan but using more memory.
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
@ -344,11 +345,9 @@ pub(crate) struct RenderState {
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
// First we retrieve the extended area of the viewport that we could render.
|
||||
let TileRect(isx, isy, iex, iey) = tiles::get_tiles_for_viewbox_with_interest(
|
||||
viewbox,
|
||||
VIEWPORT_INTEREST_AREA_THRESHOLD,
|
||||
scale,
|
||||
);
|
||||
let interest = VIEWPORT_INTEREST_AREA_THRESHOLD;
|
||||
let TileRect(isx, isy, iex, iey) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(viewbox, interest, scale);
|
||||
|
||||
let dx = if isx.signum() != iex.signum() { 1 } else { 0 };
|
||||
let dy = if isy.signum() != iey.signum() { 1 } else { 0 };
|
||||
@ -704,20 +703,24 @@ impl RenderState {
|
||||
}
|
||||
|
||||
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> {
|
||||
let fast_mode = self.options.is_fast_mode();
|
||||
// Decide *now* (at the first real cache blit) whether we need to clear Cache.
|
||||
// This avoids clearing Cache on renders that don't actually paint tiles (e.g. hover/UI),
|
||||
// while still preventing stale pixels from surviving across full-quality renders.
|
||||
if !self.options.is_fast_mode() && !self.cache_cleared_this_render {
|
||||
if !fast_mode && !self.cache_cleared_this_render {
|
||||
self.surfaces.clear_cache(self.background_color);
|
||||
self.cache_cleared_this_render = true;
|
||||
}
|
||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
||||
// positions would be wrong — only save to the tile HashMap.
|
||||
self.surfaces.cache_current_tile_texture(
|
||||
&self.tile_viewbox,
|
||||
&self
|
||||
.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
||||
&tile_rect,
|
||||
fast_mode,
|
||||
);
|
||||
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
@ -1454,18 +1457,19 @@ impl RenderState {
|
||||
pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) {
|
||||
let _start = performance::begin_timed_log!("render_from_cache");
|
||||
performance::begin_measure!("render_from_cache");
|
||||
let scale = self.get_cached_scale();
|
||||
let cached_scale = self.get_cached_scale();
|
||||
|
||||
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
||||
if self.cached_viewbox.area.width() > 0.0 {
|
||||
// Scale and translate the target according to the cached data
|
||||
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
|
||||
|
||||
let interest = VIEWPORT_INTEREST_AREA_THRESHOLD;
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(
|
||||
self.cached_viewbox,
|
||||
VIEWPORT_INTEREST_AREA_THRESHOLD,
|
||||
scale,
|
||||
interest,
|
||||
cached_scale,
|
||||
);
|
||||
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr();
|
||||
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
|
||||
@ -1488,6 +1492,36 @@ impl RenderState {
|
||||
// Restore canvas state
|
||||
self.surfaces.canvas(SurfaceId::Target).restore();
|
||||
|
||||
// During pure pan (same zoom), draw tiles from the HashMap
|
||||
// on top of the scaled Cache surface. Cached tile textures
|
||||
// include full-quality effects (shadows, blur) from the last
|
||||
// render, so blitting them avoids re-rendering and keeps pan
|
||||
// smooth. During zoom the tile grid changes so HashMap tiles
|
||||
// would be at wrong positions — skip them and let the full
|
||||
// render after set_view_end handle it.
|
||||
if !self.zoom_changed() {
|
||||
let current_scale = self.get_scale();
|
||||
let visible_rect = tiles::get_tiles_for_viewbox(self.viewbox, current_scale);
|
||||
let vb_offset_x = self.viewbox.area.left * current_scale;
|
||||
let vb_offset_y = self.viewbox.area.top * current_scale;
|
||||
|
||||
for tx in visible_rect.x1()..=visible_rect.x2() {
|
||||
for ty in visible_rect.y1()..=visible_rect.y2() {
|
||||
let tile = tiles::Tile::from(tx, ty);
|
||||
if self.surfaces.has_cached_tile_surface(tile) {
|
||||
let tile_rect = skia::Rect::from_xywh(
|
||||
tx as f32 * tiles::TILE_SIZE - vb_offset_x,
|
||||
ty as f32 * tiles::TILE_SIZE - vb_offset_y,
|
||||
tiles::TILE_SIZE,
|
||||
tiles::TILE_SIZE,
|
||||
);
|
||||
self.surfaces
|
||||
.draw_cached_tile_surface(tile, tile_rect, bg_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
}
|
||||
@ -1622,7 +1656,15 @@ impl RenderState {
|
||||
if tree.len() != 0 {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
|
||||
}
|
||||
self.flush_and_submit();
|
||||
|
||||
// In fast mode (pan/zoom in progress), render_from_cache owns
|
||||
// the Target surface — skip flush so we don't present stale
|
||||
// tile positions. The rAF still populates the Cache surface
|
||||
// and tile HashMap so render_from_cache progressively shows
|
||||
// more complete content.
|
||||
if !self.options.is_fast_mode() {
|
||||
self.flush_and_submit();
|
||||
}
|
||||
|
||||
if self.render_in_progress {
|
||||
self.cancel_animation_frame();
|
||||
@ -1668,7 +1710,10 @@ impl RenderState {
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
if tree.len() != 0 {
|
||||
let shape = tree.get(id).unwrap();
|
||||
let Some(shape) = tree.get(id) else {
|
||||
// FIXME
|
||||
return Ok((Vec::new(), 0, 0));
|
||||
};
|
||||
let mut extrect = shape.extrect(tree, scale);
|
||||
self.export_context = Some((extrect, scale));
|
||||
let margins = self.surfaces.margins;
|
||||
@ -2736,8 +2781,17 @@ impl RenderState {
|
||||
|
||||
self.surfaces.gc();
|
||||
|
||||
// Mark cache as valid for render_from_cache
|
||||
self.cached_viewbox = self.viewbox;
|
||||
// Mark cache as valid for render_from_cache.
|
||||
// Only update for full-quality renders (non-fast mode).
|
||||
// An async render can complete while fast mode is active
|
||||
// (e.g. interest-area tiles finish during a pan gesture).
|
||||
// Those tiles lack effects (shadows, blur). Updating
|
||||
// cached_viewbox here would make zoom_changed() return false,
|
||||
// so set_view_end would skip tile invalidation and the next
|
||||
// full render would reuse the low-quality tiles.
|
||||
if !self.options.is_fast_mode() {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
|
||||
@ -549,6 +549,7 @@ impl Surfaces {
|
||||
tile_viewbox: &TileViewbox,
|
||||
tile: &Tile,
|
||||
tile_rect: &skia::Rect,
|
||||
skip_cache_surface: bool,
|
||||
) {
|
||||
let rect = IRect::from_xywh(
|
||||
self.margins.width,
|
||||
@ -560,13 +561,15 @@ impl Surfaces {
|
||||
let tile_image_opt = self.current.image_snapshot_with_bounds(rect);
|
||||
|
||||
if let Some(tile_image) = tile_image_opt {
|
||||
// Draw to cache first (takes reference), then move to tile cache
|
||||
self.cache.canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
None,
|
||||
tile_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
if !skip_cache_surface {
|
||||
// Draw to cache surface for render_from_cache
|
||||
self.cache.canvas().draw_image_rect(
|
||||
&tile_image,
|
||||
None,
|
||||
tile_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
|
||||
self.tiles.add(tile_viewbox, tile, tile_image);
|
||||
}
|
||||
@ -610,9 +613,12 @@ impl Surfaces {
|
||||
let mut bg = skia::Paint::default();
|
||||
bg.set_color(color);
|
||||
self.cache.canvas().draw_rect(aligned_rect, &bg);
|
||||
self.cache
|
||||
.canvas()
|
||||
.draw_image_rect(&image, None, aligned_rect, &skia::Paint::default());
|
||||
self.cache.canvas().draw_image_rect(
|
||||
&image,
|
||||
None,
|
||||
aligned_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user