From dfbc1ee866d602cfc06e2b38cdbd323b1f63928f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 01:19:02 -0700 Subject: [PATCH] Render: brush art is made again when a symbol or pattern it can show changes Brush art leaves out the symbol instances and pattern paints whose art draws a brushed stroke. The brush cache now records whether a brush's art or the stroke's paint can show a symbol or a pattern, and drops those entries when the art of the document's symbols or patterns changes, so an edit that makes a symbol or pattern start or stop drawing a brushed stroke draws as a new renderer does. The parity line gives the median and fastest-frame figures of the benchmark for both documents it names. --- crates/render/src/brush_fx.rs | 43 ++++++++++++++++++++--- crates/render/src/fx.rs | 5 +++ crates/render/src/tests_memory.rs | 58 +++++++++++++++++++++++++++++++ docs/target-app-parity.md | 2 +- 4 files changed, 102 insertions(+), 6 deletions(-) diff --git a/crates/render/src/brush_fx.rs b/crates/render/src/brush_fx.rs index a2aa34e9f..1322ef056 100644 --- a/crates/render/src/brush_fx.rs +++ b/crates/render/src/brush_fx.rs @@ -6,13 +6,16 @@ //! any of them regenerate the art and nothing else does. The copies of an object that share its id //! (symbol instances, inline graphics, repeat copies) have their own paths, so each keeps its own //! pieces. The pieces keep their `Arc`s while they are cached, so their stroke outlines, live-effect -//! geometry and the evaluations of live objects come from the renderer's caches too. +//! geometry and the evaluations of live objects come from the renderer's caches too. Brush art +//! leaves out the symbol instances and pattern paints whose art draws a brushed stroke, so when +//! the art of a symbol or pattern changes, the pieces of brushes and strokes that can show one are +//! made again. use std::collections::HashMap; use std::hash::{DefaultHasher, Hash, Hasher}; -use std::sync::Arc; +use std::sync::{Arc, Weak}; -use vectorcraft_brush::Brush; +use vectorcraft_brush::{Brush, BrushKind}; use vectorcraft_doc::{AppearanceItem, Document, Node, NodeKind, StrokeLayer, SymbolBoxes}; use vectorcraft_geom::{BezPath, PathEl, Point, Rect}; use vello_cpu::RenderContext; @@ -25,6 +28,27 @@ pub(crate) struct Entry { stroke: StrokeLayer, brush: Arc, pieces: Arc>>, + /// Can the pieces show symbol or pattern art ([`shows_definitions`])? + definitions: bool, +} + +/// Can the brush art of `brush` along a stroke `st` show symbol or pattern art: does the stroke +/// paint with a pattern, or does the brush's own art show a symbol or a pattern? +fn shows_definitions(brush: &Brush, st: &StrokeLayer) -> bool { + let art: Vec<&Node> = match &brush.kind { + BrushKind::Art(a) => vec![&a.art], + BrushKind::Scatter(s) => vec![&s.art], + BrushKind::Pattern(p) => { + [Some(&p.side), p.outer_corner.as_ref(), p.inner_corner.as_ref(), p.start.as_ref(), p.end.as_ref()].into_iter().flatten().collect() + } + BrushKind::Calligraphic(_) | BrushKind::Bristle(_) => vec![], + }; + matches!(st.paint, vectorcraft_color::Paint::Pattern { .. }) || crate::fx::Reads::of_all(art.into_iter()).shows_definitions() +} + +/// The art of `doc`'s patterns and symbols, in order. +fn definitions(doc: &Document) -> impl Iterator> { + doc.patterns.iter().flat_map(|p| &p.art).chain(doc.symbols.iter().map(|s| &s.art)) } /// Parsed brush library and generated brush art. @@ -37,6 +61,9 @@ pub(crate) struct BrushCache { /// (frame stamp, document address) the library was last validated for. checked: (u64, usize), lib: HashMap>, + /// The art of the document's patterns and symbols ([`definitions`]) when the library was last + /// validated, held weakly as the pattern tiles hold theirs. + definitions: Vec>, /// Brush art per (node id, stroke item index, [`path_key`] of the stroked path). pub(crate) entries: FrameCache<(u64, usize, u64), Entry>, /// How many times [`Renderer::draw_brush`] made brush art. @@ -51,6 +78,7 @@ impl Default for BrushCache { key: 0, checked: (0, 0), lib: HashMap::new(), + definitions: Vec::new(), entries: FrameCache::new(lru::BRUSH_BUDGET, 512), #[cfg(test)] made: 0, @@ -65,6 +93,10 @@ impl BrushCache { return; } self.checked = key; + if !self.definitions.iter().map(Weak::as_ptr).eq(definitions(doc).map(Arc::as_ptr)) { + self.definitions = definitions(doc).map(Arc::downgrade).collect(); + self.entries.retain(|e| !e.definitions); + } let cur = doc.unknown.get(vectorcraft_brush::DOC_KEY); if self.src.as_ref().is_some_and(|s| s.as_ref() == cur) { return; @@ -149,8 +181,9 @@ impl Renderer { let pieces: Arc>> = Arc::new(vectorcraft_brush::stroke_pieces(f.doc, &brush, bp, st).into_iter().map(Arc::new).collect()); let bytes = size_of::() + lru::path_bytes(bp) + pieces.iter().map(|p| lru::node_bytes(p)).sum::(); - let kept = - self.brushes.entries.insert(key, Entry { path: bp.clone(), stroke: st.clone(), brush, pieces: pieces.clone() }, bytes, stamp); + let definitions = shows_definitions(&brush, st); + let entry = Entry { path: bp.clone(), stroke: st.clone(), brush, pieces: pieces.clone(), definitions }; + let kept = self.brushes.entries.insert(key, entry, bytes, stamp); (pieces, kept) } }; diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index cb314ff61..7bd46ffd2 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -977,6 +977,11 @@ impl Reads { self.plugins || self.symbols || self.patterns } + /// Does the content show symbol or pattern art? + pub(crate) fn shows_definitions(self) -> bool { + self.symbols || self.patterns + } + fn add(&mut self, n: &Node) { let pattern = |p: &vectorcraft_color::Paint| matches!(p, vectorcraft_color::Paint::Pattern { .. }); n.walk(&mut |c| { diff --git a/crates/render/src/tests_memory.rs b/crates/render/src/tests_memory.rs index 69ebd726b..d5f9e2e65 100644 --- a/crates/render/src/tests_memory.rs +++ b/crates/render/src/tests_memory.rs @@ -743,6 +743,64 @@ fn brush_art_of_copies_draws_symbol_brush_and_stroke_edits() { }); } +/// A path stroked with scatter brush `Dots`, whose art is an instance of symbol `Dot` (a blue +/// square) and a square painted with pattern `Fill` (a green square). After a frame, the next one +/// draws as a new renderer does when the art of `Dot` or `Fill` starts or stops drawing a brushed +/// stroke, which decides whether the brush art shows that instance or that pattern. +#[test] +fn brush_art_draws_edits_of_the_symbols_and_patterns_it_shows() { + let _fonts = fonts_steady(); + use vectorcraft_brush::{Brush, BrushKind, Scatter}; + use vectorcraft_doc::pattern::{PatternDef, pattern_paint}; + use vectorcraft_geom::shapes::{line, rectangle}; + let square = |d: &mut Document, paint: Paint| filled(d, rectangle(Rect::new(-4.0, -4.0, 4.0, 4.0)), paint, vec![]); + // A square painted `paint`, with a brushed line beside it when `brushed`. + let art = |d: &mut Document, paint: Paint, brushed: bool| { + let mut members = vec![Arc::new(square(d, paint))]; + if brushed { + members.push(Arc::new(brushed_line(d))); + } + Node::group(d.alloc_id(), members) + }; + let doc = |dot_brushed: bool, fill_brushed: bool| { + let mut d = Document::new(200.0, 100.0); + let dot = art(&mut d, Paint::solid(Color::rgb(0.0, 0.0, 1.0)), dot_brushed); + d.symbols.push(vectorcraft_doc::Symbol { name: "Dot".into(), art: Arc::new(dot) }); + let fill = art(&mut d, Paint::solid(Color::rgb(0.0, 0.6, 0.0)), fill_brushed); + let mut def = PatternDef::new("Fill", vec![Arc::new(fill)]); + def.tile = Rect::new(-6.0, -6.0, 6.0, 6.0); + d.patterns.push(def); + let id = d.alloc_id(); + let instance = Node::new(id, NodeKind::SymbolInstance { symbol: "Dot".into(), xf: Affine::translate((0.0, -8.0)) }); + let mut painted = square(&mut d, pattern_paint("Fill")); + painted.transform(Affine::translate((0.0, 8.0)), true); + let scatter = Node::group(d.alloc_id(), vec![Arc::new(instance), Arc::new(painted)]); + let mut lib = vectorcraft_brush::library(&d); + lib.push(Brush { name: "Dots".into(), kind: BrushKind::Scatter(Scatter { art: scatter, spacing: (150.0, 150.0), ..Default::default() }) }); + vectorcraft_brush::store(&mut d, &lib); + let mut ap = vectorcraft_doc::Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0); + if let Some(s) = ap.stroke_mut() { + s.brush = Some("Dots".into()); + } + let path = Node::path(d.alloc_id(), line((20.0, 50.0).into(), (180.0, 50.0).into()), ap); + let l = d.layers[0].id; + d.insert(Some(l), usize::MAX, path).unwrap(); + d + }; + for brushed in [false, true] { + edit_draws_as_new(doc(brushed, false), |d| { + let dot = art(d, Paint::solid(Color::rgb(0.0, 0.0, 1.0)), !brushed); + d.symbols[0].art = Arc::new(dot); + }); + edit_draws_as_new(doc(false, brushed), |d| { + let fill = art(d, Paint::solid(Color::rgb(0.0, 0.6, 0.0)), !brushed); + if let Some(def) = d.pattern_mut("Fill") { + def.art = vec![Arc::new(fill)]; + } + }); + } +} + /// A path of the document and an instance of symbol `Wave` (a group holding another path) are /// stroked with scatter brush `Bent`, whose art is an arc envelope around a green band: the second /// frame evaluates none of the envelopes in their brush art again, and draws what the first drew diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4fff1312d..4cb98bd74 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -989,7 +989,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Type with transparency or effects:** type at less than 100% opacity, in a blend mode other than Normal, or with raster effects such as a drop shadow keeps its glyph outlines between frames, as opaque type does, and is laid out again when it is edited or the fonts change. At 2880 × 1800 on 8 threads, a page of 1,000 type objects takes 13 ms a frame instead of 23 ms at 80% opacity, 31 ms instead of 41 ms in Multiply and 16 ms instead of 26 ms with a drop shadow, with the same pixels. -- **Brush art of copies:** symbol instances, inline graphics and repeat copies of a brushed path keep their own brush art between frames, as the same path on the page does. The renderer also keeps the stroke outlines of brush art and the evaluation of a live object in it, such as an envelope that a scatter brush places. The brush art is made again when the path, its stroke or the brush changes. On one thread at 1000 × 1000 pixels, 2,000 paths on the page whose scatter brush art is an envelope take 94 ms a frame instead of 400 ms, and the fastest frames of 2,000 instances of a symbol whose art is a brushed path take 12 ms instead of 16 ms, with the same pixels. +- **Brush art of copies:** symbol instances, inline graphics and repeat copies of a brushed path keep their own brush art between frames, as the same path on the page does. The renderer also keeps the stroke outlines of brush art and the evaluation of a live object in it, such as an envelope that a scatter brush places. The brush art is made again when the path, its stroke or the brush changes, and when the art of a symbol or pattern that the brush art can show changes. On one thread at 1000 × 1000 pixels (the median frame, with the median fastest frame in brackets), 2,000 paths on the page whose scatter brush art is an envelope take 94 ms (80 ms) a frame instead of 400 ms (306 ms), and 2,000 instances of a symbol whose art is a brushed path take 13.4 ms (12.0 ms) instead of 19.3 ms (15.6 ms), with the same pixels. - **Live objects in symbols and masks:** symbol instances and inline graphics whose symbol is a blend, an envelope, a repeat, an Intertwine object or a gradient mesh, or a group that holds one, draw the evaluation the renderer keeps for that object between frames, as the same object on the page does. So do opacity masks whose art is such an object, and such objects in clip groups in Outline view. The object is evaluated again when the symbols, patterns, fonts or color settings it shows change. Symbol art with effects of its own, or that holds a symbol instance, an image, a placed file, type with an inline graphic or a raster effect, is still placed and evaluated again in every frame. At 1000 × 1000 pixels, a page of 2,000 instances of a symbol that holds an envelope takes 28 ms a frame instead of 126 ms, with the same pixels.