diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index 7cefd6ee..2b7111b4 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -754,6 +754,10 @@ impl Renderer { /// What [`Self::draw_object_fx`] draws inside the object's transparency group. fn draw_object_fx_art(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc, cache: bool) { let art = self.fx_art(f.doc, a, cache); + // Art the cache did not keep is made again in the next frame. + let kept = cache && self.fx_arts.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, &art)); + let outer = self.made_again; + self.made_again = outer || !kept; // With Crop Marks, the object inside the art paints its own raster effects. let rfx = if effects::has_crop_marks(a) { vec![] } else { effects::raster_effects(&a.appearance.effects) }; match cull_bounds(&art) { @@ -769,6 +773,7 @@ impl Renderer { } _ => self.draw_node(ctx, f, &art, true), } + self.made_again = outer; } /// The art [`Self::draw_object_fx`] draws for `a` (cached by `Arc` identity when `cache`): diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index cf09ef8a..c1bc19df 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -395,11 +395,15 @@ pub struct Renderer { /// Worker threads for the multithreaded rasterizer (0 = single-threaded). pub threads: u16, geom: PtrMap, - /// Expanded stroke outlines keyed by (stroke layer address, width bits, tolerance level): - /// panning re-renders reuse them instead of re-expanding every stroke. - strokes: PtrMap<(usize, u64, i32), StrokeEntry>, + /// Expanded stroke outlines keyed by ([`Self::stroke_owner`], stroke layer address, width bits, + /// tolerance level): panning re-renders reuse them instead of re-expanding every stroke. + strokes: PtrMap<(usize, usize, u64, i32), StrokeEntry>, /// The node being drawn on the plain-path fast path (owner of cached stroke outlines). cur: Option>, + /// Whether the art being drawn is made again in every frame: symbol art that + /// [`Self::placed_art`] did not keep, and evaluations of live objects and effect art that their + /// caches did not keep. + made_again: bool, /// The node `draw_shape_fx` may cache the live-effect geometry of (the one `draw_arc` is drawing). fx_owner: Option>, /// Live-effect geometry per node ([`fx::FxGeom`]), keyed by `Arc` identity like `geom`. @@ -445,8 +449,9 @@ pub struct Renderer { clip_paths: Vec<(BezPath, FillRule, Affine)>, /// What clipping paths paint, per clipping path (see [`Self::clip_paint_of`]). clip_paints: FrameCache, - /// Gradients along or across strokes, keyed like `strokes` (see [`Self::fill_path_gradient`]). - stroke_slices: PtrMap<(usize, i32), SliceEntry>, + /// Gradients along or across strokes (see [`Self::fill_path_gradient`]), keyed by + /// ([`Self::stroke_owner`], stroke layer address, tolerance level). + stroke_slices: PtrMap<(usize, usize, i32), SliceEntry>, /// What the image keys colour adjustments give their recoloured images name: the image /// recoloured and the adjustment, with its hash (see [`Self::adjusted_image`]). adjusts: HashMap, @@ -529,6 +534,7 @@ impl Renderer { geom: PtrMap::default(), strokes: PtrMap::default(), cur: None, + made_again: false, fx_owner: None, fx_geom: PtrMap::default(), stamp: 0, @@ -1190,8 +1196,10 @@ impl Renderer { NodeKind::Image(im) => self.draw_image(ctx, f, im), NodeKind::SymbolInstance { symbol, xf } => { if let Some(sym) = f.doc.symbols.iter().find(|s| &s.name == symbol) { - let art = self.placed_art(&sym.art, *xf, false, Some(n)); + let (art, kept) = self.placed_art(&sym.art, *xf, false, Some(n)); + let outer = std::mem::replace(&mut self.made_again, !kept); self.draw_node(ctx, f, &art, true); + self.made_again = outer; } } NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) | NodeKind::PlacedDocument(_) => { @@ -1443,7 +1451,7 @@ impl Renderer { }; let slices = match self.cur.clone() { Some(node) => { - let key = (st as *const StrokeLayer as usize, level); + let key = (self.stroke_owner(&node), st as *const StrokeLayer as usize, level); match self.stroke_slices.get_mut(&key) { Some(e) if Arc::ptr_eq(&e.node, &node) => { e.stamp = self.stamp; @@ -1481,13 +1489,13 @@ impl Renderer { } /// The line part of a stroke of the fast-path node `self.cur` (see - /// [`effects::stroke::line_outline`]), cached across frames. The tolerance (vello's 0.25 - /// device px) is bucketed to powers of two, never coarser than needed, so zooming reuses - /// outlines until the level changes. + /// [`effects::stroke::line_outline`]), cached across frames under [`Self::stroke_owner`] and + /// the stroke layer. The tolerance (vello's 0.25 device px) is bucketed to powers of two, never + /// coarser than needed, so zooming reuses outlines until the level changes. fn cached_stroke(&mut self, f: &Frame, line: &BezPath, st: &StrokeLayer, width: f64) -> Option> { let node = self.cur.clone()?; let level = (f.px * 0.25).log2().floor() as i32; - let key = (st as *const StrokeLayer as usize, width.to_bits(), level); + let key = (self.stroke_owner(&node), st as *const StrokeLayer as usize, width.to_bits(), level); if let Some(e) = self.strokes.get_mut(&key) && Arc::ptr_eq(&e.node, &node) { @@ -1499,6 +1507,12 @@ impl Renderer { Some(outline) } + /// What the `strokes` and `stroke_slices` keys hold for the fast-path node `node`: its address, + /// or 0 while art made again in every frame is drawn ([`Self::made_again`]). + fn stroke_owner(&self, node: &Arc) -> usize { + if self.made_again { 0 } else { Arc::as_ptr(node) as usize } + } + fn draw_text(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, t: &TextObject) { let t = &*f.doc.inline_resolved(t); let g = text_geom_snapped(t, f.text_snap(t)); @@ -1525,23 +1539,26 @@ impl Renderer { let Some(art) = t.runs.get(ig.run).and_then(|r| r.inline.as_ref()) else { continue }; let Some(sym) = f.doc.symbols.iter().find(|s| s.name == art.symbol) else { continue }; // Strokes scale with the art (it is sized to the type, as in the SVG ``). - let node = self.placed_art(&sym.art, t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true, None); + let (node, kept) = self.placed_art(&sym.art, t.xf * ig.xf * f.doc.symbol_natural_xf(&art.symbol), true, None); + let outer = std::mem::replace(&mut self.made_again, !kept); self.draw_node(ctx, f, &node, true); + self.made_again = outer; } self.inline_depth -= 1; } - /// Symbol art `art` as an instance or an inline graphic draws it ([`place`]). Art that is a - /// group or a layer without an appearance of its own is kept between frames under the art's - /// `Arc`, `xf`, `scale_strokes` and the stain of instance `inst`: its members keep their `Arc`s - /// from frame to frame, and the caches keyed by a node's address (geometry, glyphs, live-effect - /// geometry) hit for them. Other art is placed again in every frame, and so is art that holds - /// symbol instances, type with inline graphics, images, placed documents, live blends, - /// envelopes or repeats, or raster effects ([`depends_on_document`]). Art that holds type is + /// Symbol art `art` as an instance or an inline graphic draws it ([`place`]), and whether it is + /// kept. Art that is a group or a layer without an appearance of its own is kept between frames + /// under the art's `Arc`, `xf`, `scale_strokes` and the stain of instance `inst`: its members + /// keep their `Arc`s from frame to frame, and the caches keyed by a node's address (geometry, + /// stroke outlines, glyphs, live-effect geometry) hit for them. Other art is placed again in + /// every frame, and so is art that holds symbol instances, type with inline graphics, images, + /// placed documents, live blends, envelopes or repeats, or raster effects + /// ([`depends_on_document`]), and art that the cache has no room for. Art that holds type is /// placed again when the fonts change. - fn placed_art(&mut self, art: &Arc, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> Arc { + fn placed_art(&mut self, art: &Arc, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc, bool) { if !matches!(art.kind, NodeKind::Group { .. } | NodeKind::Layer { .. }) || fx::has_object_fx(art) { - return place(art, xf, scale_strokes, inst); + return (place(art, xf, scale_strokes, inst), false); } let stain = stain_bits(inst.and_then(vectorcraft_brush::stain)); let key = (Arc::as_ptr(art) as usize, xf.as_coeffs().map(f64::to_bits), scale_strokes, stain); @@ -1550,18 +1567,19 @@ impl Renderer { && e.fonts.is_none_or(|g| g == vectorcraft_text::FontDb::global().generation()) { return match &e.placed { - Some(placed) => placed.clone(), - None => place(art, xf, scale_strokes, inst), + Some(placed) => (placed.clone(), true), + None => (place(art, xf, scale_strokes, inst), false), }; } let placed = place(art, xf, scale_strokes, inst); let kept = (!depends_on_document(art)).then(|| placed.clone()); - let fonts = (kept.is_some() && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation()); + let keeps = kept.is_some(); + let fonts = (keeps && holds_type(art)).then(|| vectorcraft_text::FontDb::global().generation()); // The bytes count the key and the entry, the art's allocation that the weak reference // keeps, and the kept art. let bytes = size_of::<(PlacedKey, PlacedEntry, Node)>() + kept.as_deref().map_or(0, lru::node_bytes); - self.placed_arts.insert(key, PlacedEntry { art: Arc::downgrade(art), placed: kept, fonts }, bytes, self.stamp); - placed + let inserted = self.placed_arts.insert(key, PlacedEntry { art: Arc::downgrade(art), placed: kept, fonts }, bytes, self.stamp); + (placed, keeps && inserted) } /// Cached glyph geometry for a text node (keyed by Arc identity like paths). diff --git a/crates/render/src/live.rs b/crates/render/src/live.rs index 2b35b752..d317394e 100644 --- a/crates/render/src/live.rs +++ b/crates/render/src/live.rs @@ -42,6 +42,12 @@ impl LiveCache { self.expanded.sweep(stamp); self.meshes.sweep(stamp); } + + /// A cache whose evaluations take at most `budget` bytes. + #[cfg(test)] + pub(crate) fn with_budget(budget: usize) -> Self { + Self { expanded: FrameCache::new(budget, 256), ..Default::default() } + } } /// One level of evaluation of a live node (blend steps, envelope result with type outlined run by @@ -146,6 +152,10 @@ impl Renderer { NodeKind::Mesh(m) => self.draw_mesh(ctx, f, a, m, cache), NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Repeat(_) | NodeKind::Group { .. } | NodeKind::Layer { .. } => { let items = self.live_expanded(f.doc, a, cache); + // An evaluation the cache did not keep is made again in the next frame. + let kept = cache && self.live.expanded.get(&(Arc::as_ptr(a) as usize), self.stamp).is_some_and(|e| Arc::ptr_eq(&e.1, &items)); + let outer = self.made_again; + self.made_again = outer || !kept; // The flag holds while this blend is drawn (see `draw_live`). if self.knockout && matches!(a.kind, NodeKind::Blend { .. }) { self.draw_knockout(ctx, f, &items); @@ -154,6 +164,7 @@ impl Renderer { self.draw_arc(ctx, f, c); } } + self.made_again = outer; } _ => {} } diff --git a/crates/render/src/tests_memory.rs b/crates/render/src/tests_memory.rs index b218a15b..46e74997 100644 --- a/crates/render/src/tests_memory.rs +++ b/crates/render/src/tests_memory.rs @@ -1,7 +1,7 @@ //! What renderers keep between frames: decoded images shared by all of them, image mips, symbol -//! art placed for instances and inline graphics, raster effects made again when what they read -//! changes, and caches swept by every kind of render. Each test holds [`fonts_steady`]: fonts added -//! while it runs would make its renderers lay type out again. +//! art placed for instances and inline graphics, stroke outlines, raster effects made again when +//! what they read changes, and caches swept by every kind of render. Each test holds [`fonts_steady`]: +//! fonts added while it runs would make its renderers lay type out again. use vectorcraft_color::{Color, Paint}; use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind}; @@ -244,6 +244,187 @@ fn symbol_instances_keep_their_placed_art_between_frames() { assert_eq!(Arc::strong_count(&d.symbols[0].art), 1, "only the document holds the symbol's art"); } +/// The stroke outlines and the gradient slices of strokes that `r` keeps and that its last frame made +/// or used. +fn stroke_outlines(r: &Renderer) -> (Vec>, Vec) { + let outlines = r.strokes.values().filter(|e| e.stamp == r.stamp).map(|e| e.outline.clone()).collect(); + (outlines, r.stroke_slices.values().filter(|e| e.stamp == r.stamp).map(|e| e.slices.clone()).collect()) +} + +/// How many of the outlines and of the slices in `before` (see [`stroke_outlines`]) the last frame of +/// `r` used again. +fn used_again(r: &Renderer, before: &(Vec>, Vec)) -> (usize, usize) { + fn again(was: &[Arc], now: &[Arc]) -> usize { + was.iter().filter(|a| now.iter().any(|b| Arc::ptr_eq(a, b))).count() + } + let (outlines, slices) = stroke_outlines(r); + (again(&before.0, &outlines), again(&before.1, &slices)) +} + +/// Copies can share the original's appearance: three copies of [`symbol_art`] made as +/// `edit.duplicate` makes them, and a wave whose stroke has a gradient along it with a copy made as +/// the Layers panel's Duplicate Selection makes it. Each object keeps its own stroke outlines and +/// gradient slices from frame to frame, a copy moved gets new ones while the others keep theirs, +/// and each frame draws what a new renderer draws. +#[test] +fn copies_sharing_an_appearance_keep_their_own_stroke_outlines() { + use vectorcraft_doc::{Appearance, StrokeGradientMode}; + let mut d = Document::new(200.0, 100.0); + let art = symbol_art(&mut d); + let l = d.layers[0].id; + let mut copies = vec![]; + for x in [30.0, 80.0, 130.0] { + let mut copy = d.reid(&art); + copy.transform(Affine::translate((x, 30.0)), true); + copies.push(d.insert(Some(l), usize::MAX, copy).unwrap()); + } + let mut st = StrokeLayer::new(Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(Default::default()))), 6.0); + st.gradient_mode = StrokeGradientMode::Along; + let mut wave = BezPath::new(); + wave.move_to((20.0, 75.0)); + wave.curve_to((60.0, 55.0), (100.0, 95.0), (180.0, 75.0)); + let ap = Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() }; + let wave = Node::path(d.alloc_id(), vectorcraft_geom::PathData::from_bezpath(&wave), ap); + let duplicate = d.reid(&wave); + d.insert(Some(l), usize::MAX, wave).unwrap(); + d.insert(Some(l), usize::MAX, duplicate).unwrap(); + let opts = RenderOptions::default(); + let frame = |r: &mut Renderer, d: &Document| r.render(d, 200, 100, Affine::IDENTITY, &opts).pixels; + let mut r = Renderer::new(); + frame(&mut r, &d); + let before = stroke_outlines(&r); + assert_eq!((before.0.len(), before.1.len()), (8, 2), "the strokes of three copies and two waves, and the waves' slices"); + assert!(frame(&mut r, &d) == frame(&mut Renderer::new(), &d)); + assert_eq!(used_again(&r, &before), (8, 2), "the second frame draws the outlines the first made"); + // The first copy moves 5 pt right. + if let Some(n) = d.node_mut(copies[0]) { + n.transform(Affine::translate((5.0, 0.0)), true); + } + assert!(frame(&mut r, &d) == frame(&mut Renderer::new(), &d)); + assert_eq!(used_again(&r, &before), (6, 2), "the other copies and the waves keep theirs"); +} + +/// The art placed for unstained symbol instances shares its appearance with the symbol's art (the +/// first and the third instance of [`symbol_doc`]). Each instance keeps its own stroke outlines from +/// frame to frame, and the second frame draws what a new renderer draws. +#[test] +fn symbol_instances_keep_their_own_stroke_outlines_between_frames() { + let d = symbol_doc(); + let opts = RenderOptions::default(); + let frame = |r: &mut Renderer| r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels; + let mut r = Renderer::new(); + frame(&mut r); + let before = stroke_outlines(&r); + assert_eq!(before.0.len(), 6, "the square's and the line's strokes of three instances"); + assert!(frame(&mut r) == frame(&mut Renderer::new())); + assert_eq!(used_again(&r, &before), (6, 0), "the second frame draws the outlines the first made"); +} + +/// The art of symbol `I` ([`symbol_art`] with an image over it) is placed again in every frame. +/// The copies placed for its three unstained instances and for an inline graphic at the art's own +/// size replace each other's stroke outlines: the renderer keeps one outline for each of the art's +/// two strokes. Two copies of [`symbol_art`] made as `edit.duplicate` makes them and two instances +/// of symbol `S` ([`symbol_art`]), drawn after those, keep their own outlines. With no room in the +/// placed-art cache, `S`'s art is placed again in every frame as well, and its copies keep one +/// outline for each stroke. Each frame draws what a new renderer draws. +#[test] +fn art_placed_again_in_every_frame_keeps_one_outline_per_stroke() { + use vectorcraft_doc::text::{CharStyle, InlineArt, TextObject, TextRun}; + let mut d = Document::new(200.0, 100.0); + d.images.insert("dot".into(), ImageBlob::new("image/png", png(4, 4, 108))); + let mut art = symbol_art(&mut d); + let im = ImageObject { key: "dot".into(), width: 4, height: 4, xf: Affine::translate((-2.0, -2.0)), link: None, placement: Default::default() }; + let image = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im))); + if let Some(c) = art.children_mut() { + c.push(Arc::new(image)); + } + d.symbols.push(vectorcraft_doc::Symbol { name: "I".into(), art: Arc::new(art) }); + let art = symbol_art(&mut d); + d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: Arc::new(art) }); + let l = d.layers[0].id; + let instance = |d: &mut Document, symbol: &str, x: f64, y: f64| { + let id = d.alloc_id(); + d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: symbol.into(), xf: Affine::translate((x, y)) })).unwrap(); + }; + for x in [25.0, 65.0, 105.0] { + instance(&mut d, "I", x, 25.0); + } + // Type as tall as `I`'s art shows the inline graphic at scale 1. + let size = d.symbol_art_bounds("I").map_or(0.0, |b| b.height()); + let style = CharStyle { size, ..Default::default() }; + let mut t = TextObject::point(vectorcraft_geom::Point::new(140.0, 32.0), "A", style.clone()); + t.runs.push(TextRun::inline(InlineArt::new("I"), style)); + let id = d.alloc_id(); + d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap(); + let art = symbol_art(&mut d); + for x in [105.0, 145.0] { + let mut copy = d.reid(&art); + copy.transform(Affine::translate((x, 72.0)), true); + d.insert(Some(l), usize::MAX, copy).unwrap(); + } + for x in [25.0, 65.0] { + instance(&mut d, "S", x, 72.0); + } + let opts = RenderOptions::default(); + let frame = |r: &mut Renderer| r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels; + let counts = |r: &Renderer| (r.strokes.len(), r.stroke_slices.len()); + let mut r = Renderer::new(); + let mut full = Renderer::new(); + full.placed_arts = FrameCache::new(0, 0); + let mut before = (vec![], vec![]); + for n in 1..=4 { + let want = frame(&mut Renderer::new()); + assert!(frame(&mut r) == want && frame(&mut full) == want, "frame {n}"); + // Two outlines for the art placed for `I`, two for each copy and two for each instance of + // `S`; with no room in the cache, two for the art placed for `S`. + assert_eq!((counts(&r), counts(&full)), ((10, 0), (8, 0)), "frame {n}"); + if n == 1 { + before = stroke_outlines(&r); + } + } + assert_eq!(used_again(&r, &before), (8, 0), "the instances of `S` and the copies keep theirs"); +} + +/// The document holds a Grid Repeat of [`symbol_art`] (nine groups) and a group with a Transform +/// effect around two copies of [`symbol_art`] made as `edit.duplicate` makes them. With no room in +/// the caches of evaluated live objects and of effect art, the repeat is evaluated again and the +/// group's art is made again in every frame. Their copies then replace each other's stroke +/// outlines: the renderer keeps two outlines for the repeat and two for the copies. With room, each +/// copy keeps its own. Each frame draws what a new renderer draws. +#[test] +fn art_evaluated_again_in_every_frame_keeps_one_outline_per_stroke() { + let mut d = Document::new(200.0, 100.0); + let l = d.layers[0].id; + let mut source = symbol_art(&mut d); + source.transform(Affine::translate((20.0, 20.0)), true); + let repeat = vectorcraft_doc::pattern::RepeatSpec::grid(vec![Arc::new(source)], 4.0, 4.0); + let id = d.alloc_id(); + d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::Repeat(repeat))).unwrap(); + let art = symbol_art(&mut d); + let mut copies = vec![]; + for x in [130.0, 170.0] { + let mut copy = d.reid(&art); + copy.transform(Affine::translate((x, 30.0)), true); + copies.push(Arc::new(copy)); + } + let mut moved = Node::group(d.alloc_id(), copies); + moved.appearance.effects.push(effect("distort.transform", serde_json::json!({"moveV": 30.0}))); + d.insert(Some(l), usize::MAX, moved).unwrap(); + let opts = RenderOptions::default(); + let frame = |r: &mut Renderer| r.render(&d, 200, 100, Affine::IDENTITY, &opts).pixels; + let mut r = Renderer::new(); + let mut full = Renderer::new(); + full.live = live::LiveCache::with_budget(0); + full.fx_arts = FrameCache::new(0, 0); + for n in 1..=4 { + let want = frame(&mut Renderer::new()); + assert!(frame(&mut r) == want && frame(&mut full) == want, "frame {n}"); + // Two outlines for each of the nine repeated groups and each copy; with no room in the + // caches, two for the repeat and two for the copies. + assert_eq!((r.strokes.len(), full.strokes.len()), (22, 4), "frame {n}"); + } +} + /// Moving an instance places its art again, and editing a symbol's art places the art of its /// instances again: the other instances keep theirs, and each frame draws what a new renderer /// draws.