mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Render: objects that share an appearance keep their own stroke outlines between frames
The renderer kept the outline of a stroke under the address of the stroke in the object's appearance, the bits of the width and the tolerance level, and used an entry only when the entry held the object being drawn. The slices of a gradient along or across a stroke were kept the same way. Objects can share one appearance until one of them changes it. Copies made by Duplicate Selection share the original's appearance, gradients included. For objects without a gradient fill or stroke, so do the art the renderer places for unstained symbol instances, the copies a live repeat draws and copies made by edit.duplicate. In each frame, the objects that share an appearance replaced each other's entries. The renderer therefore outlined their strokes again in every frame. The two caches now key each entry by the address of the object as well, and each object keeps its own outlines from frame to frame. Art that the renderer makes again in every frame keeps the old key: symbol art that placed_art does not keep, and the evaluations of live objects and the art of objects with effects that their caches do not keep. The copies in such art replace each other's entries as before. vectorcraft-cli bench of 2,000 unstained instances of a symbol of eight paths took 45.7 ms a frame on main and 31.1 ms with this change, at 2880x1800 on 8 threads, and a grid repeat of the same paths, which draws 2,000 groups, took 45.0 ms on main and 27.2 ms with this change. The peak working set of a bench run on the instances went from 88 MiB to 122 MiB. In the tests, each frame draws what a new renderer draws.
This commit is contained in:
+44
-26
@@ -395,11 +395,15 @@ pub struct Renderer {
|
||||
/// Worker threads for the multithreaded rasterizer (0 = single-threaded).
|
||||
pub threads: u16,
|
||||
geom: PtrMap<usize, GeomEntry>,
|
||||
/// 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<Arc<Node>>,
|
||||
/// 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<Arc<Node>>,
|
||||
/// 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<usize, ClipPaintEntry>,
|
||||
/// 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<String, (String, u64, vectorcraft_effects::ColorMap)>,
|
||||
@@ -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<Arc<BezPath>> {
|
||||
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<Node>) -> 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 `<use>`).
|
||||
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<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> Arc<Node> {
|
||||
fn placed_art(&mut self, art: &Arc<Node>, xf: Affine, scale_strokes: bool, inst: Option<&Node>) -> (Arc<Node>, 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).
|
||||
|
||||
Reference in New Issue
Block a user