vello_common/
tile.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Primitives for creating tiles.
5
6use crate::flatten::Line;
7use alloc::vec;
8use alloc::vec::Vec;
9use fearless_simd::Level;
10#[cfg(not(feature = "std"))]
11use peniko::kurbo::common::FloatFuncs as _;
12
13/// The max number of lines per path.
14///
15/// Trying to render a path with more lines than this may result in visual artifacts.
16pub const MAX_LINES_PER_PATH: u32 = 1 << 31;
17
18/// A tile represents an aligned area on the pixmap, used to subdivide the viewport into sub-areas
19/// (currently 4x4) and analyze line intersections inside each such area.
20///
21/// Keep in mind that it is possible to have multiple tiles with the same index,
22/// namely if we have multiple lines crossing the same 4x4 area!
23///
24/// # Note
25///
26/// This struct is `#[repr(C)]`, but the byte order of its fields is dependent on the endianness of
27/// the compilation target.
28#[derive(Debug, Clone, Copy)]
29#[repr(C)]
30pub struct Tile {
31    // The field ordering is important.
32    //
33    // The given ordering (variant over little and big endian compilation targets), ensures that
34    // `Tile::to_bits` doesn't do any actual work, as the ordering of the fields is such that the
35    // numeric value of a `Tile` in memory is identical as returned by that method. This allows
36    // for, e.g., comparison and sorting.
37    #[cfg(target_endian = "big")]
38    /// The index of the tile in the y direction.
39    pub y: u16,
40
41    #[cfg(target_endian = "big")]
42    /// The index of the tile in the x direction.
43    pub x: u16,
44
45    /// The index of the line this tile belongs to into the line buffer, plus whether the line
46    /// crosses the top edge of the tile, packed together.
47    ///
48    /// The index is the unsigned number in the 31 least significant bits of this value.
49    ///
50    /// The last bit is 1 if and only if the lines crosses the tile's top edge. Lines making this
51    /// crossing increment or decrement the coarse tile winding, depending on the line direction.
52    pub packed_winding_line_idx: u32,
53
54    #[cfg(target_endian = "little")]
55    /// The index of the tile in the x direction.
56    pub x: u16,
57
58    #[cfg(target_endian = "little")]
59    /// The index of the tile in the y direction.
60    pub y: u16,
61}
62
63impl Tile {
64    /// The width of a tile in pixels.
65    pub const WIDTH: u16 = 4;
66
67    /// The height of a tile in pixels.
68    pub const HEIGHT: u16 = 4;
69
70    /// Create a new tile.
71    ///
72    /// `line_idx` must be smaller than [`MAX_LINES_PER_PATH`].
73    #[inline]
74    pub const fn new(x: u16, y: u16, line_idx: u32, winding: bool) -> Self {
75        #[cfg(debug_assertions)]
76        if line_idx >= MAX_LINES_PER_PATH {
77            panic!("Max. number of lines per path exceeded.");
78        }
79        Self {
80            x,
81            y,
82            packed_winding_line_idx: ((winding as u32) << 31) | line_idx,
83        }
84    }
85
86    /// Check whether two tiles are at the same location.
87    #[inline]
88    pub const fn same_loc(&self, other: &Self) -> bool {
89        self.same_row(other) && self.x == other.x
90    }
91
92    /// Check whether `self` is adjacent to the left of `other`.
93    #[inline]
94    pub const fn prev_loc(&self, other: &Self) -> bool {
95        self.same_row(other) && self.x + 1 == other.x
96    }
97
98    /// Check whether two tiles are on the same row.
99    #[inline]
100    pub const fn same_row(&self, other: &Self) -> bool {
101        self.y == other.y
102    }
103
104    /// The index of the line this tile belongs to into the line buffer.
105    #[inline]
106    pub const fn line_idx(&self) -> u32 {
107        self.packed_winding_line_idx & (MAX_LINES_PER_PATH - 1)
108    }
109
110    /// Whether the line crosses the top edge of the tile.
111    ///
112    /// Lines making this crossing increment or decrement the coarse tile winding, depending on the
113    /// line direction.
114    #[inline]
115    pub const fn winding(&self) -> bool {
116        (self.packed_winding_line_idx & MAX_LINES_PER_PATH) != 0
117    }
118
119    /// Return the `u64` representation of this tile.
120    ///
121    /// This is the u64 interpretation of `(y, x, packed_winding_line_idx)` where `y` is the
122    /// most-significant part of the number and `packed_winding_line_idx` the least significant.
123    #[inline(always)]
124    const fn to_bits(self) -> u64 {
125        ((self.y as u64) << 48) | ((self.x as u64) << 32) | self.packed_winding_line_idx as u64
126    }
127}
128
129impl PartialEq for Tile {
130    #[inline(always)]
131    fn eq(&self, other: &Self) -> bool {
132        self.to_bits() == other.to_bits()
133    }
134}
135
136impl Ord for Tile {
137    #[inline(always)]
138    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
139        self.to_bits().cmp(&other.to_bits())
140    }
141}
142
143impl PartialOrd for Tile {
144    #[inline(always)]
145    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
146        Some(self.cmp(other))
147    }
148}
149
150impl Eq for Tile {}
151
152/// Handles the tiling of paths.
153#[derive(Clone, Debug)]
154pub struct Tiles {
155    tile_buf: Vec<Tile>,
156    level: Level,
157    sorted: bool,
158}
159
160impl Tiles {
161    /// Create a new tiles container.
162    pub fn new(level: Level) -> Self {
163        Self {
164            tile_buf: vec![],
165            level,
166            sorted: false,
167        }
168    }
169
170    /// Get the number of tiles in the container.
171    pub fn len(&self) -> u32 {
172        self.tile_buf.len() as u32
173    }
174
175    /// Returns `true` if the container has no tiles.
176    pub fn is_empty(&self) -> bool {
177        self.tile_buf.is_empty()
178    }
179
180    /// Reset the tiles' container.
181    pub fn reset(&mut self) {
182        self.tile_buf.clear();
183        self.sorted = false;
184    }
185
186    /// Sort the tiles in the container.
187    pub fn sort_tiles(&mut self) {
188        self.sorted = true;
189        // To enable auto-vectorization.
190        self.level.dispatch(|_| self.tile_buf.sort_unstable());
191    }
192
193    /// Get the tile at a certain index.
194    ///
195    /// Panics if the container hasn't been sorted before.
196    #[inline]
197    pub fn get(&self, index: u32) -> &Tile {
198        assert!(
199            self.sorted,
200            "attempted to call `get` before sorting the tile container."
201        );
202
203        &self.tile_buf[index as usize]
204    }
205
206    /// Iterate over the tiles in sorted order.
207    ///
208    /// Panics if the container hasn't been sorted before.
209    #[inline]
210    pub fn iter(&self) -> impl Iterator<Item = &Tile> {
211        assert!(
212            self.sorted,
213            "attempted to call `iter` before sorting the tile container."
214        );
215
216        self.tile_buf.iter()
217    }
218
219    /// Populate the tiles' container with a buffer of lines.
220    ///
221    /// Tiles exceeding the top, right or bottom of the viewport (given by `width` and `height` in
222    /// pixels) are culled.
223    //
224    // TODO: Tiles are clamped to the left edge of the viewport, but lines fully to the left of the
225    // viewport are not culled yet. These lines impact winding, and would need forwarding of
226    // winding to the strip generation stage.
227    pub fn make_tiles(&mut self, lines: &[Line], width: u16, height: u16) {
228        self.reset();
229
230        if width == 0 || height == 0 {
231            return;
232        }
233
234        debug_assert!(
235            lines.len() <= MAX_LINES_PER_PATH as usize,
236            "Max. number of lines per path exceeded. Max is {}, got {}.",
237            MAX_LINES_PER_PATH,
238            lines.len()
239        );
240
241        let tile_columns = width.div_ceil(Tile::WIDTH);
242        let tile_rows = height.div_ceil(Tile::HEIGHT);
243
244        for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() {
245            let line_idx = line_idx as u32;
246
247            let p0_x = line.p0.x / f32::from(Tile::WIDTH);
248            let p0_y = line.p0.y / f32::from(Tile::HEIGHT);
249            let p1_x = line.p1.x / f32::from(Tile::WIDTH);
250            let p1_y = line.p1.y / f32::from(Tile::HEIGHT);
251
252            let (line_left_x, line_right_x) = if p0_x < p1_x {
253                (p0_x, p1_x)
254            } else {
255                (p1_x, p0_x)
256            };
257            let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
258                (p0_y, p0_x, p1_y, p1_x)
259            } else {
260                (p1_y, p1_x, p0_y, p0_x)
261            };
262
263            // For ease of logic, special-case purely vertical tiles.
264            if line_left_x == line_right_x {
265                // Do not emit tiles that are strictly on the right of the viewport. They do not
266                // impact winding, and if we don't do this, we might end up with too big tile
267                // coordinates, which will cause overflows in strip rendering.
268                if line_left_x as u16 >= tile_columns {
269                    continue;
270                }
271
272                let y_top_tiles = (line_top_y as u16).min(tile_rows);
273                let y_bottom_tiles = (line_bottom_y.ceil() as u16).min(tile_rows);
274
275                let x = line_left_x as u16;
276                for y_idx in y_top_tiles..y_bottom_tiles {
277                    let y = f32::from(y_idx);
278
279                    let tile = Tile::new(x, y_idx, line_idx, y >= line_top_y);
280                    self.tile_buf.push(tile);
281                }
282            } else {
283                let x_slope = (p1_x - p0_x) / (p1_y - p0_y);
284
285                let y_top_tiles = (line_top_y as u16).min(tile_rows);
286                let y_bottom_tiles = (line_bottom_y.ceil() as u16).min(tile_rows);
287
288                for y_idx in y_top_tiles..y_bottom_tiles {
289                    let y = f32::from(y_idx);
290
291                    // The line's y-coordinates at the line's top- and bottom-most points within
292                    // the tile row.
293                    let line_row_top_y = line_top_y.max(y).min(y + 1.);
294                    let line_row_bottom_y = line_bottom_y.max(y).min(y + 1.);
295
296                    // The line's x-coordinates at the line's top- and bottom-most points within the
297                    // tile row.
298                    let line_row_top_x = p0_x + (line_row_top_y - p0_y) * x_slope;
299                    let line_row_bottom_x = p0_x + (line_row_bottom_y - p0_y) * x_slope;
300
301                    // The line's x-coordinates at the line's left- and right-most points within the
302                    // tile row.
303                    let line_row_left_x =
304                        f32::min(line_row_top_x, line_row_bottom_x).max(line_left_x);
305                    let line_row_right_x =
306                        f32::max(line_row_top_x, line_row_bottom_x).min(line_right_x);
307
308                    let winding_x = if line_top_x < line_bottom_x {
309                        line_row_left_x as u16
310                    } else {
311                        line_row_right_x as u16
312                    };
313
314                    for x_idx in
315                        line_row_left_x as u16..=(line_row_right_x as u16).min(tile_columns - 1)
316                    {
317                        let tile = Tile::new(
318                            x_idx,
319                            y_idx,
320                            line_idx,
321                            y >= line_top_y && x_idx == winding_x,
322                        );
323                        self.tile_buf.push(tile);
324                    }
325                }
326            }
327        }
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use crate::flatten::{FlattenCtx, Line, Point, fill};
334    use crate::kurbo::{Affine, BezPath};
335    use crate::tile::{Tile, Tiles};
336    use fearless_simd::Level;
337    use std::vec;
338
339    #[test]
340    fn cull_line_at_top() {
341        let line = Line {
342            p0: Point { x: 3.0, y: -5.0 },
343            p1: Point { x: 9.0, y: -1.0 },
344        };
345
346        let mut tiles = Tiles::new(Level::new());
347        tiles.make_tiles(&[line], 100, 100);
348
349        assert!(tiles.is_empty());
350    }
351
352    #[test]
353    fn cull_line_at_right() {
354        let line = Line {
355            p0: Point { x: 101.0, y: 0.0 },
356            p1: Point { x: 103.0, y: 20.0 },
357        };
358
359        let mut tiles = Tiles::new(Level::new());
360        tiles.make_tiles(&[line], 100, 100);
361
362        assert!(tiles.is_empty());
363    }
364
365    #[test]
366    fn cull_line_at_bottom() {
367        let line = Line {
368            p0: Point { x: 30.0, y: 101.0 },
369            p1: Point { x: 35.0, y: 105.0 },
370        };
371
372        let mut tiles = Tiles::new(Level::new());
373        tiles.make_tiles(&[line], 100, 100);
374
375        assert!(tiles.is_empty());
376    }
377
378    #[test]
379    fn partially_cull_line_exceeding_viewport() {
380        let line = Line {
381            p0: Point { x: -2.0, y: -3.0 },
382            p1: Point { x: 2.0, y: 1.0 },
383        };
384
385        let mut tiles = Tiles::new(Level::new());
386        tiles.make_tiles(&[line], 100, 100);
387
388        assert_eq!(tiles.tile_buf, [Tile::new(0, 0, 0, true)]);
389    }
390
391    #[test]
392    fn horizontal_straight_line() {
393        let line = Line {
394            p0: Point { x: 1.5, y: 1.0 },
395            p1: Point { x: 8.5, y: 1.0 },
396        };
397
398        let mut tiles = Tiles::new(Level::new());
399        tiles.make_tiles(&[line], 100, 100);
400        tiles.sort_tiles();
401
402        assert_eq!(
403            tiles.tile_buf,
404            [
405                Tile::new(0, 0, 0, false),
406                Tile::new(1, 0, 0, false),
407                Tile::new(2, 0, 0, false),
408            ]
409        );
410    }
411
412    #[test]
413    fn vertical_straight_line() {
414        let line = Line {
415            p0: Point { x: 1.0, y: 1.5 },
416            p1: Point { x: 1.0, y: 8.5 },
417        };
418
419        let mut tiles = Tiles::new(Level::new());
420        tiles.make_tiles(&[line], 100, 100);
421        tiles.sort_tiles();
422
423        assert_eq!(
424            tiles.tile_buf,
425            [
426                Tile::new(0, 0, 0, false),
427                Tile::new(0, 1, 0, true),
428                Tile::new(0, 2, 0, true),
429            ]
430        );
431    }
432
433    #[test]
434    fn top_left_to_bottom_right() {
435        let line = Line {
436            p0: Point { x: 1.0, y: 1.0 },
437            p1: Point { x: 11.0, y: 8.5 },
438        };
439
440        let mut tiles = Tiles::new(Level::new());
441        tiles.make_tiles(&[line], 100, 100);
442        tiles.sort_tiles();
443
444        assert_eq!(
445            tiles.tile_buf,
446            [
447                Tile::new(0, 0, 0, false),
448                Tile::new(1, 0, 0, false),
449                Tile::new(1, 1, 0, true),
450                Tile::new(2, 1, 0, false),
451                Tile::new(2, 2, 0, true),
452            ]
453        );
454    }
455
456    #[test]
457    fn bottom_right_to_top_left() {
458        let line = Line {
459            p0: Point { x: 11.0, y: 8.5 },
460            p1: Point { x: 1.0, y: 1.0 },
461        };
462
463        let mut tiles = Tiles::new(Level::new());
464        tiles.make_tiles(&[line], 100, 100);
465        tiles.sort_tiles();
466
467        assert_eq!(
468            tiles.tile_buf,
469            [
470                Tile::new(0, 0, 0, false),
471                Tile::new(1, 0, 0, false),
472                Tile::new(1, 1, 0, true),
473                Tile::new(2, 1, 0, false),
474                Tile::new(2, 2, 0, true),
475            ]
476        );
477    }
478
479    #[test]
480    fn bottom_left_to_top_right() {
481        let line = Line {
482            p0: Point { x: 2.0, y: 11.0 },
483            p1: Point { x: 14.0, y: 6.0 },
484        };
485
486        let mut tiles = Tiles::new(Level::new());
487        tiles.make_tiles(&[line], 100, 100);
488        tiles.sort_tiles();
489
490        assert_eq!(
491            tiles.tile_buf,
492            [
493                Tile::new(2, 1, 0, false),
494                Tile::new(3, 1, 0, false),
495                Tile::new(0, 2, 0, false),
496                Tile::new(1, 2, 0, false),
497                Tile::new(2, 2, 0, true),
498            ]
499        );
500    }
501
502    #[test]
503    fn top_right_to_bottom_left() {
504        let line = Line {
505            p0: Point { x: 14.0, y: 6.0 },
506            p1: Point { x: 2.0, y: 11.0 },
507        };
508
509        let mut tiles = Tiles::new(Level::new());
510        tiles.make_tiles(&[line], 100, 100);
511        tiles.sort_tiles();
512
513        assert_eq!(
514            tiles.tile_buf,
515            [
516                Tile::new(2, 1, 0, false),
517                Tile::new(3, 1, 0, false),
518                Tile::new(0, 2, 0, false),
519                Tile::new(1, 2, 0, false),
520                Tile::new(2, 2, 0, true),
521            ]
522        );
523    }
524
525    #[test]
526    fn two_lines_in_single_tile() {
527        let line_1 = Line {
528            p0: Point { x: 1.0, y: 3.0 },
529            p1: Point { x: 3.0, y: 3.0 },
530        };
531
532        let line_2 = Line {
533            p0: Point { x: 3.0, y: 3.0 },
534            p1: Point { x: 0.0, y: 1.0 },
535        };
536
537        let mut tiles = Tiles::new(Level::new());
538        tiles.make_tiles(&[line_1, line_2], 100, 100);
539
540        assert_eq!(
541            tiles.tile_buf,
542            [Tile::new(0, 0, 0, false), Tile::new(0, 0, 1, false),]
543        );
544    }
545
546    #[test]
547    // See https://github.com/LaurenzV/cpu-sparse-experiments/issues/46.
548    fn infinite_loop() {
549        let line = Line {
550            p0: Point { x: 22.0, y: 552.0 },
551            p1: Point { x: 224.0, y: 388.0 },
552        };
553
554        let mut tiles = Tiles::new(Level::new());
555        tiles.make_tiles(&[line], 600, 600);
556    }
557
558    #[test]
559    fn vertical_path_on_the_right_of_viewport() {
560        let path = BezPath::from_svg("M261,0 L78848,0 L78848,4 L261,4 Z").unwrap();
561        let mut line_buf = vec![];
562        fill(
563            Level::new(),
564            &path,
565            Affine::IDENTITY,
566            &mut line_buf,
567            &mut FlattenCtx::default(),
568        );
569
570        let mut tiles = Tiles::new(Level::new());
571        tiles.make_tiles(&line_buf, 10, 10);
572        assert!(tiles.is_empty());
573    }
574}