Skip to content

Add fast path for stripping rectangles #900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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
184 changes: 184 additions & 0 deletions sparse_strips/vello_common/src/strip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use vello_api::peniko::Fill;

use crate::flatten::Line;
use crate::kurbo::Rect;
use crate::tile::{Tile, Tiles};

/// A strip.
Expand Down Expand Up @@ -302,3 +303,186 @@ pub fn render(
}
}
}

/// Draw the strips of a rectangle. This is faster than using the normal path, because we
/// do not need to go through the "flatten", "tiling" and "sort" stages, but can instead
/// directly emit strips.
pub fn render_rect(
rect: &Rect,
strip_buf: &mut Vec<Strip>,
alpha_buf: &mut Vec<u8>,
width: u16,
height: u16,
) {
// The idea for this fast path is as follows:
// - We generate strips of width 1 for the left as well as the right side of the rectangle. The
// left side has a winding number of 0, the right side has a winding number of 1.
// - We generate a strip of the full rectangle width for the top and bottom part of the rectangle.
// - Of course, it's also possible that a rectangle has a width of less than 2 or a height of
// less than 4. The current logic does account for those edge cases.
// - There could be some further optimizations (for example, if a rectangle is strip-aligned on
// the y-axis, we don't need the strips for the top part of the rectangle), but I don't think
// those edge cases are worth adding to the complexity of this method.

strip_buf.clear();

// Don't try to draw empty rectangles.
if rect.is_zero_area() {
return;
}

// Note that we currently deal with negative-area rects as positive-area rects.
// Shouldn't be a problem for solid fill, but might need some tweaking for gradient
// and pattern fills.
let (x0, x1, y0, y1) = (
rect.min_x().max(0.0) as f32,
rect.max_x().min(width as f64) as f32,
rect.min_y().max(0.0) as f32,
rect.max_y().min(height as f64) as f32,
);

let top_strip_idx = (y0 as u16) / Tile::HEIGHT;
let top_strip_y = top_strip_idx * Tile::HEIGHT;
// In the wide tile generation stage, there is an assertion that all strips outside the
// viewport must have been culled, so we cull here.
//
// This index is inclusive, i.e. pixels at row `bottom_strip_idx`
// are still part of the rectangle.
let bottom_strip_idx = (y1 as u16).min(height - 1) / Tile::HEIGHT;
let bottom_strip_y = bottom_strip_idx * Tile::HEIGHT;

let x0_floored = x0.floor();
let x1_floored = x1.floor();

let x_start = x0_floored as u16;
// Inclusive, i.e. the pixel at column `x_end` is the very right border (possibly only anti-aliased)
// of the rectangle, which should still be stripped.
let x_end = (x1_floored as u16).min(width - 1);

// Calculate the vertical/horizontal coverage of a pixel, using a start
// and end point. The area between the start and end point is considered to be
// covered by the shape.
let pixel_coverage = |pixel_pos: u16, start: f32, end: f32| {
let pixel_pos = pixel_pos as f32;
let end = (end - pixel_pos).clamp(0.0, 1.0);
let start = (start - pixel_pos).clamp(0.0, 1.0);

end - start
};

// Calculate the alpha coverages of the strips containing the top/bottom
// borders of the rectangle.
let vertical_alpha_coverage = |strip_y: u16| {
let mut buf = [0.0_f32; Tile::HEIGHT as usize];

// For each row in the strip, calculate how much it is covered by given the
// vertical endpoints y0 and y1.
for i in 0..Tile::HEIGHT {
buf[i as usize] = pixel_coverage(strip_y + i, y0, y1);
}

buf
};

// Note that the alpha coverage of all pixels on either the left or ride side of a
// rectangle is always the same (except for corners), so we just need to calculate
// a single value. The coverage of corners will be calculated by adding an additional
// opacity mask as calculated in `horizontal_alphas`.
let left_alpha = pixel_coverage(x_start, x0, x1);
let right_alpha = pixel_coverage(x_end, x0, x1);

// Calculate the alpha coverages of a strip using an alpha mask. For example, if we
// want to calculate the coverage of the very first column of the top line in the
// rect (which might start at the horizontal offset .5), then we need to multiply
// all its alpha values by 0.5 to account for anti-aliasing of the left edge.
let push_alpha = |col_alphas: &[f32; 4], alpha_mask: f32, alpha_buffer: &mut Vec<u8>| {
for alpha in col_alphas {
let u8_alpha = ((*alpha * alpha_mask) * 255.0 + 0.5) as u8;
alpha_buffer.push(u8_alpha);
}
};

// Create a strip for the top/bottom edge of the rectangle.
let horizontal_strip = |alpha_buffer: &mut Vec<u8>,
strip_buffer: &mut Vec<Strip>,
col_alphas: &[f32; 4],
strip_y: u16| {
// Strip the first column, which might have an additional alpha mask due to non-integer
// alignment of x0. If the rectangle is less than 1 pixel wide, this will represent
// the total coverage of the rectangle inside the pixel.
let alpha_idx = alpha_buffer.len() as u32;
push_alpha(col_alphas, left_alpha, alpha_buffer);

// If the rect covers more than one pixel horizontally, fill all the remaining ones
// except for the last one with the same opacity as in `alphas`.
// If the rect is contained within one pixel horizontally,
// then right_alpha == left_alpha, and thus the alpha we pushed above is enough.
if x_end - x_start >= 1 {
for _ in (x_start + 1)..x_end {
push_alpha(col_alphas, 1.0, alpha_buffer);
}

// Fill the last, right column, which might also need an additional alpha mask
// due to non-integer alignment of x1.
push_alpha(col_alphas, right_alpha, alpha_buffer);
}

// Push the actual strip.
strip_buffer.push(Strip {
x: x0_floored as u16,
y: strip_y,
alpha_idx,
winding: 0,
});
};

let top_alphas = vertical_alpha_coverage(top_strip_y);
// Create the strip for the top part of the rectangle.
horizontal_strip(alpha_buf, strip_buf, &top_alphas, top_strip_y);

// If rect covers more than one strip vertically, we need to strip the vertical line
// segments of the rectangle, and finally the bottom horizontal line segment.
if top_strip_idx != bottom_strip_idx {
let alphas = [1.0, 1.0, 1.0, 1.0];

// Strip all parts that are inside the rectangle (i.e. neither the top nor the
// bottom part. In this case, all pixels will have full opacity).
for i in (top_strip_idx + 1)..bottom_strip_idx {
// Left side (and right side if rect is only one pixel wide).
let mut alpha_idx = alpha_buf.len() as u32;
push_alpha(&alphas, left_alpha, alpha_buf);

strip_buf.push(Strip {
x: x0_floored as u16,
y: i * Tile::HEIGHT,
alpha_idx,
winding: 0,
});

if x_end > x_start {
// Right side.
alpha_idx = alpha_buf.len() as u32;
push_alpha(&alphas, right_alpha, alpha_buf);

strip_buf.push(Strip {
x: x1_floored as u16,
y: i * Tile::HEIGHT,
alpha_idx,
winding: 1,
});
}
}

// Strip the bottom part of the rectangle.
let bottom_alphas = vertical_alpha_coverage(bottom_strip_y);
horizontal_strip(alpha_buf, strip_buf, &bottom_alphas, bottom_strip_y);
}

// Push sentinel strip.
strip_buf.push(Strip {
x: u16::MAX,
y: bottom_strip_y,
alpha_idx: alpha_buf.len() as u32,
winding: 0,
});
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 43 additions & 3 deletions sparse_strips/vello_cpu/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ impl RenderContext {

/// Fill a rectangle.
pub fn fill_rect(&mut self, rect: &Rect) {
self.fill_path(&rect.to_path(DEFAULT_TOLERANCE));
if self.transform.has_skew() {
self.fill_path(&rect.to_path(DEFAULT_TOLERANCE));
} else {
let rect = transform_non_skewed_rect(rect, self.transform);
self.render_rect(&rect, self.paint.clone());
}
}

/// Stroke a rectangle.
Expand Down Expand Up @@ -206,6 +211,18 @@ impl RenderContext {
self.wide.generate(&self.strip_buf, fill_rule, paint);
}

fn render_rect(&mut self, rect: &Rect, paint: Paint) {
self.tiles.reset();
strip::render_rect(
rect,
&mut self.strip_buf,
&mut self.alphas,
self.width,
self.height,
);
self.wide.generate(&self.strip_buf, Fill::NonZero, paint);
}

fn make_strips(&mut self, fill_rule: Fill) {
self.tiles
.make_tiles(&self.line_buf, self.width, self.height);
Expand Down Expand Up @@ -246,17 +263,40 @@ impl GlyphRenderer for RenderContext {
}
}

trait AffineExt {
fn has_skew(&self) -> bool;
}

impl AffineExt for Affine {
fn has_skew(&self) -> bool {
let coeffs = self.as_coeffs();

coeffs[1] != 0.0 || coeffs[2] != 0.0
}
}

fn transform_non_skewed_rect(rect: &Rect, affine: Affine) -> Rect {
debug_assert!(
!affine.has_skew(),
"this method should only be called with non-skewing transforms"
);
let [a, _, _, d, _, _] = affine.as_coeffs();

Rect::new(a * rect.x0, d * rect.y0, a * rect.x1, d * rect.y1) + affine.translation()
}

#[cfg(test)]
mod tests {
use crate::RenderContext;
use vello_common::kurbo::Rect;
use crate::render::DEFAULT_TOLERANCE;
use vello_common::kurbo::{Rect, Shape};

#[test]
fn reset_render_context() {
let mut ctx = RenderContext::new(100, 100);
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);

ctx.fill_rect(&rect);
ctx.fill_path(&rect.to_path(DEFAULT_TOLERANCE));

assert!(!ctx.line_buf.is_empty());
assert!(!ctx.strip_buf.is_empty());
Expand Down
47 changes: 47 additions & 0 deletions sparse_strips/vello_cpu/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,17 @@ fn filled_unaligned_rect() {
check_ref(&ctx, "filled_unaligned_rect");
}

#[test]
fn filled_unaligned_rect_as_path() {
let mut ctx = get_ctx(30, 20, false);
let rect = Rect::new(1.5, 1.5, 28.5, 18.5).to_path(0.1);

ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into());
ctx.fill_path(&rect);

check_ref(&ctx, "filled_unaligned_rect_as_path");
}

#[test]
fn filled_transformed_rect_1() {
let mut ctx = get_ctx(30, 30, false);
Expand Down Expand Up @@ -438,6 +449,18 @@ fn strip_inscribed_rect() {
check_ref(&ctx, "strip_inscribed_rect");
}

// Should yield the same result as `strip_inscribed_rect`.
#[test]
fn strip_inscribed_rect_as_path() {
let mut ctx = get_ctx(30, 20, false);
let rect = Rect::new(1.5, 9.5, 28.5, 11.5);

ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into());
ctx.fill_path(&rect.to_path(0.1));

check_ref(&ctx, "strip_inscribed_rect");
}

#[test]
fn filled_vertical_hairline_rect() {
let mut ctx = get_ctx(5, 8, false);
Expand All @@ -449,6 +472,18 @@ fn filled_vertical_hairline_rect() {
check_ref(&ctx, "filled_vertical_hairline_rect");
}

// Should yield the same result as `filled_vertical_hairline_rect`.
#[test]
fn filled_vertical_hairline_rect_as_path() {
let mut ctx = get_ctx(5, 8, false);
let rect = Rect::new(2.25, 0.0, 2.75, 8.0).to_path(0.1);

ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into());
ctx.fill_path(&rect);

check_ref(&ctx, "filled_vertical_hairline_rect");
}

#[test]
fn filled_vertical_hairline_rect_2() {
let mut ctx = get_ctx(10, 10, false);
Expand All @@ -460,6 +495,18 @@ fn filled_vertical_hairline_rect_2() {
check_ref(&ctx, "filled_vertical_hairline_rect_2");
}

// Should yield the same result as `filled_vertical_hairline_rect_2`.
#[test]
fn filled_vertical_hairline_rect_as_path_2() {
let mut ctx = get_ctx(10, 10, false);
let rect = Rect::new(4.5, 0.5, 5.5, 9.5);

ctx.set_paint(REBECCA_PURPLE.with_alpha(0.5).into());
ctx.fill_path(&rect.to_path(0.1));

check_ref(&ctx, "filled_vertical_hairline_rect_2");
}

#[test]
fn oversized_star() {
let mut ctx = get_ctx(100, 100, true);
Expand Down