Render: a long path is checked against the rasterizer's limits from its kept boxes

Before drawing a shape, the renderer checks that its control points stay
within the rasterizer's limits. For a long path it measured every point
of the path for this check in each frame. The kept path of a long path
now keeps the box of the points of each run of segments next to the run's
bounds, updates it with the runs an edit changes, and answers the check
from those boxes.

With a closed path of 200,000 anchors and no paint (release build,
single-threaded, medians of 21 frames), the part of the frame redrawn
after one anchor moves took 1.93 ms instead of 2.69 ms, and drawing the
same part again with nothing changed took 0.015 ms instead of 0.343 ms.
This commit is contained in:
dotCooCoo
2026-11-11 17:11:14 -08:00
parent 7f3a046b18
commit 7de42c294d
5 changed files with 97 additions and 21 deletions
+44 -11
View File
@@ -46,6 +46,8 @@ struct Sub {
ends: Vec<usize>,
/// The box of each run of segments ([`segment_bounds`]).
boxes: Vec<Option<Rect>>,
/// The box of the points of each run of segments: their ends and their control points.
controls: Vec<Option<Rect>>,
/// The box of the miter tips of `tips_of` at the joints whose first segment is in each run.
tips: Vec<Option<Rect>>,
}
@@ -122,6 +124,15 @@ fn end(el: &PathEl) -> Option<Point> {
}
}
/// The box of the points of segment `s`: its ends and its control points.
fn segment_controls(s: PathSeg) -> Rect {
match s {
PathSeg::Line(l) => Rect::from_points(l.p0, l.p1),
PathSeg::Quad(q) => Rect::from_points(q.p0, q.p2).union_pt(q.p1),
PathSeg::Cubic(c) => Rect::from_points(c.p0, c.p3).union_pt(c.p1).union_pt(c.p2),
}
}
/// The ends of the runs of a subpath of `segs` segments measured whole: [`RUN`] segments each.
fn whole_runs(segs: usize) -> Vec<usize> {
(1..=segs.div_ceil(RUN)).map(|r| (r * RUN).min(segs)).collect()
@@ -146,8 +157,10 @@ impl Sub {
/// Subpath `sp`, its MoveTo at `start` in `els`, measured whole, with the tips of `st`.
fn measured(sp: &SubPath, start: usize, els: &[PathEl], st: Option<&StrokeLayer>) -> Self {
let segs = sp.segment_count();
let mut sub = Sub { start, segs, anchors: sp.anchors.len(), closed: sp.closed, ends: whole_runs(segs), boxes: vec![], tips: vec![] };
let ends = whole_runs(segs);
let mut sub = Sub { start, segs, anchors: sp.anchors.len(), closed: sp.closed, ends, boxes: vec![], controls: vec![], tips: vec![] };
sub.boxes = (0..sub.runs()).map(|r| sub.run_box(els, r)).collect();
sub.controls = (0..sub.runs()).map(|r| sub.run_controls(els, r)).collect();
if let Some(st) = st {
sub.tips = (0..sub.runs()).map(|r| sub.run_tips(els, r, st)).collect();
}
@@ -191,6 +204,22 @@ impl Sub {
self.range(r).filter_map(|i| self.seg(els, i)).map(segment_bounds).reduce(|a, b| a.union(b))
}
/// The box of the points of the segments of run `r`: their ends and their control points.
fn run_controls(&self, els: &[PathEl], r: usize) -> Option<Rect> {
self.range(r).filter_map(|i| self.seg(els, i)).map(segment_controls).reduce(|a, b| a.union(b))
}
/// Measure again the box and the box of the points of run `r`.
fn measure_run(&mut self, els: &[PathEl], r: usize) {
let (b, c) = (self.run_box(els, r), self.run_controls(els, r));
if let Some(slot) = self.boxes.get_mut(r) {
*slot = b;
}
if let Some(slot) = self.controls.get_mut(r) {
*slot = c;
}
}
/// The segment with length that follows segment `i` (round to the start when closed), or
/// `None` when there is none other than `i`.
fn next_drawn(&self, els: &[PathEl], i: usize) -> Option<PathSeg> {
@@ -297,10 +326,7 @@ impl Sub {
let mut boxes: Vec<usize> = if all { (0..self.runs()).collect() } else { segs.iter().map(|&i| self.run_of(i)).collect() };
boxes.dedup();
for r in boxes {
let b = self.run_box(els, r);
if let Some(slot) = self.boxes.get_mut(r) {
*slot = b;
}
self.measure_run(els, r);
}
let Some(st) = st else { return };
// A joint's tip changes with its segments and with which segment with length comes next:
@@ -380,11 +406,8 @@ impl Sub {
self.anchors = n1;
self.closed = c1;
let els: &[PathEl] = els;
for (r, _) in stale.iter().enumerate().filter(|(_, s)| **s) {
let b = self.run_box(els, r);
if let Some(slot) = self.boxes.get_mut(r) {
*slot = b;
}
for r in stale.iter().enumerate().filter(|(_, s)| **s).map(|(r, _)| r) {
self.measure_run(els, r);
}
let Some(st) = st else { return Some(()) };
let mut runs: Vec<usize> = stale.iter().enumerate().filter(|(_, s)| **s).map(|(r, _)| r).collect();
@@ -422,7 +445,7 @@ impl Sub {
let len = last.checked_sub(first)?;
let segs = self.segs.checked_add(new)?.checked_sub(old)?;
let after: Vec<usize> = self.ends.get(r1..)?.iter().map(|&e| e.checked_add(new)?.checked_sub(old)).collect::<Option<_>>()?;
if r1 > self.boxes.len() || r1 > stale.len() {
if r1 > self.boxes.len() || r1 > self.controls.len() || r1 > stale.len() {
return None;
}
let count = if len > 2 * RUN { len.div_ceil(RUN) } else { usize::from(len > 0) };
@@ -430,6 +453,7 @@ impl Sub {
self.ends.extend(after);
self.ends.splice(r0..r1, (1..=count).map(|j| if j == count { last } else { first + j * RUN }));
self.boxes.splice(r0..r1, std::iter::repeat_n(None, count));
self.controls.splice(r0..r1, std::iter::repeat_n(None, count));
if tips {
self.tips.splice(r0.min(self.tips.len())..r1.min(self.tips.len()), std::iter::repeat_n(None, count));
}
@@ -602,6 +626,15 @@ impl KeptPath {
})
}
/// The box of the points of the path's elements, as [`BezPath::control_box`] measures it: each
/// subpath's MoveTo and the boxes of the points of its runs, joined.
pub(crate) fn control_box(&self) -> Rect {
let els = self.path.elements();
let start = |s: &Sub| els.get(s.start).and_then(end).filter(|_| s.anchors > 0).map(|p| Rect::from_points(p, p));
let runs = |s: &Sub| s.controls.iter().flatten().copied().reduce(|a, b| a.union(b));
self.parts.iter().flatten().flat_map(|s| [start(s), runs(s)]).flatten().reduce(|a, b| a.union(b)).unwrap_or_default()
}
/// The box of the miter tips of `st` at every joint of the path ([`StrokeLayer::miter_tip`]),
/// for [`StrokeLayer::bounds_with_tips`]: measured whole when the tips kept are another
/// stroke's, then kept up to date with the path.
+6 -5
View File
@@ -1,4 +1,4 @@
use super::{Affine, AppearanceItem, Arc, BezPath, Document, Node, NodeKind, StrokeLayer, brush_fx, effects};
use super::{Affine, AppearanceItem, Arc, Document, Node, NodeKind, Rect, StrokeLayer, brush_fx, effects};
/// Most pixels from the origin that a shape handed to the rasterizer may reach, or a stroke be
/// wide. A document's art stays within 4 million points, 2.6 billion pixels at the closest zoom.
@@ -6,10 +6,11 @@ use super::{Affine, AppearanceItem, Arc, BezPath, Document, Node, NodeKind, Stro
/// for more memory than there is, either of which ends the app: it is left undrawn.
const MAX_DEVICE: f64 = 1e10;
/// Can shape `bp` of `n` be drawn through `view`: its control points (finite) within
/// [`MAX_DEVICE`] pixels, and its visible strokes no wider?
pub(crate) fn drawable(bp: &BezPath, view: Affine, n: &Node) -> bool {
let b = view.transform_rect_bbox(bp.control_box());
/// Can a shape of `n` whose control points have the box `control` (`BezPath::control_box`) be
/// drawn through `view`: its control points (finite) within [`MAX_DEVICE`] pixels, and its visible
/// strokes no wider?
pub(crate) fn drawable(control: Rect, view: Affine, n: &Node) -> bool {
let b = view.transform_rect_bbox(control);
let scale = view.determinant().abs().sqrt();
let widest = n.appearance.items.iter().filter_map(|i| if let AppearanceItem::Stroke(st) = i { Some(st.width) } else { None }).fold(0.0, f64::max);
[b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite() && v.abs() <= MAX_DEVICE) && (widest * scale).is_finite() && widest * scale <= MAX_DEVICE
+1 -1
View File
@@ -19,7 +19,7 @@ use super::{
};
#[cfg(test)]
pub(crate) use paint::{SLICES_MADE, STROKES_OUTLINED, anchors_moved, offset_between};
pub(crate) use paint::{CONTROL_BOXES_MEASURED, SLICES_MADE, STROKES_OUTLINED, anchors_moved, offset_between};
mod images;
mod nodes;
+17 -1
View File
@@ -22,7 +22,7 @@ impl Renderer {
}
pub(super) fn draw_shape(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, bp: &BezPath, rule: FillRule) {
if !drawable(bp, f.view, n) {
if !drawable(self.control_box(n, bp), f.view, n) {
return;
}
if fx::has_fx(n) {
@@ -73,6 +73,19 @@ impl Renderer {
}
}
/// The box of the control points of shape `bp` of `n`: kept with its path when `bp` is the
/// path kept for `n`'s id ([`crate::kept_paths::KeptPath::control_box`]), else measured.
fn control_box(&self, n: &Node, bp: &BezPath) -> Rect {
if let Some(k) = self.kept.get(&n.id)
&& std::ptr::eq(Arc::as_ptr(&k.path), bp)
{
return k.control_box();
}
#[cfg(test)]
CONTROL_BOXES_MEASURED.with(|c| c.set(c.get() + 1));
bp.control_box()
}
/// Multiply the folded object opacity into a solid paint.
pub(super) fn fold_alpha(&self, ctx: &mut RenderContext, ink: Ink, p: &vectorcraft_color::Paint) {
if self.alpha < 1.0
@@ -471,6 +484,9 @@ thread_local! {
/// How many sets of gradient stroke slices [`Renderer::fill_path_gradient`] has made on this
/// thread.
pub(crate) static SLICES_MADE: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
/// How many shapes' control boxes [`Renderer::draw_shape`] has measured from their path on this
/// thread.
pub(crate) static CONTROL_BOXES_MEASURED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// Paint `p` of a slice ([`Slices`]) moved by `by`: a gradient's start, end and focal point moved.
+29 -3
View File
@@ -72,8 +72,8 @@ fn check(r: &mut Renderer, d: &Document, id: NodeId, what: &str) {
let k = r.kept.get(&id).unwrap_or_else(|| panic!("{what}: its path kept"));
assert!(std::ptr::eq(Arc::as_ptr(&k.node), n), "{what}: its path kept");
// The subpaths kept are those of the path measured whole, each run holds from one segment to
// twice the segments of a run, and the boxes and miter tips kept, run by run, are those
// measured again from the path.
// twice the segments of a run, and the boxes and miter tips kept, run by run, and the control
// box are those measured again from the path.
let whole = crate::kept_paths::KeptPath::new(&Arc::new(n.clone()), 0).unwrap();
let (layout, fresh) = (k.layout(), whole.layout());
assert!(layout.iter().map(|l| l.0).eq(fresh.iter().map(|l| l.0)), "{what}: the same subpaths as the path measured whole");
@@ -83,6 +83,7 @@ fn check(r: &mut Renderer, d: &Document, id: NodeId, what: &str) {
}
let (runs, again, _) = k.runs();
assert!(runs == again, "{what}: the same boxes and tips as measured again");
assert_eq!(k.control_box(), k.path.control_box(), "{what}: the same control box as measured again");
}
/// Check `d` as [`check`] does, then the document before the edit (undone) and `d` again (redone).
@@ -463,7 +464,7 @@ fn random_edit(g: &mut Seeded, p: &mut PathData) -> String {
/// Bring `k` up to date with `n` and check that it holds what [`KeptPath::new`] makes from `n`:
/// the same path elements and subpaths, runs that hold each subpath's segments, the boxes and miter
/// tips of `st` measured again over its runs, and the same bounds and tips in all.
/// tips of `st` measured again over its runs, and the same bounds, tips and control box in all.
fn same_as_made_again(k: &mut crate::kept_paths::KeptPath, n: &Arc<Node>, st: &StrokeLayer, what: &str) {
assert!(k.update(n), "{what}: brought up to date");
let mut fresh = crate::kept_paths::KeptPath::new(n, 0).unwrap_or_else(|| panic!("{what}: kept"));
@@ -478,6 +479,7 @@ fn same_as_made_again(k: &mut crate::kept_paths::KeptPath, n: &Arc<Node>, st: &S
assert!(runs == again, "{what}: the same boxes and tips as measured again");
assert_eq!(k.geometric(), fresh.geometric(), "{what}: the same bounds");
assert_eq!(k.tips(st), fresh.tips(st), "{what}: the same miter tips");
assert_eq!(k.control_box(), fresh.path.control_box(), "{what}: the same control box");
}
#[test]
@@ -578,3 +580,27 @@ fn moving_one_anchor_of_a_long_path_changes_its_kept_path_in_place() {
check(&mut r, &d, id, "one anchor moved");
assert_eq!(kept(&r), before, "the kept path changed in place");
}
#[test]
fn a_long_path_is_drawn_without_measuring_all_of_its_control_points_in_each_frame() {
let mut d = Document::new(1000.0, 1000.0);
let n = Node::path(d.alloc_id(), PathData::single(zigzag(3000, 0.0)), look(vec![stroke(6.0, LineJoin::Miter, LineCap::Butt)]));
let id = add(&mut d, n);
let empty = {
let mut e = d.clone();
Arc::make_mut(&mut e.layers[0]).children_mut().unwrap().clear();
render(&mut Renderer::new(), &e).pixels
};
let measured = || crate::renderer::CONTROL_BOXES_MEASURED.with(std::cell::Cell::get);
let before = measured();
let mut r = Renderer::new();
assert!(render(&mut r, &d).pixels != empty, "the path is drawn");
render(&mut r, &d);
moved(&mut path_of(&mut d, id).subpaths[0].anchors[1500], Vec2::new(0.0, 40.0));
check(&mut r, &d, id, "one anchor moved");
assert_eq!(measured(), before, "drawn from the control box kept with its path");
// An anchor far past what the rasterizer takes still leaves the path undrawn.
path_of(&mut d, id).subpaths[0].anchors[1500] = corner(12_000.0, 1e11);
assert!(render(&mut r, &d).pixels == empty, "an anchor 1e11 pt away: undrawn");
assert_eq!(measured(), before, "drawn from the control box kept with its path");
}