mirror of
https://github.com/penpot/penpot.git
synced 2026-05-03 15:18:59 +00:00
Merge pull request #9190 from penpot/elenatorro-improve-performance-on-dragging
⚡ Improve drag performance
This commit is contained in:
commit
2aff116906
@ -225,6 +225,17 @@ pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||
pub extern "C" fn render(_: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.rebuild_touched_tiles();
|
||||
// Drain the throttled modifier-tile invalidation accumulated
|
||||
// since the previous rAF. set_modifiers skips this work during
|
||||
// interactive_transform; we do it once here, with the current
|
||||
// modifier set, so the cost is paid once per rAF rather than
|
||||
// once per pointer move.
|
||||
if state.render_state.options.is_interactive_transform() {
|
||||
let ids = state.shapes.modifier_ids();
|
||||
if !ids.is_empty() {
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
}
|
||||
}
|
||||
state
|
||||
.start_render_loop(performance::get_time())
|
||||
.map_err(|_| Error::RecoverableError("Error rendering".to_string()))?;
|
||||
@ -344,11 +355,9 @@ pub extern "C" fn render_loading_overlay() -> Result<()> {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||||
let result = with_state_mut!(state, { state.process_animation_frame(timestamp) });
|
||||
|
||||
if let Err(err) = result {
|
||||
eprintln!("process_animation_frame error: {}", err);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -453,9 +462,11 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_modifiers_start");
|
||||
let opts = &mut state.render_state.options;
|
||||
opts.set_fast_mode(true);
|
||||
opts.set_interactive_transform(true);
|
||||
state.render_state.options.set_fast_mode(true);
|
||||
state.render_state.options.set_interactive_transform(true);
|
||||
// Capture the last fully-rendered frame as a stable backdrop for the drag.
|
||||
// This avoids relying on atlas/cache correctness during fast_mode.
|
||||
state.render_state.surfaces.copy_target_to_backbuffer();
|
||||
performance::end_measure!("set_modifiers_start");
|
||||
});
|
||||
Ok(())
|
||||
@ -470,9 +481,8 @@ pub extern "C" fn set_modifiers_start() -> Result<()> {
|
||||
pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_modifiers_end");
|
||||
let opts = &mut state.render_state.options;
|
||||
opts.set_fast_mode(false);
|
||||
opts.set_interactive_transform(false);
|
||||
state.render_state.options.set_fast_mode(false);
|
||||
state.render_state.options.set_interactive_transform(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
});
|
||||
@ -945,7 +955,11 @@ pub extern "C" fn set_structure_modifiers() -> Result<()> {
|
||||
pub extern "C" fn clean_modifiers() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let prev_modifier_ids = state.shapes.clean_all();
|
||||
if !prev_modifier_ids.is_empty() {
|
||||
// Skip the tile-cache cleanup during interactive transform: the
|
||||
// per-rAF `rebuild_modifier_tiles` in `render()` already evicts
|
||||
// the same tiles for the active modifier set, so the eviction
|
||||
// here is redundant and doubles the per-emission cost.
|
||||
if !prev_modifier_ids.is_empty() && !state.render_state.options.is_interactive_transform() {
|
||||
state
|
||||
.render_state
|
||||
.update_tiles_shapes(&prev_modifier_ids, &mut state.shapes)?;
|
||||
@ -973,7 +987,10 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
|
||||
with_state_mut!(state, {
|
||||
state.set_modifiers(modifiers);
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
// TO CHECK
|
||||
if !state.render_state.options.is_interactive_transform() {
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -334,6 +334,17 @@ pub(crate) struct RenderState {
|
||||
/// Cleared at the beginning of a render pass; set to true after we clear Cache the first
|
||||
/// time we are about to blit a tile into Cache for this pass.
|
||||
pub cache_cleared_this_render: bool,
|
||||
/// True iff the current tile had shapes assigned to it when we
|
||||
/// started rendering it. Lets us distinguish a genuinely empty
|
||||
/// tile (skip composite, just clear) from a tile whose walker
|
||||
/// finished its work in a previous PAF and is now being resumed
|
||||
/// (must composite to present the work). Reset when current_tile
|
||||
/// changes.
|
||||
pub current_tile_had_shapes: bool,
|
||||
/// During interactive transforms we keep `Target` between rAFs. Seed the
|
||||
/// 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,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize {
|
||||
@ -407,6 +418,8 @@ impl RenderState {
|
||||
preview_mode: false,
|
||||
export_context: None,
|
||||
cache_cleared_this_render: false,
|
||||
current_tile_had_shapes: false,
|
||||
interactive_target_seeded: false,
|
||||
})
|
||||
}
|
||||
|
||||
@ -624,6 +637,10 @@ impl RenderState {
|
||||
|
||||
pub fn set_viewport_interest_area_threshold(&mut self, value: i32) {
|
||||
self.options.set_viewport_interest_area_threshold(value);
|
||||
// The TileViewbox stores its own copy of `interest` (set at
|
||||
// construction). Without propagating, options change wouldn't
|
||||
// affect pending_tiles generation.
|
||||
self.tile_viewbox.set_interest(value);
|
||||
}
|
||||
|
||||
pub fn set_node_batch_threshold(&mut self, value: i32) {
|
||||
@ -712,6 +729,16 @@ impl RenderState {
|
||||
}
|
||||
|
||||
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> {
|
||||
// During interactive transforms we render tiles directly into Target; updating the cache
|
||||
// (snapshot -> atlas blit -> tiles.add) can force GPU stalls. Defer cache rebuild until
|
||||
// the interaction ends.
|
||||
if self.options.is_interactive_transform() {
|
||||
let tile_rect = self.get_current_aligned_tile_bounds()?;
|
||||
self.surfaces
|
||||
.draw_current_tile_direct_target_only(&tile_rect, self.background_color);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let fast_mode = self.options.is_fast_mode();
|
||||
// Decide *now* (at the first real cache blit) whether we need to clear Cache.
|
||||
// This avoids clearing Cache on renders that don't actually paint tiles (e.g. hover/UI),
|
||||
@ -864,10 +891,14 @@ impl RenderState {
|
||||
s.canvas().save();
|
||||
});
|
||||
}
|
||||
|
||||
let antialias =
|
||||
shape.should_use_antialias(self.get_scale(), self.options.antialias_threshold);
|
||||
let fast_mode = self.options.is_fast_mode();
|
||||
// Skip anti-aliasing entirely during fast_mode (interactive
|
||||
// gestures + pan/zoom). AA edge sampling is per-pixel and adds
|
||||
// up across many shapes; reverts to full quality on commit.
|
||||
let antialias = !fast_mode
|
||||
&& shape.should_use_antialias(self.get_scale(), self.options.antialias_threshold);
|
||||
let skip_effects = fast_mode;
|
||||
|
||||
let has_nested_fills = self
|
||||
.nested_fills
|
||||
.last()
|
||||
@ -910,7 +941,6 @@ impl RenderState {
|
||||
});
|
||||
|
||||
fills::render(self, shape, &shape.fills, antialias, target_surface, None)?;
|
||||
|
||||
// Pass strokes in natural order; stroke merging handles top-most ordering internally.
|
||||
let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect();
|
||||
strokes::render(
|
||||
@ -995,7 +1025,7 @@ impl RenderState {
|
||||
// Remove background blur from the shape so it doesn't get processed
|
||||
// as a layer blur. The actual rendering is done before the save_layer
|
||||
// in render_background_blur() so it's independent of shape opacity.
|
||||
if !fast_mode
|
||||
if !skip_effects
|
||||
&& apply_to_current_surface
|
||||
&& fills_surface_id == SurfaceId::Fills
|
||||
&& !matches!(shape.shape_type, Type::Text(_))
|
||||
@ -1021,14 +1051,14 @@ impl RenderState {
|
||||
} else if shape_has_blur {
|
||||
shape.to_mut().set_blur(None);
|
||||
}
|
||||
if fast_mode {
|
||||
if skip_effects {
|
||||
shape.to_mut().set_blur(None);
|
||||
}
|
||||
|
||||
// For non-text, non-SVG shapes in the normal rendering path, apply blur
|
||||
// via a single save_layer on each render surface
|
||||
// Clip correctness is preserved
|
||||
let blur_sigma_for_layers: Option<f32> = if !fast_mode
|
||||
let blur_sigma_for_layers: Option<f32> = if !skip_effects
|
||||
&& apply_to_current_surface
|
||||
&& fills_surface_id == SurfaceId::Fills
|
||||
&& !matches!(shape.shape_type, Type::Text(_))
|
||||
@ -1107,7 +1137,7 @@ impl RenderState {
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
if fast_mode {
|
||||
if skip_effects {
|
||||
// Fast path: render fills and strokes only (skip shadows/blur).
|
||||
text::render(
|
||||
Some(self),
|
||||
@ -1396,7 +1426,7 @@ impl RenderState {
|
||||
antialias,
|
||||
outset,
|
||||
)?;
|
||||
if !fast_mode {
|
||||
if !skip_effects {
|
||||
for stroke in &visible_strokes {
|
||||
shadows::render_stroke_inner_shadows(
|
||||
self,
|
||||
@ -1409,7 +1439,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
if !fast_mode {
|
||||
if !skip_effects {
|
||||
shadows::render_fill_inner_shadows(
|
||||
self,
|
||||
shape,
|
||||
@ -1669,30 +1699,28 @@ impl RenderState {
|
||||
performance::begin_measure!("render");
|
||||
performance::begin_measure!("start_render_loop");
|
||||
|
||||
self.cache_cleared_this_render = false;
|
||||
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
|
||||
// Target is repainted tile-by-tile. If only a subset of the
|
||||
// invalidated tiles finishes in this rAF the remaining area
|
||||
// would either show stale content from the previous frame or,
|
||||
// on buffer swaps, show blank pixels — either way the user
|
||||
// perceives tiles appearing sequentially. Paint the persistent
|
||||
// 1:1 atlas as a stable backdrop so every flush presents a
|
||||
// coherent picture: unchanged tiles come from the atlas and
|
||||
// invalidated tiles are overwritten on top as they finish.
|
||||
if self.options.is_interactive_transform() && self.surfaces.has_atlas() {
|
||||
self.surfaces.draw_atlas_to_target(
|
||||
self.viewbox,
|
||||
self.options.dpr(),
|
||||
self.background_color,
|
||||
);
|
||||
self.cache_cleared_this_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
|
||||
// every rAF during drag (a common source of GPU work/stalls).
|
||||
self.surfaces
|
||||
.reset_interactive_transform(self.background_color);
|
||||
if !self.interactive_target_seeded {
|
||||
// Seed from the last presented frame; this is stable even when
|
||||
// fast_mode skips cache updates and regardless of atlas coverage.
|
||||
self.surfaces.seed_target_from_backbuffer();
|
||||
self.interactive_target_seeded = true;
|
||||
}
|
||||
} else {
|
||||
self.reset_canvas();
|
||||
self.interactive_target_seeded = false;
|
||||
}
|
||||
|
||||
let surface_ids = SurfaceId::Strokes as u32
|
||||
@ -1729,8 +1757,9 @@ impl RenderState {
|
||||
|
||||
let _tile_start = performance::begin_timed_log!("tile_cache_update");
|
||||
performance::begin_measure!("tile_cache");
|
||||
let only_visible = self.options.is_interactive_transform();
|
||||
self.pending_tiles
|
||||
.update(&self.tile_viewbox, &self.surfaces);
|
||||
.update(&self.tile_viewbox, &self.surfaces, only_visible);
|
||||
performance::end_measure!("tile_cache");
|
||||
performance::end_timed_log!("tile_cache_update", _tile_start);
|
||||
|
||||
@ -1815,20 +1844,16 @@ impl RenderState {
|
||||
}
|
||||
|
||||
// In a pure viewport interaction (pan/zoom), render_from_cache
|
||||
// owns the Target surface — don't flush Target so we don't
|
||||
// present stale tile positions. We still drain the GPU command
|
||||
// queue with a non-Target `flush_and_submit` so the backlog
|
||||
// of tile-render commands executes incrementally instead of
|
||||
// piling up for hundreds of milliseconds and blowing up the
|
||||
// next `render_from_cache` call into a multi-frame hitch.
|
||||
// owns the Target surface — skip flush so we don't present
|
||||
// stale tile positions. The rAF still populates the Cache
|
||||
// surface and tile HashMap so render_from_cache progressively
|
||||
// shows more complete content.
|
||||
//
|
||||
// During interactive shape transforms (drag/resize/rotate) we
|
||||
// still need to flush every rAF so the user sees the updated
|
||||
// shape position — render_from_cache is not in the loop here.
|
||||
if !self.options.is_viewport_interaction() {
|
||||
self.flush_and_submit();
|
||||
} else {
|
||||
self.gpu_state.context.flush_and_submit();
|
||||
}
|
||||
|
||||
if self.render_in_progress {
|
||||
@ -2030,7 +2055,7 @@ impl RenderState {
|
||||
paint.set_blend_mode(element.blend_mode().into());
|
||||
paint.set_alpha_f(element.opacity());
|
||||
|
||||
// Skip frame-level blur in fast mode (pan/zoom)
|
||||
// Skip frame-level blur in fast mode (pan/zoom).
|
||||
if !self.options.is_fast_mode() {
|
||||
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
||||
let scale = self.get_scale();
|
||||
@ -2549,7 +2574,8 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if let Some(clips) = clip_bounds.as_ref() {
|
||||
let antialias = element.should_use_antialias(scale, self.options.antialias_threshold);
|
||||
let antialias = !self.options.is_fast_mode()
|
||||
&& element.should_use_antialias(scale, self.options.antialias_threshold);
|
||||
self.surfaces.canvas(target_surface).save();
|
||||
for (bounds, corners, transform) in clips.iter() {
|
||||
if target_surface == SurfaceId::Export {
|
||||
@ -2753,7 +2779,7 @@ impl RenderState {
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
|
||||
// Skip expensive drop shadow rendering in fast mode (during pan/zoom)
|
||||
// Skip expensive drop shadow rendering in fast mode (during pan/zoom).
|
||||
let skip_shadows = self.options.is_fast_mode();
|
||||
|
||||
// Skip shadow block when already rendered before the layer (frame_clip_layer_blur)
|
||||
@ -2893,12 +2919,17 @@ impl RenderState {
|
||||
if let Some(current_tile) = self.current_tile {
|
||||
if self.surfaces.has_cached_tile_surface(current_tile) {
|
||||
performance::begin_measure!("render_shape_tree::cached");
|
||||
// During interactive transforms, `Target` is preserved and seeded once
|
||||
// from Backbuffer. Cached tiles are therefore already visible and
|
||||
// re-blitting them costs extra GPU work.
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
current_tile,
|
||||
tile_rect,
|
||||
self.background_color,
|
||||
);
|
||||
if !self.options.is_interactive_transform() {
|
||||
self.surfaces.draw_cached_tile_surface(
|
||||
current_tile,
|
||||
tile_rect,
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
|
||||
// Also draw the cached tile to the Cache surface so
|
||||
// render_from_cache (used during pan) has the full scene.
|
||||
@ -2936,8 +2967,19 @@ impl RenderState {
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::uncached");
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
if !is_empty {
|
||||
self.apply_render_to_final_canvas(tile_rect)?;
|
||||
// Composite if the walker did work in this PAF (`!is_empty`) OR
|
||||
// the tile has unfinished work from a previous PAF
|
||||
// (`current_tile_had_shapes` was set when we populated pending_nodes
|
||||
// for this tile).
|
||||
if !is_empty || self.current_tile_had_shapes {
|
||||
if self.options.is_interactive_transform() {
|
||||
// During drag, avoid snapshot-based caching. Draw Current directly
|
||||
// into Target (and Cache) to reduce stalls.
|
||||
self.surfaces
|
||||
.draw_current_tile_direct(&tile_rect, self.background_color);
|
||||
} else {
|
||||
self.apply_render_to_final_canvas(tile_rect)?;
|
||||
}
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
debug::render_workspace_current_tile(
|
||||
@ -2978,6 +3020,11 @@ impl RenderState {
|
||||
// let's check if there are more pending nodes
|
||||
if let Some(next_tile) = self.pending_tiles.pop() {
|
||||
self.update_render_context(next_tile);
|
||||
// Reset for the new tile. We'll flip it to true if the
|
||||
// tile has shapes, so a later "is_empty=true" reflects
|
||||
// a resumed-from-yield case rather than a genuinely
|
||||
// empty tile.
|
||||
self.current_tile_had_shapes = false;
|
||||
|
||||
if !self.surfaces.has_cached_tile_surface(next_tile) {
|
||||
if let Some(ids) = self.tiles.get_shapes_at(next_tile) {
|
||||
@ -2993,14 +3040,28 @@ impl RenderState {
|
||||
})
|
||||
});
|
||||
|
||||
// We only need first level shapes, in the same order as the parent node
|
||||
// We only need first level shapes, in the same order as the parent node.
|
||||
//
|
||||
// During interactive transforms we may invalidate only the modified shapes
|
||||
// (to avoid massive ancestor eviction). However, we still composite full
|
||||
// tiles (we clear the tile rect before drawing Current), so we must render
|
||||
// all root shapes that can contribute to this tile; otherwise, unchanged
|
||||
// siblings inside the same tile would disappear.
|
||||
let mut valid_ids = Vec::with_capacity(ids.len());
|
||||
for root_id in root_ids.iter() {
|
||||
if tile_has_bg_blur || ids.contains(root_id) {
|
||||
valid_ids.push(*root_id);
|
||||
if self.options.is_interactive_transform() || tile_has_bg_blur {
|
||||
valid_ids.extend(root_ids.iter().copied());
|
||||
} else {
|
||||
for root_id in root_ids.iter() {
|
||||
if ids.contains(root_id) {
|
||||
valid_ids.push(*root_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !valid_ids.is_empty() {
|
||||
self.current_tile_had_shapes = true;
|
||||
}
|
||||
|
||||
self.pending_nodes.extend(valid_ids.into_iter().map(|id| {
|
||||
NodeRenderState {
|
||||
id,
|
||||
@ -3336,8 +3397,17 @@ impl RenderState {
|
||||
tree: ShapesPoolMutRef<'_>,
|
||||
ids: Vec<Uuid>,
|
||||
) -> Result<()> {
|
||||
let ancestors = all_with_ancestors(&ids, tree, false);
|
||||
self.update_tiles_shapes(&ancestors, tree)?;
|
||||
// During interactive transform, skip ancestor invalidation: walking up to the
|
||||
// parent frame evicts every tile the frame covers, including dense tiles with
|
||||
// many siblings. Ancestor extrect caches are already invalidated by
|
||||
// `ShapesPool::set_modifiers`; the tile index is reconciled post-gesture by
|
||||
// the committing code path (rebuild_touched_tiles).
|
||||
if self.options.is_interactive_transform() {
|
||||
self.update_tiles_shapes(&ids, tree)?;
|
||||
} else {
|
||||
let ancestors = all_with_ancestors(&ids, tree, false);
|
||||
self.update_tiles_shapes(&ancestors, tree)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -42,6 +42,7 @@ pub enum SurfaceId {
|
||||
UI = 0b100_0000_0000,
|
||||
Debug = 0b100_0000_0001,
|
||||
Atlas = 0b100_0000_0010,
|
||||
Backbuffer = 0b100_0000_0100,
|
||||
}
|
||||
|
||||
pub struct Surfaces {
|
||||
@ -67,6 +68,8 @@ pub struct Surfaces {
|
||||
debug: skia::Surface,
|
||||
// for drawing tiles.
|
||||
export: skia::Surface,
|
||||
// Persistent viewport-sized surface used to keep the last presented frame.
|
||||
backbuffer: skia::Surface,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
// Persistent 1:1 document-space atlas that gets incrementally updated as tiles render.
|
||||
@ -112,6 +115,8 @@ impl Surfaces {
|
||||
let target = gpu_state.create_target_surface(width, height)?;
|
||||
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims)?;
|
||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?;
|
||||
let backbuffer =
|
||||
gpu_state.create_surface_with_dimensions("backbuffer".to_string(), width, height)?;
|
||||
let current =
|
||||
gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?;
|
||||
|
||||
@ -148,6 +153,7 @@ impl Surfaces {
|
||||
ui,
|
||||
debug,
|
||||
export,
|
||||
backbuffer,
|
||||
tiles,
|
||||
atlas,
|
||||
atlas_origin: skia::Point::new(0.0, 0.0),
|
||||
@ -399,10 +405,12 @@ impl Surfaces {
|
||||
|
||||
/// Draw the persistent atlas onto the target using the current viewbox transform.
|
||||
/// Intended for fast pan/zoom-out previews (avoids per-tile composition).
|
||||
/// Clears Target to `background` first so atlas-uncovered regions don't
|
||||
/// show stale content when the atlas only partially covers the viewport.
|
||||
pub fn draw_atlas_to_target(&mut self, viewbox: Viewbox, dpr: f32, background: skia::Color) {
|
||||
if !self.has_atlas() {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
let canvas = self.target.canvas();
|
||||
canvas.save();
|
||||
@ -413,11 +421,10 @@ impl Surfaces {
|
||||
None,
|
||||
true,
|
||||
);
|
||||
canvas.clear(background);
|
||||
|
||||
let s = viewbox.zoom * dpr;
|
||||
let atlas_scale = self.atlas_scale.max(0.01);
|
||||
|
||||
canvas.clear(background);
|
||||
canvas.translate((
|
||||
(self.atlas_origin.x + viewbox.pan_x) * s,
|
||||
(self.atlas_origin.y + viewbox.pan_y) * s,
|
||||
@ -581,6 +588,9 @@ impl Surfaces {
|
||||
if ids & SurfaceId::Cache as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Cache));
|
||||
}
|
||||
if ids & SurfaceId::Backbuffer as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Backbuffer));
|
||||
}
|
||||
if ids & SurfaceId::Fills as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Fills));
|
||||
}
|
||||
@ -655,6 +665,7 @@ impl Surfaces {
|
||||
SurfaceId::Target => &mut self.target,
|
||||
SurfaceId::Filter => &mut self.filter,
|
||||
SurfaceId::Cache => &mut self.cache,
|
||||
SurfaceId::Backbuffer => &mut self.backbuffer,
|
||||
SurfaceId::Current => &mut self.current,
|
||||
SurfaceId::DropShadows => &mut self.drop_shadows,
|
||||
SurfaceId::InnerShadows => &mut self.inner_shadows,
|
||||
@ -673,6 +684,7 @@ impl Surfaces {
|
||||
SurfaceId::Target => &self.target,
|
||||
SurfaceId::Filter => &self.filter,
|
||||
SurfaceId::Cache => &self.cache,
|
||||
SurfaceId::Backbuffer => &self.backbuffer,
|
||||
SurfaceId::Current => &self.current,
|
||||
SurfaceId::DropShadows => &self.drop_shadows,
|
||||
SurfaceId::InnerShadows => &self.inner_shadows,
|
||||
@ -686,6 +698,29 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy the current `Target` contents into the persistent `Backbuffer`.
|
||||
/// This is a GPU→GPU copy via Skia (no ReadPixels).
|
||||
pub fn copy_target_to_backbuffer(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.target.clone().draw(
|
||||
self.backbuffer.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Seed `Target` from `Backbuffer` (last presented frame).
|
||||
pub fn seed_target_from_backbuffer(&mut self) {
|
||||
let sampling_options = self.sampling_options;
|
||||
self.backbuffer.clone().draw(
|
||||
self.target.canvas(),
|
||||
(0.0, 0.0),
|
||||
sampling_options,
|
||||
Some(&skia::Paint::default()),
|
||||
);
|
||||
}
|
||||
|
||||
fn reset_from_target(&mut self, target: skia::Surface) -> Result<()> {
|
||||
let dim = (target.width(), target.height());
|
||||
self.target = target;
|
||||
@ -693,6 +728,10 @@ impl Surfaces {
|
||||
.target
|
||||
.new_surface_with_dimensions(dim)
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
self.backbuffer = self
|
||||
.target
|
||||
.new_surface_with_dimensions(dim)
|
||||
.ok_or(Error::CriticalError("Failed to create surface".to_string()))?;
|
||||
self.debug = self
|
||||
.target
|
||||
.new_surface_with_dimensions(dim)
|
||||
@ -849,6 +888,43 @@ impl Surfaces {
|
||||
self.clear_all_dirty();
|
||||
}
|
||||
|
||||
/// Reset render surfaces for interactive transforms without clearing `Target`.
|
||||
/// Keeping `Target` avoids having to redraw an atlas backdrop each frame; we
|
||||
/// then overwrite only the tiles that changed.
|
||||
pub fn reset_interactive_transform(&mut self, color: skia::Color) {
|
||||
self.canvas(SurfaceId::Fills).restore_to_count(1);
|
||||
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::TextDropShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::DropShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Strokes).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Current).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Export).restore_to_count(1);
|
||||
|
||||
// Clear tile-sized/intermediate surfaces; leave Target intact.
|
||||
self.apply_mut(
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::Current as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32
|
||||
| SurfaceId::DropShadows as u32
|
||||
| SurfaceId::Export as u32,
|
||||
|s| {
|
||||
s.canvas().clear(color).reset_matrix();
|
||||
},
|
||||
);
|
||||
|
||||
// UI/debug can be redrawn; clearing them is fine.
|
||||
self.canvas(SurfaceId::Debug)
|
||||
.clear(skia::Color::TRANSPARENT)
|
||||
.reset_matrix();
|
||||
self.canvas(SurfaceId::UI)
|
||||
.clear(skia::Color::TRANSPARENT)
|
||||
.reset_matrix();
|
||||
|
||||
self.clear_all_dirty();
|
||||
}
|
||||
|
||||
/// Clears the whole cache surface without disturbing its configured transform.
|
||||
pub fn clear_cache(&mut self, color: skia::Color) {
|
||||
let canvas = self.cache.canvas();
|
||||
@ -986,6 +1062,37 @@ impl Surfaces {
|
||||
);
|
||||
}
|
||||
|
||||
/// Same as `draw_current_tile_direct` but draws only into Target.
|
||||
/// Useful during interactive transforms to reduce extra GPU work.
|
||||
pub fn draw_current_tile_direct_target_only(
|
||||
&mut self,
|
||||
tile_rect: &skia::Rect,
|
||||
color: skia::Color,
|
||||
) {
|
||||
let sampling_options = self.sampling_options;
|
||||
let src_rect = IRect::from_xywh(
|
||||
self.margins.width,
|
||||
self.margins.height,
|
||||
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
|
||||
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||
);
|
||||
let src_rect_f = skia::Rect::from(src_rect);
|
||||
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
self.target.canvas().draw_rect(tile_rect, &paint);
|
||||
|
||||
self.current.clone().draw(
|
||||
self.target.canvas(),
|
||||
(
|
||||
tile_rect.left - src_rect_f.left,
|
||||
tile_rect.top - src_rect_f.top,
|
||||
),
|
||||
sampling_options,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
/// Full cache reset: clears both the tile texture cache and the cache canvas.
|
||||
/// Used by `rebuild_tiles` (full rebuild). For shallow rebuilds that preserve
|
||||
/// the cache canvas for scaled previews, use `invalidate_tile_cache` instead.
|
||||
|
||||
@ -195,7 +195,7 @@ pub struct Shape {
|
||||
pub shadows: Vec<Shadow>,
|
||||
pub layout_item: Option<LayoutItem>,
|
||||
pub bounds: OnceCell<math::Bounds>,
|
||||
pub extrect_cache: RefCell<Option<(math::Rect, u32)>>,
|
||||
pub extrect_cache: RefCell<Option<math::Rect>>,
|
||||
pub svg_transform: Option<Matrix>,
|
||||
pub ignore_constraints: bool,
|
||||
deleted: bool,
|
||||
@ -1015,17 +1015,13 @@ impl Shape {
|
||||
}
|
||||
|
||||
pub fn calculate_extrect(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect {
|
||||
let scale_key = (scale * 1000.0).round() as u32;
|
||||
|
||||
if let Some((cached_extrect, cached_scale)) = *self.extrect_cache.borrow() {
|
||||
if cached_scale == scale_key {
|
||||
return cached_extrect;
|
||||
}
|
||||
if let Some(cached_extrect) = *self.extrect_cache.borrow() {
|
||||
return cached_extrect;
|
||||
}
|
||||
|
||||
let extrect = self.calculate_extrect_uncached(shapes_pool, scale);
|
||||
|
||||
*self.extrect_cache.borrow_mut() = Some((extrect, scale_key));
|
||||
*self.extrect_cache.borrow_mut() = Some(extrect);
|
||||
extrect
|
||||
}
|
||||
|
||||
|
||||
@ -309,6 +309,24 @@ impl ShapesPoolImpl {
|
||||
modified_uuids
|
||||
}
|
||||
|
||||
/// UUIDs of all shapes that currently have a transform modifier.
|
||||
/// Used by the throttled drag path so per-rAF tile invalidation can
|
||||
/// be done once with the current modifier set instead of once per
|
||||
/// pointer move.
|
||||
pub fn modifier_ids(&self) -> Vec<Uuid> {
|
||||
if self.modifiers.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut idx_to_uuid: HashMap<usize, Uuid> = HashMap::with_capacity(self.uuid_to_idx.len());
|
||||
for (uuid, idx) in self.uuid_to_idx.iter() {
|
||||
idx_to_uuid.insert(*idx, *uuid);
|
||||
}
|
||||
self.modifiers
|
||||
.keys()
|
||||
.filter_map(|idx| idx_to_uuid.get(idx).copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl {
|
||||
let Some(shape) = self.get(id) else {
|
||||
panic!("Subtree not found");
|
||||
|
||||
@ -86,6 +86,10 @@ impl TileViewbox {
|
||||
self.center = get_tile_center_for_viewbox(viewbox, scale);
|
||||
}
|
||||
|
||||
pub fn set_interest(&mut self, interest: i32) {
|
||||
self.interest = interest;
|
||||
}
|
||||
|
||||
pub fn is_visible(&self, tile: &Tile) -> bool {
|
||||
// TO CHECK self.interest_rect.contains(tile)
|
||||
self.visible_rect.contains(tile)
|
||||
@ -261,11 +265,20 @@ impl PendingTiles {
|
||||
result
|
||||
}
|
||||
|
||||
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces) {
|
||||
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces, only_visible: bool) {
|
||||
self.list.clear();
|
||||
|
||||
// Generate spiral for the interest area (viewport + margin)
|
||||
let spiral = Self::generate_spiral(&tile_viewbox.interest_rect);
|
||||
// During interactive transform, skip the interest-area ring
|
||||
// entirely — the user is dragging, every rAF is on the critical
|
||||
// path, and pre-rendering tiles outside the viewport is wasted
|
||||
// work that just gets evicted on the next pointer move. The ring
|
||||
// is repopulated naturally on gesture end / on idle rAFs.
|
||||
let spiral_rect = if only_visible {
|
||||
&tile_viewbox.visible_rect
|
||||
} else {
|
||||
&tile_viewbox.interest_rect
|
||||
};
|
||||
let spiral = Self::generate_spiral(spiral_rect);
|
||||
|
||||
// Partition tiles into 4 priority groups (highest priority = processed last due to pop()):
|
||||
// 1. visible + cached (fastest - just blit from cache)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user