Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sparse_strips/vello_bench/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ impl DataItem {
path.transform,
&mut temp_buf,
&mut FlattenCtx::default(),
self.width,
self.height,
);
line_buf.extend(&temp_buf);
}
Expand All @@ -103,6 +105,8 @@ impl DataItem {
&mut temp_buf,
&mut FlattenCtx::default(),
&mut StrokeCtx::default(),
self.width,
self.height,
);
line_buf.extend(&temp_buf);
}
Expand Down
4 changes: 4 additions & 0 deletions sparse_strips/vello_bench/src/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions sparse_strips/vello_common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
132 changes: 124 additions & 8 deletions sparse_strips/vello_common/src/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ impl Point {
}
}

impl From<kurbo::Point> 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;

Expand Down Expand Up @@ -76,25 +86,118 @@ 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<Item = PathEl>,
affine: Affine,
line_buf: &mut Vec<Line>,
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<S: Simd>(
simd: S,
path: impl IntoIterator<Item = PathEl>,
affine: Affine,
line_buf: &mut Vec<Line>,
flatten_ctx: &mut FlattenCtx,
width: u16,
height: u16,
) {
line_buf.clear();
let iter = path.into_iter().map(
Expand All @@ -109,7 +212,7 @@ pub fn fill_impl<S: Simd>(
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 {
Expand All @@ -118,7 +221,9 @@ pub fn fill_impl<S: Simd>(
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<Item = PathEl>,
Expand All @@ -127,6 +232,8 @@ pub fn stroke(
line_buf: &mut Vec<Line>,
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
Expand All @@ -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.
Expand All @@ -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;
}
Expand Down
58 changes: 47 additions & 11 deletions sparse_strips/vello_common/src/flatten_simd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ pub(crate) fn flatten<S: Simd>(
path: impl IntoIterator<Item = PathEl>,
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;
Expand All @@ -72,11 +77,26 @@ pub(crate) fn flatten<S: Simd>(
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
Expand All @@ -88,8 +108,9 @@ pub(crate) fn flatten<S: Simd>(
//
// 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);
Expand All @@ -109,7 +130,22 @@ pub(crate) fn flatten<S: Simd>(
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.
//
Expand All @@ -128,11 +164,11 @@ pub(crate) fn flatten<S: Simd>(
//
// 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 {
Expand Down
4 changes: 4 additions & 0 deletions sparse_strips/vello_common/src/strip_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down
7 changes: 6 additions & 1 deletion sparse_strips/vello_common/src/tile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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]
Expand Down
Loading