blob: e6f9deb6fcf3ad02ce48762ccfd5d45efed30f8d [file] [log] [blame]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Primitives for creating tiles.
use crate::flatten::Line;
use alloc::vec;
use alloc::vec::Vec;
/// The max number of lines per path.
///
/// Trying to render a path with more lines than this may result in visual artifacts.
pub const MAX_LINES_PER_PATH: u32 = 1 << 31;
/// 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!
///
/// # Note
///
/// This struct is `#[repr(C)]`, but the byte order of its fields is dependent on the endianness of
/// the compilation target.
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct Tile {
// The field ordering is important.
//
// The given ordering (variant over little and big endian compilation targets), ensures that
// `Tile::to_bits` doesn't do any actual work, as the ordering of the fields is such that the
// numeric value of a `Tile` in memory is identical as returned by that method. This allows
// for, e.g., comparison and sorting.
#[cfg(target_endian = "big")]
/// The index of the tile in the y direction.
pub y: u16,
#[cfg(target_endian = "big")]
/// The index of the tile in the x direction.
pub x: u16,
/// The index of the line this tile belongs to into the line buffer, plus whether the line
/// crosses the top edge of the tile, packed together.
///
/// The index is the unsigned number in the 31 least significant bits of this value.
///
/// The last bit is 1 if and only if the lines crosses the tile's top edge. Lines making this
/// crossing increment or decrement the coarse tile winding, depending on the line direction.
pub packed_winding_line_idx: u32,
#[cfg(target_endian = "little")]
/// The index of the tile in the x direction.
pub x: u16,
#[cfg(target_endian = "little")]
/// The index of the tile in the y direction.
pub y: u16,
}
impl Tile {
/// The width of a tile in pixels.
pub const WIDTH: u16 = 4;
/// The height of a tile in pixels.
pub const HEIGHT: u16 = 4;
/// Create a new tile.
///
/// `line_idx` must be smaller than [`MAX_LINES_PER_PATH`].
#[inline]
pub const fn new(x: u16, y: u16, line_idx: u32, winding: bool) -> Self {
#[cfg(debug_assertions)]
if line_idx >= MAX_LINES_PER_PATH {
panic!("Max. number of lines per path exceeded.");
}
Self {
x,
y,
packed_winding_line_idx: ((winding as u32) << 31) | line_idx,
}
}
/// Check whether two tiles are at the same location.
#[inline]
pub const fn same_loc(&self, other: &Self) -> bool {
self.same_row(other) && self.x == other.x
}
/// Check whether `self` is adjacent to the left of `other`.
#[inline]
pub const 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.
#[inline]
pub const fn same_row(&self, other: &Self) -> bool {
self.y == other.y
}
/// The index of the line this tile belongs to into the line buffer.
#[inline]
pub const fn line_idx(&self) -> u32 {
self.packed_winding_line_idx & (MAX_LINES_PER_PATH - 1)
}
/// Whether the line crosses the top edge of the tile.
///
/// Lines making this crossing increment or decrement the coarse tile winding, depending on the
/// line direction.
#[inline]
pub const fn winding(&self) -> bool {
(self.packed_winding_line_idx & MAX_LINES_PER_PATH) != 0
}
/// Return the `u64` representation of this tile.
///
/// This is the u64 interpretation of `(y, x, packed_winding_line_idx)` where `y` is the
/// most-significant part of the number and `packed_winding_line_idx` the least significant.
#[inline(always)]
const fn to_bits(self) -> u64 {
((self.y as u64) << 48) | ((self.x as u64) << 32) | self.packed_winding_line_idx as u64
}
}
impl core::cmp::PartialEq for Tile {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
self.to_bits() == other.to_bits()
}
}
impl core::cmp::Ord for Tile {
#[inline(always)]
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.to_bits().cmp(&other.to_bits())
}
}
impl core::cmp::PartialOrd for Tile {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl core::cmp::Eq for Tile {}
/// Handles the tiling of paths.
#[derive(Clone, Debug)]
pub struct Tiles {
tile_buf: Vec<Tile>,
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,
}
}
/// 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.sorted = false;
}
/// Sort the tiles in the container.
pub fn sort_tiles(&mut self) {
self.sorted = true;
self.tile_buf.sort_unstable();
}
/// Get the tile at a certain index.
///
/// Panics if the container hasn't been sorted before.
#[inline]
pub fn get(&self, index: u32) -> &Tile {
assert!(
self.sorted,
"attempted to call `get` before sorting the tile container."
);
&self.tile_buf[index as usize]
}
/// Iterate over the tiles in sorted order.
///
/// Panics if the container hasn't been sorted before.
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &Tile> {
assert!(
self.sorted,
"attempted to call `iter` before sorting the tile container."
);
self.tile_buf.iter()
}
/// Populate the tiles' container with a buffer of lines.
///
/// Tiles exceeding the top, right or bottom of the viewport (given by `width` and `height` in
/// pixels) are culled.
//
// TODO: Tiles are clamped to the left edge of the viewport, but lines fully to the left of the
// viewport are not culled yet. These lines impact winding, and would need forwarding of
// winding to the strip generation stage.
pub fn make_tiles(&mut self, lines: &[Line], width: u16, height: u16) {
self.reset();
if width == 0 || height == 0 {
return;
}
debug_assert!(
lines.len() <= MAX_LINES_PER_PATH as usize,
"Max. number of lines per path exceeded. Max is {}, got {}.",
MAX_LINES_PER_PATH,
lines.len()
);
let tile_columns = width.div_ceil(Tile::WIDTH);
let tile_rows = height.div_ceil(Tile::HEIGHT);
for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() {
let line_idx = line_idx as u32;
let p0_x = line.p0.x / Tile::WIDTH as f32;
let p0_y = line.p0.y / Tile::HEIGHT as f32;
let p1_x = line.p1.x / Tile::WIDTH as f32;
let p1_y = line.p1.y / Tile::HEIGHT as f32;
let (line_left_x, line_right_x) = if p0_x < p1_x {
(p0_x, p1_x)
} else {
(p1_x, p0_x)
};
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)
};
// For ease of logic, special-case purely vertical tiles.
if line_left_x == line_right_x {
let y_top_tiles = (line_top_y as u16).min(tile_rows);
let y_bottom_tiles = (line_bottom_y.ceil() as u16).min(tile_rows);
let x = line_left_x as u16;
for y_idx in y_top_tiles..y_bottom_tiles {
let y = y_idx as f32;
let tile = Tile::new(x, y_idx, line_idx, y >= line_top_y);
self.tile_buf.push(tile);
}
} else {
let x_slope = (p1_x - p0_x) / (p1_y - p0_y);
let y_top_tiles = (line_top_y as u16).min(tile_rows);
let y_bottom_tiles = (line_bottom_y.ceil() as u16).min(tile_rows);
for y_idx in y_top_tiles..y_bottom_tiles {
let y = y_idx as f32;
// The line's y-coordinates at the line's top- and bottom-most points within
// the tile row.
let line_row_top_y = line_top_y.max(y).min(y + 1.);
let line_row_bottom_y = line_bottom_y.max(y).min(y + 1.);
// The line's x-coordinates at the line's top- and bottom-most points within the
// tile row.
let line_row_top_x = p0_x + (line_row_top_y - p0_y) * x_slope;
let line_row_bottom_x = p0_x + (line_row_bottom_y - p0_y) * x_slope;
// The line's x-coordinates at the line's left- and right-most points within the
// tile row.
let line_row_left_x =
f32::min(line_row_top_x, line_row_bottom_x).max(line_left_x);
let line_row_right_x =
f32::max(line_row_top_x, line_row_bottom_x).min(line_right_x);
let winding_x = if line_top_x < line_bottom_x {
line_row_left_x as u16
} else {
line_row_right_x as u16
};
for x_idx in
line_row_left_x as u16..=(line_row_right_x as u16).min(tile_columns - 1)
{
let tile = Tile::new(
x_idx,
y_idx,
line_idx,
y >= line_top_y && x_idx == winding_x,
);
self.tile_buf.push(tile);
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use crate::flatten::{Line, Point};
use crate::tile::{Tile, Tiles};
#[test]
fn cull_line_at_top() {
let line = Line {
p0: Point { x: 3.0, y: -5.0 },
p1: Point { x: 9.0, y: -1.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
assert!(tiles.is_empty());
}
#[test]
fn cull_line_at_right() {
let line = Line {
p0: Point { x: 101.0, y: 0.0 },
p1: Point { x: 103.0, y: 20.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
assert!(tiles.is_empty());
}
#[test]
fn cull_line_at_bottom() {
let line = Line {
p0: Point { x: 30.0, y: 101.0 },
p1: Point { x: 35.0, y: 105.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
assert!(tiles.is_empty());
}
#[test]
fn partially_cull_line_exceeding_viewport() {
let line = Line {
p0: Point { x: -2.0, y: -3.0 },
p1: Point { x: 2.0, y: 1.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
assert_eq!(tiles.tile_buf, [Tile::new(0, 0, 0, true)]);
}
#[test]
fn horizontal_straight_line() {
let line = Line {
p0: Point { x: 1.5, y: 1.0 },
p1: Point { x: 8.5, y: 1.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
tiles.sort_tiles();
assert_eq!(
tiles.tile_buf,
[
Tile::new(0, 0, 0, false),
Tile::new(1, 0, 0, false),
Tile::new(2, 0, 0, false),
]
);
}
#[test]
fn vertical_straight_line() {
let line = Line {
p0: Point { x: 1.0, y: 1.5 },
p1: Point { x: 1.0, y: 8.5 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
tiles.sort_tiles();
assert_eq!(
tiles.tile_buf,
[
Tile::new(0, 0, 0, false),
Tile::new(0, 1, 0, true),
Tile::new(0, 2, 0, true),
]
);
}
#[test]
fn top_left_to_bottom_right() {
let line = Line {
p0: Point { x: 1.0, y: 1.0 },
p1: Point { x: 11.0, y: 8.5 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
tiles.sort_tiles();
assert_eq!(
tiles.tile_buf,
[
Tile::new(0, 0, 0, false),
Tile::new(1, 0, 0, false),
Tile::new(1, 1, 0, true),
Tile::new(2, 1, 0, false),
Tile::new(2, 2, 0, true),
]
);
}
#[test]
fn bottom_right_to_top_left() {
let line = Line {
p0: Point { x: 11.0, y: 8.5 },
p1: Point { x: 1.0, y: 1.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
tiles.sort_tiles();
assert_eq!(
tiles.tile_buf,
[
Tile::new(0, 0, 0, false),
Tile::new(1, 0, 0, false),
Tile::new(1, 1, 0, true),
Tile::new(2, 1, 0, false),
Tile::new(2, 2, 0, true),
]
);
}
#[test]
fn bottom_left_to_top_right() {
let line = Line {
p0: Point { x: 2.0, y: 11.0 },
p1: Point { x: 14.0, y: 6.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
tiles.sort_tiles();
assert_eq!(
tiles.tile_buf,
[
Tile::new(2, 1, 0, false),
Tile::new(3, 1, 0, false),
Tile::new(0, 2, 0, false),
Tile::new(1, 2, 0, false),
Tile::new(2, 2, 0, true),
]
);
}
#[test]
fn top_right_to_bottom_left() {
let line = Line {
p0: Point { x: 14.0, y: 6.0 },
p1: Point { x: 2.0, y: 11.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line], 100, 100);
tiles.sort_tiles();
assert_eq!(
tiles.tile_buf,
[
Tile::new(2, 1, 0, false),
Tile::new(3, 1, 0, false),
Tile::new(0, 2, 0, false),
Tile::new(1, 2, 0, false),
Tile::new(2, 2, 0, true),
]
);
}
#[test]
fn two_lines_in_single_tile() {
let line_1 = Line {
p0: Point { x: 1.0, y: 3.0 },
p1: Point { x: 3.0, y: 3.0 },
};
let line_2 = Line {
p0: Point { x: 3.0, y: 3.0 },
p1: Point { x: 0.0, y: 1.0 },
};
let mut tiles = Tiles::new();
tiles.make_tiles(&[line_1, line_2], 100, 100);
assert_eq!(
tiles.tile_buf,
[Tile::new(0, 0, 0, false), Tile::new(0, 0, 1, false),]
);
}
#[test]
// See https://github.com/LaurenzV/cpu-sparse-experiments/issues/46.
fn 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], 600, 600);
}
}