Skip to content

Commit 78108a4

Browse files
committed
perf(vello_common): Cull Bèziers path elements during flattening
This conservatively checks whether Bèzier path elements we're about to flatten are outside the viewport. If they are fully to the right, top, or bottom of the viewport, the Bèzier does not impact pixel coverage or coarse winding at all, and can be ignored. If it is fully to the left, it does impact pixel coverage and coarse winding, but only the element's start and endpoint y-values matter, not the exact shape, meaning we can just yield a line rather than finely flattening. If more or less everything ends up to be in the viewport, the additional calculation is wasted and increases flattening time by ~3%. If geometry ends up culled, flattening and tiling times can be reduced significantly, but this is of course workload-dependent. The following two Ghostscript Tiger's have their viewboxes reduced to `50 50 100 100` and `90 90 20 20`, down from `0 0 200 200`. Their flattening time is reduce by 52% and 90% respectively, and their tiling time by 22% and 60%. Flattening timings: ``` flatten/Ghostscript_Tiger time: [209.94 µs 210.21 µs 210.51 µs] change: [+2.6850% +3.1753% +3.6309%] (p = 0.00 < 0.05) Performance has regressed. Found 5 outliers among 100 measurements (5.00%) 4 (4.00%) high mild 1 (1.00%) high severe flatten/Ghostscript_Tiger-viewboxed time: [97.189 µs 97.287 µs 97.399 µs] change: [-52.787% -52.650% -52.514%] (p = 0.00 < 0.05) Performance has improved. Found 8 outliers among 100 measurements (8.00%) 6 (6.00%) high mild 2 (2.00%) high severe flatten/Ghostscript_Tiger-viewboxed-extreme time: [19.722 µs 19.741 µs 19.761 µs] change: [-90.311% -90.280% -90.255%] (p = 0.00 < 0.05) Performance has improved. Found 8 outliers among 100 measurements (8.00%) 7 (7.00%) high mild 1 (1.00%) high severe flatten/paris-30k time: [12.740 ms 12.764 ms 12.788 ms] change: [+2.6014% +3.3631% +4.0837%] (p = 0.00 < 0.05) Performance has regressed. Found 3 outliers among 100 measurements (3.00%) 3 (3.00%) high mild ``` Tiling timings: ``` tile/Ghostscript_Tiger time: [175.39 µs 175.79 µs 176.28 µs] change: [-0.4403% -0.0016% +0.4400%] (p = 1.00 > 0.05) No change in performance detected. Found 1 outliers among 50 measurements (2.00%) 1 (2.00%) high mild tile/Ghostscript_Tiger-viewboxed time: [78.932 µs 79.147 µs 79.409 µs] change: [-23.209% -22.803% -22.369%] (p = 0.00 < 0.05) Performance has improved. Found 5 outliers among 50 measurements (10.00%) 1 (2.00%) high mild 4 (8.00%) high severe tile/Ghostscript_Tiger-viewboxed-extreme time: [13.378 µs 13.390 µs 13.405 µs] change: [-60.417% -60.306% -60.199%] (p = 0.00 < 0.05) Performance has improved. Found 6 outliers among 50 measurements (12.00%) 2 (4.00%) high mild 4 (8.00%) high severe tile/paris-30k time: [20.970 ms 21.001 ms 21.034 ms] change: [-0.4881% -0.2397% +0.0108%] (p = 0.07 > 0.05) No change in performance detected. Found 1 outliers among 50 measurements (2.00%) 1 (2.00%) high mild ```
1 parent ded9210 commit 78108a4

6 files changed

Lines changed: 95 additions & 17 deletions

File tree

sparse_strips/vello_bench/src/data.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ impl DataItem {
8686
path.transform,
8787
&mut temp_buf,
8888
&mut FlattenCtx::default(),
89+
self.width,
90+
self.height,
8991
);
9092
line_buf.extend(&temp_buf);
9193
}
@@ -103,6 +105,8 @@ impl DataItem {
103105
&mut temp_buf,
104106
&mut FlattenCtx::default(),
105107
&mut StrokeCtx::default(),
108+
self.width,
109+
self.height,
106110
);
107111
line_buf.extend(&temp_buf);
108112
}

