diff --git a/sparse_strips/vello_bench/src/data.rs b/sparse_strips/vello_bench/src/data.rs index 237a4168f..5a3b1c83b 100644 --- a/sparse_strips/vello_bench/src/data.rs +++ b/sparse_strips/vello_bench/src/data.rs @@ -86,6 +86,8 @@ impl DataItem { path.transform, &mut temp_buf, &mut FlattenCtx::default(), + self.width, + self.height, ); line_buf.extend(&temp_buf); } @@ -103,6 +105,8 @@ impl DataItem { &mut temp_buf, &mut FlattenCtx::default(), &mut StrokeCtx::default(), + self.width, + self.height, ); line_buf.extend(&temp_buf); } diff --git a/sparse_strips/vello_bench/src/flatten.rs b/sparse_strips/vello_bench/src/flatten.rs index 2209a9080..4a67f8185 100644 --- a/sparse_strips/vello_bench/src/flatten.rs +++ b/sparse_strips/vello_bench/src/flatten.rs @@ -33,6 +33,8 @@ pub fn flatten(c: &mut Criterion) { path.transform, &mut temp_buf, &mut flatten_ctx, + $item.width, + $item.height, ); line_buf.extend(&temp_buf); } @@ -44,6 +46,8 @@ pub fn flatten(c: &mut Criterion) { Affine::IDENTITY, &mut temp_buf, &mut flatten_ctx, + $item.width, + $item.height, ); line_buf.extend(&temp_buf); } diff --git a/sparse_strips/vello_common/CHANGELOG.md b/sparse_strips/vello_common/CHANGELOG.md index 6de85132f..a8ddae1d2 100644 --- a/sparse_strips/vello_common/CHANGELOG.md +++ b/sparse_strips/vello_common/CHANGELOG.md @@ -19,6 +19,9 @@ This release has an [MSRV][] of 1.88. - Improved Bézier flattening performance by catching more Béziers whose chords are immediately within rendering tolerance. ([#1216][] by [@tomcur][]) - Significantly improved rendering performance of scenes including blend layers by ensuring no commands are generated for wide tiles without layer content. ([#1399][] by [@tomcur][]) +- Improved flattening and tiling performance by culling out-of-viewport Béziers before flattening. ([#1341][] by [@tomcur][]) + This drops all out-of-viewport geometry that does not impact winding or pixel coverage (i.e., geometry above, to the right, or below the viewport). + For geometry that is to the left of the viewport (impacting winding), this skips flattening by directly yielding the Bézier's chord. ### Fixed @@ -121,6 +124,7 @@ See also the [vello_cpu 0.0.1](../vello_cpu/CHANGELOG.md#001---2025-05-10) relea [#1329]: https://github.com/linebender/vello/pull/1329 [#1336]: https://github.com/linebender/vello/pull/1327 [#1338]: https://github.com/linebender/vello/pull/1327 +[#1341]: https://github.com/linebender/vello/pull/1341 [#1349]: https://github.com/linebender/vello/pull/1349 [#1353]: https://github.com/linebender/vello/pull/1353 [#1354]: https://github.com/linebender/vello/pull/1354 diff --git a/sparse_strips/vello_common/src/flatten.rs b/sparse_strips/vello_common/src/flatten.rs index 9451a48a4..f9707635d 100644 --- a/sparse_strips/vello_common/src/flatten.rs +++ b/sparse_strips/vello_common/src/flatten.rs @@ -36,6 +36,16 @@ impl Point { } } +impl From for Point { + #[inline(always)] + fn from(value: kurbo::Point) -> Self { + Self { + x: value.x as f32, + y: value.y as f32, + } + } +} + impl core::ops::Add for Point { type Output = Self; @@ -76,18 +86,109 @@ impl Line { } } -/// Flatten a filled bezier path into line segments. +/// Flatten a filled Bézier path into line segments. +/// +/// # Open subpaths and culling +/// +/// Open subpaths in the input path get closed by connecting the last endpoint in the subpath to +/// the starting point. The output lines in `line_buf` describe the flattened path, but these lines +/// may describe open subpaths, as some path elements may have been culled. +/// +/// For example, consider the following, where the box describes the viewport, a path is marked by +/// `*`, and the region to be filled in the viewport is shaded. For ease of drawing the ASCII art, +/// the path elements are all lines ([`PathEl::LineTo`]), but the same also holds for Bézier path +/// elements. +/// +/// ```text +/// ---> winding scan direction +/// +/// * * * * * +/// * * +/// ---------- * ----- * +/// | *░░░░░░░| * +/// | *░░░░░░░░░| * +/// | *░░░░░░░░░░░| * +/// | *░░░░░░░░░░░░░| * +/// |*░░░░░░░░░░░░░░░| * +/// *|░░░░░░░░░░░░░░░░| * +/// * |░░░░░░░░░░░░░░░░| * +/// * |░░░░░░░░░░░░░░░░| * +/// * ------------------ * +/// * * +/// * * +/// * * +/// * * +/// * * +/// * * +/// * * +/// * * * * * * * * * * * * * * +/// ``` +/// +/// Because the winding scan direction is from left to right, only the left-of-viewport and +/// diagonal lines matter in later stages of rendering for the winding number and pixel coverage. +/// The other three lines can be culled. +/// +/// ```text +/// * +/// * +/// ---------- * ----- +/// | *░░░░░░░| +/// | *░░░░░░░░░| +/// | *░░░░░░░░░░░| +/// | *░░░░░░░░░░░░░| +/// |*░░░░░░░░░░░░░░░| +/// *|░░░░░░░░░░░░░░░░| +/// * |░░░░░░░░░░░░░░░░| +/// * |░░░░░░░░░░░░░░░░| +/// * ------------------ +/// * +/// * +/// * +/// * +/// * +/// * +/// * +/// ``` +/// +/// It is important to keep these flattened subpaths open after culling, as closing the subpaths +/// might yield different geometry like the following. +/// +/// ```text +/// * +/// ** +/// ---------- * *---- +/// | *░░* | +/// | *░░░* | +/// | *░░░░* | +/// | *░░░░░* | +/// |*░░░░░░* | +/// *|░░░░░░* | +/// * |░░░░░* | +/// * |░░░░* | +/// * --- * ------------ +/// * * +/// * * +/// * * +/// * * +/// * * +/// ** +/// * +/// ``` pub fn fill( level: Level, path: impl IntoIterator, affine: Affine, line_buf: &mut Vec, ctx: &mut FlattenCtx, + width: u16, + height: u16, ) { - dispatch!(level, simd => fill_impl(simd, path, affine, line_buf, ctx)); + dispatch!(level, simd => fill_impl(simd, path, affine, line_buf, ctx, width, height)); } /// Flatten a filled bezier path into line segments. +/// +/// See the note about open subpaths and culling on [`fill`]. #[inline(always)] pub fn fill_impl( simd: S, @@ -95,6 +196,8 @@ pub fn fill_impl( affine: Affine, line_buf: &mut Vec, flatten_ctx: &mut FlattenCtx, + width: u16, + height: u16, ) { line_buf.clear(); let iter = path.into_iter().map( @@ -109,7 +212,7 @@ pub fn fill_impl( is_nan: false, }; - crate::flatten_simd::flatten(simd, iter, &mut lb, flatten_ctx); + crate::flatten_simd::flatten(simd, iter, &mut lb, flatten_ctx, width, height); // A path that contains NaN is ill-defined, so ignore it. if lb.is_nan { @@ -118,7 +221,9 @@ pub fn fill_impl( line_buf.clear(); } } -/// Flatten a stroked bezier path into line segments. +/// Flatten a stroked Bézier path into line segments. +/// +/// See the note about open subpaths and culling on [`fill`]. pub fn stroke( level: Level, path: impl IntoIterator, @@ -127,6 +232,8 @@ pub fn stroke( line_buf: &mut Vec, flatten_ctx: &mut FlattenCtx, stroke_ctx: &mut StrokeCtx, + width: u16, + height: u16, ) { // TODO: Temporary hack to ensure that strokes are scaled properly by the transform. let tolerance = TOL @@ -136,7 +243,15 @@ pub fn stroke( .max(1.); expand_stroke(path, style, tolerance, stroke_ctx); - fill(level, stroke_ctx.output(), affine, line_buf, flatten_ctx); + fill( + level, + stroke_ctx.output(), + affine, + line_buf, + flatten_ctx, + width, + height, + ); } /// Expand a stroked path to a filled path. @@ -163,13 +278,14 @@ impl Callback for FlattenerCallback<'_> { LinePathEl::MoveTo(p) => { self.is_nan |= p.is_nan(); - self.start = Point::new(p.x as f32, p.y as f32); - self.p0 = self.start; + let p = p.into(); + self.start = p; + self.p0 = p; } LinePathEl::LineTo(p) => { self.is_nan |= p.is_nan(); - let p = Point::new(p.x as f32, p.y as f32); + let p = p.into(); self.line_buf.push(Line::new(self.p0, p)); self.p0 = p; } diff --git a/sparse_strips/vello_common/src/flatten_simd.rs b/sparse_strips/vello_common/src/flatten_simd.rs index 10f447d05..32e2e64b1 100644 --- a/sparse_strips/vello_common/src/flatten_simd.rs +++ b/sparse_strips/vello_common/src/flatten_simd.rs @@ -46,9 +46,14 @@ pub(crate) fn flatten( path: impl IntoIterator, callback: &mut impl Callback, flatten_ctx: &mut FlattenCtx, + width: u16, + height: u16, ) { flatten_ctx.flattened_cubics.clear(); + let width = width as f64; + let height = height as f64; + let mut closed = true; let mut start_pt = Point::ZERO; let mut last_pt = Point::ZERO; @@ -72,11 +77,26 @@ pub(crate) fn flatten( PathEl::QuadTo(p1, p2) => { debug_assert!(!closed, "Expected a `MoveTo` before a `QuadTo`"); let p0 = last_pt; - // An upper bound on the shortest distance of any point on the quadratic Bezier - // curve to the line segment [p0, p2] is 1/2 of the control-point-to-line-segment + let line = Line::new(p0, p2); + // If the quadratic Bézier is fully to the right, top, or bottom of the viewport, + // it does not impact pixel coverage or winding. We can ignore it. The following + // checks that conservatively by checking whether the bounding box of the Bézier's + // control points is fully to the right, top, or bottom of the viewport. + if [p0, p1, p2].into_iter().all(|p| p.x > width) + || [p0, p1, p2].into_iter().all(|p| p.y < 0.) + || [p0, p1, p2].into_iter().all(|p| p.y > height) + { + callback.callback(LinePathEl::MoveTo(p2)); + } + // The following checks two things. First, if the quadratic Bézier is fully to the + // left of the viewport, it may affect pixel coverage and winding, but its exact + // shape does not matter. It can be emitted as a line segment [p0, p2]. + // + // Second, an upper bound on the shortest distance of any point on the quadratic + // Bézier curve to the line segment [p0, p2] is 1/2 of the control-point-to-line-segment // distance. // - // The derivation is similar to that for the cubic Bezier (see below). In + // The derivation is similar to that for the cubic Bézier (see below). In // short: // // q(t) = B0(t) p0 + B1(t) p1 + B2(t) p2 @@ -88,8 +108,9 @@ pub(crate) fn flatten( // // The following takes the square to elide the square root of the Euclidean // distance. - let line = Line::new(p0, p2); - if line.nearest(p1, 0.).distance_sq <= 4. * TOL_2 { + else if [p0, p1, p2].into_iter().all(|p| p.x < 0.) + || line.nearest(p1, 0.).distance_sq <= 4. * TOL_2 + { callback.callback(LinePathEl::LineTo(p2)); } else { let q = QuadBez::new(p0, p1, p2); @@ -109,7 +130,22 @@ pub(crate) fn flatten( PathEl::CurveTo(p1, p2, p3) => { debug_assert!(!closed, "Expected a `MoveTo` before a `CurveTo`"); let p0 = last_pt; - // An upper bound on the shortest distance of any point on the cubic Bezier + let line = Line::new(p0, p3); + // If the cubic Bézier is fully to the right, top, or bottom of the viewport, it + // does not impact pixel coverage or winding. We can ignore it. The following + // checks that conservatively by checking whether the bounding box of the Bézier's + // control points is fully to the right, top, or bottom of the viewport. + if [p0, p1, p2, p3].into_iter().all(|p| p.x > width) + || [p0, p1, p2, p3].into_iter().all(|p| p.y < 0.) + || [p0, p1, p2, p3].into_iter().all(|p| p.y > height) + { + callback.callback(LinePathEl::MoveTo(p3)); + } + // The following checks two things. First, if the cubic Bézier is fully to the + // left of the viewport, it may affect pixel coverage and winding, but its exact + // shape does not matter. It can be emitted as a line segment [p0, p3]. + // + // Second, an upper bound on the shortest distance of any point on the cubic Bézier // curve to the line segment [p0, p3] is 3/4 of the maximum of the // control-point-to-line-segment distances. // @@ -128,11 +164,11 @@ pub(crate) fn flatten( // // The following takes the square to elide the square root of the Euclidean // distance. - let line = Line::new(p0, p3); - if f64::max( - line.nearest(p1, 0.).distance_sq, - line.nearest(p2, 0.).distance_sq, - ) <= 16. / 9. * TOL_2 + else if [p0, p1, p2, p3].into_iter().all(|p| p.x < 0.) + || f64::max( + line.nearest(p1, 0.).distance_sq, + line.nearest(p2, 0.).distance_sq, + ) <= 16. / 9. * TOL_2 { callback.callback(LinePathEl::LineTo(p3)); } else { diff --git a/sparse_strips/vello_common/src/strip_generator.rs b/sparse_strips/vello_common/src/strip_generator.rs index 24414310b..3c519dfcb 100644 --- a/sparse_strips/vello_common/src/strip_generator.rs +++ b/sparse_strips/vello_common/src/strip_generator.rs @@ -102,6 +102,8 @@ impl StripGenerator { transform, &mut self.line_buf, &mut self.flatten_ctx, + self.width, + self.height, ); self.generate_with_clip(aliasing_threshold, strip_storage, fill_rule, clip_path); @@ -125,6 +127,8 @@ impl StripGenerator { &mut self.line_buf, &mut self.flatten_ctx, &mut self.stroke_ctx, + self.width, + self.height, ); self.generate_with_clip(aliasing_threshold, strip_storage, Fill::NonZero, clip_path); } diff --git a/sparse_strips/vello_common/src/tile.rs b/sparse_strips/vello_common/src/tile.rs index b93a45bb2..a58491107 100644 --- a/sparse_strips/vello_common/src/tile.rs +++ b/sparse_strips/vello_common/src/tile.rs @@ -1251,6 +1251,9 @@ mod tests { #[test] fn vertical_path_on_the_right_of_viewport() { + const VIEWPORT_WIDTH: u16 = 10; + const VIEWPORT_HEIGHT: u16 = 10; + let path = BezPath::from_svg("M261,0 L78848,0 L78848,4 L261,4 Z").unwrap(); let mut line_buf = vec![]; fill( @@ -1259,10 +1262,12 @@ mod tests { Affine::IDENTITY, &mut line_buf, &mut FlattenCtx::default(), + VIEWPORT_WIDTH, + VIEWPORT_HEIGHT, ); let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback())); - tiles.assert_tiles_match(&line_buf, 10, 10, &[]); + tiles.assert_tiles_match(&line_buf, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, &[]); } #[test] diff --git a/sparse_strips/vello_toy/src/debug.rs b/sparse_strips/vello_toy/src/debug.rs index ac1df2643..a373b3093 100644 --- a/sparse_strips/vello_toy/src/debug.rs +++ b/sparse_strips/vello_toy/src/debug.rs @@ -49,6 +49,8 @@ fn main() { Affine::IDENTITY, &mut line_buf, &mut FlattenCtx::default(), + args.width, + args.height, ); } else { let stroke = Stroke { @@ -66,6 +68,8 @@ fn main() { &mut line_buf, &mut FlattenCtx::default(), &mut StrokeCtx::default(), + args.width, + args.height, ); } }