🐛 Fix performance regression

* 🔧 Preserve atlas on zoom interaction 

Co-authored-by: Elena Torro <elenatorro@gmail.com>
This commit is contained in:
Alejandro Alonso 2026-06-17 14:52:12 +02:00 committed by GitHub
parent bdc9b092c5
commit 61cd9fe886
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 71 additions and 3 deletions

View File

@ -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) {

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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")
};