sparse_strips/vello_bench/src/flatten.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub fn flatten(c: &mut Criterion) {
3333
path.transform,
3434
&mut temp_buf,
3535
&mut flatten_ctx,
36+
$item.width,
37+
$item.height,
3638
);
3739
line_buf.extend(&temp_buf);
3840
}
@@ -44,6 +46,8 @@ pub fn flatten(c: &mut Criterion) {
4446
Affine::IDENTITY,
4547
&mut temp_buf,
4648
&mut flatten_ctx,
49+
$item.width,
50+
$item.height,
4751
);
4852
line_buf.extend(&temp_buf);
4953
}

sparse_strips/vello_common/src/flatten.rs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ impl Point {
3434
}
3535
}
3636

37+
impl From<crate::kurbo::Point> for Point {
38+
#[inline(always)]
39+
fn from(value: crate::kurbo::Point) -> Self {
40+
Self {
41+
x: value.x as f32,
42+
y: value.y as f32,
43+
}
44+
}
45+
}
46+
3747
impl core::ops::Add for Point {
3848
type Output = Self;
3949

@@ -81,8 +91,10 @@ pub fn fill(
8191
affine: Affine,
8292
line_buf: &mut Vec<Line>,
8393
ctx: &mut FlattenCtx,
94+
width: u16,
95+
height: u16,
8496
) {
85-
dispatch!(level, simd => fill_impl(simd, path, affine, line_buf, ctx));
97+
dispatch!(level, simd => fill_impl(simd, path, affine, line_buf, ctx, width, height));
8698
}
8799

88100
/// Flatten a filled bezier path into line segments.
@@ -93,12 +105,10 @@ pub fn fill_impl<S: Simd>(
93105
affine: Affine,
94106
line_buf: &mut Vec<Line>,
95107
flatten_ctx: &mut FlattenCtx,
108+
width: u16,
109+
height: u16,
96110
) {
97111
line_buf.clear();
98-
let iter = path.into_iter().map(
99-
#[inline(always)]
100-
|el| affine * el,
101-
);
102112

103113
let mut lb = FlattenerCallback {
104114
line_buf,
@@ -107,7 +117,7 @@ pub fn fill_impl<S: Simd>(
107117
is_nan: false,
108118
};
109119

110-
crate::flatten_simd::flatten(simd, iter, TOL, &mut lb, flatten_ctx);
120+
crate::flatten_simd::flatten(simd, affine, path, TOL, &mut lb, flatten_ctx, width, height);
111121

112122
// A path that contains NaN is ill-defined, so ignore it.
113123
if lb.is_nan {
@@ -125,6 +135,8 @@ pub fn stroke(
125135
line_buf: &mut Vec<Line>,
126136
flatten_ctx: &mut FlattenCtx,
127137
stroke_ctx: &mut StrokeCtx,
138+
width: u16,
139+
height: u16,
128140
) {
129141
// TODO: Temporary hack to ensure that strokes are scaled properly by the transform.
130142
let tolerance = TOL
@@ -134,7 +146,15 @@ pub fn stroke(
134146
.max(1.);
135147

136148
expand_stroke(path, style, tolerance, stroke_ctx);
137-
fill(level, stroke_ctx.output(), affine, line_buf, flatten_ctx);
149+
fill(
150+
level,
151+
stroke_ctx.output(),
152+
affine,
153+
line_buf,
154+
flatten_ctx,
155+
width,
156+
height,
157+
);
138158
}
139159

140160
/// Expand a stroked path to a filled path.
@@ -161,13 +181,14 @@ impl Callback for FlattenerCallback<'_> {
161181
LinePathEl::MoveTo(p) => {
162182
self.is_nan |= p.is_nan();
163183

164-
self.start = Point::new(p.x as f32, p.y as f32);
165-
self.p0 = self.start;
184+
let p = p.into();
185+
self.start = p;
186+
self.p0 = p;
166187
}
167188
LinePathEl::LineTo(p) => {
168189
self.is_nan |= p.is_nan();
169190

170-
let p = Point::new(p.x as f32, p.y as f32);
191+
let p = p.into();
171192
self.line_buf.push(Line::new(self.p0, p));
172193
self.p0 = p;
173194
}

sparse_strips/vello_common/src/flatten_simd.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use crate::flatten::TOL_2;
99
#[cfg(not(feature = "std"))]
1010
use crate::kurbo::common::FloatFuncs as _;
11-
use crate::kurbo::{CubicBez, ParamCurve, PathEl, Point, QuadBez};
11+
use crate::kurbo::{Affine, CubicBez, ParamCurve, PathEl, Point, QuadBez};
1212
use alloc::vec::Vec;
1313
use bytemuck::{Pod, Zeroable};
1414
use fearless_simd::*;
@@ -43,19 +43,27 @@ pub(crate) trait Callback {
4343
#[inline(always)]
4444
pub(crate) fn flatten<S: Simd>(
4545
simd: S,
46+
affine: Affine,
4647
path: impl IntoIterator<Item = PathEl>,
4748
tolerance: f64,
4849
callback: &mut impl Callback,
4950
flatten_ctx: &mut FlattenCtx,
51+
width: u16,
52+
height: u16,
5053
) {
5154
flatten_ctx.flattened_cubics.clear();
5255

56+
let width = width as f64;
57+
let height = height as f64;
58+
5359
let sqrt_tol = tolerance.sqrt();
5460
let mut closed = true;
5561
let mut start_pt = Point::ZERO;
5662
let mut last_pt = Point::ZERO;
5763

5864
for el in path {
65+
let el = affine * el;
66+
5967
match el {
6068
PathEl::MoveTo(p) => {
6169
if !closed && last_pt != start_pt {
@@ -74,8 +82,22 @@ pub(crate) fn flatten<S: Simd>(
7482
PathEl::QuadTo(p1, p2) => {
7583
debug_assert!(!closed, "Expected a `MoveTo` before a `QuadTo`");
7684
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
85+
// If the quadratic Bèzier is fully to the right, top, or bottom of the viewport,
86+
// it does not impact pixel coverage or winding. We can ignore it. The following
87+
// checks that conservatively by checking whether the bounding box of the Bèzier's
88+
// control points is fully to the right, top, or bottom of the viewport.
89+
if [p0, p1, p2].into_iter().all(|p| p.x > width)
90+
|| [p0, p1, p2].into_iter().all(|p| p.y < 0.)
91+
|| [p0, p1, p2].into_iter().all(|p| p.y > height)
92+
{
93+
callback.callback(LinePathEl::MoveTo(p2));
94+
}
95+
// The following checks two things. First, if the quadratic Bèzier is fully to the
96+
// left of the viewport, it may affect pixel coverage and winding, but its exact
97+
// shape does not matter. It can be emitted as a line segment [p0, p2].
98+
//
99+
// Second, an upper bound on the shortest distance of any point on the quadratic
100+
// Bèzier curve to the line segment [p0, p2] is 1/2 of the maximum of the
79101
// endpoint-to-control-point distances.
80102
//
81103
// The derivation is similar to that for the cubic Bezier (see below). In
@@ -92,7 +114,9 @@ pub(crate) fn flatten<S: Simd>(
92114
//
93115
// The following takes the square to elide the square root of the Euclidean
94116
// distance.
95-
if f64::max((p1 - p0).hypot2(), (p1 - p2).hypot2()) <= 4. * TOL_2 {
117+
else if [p0, p1, p2].into_iter().all(|p| p.x < 0.)
118+
|| f64::max((p1 - p0).hypot2(), (p1 - p2).hypot2()) <= 4. * TOL_2
119+
{
96120
callback.callback(LinePathEl::LineTo(p2));
97121
} else {
98122
let q = QuadBez::new(p0, p1, p2);
@@ -112,7 +136,21 @@ pub(crate) fn flatten<S: Simd>(
112136
PathEl::CurveTo(p1, p2, p3) => {
113137
debug_assert!(!closed, "Expected a `MoveTo` before a `CurveTo`");
114138
let p0 = last_pt;
115-
// An upper bound on the shortest distance of any point on the cubic Bezier
139+
// If the cubic Bèzier is fully to the right, top, or bottom of the viewport, it
140+
// does not impact pixel coverage or winding. We can ignore it. The following
141+
// checks that conservatively by checking whether the bounding box of the Bèzier's
142+
// control points is fully to the right, top, or bottom of the viewport.
143+
if [p0, p1, p2, p3].into_iter().all(|p| p.x > width)
144+
|| [p0, p1, p2, p3].into_iter().all(|p| p.y < 0.)
145+
|| [p0, p1, p2, p3].into_iter().all(|p| p.y > height)
146+
{
147+
callback.callback(LinePathEl::MoveTo(p3));
148+
}
149+
// The following checks two things. First, if the cubic Bèzier is fully to the
150+
// left of the viewport, it may affect pixel coverage and winding, but its exact
151+
// shape does not matter. It can be emitted as a line segment [p0, p3].
152+
//
153+
// Second, an upper bound on the shortest distance of any point on the cubic Bèzier
116154
// curve to the line segment [p0, p3] is 3/4 of the maximum of the
117155
// endpoint-to-control-point distances.
118156
//
@@ -132,7 +170,9 @@ pub(crate) fn flatten<S: Simd>(
132170
//
133171
// The following takes the square to elide the square root of the Euclidean
134172
// distance.
135-
if f64::max((p0 - p1).hypot2(), (p3 - p2).hypot2()) <= 16. / 9. * TOL_2 {
173+
else if [p0, p1, p2].into_iter().all(|p| p.x < 0.)
174+
|| f64::max((p0 - p1).hypot2(), (p3 - p2).hypot2()) <= 16. / 9. * TOL_2
175+
{
136176
callback.callback(LinePathEl::LineTo(p3));
137177
} else {
138178
let c = CubicBez::new(p0, p1, p2, p3);

sparse_strips/vello_common/src/strip_generator.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ impl StripGenerator {
102102
transform,
103103
&mut self.line_buf,
104104
&mut self.flatten_ctx,
105+
self.width,
106+
self.height,
105107
);
106108

107109
self.generate_with_clip(aliasing_threshold, strip_storage, fill_rule, clip_path);
@@ -125,6 +127,8 @@ impl StripGenerator {
125127
&mut self.line_buf,
126128
&mut self.flatten_ctx,
127129
&mut self.stroke_ctx,
130+
self.width,
131+
self.height,
128132
);
129133
self.generate_with_clip(aliasing_threshold, strip_storage, Fill::NonZero, clip_path);
130134
}

sparse_strips/vello_common/src/tile.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,9 @@ mod tests {
13411341

13421342
#[test]
13431343
fn vertical_path_on_the_right_of_viewport() {
1344+
const VIEWPORT_WIDTH: u16 = 10;
1345+
const VIEWPORT_HEIGHT: u16 = 10;
1346+
13441347
let path = BezPath::from_svg("M261,0 L78848,0 L78848,4 L261,4 Z").unwrap();
13451348
let mut line_buf = vec![];
13461349
fill(
@@ -1349,10 +1352,12 @@ mod tests {
13491352
Affine::IDENTITY,
13501353
&mut line_buf,
13511354
&mut FlattenCtx::default(),
1355+
VIEWPORT_WIDTH,
1356+
VIEWPORT_HEIGHT,
13521357
);
13531358

13541359
let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1355-
tiles.assert_tiles_match(&line_buf, 10, 10, &[]);
1360+
tiles.assert_tiles_match(&line_buf, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, &[]);
13561361
}
13571362

13581363
#[test]

0 commit comments

Comments
 (0)