Add the code for `vello_common` (#828)

Note that this is not ready for merging yet as I'm still tracking down a
regression I introduced. I didn't notice this because in my branch all
affected tests use the rectangle fast path, which I removed here for
now. So that might take a while.

But other than that, this part of the code should be pretty much done
(and reviewable).
diff --git a/Cargo.lock b/Cargo.lock
index d213d27..5f478d1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2602,6 +2602,11 @@
 [[package]]
 name = "vello_common"
 version = "0.4.0"
+dependencies = [
+ "kurbo",
+ "peniko",
+ "vello_api",
+]
 
 [[package]]
 name = "vello_cpu"
diff --git a/Cargo.toml b/Cargo.toml
index d363e22..6fdd4a3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -86,7 +86,6 @@
 
 [workspace.dependencies]
 vello = { version = "0.4.0", path = "vello" }
-vello_api = { path = "sparse_strips/vello_api" }
 vello_encoding = { version = "0.4.0", path = "vello_encoding" }
 vello_shaders = { version = "0.4.0", path = "vello_shaders" }
 bytemuck = { version = "1.21.0", features = ["derive"] }
@@ -101,6 +100,10 @@
 static_assertions = "1.1.0"
 thiserror = "2.0.11"
 
+# The below crates are experimental!
+vello_api = { path = "sparse_strips/vello_api" }
+vello_common = { path = "sparse_strips/vello_common" }
+
 # NOTE: Make sure to keep this in sync with the version badge in README.md and vello/README.md
 wgpu = { version = "24.0.1" }
 log = "0.4.22"
diff --git a/sparse_strips/vello_common/Cargo.toml b/sparse_strips/vello_common/Cargo.toml
index 429d064..740301c 100644
--- a/sparse_strips/vello_common/Cargo.toml
+++ b/sparse_strips/vello_common/Cargo.toml
@@ -12,6 +12,12 @@
 publish = false
 
 [dependencies]
+vello_api = { workspace = true }
+kurbo = { workspace = true }
+peniko = { workspace = true }
+
+[features]
+simd = ["vello_api/simd"]
 
 [lints]
 workspace = true
diff --git a/sparse_strips/vello_common/src/coarse.rs b/sparse_strips/vello_common/src/coarse.rs
new file mode 100644
index 0000000..af39223
--- /dev/null
+++ b/sparse_strips/vello_common/src/coarse.rs
@@ -0,0 +1,246 @@
+// Copyright 2025 the Vello Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+//! Generating and processing wide tiles.
+
+use crate::color::{AlphaColor, Srgb};
+use crate::strip::{Strip, STRIP_HEIGHT};
+use vello_api::paint::Paint;
+use vello_api::peniko::Fill;
+
+/// The width of a wide tile.
+pub const WIDE_TILE_WIDTH: usize = 256;
+
+/// A container for wide tiles.
+#[derive(Debug)]
+pub struct Wide {
+    tiles: Vec<WideTile>,
+    width: usize,
+    height: usize,
+}
+
+impl Wide {
+    /// Create a new container for wide tiles.
+    pub fn new(width: usize, height: usize) -> Self {
+        let width_tiles = width.div_ceil(WIDE_TILE_WIDTH);
+        let height_tiles = height.div_ceil(STRIP_HEIGHT);
+        let mut tiles = Vec::with_capacity(width_tiles * height_tiles);
+
+        for w in 0..width_tiles {
+            for h in 0..height_tiles {
+                tiles.push(WideTile::new(w * WIDE_TILE_WIDTH, h * STRIP_HEIGHT));
+            }
+        }
+
+        Self {
+            tiles,
+            width,
+            height,
+        }
+    }
+
+    /// Reset all tiles in the container.
+    pub fn reset(&mut self) {
+        for tile in &mut self.tiles {
+            tile.bg = AlphaColor::TRANSPARENT;
+            tile.cmds.clear();
+        }
+    }
+
+    /// Return the number of horizontal tiles.
+    pub fn width_tiles(&self) -> usize {
+        self.width.div_ceil(WIDE_TILE_WIDTH)
+    }
+
+    /// Return the number of vertical tiles.
+    pub fn height_tiles(&self) -> usize {
+        self.height.div_ceil(STRIP_HEIGHT)
+    }
+
+    /// Get the wide tile at a certain index.
+    ///
+    /// Panics if the index is out-of-range.
+    pub fn get(&self, x: usize, y: usize) -> &WideTile {
+        assert!(
+            x < self.width && y < self.height,
+            "attempted to access out-of-bounds wide tile"
+        );
+
+        &self.tiles[y * self.width_tiles() + x]
+    }
+
+    /// Get mutable access to the wide tile at a certain index.
+    ///
+    /// Panics if the index is out-of-range.
+    pub fn get_mut(&mut self, x: usize, y: usize) -> &mut WideTile {
+        assert!(
+            x < self.width && y < self.height,
+            "attempted to access out-of-bounds wide tile"
+        );
+
+        let idx = y * self.width_tiles() + x;
+        &mut self.tiles[idx]
+    }
+
+    /// Return a reference to all wide tiles.
+    pub fn tiles(&self) -> &[WideTile] {
+        self.tiles.as_slice()
+    }
+
+    /// Generate wide tile commands from the strip buffer.
+    pub fn generate(&mut self, strip_buf: &[Strip], fill_rule: Fill, paint: Paint) {
+        let width_tiles = self.width_tiles();
+
+        if strip_buf.is_empty() {
+            return;
+        }
+
+        for i in 0..strip_buf.len() - 1 {
+            let strip = &strip_buf[i];
+
+            if strip.x >= self.width as i32 {
+                // Don't render strips that are outside the viewport.
+                continue;
+            }
+
+            if strip.y >= self.height as u16 {
+                // Since strips are sorted by location, any subsequent strips will also be
+                // outside the viewport, so we can abort entirely.
+                break;
+            }
+
+            let next_strip = &strip_buf[i + 1];
+            // Currently, strips can also start at a negative x position, since we don't
+            // support viewport culling yet. However, when generating the commands
+            // we only want to emit strips >= 0, so we calculate the adjustment
+            // and then only include the alpha indices for columns where x >= 0.
+            let x0_adjustment = strip.x.min(0).unsigned_abs();
+            let x0 = (strip.x + x0_adjustment as i32) as u32;
+            let strip_y = strip.strip_y();
+            let mut col = strip.col + x0_adjustment;
+            // Can potentially be 0, if the next strip's x values is also < 0.
+            let strip_width = next_strip.col.saturating_sub(col);
+            let x1 = x0 + strip_width;
+            let tile_x0 = x0 as usize / WIDE_TILE_WIDTH;
+            // It's possible that a strip extends into a new wide tile, but we don't actually
+            // have as many wide tiles (e.g. because the pixmap width is only 512, but
+            // strip ends at 513), so take the minimum between the rounded values and `width_tiles`.
+            let tile_x1 = (x1 as usize).div_ceil(WIDE_TILE_WIDTH).min(width_tiles);
+            let mut x = x0;
+
+            for tile_x in tile_x0..tile_x1 {
+                let x_tile_rel = x % WIDE_TILE_WIDTH as u32;
+                let width = x1.min(((tile_x + 1) * WIDE_TILE_WIDTH) as u32) - x;
+                let cmd = CmdAlphaFill {
+                    x: x_tile_rel,
+                    width,
+                    alpha_ix: col as usize,
+                    paint: paint.clone(),
+                };
+                x += width;
+                col += width;
+                self.get_mut(tile_x, strip_y as usize)
+                    .push(Cmd::AlphaFill(cmd));
+            }
+
+            let active_fill = match fill_rule {
+                Fill::NonZero => next_strip.winding != 0,
+                Fill::EvenOdd => next_strip.winding % 2 != 0,
+            };
+
+            if active_fill
+                && strip_y == next_strip.strip_y()
+                // Only fill if we are actually inside the viewport.
+                && next_strip.x >= 0
+            {
+                x = x1;
+                let x2 = next_strip.x as u32;
+                let fxt0 = x1 as usize / WIDE_TILE_WIDTH;
+                let fxt1 = (x2 as usize).div_ceil(WIDE_TILE_WIDTH);
+                for tile_x in fxt0..fxt1 {
+                    let x_tile_rel = x % WIDE_TILE_WIDTH as u32;
+                    let width = x2.min(((tile_x + 1) * WIDE_TILE_WIDTH) as u32) - x;
+                    x += width;
+                    self.get_mut(tile_x, strip_y as usize)
+                        .fill(x_tile_rel, width, paint.clone());
+                }
+            }
+        }
+    }
+}
+
+/// A wide tile.
+#[derive(Debug)]
+pub struct WideTile {
+    /// The x coordinate of the wide tile.
+    pub x: usize,
+    /// The y coordinate of the wide tile.
+    pub y: usize,
+    /// The background of the tile.
+    pub bg: AlphaColor<Srgb>,
+    /// The draw commands of the tile.
+    pub cmds: Vec<Cmd>,
+}
+
+impl WideTile {
+    /// Create a new wide tile.
+    pub fn new(x: usize, y: usize) -> Self {
+        Self {
+            x,
+            y,
+            bg: AlphaColor::TRANSPARENT,
+            cmds: vec![],
+        }
+    }
+
+    pub(crate) fn fill(&mut self, x: u32, width: u32, paint: Paint) {
+        let Paint::Solid(s) = &paint else {
+            unimplemented!()
+        };
+        let can_override = x == 0 && width == WIDE_TILE_WIDTH as u32 && s.components[3] == 1.0;
+
+        if can_override {
+            self.cmds.clear();
+            self.bg = *s;
+        } else {
+            self.cmds.push(Cmd::Fill(CmdFill { x, width, paint }));
+        }
+    }
+
+    pub(crate) fn push(&mut self, cmd: Cmd) {
+        self.cmds.push(cmd);
+    }
+}
+
+/// A drawing command.
+#[derive(Debug)]
+pub enum Cmd {
+    /// A fill command.
+    Fill(CmdFill),
+    /// A fill command with alpha mask.
+    AlphaFill(CmdAlphaFill),
+}
+
+/// Fill a consecutive region of a wide tile.
+#[derive(Debug)]
+pub struct CmdFill {
+    /// The horizontal start position of the command.
+    pub x: u32,
+    /// The width of the command.
+    pub width: u32,
+    /// The paint that should be used to fill the area.
+    pub paint: Paint,
+}
+
+/// Fill a consecutive region of a wide tile with an alpha mask.
+#[derive(Debug)]
+pub struct CmdAlphaFill {
+    /// The horizontal start position of the command.
+    pub x: u32,
+    /// The width of the command.
+    pub width: u32,
+    /// The start index in the alpha buffer of the command.
+    pub alpha_ix: usize,
+    /// The paint that should be used to fill the area.
+    pub paint: Paint,
+}
diff --git a/sparse_strips/vello_common/src/flatten.rs b/sparse_strips/vello_common/src/flatten.rs
new file mode 100644
index 0000000..aada20c
--- /dev/null
+++ b/sparse_strips/vello_common/src/flatten.rs
@@ -0,0 +1,124 @@
+// Copyright 2025 the Vello Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+//! Flattening filled and stroked paths.
+
+use kurbo::StrokeOpts;
+use vello_api::kurbo;
+use vello_api::kurbo::{Affine, BezPath, Stroke};
+
+/// The flattening tolerance.
+const TOL: f64 = 0.25;
+
+/// A point.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct Point {
+    /// The x coordinate of the point.
+    pub x: f32,
+    /// The y coordinate of the point.
+    pub y: f32,
+}
+
+impl Point {
+    /// Create a new point.
+    pub const fn new(x: f32, y: f32) -> Self {
+        Self { x, y }
+    }
+}
+
+impl std::ops::Add for Point {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self {
+        Self::new(self.x + rhs.x, self.y + rhs.y)
+    }
+}
+
+impl std::ops::Sub for Point {
+    type Output = Self;
+
+    fn sub(self, rhs: Self) -> Self {
+        Self::new(self.x - rhs.x, self.y - rhs.y)
+    }
+}
+
+impl std::ops::Mul<f32> for Point {
+    type Output = Self;
+
+    fn mul(self, rhs: f32) -> Self {
+        Self::new(self.x * rhs, self.y * rhs)
+    }
+}
+
+/// A line.
+#[derive(Clone, Copy, Debug)]
+pub struct Line {
+    /// The start point of the line.
+    pub p0: Point,
+    /// The end point of the line.
+    pub p1: Point,
+}
+
+impl Line {
+    /// Create a new line.
+    pub fn new(p0: Point, p1: Point) -> Self {
+        Self { p0, p1 }
+    }
+}
+
+/// Flatten a filled bezier path into line segments.
+pub fn fill(path: &BezPath, affine: Affine, line_buf: &mut Vec<Line>) {
+    line_buf.clear();
+    let mut start = kurbo::Point::default();
+    let mut p0 = kurbo::Point::default();
+    let iter = path.iter().map(|el| affine * el);
+
+    let mut closed = false;
+
+    kurbo::flatten(iter, TOL, |el| match el {
+        kurbo::PathEl::MoveTo(p) => {
+            if !closed && p0 != start {
+                close_path(start, p0, line_buf);
+            }
+
+            closed = false;
+            start = p;
+            p0 = p;
+        }
+        kurbo::PathEl::LineTo(p) => {
+            let pt0 = Point::new(p0.x as f32, p0.y as f32);
+            let pt1 = Point::new(p.x as f32, p.y as f32);
+            line_buf.push(Line::new(pt0, pt1));
+            p0 = p;
+        }
+        kurbo::PathEl::QuadTo(_, _) => unreachable!(),
+        kurbo::PathEl::CurveTo(_, _, _) => unreachable!(),
+        kurbo::PathEl::ClosePath => {
+            closed = true;
+
+            close_path(start, p0, line_buf);
+        }
+    });
+
+    if !closed {
+        close_path(start, p0, line_buf);
+    }
+}
+
+/// Flatten a stroked bezier path into line segments.
+pub fn stroke(path: &BezPath, style: &Stroke, affine: Affine, line_buf: &mut Vec<Line>) {
+    // TODO: Temporary hack to ensure that strokes are scaled properly by the transform.
+    let tolerance = TOL / affine.as_coeffs()[0].abs().max(affine.as_coeffs()[3].abs());
+
+    let expanded = kurbo::stroke(path.iter(), style, &StrokeOpts::default(), tolerance);
+    fill(&expanded, affine, line_buf);
+}
+
+fn close_path(start: kurbo::Point, p0: kurbo::Point, line_buf: &mut Vec<Line>) {
+    let pt0 = Point::new(p0.x as f32, p0.y as f32);
+    let pt1 = Point::new(start.x as f32, start.y as f32);
+
+    if pt0 != pt1 {
+        line_buf.push(Line::new(pt0, pt1));
+    }
+}
diff --git a/sparse_strips/vello_common/src/footprint.rs b/sparse_strips/vello_common/src/footprint.rs
new file mode 100644
index 0000000..485f262
--- /dev/null
+++ b/sparse_strips/vello_common/src/footprint.rs
@@ -0,0 +1,145 @@
+// Copyright 2025 the Vello Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+use crate::tile::{Tile, TILE_WIDTH};
+
+/// A footprint represents in a compact fashion the range of pixels covered by a tile.
+/// We represent this as a u32 so that we can work with bit-shifting for better performance.
+pub(crate) struct Footprint(pub(crate) u32);
+
+impl Footprint {
+    /// Create a new, empty footprint.
+    pub(crate) fn empty() -> Self {
+        Self(0)
+    }
+
+    /// Create a new footprint from a single index, i.e. [i, i + 1).
+    pub(crate) fn from_index(index: u8) -> Self {
+        Self(1 << index)
+    }
+
+    /// Create a new footprint from a single index, i.e. [start, end).
+    pub(crate) fn from_range(start: u8, end: u8) -> Self {
+        Self((1 << end) - (1 << start))
+    }
+
+    /// The start point of the covered range (inclusive).
+    pub(crate) fn x0(&self) -> u32 {
+        self.0.trailing_zeros()
+    }
+
+    /// The end point of the covered range (exclusive).
+    pub(crate) fn x1(&self) -> u32 {
+        32 - self.0.leading_zeros()
+    }
+
+    /// Extend the range with a single index.
+    pub(crate) fn extend(&mut self, index: u8) {
+        self.0 |= (1 << index) as u32;
+    }
+
+    /// Merge another footprint with the current one.
+    pub(crate) fn merge(&mut self, fp: &Self) {
+        self.0 |= fp.0;
+    }
+}
+
+impl Tile {
+    // TODO: Profiling shows that this method takes up quite a lot of time in AVX SIMD, investigate
+    // if it can be improved.
+    pub(crate) fn footprint(&self) -> Footprint {
+        let x0 = self.p0.x;
+        let x1 = self.p1.x;
+        let x_min = x0.min(x1).floor();
+        let x_max = x0.max(x1).ceil();
+        let start_i = x_min as u32;
+        let end_i = (start_i + 1).max(x_max as u32).min(TILE_WIDTH);
+
+        Footprint::from_range(start_i as u8, end_i as u8)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::footprint::Footprint;
+
+    #[test]
+    fn footprint_empty() {
+        let fp1 = Footprint::empty();
+        // Not optimal behavior, but currently how it is.
+        assert_eq!(fp1.x0(), 32);
+        assert_eq!(fp1.x1(), 0);
+    }
+
+    #[test]
+    fn footprint_from_index() {
+        let fp1 = Footprint::from_index(0);
+        assert_eq!(fp1.x0(), 0);
+        assert_eq!(fp1.x1(), 1);
+
+        let fp2 = Footprint::from_index(3);
+        assert_eq!(fp2.x0(), 3);
+        assert_eq!(fp2.x1(), 4);
+
+        let fp3 = Footprint::from_index(6);
+        assert_eq!(fp3.x0(), 6);
+        assert_eq!(fp3.x1(), 7);
+    }
+
+    #[test]
+    fn footprint_from_range() {
+        let fp1 = Footprint::from_range(1, 3);
+        assert_eq!(fp1.x0(), 1);
+        assert_eq!(fp1.x1(), 3);
+
+        // Same comment as for empty.
+        let fp2 = Footprint::from_range(2, 2);
+        assert_eq!(fp2.x0(), 32);
+        assert_eq!(fp2.x1(), 0);
+
+        let fp3 = Footprint::from_range(3, 7);
+        assert_eq!(fp3.x0(), 3);
+        assert_eq!(fp3.x1(), 7);
+    }
+
+    #[test]
+    fn footprint_extend() {
+        let mut fp = Footprint::empty();
+        fp.extend(5);
+        assert_eq!(fp.x0(), 5);
+        assert_eq!(fp.x1(), 6);
+
+        fp.extend(3);
+        assert_eq!(fp.x0(), 3);
+        assert_eq!(fp.x1(), 6);
+
+        fp.extend(8);
+        assert_eq!(fp.x0(), 3);
+        assert_eq!(fp.x1(), 9);
+
+        fp.extend(0);
+        assert_eq!(fp.x0(), 0);
+        assert_eq!(fp.x1(), 9);
+
+        fp.extend(9);
+        assert_eq!(fp.x0(), 0);
+        assert_eq!(fp.x1(), 10);
+    }
+
+    #[test]
+    fn footprint_merge() {
+        let mut fp1 = Footprint::from_range(2, 4);
+        let fp2 = Footprint::from_range(5, 6);
+        fp1.merge(&fp2);
+
+        assert_eq!(fp1.x0(), 2);
+        assert_eq!(fp1.x1(), 6);
+
+        let mut fp3 = Footprint::from_range(5, 9);
+        let fp4 = Footprint::from_range(7, 10);
+        fp3.merge(&fp4);
+
+        assert_eq!(fp3.x0(), 5);
+        assert_eq!(fp3.x1(), 10);
+    }
+}
diff --git a/sparse_strips/vello_common/src/lib.rs b/sparse_strips/vello_common/src/lib.rs
index 8024d24..7766d8a 100644
--- a/sparse_strips/vello_common/src/lib.rs
+++ b/sparse_strips/vello_common/src/lib.rs
@@ -4,3 +4,18 @@
 //! This crate contains core data structures and utilities shared across crates. It includes
 //! foundational types for path geometry, tiling, and other common operations used in both CPU and
 //! hybrid CPU/GPU rendering.
+
+#![cfg_attr(not(feature = "simd"), forbid(unsafe_code))]
+#![expect(
+    clippy::cast_possible_truncation,
+    reason = "We temporarily ignore those because the casts\
+only break in edge cases, and some of them are also only related to conversions from f64 to f32."
+)]
+mod footprint;
+
+pub mod coarse;
+pub mod flatten;
+pub mod strip;
+pub mod tile;
+
+pub use vello_api::*;
diff --git a/sparse_strips/vello_common/src/strip.rs b/sparse_strips/vello_common/src/strip.rs
new file mode 100644
index 0000000..312c9f2
--- /dev/null
+++ b/sparse_strips/vello_common/src/strip.rs
@@ -0,0 +1,190 @@
+// Copyright 2025 the Vello Authors
+// SPDX-License-Identifier: Apache-2.0 OR MIT
+
+//! Rendering strips.
+
+use crate::footprint::Footprint;
+use crate::tile::{Tiles, TILE_HEIGHT, TILE_WIDTH};
+use peniko::Fill;
+
+// Note that this will probably disappear and be turned into a const generic in the future.
+/// The height of a strip.
+pub const STRIP_HEIGHT: usize = 4;
+
+/// A strip.
+#[derive(Debug, Clone, Copy)]
+pub struct Strip {
+    /// The x coordinate of the strip, in user coordinates.
+    pub x: i32,
+    /// The y coordinate of the strip, in user coordinates.
+    pub y: u16,
+    /// The index into the alpha buffer
+    pub col: 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 / u16::try_from(STRIP_HEIGHT).unwrap()
+    }
+}
+
+/// 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<u32>,
+    fill_rule: Fill,
+) {
+    strip_buf.clear();
+
+    let mut strip_start = true;
+    let mut cols = alpha_buf.len() as u32;
+    let mut prev_tile = tiles.get(0);
+    let mut fp = prev_tile.footprint();
+    let mut seg_start = 0;
+    let mut delta = 0;
+
+    // Note: the input should contain a sentinel tile, to avoid having
+    // logic here to process the final strip.
+    for i in 1..tiles.len() {
+        let cur_tile = tiles.get(i);
+
+        if !prev_tile.same_loc(cur_tile) {
+            let start_delta = delta;
+            let same_strip = prev_tile.prev_loc(cur_tile);
+
+            if same_strip {
+                fp.extend(3);
+            }
+
+            let x0 = fp.x0();
+            let x1 = fp.x1();
+            let mut areas = [[start_delta as f32; TILE_WIDTH as usize]; TILE_HEIGHT as usize];
+
+            for j in seg_start..i {
+                let tile = tiles.get(j);
+
+                delta += tile.delta();
+
+                let p0 = tile.p0;
+                let p1 = tile.p1;
+                let inv_slope = (p1.x - p0.x) / (p1.y - p0.y);
+
+                // Note: We are iterating in column-major order because the inner loop always
+                // has a constant number of iterations, which makes it more SIMD-friendly. Worth
+                // running some tests whether a different order allows for better performance.
+                for x in x0..x1 {
+                    // Relative x offset of the start point from the
+                    // current column.
+                    let rel_x = p0.x - x as f32;
+
+                    for y in 0..STRIP_HEIGHT {
+                        // Relative y offset of the start
+                        // point from the current row.
+                        let rel_y = p0.y - y as f32;
+                        // y values will be 1 if the point is below the current row,
+                        // 0 if the point is above the current row, and between 0-1
+                        // if it is on the same row.
+                        let y0 = rel_y.clamp(0.0, 1.0);
+                        let y1 = (p1.y - y as f32).clamp(0.0, 1.0);
+                        // If != 0, then the line intersects the current row
+                        // in the current tile.
+                        let dy = y0 - y1;
+
+                        // x intersection points in the current tile.
+                        let xx0 = rel_x + (y0 - rel_y) * inv_slope;
+                        let xx1 = rel_x + (y1 - rel_y) * inv_slope;
+                        let xmin0 = xx0.min(xx1);
+                        let xmax = xx0.max(xx1);
+                        // Subtract a small delta to prevent a division by zero below.
+                        let xmin = xmin0.min(1.0) - 1e-6;
+                        // Clip x_max to the right side of the pixel.
+                        let b = xmax.min(1.0);
+                        // Clip x_max to the left side of the pixel.
+                        let c = b.max(0.0);
+                        // Clip x_min to the left side of the pixel.
+                        let d = xmin.max(0.0);
+                        // Calculate the covered area.
+                        // TODO: How is this formula derived?
+                        let mut a = (b + 0.5 * (d * d - c * c) - xmin) / (xmax - xmin);
+                        // a can be NaN if dy == 0 (and thus xmax - xmin = 0, resulting in
+                        // a division by 0 above). This code changes those NaNs to 0.
+                        a = a.abs().max(0.).copysign(a);
+
+                        areas[x as usize][y] += a * dy;
+
+                        // Making this branchless doesn't lead to any performance improvements
+                        // according to my measurements.
+                        if p0.x == 0.0 {
+                            areas[x as usize][y] += (y as f32 - p0.y + 1.0).clamp(0.0, 1.0);
+                        } else if p1.x == 0.0 {
+                            areas[x as usize][y] -= (y as f32 - p1.y + 1.0).clamp(0.0, 1.0);
+                        }
+                    }
+                }
+            }
+
+            macro_rules! fill {
+                ($rule:expr) => {
+                    for x in x0..x1 {
+                        let mut alphas = 0_u32;
+
+                        for y in 0..STRIP_HEIGHT {
+                            let area = areas[x as usize][y];
+                            let coverage = $rule(area);
+                            let area_u8 = (coverage * 255.0 + 0.5) as u32;
+
+                            alphas += area_u8 << (y * 8);
+                        }
+
+                        alpha_buf.push(alphas);
+                    }
+                };
+            }
+
+            match fill_rule {
+                Fill::NonZero => {
+                    fill!(|area: f32| area.abs().min(1.0))
+                }
+                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())
+                }
+            }
+
+            if strip_start {
+                let strip = Strip {
+                    x: 4 * prev_tile.x + x0 as i32,
+                    y: 4 * prev_tile.y,
+                    col: cols,
+                    winding: start_delta,
+                };
+
+                strip_buf.push(strip);
+            }
+
+            cols += x1 - x0;
+            fp = if same_strip {
+                Footprint::from_index(0)
+            } else {
+                Footprint::empty()
+            };
+
+            strip_start = !same_strip;
+            seg_start = i;
+
+            if !prev_tile.same_row(cur_tile) {
+                delta = 0;
+            }
+        }
+
+        fp.merge(&cur_tile.footprint());
+
+        prev_tile = cur_tile;
+    }
+}
diff --git a/sparse_strips/vello_common/src/tile.rs b/sparse_strips/vello_common/src/tile.rs
new file mode 100644
index 0000000..0cceae6
--- /dev/null
+++ b/sparse_strips/vello_common/src/tile.rs
@@ -0,0 +1,516 @@
+// 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]);
+    }
+}