Skip to content

Commit cc2dd70

Browse files
authored
vello_common: move subpath closing logic into flatten (#1340)
This is just a logic change and slight simplification of the lines representation. By moving the burden of closing input subpaths to flattening itself, this will allow, e.g., culling geometry at the level of Béziers. Before, the closing of subpaths lived as a post-processing step after flattening. The actual change that starts using this for something more exciting is in #1341. This by itself does not really bring down timings (plus noise makes it hard to measure). ``` flatten/Ghostscript_Tiger time: [203.93 µs 204.28 µs 204.68 µs] change: [-2.1022% -1.8481% -1.5684%] (p = 0.00 < 0.05) Performance has improved. Found 12 outliers among 100 measurements (12.00%) 10 (10.00%) high mild 2 (2.00%) high severe flatten/paris-30k time: [12.222 ms 12.298 ms 12.384 ms] change: [+0.7747% +1.3966% +2.2290%] (p = 0.00 < 0.05) Change within noise threshold. Found 13 outliers among 100 measurements (13.00%) 13 (13.00%) high severe ```
1 parent ee0d227 commit cc2dd70

File tree

2 files changed

+118
-115
lines changed

2 files changed

+118
-115
lines changed

sparse_strips/vello_common/src/flatten.rs

Lines changed: 15 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
//! Flattening filled and stroked paths.
55
6-
use crate::flatten_simd::Callback;
6+
use crate::flatten_simd::{Callback, LinePathEl};
77
use crate::kurbo::{self, Affine, PathEl, Stroke, StrokeCtx, StrokeOpts};
88
use alloc::vec::Vec;
99
use fearless_simd::{Level, Simd, dispatch};
@@ -102,18 +102,13 @@ pub fn fill_impl<S: Simd>(
102102

103103
let mut lb = FlattenerCallback {
104104
line_buf,
105-
start: kurbo::Point::default(),
106-
p0: kurbo::Point::default(),
105+
start: Point::ZERO,
106+
p0: Point::ZERO,
107107
is_nan: false,
108-
closed: false,
109108
};
110109

111110
crate::flatten_simd::flatten(simd, iter, TOL, &mut lb, flatten_ctx);
112111

113-
if !lb.closed {
114-
close_path(lb.start, lb.p0, lb.line_buf);
115-
}
116-
117112
// A path that contains NaN is ill-defined, so ignore it.
118113
if lb.is_nan {
119114
warn!("A path contains NaN, ignoring it.");
@@ -154,50 +149,28 @@ pub fn expand_stroke(
154149

155150
struct FlattenerCallback<'a> {
156151
line_buf: &'a mut Vec<Line>,
157-
start: kurbo::Point,
158-
p0: kurbo::Point,
152+
start: Point,
153+
p0: Point,
159154
is_nan: bool,
160-
closed: bool,
161155
}
162156

163157
impl Callback for FlattenerCallback<'_> {
164158
#[inline(always)]
165-
fn callback(&mut self, el: PathEl) {
166-
self.is_nan |= el.is_nan();
167-
159+
fn callback(&mut self, el: LinePathEl) {
168160
match el {
169-
kurbo::PathEl::MoveTo(p) => {
170-
if !self.closed && self.p0 != self.start {
171-
close_path(self.start, self.p0, self.line_buf);
172-
}
161+
LinePathEl::MoveTo(p) => {
162+
self.is_nan |= p.is_nan();
173163

174-
self.closed = false;
175-
self.start = p;
176-
self.p0 = p;
177-
}
178-
kurbo::PathEl::LineTo(p) => {
179-
let pt0 = Point::new(self.p0.x as f32, self.p0.y as f32);
180-
let pt1 = Point::new(p.x as f32, p.y as f32);
181-
self.line_buf.push(Line::new(pt0, pt1));
182-
self.p0 = p;
164+
self.start = Point::new(p.x as f32, p.y as f32);
165+
self.p0 = self.start;
183166
}
184-
el @ (kurbo::PathEl::QuadTo(_, _) | kurbo::PathEl::CurveTo(_, _, _)) => {
185-
unreachable!("Path has been flattened, so shouldn't contain {el:?}.")
186-
}
187-
kurbo::PathEl::ClosePath => {
188-
self.closed = true;
167+
LinePathEl::LineTo(p) => {
168+
self.is_nan |= p.is_nan();
189169

190-
close_path(self.start, self.p0, self.line_buf);
170+
let p = Point::new(p.x as f32, p.y as f32);
171+
self.line_buf.push(Line::new(self.p0, p));
172+
self.p0 = p;
191173
}
192174
}
193175
}
194176
}
195-
196-
fn close_path(start: kurbo::Point, p0: kurbo::Point, line_buf: &mut Vec<Line>) {
197-
let pt0 = Point::new(p0.x as f32, p0.y as f32);
198-
let pt1 = Point::new(start.x as f32, start.y as f32);
199-
200-
if pt0 != pt1 {
201-
line_buf.push(Line::new(pt0, pt1));
202-
}
203-
}

sparse_strips/vello_common/src/flatten_simd.rs

Lines changed: 103 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,27 @@ use alloc::vec::Vec;
1313
use bytemuck::{Pod, Zeroable};
1414
use fearless_simd::*;
1515

16+
/// The element of a path made of lines.
17+
///
18+
/// Each subpath must start with a `MoveTo`. Closing of subpaths is not supported, and subpaths are
19+
/// not closed implicitly when a new subpath (with `MoveTo`) is started. It is expected that closed
20+
/// subpaths are watertight in the sense that the last `LineTo` matches exactly with the first
21+
/// `MoveTo`.
22+
///
23+
/// This intentionally allows for non-watertight subpaths, as, e.g., lines that are fully outside
24+
/// of the viewport do not need to be drawn.
25+
///
26+
/// See [`PathEl`] for a more general-purpose path element type.
27+
pub(crate) enum LinePathEl {
28+
MoveTo(Point),
29+
LineTo(Point),
30+
}
31+
1632
// Unlike kurbo, which takes a closure with a callback for outputting the lines, we use a trait
1733
// instead. The reason is that this way the callback can be inlined, which is not possible with
1834
// a closure and turned out to have a noticeable overhead.
1935
pub(crate) trait Callback {
20-
fn callback(&mut self, el: PathEl);
36+
fn callback(&mut self, el: LinePathEl);
2137
}
2238

2339
/// See the docs for the kurbo implementation of flattening:
@@ -35,97 +51,111 @@ pub(crate) fn flatten<S: Simd>(
3551
flatten_ctx.flattened_cubics.clear();
3652

3753
let sqrt_tol = tolerance.sqrt();
38-
let mut last_pt = None;
54+
let mut closed = true;
55+
let mut start_pt = Point::ZERO;
56+
let mut last_pt = Point::ZERO;
3957

4058
for el in path {
4159
match el {
4260
PathEl::MoveTo(p) => {
43-
last_pt = Some(p);
44-
callback.callback(PathEl::MoveTo(p));
61+
if !closed && last_pt != start_pt {
62+
callback.callback(LinePathEl::LineTo(start_pt));
63+
}
64+
closed = false;
65+
last_pt = p;
66+
start_pt = p;
67+
callback.callback(LinePathEl::MoveTo(p));
4568
}
4669
PathEl::LineTo(p) => {
47-
last_pt = Some(p);
48-
callback.callback(PathEl::LineTo(p));
70+
debug_assert!(!closed, "Expected a `MoveTo` before a `LineTo`");
71+
last_pt = p;
72+
callback.callback(LinePathEl::LineTo(p));
4973
}
5074
PathEl::QuadTo(p1, p2) => {
51-
if let Some(p0) = last_pt {
52-
// An upper bound on the shortest distance of any point on the quadratic Bezier
53-
// curve to the line segment [p0, p2] is 1/2 of the maximum of the
54-
// endpoint-to-control-point distances.
55-
//
56-
// The derivation is similar to that for the cubic Bezier (see below). In
57-
// short:
58-
//
59-
// q(t) = B0(t) p0 + B1(t) p1 + B2(t) p2
60-
// dist(q(t), [p0, p1]) <= B1(t) dist(p1, [p0, p1])
61-
// = 2 (1-t)t dist(p1, [p0, p1]).
62-
//
63-
// The maximum occurs at t=1/2, hence
64-
// max(dist(q(t), [p0, p1] <= 1/2 dist(p1, [p0, p1])).
65-
//
66-
// A cheap upper bound for dist(p1, [p0, p1]) is max(dist(p1, p0), dist(p1, p2)).
67-
//
68-
// The following takes the square to elide the square root of the Euclidean
69-
// distance.
70-
if f64::max((p1 - p0).hypot2(), (p1 - p2).hypot2()) <= 4. * TOL_2 {
71-
callback.callback(PathEl::LineTo(p2));
72-
} else {
73-
let q = QuadBez::new(p0, p1, p2);
74-
let params = q.estimate_subdiv(sqrt_tol);
75-
let n = ((0.5 * params.val / sqrt_tol).ceil() as usize).max(1);
76-
let step = 1.0 / (n as f64);
77-
for i in 1..n {
78-
let u = (i as f64) * step;
79-
let t = q.determine_subdiv_t(&params, u);
80-
let p = q.eval(t);
81-
callback.callback(PathEl::LineTo(p));
82-
}
83-
callback.callback(PathEl::LineTo(p2));
75+
debug_assert!(!closed, "Expected a `MoveTo` before a `QuadTo`");
76+
let p0 = last_pt;
77+
// An upper bound on the shortest distance of any point on the quadratic Bezier
78+
// curve to the line segment [p0, p2] is 1/2 of the maximum of the
79+
// endpoint-to-control-point distances.
80+
//
81+
// The derivation is similar to that for the cubic Bezier (see below). In
82+
// short:
83+
//
84+
// q}(t) = B0(t) p0 + B1(t) p1 + B2(t) p2
85+
// dist(q(t), [p0, p1]) <= B1(t) dist(p1, [p0, p1])
86+
// = 2 (1-t)t dist(p1, [p0, p1]).
87+
//
88+
// The maximum occurs at t=1/2, hence
89+
// max(dist(q(t), [p0, p1] <= 1/2 dist(p1, [p0, p1])).
90+
//
91+
// A cheap upper bound for dist(p1, [p0, p1]) is max(dist(p1, p0), dist(p1, p2)).
92+
//
93+
// The following takes the square to elide the square root of the Euclidean
94+
// distance.
95+
if f64::max((p1 - p0).hypot2(), (p1 - p2).hypot2()) <= 4. * TOL_2 {
96+
callback.callback(LinePathEl::LineTo(p2));
97+
} else {
98+
let q = QuadBez::new(p0, p1, p2);
99+
let params = q.estimate_subdiv(sqrt_tol);
100+
let n = ((0.5 * params.val / sqrt_tol).ceil() as usize).max(1);
101+
let step = 1.0 / (n as f64);
102+
for i in 1..n {
103+
let u = (i as f64) * step;
104+
let t = q.determine_subdiv_t(&params, u);
105+
let p = q.eval(t);
106+
callback.callback(LinePathEl::LineTo(p));
84107
}
108+
callback.callback(LinePathEl::LineTo(p2));
85109
}
86-
last_pt = Some(p2);
110+
last_pt = p2;
87111
}
88112
PathEl::CurveTo(p1, p2, p3) => {
89-
if let Some(p0) = last_pt {
90-
// An upper bound on the shortest distance of any point on the cubic Bezier
91-
// curve to the line segment [p0, p3] is 3/4 of the maximum of the
92-
// endpoint-to-control-point distances.
93-
//
94-
// With Bernstein weights Bi(t), we have
95-
// c(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 + B3(t) p3
96-
// with t from 0 to 1 (inclusive).
97-
//
98-
// Through convexivity of the Euclidean distance function and the line segment,
99-
// we have
100-
// dist(c(t), [p0, p3]) <= B1(t) dist(p1, [p0, p3]) + B2(t) dist(p2, [p0, p3])
101-
// <= B1(t) ||p1-p0|| + B2(t) ||p2-p3||
102-
// <= (B1(t) + B2(t)) max(||p1-p0||, ||p2-p3|||)
103-
// = 3 ((1-t)t^2 + (1-t)^2t) max(||p1-p0||, ||p2-p3||).
104-
//
105-
// The inner polynomial has its maximum of 1/4 at t=1/2, hence
106-
// max(dist(c(t), [p0, p3])) <= 3/4 max(||p1-p0||, ||p2-p3||).
107-
//
108-
// The following takes the square to elide the square root of the Euclidean
109-
// distance.
110-
if f64::max((p0 - p1).hypot2(), (p3 - p2).hypot2()) <= 16. / 9. * TOL_2 {
111-
callback.callback(PathEl::LineTo(p3));
112-
} else {
113-
let c = CubicBez::new(p0, p1, p2, p3);
114-
let max = flatten_cubic_simd(simd, c, flatten_ctx, tolerance as f32);
115-
116-
for p in &flatten_ctx.flattened_cubics[1..max] {
117-
callback.callback(PathEl::LineTo(Point::new(p.x as f64, p.y as f64)));
118-
}
113+
debug_assert!(!closed, "Expected a `MoveTo` before a `CurveTo`");
114+
let p0 = last_pt;
115+
// An upper bound on the shortest distance of any point on the cubic Bezier
116+
// curve to the line segment [p0, p3] is 3/4 of the maximum of the
117+
// endpoint-to-control-point distances.
118+
//
119+
// With Bernstein weights Bi(t), we have
120+
// c(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 + B3(t) p3
121+
// with t from 0 to 1 (inclusive).
122+
//
123+
// Through convexivity of the Euclidean distance function and the line segment,
124+
// we have
125+
// dist(c(t), [p0, p3]) <= B1(t) dist(p1, [p0, p3]) + B2(t) dist(p2, [p0, p3])
126+
// <= B1(t) ||p1-p0|| + B2(t) ||p2-p3||
127+
// <= (B1(t) + B2(t)) max(||p1-p0||, ||p2-p3|||)
128+
// = 3 ((1-t)t^2 + (1-t)^2t) max(||p1-p0||, ||p2-p3||).
129+
//
130+
// The inner polynomial has its maximum of 1/4 at t=1/2, hence
131+
// max(dist(c(t), [p0, p3])) <= 3/4 max(||p1-p0||, ||p2-p3||).
132+
//
133+
// The following takes the square to elide the square root of the Euclidean
134+
// distance.
135+
if f64::max((p0 - p1).hypot2(), (p3 - p2).hypot2()) <= 16. / 9. * TOL_2 {
136+
callback.callback(LinePathEl::LineTo(p3));
137+
} else {
138+
let c = CubicBez::new(p0, p1, p2, p3);
139+
let max = flatten_cubic_simd(simd, c, flatten_ctx, tolerance as f32);
140+
141+
for p in &flatten_ctx.flattened_cubics[1..max] {
142+
callback.callback(LinePathEl::LineTo(Point::new(p.x as f64, p.y as f64)));
119143
}
120144
}
121-
last_pt = Some(p3);
145+
last_pt = p3;
122146
}
123147
PathEl::ClosePath => {
124-
last_pt = None;
125-
callback.callback(PathEl::ClosePath);
148+
closed = true;
149+
if last_pt != start_pt {
150+
callback.callback(LinePathEl::LineTo(start_pt));
151+
}
126152
}
127153
}
128154
}
155+
156+
if !closed && last_pt != start_pt {
157+
callback.callback(LinePathEl::LineTo(start_pt));
158+
}
129159
}
130160

131161
// The below methods are copied from kurbo and needed to implement flattening of normal quad curves.

0 commit comments

Comments
 (0)