This commit is contained in:
Alejandro Alonso 2026-04-22 17:51:53 +02:00
parent d549be3376
commit bc8ca5f2a7
3 changed files with 162 additions and 14 deletions

View File

@ -456,6 +456,7 @@ pub extern "C" fn set_modifiers_start() -> Result<()> {
let opts = &mut state.render_state.options;
opts.set_fast_mode(true);
opts.set_interactive_transform(true);
state.render_state.clear_picture_cache();
performance::end_measure!("set_modifiers_start");
});
Ok(())
@ -473,6 +474,7 @@ pub extern "C" fn set_modifiers_end() -> Result<()> {
let opts = &mut state.render_state.options;
opts.set_fast_mode(false);
opts.set_interactive_transform(false);
state.render_state.clear_picture_cache();
state.render_state.cancel_animation_frame();
performance::end_measure!("set_modifiers_end");
});

View File

@ -15,7 +15,7 @@ mod ui;
use skia_safe::{self as skia, Matrix, RRect, Rect};
use std::borrow::Cow;
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use gpu_state::GpuState;
@ -334,6 +334,9 @@ 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,
/// Per-gesture cache of recorded draw commands for leaf shapes.
/// Used to speed up interactive transforms by replaying commands with a modifier matrix.
picture_cache: HashMap<Uuid, skia::Picture>,
}
pub fn get_cache_size(viewbox: Viewbox, scale: f32, interest: i32) -> skia::ISize {
@ -407,9 +410,14 @@ impl RenderState {
preview_mode: false,
export_context: None,
cache_cleared_this_render: false,
picture_cache: HashMap::new(),
})
}
pub fn clear_picture_cache(&mut self) {
self.picture_cache.clear();
}
/// Combines every visible layer blur currently active (ancestors + shape)
/// into a single equivalent blur. Layer blur radii compound by adding their
/// variances (σ² = radius²), so we:
@ -1674,6 +1682,15 @@ impl RenderState {
self.cache_cleared_this_render = false;
self.reset_canvas();
// `picture_cache` is per-gesture and is cleared from the WASM exports
// (`set_modifiers_start`/`set_modifiers_end`). Keep this method side-effect free.
// 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
@ -1767,6 +1784,64 @@ impl RenderState {
Ok(())
}
fn get_or_record_simple_rect_picture(
&mut self,
shape_id: &Uuid,
base_shape: &Shape,
fill: &Fill,
antialias: bool,
) -> Option<skia::Picture> {
if let Some(picture) = self.picture_cache.get(shape_id) {
return Some(picture.clone());
}
let bounds = base_shape.selrect();
if bounds.is_empty() {
return None;
}
let mut recorder = skia::PictureRecorder::new();
let canvas = recorder.begin_recording(bounds, false);
let mut paint = fill.to_paint(&bounds, antialias);
canvas.draw_rect(bounds, &paint);
drop(paint);
let picture = recorder.finish_recording_as_picture(None)?;
self.picture_cache.insert(*shape_id, picture.clone());
Some(picture)
}
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(
&mut self,
base_object: Option<&Uuid>,
@ -2741,19 +2816,76 @@ impl RenderState {
.draw_into(SurfaceId::DropShadows, target_surface, None);
}
self.render_shape(
element,
clip_bounds.clone(),
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::InnerShadows,
SurfaceId::TextDropShadows,
true,
None,
None,
None,
target_surface,
)?;
// Interactive-transform fast path: replay a recorded Picture for very simple
// leaf rects, applying only the modifier matrix per frame.
let mut rendered_via_picture = false;
if self.options.is_interactive_transform()
&& !element.is_recursive()
&& clip_bounds.is_none()
&& element.transform.is_identity()
&& element.opacity() == 1.0
&& element.shadows.is_empty()
&& element.blur.is_none()
&& element.strokes.is_empty()
&& matches!(element.shape_type, Type::Rect(_))
&& element.fills.len() == 1
&& matches!(element.fills[0], Fill::Solid(_))
&& target_surface != SurfaceId::Export
{
if let (Some(modifier), Some(base_shape)) =
(tree.get_modifier(&node_id), tree.get_base(&node_id))
{
if base_shape.transform.is_identity()
&& base_shape.fills.len() == 1
&& matches!(base_shape.fills[0], Fill::Solid(_))
{
let antialias = base_shape.should_use_antialias(
self.get_scale(),
self.options.antialias_threshold,
);
if let Some(picture) = self.get_or_record_simple_rect_picture(
&node_id,
base_shape,
&base_shape.fills[0],
antialias,
) {
println!("draw picture");
let picture = picture.clone();
let scale = self.get_scale();
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
self.surfaces.apply_mut(target_surface as u32, |s| {
let canvas = s.canvas();
canvas.save();
canvas.scale((scale, scale));
canvas.translate(translation);
canvas.concat(modifier);
canvas.draw_picture(&picture, None, None);
canvas.restore();
});
rendered_via_picture = true;
}
}
}
}
if !rendered_via_picture {
println!("render_shape");
self.render_shape(
element,
clip_bounds.clone(),
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::InnerShadows,
SurfaceId::TextDropShadows,
true,
None,
None,
None,
target_surface,
)?;
}
self.surfaces
.canvas(SurfaceId::DropShadows)

View File

@ -177,6 +177,20 @@ impl ShapesPoolImpl {
}
}
/// Returns the base (unmodified) shape as stored in the pool.
///
/// Unlike `get`, this does **not** apply transient modifiers/structure/scale_content.
pub fn get_base(&self, id: &Uuid) -> Option<&Shape> {
let idx = *self.uuid_to_idx.get(id)?;
Some(&self.shapes[idx])
}
/// Returns the current transient modifier matrix for `id` if present.
pub fn get_modifier(&self, id: &Uuid) -> Option<&skia::Matrix> {
let idx = *self.uuid_to_idx.get(id)?;
self.modifiers.get(&idx)
}
// Given an id, returns the depth in the tree-shaped structure
// of shapes.
pub fn get_depth(&self, id: &Uuid) -> usize {