mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🎉 Reuse levels
This commit is contained in:
parent
a87552bc45
commit
93d227e07d
@ -361,6 +361,23 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Origin (px) of the tile-aligned grid used when blitting into `SurfaceId::Cache`.
|
||||
///
|
||||
/// The cache mosaic stores tiles on a fixed \(TILE_SIZE × TILE_SIZE\) grid in *scaled* pixel
|
||||
/// space (i.e. after applying `scale`). When the viewbox is between tile boundaries (pan),
|
||||
/// the on-screen tile bounds (`get_current_tile_bounds`) move continuously, but the cache mosaic
|
||||
/// must remain snapped to the global grid so previously cached tiles keep landing in the same
|
||||
/// slots.
|
||||
///
|
||||
/// This function computes that snapped grid origin by rounding the viewbox top-left down to the
|
||||
/// nearest multiple of `tiles::TILE_SIZE` in scaled space. Callers then position individual tiles
|
||||
/// relative to this origin via `get_aligned_tile_bounds`.
|
||||
fn cache_mosaic_snap_origin(viewbox: Viewbox, scale: f32) -> (f32, f32) {
|
||||
let sx = (viewbox.area.left * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
|
||||
let sy = (viewbox.area.top * scale / tiles::TILE_SIZE).floor() * tiles::TILE_SIZE;
|
||||
(sx, sy)
|
||||
}
|
||||
|
||||
impl RenderState {
|
||||
pub fn try_new(width: i32, height: i32) -> Result<RenderState> {
|
||||
// This needs to be done once per WebGL context.
|
||||
@ -676,19 +693,23 @@ impl RenderState {
|
||||
self.surfaces.clear_cache(self.background_color);
|
||||
self.cache_cleared_this_render = true;
|
||||
}
|
||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
// Cache mosaic uses a tile grid snapped to TILE_SIZE in scaled space; `render_from_cache`
|
||||
// relies on that (continuous view offset minus snapped tile origin). The target blit must
|
||||
// still use `rect` from `get_current_tile_bounds` for smooth sub-tile pan on screen.
|
||||
let cache_tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
self.surfaces.cache_current_tile_texture(
|
||||
&self.tile_viewbox,
|
||||
&self
|
||||
.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
||||
&tile_rect,
|
||||
&cache_tile_rect,
|
||||
);
|
||||
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
self.current_tile
|
||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
||||
rect,
|
||||
None,
|
||||
self.background_color,
|
||||
);
|
||||
Ok(())
|
||||
@ -1510,6 +1531,7 @@ impl RenderState {
|
||||
|
||||
self.cache_cleared_this_render = false;
|
||||
self.reset_canvas();
|
||||
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
| SurfaceId::Fills as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
@ -1519,7 +1541,8 @@ impl RenderState {
|
||||
});
|
||||
|
||||
let viewbox_cache_size = get_cache_size(self.viewbox, scale);
|
||||
let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox, scale);
|
||||
let cached_scale = self.get_cached_scale();
|
||||
let cached_viewbox_cache_size = get_cache_size(self.cached_viewbox, cached_scale);
|
||||
// Only resize cache if the new size is larger than the cached size
|
||||
// This avoids unnecessary surface recreations when the cache size decreases
|
||||
if viewbox_cache_size.width > cached_viewbox_cache_size.width
|
||||
@ -1529,6 +1552,26 @@ impl RenderState {
|
||||
.resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD)?;
|
||||
}
|
||||
|
||||
// The cache mosaic uses `get_aligned_tile_bounds` (snapped origin). If we pan across a
|
||||
// TILE_SIZE boundary in scaled space, new tiles would map to different slots while old
|
||||
// pixels stay put — `render_from_cache` then shows a sheared / shifted composite.
|
||||
if self.cached_viewbox.area.width() > 0.0 {
|
||||
println!("1");
|
||||
let mosaic_stale = if self.zoom_changed() {
|
||||
true
|
||||
} else {
|
||||
let snap_now = cache_mosaic_snap_origin(self.viewbox, scale);
|
||||
let snap_cached = cache_mosaic_snap_origin(self.cached_viewbox, cached_scale);
|
||||
snap_now != snap_cached
|
||||
};
|
||||
println!("mosaic_stale: {}", mosaic_stale);
|
||||
if mosaic_stale {
|
||||
println!("clearing cache");
|
||||
self.surfaces.clear_cache(self.background_color);
|
||||
self.cache_cleared_this_render = true;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME - review debug
|
||||
// debug::render_debug_tiles_for_viewbox(self);
|
||||
|
||||
@ -1879,14 +1922,12 @@ impl RenderState {
|
||||
)
|
||||
}
|
||||
|
||||
// Returns the bounds of the current tile relative to the viewbox,
|
||||
// aligned to the nearest tile grid origin.
|
||||
//
|
||||
// Unlike `get_current_tile_bounds`, which calculates bounds using the exact
|
||||
// scaled offset of the viewbox, this method snaps the origin to the nearest
|
||||
// lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned
|
||||
// with the global tile grid, which is useful for rendering tiles in a
|
||||
/// consistent and predictable layout.
|
||||
/// Returns the bounds of the current tile relative to the viewbox, aligned to the nearest
|
||||
/// tile grid origin.
|
||||
///
|
||||
/// Unlike [`Self::get_current_tile_bounds`], which uses the exact scaled offset of the
|
||||
/// viewbox, this snaps the origin to the nearest lower multiple of [`tiles::TILE_SIZE`], so
|
||||
/// tile bounds line up with the global tile grid used for the cache mosaic.
|
||||
pub fn get_current_aligned_tile_bounds(&mut self) -> Result<Rect> {
|
||||
Ok(self.get_aligned_tile_bounds(
|
||||
self.current_tile
|
||||
@ -2571,9 +2612,15 @@ impl RenderState {
|
||||
if self.surfaces.has_cached_tile_surface(current_tile) {
|
||||
performance::begin_measure!("render_shape_tree::cached");
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
let cache_rect = if self.zoom_changed() {
|
||||
None
|
||||
} else {
|
||||
Some(self.get_aligned_tile_bounds(current_tile))
|
||||
};
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
current_tile,
|
||||
tile_rect,
|
||||
cache_rect,
|
||||
self.background_color,
|
||||
);
|
||||
performance::end_measure!("render_shape_tree::cached");
|
||||
@ -2866,13 +2913,10 @@ impl RenderState {
|
||||
|
||||
self.rebuild_tile_index(tree);
|
||||
|
||||
// Zoom changes world tile size: a partial cache update would mix scales in the
|
||||
// mosaic and glitch. Same zoom as last finished render (typical pan): drop only
|
||||
// tile textures and keep the cache canvas for render_from_cache.
|
||||
// Typical pan at the same zoom: keep tile textures so we can blit from cache.
|
||||
// Zoom changes invalidate tile textures (different scale); drop caches then.
|
||||
if self.zoom_changed() {
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
} else {
|
||||
self.surfaces.invalidate_tile_cache();
|
||||
}
|
||||
|
||||
performance::end_measure!("rebuild_tiles_shallow");
|
||||
|
||||
@ -581,16 +581,34 @@ impl Surfaces {
|
||||
self.tiles.remove(tile);
|
||||
}
|
||||
|
||||
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||
/// Draws a cached tile image to the target. When `cache_rect` is set, also blits into the
|
||||
/// cache mosaic (`render_from_cache`).
|
||||
pub fn draw_cached_tile_surface(
|
||||
&mut self,
|
||||
tile: Tile,
|
||||
target_rect: skia::Rect,
|
||||
cache_rect: Option<skia::Rect>,
|
||||
color: skia::Color,
|
||||
) {
|
||||
if let Some(image) = self.tiles.get(tile) {
|
||||
let img: &skia::Image = &*image;
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
|
||||
self.target.canvas().draw_rect(rect, &paint);
|
||||
self.target.canvas().draw_rect(target_rect, &paint);
|
||||
|
||||
self.target
|
||||
.canvas()
|
||||
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
||||
self.target.canvas().draw_image_rect(
|
||||
img,
|
||||
None,
|
||||
target_rect,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
|
||||
if let Some(cr) = cache_rect {
|
||||
self.cache
|
||||
.canvas()
|
||||
.draw_image_rect(img, None, cr, &skia::Paint::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -726,11 +744,7 @@ impl TileTextureCache {
|
||||
.grid
|
||||
.iter_mut()
|
||||
.filter_map(|(tile, _)| {
|
||||
if !tile_viewbox.is_visible(tile) {
|
||||
Some(*tile)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
(!tile_viewbox.is_visible(tile)).then_some(*tile)
|
||||
})
|
||||
.take(TEXTURES_BATCH_DELETE)
|
||||
.collect();
|
||||
|
||||
@ -112,14 +112,16 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> {
|
||||
// If zoom changed (e.g. interrupted zoom render followed by pan), the
|
||||
// tile index may be stale for the new viewport position. Rebuild the
|
||||
// index so shapes are mapped to the correct tiles. We use
|
||||
// rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile
|
||||
// texture cache — otherwise cached tiles with shadows/blur would be
|
||||
// cleared and re-rendered in fast mode without effects.
|
||||
// During view interactions (wheel/pinch), the frontend paints intermediate frames via
|
||||
// `render_from_cache` and may interrupt renders. If the zoom changes mid-flight, the
|
||||
// tile index can become stale for the new viewbox and cached tile textures (rasterized
|
||||
// at the previous scale) must not be blitted again.
|
||||
//
|
||||
// We therefore use `rebuild_tiles_shallow` on zoom changes: it rebuilds the tile index
|
||||
// and drops cached tiles for zoom transitions, but keeps the cache intact for same-zoom
|
||||
// pans (so panning stays fast).
|
||||
if self.render_state.zoom_changed() {
|
||||
self.render_state.rebuild_tile_index(&self.shapes);
|
||||
self.render_state.rebuild_tiles_shallow(&self.shapes);
|
||||
}
|
||||
|
||||
self.render_state
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user