mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
Merge pull request #9112 from penpot/superalex-improve-atlas-growth
🎉 Improve atlas growth
This commit is contained in:
commit
d43d1f431f
@ -720,26 +720,24 @@ impl RenderState {
|
|||||||
self.surfaces.clear_cache(self.background_color);
|
self.surfaces.clear_cache(self.background_color);
|
||||||
self.cache_cleared_this_render = true;
|
self.cache_cleared_this_render = true;
|
||||||
}
|
}
|
||||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
|
||||||
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
// In fast mode the viewport is moving (pan/zoom) so Cache surface
|
||||||
// positions would be wrong — only save to the tile HashMap.
|
// positions would be wrong — only save to the tile HashMap.
|
||||||
|
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||||
|
let current_tile = *self
|
||||||
|
.current_tile
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(Error::CriticalError("Current tile not found".to_string()))?;
|
||||||
self.surfaces.cache_current_tile_texture(
|
self.surfaces.cache_current_tile_texture(
|
||||||
&mut self.gpu_state,
|
&mut self.gpu_state,
|
||||||
&self.tile_viewbox,
|
&self.tile_viewbox,
|
||||||
&self
|
¤t_tile,
|
||||||
.current_tile
|
|
||||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
|
||||||
&tile_rect,
|
&tile_rect,
|
||||||
fast_mode,
|
fast_mode,
|
||||||
self.render_area,
|
self.render_area,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.surfaces.draw_cached_tile_surface(
|
self.surfaces
|
||||||
self.current_tile
|
.draw_cached_tile_surface(current_tile, rect, self.background_color);
|
||||||
.ok_or(Error::CriticalError("Current tile not found".to_string()))?,
|
|
||||||
rect,
|
|
||||||
self.background_color,
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1674,6 +1672,12 @@ impl RenderState {
|
|||||||
self.cache_cleared_this_render = false;
|
self.cache_cleared_this_render = false;
|
||||||
self.reset_canvas();
|
self.reset_canvas();
|
||||||
|
|
||||||
|
// Compute and set document-space bounds (1 unit == 1 doc px @ 100% zoom)
|
||||||
|
// to clamp atlas updates. This prevents zoom-out tiles from forcing atlas
|
||||||
|
// growth far beyond real content.
|
||||||
|
let doc_bounds = self.compute_document_bounds(base_object, tree);
|
||||||
|
self.surfaces.set_atlas_doc_bounds(doc_bounds);
|
||||||
|
|
||||||
// During an interactive shape transform (drag/resize/rotate) the
|
// During an interactive shape transform (drag/resize/rotate) the
|
||||||
// Target is repainted tile-by-tile. If only a subset of the
|
// Target is repainted tile-by-tile. If only a subset of the
|
||||||
// invalidated tiles finishes in this rAF the remaining area
|
// invalidated tiles finishes in this rAF the remaining area
|
||||||
@ -1767,6 +1771,37 @@ impl RenderState {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compute_document_bounds(
|
||||||
|
&mut self,
|
||||||
|
base_object: Option<&Uuid>,
|
||||||
|
tree: ShapesPoolRef,
|
||||||
|
) -> Option<skia::Rect> {
|
||||||
|
let ids: Vec<Uuid> = if let Some(id) = base_object {
|
||||||
|
vec![*id]
|
||||||
|
} else {
|
||||||
|
let root = tree.get(&Uuid::nil())?;
|
||||||
|
root.children_ids(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut acc: Option<skia::Rect> = None;
|
||||||
|
for id in ids.iter() {
|
||||||
|
let Some(shape) = tree.get(id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let r = self.get_cached_extrect(shape, tree, 1.0);
|
||||||
|
if r.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
acc = Some(if let Some(mut a) = acc {
|
||||||
|
a.join(r);
|
||||||
|
a
|
||||||
|
} else {
|
||||||
|
r
|
||||||
|
});
|
||||||
|
}
|
||||||
|
acc
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_animation_frame(
|
pub fn process_animation_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
base_object: Option<&Uuid>,
|
base_object: Option<&Uuid>,
|
||||||
@ -2927,11 +2962,6 @@ impl RenderState {
|
|||||||
s.canvas().draw_rect(aligned_rect, &paint);
|
s.canvas().draw_rect(aligned_rect, &paint);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear atlas region to transparent so background shows through.
|
|
||||||
let _ = self
|
|
||||||
.surfaces
|
|
||||||
.clear_doc_rect_in_atlas(&mut self.gpu_state, self.render_area);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3156,7 +3186,8 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_cached_tile(&mut self, tile: tiles::Tile) {
|
pub fn remove_cached_tile(&mut self, tile: tiles::Tile) {
|
||||||
self.surfaces.remove_cached_tile_surface(tile);
|
self.surfaces
|
||||||
|
.remove_cached_tile_surface(&mut self.gpu_state, tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rebuild the tile index (shape→tile mapping) for all top-level shapes.
|
/// Rebuild the tile index (shape→tile mapping) for all top-level shapes.
|
||||||
|
|||||||
@ -78,10 +78,16 @@ pub struct Surfaces {
|
|||||||
/// When the atlas would exceed `max_atlas_texture_size`, this value is
|
/// When the atlas would exceed `max_atlas_texture_size`, this value is
|
||||||
/// reduced so the atlas stays within the fixed texture cap.
|
/// reduced so the atlas stays within the fixed texture cap.
|
||||||
atlas_scale: f32,
|
atlas_scale: f32,
|
||||||
|
/// Optional document-space bounds (1 unit == 1 doc px @ 100% zoom) used to
|
||||||
|
/// clamp atlas writes/clears so the atlas doesn't grow due to outlier tile rects.
|
||||||
|
atlas_doc_bounds: Option<skia::Rect>,
|
||||||
/// Max width/height in pixels for the atlas surface (typically browser
|
/// Max width/height in pixels for the atlas surface (typically browser
|
||||||
/// `MAX_TEXTURE_SIZE`). Set from ClojureScript after WebGL context creation.
|
/// `MAX_TEXTURE_SIZE`). Set from ClojureScript after WebGL context creation.
|
||||||
max_atlas_texture_size: i32,
|
max_atlas_texture_size: i32,
|
||||||
sampling_options: skia::SamplingOptions,
|
sampling_options: skia::SamplingOptions,
|
||||||
|
/// Tracks the last document-space rect written to the atlas per tile.
|
||||||
|
/// Used to clear old content without clearing the whole (potentially huge) tile rect.
|
||||||
|
atlas_tile_doc_rects: HashMap<Tile, skia::Rect>,
|
||||||
pub margins: skia::ISize,
|
pub margins: skia::ISize,
|
||||||
// Tracks which surfaces have content (dirty flag bitmask)
|
// Tracks which surfaces have content (dirty flag bitmask)
|
||||||
dirty_surfaces: u32,
|
dirty_surfaces: u32,
|
||||||
@ -147,8 +153,10 @@ impl Surfaces {
|
|||||||
atlas_origin: skia::Point::new(0.0, 0.0),
|
atlas_origin: skia::Point::new(0.0, 0.0),
|
||||||
atlas_size: skia::ISize::new(0, 0),
|
atlas_size: skia::ISize::new(0, 0),
|
||||||
atlas_scale: 1.0,
|
atlas_scale: 1.0,
|
||||||
|
atlas_doc_bounds: None,
|
||||||
max_atlas_texture_size: DEFAULT_MAX_ATLAS_TEXTURE_SIZE,
|
max_atlas_texture_size: DEFAULT_MAX_ATLAS_TEXTURE_SIZE,
|
||||||
sampling_options,
|
sampling_options,
|
||||||
|
atlas_tile_doc_rects: HashMap::default(),
|
||||||
margins,
|
margins,
|
||||||
dirty_surfaces: 0,
|
dirty_surfaces: 0,
|
||||||
extra_tile_dims,
|
extra_tile_dims,
|
||||||
@ -162,6 +170,28 @@ impl Surfaces {
|
|||||||
self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE);
|
self.max_atlas_texture_size = max_px.clamp(TILE_SIZE as i32, MAX_ATLAS_TEXTURE_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the document-space bounds used to clamp atlas updates.
|
||||||
|
/// Pass `None` to disable clamping.
|
||||||
|
pub fn set_atlas_doc_bounds(&mut self, bounds: Option<skia::Rect>) {
|
||||||
|
self.atlas_doc_bounds = bounds.filter(|b| !b.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clamp_doc_rect_to_bounds(&self, doc_rect: skia::Rect) -> skia::Rect {
|
||||||
|
if doc_rect.is_empty() {
|
||||||
|
return doc_rect;
|
||||||
|
}
|
||||||
|
if let Some(bounds) = self.atlas_doc_bounds {
|
||||||
|
let mut r = doc_rect;
|
||||||
|
if r.intersect(bounds) {
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
skia::Rect::new_empty()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc_rect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_atlas_contains(
|
fn ensure_atlas_contains(
|
||||||
&mut self,
|
&mut self,
|
||||||
gpu_state: &mut GpuState,
|
gpu_state: &mut GpuState,
|
||||||
@ -271,21 +301,51 @@ impl Surfaces {
|
|||||||
&mut self,
|
&mut self,
|
||||||
gpu_state: &mut GpuState,
|
gpu_state: &mut GpuState,
|
||||||
tile_image: &skia::Image,
|
tile_image: &skia::Image,
|
||||||
doc_rect: skia::Rect,
|
tile_doc_rect: skia::Rect,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
self.ensure_atlas_contains(gpu_state, doc_rect)?;
|
if tile_doc_rect.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp to document bounds (if any) and compute a matching source-rect in tile pixels.
|
||||||
|
let mut clipped_doc_rect = tile_doc_rect;
|
||||||
|
if let Some(bounds) = self.atlas_doc_bounds {
|
||||||
|
if !clipped_doc_rect.intersect(bounds) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if clipped_doc_rect.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.ensure_atlas_contains(gpu_state, clipped_doc_rect)?;
|
||||||
|
|
||||||
// Destination is document-space rect mapped into atlas pixel coords.
|
// Destination is document-space rect mapped into atlas pixel coords.
|
||||||
let dst = skia::Rect::from_xywh(
|
let dst = skia::Rect::from_xywh(
|
||||||
(doc_rect.left - self.atlas_origin.x) * self.atlas_scale,
|
(clipped_doc_rect.left - self.atlas_origin.x) * self.atlas_scale,
|
||||||
(doc_rect.top - self.atlas_origin.y) * self.atlas_scale,
|
(clipped_doc_rect.top - self.atlas_origin.y) * self.atlas_scale,
|
||||||
doc_rect.width() * self.atlas_scale,
|
clipped_doc_rect.width() * self.atlas_scale,
|
||||||
doc_rect.height() * self.atlas_scale,
|
clipped_doc_rect.height() * self.atlas_scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.atlas
|
// Compute source rect in tile_image pixel coordinates.
|
||||||
.canvas()
|
let img_w = tile_image.width() as f32;
|
||||||
.draw_image_rect(tile_image, None, dst, &skia::Paint::default());
|
let img_h = tile_image.height() as f32;
|
||||||
|
let tw = tile_doc_rect.width().max(1.0);
|
||||||
|
let th = tile_doc_rect.height().max(1.0);
|
||||||
|
|
||||||
|
let sx = ((clipped_doc_rect.left - tile_doc_rect.left) / tw) * img_w;
|
||||||
|
let sy = ((clipped_doc_rect.top - tile_doc_rect.top) / th) * img_h;
|
||||||
|
let sw = (clipped_doc_rect.width() / tw) * img_w;
|
||||||
|
let sh = (clipped_doc_rect.height() / th) * img_h;
|
||||||
|
let src = skia::Rect::from_xywh(sx, sy, sw, sh);
|
||||||
|
|
||||||
|
self.atlas.canvas().draw_image_rect(
|
||||||
|
tile_image,
|
||||||
|
Some((&src, skia::canvas::SrcRectConstraint::Fast)),
|
||||||
|
dst,
|
||||||
|
&skia::Paint::default(),
|
||||||
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,6 +354,7 @@ impl Surfaces {
|
|||||||
gpu_state: &mut GpuState,
|
gpu_state: &mut GpuState,
|
||||||
doc_rect: skia::Rect,
|
doc_rect: skia::Rect,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let doc_rect = self.clamp_doc_rect_to_bounds(doc_rect);
|
||||||
if doc_rect.is_empty() {
|
if doc_rect.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -316,6 +377,18 @@ impl Surfaces {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clears the last atlas region written by `tile` (if any).
|
||||||
|
///
|
||||||
|
/// This avoids clearing the entire logical tile rect which, at very low
|
||||||
|
/// zoom levels, can be enormous in document space and would unnecessarily
|
||||||
|
/// grow / rescale the atlas.
|
||||||
|
pub fn clear_tile_in_atlas(&mut self, gpu_state: &mut GpuState, tile: Tile) -> Result<()> {
|
||||||
|
if let Some(doc_rect) = self.atlas_tile_doc_rects.remove(&tile) {
|
||||||
|
self.clear_doc_rect_in_atlas(gpu_state, doc_rect)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clear_tiles(&mut self) {
|
pub fn clear_tiles(&mut self) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
}
|
}
|
||||||
@ -817,6 +890,7 @@ impl Surfaces {
|
|||||||
// Incrementally update persistent 1:1 atlas in document space.
|
// Incrementally update persistent 1:1 atlas in document space.
|
||||||
// `tile_doc_rect` is in world/document coordinates (1 unit == 1 px at 100%).
|
// `tile_doc_rect` is in world/document coordinates (1 unit == 1 px at 100%).
|
||||||
let _ = self.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect);
|
let _ = self.blit_tile_image_into_atlas(gpu_state, &tile_image, tile_doc_rect);
|
||||||
|
self.atlas_tile_doc_rects.insert(*tile, tile_doc_rect);
|
||||||
self.tiles.add(tile_viewbox, tile, tile_image);
|
self.tiles.add(tile_viewbox, tile, tile_image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -825,11 +899,14 @@ impl Surfaces {
|
|||||||
self.tiles.has(tile)
|
self.tiles.has(tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
|
pub fn remove_cached_tile_surface(&mut self, gpu_state: &mut GpuState, tile: Tile) {
|
||||||
// Mark tile as invalid
|
// Mark tile as invalid
|
||||||
// Old content stays visible until new tile overwrites it atomically,
|
// Old content stays visible until new tile overwrites it atomically,
|
||||||
// preventing flickering during tile re-renders.
|
// preventing flickering during tile re-renders.
|
||||||
self.tiles.remove(tile);
|
self.tiles.remove(tile);
|
||||||
|
// Also clear the corresponding region in the persistent atlas to avoid
|
||||||
|
// leaving stale pixels when shapes move/delete.
|
||||||
|
let _ = self.clear_tile_in_atlas(gpu_state, tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||||
@ -914,6 +991,7 @@ impl Surfaces {
|
|||||||
/// the cache canvas for scaled previews, use `invalidate_tile_cache` instead.
|
/// the cache canvas for scaled previews, use `invalidate_tile_cache` instead.
|
||||||
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
|
self.atlas_tile_doc_rects.clear();
|
||||||
self.cache.canvas().clear(color);
|
self.cache.canvas().clear(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -923,6 +1001,7 @@ impl Surfaces {
|
|||||||
/// content while new tiles are being rendered.
|
/// content while new tiles are being rendered.
|
||||||
pub fn invalidate_tile_cache(&mut self) {
|
pub fn invalidate_tile_cache(&mut self) {
|
||||||
self.tiles.clear();
|
self.tiles.clear();
|
||||||
|
self.atlas_tile_doc_rects.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gc(&mut self) {
|
pub fn gc(&mut self) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user