Skip to main content

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 << 26;
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, intersection data,
46    /// and winding data packed together.
47    ///
48    /// The layout is:
49    /// - **Bits 0-5 (6 bits):** Intersection and Winding Mask (`W | P | R | L | B | T`).
50    ///   - Bit 0 (mask `0b000001`): Intersects top edge (T)
51    ///   - Bit 1 (mask `0b000010`): Intersects bottom edge (B)
52    ///   - Bit 2 (mask `0b000100`): Intersects left edge (L)
53    ///   - Bit 3 (mask `0b001000`): Intersects right edge (R)
54    ///   - Bit 4 (mask `0b010000`): Perfect Corner (P) - True if line intersects ANY corner exactly.
55    ///   - Bit 5 (mask `0b100000`): Winding (W) - 1 if crosses top edge.
56    /// - **Bits 6-31 (26 bits):** The line index (`line_idx`).
57    ///
58    /// **Sorting Note:** The `line_idx` occupies the higher bits to ensure that when sorting
59    /// tiles with the same (x, y) coordinates, they are sorted by their line index first,
60    /// and then by their intersection mask.
61    pub packed_winding_line_idx: u32,
62
63    #[cfg(target_endian = "little")]
64    /// The index of the tile in the x direction.
65    pub x: u16,
66
67    #[cfg(target_endian = "little")]
68    /// The index of the tile in the y direction.
69    pub y: u16,
70}
71
72impl Tile {
73    /// The width of a tile in pixels.
74    pub const WIDTH: u16 = 4;
75
76    /// The height of a tile in pixels.
77    pub const HEIGHT: u16 = 4;
78
79    /// Create a new tile.
80    /// `x` and `y` will be clamped to the largest possible coordinate if they are too large.
81    ///
82    /// `line_idx` must be smaller than [`MAX_LINES_PER_PATH`].
83    #[inline]
84    pub fn new_clamped(x: u16, y: u16, line_idx: u32, intersection_mask: u32) -> Self {
85        Self::new(
86            // Make sure that x and y stay in range when multiplying
87            // with the tile width and height during strips generation.
88            x.min(u16::MAX / Self::WIDTH),
89            y.min(u16::MAX / Self::HEIGHT),
90            line_idx,
91            intersection_mask,
92        )
93    }
94
95    /// The base tile constructor
96    ///
97    /// Unlike [`Self::new_clamped`], this constructor stores `x` and `y` exactly as provided.
98    /// Callers must ensure these coordinates do not exceed the limits required by downstream
99    /// processing (typically `u16::MAX / WIDTH` and `u16::MAX / HEIGHT`).
100    #[inline]
101    pub const fn new(x: u16, y: u16, line_idx: u32, intersection_mask: u32) -> Self {
102        #[cfg(debug_assertions)]
103        if line_idx >= MAX_LINES_PER_PATH {
104            panic!("Max. number of lines per path exceeded.");
105        }
106        // The intersection_mask is expected to contain bits 0-5 (T, B, L, R, P, W).
107        // We pack line_idx into the high bits (6-31) and intersection_mask into low bits (0-5).
108        Self {
109            x,
110            y,
111            packed_winding_line_idx: (line_idx << 6) | intersection_mask,
112        }
113    }
114
115    /// Check whether two tiles are at the same location.
116    #[inline]
117    pub const fn same_loc(&self, other: &Self) -> bool {
118        self.same_row(other) && self.x == other.x
119    }
120
121    /// Check whether `self` is adjacent to the left of `other`.
122    #[inline]
123    pub const fn prev_loc(&self, other: &Self) -> bool {
124        self.same_row(other) && self.x + 1 == other.x
125    }
126
127    /// Check whether two tiles are on the same row.
128    #[inline]
129    pub const fn same_row(&self, other: &Self) -> bool {
130        self.y == other.y
131    }
132
133    /// The index of the line this tile belongs to into the line buffer.
134    ///
135    /// Returns the high 26 bits.
136    #[inline]
137    pub const fn line_idx(&self) -> u32 {
138        self.packed_winding_line_idx >> 6
139    }
140
141    /// Whether the line crosses the top edge of the tile.
142    ///
143    /// Lines making this crossing increment or decrement the coarse tile winding, depending on the
144    /// line direction.
145    ///
146    /// Checks Bit 5 (Winding).
147    #[inline]
148    pub const fn winding(&self) -> bool {
149        (self.packed_winding_line_idx & (1 << 5)) != 0
150    }
151
152    /// The 6 bits of intersection and winding data.
153    ///
154    /// - **Bits 0-5 (mask `0b111111`):** Mask `W | P | R | L | B | T`
155    ///   - Bit 0 (mask `0b000001`): Intersects top edge
156    ///   - Bit 1 (mask `0b000010`): Intersects bottom edge
157    ///   - Bit 2 (mask `0b000100`): Intersects left edge
158    ///   - Bit 3 (mask `0b001000`): Intersects right edge
159    ///   - Bit 4 (mask `0b010000`): Perfect Corner (intersects a corner exactly)
160    ///   - Bit 5 (mask `0b100000`): Winding
161    #[inline]
162    pub const fn intersection_mask(&self) -> u32 {
163        self.packed_winding_line_idx & 0b111111
164    }
165
166    /// Whether the line intersects the top edge of the tile.
167    #[inline]
168    pub const fn intersects_top(&self) -> bool {
169        (self.intersection_mask() & 0b000001) != 0
170    }
171
172    /// Whether the line intersects the bottom edge of the tile.
173    #[inline]
174    pub const fn intersects_bottom(&self) -> bool {
175        (self.intersection_mask() & 0b000010) != 0
176    }
177
178    /// Whether the line intersects the left edge of the tile.
179    #[inline]
180    pub const fn intersects_left(&self) -> bool {
181        (self.intersection_mask() & 0b000100) != 0
182    }
183
184    /// Whether the line intersects the right edge of the tile.
185    #[inline]
186    pub const fn intersects_right(&self) -> bool {
187        (self.intersection_mask() & 0b001000) != 0
188    }
189
190    /// Whether the tile intersects a perfect corner.
191    ///
192    /// This is true when the line intersects TL, TR, BL, or BR exactly.
193    #[inline]
194    pub const fn is_perfect_corner(&self) -> bool {
195        (self.intersection_mask() & 0b010000) != 0
196    }
197
198    /// Return the `u64` representation of this tile.
199    ///
200    /// This is the u64 interpretation of `(y, x, packed_winding_line_idx)` where `y` is the
201    /// most-significant part of the number and `packed_winding_line_idx` the least significant.
202    #[inline(always)]
203    const fn to_bits(self) -> u64 {
204        ((self.y as u64) << 48) | ((self.x as u64) << 32) | self.packed_winding_line_idx as u64
205    }
206}
207
208impl PartialEq for Tile {
209    #[inline(always)]
210    fn eq(&self, other: &Self) -> bool {
211        self.to_bits() == other.to_bits()
212    }
213}
214
215impl Ord for Tile {
216    #[inline(always)]
217    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
218        self.to_bits().cmp(&other.to_bits())
219    }
220}
221
222impl PartialOrd for Tile {
223    #[inline(always)]
224    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
225        Some(self.cmp(other))
226    }
227}
228
229impl Eq for Tile {}
230
231/// Handles the tiling of paths.
232#[derive(Clone, Debug)]
233pub struct Tiles {
234    tile_buf: Vec<Tile>,
235    level: Level,
236    sorted: bool,
237}
238
239impl Tiles {
240    /// Create a new tiles container.
241    pub fn new(level: Level) -> Self {
242        Self {
243            tile_buf: vec![],
244            level,
245            sorted: false,
246        }
247    }
248
249    /// Get the number of tiles in the container.
250    pub fn len(&self) -> u32 {
251        self.tile_buf.len() as u32
252    }
253
254    /// Returns `true` if the container has no tiles.
255    pub fn is_empty(&self) -> bool {
256        self.tile_buf.is_empty()
257    }
258
259    /// Reset the tiles' container.
260    pub fn reset(&mut self) {
261        self.tile_buf.clear();
262        self.sorted = false;
263    }
264
265    /// Sort the tiles in the container.
266    pub fn sort_tiles(&mut self) {
267        self.sorted = true;
268        // To enable auto-vectorization.
269        self.level.dispatch(|_| self.tile_buf.sort_unstable());
270    }
271
272    /// Get the tile at a certain index.
273    ///
274    /// Panics if the container hasn't been sorted before.
275    #[inline]
276    pub fn get(&self, index: u32) -> &Tile {
277        assert!(
278            self.sorted,
279            "attempted to call `get` before sorting the tile container."
280        );
281
282        &self.tile_buf[index as usize]
283    }
284
285    /// Iterate over the tiles in sorted order.
286    ///
287    /// Panics if the container hasn't been sorted before.
288    #[inline]
289    pub fn iter(&self) -> impl Iterator<Item = &Tile> {
290        assert!(
291            self.sorted,
292            "attempted to call `iter` before sorting the tile container."
293        );
294
295        self.tile_buf.iter()
296    }
297
298    /// Generates tile commands for Analytic Anti-Aliasing rasterization. Unlike the MSAA path, this
299    /// function performs "coarse binning" to simply identify every tile a line segment traverses.
300    /// It encodes the line index and winding direction, delegating the precise calculation of pixel
301    /// coverage to `strip::render`.
302    //
303    // TODO: Tiles are clamped to the left edge of the viewport, but lines fully to the left of the
304    // viewport are not culled yet. These lines impact winding, and would need forwarding of
305    // winding to the strip generation stage.
306    pub fn make_tiles_analytic_aa(&mut self, lines: &[Line], width: u16, height: u16) {
307        self.reset();
308
309        if width == 0 || height == 0 {
310            return;
311        }
312
313        debug_assert!(
314            lines.len() <= MAX_LINES_PER_PATH as usize,
315            "Max. number of lines per path exceeded. Max is {}, got {}.",
316            MAX_LINES_PER_PATH,
317            lines.len()
318        );
319
320        let tile_columns = width.div_ceil(Tile::WIDTH);
321        let tile_rows = height.div_ceil(Tile::HEIGHT);
322
323        for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() {
324            let line_idx = line_idx as u32;
325
326            let p0_x = line.p0.x / f32::from(Tile::WIDTH);
327            let p0_y = line.p0.y / f32::from(Tile::HEIGHT);
328            let p1_x = line.p1.x / f32::from(Tile::WIDTH);
329            let p1_y = line.p1.y / f32::from(Tile::HEIGHT);
330
331            let (line_left_x, line_right_x) = if p0_x < p1_x {
332                (p0_x, p1_x)
333            } else {
334                (p1_x, p0_x)
335            };
336
337            // Lines whose left-most endpoint exceed the right edge of the viewport are culled
338            if line_left_x > tile_columns as f32 {
339                continue;
340            }
341
342            let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
343                (p0_y, p0_x, p1_y, p1_x)
344            } else {
345                (p1_y, p1_x, p0_y, p0_x)
346            };
347
348            // The `as u16` casts here intentionally clamp negative coordinates to 0.
349            let y_top_tiles = (line_top_y as u16).min(tile_rows);
350            let line_bottom_y_ceil = line_bottom_y.ceil();
351            let y_bottom_tiles = (line_bottom_y_ceil as u16).min(tile_rows);
352
353            // If y_top_tiles == y_bottom_tiles, then the line is either completely above or below
354            // the viewport OR it is perfectly horizontal and aligned to the tile grid, contributing
355            // no winding. In either case, it should be culled.
356            if y_top_tiles >= y_bottom_tiles {
357                // Technically, the `>` part of the `>=` is unnecessary due to clamping, but this
358                // gives stronger signal
359                continue;
360            }
361
362            // Get tile coordinates for start/end points, use i32 to preserve negative coordinates
363            let p0_tile_x = line_top_x.floor() as i32;
364            let p0_tile_y = line_top_y.floor() as i32;
365            let p1_tile_x = line_bottom_x.floor() as i32;
366            let p1_tile_y = line_bottom_y.floor() as i32;
367
368            // Special-case out lines which are fully contained within a tile.
369            let not_same_tile = p0_tile_y != p1_tile_y || p0_tile_x != p1_tile_x;
370            if not_same_tile {
371                // For ease of logic, special-case purely vertical tiles.
372                if line_left_x == line_right_x {
373                    let x = (line_left_x as u16).min(tile_columns.saturating_sub(1));
374
375                    // Row Start, not culled.
376                    let is_start_culled = line_top_y < 0.0;
377                    if !is_start_culled {
378                        let winding = ((f32::from(y_top_tiles) >= line_top_y) as u32) << 5;
379                        let tile = Tile::new_clamped(x, y_top_tiles, line_idx, winding);
380                        self.tile_buf.push(tile);
381                    }
382
383                    // Middle
384                    // If the start was culled, the first tile inside the viewport is a middle.
385                    let y_start = if is_start_culled {
386                        y_top_tiles
387                    } else {
388                        y_top_tiles + 1
389                    };
390                    let line_bottom_floor = line_bottom_y.floor();
391                    let y_end_idx = (line_bottom_floor as u16).min(tile_rows);
392
393                    for y_idx in y_start..y_end_idx {
394                        let tile = Tile::new_clamped(x, y_idx, line_idx, 0b100000);
395                        self.tile_buf.push(tile);
396                    }
397
398                    // Row End, handle the final tile (y_end_idx), but *only* if the line does
399                    // not perfectly end on the top edge of the tile. In the case that it does,
400                    // it gets handled by the middle logic above.
401                    if line_bottom_y != line_bottom_floor && y_end_idx < tile_rows {
402                        let tile = Tile::new_clamped(x, y_end_idx, line_idx, 0b100000);
403                        self.tile_buf.push(tile);
404                    }
405                } else {
406                    let dx = p1_x - p0_x;
407                    let dy = p1_y - p0_y;
408                    let x_slope = dx / dy;
409                    let dx_dir = (line_bottom_x >= line_top_x) as u32;
410                    let not_dx_dir = dx_dir ^ 1;
411
412                    let w_start_base = dx_dir << 5;
413                    let w_end_base = not_dx_dir << 5;
414
415                    let mut push_row = |y_idx: u16,
416                                        row_top_y: f32,
417                                        row_bottom_y: f32,
418                                        w_start: u32,
419                                        w_end: u32,
420                                        w_single: u32| {
421                        let row_top_x = p0_x + (row_top_y - p0_y) * x_slope;
422                        let row_bottom_x = p0_x + (row_bottom_y - p0_y) * x_slope;
423
424                        let row_left_x = f32::min(row_top_x, row_bottom_x).max(line_left_x);
425                        let row_right_x = f32::max(row_top_x, row_bottom_x).min(line_right_x);
426
427                        let x_start = row_left_x as u16;
428                        let x_end = (row_right_x as u16).min(tile_columns.saturating_sub(1));
429
430                        if x_start <= x_end {
431                            let winding = if x_start == x_end { w_single } else { w_start };
432
433                            self.tile_buf
434                                .push(Tile::new(x_start, y_idx, line_idx, winding));
435
436                            for x_idx in x_start.saturating_add(1)..x_end {
437                                self.tile_buf.push(Tile::new(x_idx, y_idx, line_idx, 0));
438                            }
439
440                            if x_start < x_end {
441                                self.tile_buf.push(Tile::new(x_end, y_idx, line_idx, w_end));
442                            }
443                        }
444                    };
445
446                    let is_start_culled = line_top_y < 0.0;
447                    if !is_start_culled {
448                        let y = f32::from(y_top_tiles);
449                        let row_bottom_y = (y + 1.0).min(line_bottom_y);
450                        let mask = ((y >= line_top_y) as u32) << 5;
451                        push_row(
452                            y_top_tiles,
453                            line_top_y,
454                            row_bottom_y,
455                            w_start_base & mask,
456                            w_end_base & mask,
457                            0b100000 & mask,
458                        );
459                    }
460
461                    let y_start_middle = if is_start_culled {
462                        y_top_tiles
463                    } else {
464                        y_top_tiles + 1
465                    };
466
467                    let line_bottom_floor = line_bottom_y.floor();
468                    let y_end_middle = (line_bottom_floor as u16).min(tile_rows);
469                    for y_idx in y_start_middle..y_end_middle {
470                        let y = f32::from(y_idx);
471                        let row_bottom_y = (y + 1.0).min(line_bottom_y);
472                        push_row(y_idx, y, row_bottom_y, w_start_base, w_end_base, 0b100000);
473                    }
474
475                    if line_bottom_y != line_bottom_floor
476                        && y_end_middle < tile_rows
477                        && (is_start_culled || y_end_middle != y_top_tiles)
478                    {
479                        let y_idx = y_end_middle;
480                        let y = f32::from(y_idx);
481                        push_row(y_idx, y, line_bottom_y, w_start_base, w_end_base, 0b100000);
482                    }
483                }
484            } else {
485                // Case: Line is fully contained within a single tile.
486                let tile = Tile::new_clamped(
487                    (line_left_x as u16).min(tile_columns + 1),
488                    y_top_tiles,
489                    line_idx,
490                    ((f32::from(y_top_tiles) >= line_top_y) as u32) << 5,
491                );
492                self.tile_buf.push(tile);
493            }
494        }
495    }
496
497    /// Generates tile commands for MSAA (Multisample Anti-Aliasing) rasterization.
498    ///
499    /// [ Architecture & Watertightness ]
500    /// The primary goal of this function is to establish a source of "ground truth" for line-tile
501    /// intersections. Because the downstream rasterization (MSAA) occurs in parallel, it is
502    /// critical that intersections are "watertight." If Thread A handles Tile (0,0) and Thread B
503    /// handles Tile (1,0), they must agree exactly on whether a line crosses the shared edge.
504    ///
505    /// While calculating exact intersection coordinates here is feasible, it is computationally
506    /// expensive. Instead, we defer the heavy math to the GPU/rasterizer and produce a lightweight
507    /// Intersection Bitmask. This mask unambiguously defines which edges of a tile a line segment
508    /// touches or crosses.
509    ///
510    /// [ The Intersection Bitmask (6 bits) ]
511    /// The bitmask encodes winding information and edge intersections. A line is said to
512    /// "intersect" an edge if it touches that edge AND continues into the neighboring tile.
513    ///
514    /// Bit representation:
515    /// Bit: 5 | 4 | 3 | 2 | 1 | 0
516    /// Val: W | P | R | L | B | T
517    ///
518    /// - W (Winding): Tracks the direction of the line (downward vs upward).
519    /// - P (Perfect): Indicates a "Perfect" intersection (e.g., passing exactly through a corner).
520    /// - R/L/B/T: Right, Left, Bottom, and Top edge intersections.
521    ///
522    /// [ Implementation Details & Macros ]
523    /// The logic handles vertical lines and fully-contained lines as special fast paths. Sloped
524    /// lines are handled via a set of internal macros to manage complexity:
525    ///
526    /// 1. `push_edge!`: The core logic. Given start/end coordinates within a specific row, it
527    ///    determines exactly which edges (L/R/B/T) are crossed and handles corner cases
528    ///    (tie-breaking) to generate the final bitmask.
529    ///
530    /// 2. `process_row!`: Calculates the horizontal span (`x_start` to `x_end`) for a specific y-row
531    ///    using the standard linear equation (x= x0 + dY * slope). It generates the edge tiles
532    ///    using `push_edge!` and fills the interior tiles with a "pass-through" mask.
533    ///
534    /// 3. `process_middle_row_incremental!`: For the "middle" rows of a line (rows
535    ///    between the top and bottom blocks), we know the line traverses the full height of the
536    ///    tile. Instead of recalculating x from the start point (which requires multiplication), we
537    ///    use incremental addition: `x_next` = `x_curr` + slope.
538    ///
539    ///    NOTE: regarding floating point errors: The incremental stepping is only used for the
540    ///    middle tiles, the endpoints are still calculated using line equation. So the endpoints
541    ///    should still be watertight. I don't think the viewport can be large enough where enough
542    ///    floating-point error can occur that an issue arises.
543    pub fn make_tiles_msaa(&mut self, lines: &[Line], width: u16, height: u16) {
544        self.reset();
545
546        if width == 0 || height == 0 {
547            return;
548        }
549
550        debug_assert!(
551            lines.len() <= MAX_LINES_PER_PATH as usize,
552            "Max. number of lines per path exceeded. Max is {}, got {}.",
553            MAX_LINES_PER_PATH,
554            lines.len()
555        );
556
557        let tile_columns = width.div_ceil(Tile::WIDTH);
558        let tile_rows = height.div_ceil(Tile::HEIGHT);
559
560        for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() {
561            let line_idx = line_idx as u32;
562
563            let p0_x = line.p0.x / f32::from(Tile::WIDTH);
564            let p0_y = line.p0.y / f32::from(Tile::HEIGHT);
565            let p1_x = line.p1.x / f32::from(Tile::WIDTH);
566            let p1_y = line.p1.y / f32::from(Tile::HEIGHT);
567
568            let (line_left_x, line_right_x) = if p0_x < p1_x {
569                (p0_x, p1_x)
570            } else {
571                (p1_x, p0_x)
572            };
573
574            // Lines whose left-most endpoint exceed the right edge of the viewport are culled
575            if line_left_x > tile_columns as f32 {
576                continue;
577            }
578
579            let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
580                (p0_y, p0_x, p1_y, p1_x)
581            } else {
582                (p1_y, p1_x, p0_y, p0_x)
583            };
584
585            // The `as u16` casts here intentionally clamp negative coordinates to 0.
586            let y_top_tiles = (line_top_y as u16).min(tile_rows);
587            let line_bottom_y_ceil = line_bottom_y.ceil();
588            let y_bottom_tiles = (line_bottom_y_ceil as u16).min(tile_rows);
589
590            // If y_top_tiles == y_bottom_tiles, then the line is either completely above or below
591            // the viewport OR it is perfectly horizontal and aligned to the tile grid, contributing
592            // no winding. In either case, it should be culled.
593            if y_top_tiles >= y_bottom_tiles {
594                continue;
595            }
596
597            // Get tile coordinates for start/end points, use i32 to preserve negative coordinates
598            let p0_tile_x = line_top_x.floor() as i32;
599            let p0_tile_y = line_top_y.floor() as i32;
600            let p1_tile_x = line_bottom_x.floor() as i32;
601
602            let p1_tile_y = if line_bottom_y == line_bottom_y_ceil {
603                line_bottom_y as i32 - 1
604            } else {
605                line_bottom_y.floor() as i32
606            };
607
608            // Special-case out lines which are fully contained within a tile.
609            let not_same_tile = p0_tile_y != p1_tile_y || p0_tile_x != p1_tile_x;
610            if not_same_tile {
611                // For ease of logic, special-case purely vertical tiles.
612                if line_left_x == line_right_x {
613                    let x = (line_left_x as u16).min(tile_columns.saturating_sub(1));
614
615                    // Row Start, not culled.
616                    let is_start_culled = line_top_y < 0.0;
617                    if !is_start_culled {
618                        let winding = ((f32::from(y_top_tiles) >= line_top_y) as u32) << 5;
619                        let intersection_mask = 0b10 | winding;
620                        let tile = Tile::new_clamped(x, y_top_tiles, line_idx, intersection_mask);
621                        self.tile_buf.push(tile);
622                    }
623
624                    // Middle
625                    // If the start was culled, the first tile inside the viewport is a middle.
626                    let y_start = if is_start_culled {
627                        y_top_tiles
628                    } else {
629                        y_top_tiles + 1
630                    };
631                    let line_bottom_floor = line_bottom_y.floor();
632                    let y_end_idx = (line_bottom_floor as u16).min(tile_rows);
633
634                    if y_start < y_end_idx {
635                        let y_last = y_end_idx - 1;
636                        for y_idx in y_start..y_last {
637                            let intersection_mask = 0b100011; // W | B | T
638                            let tile = Tile::new_clamped(x, y_idx, line_idx, intersection_mask);
639                            self.tile_buf.push(tile);
640                        }
641
642                        // Perfect touching B case.
643                        {
644                            let is_end_tile = ((y_last as i32) == p1_tile_y) as u32;
645                            let intersection_mask = 0b100001 | ((1 ^ is_end_tile) << 1);
646                            let tile = Tile::new_clamped(x, y_last, line_idx, intersection_mask);
647                            self.tile_buf.push(tile);
648                        }
649                    }
650
651                    // Row End, handle the final tile (y_end_idx), but *only* if the line does
652                    // not perfectly end on the top edge of the tile. In the case that it does,
653                    // it gets handled by the middle logic above.
654                    if line_bottom_y != line_bottom_floor && y_end_idx < tile_rows {
655                        let intersection_mask = 0b100001; // W | T
656                        let tile = Tile::new_clamped(x, y_end_idx, line_idx, intersection_mask);
657                        self.tile_buf.push(tile);
658                    }
659                } else {
660                    let dx = p1_x - p0_x;
661                    let dy = p1_y - p0_y;
662                    let x_slope = dx / dy;
663                    let slope_is_pos = x_slope >= 0.0;
664                    let dx_dir = (line_bottom_x >= line_top_x) as u32;
665                    let not_dx_dir = dx_dir ^ 1;
666
667                    let w_start_base = dx_dir << 5;
668                    let w_end_base = not_dx_dir << 5;
669
670                    // Check if the line is fully within the horizontal viewport bounds. If it is,
671                    // we can skip the min/max clamping per row.
672                    let min_x = p0_x.min(p1_x);
673                    let max_x = p0_x.max(p1_x);
674                    // Note: We use >= on the right edge to ensure strictly safe integer truncation
675                    let needs_clamping = min_x < line_left_x || max_x >= line_right_x;
676
677                    // Handles the bitmask logic for start/end tiles. Invariant to clamping.
678                    macro_rules! push_edge {
679                        ($x:expr, $y:expr, $row_top_x:expr, $row_bottom_x:expr,
680                         $canonical_start:expr, $canonical_end:expr, $winding_input:expr,
681                         $check_s:tt, $check_e:expr) => {{
682                            let x_idx = $x;
683
684                            let unc_row_start = (x_idx as i32 == $canonical_start) as u32;
685                            let unc_row_end = (x_idx == $canonical_end) as u32;
686
687                            let canonical_row_start =
688                                (dx_dir & unc_row_start) | (not_dx_dir & unc_row_end);
689                            let canonical_row_end =
690                                (not_dx_dir & unc_row_start) | (dx_dir & unc_row_end);
691
692                            let start_tile = if $check_s {
693                                ((x_idx as i32 == p0_tile_x) && ($y as i32 == p0_tile_y)) as u32
694                            } else {
695                                0
696                            };
697
698                            let end_tile = if $check_e {
699                                ((x_idx as i32 == p1_tile_x) && ($y as i32 == p1_tile_y)) as u32
700                            } else {
701                                0
702                            };
703
704                            let mut mask = $winding_input;
705
706                            // Entrant/Exit
707                            mask |= canonical_row_start & (1 ^ start_tile);
708                            mask |= (1 ^ canonical_row_start) << not_dx_dir << 2;
709                            mask |= (canonical_row_end & (1 ^ end_tile)) << 1;
710                            mask |= (1 ^ canonical_row_end) << dx_dir << 2;
711
712                            // Corner
713                            let x_left_f = x_idx as f32;
714                            let x_right_f = (x_idx + 1) as f32;
715                            let trc = (($row_top_x == x_right_f) as u32) & (1 ^ start_tile);
716                            let tlc = (($row_top_x == x_left_f) as u32) & (1 ^ start_tile);
717                            let brc = (($row_bottom_x == x_right_f) as u32) & (1 ^ end_tile);
718                            let blc = (($row_bottom_x == x_left_f) as u32) & (1 ^ end_tile);
719                            // Top left is handled specially
720                            let tie_break = tlc & (canonical_row_start ^ 1);
721
722                            mask |= (tie_break | blc) << 2;
723                            mask |= (trc | brc) << 3;
724                            mask &= !(tie_break | trc);
725                            mask &= !((blc | brc) << 1);
726
727                            // Set the perfect bit if in a corner
728                            mask |= (trc | tlc | brc | blc) << 4;
729
730                            self.tile_buf.push(Tile::new(x_idx, $y, line_idx, mask));
731                        }};
732                    }
733
734                    // Handles row geometry and clamping logic.
735                    macro_rules! process_row {
736                        ($y_idx:expr, $row_top_y:expr, $row_bottom_y:expr, $w_mask:expr,
737                     $check_s:tt, $check_e:tt, $clamped:tt) => {{
738                            let row_top_x = p0_x + ($row_top_y - p0_y) * x_slope;
739                            let row_bottom_x = p0_x + ($row_bottom_y - p0_y) * x_slope;
740
741                            let (row_left_x, row_right_x, x_end) = if $clamped {
742                                let lx = f32::min(row_top_x, row_bottom_x).max(line_left_x);
743                                let rx = f32::max(row_top_x, row_bottom_x).min(line_right_x);
744                                let xe = (rx as u16).min(tile_columns.saturating_sub(1));
745                                (lx, rx, xe)
746                            } else {
747                                let lx = f32::min(row_top_x, row_bottom_x);
748                                let rx = f32::max(row_top_x, row_bottom_x);
749                                let xe = rx as u16; // Safe because we checked bounds earlier
750                                (lx, rx, xe)
751                            };
752
753                            let canonical_x_start = row_left_x.floor() as i32;
754                            let canonical_x_end = row_right_x as u16;
755                            let x_start = row_left_x as u16;
756
757                            if x_start <= x_end {
758                                let is_single = (x_start == x_end) as u32;
759                                let w_left = (w_start_base | (is_single << 5)) & $w_mask;
760
761                                push_edge!(
762                                    x_start,
763                                    $y_idx,
764                                    row_top_x,
765                                    row_bottom_x,
766                                    canonical_x_start,
767                                    canonical_x_end,
768                                    w_left,
769                                    $check_s,
770                                    $check_e
771                                );
772
773                                for x_idx in x_start.saturating_add(1)..x_end {
774                                    self.tile_buf.push(Tile::new(x_idx, $y_idx, line_idx, 12));
775                                }
776
777                                if x_start < x_end {
778                                    let w_right = w_end_base & $w_mask;
779                                    push_edge!(
780                                        x_end,
781                                        $y_idx,
782                                        row_top_x,
783                                        row_bottom_x,
784                                        canonical_x_start,
785                                        canonical_x_end,
786                                        w_right,
787                                        $check_s,
788                                        $check_e
789                                    );
790                                }
791                            }
792                        }};
793                    }
794
795                    // Specialized for the middle loop. Uses incremental x steps.
796                    macro_rules! process_middle_row_incremental {
797                        ($y_idx:expr, $x_curr:expr, $x_next:expr, $check_e:expr, $clamped:tt) => {{
798                            // Determine Left/Right based on slope sign.
799                            let (raw_left, raw_right) = if slope_is_pos {
800                                ($x_curr, $x_next)
801                            } else {
802                                ($x_next, $x_curr)
803                            };
804
805                            let (row_left_x, row_right_x, x_end) = if $clamped {
806                                let lx = raw_left.max(line_left_x);
807                                let rx = raw_right.min(line_right_x);
808                                let xe = (rx as u16).min(tile_columns.saturating_sub(1));
809                                (lx, rx, xe)
810                            } else {
811                                // Unclamped: Raw values are trusted
812                                (raw_left, raw_right, raw_right as u16)
813                            };
814
815                            let canonical_x_start = row_left_x.floor() as i32;
816                            let canonical_x_end = row_right_x as u16;
817                            let x_start = row_left_x as u16;
818
819                            if x_start <= x_end {
820                                let is_single = (x_start == x_end) as u32;
821                                let w_left = w_start_base | (is_single << 5);
822
823                                // Note: We pass raw x_curr/x_next as top/bottom x
824                                push_edge!(
825                                    x_start,
826                                    $y_idx,
827                                    $x_curr,
828                                    $x_next,
829                                    canonical_x_start,
830                                    canonical_x_end,
831                                    w_left,
832                                    false,
833                                    $check_e
834                                );
835
836                                for x_idx in x_start.saturating_add(1)..x_end {
837                                    self.tile_buf.push(Tile::new(x_idx, $y_idx, line_idx, 12));
838                                }
839
840                                if x_start < x_end {
841                                    push_edge!(
842                                        x_end,
843                                        $y_idx,
844                                        $x_curr,
845                                        $x_next,
846                                        canonical_x_start,
847                                        canonical_x_end,
848                                        w_end_base,
849                                        false,
850                                        $check_e
851                                    );
852                                }
853                            }
854                        }};
855                    }
856
857                    // Central macro
858                    macro_rules! run_loops {
859                        ($clamped:tt) => {{
860                            // Top Row
861                            let is_start_culled = line_top_y < 0.0;
862                            if !is_start_culled {
863                                let y = f32::from(y_top_tiles);
864                                let row_bottom_y = (y + 1.0).min(line_bottom_y);
865                                let mask = ((y >= line_top_y) as u32) << 5;
866                                process_row!(
867                                    y_top_tiles,
868                                    line_top_y,
869                                    row_bottom_y,
870                                    mask,
871                                    true,
872                                    true,
873                                    $clamped
874                                );
875                            }
876
877                            let y_start_middle = if is_start_culled {
878                                y_top_tiles
879                            } else {
880                                y_top_tiles + 1
881                            };
882                            let line_bottom_floor = line_bottom_y.floor();
883                            let y_end_middle = (line_bottom_floor as u16).min(tile_rows);
884
885                            // Middle Rows, walk incrementally
886                            if y_start_middle < y_end_middle {
887                                let start_y_f = f32::from(y_start_middle);
888                                let mut x_curr = p0_x + (start_y_f - p0_y) * x_slope;
889
890                                let y_last = y_end_middle - 1;
891                                for y_idx in y_start_middle..y_last {
892                                    let x_next = x_curr + x_slope;
893                                    process_middle_row_incremental!(
894                                        y_idx, x_curr, x_next, false, $clamped
895                                    );
896                                    x_curr = x_next;
897                                }
898
899                                // Perfect Touching B
900                                {
901                                    let x_next = x_curr + x_slope;
902                                    let check_end = (y_last as i32) == p1_tile_y;
903                                    process_middle_row_incremental!(
904                                        y_last, x_curr, x_next, check_end, $clamped
905                                    );
906                                }
907                            }
908
909                            // Bottom Row
910                            if line_bottom_y != line_bottom_floor
911                                && y_end_middle < tile_rows
912                                && (is_start_culled || y_end_middle != y_top_tiles)
913                            {
914                                let y_idx = y_end_middle;
915                                let y = f32::from(y_idx);
916                                process_row!(
917                                    y_idx,
918                                    y,
919                                    line_bottom_y,
920                                    u32::MAX,
921                                    false,
922                                    true,
923                                    $clamped
924                                );
925                            }
926                        }};
927                    }
928
929                    if needs_clamping {
930                        run_loops!(true);
931                    } else {
932                        run_loops!(false);
933                    }
934                }
935            } else {
936                // Case: Line is fully contained within a single tile.
937                let tile = Tile::new_clamped(
938                    (line_left_x as u16).min(tile_columns + 1),
939                    y_top_tiles,
940                    line_idx,
941                    ((f32::from(y_top_tiles) >= line_top_y) as u32) << 5,
942                );
943                self.tile_buf.push(tile);
944            }
945        }
946    }
947}
948
949#[cfg(test)]
950mod tests {
951    use crate::flatten::{FlattenCtx, Line, Point, fill};
952    use crate::kurbo::{Affine, BezPath};
953    use crate::tile::{Tile, Tiles};
954    use fearless_simd::Level;
955    use std::vec;
956
957    const W: u32 = 0b100000;
958    const P: u32 = 0b010000;
959    const R: u32 = 0b001000;
960    const L: u32 = 0b000100;
961    const B: u32 = 0b000010;
962    const T: u32 = 0b000001;
963
964    const VIEW_DIM: u16 = 100;
965    const F_V_DIM: f32 = VIEW_DIM as f32;
966
967    impl Tiles {
968        fn assert_tiles_match(
969            &mut self,
970            lines: &[Line],
971            width: u16,
972            height: u16,
973            expected: &[Tile],
974        ) {
975            self.make_tiles_msaa(lines, width, height);
976            assert_eq!(self.tile_buf, expected, "MSAA: Tile buffer mismatch");
977
978            self.make_tiles_analytic_aa(lines, width, height);
979            check_analytic_aa_matches(&self.tile_buf, expected);
980        }
981    }
982
983    fn check_analytic_aa_matches(actual: &[Tile], expected: &[Tile]) {
984        assert_eq!(
985            actual.len(),
986            expected.len(),
987            "Analytic AA: Tile count mismatch."
988        );
989
990        for (i, (got, want)) in actual.iter().zip(expected.iter()).enumerate() {
991            assert_eq!(got.x, want.x, "Analytic AA: Tile[{}] X mismatch", i);
992            assert_eq!(got.y, want.y, "Analytic AA: Tile[{}] Y mismatch", i);
993            assert_eq!(
994                got.line_idx(),
995                want.line_idx(),
996                "Analytic AA: Tile[{}] Line Index mismatch",
997                i
998            );
999
1000            let got_winding = got.packed_winding_line_idx & W;
1001            let want_winding = want.packed_winding_line_idx & W;
1002            assert_eq!(
1003                got_winding, want_winding,
1004                "Analytic AA: Tile[{}] Winding mismatch",
1005                i
1006            );
1007        }
1008    }
1009
1010    //==============================================================================================
1011    // Culled Lines
1012    //==============================================================================================
1013    #[test]
1014    fn cull_sloped_outside_lines() {
1015        let lines = [
1016            Line {
1017                p0: Point { x: 1.0, y: -7.0 },
1018                p1: Point { x: 3.0, y: -1.0 },
1019            },
1020            Line {
1021                p0: Point { x: 1.0, y: -11.0 },
1022                p1: Point { x: 3.0, y: -1.0 },
1023            },
1024            Line {
1025                p0: Point {
1026                    x: F_V_DIM + 1.0,
1027                    y: 50.0,
1028                },
1029                p1: Point {
1030                    x: F_V_DIM + 3.0,
1031                    y: 70.0,
1032                },
1033            },
1034            Line {
1035                p0: Point {
1036                    x: 1.0,
1037                    y: F_V_DIM + 1.0,
1038                },
1039                p1: Point {
1040                    x: 3.0,
1041                    y: F_V_DIM + 7.0,
1042                },
1043            },
1044            Line {
1045                p0: Point {
1046                    x: 1.0,
1047                    y: F_V_DIM + 1.0,
1048                },
1049                p1: Point {
1050                    x: 3.0,
1051                    y: F_V_DIM + 13.0,
1052                },
1053            },
1054        ];
1055
1056        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1057        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1058    }
1059
1060    #[test]
1061    fn sloped_line_crossing_top() {
1062        let lines = [
1063            Line {
1064                p0: Point { x: -2.0, y: -3.0 },
1065                p1: Point { x: 2.0, y: 1.0 },
1066            },
1067            Line {
1068                p0: Point { x: 6.0, y: -1.0 },
1069                p1: Point { x: 5.0, y: 2.0 },
1070            },
1071            Line {
1072                p0: Point { x: 9.0, y: -10.0 },
1073                p1: Point { x: 10.0, y: 3.0 },
1074            },
1075            Line {
1076                p0: Point { x: 2.0, y: 1.0 },
1077                p1: Point { x: -2.0, y: -3.0 },
1078            },
1079        ];
1080
1081        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1082        let expected = [
1083            Tile::new(0, 0, 0, W | T),
1084            Tile::new(1, 0, 1, W | T),
1085            Tile::new(2, 0, 2, W | T),
1086            Tile::new(0, 0, 3, W | T),
1087        ];
1088
1089        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1090    }
1091
1092    #[test]
1093    fn sloped_line_crossing_bot() {
1094        let lines = [
1095            Line {
1096                p0: Point {
1097                    x: 5.0,
1098                    y: F_V_DIM + 3.0,
1099                },
1100                p1: Point {
1101                    x: 6.0,
1102                    y: F_V_DIM - 2.0,
1103                },
1104            },
1105            Line {
1106                p0: Point {
1107                    x: 10.0,
1108                    y: F_V_DIM + 1.0,
1109                },
1110                p1: Point {
1111                    x: 9.0,
1112                    y: F_V_DIM - 1.0,
1113                },
1114            },
1115            Line {
1116                p0: Point {
1117                    x: 2.0,
1118                    y: F_V_DIM - 2.0,
1119                },
1120                p1: Point {
1121                    x: 3.0,
1122                    y: F_V_DIM + 3.0,
1123                },
1124            },
1125        ];
1126
1127        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1128        let expected = [
1129            Tile::new(1, 24, 0, B),
1130            Tile::new(2, 24, 1, B),
1131            Tile::new(0, 24, 2, B),
1132        ];
1133
1134        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1135    }
1136
1137    #[test]
1138    fn sloped_line_crossing_top_multi_tile() {
1139        let lines = [
1140            Line {
1141                p0: Point { x: 1.0, y: -5.0 },
1142                p1: Point { x: 6.0, y: 7.0 },
1143            },
1144            Line {
1145                p0: Point { x: 2.5, y: -10.0 },
1146                p1: Point { x: 3.5, y: 6.0 },
1147            },
1148        ];
1149
1150        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1151        let expected = [
1152            Tile::new(0, 0, 0, W | T | R),
1153            Tile::new(1, 0, 0, L | B),
1154            Tile::new(1, 1, 0, W | T),
1155            Tile::new(0, 0, 1, W | T | B),
1156            Tile::new(0, 1, 1, W | T),
1157        ];
1158
1159        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1160    }
1161
1162    #[test]
1163    fn sloped_line_crossing_bot_multi_tile() {
1164        let lines = [
1165            Line {
1166                p0: Point {
1167                    x: 12.0,
1168                    y: F_V_DIM + 10.0,
1169                },
1170                p1: Point { x: 2.0, y: 94.0 },
1171            },
1172            Line {
1173                p0: Point {
1174                    x: 1.5,
1175                    y: F_V_DIM + 5.0,
1176                },
1177                p1: Point { x: 3.5, y: 94.0 },
1178            },
1179        ];
1180
1181        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1182        let expected = [
1183            Tile::new(0, 23, 0, B),
1184            Tile::new(0, 24, 0, W | T | R),
1185            Tile::new(1, 24, 0, B | L),
1186            Tile::new(0, 23, 1, B),
1187            Tile::new(0, 24, 1, W | T | B),
1188        ];
1189
1190        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1191    }
1192
1193    #[test]
1194    fn sloped_line_crossing_right() {
1195        let lines = [
1196            Line {
1197                p0: Point { x: 97.0, y: 1.0 },
1198                p1: Point {
1199                    x: F_V_DIM + 1.0,
1200                    y: 2.0,
1201                },
1202            },
1203            Line {
1204                p0: Point { x: 93.0, y: 1.0 },
1205                p1: Point {
1206                    x: F_V_DIM + 5.0,
1207                    y: 2.0,
1208                },
1209            },
1210        ];
1211
1212        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1213        let expected = [
1214            Tile::new(24, 0, 0, R),
1215            Tile::new(23, 0, 1, R),
1216            Tile::new(24, 0, 1, R | L),
1217        ];
1218
1219        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1220    }
1221
1222    #[test]
1223    fn sloped_line_crossing_left() {
1224        let lines = [
1225            Line {
1226                p0: Point { x: -5.0, y: 1.0 },
1227                p1: Point { x: 1.0, y: 2.0 },
1228            },
1229            Line {
1230                p0: Point { x: -5.0, y: 1.0 },
1231                p1: Point { x: 5.0, y: 2.0 },
1232            },
1233            Line {
1234                p0: Point { x: -5.0, y: 1.0 },
1235                p1: Point { x: 13.0, y: 9.0 },
1236            },
1237        ];
1238
1239        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1240        let expected = [
1241            Tile::new(0, 0, 0, L),
1242            Tile::new(0, 0, 1, L | R),
1243            Tile::new(1, 0, 1, L),
1244            Tile::new(0, 0, 2, L | B),
1245            Tile::new(0, 1, 2, W | R | T),
1246            Tile::new(1, 1, 2, R | L),
1247            Tile::new(2, 1, 2, L | B),
1248            Tile::new(2, 2, 2, W | R | T),
1249            Tile::new(3, 2, 2, L),
1250        ];
1251
1252        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1253    }
1254
1255    #[test]
1256    fn horizontal_line_above_viewport() {
1257        let lines = [Line {
1258            p0: Point { x: 10.0, y: -5.0 },
1259            p1: Point { x: 90.0, y: -5.0 },
1260        }];
1261
1262        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1263        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1264    }
1265
1266    #[test]
1267    fn horizontal_line_below_viewport() {
1268        let lines = [Line {
1269            p0: Point {
1270                x: 10.0,
1271                y: F_V_DIM + 5.0,
1272            },
1273            p1: Point {
1274                x: 90.0,
1275                y: F_V_DIM + 5.0,
1276            },
1277        }];
1278
1279        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1280        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1281    }
1282
1283    #[test]
1284    fn horizontal_line_crossing_left_viewport() {
1285        let lines = [Line {
1286            p0: Point { x: -10.0, y: 10.0 },
1287            p1: Point { x: 10.0, y: 10.0 },
1288        }];
1289
1290        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1291        let expected = [
1292            Tile::new(0, 2, 0, L | R),
1293            Tile::new(1, 2, 0, L | R),
1294            Tile::new(2, 2, 0, L),
1295        ];
1296
1297        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1298    }
1299
1300    #[test]
1301    fn horizontal_line_crossing_right_viewport() {
1302        let lines = [Line {
1303            p0: Point {
1304                x: F_V_DIM - 5.0,
1305                y: 10.0,
1306            },
1307            p1: Point {
1308                x: F_V_DIM + 5.0,
1309                y: 10.0,
1310            },
1311        }];
1312
1313        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1314        let expected = [Tile::new(23, 2, 0, R), Tile::new(24, 2, 0, L | R)];
1315
1316        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1317    }
1318
1319    #[test]
1320    fn vertical_lines_outside_viewport() {
1321        let lines = [
1322            Line {
1323                p0: Point { x: 1.0, y: -5.0 },
1324                p1: Point { x: 1.0, y: -1.0 },
1325            },
1326            Line {
1327                p0: Point {
1328                    x: 1.0,
1329                    y: F_V_DIM + 1.0,
1330                },
1331                p1: Point {
1332                    x: 1.0,
1333                    y: F_V_DIM + 5.0,
1334                },
1335            },
1336        ];
1337
1338        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1339        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1340    }
1341
1342    #[test]
1343    fn vertical_path_on_the_right_of_viewport() {
1344        let path = BezPath::from_svg("M261,0 L78848,0 L78848,4 L261,4 Z").unwrap();
1345        let mut line_buf = vec![];
1346        fill(
1347            Level::try_detect().unwrap_or(Level::fallback()),
1348            &path,
1349            Affine::IDENTITY,
1350            &mut line_buf,
1351            &mut FlattenCtx::default(),
1352        );
1353
1354        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1355        tiles.assert_tiles_match(&line_buf, 10, 10, &[]);
1356    }
1357
1358    #[test]
1359    fn vertical_line_crossing_top_viewport() {
1360        let lines = [
1361            Line {
1362                p0: Point { x: 1.0, y: -7.0 },
1363                p1: Point { x: 1.0, y: 3.0 },
1364            },
1365            Line {
1366                p0: Point { x: 1.0, y: -7.0 },
1367                p1: Point { x: 1.0, y: 7.0 },
1368            },
1369            Line {
1370                p0: Point { x: 1.0, y: -7.0 },
1371                p1: Point { x: 1.0, y: 8.0 },
1372            },
1373        ];
1374
1375        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1376        let expected = [
1377            Tile::new(0, 0, 0, W | T),
1378            Tile::new(0, 0, 1, W | B | T),
1379            Tile::new(0, 1, 1, W | T),
1380            Tile::new(0, 0, 2, W | B | T),
1381            Tile::new(0, 1, 2, W | T),
1382        ];
1383
1384        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1385    }
1386
1387    #[test]
1388    fn vertical_line_crossing_bot_viewport() {
1389        let lines = [
1390            Line {
1391                p0: Point {
1392                    x: 1.0,
1393                    y: F_V_DIM - 1.0,
1394                },
1395                p1: Point {
1396                    x: 1.0,
1397                    y: F_V_DIM + 5.0,
1398                },
1399            },
1400            Line {
1401                p0: Point {
1402                    x: 1.0,
1403                    y: F_V_DIM - 5.0,
1404                },
1405                p1: Point {
1406                    x: 1.0,
1407                    y: F_V_DIM + 5.0,
1408                },
1409            },
1410        ];
1411
1412        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1413        let expected = [
1414            Tile::new(0, 24, 0, B),
1415            Tile::new(0, 23, 1, B),
1416            Tile::new(0, 24, 1, W | T | B),
1417        ];
1418
1419        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1420    }
1421
1422    #[test]
1423    fn clip_top_left_corner() {
1424        let lines = [Line {
1425            p0: Point { x: -1.0, y: 2.0 },
1426            p1: Point { x: 2.0, y: -1.0 },
1427        }];
1428
1429        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1430        let expected = [Tile::new(0, 0, 0, W | L | T)];
1431
1432        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1433    }
1434
1435    #[test]
1436    fn clip_bottom_right_corner() {
1437        let lines = [Line {
1438            p0: Point {
1439                x: F_V_DIM + 1.0,
1440                y: F_V_DIM - 2.0,
1441            },
1442            p1: Point {
1443                x: F_V_DIM - 2.0,
1444                y: F_V_DIM + 1.0,
1445            },
1446        }];
1447
1448        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1449        let expected = [Tile::new(24, 24, 0, R | B)];
1450
1451        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1452    }
1453
1454    //==============================================================================================
1455    // Axis-aligned lines
1456    //==============================================================================================
1457    #[test]
1458    fn horizontal_line_left_to_right_three_tile() {
1459        let lines = [Line {
1460            p0: Point { x: 1.5, y: 1.0 },
1461            p1: Point { x: 8.5, y: 1.0 },
1462        }];
1463
1464        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1465        let expected = [
1466            Tile::new(0, 0, 0, R),
1467            Tile::new(1, 0, 0, R | L),
1468            Tile::new(2, 0, 0, L),
1469        ];
1470
1471        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1472    }
1473
1474    #[test]
1475    fn horizontal_line_right_to_left_three_tile() {
1476        let lines = [Line {
1477            p0: Point { x: 8.5, y: 1.0 },
1478            p1: Point { x: 1.5, y: 1.0 },
1479        }];
1480
1481        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1482        let expected = [
1483            Tile::new(0, 0, 0, R),
1484            Tile::new(1, 0, 0, R | L),
1485            Tile::new(2, 0, 0, L),
1486        ];
1487
1488        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1489    }
1490
1491    #[test]
1492    fn horizontal_line_multi_tile() {
1493        let lines = [Line {
1494            p0: Point { x: 1.5, y: 1.0 },
1495            p1: Point { x: 12.5, y: 1.0 },
1496        }];
1497
1498        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1499        let expected = [
1500            Tile::new(0, 0, 0, R),
1501            Tile::new(1, 0, 0, R | L),
1502            Tile::new(2, 0, 0, R | L),
1503            Tile::new(3, 0, 0, L),
1504        ];
1505
1506        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1507    }
1508
1509    #[test]
1510    fn vertical_line_down_three_tile() {
1511        let lines = [Line {
1512            p0: Point { x: 1.0, y: 1.5 },
1513            p1: Point { x: 1.0, y: 8.5 },
1514        }];
1515
1516        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1517        let expected = [
1518            Tile::new(0, 0, 0, B),
1519            Tile::new(0, 1, 0, W | T | B),
1520            Tile::new(0, 2, 0, W | T),
1521        ];
1522
1523        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1524    }
1525
1526    #[test]
1527    fn vertical_line_down_multi_tile() {
1528        let lines = [Line {
1529            p0: Point { x: 1.0, y: 1.0 },
1530            p1: Point { x: 1.0, y: 13.0 },
1531        }];
1532
1533        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1534        let expected = [
1535            Tile::new(0, 0, 0, B),
1536            Tile::new(0, 1, 0, W | T | B),
1537            Tile::new(0, 2, 0, W | T | B),
1538            Tile::new(0, 3, 0, W | T),
1539        ];
1540
1541        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1542    }
1543
1544    #[test]
1545    fn vertical_line_up_three_tile() {
1546        let lines = [Line {
1547            p0: Point { x: 1.0, y: 13.0 },
1548            p1: Point { x: 1.0, y: 1.0 },
1549        }];
1550
1551        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1552        let expected = [
1553            Tile::new(0, 0, 0, B),
1554            Tile::new(0, 1, 0, W | T | B),
1555            Tile::new(0, 2, 0, W | T | B),
1556            Tile::new(0, 3, 0, W | T),
1557        ];
1558
1559        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1560    }
1561
1562    #[test]
1563    fn vertical_line_up_multi_tile() {
1564        let lines = [Line {
1565            p0: Point { x: 1.0, y: 8.5 },
1566            p1: Point { x: 1.0, y: 1.5 },
1567        }];
1568
1569        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1570        let expected = [
1571            Tile::new(0, 0, 0, B),
1572            Tile::new(0, 1, 0, W | T | B),
1573            Tile::new(0, 2, 0, W | T),
1574        ];
1575
1576        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1577    }
1578
1579    // Exclusive to the bottom edge, no P required.
1580    #[test]
1581    fn vertical_line_touching_bot() {
1582        let lines = [Line {
1583            p0: Point { x: 1.0, y: 1.0 },
1584            p1: Point { x: 1.0, y: 8.0 },
1585        }];
1586
1587        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1588        let expected = [Tile::new(0, 0, 0, B), Tile::new(0, 1, 0, W | T)];
1589
1590        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1591    }
1592
1593    #[test]
1594    fn vertical_line_touching_top() {
1595        let lines = [Line {
1596            p0: Point { x: 1.0, y: 0.0 },
1597            p1: Point { x: 1.0, y: 7.0 },
1598        }];
1599
1600        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1601        let expected = [Tile::new(0, 0, 0, W | B), Tile::new(0, 1, 0, W | T)];
1602
1603        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1604    }
1605
1606    //==============================================================================================
1607    // Sloped Lines
1608    //==============================================================================================
1609    #[test]
1610    fn top_left_to_bottom_right() {
1611        let lines = [Line {
1612            p0: Point { x: 1.0, y: 1.0 },
1613            p1: Point { x: 11.0, y: 9.0 },
1614        }];
1615
1616        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1617        let expected = [
1618            Tile::new(0, 0, 0, R),
1619            Tile::new(1, 0, 0, L | B),
1620            Tile::new(1, 1, 0, W | R | T),
1621            Tile::new(2, 1, 0, L | B),
1622            Tile::new(2, 2, 0, W | T),
1623        ];
1624
1625        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1626    }
1627
1628    #[test]
1629    fn bottom_right_to_top_left() {
1630        let lines = [Line {
1631            p0: Point { x: 11.0, y: 9.0 },
1632            p1: Point { x: 1.0, y: 1.0 },
1633        }];
1634
1635        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1636        let expected = [
1637            Tile::new(0, 0, 0, R),
1638            Tile::new(1, 0, 0, L | B),
1639            Tile::new(1, 1, 0, W | R | T),
1640            Tile::new(2, 1, 0, L | B),
1641            Tile::new(2, 2, 0, W | T),
1642        ];
1643
1644        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1645    }
1646
1647    #[test]
1648    fn bottom_left_to_top_right() {
1649        let lines = [Line {
1650            p0: Point { x: 2.0, y: 11.0 },
1651            p1: Point { x: 14.0, y: 6.0 },
1652        }];
1653
1654        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1655        let expected = [
1656            Tile::new(2, 1, 0, R | B),
1657            Tile::new(3, 1, 0, L),
1658            Tile::new(0, 2, 0, R),
1659            Tile::new(1, 2, 0, R | L),
1660            Tile::new(2, 2, 0, W | L | T),
1661        ];
1662
1663        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1664    }
1665
1666    #[test]
1667    fn top_right_to_bottom_left() {
1668        let lines = [Line {
1669            p0: Point { x: 14.0, y: 6.0 },
1670            p1: Point { x: 2.0, y: 11.0 },
1671        }];
1672
1673        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1674        let expected = [
1675            Tile::new(2, 1, 0, R | B),
1676            Tile::new(3, 1, 0, L),
1677            Tile::new(0, 2, 0, R),
1678            Tile::new(1, 2, 0, R | L),
1679            Tile::new(2, 2, 0, W | L | T),
1680        ];
1681
1682        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1683    }
1684
1685    #[test]
1686    fn two_lines_in_single_tile() {
1687        let lines = [
1688            Line {
1689                p0: Point { x: 1.0, y: 3.0 },
1690                p1: Point { x: 3.0, y: 3.0 },
1691            },
1692            Line {
1693                p0: Point { x: 3.0, y: 3.0 },
1694                p1: Point { x: 0.0, y: 1.0 },
1695            },
1696        ];
1697
1698        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1699        let expected = [Tile::new(0, 0, 0, 0), Tile::new(0, 0, 1, 0)];
1700
1701        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1702    }
1703
1704    #[test]
1705    fn diagonal_cross_corner() {
1706        let lines = [Line {
1707            p0: Point { x: 3.0, y: 5.0 },
1708            p1: Point { x: 5.0, y: 3.0 },
1709        }];
1710
1711        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1712        let expected = [
1713            Tile::new(1, 0, 0, P | L),
1714            Tile::new(0, 1, 0, P | R),
1715            Tile::new(1, 1, 0, W | P | L | T),
1716        ];
1717
1718        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1719    }
1720
1721    #[test]
1722    fn diagonal_cross_corner_two() {
1723        let lines = [Line {
1724            p0: Point { x: 7.9, y: 7.9 },
1725            p1: Point { x: 0.1, y: 0.1 },
1726        }];
1727
1728        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1729        let expected = [
1730            Tile::new(0, 0, 0, P | R),
1731            Tile::new(1, 0, 0, P | L),
1732            Tile::new(1, 1, 0, W | P | T),
1733        ];
1734
1735        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1736    }
1737
1738    #[test]
1739    fn diagonal_down_slope_tiles() {
1740        let lines = [Line {
1741            p0: Point { x: 5.0, y: 5.0 },
1742            p1: Point { x: 9.0, y: 9.0 },
1743        }];
1744
1745        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1746        let expected = [
1747            Tile::new(1, 1, 0, P | R),
1748            Tile::new(2, 1, 0, P | L),
1749            Tile::new(2, 2, 0, W | P | T),
1750        ];
1751
1752        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1753    }
1754
1755    #[test]
1756    fn diagonal_up_slope_tiles() {
1757        let lines = [Line {
1758            p0: Point { x: 5.0, y: 9.0 },
1759            p1: Point { x: 9.0, y: 5.0 },
1760        }];
1761
1762        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1763        let expected = [
1764            Tile::new(1, 1, 0, R | B),
1765            Tile::new(2, 1, 0, L),
1766            Tile::new(1, 2, 0, W | T),
1767        ];
1768
1769        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1770    }
1771
1772    #[test]
1773    fn diagonal_down_one_tile() {
1774        let lines = [Line {
1775            p0: Point { x: 0.0, y: 0.0 },
1776            p1: Point { x: 4.0, y: 4.0 },
1777        }];
1778
1779        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1780        let expected = [Tile::new(0, 0, 0, W | P | R), Tile::new(1, 0, 0, L)];
1781
1782        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1783    }
1784
1785    #[test]
1786    fn diagonal_up_one_tile() {
1787        let lines = [Line {
1788            p0: Point { x: 0.0, y: 4.0 },
1789            p1: Point { x: 4.0, y: 0.0 },
1790        }];
1791
1792        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1793        let expected = [Tile::new(0, 0, 0, P | R), Tile::new(1, 0, 0, W | L)];
1794
1795        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1796    }
1797
1798    #[test]
1799    fn diagonal_down_two_tile() {
1800        let lines = [Line {
1801            p0: Point { x: 0.0, y: 0.0 },
1802            p1: Point { x: 8.0, y: 8.0 },
1803        }];
1804
1805        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1806        let expected = [
1807            Tile::new(0, 0, 0, W | P | R),
1808            Tile::new(1, 0, 0, P | L),
1809            Tile::new(1, 1, 0, W | P | R | T),
1810            Tile::new(2, 1, 0, L),
1811        ];
1812
1813        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1814    }
1815
1816    #[test]
1817    fn diagonal_up_two_tile() {
1818        let lines = [Line {
1819            p0: Point { x: 0.0, y: 8.0 },
1820            p1: Point { x: 8.0, y: 0.0 },
1821        }];
1822
1823        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1824        let expected = [
1825            Tile::new(1, 0, 0, P | R | L),
1826            Tile::new(2, 0, 0, W | L),
1827            Tile::new(0, 1, 0, P | R),
1828            Tile::new(1, 1, 0, W | P | L | T),
1829        ];
1830
1831        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1832    }
1833
1834    #[test]
1835    fn sloped_ending_right() {
1836        let lines = [Line {
1837            p0: Point { x: 1.0, y: 1.0 },
1838            p1: Point { x: 8.0, y: 2.0 },
1839        }];
1840
1841        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1842        let expected = [
1843            Tile::new(0, 0, 0, R),
1844            Tile::new(1, 0, 0, R | L),
1845            Tile::new(2, 0, 0, L),
1846        ];
1847
1848        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1849    }
1850
1851    #[test]
1852    fn sloped_touching_top() {
1853        let lines = [Line {
1854            p0: Point { x: 0.0, y: 8.0 },
1855            p1: Point { x: 4.0, y: 0.0 },
1856        }];
1857
1858        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1859        let expected = [
1860            Tile::new(0, 0, 0, P | R | B),
1861            Tile::new(1, 0, 0, W | L),
1862            Tile::new(0, 1, 0, W | T),
1863        ];
1864
1865        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1866    }
1867
1868    #[test]
1869    fn sloped_touching_bot() {
1870        let lines = [Line {
1871            p0: Point { x: 0.0, y: 0.0 },
1872            p1: Point { x: 4.0, y: 8.0 },
1873        }];
1874
1875        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1876        let expected = [
1877            Tile::new(0, 0, 0, W | B),
1878            Tile::new(0, 1, 0, W | P | R | T),
1879            Tile::new(1, 1, 0, L),
1880        ];
1881
1882        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1883    }
1884
1885    //==============================================================================================
1886    // Same Tile Cases
1887    //==============================================================================================
1888    #[test]
1889    fn same_tile() {
1890        let lines = [Line {
1891            p0: Point { x: 1.0, y: 1.0 },
1892            p1: Point { x: 3.0, y: 3.0 },
1893        }];
1894
1895        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1896        let expected = [Tile::new(0, 0, 0, 0)];
1897
1898        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1899    }
1900
1901    #[test]
1902    fn same_tile_left() {
1903        let lines = [Line {
1904            p0: Point { x: 0.0, y: 1.0 },
1905            p1: Point { x: 3.0, y: 1.0 },
1906        }];
1907
1908        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1909        let expected = [Tile::new(0, 0, 0, 0)];
1910
1911        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1912    }
1913
1914    #[test]
1915    fn same_tile_top() {
1916        let lines = [Line {
1917            p0: Point { x: 1.0, y: 0.0 },
1918            p1: Point { x: 1.0, y: 3.0 },
1919        }];
1920
1921        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1922        let expected = [Tile::new(0, 0, 0, W)];
1923
1924        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1925    }
1926
1927    #[test]
1928    fn same_tile_right() {
1929        let lines = [Line {
1930            p0: Point { x: 1.0, y: 1.0 },
1931            p1: Point { x: 4.0, y: 1.0 },
1932        }];
1933
1934        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1935        let expected = [Tile::new(0, 0, 0, R), Tile::new(1, 0, 0, L)];
1936
1937        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1938    }
1939
1940    #[test]
1941    fn same_tile_bottom() {
1942        let lines = [
1943            Line {
1944                p0: Point { x: 1.0, y: 1.0 },
1945                p1: Point { x: 1.0, y: 4.0 },
1946            },
1947            Line {
1948                p0: Point { x: 1.0, y: 1.0 },
1949                p1: Point { x: 2.0, y: 4.0 },
1950            },
1951        ];
1952
1953        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1954        let expected = [Tile::new(0, 0, 0, 0), Tile::new(0, 0, 1, 0)];
1955
1956        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1957    }
1958
1959    #[test]
1960    fn same_tile_top_left() {
1961        let lines = [
1962            Line {
1963                p0: Point { x: 0.0, y: 1.0 },
1964                p1: Point { x: 1.0, y: 0.0 },
1965            },
1966            Line {
1967                p0: Point { x: 0.0, y: 0.0001 },
1968                p1: Point { x: 0.0001, y: 0.0 },
1969            },
1970        ];
1971
1972        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1973        let expected = [Tile::new(0, 0, 0, W), Tile::new(0, 0, 1, W)];
1974
1975        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1976    }
1977
1978    //==============================================================================================
1979    // Miscellaneous Cases
1980    //==============================================================================================
1981    #[test]
1982    // See https://github.com/LaurenzV/cpu-sparse-experiments/issues/46.
1983    fn infinite_loop() {
1984        let line = Line {
1985            p0: Point { x: 22.0, y: 552.0 },
1986            p1: Point { x: 224.0, y: 388.0 },
1987        };
1988
1989        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
1990        tiles.make_tiles_msaa(&[line], 600, 600);
1991        tiles.make_tiles_analytic_aa(&[line], 600, 600);
1992    }
1993
1994    #[test]
1995    // See https://github.com/linebender/vello/issues/1321
1996    fn overflow() {
1997        let line = Line {
1998            p0: Point {
1999                x: 59.60001,
2000                y: 40.78,
2001            },
2002            p1: Point {
2003                x: 520599.6,
2004                y: 100.18,
2005            },
2006        };
2007
2008        let mut tiles = Tiles::new(Level::try_detect().unwrap_or(Level::fallback()));
2009        tiles.make_tiles_analytic_aa(&[line], 200, 100);
2010        tiles.make_tiles_msaa(&[line], 200, 100);
2011    }
2012
2013    #[test]
2014    fn sort_test() {
2015        let mut lines = vec![];
2016        let mut tiles = Tiles::new(Level::fallback());
2017
2018        let step = 4.0;
2019        let mut y = F_V_DIM - 10.0;
2020        while y > 10.0 {
2021            lines.push(Line {
2022                p0: Point {
2023                    x: F_V_DIM - 10.0,
2024                    y,
2025                },
2026                p1: Point { x: 10.0, y },
2027            });
2028
2029            lines.push(Line {
2030                p0: Point {
2031                    x: F_V_DIM - 12.0,
2032                    y,
2033                },
2034                p1: Point { x: 12.0, y },
2035            });
2036
2037            y -= step;
2038        }
2039
2040        tiles.make_tiles_msaa(&lines, VIEW_DIM, VIEW_DIM);
2041        assert!(tiles.tile_buf.first().unwrap().y > tiles.tile_buf.last().unwrap().y);
2042        tiles.sort_tiles();
2043        check_sorted(&tiles.tile_buf);
2044
2045        tiles.make_tiles_analytic_aa(&lines, VIEW_DIM, VIEW_DIM);
2046        assert!(tiles.tile_buf.first().unwrap().y > tiles.tile_buf.last().unwrap().y);
2047        tiles.sort_tiles();
2048        check_sorted(&tiles.tile_buf);
2049    }
2050
2051    fn check_sorted(buf: &[Tile]) {
2052        for i in 0..buf.len() - 1 {
2053            let current = buf[i];
2054            let next = buf[i + 1];
2055
2056            if current.y > next.y {
2057                panic!(
2058                    "Sort Failure [Y]: Tile[{}] (y={}) > Tile[{}] (y={})",
2059                    i,
2060                    current.y,
2061                    i + 1,
2062                    next.y
2063                );
2064            }
2065
2066            if current.y == next.y {
2067                if current.x > next.x {
2068                    panic!(
2069                        "Sort Failure [X]: at Row y={}, Tile[{}] (x={}) > Tile[{}] (x={})",
2070                        current.y,
2071                        i,
2072                        current.x,
2073                        i + 1,
2074                        next.x
2075                    );
2076                }
2077
2078                if current.x == next.x
2079                    && current.packed_winding_line_idx > next.packed_winding_line_idx
2080                {
2081                    panic!(
2082                        "Sort Failure [Payload]: at {}x{}, Tile[{}] (val={}) > Tile[{}] (val={})",
2083                        current.x,
2084                        current.y,
2085                        i,
2086                        current.packed_winding_line_idx,
2087                        i + 1,
2088                        next.packed_winding_line_idx
2089                    );
2090                }
2091            }
2092        }
2093    }
2094}