mirror of
https://github.com/penpot/penpot.git
synced 2026-04-29 21:28:20 +00:00
⚡ Improve drag performance
This commit is contained in:
parent
0f65774ba9
commit
483ce8b1c9
@ -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,8 @@ 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);
|
||||
performance::end_measure!("set_modifiers_start");
|
||||
});
|
||||
Ok(())
|
||||
@ -470,9 +478,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 +952,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 +984,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,13 @@ 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,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize {
|
||||
@ -407,6 +414,7 @@ impl RenderState {
|
||||
preview_mode: false,
|
||||
export_context: None,
|
||||
cache_cleared_this_render: false,
|
||||
current_tile_had_shapes: false,
|
||||
})
|
||||
}
|
||||
|
||||
@ -624,6 +632,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) {
|
||||
@ -867,7 +879,7 @@ impl RenderState {
|
||||
|
||||
let antialias =
|
||||
shape.should_use_antialias(self.get_scale(), self.options.antialias_threshold);
|
||||
let fast_mode = self.options.is_fast_mode();
|
||||
let skip_effects = self.options.is_fast_mode();
|
||||
let has_nested_fills = self
|
||||
.nested_fills
|
||||
.last()
|
||||
@ -995,7 +1007,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 +1033,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 +1119,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 +1408,7 @@ impl RenderState {
|
||||
antialias,
|
||||
outset,
|
||||
)?;
|
||||
if !fast_mode {
|
||||
if !skip_effects {
|
||||
for stroke in &visible_strokes {
|
||||
shadows::render_stroke_inner_shadows(
|
||||
self,
|
||||
@ -1409,7 +1421,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
if !fast_mode {
|
||||
if !skip_effects {
|
||||
shadows::render_fill_inner_shadows(
|
||||
self,
|
||||
shape,
|
||||
@ -2030,7 +2042,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();
|
||||
@ -2753,7 +2765,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)
|
||||
@ -2936,7 +2948,17 @@ impl RenderState {
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::uncached");
|
||||
let tile_rect = self.get_current_tile_bounds()?;
|
||||
if !is_empty {
|
||||
// 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).
|
||||
// The explicit clear is reserved for tiles that
|
||||
// genuinely have no shapes assigned to them —
|
||||
// without this distinction, chunked-render
|
||||
// resumption was painting completed tiles back to
|
||||
// background, producing the disappearing-tile
|
||||
// flicker during drag.
|
||||
if !is_empty || self.current_tile_had_shapes {
|
||||
self.apply_render_to_final_canvas(tile_rect)?;
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
@ -2953,7 +2975,6 @@ impl RenderState {
|
||||
paint.set_color(self.background_color);
|
||||
s.canvas().draw_rect(tile_rect, &paint);
|
||||
});
|
||||
// Keep Cache surface coherent for render_from_cache.
|
||||
if !self.options.is_fast_mode() {
|
||||
if !self.cache_cleared_this_render {
|
||||
self.surfaces.clear_cache(self.background_color);
|
||||
@ -2978,6 +2999,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) {
|
||||
@ -3001,6 +3027,9 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
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 +3365,19 @@ 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 hundreds of siblings. The anti-flicker
|
||||
// guard then forces all of them to re-render in a single frame.
|
||||
// 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(())
|
||||
}
|
||||
|
||||
|
||||
@ -399,10 +399,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 +415,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,
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user