mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 19:28:12 +00:00
refresh 100% tiles after zoom
This commit is contained in:
parent
55a70e7b88
commit
8ba7b69173
@ -336,6 +336,12 @@ pub(crate) struct RenderState {
|
||||
pub show_grid: Option<Uuid>,
|
||||
pub focus_mode: FocusMode,
|
||||
pub touched_ids: HashSet<Uuid>,
|
||||
/// When true, finishing a full-quality render at a zoom != 100% should schedule
|
||||
/// a non-blocking refresh of the 100% tile cache from the rendered tiles.
|
||||
base_cache_refresh_pending: bool,
|
||||
/// Queue of tiles (at `base_cache_refresh_src_scale_bits`) to reproject into 100%.
|
||||
base_cache_refresh_queue: Vec<tiles::Tile>,
|
||||
base_cache_refresh_src_scale_bits: u32,
|
||||
shape_last_extrect_by_scale: HashMap<ShapeScaleKey, Rect>,
|
||||
/// Temporary flag used for off-screen passes (drop-shadow masks, filter surfaces, etc.)
|
||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||
@ -366,6 +372,89 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
fn schedule_base_cache_refresh_from_full_render(&mut self, src_scale_bits: u32) {
|
||||
let base_bits = self.base_zoom_placeholder_scale_bits();
|
||||
if src_scale_bits == base_bits {
|
||||
self.base_cache_refresh_pending = false;
|
||||
self.base_cache_refresh_queue.clear();
|
||||
self.base_cache_refresh_src_scale_bits = 0;
|
||||
println!("base-cache(100%) refresh skipped (already at 100%)");
|
||||
return;
|
||||
}
|
||||
// Schedule only tiles we actually have at src scale (interest area).
|
||||
let scale = f32::from_bits(src_scale_bits);
|
||||
if !scale.is_finite() || scale <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let tiles::TileRect(sx, sy, ex, ey) = tiles::get_tiles_for_viewbox_with_interest(
|
||||
self.viewbox,
|
||||
VIEWPORT_INTEREST_AREA_THRESHOLD,
|
||||
scale,
|
||||
);
|
||||
self.base_cache_refresh_queue.clear();
|
||||
for x in sx..=ex {
|
||||
for y in sy..=ey {
|
||||
let t = tiles::Tile::from(x, y);
|
||||
if self.surfaces.has_cached_tile_surface(t, src_scale_bits) {
|
||||
self.base_cache_refresh_queue.push(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.base_cache_refresh_src_scale_bits = src_scale_bits;
|
||||
|
||||
if self.base_cache_refresh_queue.is_empty() {
|
||||
// Nothing to reproject from this full render.
|
||||
self.base_cache_refresh_pending = false;
|
||||
// Mark as "scheduled" (so we don't treat it as never started).
|
||||
// Keep src bits set so `process_base_cache_refresh_batch` can report completion consistently.
|
||||
println!(
|
||||
"base-cache(100%) refresh done (empty queue, src_scale_bits={})",
|
||||
src_scale_bits
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"base-cache(100%) refresh scheduled: {} tiles (src_scale_bits={})",
|
||||
self.base_cache_refresh_queue.len(),
|
||||
src_scale_bits
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_base_cache_refresh_batch(&mut self, max_tiles: usize) {
|
||||
// If we haven't scheduled yet (src bits = 0), do nothing: we are waiting for a full render
|
||||
// at a non-100% zoom to populate the queue.
|
||||
if self.base_cache_refresh_src_scale_bits == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.base_cache_refresh_queue.is_empty() {
|
||||
// Queue completed (or was empty but we already reported in schedule()).
|
||||
// Reset src bits to avoid repeatedly printing.
|
||||
self.base_cache_refresh_pending = false;
|
||||
self.base_cache_refresh_src_scale_bits = 0;
|
||||
println!("base-cache(100%) refresh done (queue already empty)");
|
||||
return;
|
||||
}
|
||||
let base_bits = self.base_zoom_placeholder_scale_bits();
|
||||
let src_bits = self.base_cache_refresh_src_scale_bits;
|
||||
for _ in 0..max_tiles {
|
||||
let Some(tile) = self.base_cache_refresh_queue.pop() else { break };
|
||||
let Some(img) = self.surfaces.cached_tile_image(tile, src_bits) else { continue };
|
||||
self.surfaces.reproject_cached_tile_into_scale(
|
||||
&self.tile_viewbox,
|
||||
&img,
|
||||
tile,
|
||||
src_bits,
|
||||
base_bits,
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
if self.base_cache_refresh_queue.is_empty() {
|
||||
self.base_cache_refresh_pending = false;
|
||||
self.base_cache_refresh_src_scale_bits = 0;
|
||||
println!("base-cache(100%) refresh done (src_scale_bits={})", src_bits);
|
||||
}
|
||||
}
|
||||
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
|
||||
// This needs to be done once per WebGL context.
|
||||
let mut gpu_state = GpuState::try_new()?;
|
||||
@ -415,6 +504,9 @@ impl RenderState {
|
||||
show_grid: None,
|
||||
focus_mode: FocusMode::new(),
|
||||
touched_ids: HashSet::default(),
|
||||
base_cache_refresh_pending: false,
|
||||
base_cache_refresh_queue: Vec::new(),
|
||||
base_cache_refresh_src_scale_bits: 0,
|
||||
shape_last_extrect_by_scale: HashMap::new(),
|
||||
ignore_nested_blurs: false,
|
||||
preview_mode: false,
|
||||
@ -475,6 +567,40 @@ impl RenderState {
|
||||
|
||||
self.shape_last_extrect_by_scale.insert(key, new_extrect);
|
||||
}
|
||||
|
||||
// Additionally invalidate the 100% zoom cache, but only for tiles that we already had.
|
||||
// This is the fast_mode source cache; we want it refreshed after edits, without
|
||||
// creating work for tiles that were never cached at 100%.
|
||||
let base_bits = self.base_zoom_placeholder_scale_bits();
|
||||
let base_scale = f32::from_bits(base_bits);
|
||||
if base_scale.is_finite() && base_scale > 0.0 {
|
||||
let new_extrect = shape.extrect(tree, base_scale);
|
||||
let key = ShapeScaleKey {
|
||||
shape_id: shape.id,
|
||||
scale_bits: base_bits,
|
||||
};
|
||||
let rect = if let Some(old) = self.shape_last_extrect_by_scale.get(&key).copied() {
|
||||
Self::rect_union(old, new_extrect)
|
||||
} else {
|
||||
new_extrect
|
||||
};
|
||||
|
||||
let tile_size = tiles::get_tile_size(base_scale);
|
||||
let TileRect(sx, sy, ex, ey) = tiles::get_tiles_for_rect(rect, tile_size);
|
||||
for x in sx..=ex {
|
||||
for y in sy..=ey {
|
||||
let tile = tiles::Tile::from(x, y);
|
||||
if self.surfaces.has_cached_tile_surface_stale_ok(tile, base_bits) {
|
||||
self.surfaces.remove_cached_tile_surface(tile, base_bits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.shape_last_extrect_by_scale.insert(key, new_extrect);
|
||||
}
|
||||
|
||||
// Any edit means the base cache may need refresh after next full render.
|
||||
self.base_cache_refresh_pending = true;
|
||||
}
|
||||
|
||||
/// Combines every visible layer blur currently active (ancestors + shape)
|
||||
@ -1639,18 +1765,25 @@ impl RenderState {
|
||||
timestamp: i32,
|
||||
) -> Result<()> {
|
||||
performance::begin_measure!("process_animation_frame");
|
||||
// Always advance the base-cache refresh job in small batches.
|
||||
// This job may be scheduled at the end of a full-quality render, and must
|
||||
// keep progressing even after the main render finishes (non-blocking).
|
||||
self.process_base_cache_refresh_batch(4);
|
||||
|
||||
if self.render_in_progress {
|
||||
if tree.len() != 0 {
|
||||
self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
|
||||
}
|
||||
self.flush_and_submit();
|
||||
}
|
||||
|
||||
if self.render_in_progress {
|
||||
self.cancel_animation_frame();
|
||||
self.render_request_id = Some(wapi::request_animation_frame!());
|
||||
} else {
|
||||
performance::end_measure!("render");
|
||||
}
|
||||
// Keep the RAF loop alive while either rendering is in progress or the
|
||||
// base-cache refresh job still has work to do.
|
||||
if self.render_in_progress || !self.base_cache_refresh_queue.is_empty() {
|
||||
self.cancel_animation_frame();
|
||||
self.render_request_id = Some(wapi::request_animation_frame!());
|
||||
} else if !self.render_in_progress {
|
||||
performance::end_measure!("render");
|
||||
}
|
||||
performance::end_measure!("process_animation_frame");
|
||||
Ok(())
|
||||
@ -2763,6 +2896,14 @@ impl RenderState {
|
||||
// Mark cache as valid for render_from_cache
|
||||
self.cached_viewbox = self.viewbox;
|
||||
|
||||
// If there was an edit, once we finish a full-quality render at a zoom != 100%,
|
||||
// schedule a non-blocking refresh of the 100% cache from those rendered tiles.
|
||||
if self.base_cache_refresh_pending && !self.options.is_fast_mode() {
|
||||
self.schedule_base_cache_refresh_from_full_render(self.get_scale().to_bits());
|
||||
}
|
||||
// Always process a small batch so refresh progresses without blocking.
|
||||
self.process_base_cache_refresh_batch(4);
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render(self);
|
||||
}
|
||||
|
||||
@ -566,6 +566,10 @@ impl Surfaces {
|
||||
self.tiles.has(tile, scale_bits)
|
||||
}
|
||||
|
||||
pub fn has_cached_tile_surface_stale_ok(&self, tile: Tile, scale_bits: u32) -> bool {
|
||||
self.tiles.has_stale(tile, scale_bits)
|
||||
}
|
||||
|
||||
pub fn world_rect_has_any_tile_at_scale_bits(&self, world_rect: Rect, scale_bits: u32) -> bool {
|
||||
let scale = f32::from_bits(scale_bits);
|
||||
if !scale.is_finite() || scale <= 0.0 {
|
||||
@ -618,6 +622,81 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cached_tile_image(&self, tile: Tile, scale_bits: u32) -> Option<skia::Image> {
|
||||
self.tiles.get(tile, scale_bits).cloned()
|
||||
}
|
||||
|
||||
pub fn reproject_cached_tile_into_scale(
|
||||
&mut self,
|
||||
tile_viewbox: &TileViewbox,
|
||||
src_image: &skia::Image,
|
||||
src_tile: Tile,
|
||||
src_scale_bits: u32,
|
||||
dst_scale_bits: u32,
|
||||
background: skia::Color,
|
||||
) {
|
||||
let src_scale = f32::from_bits(src_scale_bits);
|
||||
let dst_scale = f32::from_bits(dst_scale_bits);
|
||||
if !src_scale.is_finite() || src_scale <= 0.0 || !dst_scale.is_finite() || dst_scale <= 0.0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let src_world_rect = super::tiles::get_tile_rect(src_tile, src_scale);
|
||||
|
||||
let dst_tile_size_world = super::tiles::get_tile_size(dst_scale);
|
||||
let super::tiles::TileRect(sx, sy, ex, ey) =
|
||||
super::tiles::get_tiles_for_rect(src_world_rect, dst_tile_size_world);
|
||||
|
||||
for x in sx..=ex {
|
||||
for y in sy..=ey {
|
||||
let dst_tile = Tile::from(x, y);
|
||||
let dst_world_rect = super::tiles::get_tile_rect(dst_tile, dst_scale);
|
||||
let Some(overlap_world) = Self::rect_intersection(src_world_rect, dst_world_rect) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let src_px_l = (overlap_world.left() - src_world_rect.left()) * src_scale;
|
||||
let src_px_t = (overlap_world.top() - src_world_rect.top()) * src_scale;
|
||||
let src_px_r = (overlap_world.right() - src_world_rect.left()) * src_scale;
|
||||
let src_px_b = (overlap_world.bottom() - src_world_rect.top()) * src_scale;
|
||||
let src_rect = Rect::from_ltrb(src_px_l, src_px_t, src_px_r, src_px_b);
|
||||
|
||||
let dst_px_l = (overlap_world.left() - dst_world_rect.left()) * dst_scale;
|
||||
let dst_px_t = (overlap_world.top() - dst_world_rect.top()) * dst_scale;
|
||||
let dst_px_r = (overlap_world.right() - dst_world_rect.left()) * dst_scale;
|
||||
let dst_px_b = (overlap_world.bottom() - dst_world_rect.top()) * dst_scale;
|
||||
let dst_rect = Rect::from_ltrb(dst_px_l, dst_px_t, dst_px_r, dst_px_b);
|
||||
|
||||
let mut tile_surface = match self
|
||||
.current
|
||||
.new_surface_with_dimensions((TILE_SIZE as i32, TILE_SIZE as i32))
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
// IMPORTANT: compose over existing dst tile image to avoid erasing
|
||||
// regions not covered by this reprojection patch.
|
||||
tile_surface.canvas().clear(background);
|
||||
if let Some(existing) = self.tiles.get_stale(dst_tile, dst_scale_bits) {
|
||||
tile_surface.canvas().draw_image_rect(
|
||||
existing,
|
||||
None,
|
||||
Rect::from_xywh(0.0, 0.0, TILE_SIZE, TILE_SIZE),
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
tile_surface.canvas().draw_image_rect(
|
||||
src_image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Fast)),
|
||||
dst_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
let new_img = tile_surface.image_snapshot();
|
||||
self.tiles.add(tile_viewbox, &dst_tile, dst_scale_bits, new_img);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the current tile directly to the target and cache surfaces without
|
||||
/// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't
|
||||
/// populate the tile texture cache (suitable for one-shot renders like tests).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user