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