mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Render: the strokes of brush art are no longer drawn with old outlines
While the renderer drew a brushed path on its fast path, it drew the pieces of brush art with that path still set as the object being drawn. cached_stroke and fill_path_gradient then kept the outlines and gradient slices of the pieces' strokes under the brushed path and the address of each piece's stroke. The entries held the brushed path and not the pieces, so a piece made later could take the address of an earlier piece's stroke and be drawn with that piece's outline. The instances of a symbol and the copies of a live repeat replace each other's brush art, so it is made again for each of them in every frame, and every brushed path gets new brush art when the document's brushes change. In a test, a renderer drawing three instances of a symbol whose art holds a path brushed with a scatter brush of a stroked square drew frames that differed from a new renderer's in 316 to 1,140 pixels. The two caches now keep only the strokes of the object being drawn. With vectorcraft-cli bench at 2880x1800 on 8 threads, 2,000 instances of a symbol of eight brushed paths took 203.4 ms a frame on main and 155.9 ms with this change. 2,000 groups of the same paths, for which main keeps the brush art and the outlines of its strokes between frames, took 133.7 ms on main and 156.9 ms with this change. The peak working set of a bench run at 2880x1800 went from 609 MiB to 127 MiB on the instances and from 331 MiB to 210 MiB on the groups.
This commit is contained in:
@@ -1591,7 +1591,7 @@ impl Renderer {
|
||||
.collect(),
|
||||
)
|
||||
};
|
||||
let slices = match self.cur.clone() {
|
||||
let slices = match self.cur.clone().filter(|n| owns_stroke(n, st)) {
|
||||
Some(node) => {
|
||||
let key = (self.stroke_owner(&node), st as *const StrokeLayer as usize, level);
|
||||
match self.stroke_slices.get_mut(&key) {
|
||||
@@ -1633,9 +1633,10 @@ impl Renderer {
|
||||
/// The line part of a stroke of the fast-path node `self.cur` (see
|
||||
/// [`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.
|
||||
/// coarser than needed, so zooming reuses outlines until the level changes. Only the node's own
|
||||
/// strokes are cached: for the strokes of the brush art drawn for it, this returns `None`.
|
||||
fn cached_stroke(&mut self, f: &Frame, line: &BezPath, st: &StrokeLayer, width: f64) -> Option<Arc<BezPath>> {
|
||||
let node = self.cur.clone()?;
|
||||
let node = self.cur.clone().filter(|n| owns_stroke(n, st))?;
|
||||
let level = (f.px * 0.25).log2().floor() as i32;
|
||||
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)
|
||||
@@ -2101,6 +2102,11 @@ fn painted_items(n: &Node) -> usize {
|
||||
.max(if n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !matches!(f.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None)) || matches!(i, AppearanceItem::Stroke(s) if s.visible && !matches!(s.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None))) { 2 } else { 0 })
|
||||
}
|
||||
|
||||
/// Whether `st` is one of `n`'s own strokes (the strokes of the brush art drawn for `n` are not).
|
||||
fn owns_stroke(n: &Node, st: &StrokeLayer) -> bool {
|
||||
n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Stroke(s) if std::ptr::eq(s, st)))
|
||||
}
|
||||
|
||||
/// Symbol art `art` stained by instance `inst` ([`instance_art`]) and placed by `xf`, with
|
||||
/// strokes scaled by `xf` when `scale_strokes`.
|
||||
fn place(art: &Node, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> Arc<Node> {
|
||||
|
||||
@@ -545,6 +545,68 @@ fn art_evaluated_again_in_every_frame_keeps_one_outline_per_stroke() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The instances of a symbol and the copies of a repeat replace each other's brush art, so their
|
||||
/// brush art is made again in every frame. A brushed path gets new brush art each time its brush
|
||||
/// changes. The renderer keeps no outline or gradient slices for the squares' strokes, and each
|
||||
/// frame draws what a new renderer draws.
|
||||
#[test]
|
||||
fn strokes_of_brush_art_made_again_draw_as_a_new_renderer_does() {
|
||||
let _fonts = fonts_steady();
|
||||
use vectorcraft_brush::{Brush, BrushKind, Scatter};
|
||||
use vectorcraft_doc::{Appearance, StrokeGradientMode};
|
||||
let mut d = Document::new(200.0, 140.0);
|
||||
// Brush `Squares` scatters 8 × 8 squares stroked 2 pt with a gradient along the stroke and
|
||||
// 0.5 pt in black over it, with a spacing of `spacing` percent of the square's width.
|
||||
let squares = |d: &mut Document, spacing: f64| {
|
||||
let mut st = StrokeLayer::new(Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(Default::default()))), 2.0);
|
||||
st.gradient_mode = StrokeGradientMode::Along;
|
||||
let black = StrokeLayer::new(Paint::solid(Color::BLACK), 0.5);
|
||||
let square = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 8.0, 8.0));
|
||||
let strokes = Appearance { items: vec![AppearanceItem::Stroke(st), AppearanceItem::Stroke(black)], ..Default::default() };
|
||||
let art = Node::path(NodeId(0), square, strokes);
|
||||
let mut lib = vectorcraft_brush::library(d);
|
||||
lib.retain(|b| b.name != "Squares");
|
||||
lib.push(Brush { name: "Squares".into(), kind: BrushKind::Scatter(Scatter { art, spacing: (spacing, spacing), ..Default::default() }) });
|
||||
vectorcraft_brush::store(d, &lib);
|
||||
};
|
||||
// A wave around `at`, brushed with `Squares`.
|
||||
let wave = |d: &mut Document, at: (f64, f64)| {
|
||||
let mut ap = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0);
|
||||
if let Some(s) = ap.stroke_mut() {
|
||||
s.brush = Some("Squares".into());
|
||||
}
|
||||
let (x, y) = at;
|
||||
let mut wave = BezPath::new();
|
||||
wave.move_to((x - 40.0, y));
|
||||
wave.curve_to((x - 15.0, y - 20.0), (x + 15.0, y + 20.0), (x + 40.0, y));
|
||||
Node::path(d.alloc_id(), vectorcraft_geom::PathData::from_bezpath(&wave), ap)
|
||||
};
|
||||
// Three instances show symbol `B`, a group of a wave. A fourth wave is a path of the document,
|
||||
// and a Mirror repeat draws a fifth wave and its reflection.
|
||||
let art = Node::group(d.alloc_id(), vec![Arc::new(wave(&mut d, (0.0, 0.0)))]);
|
||||
d.symbols.push(vectorcraft_doc::Symbol { name: "B".into(), art: Arc::new(art) });
|
||||
let l = d.layers[0].id;
|
||||
for at in [(50.0, 30.0), (150.0, 30.0), (50.0, 70.0)] {
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::SymbolInstance { symbol: "B".into(), xf: Affine::translate(at) })).unwrap();
|
||||
}
|
||||
let path = wave(&mut d, (150.0, 70.0));
|
||||
d.insert(Some(l), usize::MAX, path).unwrap();
|
||||
let source = wave(&mut d, (50.0, 110.0));
|
||||
let mirror = vectorcraft_doc::pattern::RepeatSpec::mirror(vec![Arc::new(source)], 90.0, 10.0);
|
||||
let id = d.alloc_id();
|
||||
d.insert(Some(l), usize::MAX, Node::new(id, NodeKind::Repeat(mirror))).unwrap();
|
||||
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
// Six frames of the same document, then a frame after each change of the brush's spacing.
|
||||
for (frame, spacing) in [150.0, 150.0, 150.0, 150.0, 150.0, 150.0, 120.0, 150.0, 90.0, 150.0, 120.0, 135.0].into_iter().enumerate() {
|
||||
squares(&mut d, spacing);
|
||||
let want = Renderer::new().render(&d, 200, 140, Affine::IDENTITY, &opts).pixels;
|
||||
assert!(r.render(&d, 200, 140, Affine::IDENTITY, &opts).pixels == want, "frame {frame}");
|
||||
}
|
||||
assert_eq!((r.strokes.len(), r.stroke_slices.len()), (0, 0), "no outline or slices kept for the strokes of brush art");
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
Reference in New Issue
Block a user