mirror of
https://github.com/penpot/penpot.git
synced 2026-06-20 14:22:08 +00:00
🐛 Fix performance regression
* 🔧 Preserve atlas on zoom interaction
Co-authored-by: Elena Torro <elenatorro@gmail.com>
This commit is contained in:
parent
bdc9b092c5
commit
61cd9fe886
@ -401,6 +401,11 @@ pub(crate) struct RenderState {
|
||||
/// interactive backdrop exactly once per gesture (first rAF) so we don't
|
||||
/// repeatedly overwrite tiles that have already been updated.
|
||||
pub interactive_target_seeded: bool,
|
||||
/// When true, the next `start_render_loop` keeps the last presented `Target`
|
||||
/// pixels instead of clearing the canvas. Set after incremental shape updates
|
||||
/// (e.g. adding a rect) so the workspace stays visible while only affected
|
||||
/// tiles are re-rendered asynchronously.
|
||||
pub preserve_target_during_render: bool,
|
||||
/// GPU crops from `Backbuffer` or tile atlas keyed by shape id. Filled on full-frame completion; during
|
||||
/// drag, entries for the moved top-level selection are ensured here
|
||||
pub backbuffer_crop_cache: HashMap<Uuid, InteractiveDragCrop>,
|
||||
@ -584,6 +589,7 @@ impl RenderState {
|
||||
cache_cleared_this_render: false,
|
||||
current_tile_had_shapes: false,
|
||||
interactive_target_seeded: false,
|
||||
preserve_target_during_render: false,
|
||||
backbuffer_crop_cache: HashMap::default(),
|
||||
})
|
||||
}
|
||||
@ -1994,6 +2000,24 @@ impl RenderState {
|
||||
self.surfaces
|
||||
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
|
||||
|
||||
// For pure pan (same zoom), overlay any cached per-tile textures on top of the atlas.
|
||||
// This reduces the "rubbery"/distorted look of atlas-only previews while keeping
|
||||
// fast-mode responsive. For zoom, the tile grid changes so cached tiles would be
|
||||
// mispositioned — skip them.
|
||||
if !self.zoom_changed() {
|
||||
let visible_rect = tiles::get_tiles_for_viewbox(&self.viewbox);
|
||||
let offset = self.viewbox.get_offset();
|
||||
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 rect = tile.get_rect_with_offset(&offset);
|
||||
self.surfaces.draw_cached_tile_into_backbuffer(tile, &rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
@ -2169,6 +2193,9 @@ impl RenderState {
|
||||
self.surfaces.atlas.set_doc_bounds(doc_bounds);
|
||||
|
||||
self.cache_cleared_this_render = false;
|
||||
let preserve_target = self.preserve_target_during_render;
|
||||
self.preserve_target_during_render = false;
|
||||
|
||||
if self.options.is_interactive_transform() {
|
||||
// Keep `Target` as the previous frame and overwrite only the tiles
|
||||
// that changed. This avoids clearing + redrawing an atlas backdrop
|
||||
@ -2180,6 +2207,15 @@ impl RenderState {
|
||||
// fast_mode skips cache updates and regardless of atlas coverage.
|
||||
self.interactive_target_seeded = true;
|
||||
}
|
||||
} else if preserve_target || self.zoom_changed() {
|
||||
// Shape updates or zoom-end: keep the last presented frame on screen
|
||||
// while tiles are re-rendered asynchronously. During zoom the
|
||||
// preview from render_from_cache stays visible until the full-
|
||||
// quality pass completes.
|
||||
self.surfaces
|
||||
.reset_interactive_transform(self.background_color);
|
||||
self.surfaces.seed_backbuffer_from_target();
|
||||
self.interactive_target_seeded = false;
|
||||
} else {
|
||||
self.reset_canvas();
|
||||
self.interactive_target_seeded = false;
|
||||
@ -3941,6 +3977,7 @@ impl RenderState {
|
||||
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||
|
||||
let ids = std::mem::take(&mut self.touched_ids);
|
||||
self.preserve_target_during_render = !ids.is_empty();
|
||||
|
||||
for shape_id in ids.iter() {
|
||||
if let Some(shape) = tree.get(shape_id) {
|
||||
|
||||
@ -9,7 +9,7 @@ const SHOW_WASM_INFO: u32 = 0x08;
|
||||
// Higher values pre-render more tiles, reducing empty squares during pan but using more memory.
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 1;
|
||||
const MIN_DPR_VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 2;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 8;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
const ANTIALIAS_THRESHOLD: f32 = 7.0;
|
||||
|
||||
@ -424,6 +424,7 @@ pub struct Surfaces {
|
||||
backbuffer: skia::Surface,
|
||||
// Atlas used to keep tiles.
|
||||
tile_atlas: skia::Surface,
|
||||
tile_atlas_image: Option<skia::Image>,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
pub atlas: DocAtlas,
|
||||
@ -500,6 +501,7 @@ impl Surfaces {
|
||||
export,
|
||||
backbuffer,
|
||||
tile_atlas,
|
||||
tile_atlas_image: None,
|
||||
tiles,
|
||||
atlas,
|
||||
sampling_options,
|
||||
@ -529,11 +531,17 @@ impl Surfaces {
|
||||
background: skia::Color,
|
||||
) {
|
||||
self.tiles.update(viewbox, tile_viewbox);
|
||||
let atlas_image = self.tile_atlas.image_snapshot();
|
||||
if self.tiles.needs_snapshot() || self.tile_atlas_image.is_none() {
|
||||
self.tile_atlas_image = Some(self.tile_atlas.image_snapshot());
|
||||
self.tiles.snapshot();
|
||||
}
|
||||
let Some(atlas_image) = self.tile_atlas_image.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let canvas = self.backbuffer.canvas();
|
||||
canvas.clear(background);
|
||||
canvas.draw_atlas(
|
||||
&atlas_image,
|
||||
atlas_image,
|
||||
&self.tiles.transforms,
|
||||
&self.tiles.textures,
|
||||
None,
|
||||
@ -1403,6 +1411,7 @@ impl Surfaces {
|
||||
pub fn invalidate_tile_cache(&mut self) {
|
||||
self.tiles.clear();
|
||||
self.atlas.tile_doc_rects.clear();
|
||||
self.tile_atlas_image = None;
|
||||
}
|
||||
|
||||
pub fn gc(&mut self) {
|
||||
@ -1532,6 +1541,7 @@ impl TileAtlasTextureProvider {
|
||||
|
||||
pub struct TileTextureCache {
|
||||
tile_size: f32,
|
||||
is_updated: bool,
|
||||
provider: TileAtlasTextureProvider,
|
||||
transforms: Vec<skia::RSXform>,
|
||||
textures: Vec<skia::Rect>,
|
||||
@ -1543,6 +1553,7 @@ impl TileTextureCache {
|
||||
pub fn new(texture_size: i32, capacity: usize) -> Self {
|
||||
Self {
|
||||
tile_size: tiles::TILE_SIZE,
|
||||
is_updated: false,
|
||||
provider: TileAtlasTextureProvider::new(texture_size, TILE_SIZE),
|
||||
transforms: Vec::with_capacity(capacity),
|
||||
textures: Vec::with_capacity(capacity),
|
||||
@ -1560,6 +1571,14 @@ impl TileTextureCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_snapshot(&self) -> bool {
|
||||
self.is_updated
|
||||
}
|
||||
|
||||
pub fn snapshot(&mut self) {
|
||||
self.is_updated = false;
|
||||
}
|
||||
|
||||
fn gc_non_visible(&mut self, tile_viewbox: &TileViewbox) {
|
||||
let marked: Vec<_> = self
|
||||
.grid
|
||||
@ -1610,6 +1629,10 @@ impl TileTextureCache {
|
||||
continue;
|
||||
};
|
||||
|
||||
if self.removed.contains(&tile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.transforms[index].tx = x as f32 * self.tile_size - offset.x;
|
||||
self.transforms[index].ty = y as f32 * self.tile_size - offset.y;
|
||||
|
||||
@ -1652,6 +1675,7 @@ impl TileTextureCache {
|
||||
self.removed.remove(tile);
|
||||
}
|
||||
|
||||
self.is_updated = true;
|
||||
tile_ref.clone()
|
||||
}
|
||||
|
||||
@ -1668,6 +1692,7 @@ impl TileTextureCache {
|
||||
self.textures[tile_ref.index].set_empty();
|
||||
}
|
||||
}
|
||||
self.is_updated = true;
|
||||
self.removed.insert(tile);
|
||||
}
|
||||
|
||||
@ -1675,5 +1700,6 @@ impl TileTextureCache {
|
||||
for k in self.grid.keys() {
|
||||
self.removed.insert(*k);
|
||||
}
|
||||
self.is_updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,6 +214,11 @@ impl State {
|
||||
/// invalidated and recalculated to include the new child. This ensures that frames
|
||||
/// and groups properly encompass their children.
|
||||
pub fn set_parent_for_current_shape(&mut self, id: Uuid) {
|
||||
// Reparent preview during drag is handled by structure modifiers only.
|
||||
if get_render_state().options.is_interactive_transform() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(shape) = self.current_shape_mut() else {
|
||||
panic!("Invalid current shape")
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user