mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 18:54:55 +00:00
When an edit changes an object, the canvas draws again only the area the object painted before and after it, measured by its cull bounds. Type and groups with Extrude & Bevel, Inflate or Rotate were measured by their letters or members, not by the solid the effect draws, which reaches well past them. Making the solid shallower or turning it left pieces of the old solid on the canvas outside that area until the view was drawn again. An object with a 3D effect is now measured by the effect's art, as the renderer draws it: with its geometry effects, each object in it with the raster effects of its own fills and strokes and the art its brushed strokes draw, grown by the reach of the object's own raster effects. One function measures that art for every bounds the renderer and exports take, where three copies of it disagreed. - The renderer measures the art it draws. What it makes to measure an object it holds until the frame ends, with what the art read from outside the object (fonts, symbols, color settings), and keeps it with the art it draws if it draws the object while that is unchanged. Art measured but not drawn (culled, off the view) isn't kept. It holds at most as much as it keeps for the art it draws (64 MB); past that, an object is made again to draw it. An object whose 3D effect builds nothing (Revolve on a group, a solid too complex) is measured as it is drawn otherwise, as without a renderer. FrameStats::three_d_built counts the 3D art the renderer makes. - The canvas measures what an edit changed through the renderer that drew the frame shown (Renderer::changed). When it draws the part on its own thread, it draws it with the art it made to measure it; when the render worker draws the frame, or nothing in view changed, that art is let go. Under Overprint Preview or Separations Preview, which draw copies of the objects, the edit is measured afresh. An object the frame doesn't draw (hidden, in a hidden group or layer, or hidden by the view) is measured as nothing, before and after, so an edit to it builds no solid: changed() without a renderer counts it as nothing too, where it measured it. The canvas measures the edit before it knows where the frame is drawn: when the worker draws it, the solids it built to measure it are built again there. - Fit to Artwork Bounds, the Artboards panel's check for it and per-artboard SVG export measure every artboard through one renderer (Renderer::art_bounds_on, Renderer::effect_bounds), which keeps each object's bounds for the pass, a group holding 3D art or a hidden member included, so each solid is built once: 30 extruded type objects over 30 artboards take about 2 ms, where measuring their letters took under 1 ms and building each solid for each artboard would take 70 ms. Objects with brushed strokes, which culling leaves room for and exports don't, are measured on each artboard. The Artboard tool's canvas menu keeps whether Fit to Artwork Bounds has art to fit for the document's revision, where it measured it on every frame it was open. - Other exports, Object › Rasterize and changed() without a renderer build the art on each call, as 3D paths and groups already did. Exports and Rasterize now take the solid of 3D type, where they cropped it to the letters, and the art its brushed strokes draw, not the room culling leaves for it. Rasterize on drawn 3D art measures it once while the art is kept. - A shadow after a 3D effect, on the object or on a fill after the fill's own 3D effect, reaches past the solid once; it was counted twice. - With Crop Marks or Vector Halftone, which draw other art, the object is measured by that art, as before. - Culling leaves room for a 3D object's brushed strokes as for any object's, and the renderer measures a 3D object again when the brush library changes: a brush widened past that room would leave the object culled. A document's first frame builds the solid of every 3D object, those off the view too, to know whether it reaches into the view: 200 extruded words with 150 off the view take about 12 ms, where measuring their letters took 6 ms. Type whose object effects leave it as it is (Revolve builds no art from type) is measured by its own box grown to hold its glyphs, as plain type is; it was measured by its box alone, which needn't hold them.
242 lines
12 KiB
Rust
242 lines
12 KiB
Rust
use super::options::{AntiAlias, RenderOptions};
|
|
use super::threads::single_threaded_context;
|
|
use super::{
|
|
Affine, Arc, BezPath, Document, FillRule, HashMap, Ink, Node, Pixmap, Rect, RenderContext, StrokeLayer, SymbolBoxes, TextObject, TypeAntiAlias,
|
|
fx, kurbo, live,
|
|
};
|
|
|
|
/// Hasher for pointer-keyed caches: a multiply-xorshift (SipHash showed up in frame profiles).
|
|
#[derive(Default, Clone, Copy)]
|
|
pub(crate) struct PtrHasher(pub(crate) u64);
|
|
|
|
impl std::hash::Hasher for PtrHasher {
|
|
fn write(&mut self, bytes: &[u8]) {
|
|
for b in bytes {
|
|
self.0 = (self.0 ^ *b as u64).wrapping_mul(0x0100_0000_01b3);
|
|
}
|
|
}
|
|
fn write_u64(&mut self, n: u64) {
|
|
let x = (self.0 ^ n).wrapping_mul(0x9e37_79b9_7f4a_7c15);
|
|
self.0 = x ^ (x >> 29);
|
|
}
|
|
fn write_usize(&mut self, n: usize) {
|
|
self.write_u64(n as u64);
|
|
}
|
|
fn finish(&self) -> u64 {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
pub(crate) type PtrMap<K, V> = HashMap<K, V, std::hash::BuildHasherDefault<PtrHasher>>;
|
|
|
|
/// The key of a cached stroke outline: ([`Renderer::stroke_owner`] of what owns the stroke, the
|
|
/// stroke's place in it, width bits, tolerance level).
|
|
pub(crate) type StrokeKey = (usize, usize, u64, i32);
|
|
|
|
/// What the [`StrokeKey`] of the latest outline of an object's stroke is found under once the
|
|
/// object has changed: (the object's id, the stroke's place in its appearance items, width bits,
|
|
/// tolerance level).
|
|
pub(crate) type MovedKey = (u64, usize, u64, i32);
|
|
|
|
/// A stroke expanded to a fill outline: (owning node, outline, last frame used, the region a frame
|
|
/// must lie in to draw the outline, when the outline leaves out the segments far from that region,
|
|
/// `None` when it has every segment, and whether the outline was made along a line of the node's
|
|
/// own path, the one [`Node::stroke_path`] makes, so that the line can be made again from the
|
|
/// node and its stroke).
|
|
pub(crate) struct StrokeEntry {
|
|
pub(crate) node: Arc<Node>,
|
|
pub(crate) outline: Arc<BezPath>,
|
|
pub(crate) stamp: u64,
|
|
pub(crate) crop: Option<Rect>,
|
|
pub(crate) own: bool,
|
|
}
|
|
|
|
/// The slices of a stroke whose gradient runs along or across it, each with its paint and bounds.
|
|
pub(crate) type Slices = Arc<Vec<(BezPath, vectorcraft_color::Paint, Rect)>>;
|
|
|
|
/// The key of cached [`Slices`]: ([`Renderer::stroke_owner`] of the stroke's object, stroke layer
|
|
/// address, tolerance level).
|
|
pub(crate) type SliceKey = (usize, usize, i32);
|
|
|
|
/// What the [`SliceKey`] of the latest slices of an object's stroke is found under once the object
|
|
/// has changed: (the object's id, the stroke's place in its appearance items, tolerance level).
|
|
pub(crate) type MovedSliceKey = (u64, usize, i32);
|
|
|
|
/// [`Slices`] cached for a stroke: (owning node, the node whose path and stroke the slices were
|
|
/// made from, either the owning node or an earlier copy of it that the owning node is moved from,
|
|
/// the slices, the offset they are drawn moved by, the fill rule they were made with, whether they
|
|
/// were made along the path [`Node::stroke_path`] makes from that node, last frame used).
|
|
pub(crate) struct SliceEntry {
|
|
pub(crate) node: Arc<Node>,
|
|
pub(crate) made: Arc<Node>,
|
|
pub(crate) slices: Slices,
|
|
pub(crate) by: kurbo::Vec2,
|
|
pub(crate) rule: FillRule,
|
|
pub(crate) own: bool,
|
|
pub(crate) stamp: u64,
|
|
}
|
|
|
|
/// Cached per-node geometry, keyed by `Arc` identity. Structural sharing means an unchanged node
|
|
/// keeps its allocation across edits, so a pointer match (with the Arc kept alive here so the
|
|
/// address can't be reused) is an exact cache hit — no invalidation logic needed.
|
|
pub(crate) struct GeomEntry {
|
|
pub(crate) node: Arc<Node>,
|
|
pub(crate) bounds: Option<Rect>,
|
|
pub(crate) path: Option<Arc<BezPath>>,
|
|
pub(crate) stamp: u64,
|
|
}
|
|
|
|
/// A clipping path (kept alive so its address can't be reused) and the region it clips to.
|
|
pub(crate) type ClipEntry = (Arc<Node>, Option<Arc<(BezPath, FillRule)>>);
|
|
|
|
/// A clipping path (kept alive) and what it paints: its fills and its strokes ([`Node::clip_paint`]).
|
|
pub(crate) type ClipPaintEntry = (Arc<Node>, Option<Arc<Node>>, Option<Arc<Node>>);
|
|
|
|
/// An object with effects (kept alive), the art drawn for it ([`Renderer::fx_art`]), and, when that
|
|
/// art can be made from evaluated live objects (`fx::evaluates_live`) or from type, symbol art or
|
|
/// colors shaded through the color settings (`fx::reads_outside`), the inputs it was made with
|
|
/// ([`live::Found`]).
|
|
pub(crate) type FxArtEntry = (Arc<Node>, Arc<Node>, Option<live::Found>);
|
|
|
|
/// What symbol art placed by [`Renderer::placed_art`] or [`Renderer::sliced_art`] is kept under:
|
|
/// the art's address, the bits of the transform's coefficients, whether strokes scale with it, and
|
|
/// the stain ([`stain_bits`]).
|
|
pub(crate) type PlacedKey = (usize, [u64; 6], bool, [u32; 6]);
|
|
|
|
/// Symbol art placed by [`Renderer::placed_art`] or [`Renderer::sliced_art`].
|
|
pub(crate) struct PlacedEntry {
|
|
/// The symbol's art, held weakly: the reference keeps the art's allocation, so no other art can
|
|
/// get its address while the entry exists, and does not keep the art's content.
|
|
pub(crate) art: std::sync::Weak<Node>,
|
|
/// The art placed, or `None` for art placed again in every frame ([`depends_on_document`]).
|
|
pub(crate) placed: Option<Arc<Node>>,
|
|
/// The font generation ([`vectorcraft_text::FontDb::generation`]) the art was placed in, when
|
|
/// it holds type: the art is placed again when the fonts change.
|
|
pub(crate) fonts: Option<u64>,
|
|
/// The 9-slice map the art was placed through ([`Renderer::sliced_art`]), or `None` for art
|
|
/// placed by the transform alone ([`Renderer::placed_art`]). An entry is used only by the
|
|
/// function that made it, with the same map.
|
|
pub(crate) slice: Option<vectorcraft_doc::NineSliceMap>,
|
|
}
|
|
|
|
/// Type drawn with an anti-aliasing method other than Sharp, as [`Renderer::draw_type_edges`] drew
|
|
/// it offscreen on the pixels of the output ([`Frame::output`]).
|
|
pub(crate) struct TypeEdgeEntry {
|
|
/// A copy of the type's object: the kept pixels are used only for an object equal to it.
|
|
pub(crate) node: Node,
|
|
/// The output's zoom and rotation ([`fx::linear_key`]).
|
|
pub(crate) linear: [u64; 4],
|
|
/// The method, the edge threshold, the frame's edge smoothing, whether the frame composites
|
|
/// precisely, and the opacity folded into the paints.
|
|
pub(crate) settings: (TypeAntiAlias, Option<u8>, AntiAlias, bool, u32),
|
|
/// What the type read from the document and the color settings ([`Renderer::raster_inputs`]).
|
|
pub(crate) inputs: fx::Inputs,
|
|
/// The output's offset: in a frame whose offset differs from it by whole pixels, the pixels are
|
|
/// placed moved by them.
|
|
pub(crate) shift: kurbo::Vec2,
|
|
/// The output pixel at the image's top-left corner.
|
|
pub(crate) origin: (f64, f64),
|
|
pub(crate) image: Arc<Pixmap>,
|
|
}
|
|
|
|
/// How deep inline graphics nest (text in a symbol shown inline in text…) before they draw nothing.
|
|
pub(crate) const MAX_INLINE_DEPTH: u32 = 4;
|
|
|
|
/// Set once a missing inline symbol has been logged (it would log every frame otherwise).
|
|
pub(crate) static MISSING_INLINE_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct FrameStats {
|
|
pub drawn: usize,
|
|
pub culled: usize,
|
|
/// How many times the frame's symbol boxes measured symbol art ([`SymbolBoxes::art_measured`]):
|
|
/// the art boxes of symbols and how far the effects in their art paint past it. Pattern tiles
|
|
/// and placed documents measure with symbol boxes of their own, which are not counted.
|
|
pub symbol_art: usize,
|
|
/// The drop shadows, outer glows and filtered contents the frame drew from rasters made before
|
|
/// their object moved, or made over such rasters ([`RenderOptions::interacting`]), and those it
|
|
/// drew at a drag's lower resolution ([`Self::previewed`]).
|
|
pub moved: usize,
|
|
/// The filtered contents, shadows and glows the frame drew at a drag's lower resolution
|
|
/// ([`RenderOptions::dragging`]). [`Self::moved`] counts them too.
|
|
pub previewed: usize,
|
|
/// What the frame drew in a way that depends on the frame's size: effect rasters and filter
|
|
/// layers cropped to it or made at a lower resolution, blending groups drawn as layers past its
|
|
/// offscreen budget, gradient meshes drawn quad by quad past the mesh buffer, and every filter
|
|
/// layer drawn on a single-threaded context: on a single-threaded renderer, or offscreen in a
|
|
/// group or an effect's raster. A part of the frame rendered on its own can draw that art with
|
|
/// other pixels than the whole frame does.
|
|
pub view_sized: usize,
|
|
/// How many threads composed the frame from its two ink planes, in a CMYK document that shows
|
|
/// transparency; 0 for other frames.
|
|
pub ink_threads: usize,
|
|
/// The type objects the frame drew offscreen with an anti-aliasing method other than Sharp
|
|
/// ([`Renderer::draw_type_edges`]). Type drawn from the pixels an earlier frame kept is not
|
|
/// counted.
|
|
pub type_edges: usize,
|
|
/// The art of 3D effects (Revolve, Extrude & Bevel, Inflate, Rotate) this renderer made for the
|
|
/// objects it draws or measures itself (`Renderer::bounds_of`) since its last frame
|
|
/// began: none it found kept, and none made by measures without a renderer (the free bounds
|
|
/// functions), which this renderer doesn't see. A view that measured what changed through the
|
|
/// renderer ([`crate::Renderer::changed`]) made the art then, and draws it with none made.
|
|
pub three_d_built: usize,
|
|
pub micros: u64,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct Frame<'a> {
|
|
/// Drawing into a multithreaded context: vello filter layers must be rendered offscreen.
|
|
pub(crate) mt: bool,
|
|
pub(crate) doc: &'a Document,
|
|
/// `doc`'s symbol art boxes, which culling measures symbol instances by ([`SymbolBoxes::quick`]).
|
|
pub(crate) symbols: &'a SymbolBoxes<'a>,
|
|
pub(crate) view: Affine,
|
|
/// Visible region in document coordinates (for culling).
|
|
pub(crate) visible: Rect,
|
|
/// Size of one of this frame's pixels in document units.
|
|
pub(crate) px: f64,
|
|
pub(crate) opts: &'a RenderOptions,
|
|
/// What colours paint as: screen colours, or one ink plane of a CMYK document (see [`ink`]).
|
|
pub(crate) ink: Ink,
|
|
/// Document → pixels of the output being drawn: `view` itself, except while an export draws
|
|
/// its art at several times its size (the export's own pixels) and in offscreen layers (the
|
|
/// pixels they are placed back on). Type with an anti-aliasing method other than Sharp is
|
|
/// drawn on these pixels ([`Renderer::draw_type_edges`]).
|
|
pub(crate) output: Affine,
|
|
/// With Separations Preview, the tables placed images show the visible plates through.
|
|
pub(crate) plates: Option<&'a crate::proof::PlateLut>,
|
|
}
|
|
|
|
impl Frame<'_> {
|
|
/// Text space → device pixels when type is snapped to the pixel grid (Type anti-aliasing).
|
|
pub(crate) fn text_snap(&self, t: &TextObject) -> Option<Affine> {
|
|
(self.opts.anti_alias == AntiAlias::Type).then(|| self.view * t.xf)
|
|
}
|
|
|
|
/// Stroke `st` as this frame draws it: a copy without its dashes when the frame draws dashed
|
|
/// lines as solid lines ([`RenderOptions::solid_dashes`]), else `st` itself.
|
|
pub(crate) fn stroke<'s>(&self, st: &'s StrokeLayer) -> std::borrow::Cow<'s, StrokeLayer> {
|
|
if self.opts.solid_dashes && st.dash.is_some() {
|
|
std::borrow::Cow::Owned(StrokeLayer { dash: None, ..st.clone() })
|
|
} else {
|
|
std::borrow::Cow::Borrowed(st)
|
|
}
|
|
}
|
|
|
|
/// A render context for drawing this frame's art offscreen (on the calling thread), with its
|
|
/// edge smoothing: hard edges must not depend on where the art is drawn.
|
|
pub(crate) fn offscreen_context(&self, w: u16, h: u16) -> RenderContext {
|
|
#[cfg(test)]
|
|
OFFSCREEN_PIXELS.with(|n| n.set(n.get() + u64::from(w) * u64::from(h)));
|
|
let mut ctx = single_threaded_context(w, h);
|
|
ctx.set_aliasing_threshold(self.opts.anti_alias.threshold());
|
|
ctx
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
thread_local! {
|
|
/// The pixels of the offscreen contexts [`Frame::offscreen_context`] has made on this thread.
|
|
pub(crate) static OFFSCREEN_PIXELS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
|
}
|