blob: d43c7887555ed4744497db03970b878845f31d20 [file] [log] [blame]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Rendering strips.
use alloc::vec::Vec;
use vello_api::peniko::Fill;
use crate::flatten::Line;
use crate::tile::{Tile, Tiles};
/// A strip.
#[derive(Debug, Clone, Copy)]
pub struct Strip {
/// The x coordinate of the strip, in user coordinates.
pub x: u16,
/// The y coordinate of the strip, in user coordinates.
pub y: u16,
/// The index into the alpha buffer.
pub alpha_idx: u32,
/// The winding number at the start of the strip.
pub winding: i32,
}
impl Strip {
/// Return the y coordinate of the strip, in strip units.
pub fn strip_y(&self) -> u16 {
self.y / Tile::HEIGHT
}
}
/// Render the tiles stored in `tiles` into the strip and alpha buffer.
/// The strip buffer will be cleared in the beginning.
pub fn render(
tiles: &Tiles,
strip_buf: &mut Vec<Strip>,
alpha_buf: &mut Vec<u8>,
fill_rule: Fill,
lines: &[Line],
) {
strip_buf.clear();
if tiles.is_empty() {
return;
}
// The accumulated tile winding delta. A line that crosses the top edge of a tile
// increments the delta if the line is directed upwards, and decrements it if goes
// downwards. Horizontal lines leave it unchanged.
let mut winding_delta: i32 = 0;
// The previous tile visited.
let mut prev_tile = *tiles.get(0);
// The accumulated (fractional) winding of the tile-sized location we're currently at.
// Note multiple tiles can be at the same location.
let mut location_winding = [[0_f32; Tile::HEIGHT as usize]; Tile::WIDTH as usize];
// The accumulated (fractional) windings at this location's right edge. When we move to the
// next location, this is splatted to that location's starting winding.
let mut accumulated_winding = [0_f32; Tile::HEIGHT as usize];
/// A special tile to keep the logic below simple.
const SENTINEL: Tile = Tile::new(u16::MAX, u16::MAX, 0, false);
// The strip we're building.
let mut strip = Strip {
x: prev_tile.x * Tile::WIDTH,
y: prev_tile.y * Tile::HEIGHT,
alpha_idx: alpha_buf.len() as u32,
winding: 0,
};
for (tile_idx, tile) in tiles.iter().copied().chain([SENTINEL]).enumerate() {
let line = lines[tile.line_idx() as usize];
let tile_left_x = f32::from(tile.x) * f32::from(Tile::WIDTH);
let tile_top_y = f32::from(tile.y) * f32::from(Tile::HEIGHT);
let p0_x = line.p0.x - tile_left_x;
let p0_y = line.p0.y - tile_top_y;
let p1_x = line.p1.x - tile_left_x;
let p1_y = line.p1.y - tile_top_y;
// Push out the winding as an alpha mask when we move to the next location (i.e., a tile
// without the same location).
if !prev_tile.same_loc(&tile) {
macro_rules! fill {
($rule:expr) => {
for x in 0..Tile::WIDTH as usize {
for y in 0..Tile::HEIGHT as usize {
let area = location_winding[x][y];
let coverage = $rule(area);
alpha_buf.push((coverage * 255.0 + 0.5) as u8);
}
}
};
}
match fill_rule {
Fill::NonZero => {
fill!(|area: f32| area.abs())
}
Fill::EvenOdd => {
// As in other parts of the code, we avoid using `round` since it's very
// slow on x86.
fill!(|area: f32| (area - 2.0 * ((0.5 * area) + 0.5).floor()).abs())
}
};
#[expect(clippy::needless_range_loop, reason = "dimension clarity")]
for x in 0..Tile::WIDTH as usize {
location_winding[x] = accumulated_winding;
}
}
// Push out the strip if we're moving to a next strip.
if !prev_tile.same_loc(&tile) && !prev_tile.prev_loc(&tile) {
debug_assert_eq!(
(prev_tile.x + 1) * Tile::WIDTH - strip.x,
((alpha_buf.len() - strip.alpha_idx as usize) / usize::from(Tile::HEIGHT)) as u16,
"The number of columns written to the alpha buffer should equal the number of columns spanned by this strip."
);
strip_buf.push(strip);
let is_sentinel = tile_idx == tiles.len() as usize;
if !prev_tile.same_row(&tile) {
// Emit a final strip in the row if there is non-zero winding for the sparse fill,
// or unconditionally if we've reached the sentinel tile to end the path (the
// `alpha_idx` field is used for width calculations).
if winding_delta != 0 || is_sentinel {
strip_buf.push(Strip {
x: u16::MAX,
y: prev_tile.y * Tile::HEIGHT,
alpha_idx: alpha_buf.len() as u32,
winding: winding_delta,
});
}
winding_delta = 0;
accumulated_winding.fill(0.);
#[expect(clippy::needless_range_loop, reason = "dimension clarity")]
for x in 0..Tile::WIDTH as usize {
location_winding[x].fill(0.);
}
}
if is_sentinel {
break;
}
strip = Strip {
x: tile.x * Tile::WIDTH,
y: tile.y * Tile::HEIGHT,
alpha_idx: alpha_buf.len() as u32,
winding: winding_delta,
};
// Note: this fill is mathematically not necessary. It provides a way to reduce
// accumulation of float rounding errors.
accumulated_winding.fill(winding_delta as f32);
}
prev_tile = tile;
// TODO: horizontal geometry has no impact on winding. This branch will be removed when
// horizontal geometry is culled at the tile-generation stage.
if p0_y == p1_y {
continue;
}
// Lines moving upwards (in a y-down coordinate system) add to winding; lines moving
// downwards subtract from winding.
let sign = (p0_y - p1_y).signum();
// Calculate winding / pixel area coverage.
//
// Conceptually, horizontal rays are shot from left to right. Every time the ray crosses a
// line that is directed upwards (decreasing `y`), the winding is incremented. Every time
// the ray crosses a line moving downwards (increasing `y`), the winding is decremented.
// The fractional area coverage of a pixel is the integral of the winding within it.
//
// Practically, to calculate this, each pixel is considered individually, and we determine
// whether the line moves through this pixel. The line's y-delta within this pixel is
// accumulated and added to the area coverage of pixels to the right. Within the pixel
// itself, the area to the right of the line segment forms a trapezoid (or a triangle in
// the degenerate case). The area of this trapezoid is added to the pixel's area coverage.
//
// For example, consider the following pixel square, with a line indicated by asterisks
// starting inside the pixel and crossing its bottom edge. The area covered is the
// trapezoid on the bottom-right enclosed by the line and the pixel square. The area is
// positive if the line moves down, and negative otherwise.
//
// __________________
// | |
// | *------|
// | * |
// | * |
// | * |
// | * |
// | * |
// |___*____________|
// *
// *
let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
(p0_y, p0_x, p1_y, p1_x)
} else {
(p1_y, p1_x, p0_y, p0_x)
};
let (line_left_x, line_left_y, line_right_x) = if p0_x < p1_x {
(p0_x, p0_y, p1_x)
} else {
(p1_x, p1_y, p0_x)
};
let y_slope = (line_bottom_y - line_top_y) / (line_bottom_x - line_top_x);
let x_slope = 1. / y_slope;
winding_delta += sign as i32 * i32::from(tile.winding());
// TODO: this should be removed when out-of-viewport tiles are culled at the
// tile-generation stage. That requires calculating and forwarding winding to strip
// generation.
if tile.x == 0 && line_left_x < 0. {
let (ymin, ymax) = if line.p0.x == line.p1.x {
(line_top_y, line_bottom_y)
} else {
let line_viewport_left_y = (line_top_y - line_top_x * y_slope)
.max(line_top_y)
.min(line_bottom_y);
(
f32::min(line_left_y, line_viewport_left_y),
f32::max(line_left_y, line_viewport_left_y),
)
};
for y_idx in 0..Tile::HEIGHT {
let px_top_y = f32::from(y_idx);
let px_bottom_y = 1. + f32::from(y_idx);
let ymin = f32::max(ymin, px_top_y);
let ymax = f32::min(ymax, px_bottom_y);
let h = (ymax - ymin).max(0.);
accumulated_winding[y_idx as usize] += sign * h;
for x_idx in 0..Tile::WIDTH {
location_winding[x_idx as usize][y_idx as usize] += sign * h;
}
}
if line_right_x < 0. {
// Early exit, as no part of the line is inside the tile.
continue;
}
}
for y_idx in 0..Tile::HEIGHT {
let px_top_y = f32::from(y_idx);
let px_bottom_y = 1. + f32::from(y_idx);
let ymin = f32::max(line_top_y, px_top_y);
let ymax = f32::min(line_bottom_y, px_bottom_y);
let mut acc = 0.;
for x_idx in 0..Tile::WIDTH {
let px_left_x = f32::from(x_idx);
let px_right_x = 1. + f32::from(x_idx);
// The y-coordinate of the intersections between the line and the pixel's left and
// right edges respectively.
//
// There is some subtlety going on here: `y_slope` will usually be finite, but will
// be `inf` for purely vertical lines (`p0_x == p1_x`).
//
// In the case of `inf`, the resulting slope calculation will be `-inf` or `inf`
// depending on whether the pixel edge is left or right of the line, respectively
// (from the viewport's coordinate system perspective). The `min` and `max`
// y-clamping logic generalizes nicely, as a pixel edge to the left of the line is
// clamped to `ymin`, and a pixel edge to the right is clamped to `ymax`.
//
// In the special case where a vertical line and pixel edge are at the exact same
// x-position (collinear), the line belongs to the pixel on whose _left_ edge it is
// situated. The resulting slope calculation for the edge the line is situated on
// will be NaN, as `0 * inf` results in NaN. This is true for both the left and
// right edge. In both cases, the call to `f32::max` will set this to `ymin`.
let line_px_left_y = (line_top_y + (px_left_x - line_top_x) * y_slope)
.max(ymin)
.min(ymax);
let line_px_right_y = (line_top_y + (px_right_x - line_top_x) * y_slope)
.max(ymin)
.min(ymax);
// `x_slope` is always finite, as horizontal geometry is elided.
let line_px_left_yx = line_top_x + (line_px_left_y - line_top_y) * x_slope;
let line_px_right_yx = line_top_x + (line_px_right_y - line_top_y) * x_slope;
let h = (line_px_right_y - line_px_left_y).abs();
// The trapezoidal area enclosed between the line and the right edge of the pixel
// square.
let area = 0.5 * h * (2. * px_right_x - line_px_right_yx - line_px_left_yx);
location_winding[x_idx as usize][y_idx as usize] += acc + sign * area;
acc += sign * h;
}
accumulated_winding[y_idx as usize] += acc;
}
}
}