mirror of
https://github.com/penpot/penpot.git
synced 2026-06-29 02:32:04 +00:00
Merge pull request #10407 from penpot/superalex-render-from-cache-4
🐛 Fix render from cache
This commit is contained in:
commit
888f6798cf
@ -2003,89 +2003,12 @@ 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 bg_color = self.background_color;
|
||||
|
||||
// During fast mode (pan/zoom), if a previous full-quality render still has pending tiles,
|
||||
// always prefer the persistent atlas. The atlas is incrementally updated as tiles finish,
|
||||
// and drawing from it avoids mixing a partially-updated Cache surface with missing tiles.
|
||||
if self.options.is_fast_mode() && !self.surfaces.atlas.is_empty() {
|
||||
self.surfaces
|
||||
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
|
||||
|
||||
self.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 = self.options.dpr_viewport_interest_area_threshold;
|
||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||
tiles::get_tiles_for_viewbox_with_interest(&self.cached_viewbox, interest);
|
||||
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;
|
||||
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||
|
||||
// For zoom-out, prefer cache only if it fully covers the viewport.
|
||||
// Otherwise, atlas will provide a more correct full-viewport preview.
|
||||
let zooming_out = self.viewbox.zoom < self.cached_viewbox.zoom;
|
||||
if zooming_out {
|
||||
let cache_dim = self.surfaces.cache_dimensions();
|
||||
let cache_w = cache_dim.width as f32;
|
||||
let cache_h = cache_dim.height as f32;
|
||||
|
||||
// Viewport in target pixels.
|
||||
let vw = self.viewbox.dpr_width().max(1.0);
|
||||
let vh = self.viewbox.dpr_height().max(1.0);
|
||||
|
||||
// Inverse-map viewport corners into cache coordinates.
|
||||
// target = (cache * navigate_zoom) translated by (translate_x, translate_y) (in cache coords).
|
||||
// => cache = (target / navigate_zoom) - translate
|
||||
let inv = if navigate_zoom.abs() > f32::EPSILON {
|
||||
1.0 / navigate_zoom
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// let cx0 = (0.0 * inv) - translate_x;
|
||||
// let cy0 = (0.0 * inv) - translate_y;
|
||||
// NOTA: 0.0 * inv => siempre 0
|
||||
let cx0 = -translate_x;
|
||||
let cy0 = -translate_y;
|
||||
let cx1 = (vw * inv) - translate_x;
|
||||
let cy1 = (vh * inv) - translate_y;
|
||||
|
||||
let min_x = cx0.min(cx1);
|
||||
let min_y = cy0.min(cy1);
|
||||
let max_x = cx0.max(cx1);
|
||||
let max_y = cy0.max(cy1);
|
||||
|
||||
let cache_covers =
|
||||
min_x >= 0.0 && min_y >= 0.0 && max_x <= cache_w && max_y <= cache_h;
|
||||
if !cache_covers {
|
||||
// Early return only if atlas exists; otherwise keep cache path.
|
||||
if !self.surfaces.atlas.is_empty() {
|
||||
self.surfaces
|
||||
.draw_atlas_to_backbuffer(self.viewbox, bg_color);
|
||||
|
||||
self.present_frame(shapes);
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw directly from cache surface, avoiding snapshot overhead
|
||||
self.surfaces.draw_cache_to_backbuffer();
|
||||
|
||||
self.present_frame(shapes);
|
||||
}
|
||||
self.surfaces.draw_combined_atlas_to_backbuffer(
|
||||
&self.viewbox,
|
||||
&self.tile_viewbox,
|
||||
self.background_color,
|
||||
);
|
||||
self.present_frame(shapes);
|
||||
|
||||
performance::end_measure!("render_from_cache");
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
|
||||
@ -590,6 +590,57 @@ impl Surfaces {
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
/// Fast pan/zoom preview: draw the doc atlas as backdrop, then overlay HQ
|
||||
/// cached tile textures placed via their stored document rects (pan + scale).
|
||||
pub fn draw_combined_atlas_to_backbuffer(
|
||||
&mut self,
|
||||
viewbox: &Viewbox,
|
||||
tile_viewbox: &TileViewbox,
|
||||
background: skia::Color,
|
||||
) {
|
||||
self.draw_atlas_to_backbuffer(*viewbox, background);
|
||||
|
||||
// Tile textures are keyed by grid index but positioned in document space.
|
||||
// Without `tile_doc_rects` we cannot displace/scale them correctly (e.g.
|
||||
// right after zoom invalidation); the atlas backdrop alone is enough.
|
||||
if self.atlas.tile_doc_rects.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let batch = self.tiles.build_atlas_draw_batch_for_doc_rects(
|
||||
viewbox,
|
||||
tile_viewbox,
|
||||
&self.atlas.tile_doc_rects,
|
||||
);
|
||||
if batch.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
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.save();
|
||||
canvas.reset_matrix();
|
||||
|
||||
canvas.draw_atlas(
|
||||
atlas_image,
|
||||
&batch.transforms,
|
||||
&batch.textures,
|
||||
None,
|
||||
skia::BlendMode::SrcOver,
|
||||
self.atlas_sampling_options,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
pub fn margins(&self) -> skia::ISize {
|
||||
self.margins
|
||||
}
|
||||
@ -716,18 +767,6 @@ impl Surfaces {
|
||||
);
|
||||
}
|
||||
|
||||
/// Draws the cache surface directly to the backbuffer canvas.
|
||||
/// This avoids creating an intermediate snapshot, reducing GPU stalls.
|
||||
pub fn draw_cache_to_backbuffer(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.cache.draw(
|
||||
self.backbuffer.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn cache_dimensions(&self) -> skia::ISize {
|
||||
skia::ISize::new(self.cache.width(), self.cache.height())
|
||||
}
|
||||
@ -1516,6 +1555,17 @@ pub struct TileTextureCache {
|
||||
removed: HashSet<Tile>,
|
||||
}
|
||||
|
||||
pub struct AtlasDrawBatch {
|
||||
pub transforms: Vec<skia::RSXform>,
|
||||
pub textures: Vec<skia::Rect>,
|
||||
}
|
||||
|
||||
impl AtlasDrawBatch {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.transforms.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl TileTextureCache {
|
||||
pub fn new(texture_size: i32, capacity: usize) -> Self {
|
||||
Self {
|
||||
@ -1615,6 +1665,76 @@ impl TileTextureCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_atlas_draw_batch_for_doc_rects(
|
||||
&self,
|
||||
viewbox: &Viewbox,
|
||||
tile_viewbox: &TileViewbox,
|
||||
tile_doc_rects: &HashMap<Tile, skia::Rect>,
|
||||
) -> AtlasDrawBatch {
|
||||
let mut transforms = Vec::new();
|
||||
let mut textures = Vec::new();
|
||||
|
||||
let s = viewbox.get_scale();
|
||||
let view_doc = viewbox.area;
|
||||
|
||||
for y in tile_viewbox.visible_rect.top()..=tile_viewbox.visible_rect.bottom() {
|
||||
for x in tile_viewbox.visible_rect.left()..=tile_viewbox.visible_rect.right() {
|
||||
let tile = Tile(x, y);
|
||||
|
||||
let Some(tile_ref) = self.grid.get(&tile) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if self.removed.contains(&tile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let doc_rect = tile_doc_rects
|
||||
.get(&tile)
|
||||
.copied()
|
||||
.unwrap_or_else(|| tiles::get_tile_rect(tile, s));
|
||||
if doc_rect.is_empty() || !doc_rect.intersects(view_doc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let scos = doc_rect.width() * s / self.tile_size;
|
||||
let tx = (doc_rect.left + viewbox.pan.x) * s;
|
||||
let ty = (doc_rect.top + viewbox.pan.y) * s;
|
||||
|
||||
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
||||
textures.push(tile_ref.rect);
|
||||
}
|
||||
}
|
||||
|
||||
// Cached tiles from a previous zoom level use indices outside visible_rect;
|
||||
// place them via their stored document rect, not the current grid walk above.
|
||||
for (&tile, tile_ref) in &self.grid {
|
||||
if tile_viewbox.is_visible(&tile) || self.removed.contains(&tile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let doc_rect = tile_doc_rects
|
||||
.get(&tile)
|
||||
.copied()
|
||||
.unwrap_or_else(|| tiles::get_tile_rect(tile, s));
|
||||
if doc_rect.is_empty() || !doc_rect.intersects(view_doc) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tx = (doc_rect.left + viewbox.pan.x) * s;
|
||||
let ty = (doc_rect.top + viewbox.pan.y) * s;
|
||||
let scos = doc_rect.width() * s / self.tile_size;
|
||||
|
||||
transforms.push(skia::RSXform::new(scos, 0.0, (tx, ty)));
|
||||
textures.push(tile_ref.rect);
|
||||
}
|
||||
|
||||
AtlasDrawBatch {
|
||||
transforms,
|
||||
textures,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has(&self, tile: Tile) -> bool {
|
||||
self.grid.contains_key(&tile) && !self.removed.contains(&tile)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user