blob: 0cceae61ac966625512cd06acef027639cae9efc [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Primitives for creating tiles.
use crate::flatten::{Line, Point};
/// The width of a tile.
pub const TILE_WIDTH: u32 = 4;
/// The height of a tile.
pub const TILE_HEIGHT: u32 = 4;
const TILE_WIDTH_SCALE: f32 = TILE_WIDTH as f32;
const TILE_HEIGHT_SCALE: f32 = TILE_HEIGHT as f32;
const INV_TILE_WIDTH_SCALE: f32 = 1.0 / TILE_WIDTH_SCALE;
const INV_TILE_HEIGHT_SCALE: f32 = 1.0 / TILE_HEIGHT_SCALE;
// The value of 8192.0 is mainly chosen for compatibility with the old cpu-sparse
// implementation, where we scaled to u16.
const NUDGE_FACTOR: f32 = 1.0 / 8192.0;
const SCALED_X_NUDGE_FACTOR: f32 = 1.0 / (8192.0 * TILE_WIDTH_SCALE);
/// A tile represents an aligned area on the pixmap, used to subdivide the viewport into sub-areas
/// (currently 4x4) and analyze line intersections inside each such area.
///
/// Keep in mind that it is possible to have multiple tiles with the same index,
/// namely if we have multiple lines crossing the same 4x4 area!
#[derive(Debug, Clone)]
pub struct Tile {
/// The index of the tile in the x direction.
pub x: i32,
/// The index of the tile in the y direction.
pub y: u16,
/// The start point of the line in that tile.
pub p0: Point,
/// The end point of the line in that tile.
pub p1: Point,
}
impl Tile {
/// Create a new tile.
pub fn new(x: i32, y: u16, p0: Point, p1: Point) -> Self {
Self {
// We don't need to store the exact negative location, just that it is negative,
// so that the winding number calculation is correct.
x: x.max(-1),
y,
p0,
p1,
}
}
/// Check whether two tiles are at the same location.
pub fn same_loc(&self, other: &Self) -> bool {
self.x == other.x && self.same_row(other)
}
/// Check whether `self` is adjacent to the left of `other`.
pub fn prev_loc(&self, other: &Self) -> bool {
self.same_row(other) && self.x + 1 == other.x
}
/// Check whether two tiles are on the same row.
pub fn same_row(&self, other: &Self) -> bool {
self.y == other.y
}
/// Return the delta of the tile.
pub fn delta(&self) -> i32 {
(self.p1.y == 0.0) as i32 - (self.p0.y == 0.0) as i32
}
}
/// Handles the tiling of paths.
#[derive(Clone, Debug)]
pub struct Tiles {
tile_buf: Vec<Tile>,
tile_index_buf: Vec<TileIndex>,
sorted: bool,
}
impl Default for Tiles {
fn default() -> Self {
Self::new()
}
}
impl Tiles {
/// Create a new tiles container.
pub fn new() -> Self {
Self {
tile_buf: vec![],
sorted: false,
tile_index_buf: vec![],
}
}
/// Get the number of tiles in the container.
pub fn len(&self) -> u32 {
self.tile_buf.len() as u32
}
/// Returns true if the container has no tiles.
pub fn is_empty(&self) -> bool {
self.tile_buf.is_empty()
}
/// Reset the tiles' container.
pub fn reset(&mut self) {
self.tile_buf.clear();
self.tile_index_buf.clear();
self.sorted = false;
}
/// Sort the tiles in the container.
pub fn sort_tiles(&mut self) {
self.sorted = true;
self.tile_index_buf.sort_unstable_by(TileIndex::cmp);
}
/// Get the tile at a certain index.
///
/// Panics if the container hasn't been sorted before.
pub fn get(&self, index: u32) -> &Tile {
assert!(
self.sorted,
"attempted to call `get` before sorting the tile container."
);
&self.tile_buf[self.tile_index_buf[index as usize].index()]
}
/// Populate the tiles' container with a buffer of lines.
pub fn make_tiles(&mut self, lines: &[Line]) {
self.reset();
// Calculate how many tiles are covered between two positions. p0 and p1 are scaled
// to the tile unit square.
let spanned_tiles =
|p0: f32, p1: f32| -> u32 { (p0.max(p1).ceil() - p0.min(p1).floor()).max(1.0) as u32 };
let nudge_point = |p: Point| -> Point {
// Lines that cross vertical tile boundaries need special treatment during
// anti-aliasing. This case is detected via tile-relative x == 0. However,
// lines can naturally start or end at a multiple of the 4x4 grid, too, but
// these don't constitute crossings. We nudge these points ever so slightly,
// by ensuring that xfrac0 and xfrac1 are always at least 1/8192 of a pixel.
// By doing so, whenever we encounter a point
// at a tile relative 0, we can treat it as an edge crossing. This is somewhat
// of a hack and in theory we should rather solve the underlying issue in the
// strip generation code, but it works for now.
if p.x.fract() == 0.0 {
Point {
x: p.x + SCALED_X_NUDGE_FACTOR,
y: p.y,
}
} else {
p
}
};
let mut push_tile = |x: f32, y: f32, p0: Point, p1: Point| {
if y >= 0.0 {
let tile = Tile::new(x as i32, y as u16, p0, p1);
self.tile_index_buf
.push(TileIndex::from_tile(self.tile_buf.len() as u32, &tile));
self.tile_buf.push(tile);
}
};
for line in lines {
// Points scaled to the tile unit square.
let s0 = nudge_point(scale_down(line.p0));
let s1 = nudge_point(scale_down(line.p1));
// Count how many tiles are covered on each axis.
let tile_count_x = spanned_tiles(s0.x, s1.x);
let tile_count_y = spanned_tiles(s0.y, s1.y);
// Note: This code is technically unreachable now, because we always nudge x points at tile-relative 0
// position. But we might need it again in the future if we change the logic.
let mut x = s0.x.floor();
if s0.x == x && s1.x < x {
// s0.x is on right side of first tile.
x -= 1.0;
}
let mut y = s0.y.floor();
if s0.y == y && s1.y < y {
// Since the end point of the line is above the start point,
// s0.y is conceptually on bottom of the previous tile instead of at the top
// of the current tile, so we need to adjust the y location.
y -= 1.0;
}
let xfrac0 = scale_up(s0.x - x);
let yfrac0 = scale_up(s0.y - y);
let packed0 = Point::new(xfrac0, yfrac0);
if tile_count_x == 1 {
let xfrac1 = scale_up(s1.x - x);
if tile_count_y == 1 {
let yfrac1 = scale_up(s1.y - y);
// A 1x1 tile.
push_tile(x, y, Point::new(xfrac0, yfrac0), Point::new(xfrac1, yfrac1));
} else {
// A vertical column.
let inv_slope = (s1.x - s0.x) / (s1.y - s0.y);
let sign = (s1.y - s0.y).signum();
// For downward lines, xclip0 and yclip store the x and y intersection points
// at the bottom side of the current tile. For upward lines, they store the in
// intersection points at the top side of the current tile.
let mut xclip0 = (s0.x - x) + (y - s0.y) * inv_slope;
// We handled the case of a 1x1 tile before, so in this case the line will
// definitely cross the tile either at the top or bottom, and thus yclip is
// either 0 or 1.
let (yclip, flip) = if sign > 0.0 {
// If the line goes downward, instead store where the line would intersect
// the first tile at the bottom
xclip0 += inv_slope;
(scale_up(1.0), scale_up(-1.0))
} else {
// Otherwise, the line goes up, and thus will intersect the top side of the
// tile.
(scale_up(0.0), scale_up(1.0))
};
let mut last_packed = packed0;
// For the first tile, as well as all subsequent tiles that are intersected
// at the top and bottom, calculate the x intersection points and push the
// corresponding tiles.
// Note: This could perhaps be SIMD-optimized, but initial experiments suggest
// that in the vast majority of cases the number of tiles is between 0-5, so
// it's probably not really worth it.
for i in 0..tile_count_y - 1 {
// Calculate the next x intersection point.
let xclip = xclip0 + i as f32 * sign * inv_slope;
// The .max(1) is necessary to indicate that the point actually crosses the
// edge instead of ending at it. Perhaps we can figure out a different way
// to represent this.
let xfrac = scale_up(xclip).max(NUDGE_FACTOR);
let packed = Point::new(xfrac, yclip);
push_tile(x, y, last_packed, packed);
// Flip y between top and bottom of tile (i.e. from TILE_HEIGHT
// to 0 or 0 to TILE_HEIGHT).
last_packed = Point::new(packed.x, packed.y + flip);
y += sign;
}
// Push the last tile, which might be at a fractional y offset.
let yfrac1 = scale_up(s1.y - y);
let packed1 = Point::new(xfrac1, yfrac1);
push_tile(x, y, last_packed, packed1);
}
} else if tile_count_y == 1 {
// A horizontal row.
// Same explanations apply as above, but instead in the horizontal direction.
let slope = (s1.y - s0.y) / (s1.x - s0.x);
let sign = (s1.x - s0.x).signum();
let mut yclip0 = (s0.y - y) + (x - s0.x) * slope;
let (xclip, flip) = if sign > 0.0 {
yclip0 += slope;
(scale_up(1.0), scale_up(-1.0))
} else {
(scale_up(0.0), scale_up(1.0))
};
let mut last_packed = packed0;
for i in 0..tile_count_x - 1 {
let yclip = yclip0 + i as f32 * sign * slope;
let yfrac = scale_up(yclip).max(NUDGE_FACTOR);
let packed = Point::new(xclip, yfrac);
push_tile(x, y, last_packed, packed);
last_packed = Point::new(packed.x + flip, packed.y);
x += sign;
}
let xfrac1 = scale_up(s1.x - x);
let yfrac1 = scale_up(s1.y - y);
let packed1 = Point::new(xfrac1, yfrac1);
push_tile(x, y, last_packed, packed1);
} else {
// General case (i.e. more than one tile covered in both directions). We perform a DDA
// to "walk" along the path and find out which tiles are intersected by the line
// and at which positions.
let recip_dx = 1.0 / (s1.x - s0.x);
let sign_x = (s1.x - s0.x).signum();
let recip_dy = 1.0 / (s1.y - s0.y);
let sign_y = (s1.y - s0.y).signum();
// How much we advance at each intersection with a vertical grid line.
let mut t_clipx = (x - s0.x) * recip_dx;
// Similarly to the case "horizontal column", if the line goes to the right,
// we will always intersect the tiles on the right side (except for perhaps the last
// tile, but this case is handled separately in the end). Otherwise, we always intersect
// on the left side.
let (xclip, flip_x) = if sign_x > 0.0 {
t_clipx += recip_dx;
(scale_up(1.0), scale_up(-1.0))
} else {
(scale_up(0.0), scale_up(1.0))
};
// How much we advance at each intersection with a horizontal grid line.
let mut t_clipy = (y - s0.y) * recip_dy;
// Same as xclip, but for the vertical direction, analogously to the
// "vertical column" case.
let (yclip, flip_y) = if sign_y > 0.0 {
t_clipy += recip_dy;
(scale_up(1.0), scale_up(-1.0))
} else {
(scale_up(0.0), scale_up(1.0))
};
// x and y coordinates of the target tile.
let x1 = x + (tile_count_x - 1) as f32 * sign_x;
let y1 = y + (tile_count_y - 1) as f32 * sign_y;
let mut xi = x;
let mut yi = y;
let mut last_packed = packed0;
loop {
// See https://github.com/LaurenzV/cpu-sparse-experiments/issues/46
// for why we don't just use an inequality check.
let x_cond = if sign_x > 0.0 { xi >= x1 } else { xi <= x1 };
let y_cond = if sign_y > 0.0 { yi >= y1 } else { yi <= y1 };
if x_cond && y_cond {
break;
}
if t_clipy < t_clipx {
// Intersected with a horizontal grid line.
let x_intersect = s0.x + (s1.x - s0.x) * t_clipy - xi;
let xfrac = scale_up(x_intersect).max(NUDGE_FACTOR);
let packed = Point::new(xfrac, yclip);
push_tile(xi, yi, last_packed, packed);
t_clipy += recip_dy.abs();
yi += sign_y;
last_packed = Point::new(packed.x, packed.y + flip_y);
} else {
// Intersected with vertical grid line.
let y_intersect = s0.y + (s1.y - s0.y) * t_clipx - yi;
let yfrac = scale_up(y_intersect).max(NUDGE_FACTOR);
let packed = Point::new(xclip, yfrac);
push_tile(xi, yi, last_packed, packed);
t_clipx += recip_dx.abs();
xi += sign_x;
last_packed = Point::new(packed.x + flip_x, packed.y);
}
}
// The last tile, where the end point is possibly not at an integer coordinate.
let xfrac1 = scale_up(s1.x - xi);
let yfrac1 = scale_up(s1.y - yi);
let packed1 = Point::new(xfrac1, yfrac1);
push_tile(xi, yi, last_packed, packed1);
}
}
// This particular choice of sentinel tiles generates a sentinel strip.
push_tile(
0x3ffd as f32,
0x3fff as f32,
Point::new(0.0, 0.0),
Point::new(0.0, 0.0),
);
push_tile(
0x3fff as f32,
0x3fff as f32,
Point::new(0.0, 0.0),
Point::new(0.0, 0.0),
);
}
}
/// An index into a sorted tile buffer.
#[derive(Clone, Debug)]
struct TileIndex {
x: u16,
y: u16,
index: u32,
}
impl TileIndex {
pub(crate) fn from_tile(index: u32, tile: &Tile) -> Self {
let x = (tile.x + 1).max(0) as u16;
let y = tile.y;
Self { x, y, index }
}
pub(crate) fn cmp(&self, b: &Self) -> std::cmp::Ordering {
let xya = ((self.y as u32) << 16) + (self.x as u32);
let xyb = ((b.y as u32) << 16) + (b.x as u32);
xya.cmp(&xyb)
}
pub(crate) fn index(&self) -> usize {
self.index as usize
}
}
#[cfg(test)]
const _: () = if TILE_WIDTH_SCALE != TILE_HEIGHT_SCALE {
panic!("Can only handle square tiles for now.");
};
/// Scale a tile coordinate to a viewport coordinate. Note this assumes tiles are square.
const fn scale_up(z: f32) -> f32 {
z * TILE_WIDTH_SCALE
}
/// Scale a viewport coordinate to a tile coordinate.
const fn scale_down(z: Point) -> Point {
Point::new(z.x * INV_TILE_WIDTH_SCALE, z.y * INV_TILE_HEIGHT_SCALE)
}
#[cfg(test)]
mod tests {
use crate::flatten::{Line, Point};
use crate::footprint::Footprint;
use crate::tile::{scale_up, Tile, Tiles};
impl Footprint {
pub(crate) fn is_empty(&self) -> bool {
self.0 == 0
}
}
#[test]
fn footprint_at_tile_edge() {
let tile = Tile::new(
0,
0,
Point::new(scale_up(1.0), scale_up(0.0)),
Point::new(scale_up(1.0), scale_up(1.0)),
);
assert!(tile.footprint().is_empty());
}
#[test]
fn footprints_in_tile() {
let mut tile = Tile::new(
0,
0,
Point::new(scale_up(0.5), scale_up(0.0)),
Point::new(scale_up(0.55), scale_up(1.0)),
);
assert_eq!(tile.footprint().x0(), 2);
assert_eq!(tile.footprint().x1(), 3);
tile = Tile::new(
0,
0,
Point::new(scale_up(0.1), scale_up(0.0)),
Point::new(scale_up(0.6), scale_up(1.0)),
);
assert_eq!(tile.footprint().x0(), 0);
assert_eq!(tile.footprint().x1(), 3);
tile = Tile::new(
0,
0,
Point::new(scale_up(0.0), scale_up(0.0)),
Point::new(scale_up(1.0), scale_up(1.0)),
);
assert_eq!(tile.footprint().x0(), 0);
assert_eq!(tile.footprint().x1(), 4);
tile = Tile::new(
0,
0,
Point::new(scale_up(0.74), scale_up(0.0)),
Point::new(scale_up(1.76), scale_up(1.0)),
);
assert_eq!(tile.footprint().x0(), 2);
assert_eq!(tile.footprint().x1(), 4);
}
#[test]
fn issue_46_infinite_loop() {
let line = Line {
p0: Point { x: 22.0, y: 552.0 },
p1: Point { x: 224.0, y: 388.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line]);
}
}