blob: 164e2d818886ef82db837b793928d65fbe16286d [file] [edit]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Helpers for decomposing rectangles used by the rectangle fast path.
use vello_common::geometry::RectU16;
use vello_common::kurbo::Rect;
/// The threshold of the rectangle size after which a rectangle should be split up
/// into multiple smaller ones.
const LARGE_RECT_SPLIT_THRESHOLD: u16 = 32;
/// Integer rectangle geometry and its packed fractional edge coverage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RectPart {
/// Pixel-aligned bounds of this rectangle part.
pub(crate) rect: RectU16,
/// Packed fractional coverage for the four edges.
pub(crate) frac: u32,
}
impl RectPart {
pub(crate) fn shift(self, shift: (i32, i32)) -> Self {
Self {
rect: self.rect.shift(shift),
..self
}
}
}
/// A decomposed rectangle.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SplitRect {
/// Main rectangle interior, or the complete rectangle when it is not split.
pub(crate) main: RectPart,
/// Top antialiased strip, if required.
pub(crate) top: Option<RectPart>,
/// Bottom antialiased strip, if required.
pub(crate) bottom: Option<RectPart>,
/// Left antialiased strip between the top and bottom strips, if required.
pub(crate) left: Option<RectPart>,
/// Right antialiased strip between the top and bottom strips, if required.
pub(crate) right: Option<RectPart>,
}
#[expect(
clippy::cast_possible_truncation,
reason = "recorded rect coordinates are clipped to the u16 viewport domain before packing"
)]
pub(crate) fn split_rect(rect: &Rect) -> SplitRect {
let sx0 = rect.x0.floor();
let sy0 = rect.y0.floor();
let sx1 = rect.x1.ceil();
let sy1 = rect.y1.ceil();
let x = sx0 as u16;
let y = sy0 as u16;
// Are guaranteed to be > 0 since we rejected negative rectangles.
let width = (sx1 - sx0) as u16;
let height = (sy1 - sy0) as u16;
// Note that `top_frac` and `left_frac` store the actual coverage, while
// `right_frac` and `bottom_frac` store one minus the coverage. This is on purpose
// and handled that way in the shader.
let left_frac = (rect.x0 - sx0) as f32;
let top_frac = (rect.y0 - sy0) as f32;
let right_frac = (sx1 - rect.x1) as f32;
let bottom_frac = (sy1 - rect.y1) as f32;
// There's a balance to strike between reducing work in the fragment shader by splitting
// out the inner part of the rectangle without anti-aliasing, and additional overhead
// that arises from rendering 5 rectangles instead of just one. While the exact threshold
// will obviously depend on the device, some experiments on a low-tier tablet showed that
// `LARGE_RECT_SPLIT_THRESHOLD` seems to be a a reasonable value.
if rect.x1 - rect.x0 < f64::from(LARGE_RECT_SPLIT_THRESHOLD)
|| rect.y1 - rect.y0 < f64::from(LARGE_RECT_SPLIT_THRESHOLD)
{
return SplitRect {
main: RectPart {
rect: RectU16::new(x, y, x + width, y + height),
frac: pack_unorm4x8([left_frac, top_frac, right_frac, bottom_frac]),
},
top: None,
bottom: None,
left: None,
right: None,
};
}
let has_left_aa = left_frac > 0.0;
let has_top_aa = top_frac > 0.0;
let has_right_aa = right_frac > 0.0;
let has_bottom_aa = bottom_frac > 0.0;
let has_top_strip = has_top_aa || has_left_aa || has_right_aa;
let has_bottom_strip = has_bottom_aa || has_left_aa || has_right_aa;
let left_inset = u16::from(has_left_aa);
let right_inset = u16::from(has_right_aa);
let top_inset = u16::from(has_top_strip);
let bottom_inset = u16::from(has_bottom_strip);
let inner_x = x + left_inset;
let inner_y = y + top_inset;
// Can't underflow because rectangles have at least `LARGE_RECT_SPLIT_THRESHOLD` in each
// direction, which is larger than 2.
let inner_width = width - left_inset - right_inset;
let inner_height = height - top_inset - bottom_inset;
SplitRect {
main: RectPart {
rect: RectU16::new(
inner_x,
inner_y,
inner_x + inner_width,
inner_y + inner_height,
),
frac: 0,
},
top: has_top_strip.then_some(RectPart {
rect: RectU16::new(x, y, x + width, y + 1),
frac: pack_unorm4x8([left_frac, top_frac, right_frac, 0.0]),
}),
bottom: has_bottom_strip.then_some(RectPart {
rect: RectU16::new(x, y + height - 1, x + width, y + height),
frac: pack_unorm4x8([left_frac, 0.0, right_frac, bottom_frac]),
}),
left: has_left_aa.then_some(RectPart {
rect: RectU16::new(x, inner_y, x + 1, inner_y + inner_height),
frac: pack_unorm4x8([left_frac, 0.0, 0.0, 0.0]),
}),
right: has_right_aa.then_some(RectPart {
rect: RectU16::new(x + width - 1, inner_y, x + width, inner_y + inner_height),
frac: pack_unorm4x8([0.0, 0.0, right_frac, 0.0]),
}),
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "normalized color channels are packed into u8 lanes"
)]
fn pack_unorm4x8(v: [f32; 4]) -> u32 {
let q = |f: f32| -> u8 { (f * 255.0 + 0.5) as u8 };
u32::from(q(v[0]))
| (u32::from(q(v[1])) << 8)
| (u32::from(q(v[2])) << 16)
| (u32::from(q(v[3])) << 24)
}
#[cfg(test)]
mod tests {
use super::{RectPart, SplitRect, pack_unorm4x8, split_rect};
use vello_common::geometry::RectU16;
use vello_common::kurbo::Rect;
fn part(x: u16, y: u16, width: u16, height: u16, frac: [f32; 4]) -> RectPart {
RectPart {
rect: RectU16::new(x, y, x + width, y + height),
frac: pack_unorm4x8(frac),
}
}
#[test]
fn splitter_keeps_small_rect_whole() {
let rect = Rect::new(10.25, 20.5, 25.75, 35.25);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 20, 16, 16, [0.25, 0.5, 0.25, 0.75]),
top: None,
bottom: None,
left: None,
right: None,
}
);
}
#[test]
fn splitter_keeps_subpixel_rect_inside_one_pixel() {
let rect = Rect::new(10.125, 20.25, 10.875, 20.75);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 20, 1, 1, [0.125, 0.25, 0.125, 0.25]),
top: None,
bottom: None,
left: None,
right: None,
}
);
}
#[test]
fn splitter_keeps_subpixel_rect_spanning_two_pixels_in_width() {
let rect = Rect::new(10.75, 20.125, 11.25, 20.875);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 20, 2, 1, [0.75, 0.125, 0.75, 0.125]),
top: None,
bottom: None,
left: None,
right: None,
}
);
}
#[test]
fn splitter_keeps_subpixel_rect_spanning_two_pixels_in_height() {
let rect = Rect::new(10.125, 20.75, 10.875, 21.25);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 20, 1, 2, [0.125, 0.75, 0.125, 0.75]),
top: None,
bottom: None,
left: None,
right: None,
}
);
}
#[test]
fn splitter_keeps_multi_pixel_width_rect_within_one_pixel_height() {
let rect = Rect::new(10.25, 20.125, 14.75, 20.875);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 20, 5, 1, [0.25, 0.125, 0.25, 0.125]),
top: None,
bottom: None,
left: None,
right: None,
}
);
}
#[test]
fn splitter_keeps_multi_pixel_height_rect_within_one_pixel_width() {
let rect = Rect::new(10.125, 20.25, 10.875, 24.75);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 20, 1, 5, [0.125, 0.25, 0.125, 0.25]),
top: None,
bottom: None,
left: None,
right: None,
}
);
}
#[test]
fn splitter_splits_large_rect_into_five_parts() {
let rect = Rect::new(10.25, 20.5, 42.75, 52.75);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(11, 21, 31, 31, [0.0, 0.0, 0.0, 0.0]),
top: Some(part(10, 20, 33, 1, [0.25, 0.5, 0.25, 0.0])),
bottom: Some(part(10, 52, 33, 1, [0.25, 0.0, 0.25, 0.25])),
left: Some(part(10, 21, 1, 31, [0.25, 0.0, 0.0, 0.0])),
right: Some(part(42, 21, 1, 31, [0.0, 0.0, 0.25, 0.0])),
}
);
}
#[test]
fn splitter_omits_unneeded_edge_parts() {
let rect = Rect::new(10.0, 20.5, 42.0, 53.0);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 21, 32, 32, [0.0, 0.0, 0.0, 0.0]),
top: Some(part(10, 20, 32, 1, [0.0, 0.5, 0.0, 0.0])),
bottom: None,
left: None,
right: None,
}
);
}
#[test]
fn splitter_handles_large_rect_with_only_vertical_aa() {
let rect = Rect::new(5.0, 2.25, 37.0, 34.75);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(5, 3, 32, 31, [0.0, 0.0, 0.0, 0.0]),
top: Some(part(5, 2, 32, 1, [0.0, 0.25, 0.0, 0.0])),
bottom: Some(part(5, 34, 32, 1, [0.0, 0.0, 0.0, 0.25])),
left: None,
right: None,
}
);
}
#[test]
fn splitter_keeps_large_aligned_rect_as_single_main_rect() {
let rect = Rect::new(10.0, 20.0, 42.0, 60.0);
let split = split_rect(&rect);
assert_eq!(
split,
SplitRect {
main: part(10, 20, 32, 40, [0.0, 0.0, 0.0, 0.0]),
top: None,
bottom: None,
left: None,
right: None,
}
);
}
}