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::*;
10#[cfg(not(feature = "std"))]
11use peniko::kurbo::common::FloatFuncs as _;
12
13/// T-op bit
14const T: u32 = 0b00001;
15/// B-ottom bit
16const B: u32 = 0b00010;
17/// L-eft bit
18const L: u32 = 0b00100;
19/// R-ight bit
20const R: u32 = 0b01000;
21/// W-inding bit
22const W: u32 = 0b10000;
23
24/// Shift amount corresponding to the bottom bit.
25const BOT_SHIFT: u32 = B.trailing_zeros();
26/// Shift amount corresponding to the left bit.
27const LEFT_SHIFT: u32 = L.trailing_zeros();
28/// Shift amount corresponding to the right bit.
29const RIGHT_SHIFT: u32 = R.trailing_zeros();
30/// Shift amount corresponding to the winding bit.
31const WINDING_SHIFT: u32 = W.trailing_zeros();
32
33/// Mask for all intersection and winding bits (Bits 0-4).
34const INTERSECTION_MASK: u32 = W | R | L | B | T;
35/// Shift amount corresponding to the intersection bits.
36const INT_MASK_SHIFT: u32 = INTERSECTION_MASK.count_ones();
37
38/// The max number of lines per path.
39///
40/// Trying to render a path with more lines than this may result in visual artifacts.
41pub const MAX_LINES_PER_PATH: u32 = 1 << (32 - INT_MASK_SHIFT);
42
43/// A logical grouping of arrays used for culled tile processing,
44#[derive(Debug, Clone, Default)]
45pub struct CulledWindings {
46    /// Fractional winding coverage for each individual scanline in a row.
47    pub partial: Vec<[f32; Tile::HEIGHT as usize]>,
48    // Note that this will cause issues if we have windings greater/less than i16,
49    // but this should only occur in pathological cases.
50    /// Accumulated integer winding deltas for each tile row.
51    pub coarse: Vec<i16>,
52    /// Bitmask tracking which rows contain active geometry or winding data.
53    pub active: Vec<u32>,
54    /// Flag indicating if any geometry was early-culled outside the viewport.
55    pub culled: bool,
56    height: u16,
57}
58
59impl CulledWindings {
60    /// Number of bits in a single active mask word.
61    const WORD_BITS: usize = 32;
62    /// Bit shift equivalent to dividing by `WORD_BITS` (2^5 = 32).
63    const WORD_SHIFT: usize = 5;
64    /// Bitmask equivalent to modulo `WORD_BITS` (32 - 1 = 31).
65    const WORD_MASK: usize = 31;
66
67    /// Constructor chained to `Tiles`' constructor and matching its initial viewport height.
68    pub fn new(height: u16) -> Self {
69        let (num_rows, num_bits) = Self::sizes(height);
70
71        Self {
72            partial: vec![[0.0; Tile::HEIGHT as usize]; num_rows],
73            coarse: vec![0; num_rows],
74            active: vec![0; num_bits],
75            culled: false,
76            height,
77        }
78    }
79
80    fn sizes(height: u16) -> (usize, usize) {
81        let num_rows = usize::from(height).div_ceil(Tile::HEIGHT as usize);
82        let num_bits = num_rows.div_ceil(Self::WORD_BITS);
83
84        (num_rows, num_bits)
85    }
86
87    /// Reset the winding buffers.
88    pub fn reset(&mut self, height: u16) {
89        if self.height != height {
90            let (num_rows, num_bits) = Self::sizes(height);
91            self.partial.resize(num_rows, [0.0; Tile::HEIGHT as usize]);
92            self.coarse.resize(num_rows, 0);
93            self.active.resize(num_bits, 0);
94            self.height = height;
95        }
96
97        // TODO: Maybe consider tracking touched regions and only resetting those
98        // instead of always the full array?
99        if self.culled {
100            self.partial.fill([0.0; Tile::HEIGHT as usize]);
101            self.coarse.fill(0);
102            self.active.fill(0);
103            self.culled = false;
104        }
105    }
106
107    /// Marks if a row was culled early for faster traversal in strip generation.
108    #[inline(always)]
109    pub fn mark_row_active(&mut self, row_idx: usize) {
110        self.active[row_idx >> Self::WORD_SHIFT] |= 1 << (row_idx & Self::WORD_MASK);
111    }
112
113    /// Bulk marks a range of rows as active [`start_row`, `end_row`).
114    #[inline(always)]
115    pub fn mark_row_range_active(&mut self, start_row: usize, end_row: usize) {
116        if start_row >= end_row {
117            return;
118        }
119
120        let start_word = start_row >> Self::WORD_SHIFT;
121        let end_word = (end_row - 1) >> Self::WORD_SHIFT;
122
123        if start_word == end_word {
124            // All bits fall within the same u32 word
125            let shift = start_row & Self::WORD_MASK;
126            let count = end_row - start_row;
127            let mask = if count == Self::WORD_BITS {
128                u32::MAX
129            } else {
130                ((1_u32 << count) - 1) << shift
131            };
132            self.active[start_word] |= mask;
133        } else {
134            // Bits span multiple words: handle start, full middle words, and end
135            self.active[start_word] |= u32::MAX << (start_row & Self::WORD_MASK);
136
137            self.active[(start_word + 1)..end_word].fill(u32::MAX);
138
139            let end_shift = ((end_row - 1) & Self::WORD_MASK) + 1;
140            let mask = if end_shift == Self::WORD_BITS {
141                u32::MAX
142            } else {
143                (1_u32 << end_shift) - 1
144            };
145            self.active[end_word] |= mask;
146        }
147    }
148
149    /// Calls `f` on active rows in the range [`start`, `end`).
150    #[inline(always)]
151    pub fn for_active_rows_in_range<F>(&self, start: usize, end: usize, mut f: F)
152    where
153        F: FnMut(usize),
154    {
155        if start >= end {
156            return;
157        }
158
159        let start_word = start >> Self::WORD_SHIFT;
160        let end_word = (end - 1) >> Self::WORD_SHIFT;
161
162        let mut process_word = |mut word: u32, word_idx: usize| {
163            while word != 0 {
164                let bit = word.trailing_zeros();
165                word &= !(1_u32 << bit);
166                f((word_idx << Self::WORD_SHIFT) + bit as usize);
167            }
168        };
169
170        let end_limit = ((end - 1) & Self::WORD_MASK) + 1;
171        let end_mask = if end_limit == Self::WORD_BITS {
172            u32::MAX
173        } else {
174            (1_u32 << end_limit) - 1
175        };
176
177        // Start Word
178        {
179            let mut word = self.active[start_word];
180            let start_bit = start & Self::WORD_MASK;
181            word &= !((1_u32 << start_bit) - 1);
182
183            if start_word == end_word {
184                word &= end_mask;
185            }
186
187            process_word(word, start_word);
188        }
189
190        // Middle Words & End Word
191        if start_word < end_word {
192            for (word_idx, &word_val) in self
193                .active
194                .iter()
195                .enumerate()
196                .take(end_word)
197                .skip(start_word + 1)
198            {
199                process_word(word_val, word_idx);
200            }
201
202            // End Word
203            process_word(self.active[end_word] & end_mask, end_word);
204        }
205    }
206}
207
208/// A tile represents an aligned area on the pixmap, used to subdivide the viewport into sub-areas
209/// (currently 4x4) and analyze line intersections inside each such area.
210///
211/// Keep in mind that it is possible to have multiple tiles with the same index,
212/// namely if we have multiple lines crossing the same 4x4 area!
213///
214/// # Note
215///
216/// This struct is `#[repr(C)]`, but the byte order of its fields is dependent on the endianness of
217/// the compilation target.
218#[derive(Debug, Clone, Copy)]
219#[repr(C)]
220pub struct Tile {
221    // The field ordering is important.
222    //
223    // The given ordering (variant over little and big endian compilation targets), ensures that
224    // `Tile::to_bits` doesn't do any actual work, as the ordering of the fields is such that the
225    // numeric value of a `Tile` in memory is identical as returned by that method. This allows
226    // for, e.g., comparison and sorting.
227    #[cfg(target_endian = "big")]
228    /// The index of the tile in the y direction.
229    pub y: u16,
230
231    #[cfg(target_endian = "big")]
232    /// The index of the tile in the x direction.
233    pub x: u16,
234
235    /// The index of the line this tile belongs to into the line buffer, intersection data,
236    /// and winding data packed together.
237    ///
238    /// The layout is:
239    /// - **Bits 0-4 (5 bits):** Intersection and Winding Mask (`W | R | L | B | T`).
240    ///   - Bit 0 (mask `0b00001`): Intersects top edge (T)
241    ///   - Bit 1 (mask `0b00010`): Intersects bottom edge (B)
242    ///   - Bit 2 (mask `0b00100`): Intersects left edge (L)
243    ///   - Bit 3 (mask `0b01000`): Intersects right edge (R)
244    ///   - Bit 4 (mask `0b10000`): Winding (W) - 1 if crosses top edge.
245    /// - **Bits 5-31 (27 bits):** The line index (`line_idx`).
246    ///
247    /// **Sorting Note:** The `line_idx` occupies the higher bits to ensure that when sorting
248    /// tiles with the same (x, y) coordinates, they are sorted by their line index first,
249    /// and then by their intersection mask.
250    pub packed_winding_line_idx: u32,
251
252    #[cfg(target_endian = "little")]
253    /// The index of the tile in the x direction.
254    pub x: u16,
255
256    #[cfg(target_endian = "little")]
257    /// The index of the tile in the y direction.
258    pub y: u16,
259}
260
261impl Tile {
262    /// The width of a tile in pixels.
263    pub const WIDTH: u16 = 4;
264
265    /// The height of a tile in pixels.
266    pub const HEIGHT: u16 = 4;
267
268    /// A special tile used to signal the end of a tile stream during rendering.
269    pub const SENTINEL: Self = Self::new(u16::MAX, u16::MAX, 0, 0);
270
271    /// Create a new tile.
272    /// `x` and `y` will be clamped to the largest possible coordinate if they are too large.
273    ///
274    /// `line_idx` must be smaller than [`MAX_LINES_PER_PATH`].
275    #[inline]
276    pub fn new_clamped(x: u16, y: u16, line_idx: u32, intersection_mask: u32) -> Self {
277        Self::new(
278            // Make sure that x and y stay in range when multiplying
279            // with the tile width and height during strips generation.
280            x.min(u16::MAX / Self::WIDTH),
281            y.min(u16::MAX / Self::HEIGHT),
282            line_idx,
283            intersection_mask,
284        )
285    }
286
287    /// The base tile constructor
288    ///
289    /// Unlike [`Self::new_clamped`], this constructor stores `x` and `y` exactly as provided.
290    /// Callers must ensure these coordinates do not exceed the limits required by downstream
291    /// processing (typically `u16::MAX / WIDTH` and `u16::MAX / HEIGHT`).
292    #[inline]
293    pub const fn new(x: u16, y: u16, line_idx: u32, intersection_mask: u32) -> Self {
294        #[cfg(debug_assertions)]
295        if line_idx >= MAX_LINES_PER_PATH {
296            panic!("Max. number of lines per path exceeded.");
297        }
298        // The intersection_mask is expected to contain bits 0-4 (T, B, L, R, W).
299        // We pack line_idx into the high bits (5-31) and intersection_mask into low bits (0-4).
300        Self {
301            x,
302            y,
303            packed_winding_line_idx: (line_idx << INT_MASK_SHIFT) | intersection_mask,
304        }
305    }
306
307    /// Check whether two tiles are at the same location.
308    #[inline]
309    pub const fn same_loc(&self, other: &Self) -> bool {
310        self.same_row(other) && self.x == other.x
311    }
312
313    /// Check whether `self` is adjacent to the left of `other`.
314    #[inline]
315    pub const fn prev_loc(&self, other: &Self) -> bool {
316        self.same_row(other) && self.x + 1 == other.x
317    }
318
319    /// Check whether two tiles are on the same row.
320    #[inline]
321    pub const fn same_row(&self, other: &Self) -> bool {
322        self.y == other.y
323    }
324
325    /// The index of the line this tile belongs to into the line buffer.
326    ///
327    /// Returns the high 27 bits.
328    #[inline]
329    pub const fn line_idx(&self) -> u32 {
330        self.packed_winding_line_idx >> INT_MASK_SHIFT
331    }
332
333    /// Whether the line crosses the top edge of the tile.
334    ///
335    /// Lines making this crossing increment or decrement the coarse tile winding, depending on the
336    /// line direction.
337    ///
338    /// Checks Bit 4 (Winding).
339    #[inline]
340    pub const fn winding(&self) -> bool {
341        (self.packed_winding_line_idx & W) != 0
342    }
343
344    /// The 5 bits of intersection and winding data.
345    #[inline]
346    pub const fn intersection_mask(&self) -> u32 {
347        self.packed_winding_line_idx & INTERSECTION_MASK
348    }
349
350    /// Whether the line intersects the top edge of the tile.
351    #[inline]
352    pub const fn intersects_top(&self) -> bool {
353        (self.intersection_mask() & T) != 0
354    }
355
356    /// Whether the line intersects the bottom edge of the tile.
357    #[inline]
358    pub const fn intersects_bottom(&self) -> bool {
359        (self.intersection_mask() & B) != 0
360    }
361
362    /// Whether the line intersects the left edge of the tile.
363    #[inline]
364    pub const fn intersects_left(&self) -> bool {
365        (self.intersection_mask() & L) != 0
366    }
367
368    /// Whether the line intersects the right edge of the tile.
369    #[inline]
370    pub const fn intersects_right(&self) -> bool {
371        (self.intersection_mask() & R) != 0
372    }
373
374    /// Return the `u64` representation of this tile.
375    ///
376    /// This is the u64 interpretation of `(y, x, packed_winding_line_idx)` where `y` is the
377    /// most-significant part of the number and `packed_winding_line_idx` the least significant.
378    #[inline(always)]
379    const fn to_bits(self) -> u64 {
380        // Note that for correct rendering, tiles only need to be sorted on `(y, x)`. Sorting on
381        // the line index in addition to the coordinate improves data locality in strip rendering.
382        // This is trading off increased sorting time for decreased strip rendering time. How the
383        // trade-off falls is scene-dependent.
384        //
385        // This operation compiles to a no-op: `Tile`'s field order is such that this is exactly
386        // the in-memory representation.
387        ((self.y as u64) << 48) | ((self.x as u64) << 32) | self.packed_winding_line_idx as u64
388    }
389
390    /// Whether a tile is a sentinel tile
391    //
392    // A tile produced organically by a make_tiles call can never have this coordinate because of
393    // the division by tile size on creation, so checking on x is sufficient to identify it.
394    #[inline(always)]
395    pub const fn is_sentinel(&self) -> bool {
396        self.x == u16::MAX
397    }
398}
399
400impl PartialEq for Tile {
401    #[inline(always)]
402    fn eq(&self, other: &Self) -> bool {
403        self.to_bits() == other.to_bits()
404    }
405}
406
407impl Ord for Tile {
408    #[inline(always)]
409    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
410        self.to_bits().cmp(&other.to_bits())
411    }
412}
413
414impl PartialOrd for Tile {
415    #[inline(always)]
416    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
417        Some(self.cmp(other))
418    }
419}
420
421impl Eq for Tile {}
422
423/// Handles the tiling of paths.
424#[derive(Clone, Debug)]
425pub struct Tiles {
426    tile_buf: Vec<Tile>,
427    level: Level,
428    sorted: bool,
429    width: u16,
430    height: u16,
431    /// Auxiliary data tracking row windings and active rows for early culling.
432    pub windings: CulledWindings,
433}
434
435impl Tiles {
436    /// Create a new tiles container.
437    pub fn new(level: Level, width: u16, height: u16) -> Self {
438        Self {
439            tile_buf: vec![],
440            level,
441            sorted: false,
442            width,
443            height,
444            windings: CulledWindings::new(height),
445        }
446    }
447
448    /// Get the number of tiles in the container.
449    pub fn len(&self) -> u32 {
450        self.tile_buf.len() as u32
451    }
452
453    /// Returns `true` if the container has no tiles.
454    pub fn is_empty(&self) -> bool {
455        self.tile_buf.is_empty()
456    }
457
458    /// Get the viewport width used for this tile buffer.
459    pub(crate) fn width(&self) -> u16 {
460        self.width
461    }
462
463    /// Returns `true` if any geometry was early-culled outside the viewport.
464    pub fn has_culled_tiles(&self) -> bool {
465        self.windings.culled
466    }
467
468    /// Reset the tiles' container and resize to the given dimensions.
469    pub fn reset(&mut self, width: u16, height: u16) {
470        self.windings.reset(height);
471        self.width = width;
472        self.height = height;
473        self.tile_buf.clear();
474        self.sorted = false;
475    }
476
477    /// Sort the tiles in the container.
478    pub fn sort_tiles(&mut self) {
479        self.sorted = true;
480        // To enable auto-vectorization.
481        self.level.dispatch(|_| self.tile_buf.sort_unstable());
482    }
483
484    /// Get the tile at a certain index.
485    ///
486    /// Panics if the container hasn't been sorted before.
487    #[inline]
488    pub fn get(&self, index: u32) -> &Tile {
489        assert!(
490            self.sorted,
491            "attempted to call `get` before sorting the tile container."
492        );
493
494        &self.tile_buf[index as usize]
495    }
496
497    /// Iterate over the tiles in sorted order.
498    ///
499    /// Panics if the container hasn't been sorted before.
500    #[inline]
501    pub fn iter(&self) -> impl Iterator<Item = &Tile> {
502        assert!(
503            self.sorted,
504            "attempted to call `iter` before sorting the tile container."
505        );
506
507        self.tile_buf.iter()
508    }
509
510    /// Generates tile commands for Analytic Anti-Aliasing rasterization. Unlike the MSAA path, this
511    /// function performs "coarse binning" to simply identify every tile a line segment traverses.
512    /// It encodes the line index and winding direction, delegating the precise calculation of pixel
513    /// coverage to `strip::render`.
514    pub fn make_tiles_analytic_aa(
515        &mut self,
516        level: Level,
517        lines: &[Line],
518        width: u16,
519        height: u16,
520    ) -> bool {
521        dispatch!(level, simd => self.make_tiles_analytic_aa_impl::<_>(
522            simd,
523            lines,
524            width,
525            height,
526        ))
527    }
528
529    #[inline(always)]
530    fn make_tiles_analytic_aa_impl<S: Simd>(
531        &mut self,
532        s: S,
533        lines: &[Line],
534        width: u16,
535        height: u16,
536    ) -> bool {
537        self.reset(width, height);
538
539        if width == 0 || height == 0 {
540            return self.windings.culled;
541        }
542
543        debug_assert!(
544            lines.len() <= MAX_LINES_PER_PATH as usize,
545            "Max. number of lines per path exceeded. Max is {}, got {}.",
546            MAX_LINES_PER_PATH,
547            lines.len()
548        );
549
550        let tile_columns = width.div_ceil(Tile::WIDTH);
551        let tile_rows = height.div_ceil(Tile::HEIGHT);
552
553        let px_top = f32x4::from_slice(s, &[0.0, 1.0, 2.0, 3.0]);
554        let px_bottom = px_top + f32x4::splat(s, 1.0);
555        let simd_zero = f32x4::splat(s, 0.0);
556        let tile_height_f32 = Tile::HEIGHT as f32;
557
558        for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() {
559            let line_idx = line_idx as u32;
560
561            let p0_x = line.p0.x / f32::from(Tile::WIDTH);
562            let p0_y = line.p0.y / f32::from(Tile::HEIGHT);
563            let p1_x = line.p1.x / f32::from(Tile::WIDTH);
564            let p1_y = line.p1.y / f32::from(Tile::HEIGHT);
565
566            let (line_left_x, line_right_x) = if p0_x < p1_x {
567                (p0_x, p1_x)
568            } else {
569                (p1_x, p0_x)
570            };
571
572            // Lines whose left-most endpoint exceed the right edge of the viewport are culled
573            if line_left_x > tile_columns as f32 {
574                continue;
575            }
576
577            let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
578                (p0_y, p0_x, p1_y, p1_x)
579            } else {
580                (p1_y, p1_x, p0_y, p0_x)
581            };
582
583            // The `as u16` casts here intentionally clamp negative coordinates to 0.
584            let y_top_tiles = (line_top_y as u16).min(tile_rows);
585            let line_bottom_y_ceil = line_bottom_y.ceil();
586            let y_bottom_tiles = (line_bottom_y_ceil as u16).min(tile_rows);
587
588            // If y_top_tiles == y_bottom_tiles, then the line is either completely above or below
589            // the viewport OR it is perfectly horizontal and aligned to the tile grid, contributing
590            // no winding. In either case, it should be culled.
591            if y_top_tiles >= y_bottom_tiles {
592                // Technically, the `>` part of the `>=` is unnecessary due to clamping, but this
593                // gives stronger signal
594                continue;
595            }
596
597            let dir = if p0_y >= p1_y { 1 } else { -1 };
598            let f_dir = dir as f32;
599            let f_dir_v = f32x4::splat(s, f_dir);
600
601            macro_rules! calc_fractional_coverage {
602                ($y_idx:expr, $segment_top_y:expr, $segment_bottom_y:expr) => {{
603                    let y_idx_f32 = f32::from($y_idx);
604                    let local_y_start = ($segment_top_y - y_idx_f32) * tile_height_f32;
605                    let local_y_end = ($segment_bottom_y - y_idx_f32) * tile_height_f32;
606
607                    let start_v = f32x4::splat(s, local_y_start);
608                    let end_v = f32x4::splat(s, local_y_end);
609
610                    (px_bottom.min(end_v) - px_top.max(start_v)).max(simd_zero)
611                }};
612            }
613
614            // Lines fully to the left of the viewport are not visible but still produce winding
615            // which we record here and forward to the rendering stage.
616            if line_right_x < 0.0 {
617                let is_start_culled = line_top_y < 0.0;
618
619                // This branch is for handling the "start" of the line. In case
620                // the line reaches above the viewport, we are already in the
621                // middle so we can skip that part.
622                if !is_start_culled {
623                    self.windings.mark_row_active(y_top_tiles as usize);
624
625                    // Note: In theory, == should be enough, but just as
626                    // additional safety against numerical precision errors we
627                    // use <=.
628                    let at_top_of_tile = line_top_y <= f32::from(y_top_tiles);
629                    if at_top_of_tile {
630                        self.windings.coarse[y_top_tiles as usize] += dir;
631                    }
632
633                    let fractional_coverage =
634                        calc_fractional_coverage!(y_top_tiles, line_top_y, line_bottom_y);
635                    let target_row = &mut self.windings.partial[y_top_tiles as usize];
636                    let current = f32x4::from_slice(s, target_row);
637
638                    // See comment below on the double counting risk!
639                    let double_count = if at_top_of_tile {
640                        f_dir_v
641                    } else {
642                        f32x4::splat(s, 0.0)
643                    };
644                    let next = fractional_coverage.mul_add(f_dir_v, current - double_count);
645                    next.store_slice(target_row);
646                }
647
648                let y_start_middle = if is_start_culled {
649                    y_top_tiles
650                } else {
651                    y_top_tiles + 1
652                };
653                let line_bottom_floor = line_bottom_y.floor();
654                let y_end_middle = (line_bottom_floor as u16).min(tile_rows);
655
656                for y_idx in y_start_middle..y_end_middle {
657                    self.windings.coarse[y_idx as usize] += dir;
658                }
659                self.windings
660                    .mark_row_range_active(y_start_middle as usize, y_end_middle as usize);
661
662                if line_bottom_y != line_bottom_floor
663                    && y_end_middle < tile_rows
664                    // Prevent double-processing, unless the start was off-screen and hasn't been
665                    // handled yet.
666                    && (is_start_culled || y_end_middle != y_top_tiles)
667                {
668                    self.windings.mark_row_active(y_end_middle as usize);
669                    // Ends implicitly cross the top.
670                    self.windings.coarse[y_end_middle as usize] += dir;
671                    let fractional_coverage =
672                        calc_fractional_coverage!(y_end_middle, line_top_y, line_bottom_y);
673                    let target_row = &mut self.windings.partial[y_end_middle as usize];
674                    let current = f32x4::from_slice(s, target_row);
675                    // Subtract the inverse direction to avoid double counting with the coarse winding.
676                    let next = fractional_coverage.mul_add(f_dir_v, current - f_dir_v);
677                    next.store_slice(target_row);
678                }
679
680                self.windings.culled = true;
681                continue;
682            }
683
684            // Get tile coordinates for start/end points, use i32 to preserve negative coordinates.
685            let p0_tile_x = line_top_x.floor() as i32;
686            let p0_tile_y = line_top_y.floor() as i32;
687            let p1_tile_x = line_bottom_x.floor() as i32;
688            let p1_tile_y = line_bottom_y.floor() as i32;
689
690            // Special-case out lines which are fully contained within a tile.
691            let not_same_tile = p0_tile_y != p1_tile_y || p0_tile_x != p1_tile_x;
692            if not_same_tile {
693                // Case vertical lines: By definition, these cannot be horizontally crossing, and
694                // thus require no additional left-edge culling handling.
695                if line_left_x == line_right_x {
696                    let x = (line_left_x as u16).min(tile_columns.saturating_sub(1));
697
698                    // Row Start, not culled.
699                    let is_start_culled = line_top_y < 0.0;
700                    if !is_start_culled {
701                        let winding =
702                            ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT;
703                        let tile = Tile::new_clamped(x, y_top_tiles, line_idx, winding);
704                        self.tile_buf.push(tile);
705                    }
706
707                    // Middle
708                    // If the start was culled, the first tile inside the viewport is a middle.
709                    let y_start = if is_start_culled {
710                        y_top_tiles
711                    } else {
712                        y_top_tiles + 1
713                    };
714
715                    for y_idx in y_start..y_bottom_tiles {
716                        let tile = Tile::new_clamped(x, y_idx, line_idx, W);
717                        self.tile_buf.push(tile);
718                    }
719                } else {
720                    // General case, any line which crosses more than one tile and is not vertical.
721                    let dx = p1_x - p0_x;
722                    let dy = p1_y - p0_y;
723                    let x_slope = dx / dy;
724                    let dx_dir = (line_bottom_x >= line_top_x) as u32;
725                    let not_dx_dir = dx_dir ^ 1;
726
727                    let w_start_base = dx_dir << WINDING_SHIFT;
728                    let w_end_base = not_dx_dir << WINDING_SHIFT;
729
730                    let push_row_extents = {
731                        #[inline(always)]
732                        |tile_buf: &mut Vec<Tile>,
733                         y_idx: u16,
734                         row_left_x: f32,
735                         row_right_x: f32,
736                         w_start: u32,
737                         w_end: u32,
738                         w_single: u32| {
739                            let x_start = row_left_x as u16;
740                            let x_end = (row_right_x as u16).min(tile_columns - 1);
741
742                            if x_start <= x_end {
743                                let winding = if x_start == x_end { w_single } else { w_start };
744
745                                tile_buf.push(Tile::new(x_start, y_idx, line_idx, winding));
746
747                                for x_idx in x_start.saturating_add(1)..x_end {
748                                    tile_buf.push(Tile::new(x_idx, y_idx, line_idx, 0));
749                                }
750
751                                if x_start < x_end {
752                                    tile_buf.push(Tile::new(x_end, y_idx, line_idx, w_end));
753                                }
754                            }
755                        }
756                    };
757
758                    let mut push_row = {
759                        #[inline(always)]
760                        |y_idx: u16,
761                         row_top_y: f32,
762                         row_bottom_y: f32,
763                         w_start: u32,
764                         w_end: u32,
765                         w_single: u32| {
766                            let row_top_x = p0_x + (row_top_y - p0_y) * x_slope;
767                            let row_bottom_x = p0_x + (row_bottom_y - p0_y) * x_slope;
768
769                            // TODO: Evaluate whether we need the second max/min.
770                            let row_left_x = f32::min(row_top_x, row_bottom_x).max(line_left_x);
771                            let row_right_x = f32::max(row_top_x, row_bottom_x).min(line_right_x);
772
773                            if row_left_x < 0.0 {
774                                self.windings.culled = true;
775
776                                if row_right_x < 0.0 {
777                                    // Although the line may cross the left edge, the rightmost point in
778                                    // this row may still be fully left of the viewport. In this case,
779                                    // record the winding and emit no tiles.
780                                    self.windings.mark_row_active(y_idx as usize);
781
782                                    let crosses_top = (w_single & W) != 0;
783                                    if crosses_top {
784                                        self.windings.coarse[y_idx as usize] += dir;
785                                    }
786
787                                    let fractional_coverage =
788                                        calc_fractional_coverage!(y_idx, row_top_y, row_bottom_y);
789                                    let target_row = &mut self.windings.partial[y_idx as usize];
790                                    let current = f32x4::from_slice(s, target_row);
791
792                                    let double_count = if crosses_top {
793                                        f_dir_v
794                                    } else {
795                                        f32x4::splat(s, 0.0)
796                                    };
797                                    let next = fractional_coverage
798                                        .mul_add(f_dir_v, current - double_count);
799                                    next.store_slice(target_row);
800
801                                    return;
802                                } else {
803                                    // The line crosses into the viewport in this row. Record only the
804                                    // fractional portion of the winding, as the coarse winding will
805                                    // naturally get included by the clamped tile logic!
806                                    let y_slope = dy / dx;
807                                    let y_intersect = row_top_y - (row_top_x * y_slope);
808
809                                    let (off_screen_top_y, off_screen_bottom_y) = if row_top_x < 0.0
810                                    {
811                                        (row_top_y, f32::min(row_bottom_y, y_intersect))
812                                    } else {
813                                        (f32::max(row_top_y, y_intersect), row_bottom_y)
814                                    };
815
816                                    if off_screen_top_y < off_screen_bottom_y {
817                                        self.windings.mark_row_active(y_idx as usize);
818                                        let fractional_coverage = calc_fractional_coverage!(
819                                            y_idx,
820                                            off_screen_top_y,
821                                            off_screen_bottom_y
822                                        );
823                                        let target_row = &mut self.windings.partial[y_idx as usize];
824                                        let current = f32x4::from_slice(s, target_row);
825                                        let next = fractional_coverage.mul_add(f_dir_v, current);
826                                        next.store_slice(target_row);
827                                    }
828                                }
829                            }
830
831                            push_row_extents(
832                                &mut self.tile_buf,
833                                y_idx,
834                                row_left_x,
835                                row_right_x,
836                                w_start,
837                                w_end,
838                                w_single,
839                            );
840                        }
841                    };
842
843                    let is_start_culled = line_top_y < 0.0;
844                    // This branch is taken in case the line is completely inside
845                    // the viewport, allowing us to save many calculations that
846                    // otherwise would need to be made viewport culling work.
847                    if line_left_x >= 0.0 && line_right_x < tile_columns as f32 {
848                        if !is_start_culled {
849                            let y = f32::from(y_top_tiles);
850                            let row_bottom_y = (y + 1.0).min(line_bottom_y);
851                            let row_bottom_x = if row_bottom_y == line_bottom_y {
852                                line_bottom_x
853                            } else {
854                                p0_x + (row_bottom_y - p0_y) * x_slope
855                            };
856                            let mask = ((y >= line_top_y) as u32) << WINDING_SHIFT;
857                            push_row_extents(
858                                &mut self.tile_buf,
859                                y_top_tiles,
860                                f32::min(line_top_x, row_bottom_x),
861                                f32::max(line_top_x, row_bottom_x),
862                                w_start_base & mask,
863                                w_end_base & mask,
864                                W & mask,
865                            );
866                        }
867
868                        let y_start = if is_start_culled {
869                            y_top_tiles
870                        } else {
871                            y_top_tiles + 1
872                        };
873
874                        if y_start < y_bottom_tiles {
875                            let mut row_top_x = p0_x + (f32::from(y_start) - p0_y) * x_slope;
876                            for y_idx in y_start..y_bottom_tiles {
877                                let y = f32::from(y_idx);
878                                // Note: We purposefully don't precompute it once
879                                // and just increment by `x_slope` after every iteration
880                                // to avoid errors due to floating point inaccuracies.
881                                let row_bottom_x = if line_bottom_y < y + 1.0 {
882                                    line_bottom_x
883                                } else {
884                                    p0_x + (y + 1.0 - p0_y) * x_slope
885                                };
886                                push_row_extents(
887                                    &mut self.tile_buf,
888                                    y_idx,
889                                    f32::min(row_top_x, row_bottom_x),
890                                    f32::max(row_top_x, row_bottom_x),
891                                    w_start_base,
892                                    w_end_base,
893                                    W,
894                                );
895                                row_top_x = row_bottom_x;
896                            }
897                        }
898                    } else {
899                        if !is_start_culled {
900                            let y = f32::from(y_top_tiles);
901                            let row_bottom_y = (y + 1.0).min(line_bottom_y);
902                            let mask = ((y >= line_top_y) as u32) << WINDING_SHIFT;
903                            push_row(
904                                y_top_tiles,
905                                line_top_y,
906                                row_bottom_y,
907                                w_start_base & mask,
908                                w_end_base & mask,
909                                W & mask,
910                            );
911                        }
912
913                        let y_start = if is_start_culled {
914                            y_top_tiles
915                        } else {
916                            y_top_tiles + 1
917                        };
918
919                        for y_idx in y_start..y_bottom_tiles {
920                            let y = f32::from(y_idx);
921                            let row_bottom_y = (y + 1.0).min(line_bottom_y);
922                            push_row(y_idx, y, row_bottom_y, w_start_base, w_end_base, W);
923                        }
924                    }
925                }
926            } else {
927                // Case line is fully contained within a single tile: These also cannot cross edges!
928                let tile = Tile::new_clamped(
929                    (line_left_x as u16).min(tile_columns + 1),
930                    y_top_tiles,
931                    line_idx,
932                    ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT,
933                );
934                self.tile_buf.push(tile);
935            }
936        }
937
938        self.windings.culled
939    }
940
941    /// Generates tile commands for MSAA (Multisample Anti-Aliasing) rasterization.
942    ///
943    /// [ Architecture & Watertightness ]
944    /// The primary goal of this function is to establish a source of "ground truth" for line-tile
945    /// intersections. Because the downstream rasterization (MSAA) occurs in parallel, it is
946    /// critical that intersections are "watertight." If Thread A handles Tile (0,0) and Thread B
947    /// handles Tile (1,0), they must agree exactly on whether a line crosses the shared edge.
948    ///
949    /// While calculating exact intersection coordinates here is feasible, it is computationally
950    /// expensive. Instead, we defer the heavy math to the GPU/rasterizer and produce a lightweight
951    /// Intersection Bitmask. This mask unambiguously defines which edges of a tile a line segment
952    /// touches or crosses.
953    ///
954    /// [ The Intersection Bitmask (5 bits) ]
955    /// The bitmask encodes winding information and edge intersections. A line is said to
956    /// "intersect" an edge if it touches that edge AND continues into the neighboring tile.
957    ///
958    /// Bit representation:
959    /// Bit: 4 | 3 | 2 | 1 | 0
960    /// Val: W | R | L | B | T
961    ///
962    /// - W (Winding): Tracks whether the line touched the top edge of the tile.
963    /// - R/L/B/T: Right, Left, Bottom, and Top edge intersections.
964    pub fn make_tiles_msaa(&mut self, lines: &[Line], width: u16, height: u16) {
965        self.reset(width, height);
966
967        if width == 0 || height == 0 {
968            return;
969        }
970
971        debug_assert!(
972            lines.len() <= MAX_LINES_PER_PATH as usize,
973            "Max. number of lines per path exceeded. Max is {}, got {}.",
974            MAX_LINES_PER_PATH,
975            lines.len()
976        );
977
978        let tile_columns = width.div_ceil(Tile::WIDTH);
979        let tile_rows = height.div_ceil(Tile::HEIGHT);
980
981        for (line_idx, line) in lines.iter().take(MAX_LINES_PER_PATH as usize).enumerate() {
982            let line_idx = line_idx as u32;
983
984            let p0_x = line.p0.x / f32::from(Tile::WIDTH);
985            let p0_y = line.p0.y / f32::from(Tile::HEIGHT);
986            let p1_x = line.p1.x / f32::from(Tile::WIDTH);
987            let p1_y = line.p1.y / f32::from(Tile::HEIGHT);
988
989            let (line_left_x, line_right_x) = if p0_x < p1_x {
990                (p0_x, p1_x)
991            } else {
992                (p1_x, p0_x)
993            };
994
995            // Lines whose left-most endpoint exceed the right edge of the viewport are culled
996            if line_left_x > tile_columns as f32 {
997                continue;
998            }
999
1000            let (line_top_y, line_top_x, line_bottom_y, line_bottom_x) = if p0_y < p1_y {
1001                (p0_y, p0_x, p1_y, p1_x)
1002            } else {
1003                (p1_y, p1_x, p0_y, p0_x)
1004            };
1005
1006            // The `as u16` casts here intentionally clamp negative coordinates to 0.
1007            let y_top_tiles = (line_top_y as u16).min(tile_rows);
1008            let line_bottom_y_ceil = line_bottom_y.ceil();
1009            let y_bottom_tiles = (line_bottom_y_ceil as u16).min(tile_rows);
1010
1011            // If y_top_tiles == y_bottom_tiles, then the line is either completely above or below
1012            // the viewport OR it is perfectly horizontal and aligned to the tile grid, contributing
1013            // no winding. In either case, it should be culled.
1014            if y_top_tiles >= y_bottom_tiles {
1015                continue;
1016            }
1017
1018            // Get tile coordinates for start/end points, use i32 to preserve negative coordinates
1019            let p0_tile_x = line_top_x.floor() as i32;
1020            let p0_tile_y = line_top_y.floor() as i32;
1021            let p1_tile_x = line_bottom_x.floor() as i32;
1022
1023            let p1_tile_y = if line_bottom_y == line_bottom_y_ceil {
1024                line_bottom_y as i32 - 1
1025            } else {
1026                line_bottom_y.floor() as i32
1027            };
1028
1029            // Special-case out lines which are fully contained within a tile.
1030            let not_same_tile = p0_tile_y != p1_tile_y || p0_tile_x != p1_tile_x;
1031            if not_same_tile {
1032                // For ease of logic, special-case purely vertical tiles.
1033                if line_left_x == line_right_x {
1034                    let x = (line_left_x as u16).min(tile_columns.saturating_sub(1));
1035
1036                    // Row Start, not culled.
1037                    let is_start_culled = line_top_y < 0.0;
1038                    if !is_start_culled {
1039                        let winding =
1040                            ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT;
1041                        let intersection_mask = B | winding;
1042                        let tile = Tile::new_clamped(x, y_top_tiles, line_idx, intersection_mask);
1043                        self.tile_buf.push(tile);
1044                    }
1045
1046                    // Middle
1047                    // If the start was culled, the first tile inside the viewport is a middle.
1048                    let y_start = if is_start_culled {
1049                        y_top_tiles
1050                    } else {
1051                        y_top_tiles + 1
1052                    };
1053                    let line_bottom_floor = line_bottom_y.floor();
1054                    let y_end_idx = (line_bottom_floor as u16).min(tile_rows);
1055
1056                    if y_start < y_end_idx {
1057                        let y_last = y_end_idx - 1;
1058                        for y_idx in y_start..y_last {
1059                            let intersection_mask = W | B | T;
1060                            let tile = Tile::new_clamped(x, y_idx, line_idx, intersection_mask);
1061                            self.tile_buf.push(tile);
1062                        }
1063
1064                        // Perfect touching B case.
1065                        {
1066                            let is_end_tile = ((y_last as i32) == p1_tile_y) as u32;
1067                            let intersection_mask = W | T | ((1 ^ is_end_tile) << BOT_SHIFT);
1068                            let tile = Tile::new_clamped(x, y_last, line_idx, intersection_mask);
1069                            self.tile_buf.push(tile);
1070                        }
1071                    }
1072
1073                    // Row End, handle the final tile (y_end_idx), but *only* if the line does
1074                    // not perfectly end on the top edge of the tile. In the case that it does,
1075                    // it gets handled by the middle logic above.
1076                    if line_bottom_y != line_bottom_floor && y_end_idx < tile_rows {
1077                        let intersection_mask = W | T;
1078                        let tile = Tile::new_clamped(x, y_end_idx, line_idx, intersection_mask);
1079                        self.tile_buf.push(tile);
1080                    }
1081                } else {
1082                    let dx = p1_x - p0_x;
1083                    let dy = p1_y - p0_y;
1084                    let x_slope = dx / dy;
1085                    let dx_dir = (line_bottom_x >= line_top_x) as u32;
1086                    let not_dx_dir = dx_dir ^ 1;
1087
1088                    let w_start_base = dx_dir << WINDING_SHIFT;
1089                    let w_end_base = not_dx_dir << WINDING_SHIFT;
1090
1091                    // Check if the line is fully within the horizontal viewport bounds. If it is,
1092                    // we can skip the min/max clamping per row.
1093                    let min_x = p0_x.min(p1_x);
1094                    let max_x = p0_x.max(p1_x);
1095                    // Note: We use >= on the right edge to ensure strictly safe integer truncation
1096                    let needs_clamping = min_x < line_left_x || max_x >= line_right_x;
1097
1098                    // Handles the bitmask logic for start/end tiles. Invariant to clamping.
1099                    macro_rules! push_edge {
1100                        ($x:expr, $y:expr, $row_top_x:expr, $row_bottom_x:expr,
1101                         $canonical_start:expr, $canonical_end:expr, $winding_input:expr,
1102                         $check_s:tt, $check_e:expr) => {{
1103                            let x_idx = $x;
1104
1105                            let unc_row_start = (x_idx as i32 == $canonical_start) as u32;
1106                            let unc_row_end = (x_idx == $canonical_end) as u32;
1107
1108                            let canonical_row_start =
1109                                (dx_dir & unc_row_start) | (not_dx_dir & unc_row_end);
1110                            let canonical_row_end =
1111                                (not_dx_dir & unc_row_start) | (dx_dir & unc_row_end);
1112
1113                            let start_tile = if $check_s {
1114                                ((x_idx as i32 == p0_tile_x) && ($y as i32 == p0_tile_y)) as u32
1115                            } else {
1116                                0
1117                            };
1118
1119                            let end_tile = if $check_e {
1120                                ((x_idx as i32 == p1_tile_x) && ($y as i32 == p1_tile_y)) as u32
1121                            } else {
1122                                0
1123                            };
1124
1125                            let mut mask = $winding_input;
1126
1127                            // Entrant/Exit
1128                            mask |= canonical_row_start & (1 ^ start_tile);
1129                            mask |= (1 ^ canonical_row_start) << not_dx_dir << LEFT_SHIFT;
1130                            mask |= (canonical_row_end & (1 ^ end_tile)) << BOT_SHIFT;
1131                            mask |= (1 ^ canonical_row_end) << dx_dir << LEFT_SHIFT;
1132
1133                            // Corner
1134                            let x_left_f = x_idx as f32;
1135                            let x_right_f = (x_idx + 1) as f32;
1136                            let trc = (($row_top_x == x_right_f) as u32) & (1 ^ start_tile);
1137                            let tlc = (($row_top_x == x_left_f) as u32) & (1 ^ start_tile);
1138                            let brc = (($row_bottom_x == x_right_f) as u32) & (1 ^ end_tile);
1139                            let blc = (($row_bottom_x == x_left_f) as u32) & (1 ^ end_tile);
1140                            // Top left is handled specially
1141                            let tie_break = tlc & (canonical_row_start ^ 1);
1142
1143                            mask |= (tie_break | blc) << LEFT_SHIFT;
1144                            mask |= (trc | brc) << RIGHT_SHIFT;
1145                            mask &= !(tie_break | trc);
1146                            mask &= !((blc | brc) << BOT_SHIFT);
1147
1148                            self.tile_buf.push(Tile::new(x_idx, $y, line_idx, mask));
1149                        }};
1150                    }
1151
1152                    // Handles row geometry and clamping logic.
1153                    macro_rules! process_row {
1154                        ($y_idx:expr, $row_top_y:expr, $row_bottom_y:expr, $w_mask:expr,
1155                     $check_s:tt, $check_e:tt, $clamped:tt) => {{
1156                            let row_top_x = p0_x + ($row_top_y - p0_y) * x_slope;
1157                            let row_bottom_x = p0_x + ($row_bottom_y - p0_y) * x_slope;
1158
1159                            let (row_left_x, row_right_x, x_end) = if $clamped {
1160                                let lx = f32::min(row_top_x, row_bottom_x).max(line_left_x);
1161                                let rx = f32::max(row_top_x, row_bottom_x).min(line_right_x);
1162                                let xe = (rx as u16).min(tile_columns.saturating_sub(1));
1163                                (lx, rx, xe)
1164                            } else {
1165                                let lx = f32::min(row_top_x, row_bottom_x);
1166                                let rx = f32::max(row_top_x, row_bottom_x);
1167                                let xe = rx as u16; // Safe because we checked bounds earlier
1168                                (lx, rx, xe)
1169                            };
1170
1171                            let canonical_x_start = row_left_x.floor() as i32;
1172                            let canonical_x_end = row_right_x as u16;
1173                            let x_start = row_left_x as u16;
1174
1175                            if x_start <= x_end {
1176                                let is_single = (x_start == x_end) as u32;
1177                                let w_left = (w_start_base | (is_single << 4)) & $w_mask;
1178
1179                                push_edge!(
1180                                    x_start,
1181                                    $y_idx,
1182                                    row_top_x,
1183                                    row_bottom_x,
1184                                    canonical_x_start,
1185                                    canonical_x_end,
1186                                    w_left,
1187                                    $check_s,
1188                                    $check_e
1189                                );
1190
1191                                for x_idx in x_start.saturating_add(1)..x_end {
1192                                    self.tile_buf
1193                                        .push(Tile::new(x_idx, $y_idx, line_idx, R | L));
1194                                }
1195
1196                                if x_start < x_end {
1197                                    let w_right = w_end_base & $w_mask;
1198                                    push_edge!(
1199                                        x_end,
1200                                        $y_idx,
1201                                        row_top_x,
1202                                        row_bottom_x,
1203                                        canonical_x_start,
1204                                        canonical_x_end,
1205                                        w_right,
1206                                        $check_s,
1207                                        $check_e
1208                                    );
1209                                }
1210                            }
1211                        }};
1212                    }
1213
1214                    // Central macro
1215                    macro_rules! run_loops {
1216                        ($clamped:tt) => {{
1217                            // Top Row
1218                            let is_start_culled = line_top_y < 0.0;
1219                            if !is_start_culled {
1220                                let y = f32::from(y_top_tiles);
1221                                let row_bottom_y = (y + 1.0).min(line_bottom_y);
1222                                let mask = ((y >= line_top_y) as u32) << WINDING_SHIFT;
1223                                process_row!(
1224                                    y_top_tiles,
1225                                    line_top_y,
1226                                    row_bottom_y,
1227                                    mask,
1228                                    true,
1229                                    true,
1230                                    $clamped
1231                                );
1232                            }
1233
1234                            let y_start_middle = if is_start_culled {
1235                                y_top_tiles
1236                            } else {
1237                                y_top_tiles + 1
1238                            };
1239                            let line_bottom_floor = line_bottom_y.floor();
1240                            let y_end_middle = (line_bottom_floor as u16).min(tile_rows);
1241                            let has_separate_bottom_row = line_bottom_y != line_bottom_floor
1242                                && y_end_middle < tile_rows
1243                                && (is_start_culled || y_end_middle != y_top_tiles);
1244
1245                            if y_start_middle < y_end_middle {
1246                                for y_idx in y_start_middle..y_end_middle {
1247                                    let y = f32::from(y_idx);
1248                                    let row_bottom_y = (y + 1.0).min(line_bottom_y);
1249                                    let is_last_middle = y_idx == y_end_middle - 1;
1250                                    let check_end = is_last_middle && !has_separate_bottom_row;
1251
1252                                    process_row!(
1253                                        y_idx,
1254                                        y,
1255                                        row_bottom_y,
1256                                        u32::MAX,
1257                                        false,
1258                                        check_end,
1259                                        $clamped
1260                                    );
1261                                }
1262                            }
1263
1264                            // Bottom Row
1265                            if has_separate_bottom_row {
1266                                let y_idx = y_end_middle;
1267                                let y = f32::from(y_idx);
1268                                process_row!(
1269                                    y_idx,
1270                                    y,
1271                                    line_bottom_y,
1272                                    u32::MAX,
1273                                    false,
1274                                    true,
1275                                    $clamped
1276                                );
1277                            }
1278                        }};
1279                    }
1280
1281                    if needs_clamping {
1282                        run_loops!(true);
1283                    } else {
1284                        run_loops!(false);
1285                    }
1286                }
1287            } else {
1288                // Case: Line is fully contained within a single tile.
1289                let tile = Tile::new_clamped(
1290                    (line_left_x as u16).min(tile_columns + 1),
1291                    y_top_tiles,
1292                    line_idx,
1293                    ((f32::from(y_top_tiles) >= line_top_y) as u32) << WINDING_SHIFT,
1294                );
1295                self.tile_buf.push(tile);
1296            }
1297        }
1298    }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303    use crate::flatten::{FlattenCtx, Line, Point, fill};
1304    use crate::geometry::RectU16;
1305    use crate::kurbo::{Affine, BezPath};
1306    use crate::tile::CulledWindings;
1307    use crate::tile::{B, L, R, T, Tile, Tiles, W};
1308    use fearless_simd::Level;
1309    use std::vec::Vec;
1310
1311    const VIEW_DIM: u16 = 100;
1312    const F_V_DIM: f32 = VIEW_DIM as f32;
1313
1314    fn new_tiles() -> Tiles {
1315        Tiles::new(
1316            Level::try_detect().unwrap_or(Level::baseline()),
1317            VIEW_DIM,
1318            VIEW_DIM,
1319        )
1320    }
1321
1322    impl Tiles {
1323        fn assert_tiles_match(
1324            &mut self,
1325            lines: &[Line],
1326            width: u16,
1327            height: u16,
1328            expected: &[Tile],
1329        ) {
1330            self.make_tiles_msaa(lines, width, height);
1331            assert_eq!(self.tile_buf, expected, "MSAA: Tile buffer mismatch");
1332
1333            self.make_tiles_analytic_aa(Level::baseline(), lines, width, height);
1334            check_analytic_aa_matches(&self.tile_buf, expected);
1335        }
1336    }
1337
1338    fn check_analytic_aa_matches(actual: &[Tile], expected: &[Tile]) {
1339        assert_eq!(
1340            actual.len(),
1341            expected.len(),
1342            "Analytic AA: Tile count mismatch."
1343        );
1344
1345        for (i, (got, want)) in actual.iter().zip(expected.iter()).enumerate() {
1346            assert_eq!(got.x, want.x, "Analytic AA: Tile[{}] X mismatch", i);
1347            assert_eq!(got.y, want.y, "Analytic AA: Tile[{}] Y mismatch", i);
1348            assert_eq!(
1349                got.line_idx(),
1350                want.line_idx(),
1351                "Analytic AA: Tile[{}] Line Index mismatch",
1352                i
1353            );
1354
1355            let got_winding = got.packed_winding_line_idx & W;
1356            let want_winding = want.packed_winding_line_idx & W;
1357            assert_eq!(
1358                got_winding, want_winding,
1359                "Analytic AA: Tile[{}] Winding mismatch",
1360                i
1361            );
1362        }
1363    }
1364
1365    //==============================================================================================
1366    // Culled Lines
1367    //==============================================================================================
1368    #[test]
1369    fn cull_sloped_outside_lines() {
1370        let lines = [
1371            Line {
1372                p0: Point { x: 1.0, y: -7.0 },
1373                p1: Point { x: 3.0, y: -1.0 },
1374            },
1375            Line {
1376                p0: Point { x: 1.0, y: -11.0 },
1377                p1: Point { x: 3.0, y: -1.0 },
1378            },
1379            Line {
1380                p0: Point {
1381                    x: F_V_DIM + 1.0,
1382                    y: 50.0,
1383                },
1384                p1: Point {
1385                    x: F_V_DIM + 3.0,
1386                    y: 70.0,
1387                },
1388            },
1389            Line {
1390                p0: Point {
1391                    x: 1.0,
1392                    y: F_V_DIM + 1.0,
1393                },
1394                p1: Point {
1395                    x: 3.0,
1396                    y: F_V_DIM + 7.0,
1397                },
1398            },
1399            Line {
1400                p0: Point {
1401                    x: 1.0,
1402                    y: F_V_DIM + 1.0,
1403                },
1404                p1: Point {
1405                    x: 3.0,
1406                    y: F_V_DIM + 13.0,
1407                },
1408            },
1409        ];
1410
1411        let mut tiles = new_tiles();
1412        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1413    }
1414
1415    #[test]
1416    fn sloped_line_crossing_top() {
1417        let lines = [
1418            Line {
1419                p0: Point { x: -2.0, y: -3.0 },
1420                p1: Point { x: 2.0, y: 1.0 },
1421            },
1422            Line {
1423                p0: Point { x: 6.0, y: -1.0 },
1424                p1: Point { x: 5.0, y: 2.0 },
1425            },
1426            Line {
1427                p0: Point { x: 9.0, y: -10.0 },
1428                p1: Point { x: 10.0, y: 3.0 },
1429            },
1430            Line {
1431                p0: Point { x: 2.0, y: 1.0 },
1432                p1: Point { x: -2.0, y: -3.0 },
1433            },
1434        ];
1435
1436        let mut tiles = new_tiles();
1437        let expected = [
1438            Tile::new(0, 0, 0, W | T),
1439            Tile::new(1, 0, 1, W | T),
1440            Tile::new(2, 0, 2, W | T),
1441            Tile::new(0, 0, 3, W | T),
1442        ];
1443
1444        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1445    }
1446
1447    #[test]
1448    fn sloped_line_crossing_bot() {
1449        let lines = [
1450            Line {
1451                p0: Point {
1452                    x: 5.0,
1453                    y: F_V_DIM + 3.0,
1454                },
1455                p1: Point {
1456                    x: 6.0,
1457                    y: F_V_DIM - 2.0,
1458                },
1459            },
1460            Line {
1461                p0: Point {
1462                    x: 10.0,
1463                    y: F_V_DIM + 1.0,
1464                },
1465                p1: Point {
1466                    x: 9.0,
1467                    y: F_V_DIM - 1.0,
1468                },
1469            },
1470            Line {
1471                p0: Point {
1472                    x: 2.0,
1473                    y: F_V_DIM - 2.0,
1474                },
1475                p1: Point {
1476                    x: 3.0,
1477                    y: F_V_DIM + 3.0,
1478                },
1479            },
1480        ];
1481
1482        let mut tiles = new_tiles();
1483        let expected = [
1484            Tile::new(1, 24, 0, B),
1485            Tile::new(2, 24, 1, B),
1486            Tile::new(0, 24, 2, B),
1487        ];
1488
1489        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1490    }
1491
1492    #[test]
1493    fn sloped_line_crossing_top_multi_tile() {
1494        let lines = [
1495            Line {
1496                p0: Point { x: 1.0, y: -5.0 },
1497                p1: Point { x: 6.0, y: 7.0 },
1498            },
1499            Line {
1500                p0: Point { x: 2.5, y: -10.0 },
1501                p1: Point { x: 3.5, y: 6.0 },
1502            },
1503        ];
1504
1505        let mut tiles = new_tiles();
1506        let expected = [
1507            Tile::new(0, 0, 0, W | T | R),
1508            Tile::new(1, 0, 0, L | B),
1509            Tile::new(1, 1, 0, W | T),
1510            Tile::new(0, 0, 1, W | T | B),
1511            Tile::new(0, 1, 1, W | T),
1512        ];
1513
1514        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1515    }
1516
1517    #[test]
1518    fn sloped_line_crossing_bot_multi_tile() {
1519        let lines = [
1520            Line {
1521                p0: Point {
1522                    x: 12.0,
1523                    y: F_V_DIM + 10.0,
1524                },
1525                p1: Point { x: 2.0, y: 94.0 },
1526            },
1527            Line {
1528                p0: Point {
1529                    x: 1.5,
1530                    y: F_V_DIM + 5.0,
1531                },
1532                p1: Point { x: 3.5, y: 94.0 },
1533            },
1534        ];
1535
1536        let mut tiles = new_tiles();
1537        let expected = [
1538            Tile::new(0, 23, 0, B),
1539            Tile::new(0, 24, 0, W | T | R),
1540            Tile::new(1, 24, 0, B | L),
1541            Tile::new(0, 23, 1, B),
1542            Tile::new(0, 24, 1, W | T | B),
1543        ];
1544
1545        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1546    }
1547
1548    #[test]
1549    fn sloped_line_crossing_right() {
1550        let lines = [
1551            Line {
1552                p0: Point { x: 97.0, y: 1.0 },
1553                p1: Point {
1554                    x: F_V_DIM + 1.0,
1555                    y: 2.0,
1556                },
1557            },
1558            Line {
1559                p0: Point { x: 93.0, y: 1.0 },
1560                p1: Point {
1561                    x: F_V_DIM + 5.0,
1562                    y: 2.0,
1563                },
1564            },
1565        ];
1566
1567        let mut tiles = new_tiles();
1568        let expected = [
1569            Tile::new(24, 0, 0, R),
1570            Tile::new(23, 0, 1, R),
1571            Tile::new(24, 0, 1, R | L),
1572        ];
1573
1574        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1575    }
1576
1577    #[test]
1578    fn sloped_line_crossing_left() {
1579        let lines = [
1580            Line {
1581                p0: Point { x: -5.0, y: 1.0 },
1582                p1: Point { x: 1.0, y: 2.0 },
1583            },
1584            Line {
1585                p0: Point { x: -5.0, y: 1.0 },
1586                p1: Point { x: 5.0, y: 2.0 },
1587            },
1588            Line {
1589                p0: Point { x: -5.0, y: 1.0 },
1590                p1: Point { x: 13.0, y: 9.0 },
1591            },
1592        ];
1593
1594        let mut tiles = new_tiles();
1595        let expected = [
1596            Tile::new(0, 0, 0, L),
1597            Tile::new(0, 0, 1, L | R),
1598            Tile::new(1, 0, 1, L),
1599            Tile::new(0, 0, 2, L | B),
1600            Tile::new(0, 1, 2, W | R | T),
1601            Tile::new(1, 1, 2, R | L),
1602            Tile::new(2, 1, 2, L | B),
1603            Tile::new(2, 2, 2, W | R | T),
1604            Tile::new(3, 2, 2, L),
1605        ];
1606
1607        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1608    }
1609
1610    #[test]
1611    fn horizontal_line_above_viewport() {
1612        let lines = [Line {
1613            p0: Point { x: 10.0, y: -5.0 },
1614            p1: Point { x: 90.0, y: -5.0 },
1615        }];
1616
1617        let mut tiles = new_tiles();
1618        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1619    }
1620
1621    #[test]
1622    fn horizontal_line_below_viewport() {
1623        let lines = [Line {
1624            p0: Point {
1625                x: 10.0,
1626                y: F_V_DIM + 5.0,
1627            },
1628            p1: Point {
1629                x: 90.0,
1630                y: F_V_DIM + 5.0,
1631            },
1632        }];
1633
1634        let mut tiles = new_tiles();
1635        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1636    }
1637
1638    #[test]
1639    fn horizontal_line_crossing_left_viewport() {
1640        let lines = [Line {
1641            p0: Point { x: -10.0, y: 10.0 },
1642            p1: Point { x: 10.0, y: 10.0 },
1643        }];
1644
1645        let mut tiles = new_tiles();
1646        let expected = [
1647            Tile::new(0, 2, 0, L | R),
1648            Tile::new(1, 2, 0, L | R),
1649            Tile::new(2, 2, 0, L),
1650        ];
1651
1652        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1653    }
1654
1655    #[test]
1656    fn horizontal_line_crossing_right_viewport() {
1657        let lines = [Line {
1658            p0: Point {
1659                x: F_V_DIM - 5.0,
1660                y: 10.0,
1661            },
1662            p1: Point {
1663                x: F_V_DIM + 5.0,
1664                y: 10.0,
1665            },
1666        }];
1667
1668        let mut tiles = new_tiles();
1669        let expected = [Tile::new(23, 2, 0, R), Tile::new(24, 2, 0, L | R)];
1670
1671        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1672    }
1673
1674    #[test]
1675    fn vertical_lines_outside_viewport() {
1676        let lines = [
1677            Line {
1678                p0: Point { x: 1.0, y: -5.0 },
1679                p1: Point { x: 1.0, y: -1.0 },
1680            },
1681            Line {
1682                p0: Point {
1683                    x: 1.0,
1684                    y: F_V_DIM + 1.0,
1685                },
1686                p1: Point {
1687                    x: 1.0,
1688                    y: F_V_DIM + 5.0,
1689                },
1690            },
1691        ];
1692
1693        let mut tiles = new_tiles();
1694        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &[]);
1695    }
1696
1697    #[test]
1698    fn vertical_path_on_the_right_of_viewport() {
1699        const VIEWPORT_WIDTH: u16 = 10;
1700        const VIEWPORT_HEIGHT: u16 = 10;
1701
1702        let path = BezPath::from_svg("M261,0 L78848,0 L78848,4 L261,4 Z").unwrap();
1703        let mut line_buf: Vec<Line> = Vec::new();
1704        fill(
1705            Level::try_detect().unwrap_or(Level::baseline()),
1706            &path,
1707            Affine::IDENTITY,
1708            &mut line_buf,
1709            &mut FlattenCtx::default(),
1710            RectU16::new(0, 0, VIEWPORT_WIDTH, VIEWPORT_HEIGHT),
1711        );
1712
1713        let mut tiles = new_tiles();
1714        tiles.assert_tiles_match(&line_buf, VIEWPORT_WIDTH, VIEWPORT_HEIGHT, &[]);
1715    }
1716
1717    #[test]
1718    fn vertical_line_crossing_top_viewport() {
1719        let lines = [
1720            Line {
1721                p0: Point { x: 1.0, y: -7.0 },
1722                p1: Point { x: 1.0, y: 3.0 },
1723            },
1724            Line {
1725                p0: Point { x: 1.0, y: -7.0 },
1726                p1: Point { x: 1.0, y: 7.0 },
1727            },
1728            Line {
1729                p0: Point { x: 1.0, y: -7.0 },
1730                p1: Point { x: 1.0, y: 8.0 },
1731            },
1732        ];
1733
1734        let mut tiles = new_tiles();
1735        let expected = [
1736            Tile::new(0, 0, 0, W | T),
1737            Tile::new(0, 0, 1, W | B | T),
1738            Tile::new(0, 1, 1, W | T),
1739            Tile::new(0, 0, 2, W | B | T),
1740            Tile::new(0, 1, 2, W | T),
1741        ];
1742
1743        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1744    }
1745
1746    #[test]
1747    fn vertical_line_crossing_bot_viewport() {
1748        let lines = [
1749            Line {
1750                p0: Point {
1751                    x: 1.0,
1752                    y: F_V_DIM - 1.0,
1753                },
1754                p1: Point {
1755                    x: 1.0,
1756                    y: F_V_DIM + 5.0,
1757                },
1758            },
1759            Line {
1760                p0: Point {
1761                    x: 1.0,
1762                    y: F_V_DIM - 5.0,
1763                },
1764                p1: Point {
1765                    x: 1.0,
1766                    y: F_V_DIM + 5.0,
1767                },
1768            },
1769        ];
1770
1771        let mut tiles = new_tiles();
1772        let expected = [
1773            Tile::new(0, 24, 0, B),
1774            Tile::new(0, 23, 1, B),
1775            Tile::new(0, 24, 1, W | T | B),
1776        ];
1777
1778        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1779    }
1780
1781    #[test]
1782    fn clip_top_left_corner() {
1783        let lines = [Line {
1784            p0: Point { x: -1.0, y: 2.0 },
1785            p1: Point { x: 2.0, y: -1.0 },
1786        }];
1787
1788        let mut tiles = new_tiles();
1789        let expected = [Tile::new(0, 0, 0, W | L | T)];
1790
1791        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1792    }
1793
1794    #[test]
1795    fn clip_bottom_right_corner() {
1796        let lines = [Line {
1797            p0: Point {
1798                x: F_V_DIM + 1.0,
1799                y: F_V_DIM - 2.0,
1800            },
1801            p1: Point {
1802                x: F_V_DIM - 2.0,
1803                y: F_V_DIM + 1.0,
1804            },
1805        }];
1806
1807        let mut tiles = new_tiles();
1808        let expected = [Tile::new(24, 24, 0, R | B)];
1809
1810        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1811    }
1812
1813    //==============================================================================================
1814    // Axis-aligned lines
1815    //==============================================================================================
1816    #[test]
1817    fn horizontal_line_left_to_right_three_tile() {
1818        let lines = [Line {
1819            p0: Point { x: 1.5, y: 1.0 },
1820            p1: Point { x: 8.5, y: 1.0 },
1821        }];
1822
1823        let mut tiles = new_tiles();
1824        let expected = [
1825            Tile::new(0, 0, 0, R),
1826            Tile::new(1, 0, 0, R | L),
1827            Tile::new(2, 0, 0, L),
1828        ];
1829
1830        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1831    }
1832
1833    #[test]
1834    fn resize_works_correctly() {
1835        let lines = [
1836            Line {
1837                p0: Point { x: 1.5, y: 1.0 },
1838                p1: Point { x: 8.5, y: 1.0 },
1839            },
1840            Line {
1841                p0: Point { x: 1.5, y: 13.0 },
1842                p1: Point { x: 8.5, y: 13.0 },
1843            },
1844        ];
1845        let small_expected = [
1846            Tile::new(0, 0, 0, R),
1847            Tile::new(1, 0, 0, R | L),
1848            Tile::new(2, 0, 0, L),
1849        ];
1850        let large_expected = [
1851            Tile::new(0, 0, 0, R),
1852            Tile::new(1, 0, 0, R | L),
1853            Tile::new(2, 0, 0, L),
1854            Tile::new(0, 3, 1, R),
1855            Tile::new(1, 3, 1, R | L),
1856            Tile::new(2, 3, 1, L),
1857        ];
1858
1859        let mut tiles = Tiles::new(Level::baseline(), 12, 8);
1860        tiles.assert_tiles_match(&lines, 12, 8, &small_expected);
1861        tiles.assert_tiles_match(&lines, 12, 16, &large_expected);
1862        tiles.assert_tiles_match(&lines, 12, 8, &small_expected);
1863    }
1864
1865    #[test]
1866    fn horizontal_line_right_to_left_three_tile() {
1867        let lines = [Line {
1868            p0: Point { x: 8.5, y: 1.0 },
1869            p1: Point { x: 1.5, y: 1.0 },
1870        }];
1871
1872        let mut tiles = new_tiles();
1873        let expected = [
1874            Tile::new(0, 0, 0, R),
1875            Tile::new(1, 0, 0, R | L),
1876            Tile::new(2, 0, 0, L),
1877        ];
1878
1879        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1880    }
1881
1882    #[test]
1883    fn horizontal_line_multi_tile() {
1884        let lines = [Line {
1885            p0: Point { x: 1.5, y: 1.0 },
1886            p1: Point { x: 12.5, y: 1.0 },
1887        }];
1888
1889        let mut tiles = new_tiles();
1890        let expected = [
1891            Tile::new(0, 0, 0, R),
1892            Tile::new(1, 0, 0, R | L),
1893            Tile::new(2, 0, 0, R | L),
1894            Tile::new(3, 0, 0, L),
1895        ];
1896
1897        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1898    }
1899
1900    #[test]
1901    fn vertical_line_down_three_tile() {
1902        let lines = [Line {
1903            p0: Point { x: 1.0, y: 1.5 },
1904            p1: Point { x: 1.0, y: 8.5 },
1905        }];
1906
1907        let mut tiles = new_tiles();
1908        let expected = [
1909            Tile::new(0, 0, 0, B),
1910            Tile::new(0, 1, 0, W | T | B),
1911            Tile::new(0, 2, 0, W | T),
1912        ];
1913
1914        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1915    }
1916
1917    #[test]
1918    fn vertical_line_down_multi_tile() {
1919        let lines = [Line {
1920            p0: Point { x: 1.0, y: 1.0 },
1921            p1: Point { x: 1.0, y: 13.0 },
1922        }];
1923
1924        let mut tiles = new_tiles();
1925        let expected = [
1926            Tile::new(0, 0, 0, B),
1927            Tile::new(0, 1, 0, W | T | B),
1928            Tile::new(0, 2, 0, W | T | B),
1929            Tile::new(0, 3, 0, W | T),
1930        ];
1931
1932        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1933    }
1934
1935    #[test]
1936    fn vertical_line_up_three_tile() {
1937        let lines = [Line {
1938            p0: Point { x: 1.0, y: 13.0 },
1939            p1: Point { x: 1.0, y: 1.0 },
1940        }];
1941
1942        let mut tiles = new_tiles();
1943        let expected = [
1944            Tile::new(0, 0, 0, B),
1945            Tile::new(0, 1, 0, W | T | B),
1946            Tile::new(0, 2, 0, W | T | B),
1947            Tile::new(0, 3, 0, W | T),
1948        ];
1949
1950        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1951    }
1952
1953    #[test]
1954    fn vertical_line_up_multi_tile() {
1955        let lines = [Line {
1956            p0: Point { x: 1.0, y: 8.5 },
1957            p1: Point { x: 1.0, y: 1.5 },
1958        }];
1959
1960        let mut tiles = new_tiles();
1961        let expected = [
1962            Tile::new(0, 0, 0, B),
1963            Tile::new(0, 1, 0, W | T | B),
1964            Tile::new(0, 2, 0, W | T),
1965        ];
1966
1967        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1968    }
1969
1970    // Exclusive to the bottom edge, no P required.
1971    #[test]
1972    fn vertical_line_touching_bot() {
1973        let lines = [Line {
1974            p0: Point { x: 1.0, y: 1.0 },
1975            p1: Point { x: 1.0, y: 8.0 },
1976        }];
1977
1978        let mut tiles = new_tiles();
1979        let expected = [Tile::new(0, 0, 0, B), Tile::new(0, 1, 0, W | T)];
1980
1981        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1982    }
1983
1984    #[test]
1985    fn vertical_line_touching_top() {
1986        let lines = [Line {
1987            p0: Point { x: 1.0, y: 0.0 },
1988            p1: Point { x: 1.0, y: 7.0 },
1989        }];
1990
1991        let mut tiles = new_tiles();
1992        let expected = [Tile::new(0, 0, 0, W | B), Tile::new(0, 1, 0, W | T)];
1993
1994        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
1995    }
1996
1997    //==============================================================================================
1998    // Sloped Lines
1999    //==============================================================================================
2000    #[test]
2001    fn top_left_to_bottom_right() {
2002        let lines = [Line {
2003            p0: Point { x: 1.0, y: 1.0 },
2004            p1: Point { x: 11.0, y: 9.0 },
2005        }];
2006
2007        let mut tiles = new_tiles();
2008        let expected = [
2009            Tile::new(0, 0, 0, R),
2010            Tile::new(1, 0, 0, L | B),
2011            Tile::new(1, 1, 0, W | R | T),
2012            Tile::new(2, 1, 0, L | B),
2013            Tile::new(2, 2, 0, W | T),
2014        ];
2015
2016        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2017    }
2018
2019    #[test]
2020    fn bottom_right_to_top_left() {
2021        let lines = [Line {
2022            p0: Point { x: 11.0, y: 9.0 },
2023            p1: Point { x: 1.0, y: 1.0 },
2024        }];
2025
2026        let mut tiles = new_tiles();
2027        let expected = [
2028            Tile::new(0, 0, 0, R),
2029            Tile::new(1, 0, 0, L | B),
2030            Tile::new(1, 1, 0, W | R | T),
2031            Tile::new(2, 1, 0, L | B),
2032            Tile::new(2, 2, 0, W | T),
2033        ];
2034
2035        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2036    }
2037
2038    #[test]
2039    fn bottom_left_to_top_right() {
2040        let lines = [Line {
2041            p0: Point { x: 2.0, y: 11.0 },
2042            p1: Point { x: 14.0, y: 6.0 },
2043        }];
2044
2045        let mut tiles = new_tiles();
2046        let expected = [
2047            Tile::new(2, 1, 0, R | B),
2048            Tile::new(3, 1, 0, L),
2049            Tile::new(0, 2, 0, R),
2050            Tile::new(1, 2, 0, R | L),
2051            Tile::new(2, 2, 0, W | L | T),
2052        ];
2053
2054        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2055    }
2056
2057    #[test]
2058    fn top_right_to_bottom_left() {
2059        let lines = [Line {
2060            p0: Point { x: 14.0, y: 6.0 },
2061            p1: Point { x: 2.0, y: 11.0 },
2062        }];
2063
2064        let mut tiles = new_tiles();
2065        let expected = [
2066            Tile::new(2, 1, 0, R | B),
2067            Tile::new(3, 1, 0, L),
2068            Tile::new(0, 2, 0, R),
2069            Tile::new(1, 2, 0, R | L),
2070            Tile::new(2, 2, 0, W | L | T),
2071        ];
2072
2073        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2074    }
2075
2076    #[test]
2077    fn two_lines_in_single_tile() {
2078        let lines = [
2079            Line {
2080                p0: Point { x: 1.0, y: 3.0 },
2081                p1: Point { x: 3.0, y: 3.0 },
2082            },
2083            Line {
2084                p0: Point { x: 3.0, y: 3.0 },
2085                p1: Point { x: 0.0, y: 1.0 },
2086            },
2087        ];
2088
2089        let mut tiles = new_tiles();
2090        let expected = [Tile::new(0, 0, 0, 0), Tile::new(0, 0, 1, 0)];
2091
2092        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2093    }
2094
2095    #[test]
2096    fn diagonal_cross_corner() {
2097        let lines = [Line {
2098            p0: Point { x: 3.0, y: 5.0 },
2099            p1: Point { x: 5.0, y: 3.0 },
2100        }];
2101
2102        let mut tiles = new_tiles();
2103        let expected = [
2104            Tile::new(1, 0, 0, L),
2105            Tile::new(0, 1, 0, R),
2106            Tile::new(1, 1, 0, W | L | T),
2107        ];
2108
2109        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2110    }
2111
2112    #[test]
2113    fn diagonal_cross_corner_two() {
2114        let lines = [Line {
2115            p0: Point { x: 7.9, y: 7.9 },
2116            p1: Point { x: 0.1, y: 0.1 },
2117        }];
2118
2119        let mut tiles = new_tiles();
2120        let expected = [
2121            Tile::new(0, 0, 0, R),
2122            Tile::new(1, 0, 0, L),
2123            Tile::new(1, 1, 0, W | T),
2124        ];
2125
2126        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2127    }
2128
2129    #[test]
2130    fn diagonal_down_slope_tiles() {
2131        let lines = [Line {
2132            p0: Point { x: 5.0, y: 5.0 },
2133            p1: Point { x: 9.0, y: 9.0 },
2134        }];
2135
2136        let mut tiles = new_tiles();
2137        let expected = [
2138            Tile::new(1, 1, 0, R),
2139            Tile::new(2, 1, 0, L),
2140            Tile::new(2, 2, 0, W | T),
2141        ];
2142
2143        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2144    }
2145
2146    #[test]
2147    fn diagonal_up_slope_tiles() {
2148        let lines = [Line {
2149            p0: Point { x: 5.0, y: 9.0 },
2150            p1: Point { x: 9.0, y: 5.0 },
2151        }];
2152
2153        let mut tiles = new_tiles();
2154        let expected = [
2155            Tile::new(1, 1, 0, R | B),
2156            Tile::new(2, 1, 0, L),
2157            Tile::new(1, 2, 0, W | T),
2158        ];
2159
2160        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2161    }
2162
2163    #[test]
2164    fn diagonal_down_one_tile() {
2165        let lines = [Line {
2166            p0: Point { x: 0.0, y: 0.0 },
2167            p1: Point { x: 4.0, y: 4.0 },
2168        }];
2169
2170        let mut tiles = new_tiles();
2171        let expected = [Tile::new(0, 0, 0, W | R), Tile::new(1, 0, 0, L)];
2172
2173        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2174    }
2175
2176    #[test]
2177    fn diagonal_up_one_tile() {
2178        let lines = [Line {
2179            p0: Point { x: 0.0, y: 4.0 },
2180            p1: Point { x: 4.0, y: 0.0 },
2181        }];
2182
2183        let mut tiles = new_tiles();
2184        let expected = [Tile::new(0, 0, 0, R), Tile::new(1, 0, 0, W | L)];
2185
2186        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2187    }
2188
2189    #[test]
2190    fn diagonal_down_two_tile() {
2191        let lines = [Line {
2192            p0: Point { x: 0.0, y: 0.0 },
2193            p1: Point { x: 8.0, y: 8.0 },
2194        }];
2195
2196        let mut tiles = new_tiles();
2197        let expected = [
2198            Tile::new(0, 0, 0, W | R),
2199            Tile::new(1, 0, 0, L),
2200            Tile::new(1, 1, 0, W | R | T),
2201            Tile::new(2, 1, 0, L),
2202        ];
2203
2204        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2205    }
2206
2207    #[test]
2208    fn diagonal_up_two_tile() {
2209        let lines = [Line {
2210            p0: Point { x: 0.0, y: 8.0 },
2211            p1: Point { x: 8.0, y: 0.0 },
2212        }];
2213
2214        let mut tiles = new_tiles();
2215        let expected = [
2216            Tile::new(1, 0, 0, R | L),
2217            Tile::new(2, 0, 0, W | L),
2218            Tile::new(0, 1, 0, R),
2219            Tile::new(1, 1, 0, W | L | T),
2220        ];
2221
2222        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2223    }
2224
2225    #[test]
2226    fn sloped_ending_right() {
2227        let lines = [Line {
2228            p0: Point { x: 1.0, y: 1.0 },
2229            p1: Point { x: 8.0, y: 2.0 },
2230        }];
2231
2232        let mut tiles = new_tiles();
2233        let expected = [
2234            Tile::new(0, 0, 0, R),
2235            Tile::new(1, 0, 0, R | L),
2236            Tile::new(2, 0, 0, L),
2237        ];
2238
2239        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2240    }
2241
2242    // This test reproduces an issue where a floating point inaccuracy would
2243    // cause a tile with the winding bit being emitted at a slightly earlier
2244    // position, causing a filled 4x4 block artifact to appear.
2245    #[test]
2246    fn issue_early_winding_emission() {
2247        const WIDTH: u16 = Tile::WIDTH * 35;
2248        const HEIGHT: u16 = Tile::HEIGHT * 7;
2249
2250        let tile_width = f32::from(Tile::WIDTH);
2251        let tile_height = f32::from(Tile::HEIGHT);
2252        let lines = [Line {
2253            p0: Point {
2254                x: 32.89 * tile_width,
2255                y: 0.9 * tile_height,
2256            },
2257            p1: Point {
2258                x: 33.5 * tile_width,
2259                y: 7.0 * tile_height,
2260            },
2261        }];
2262
2263        let mut tiles = Tiles::new(Level::baseline(), HEIGHT, HEIGHT);
2264        tiles.make_tiles_analytic_aa(Level::baseline(), &lines, WIDTH, HEIGHT);
2265
2266        let row_tiles: Vec<Tile> = tiles
2267            .tile_buf
2268            .iter()
2269            .copied()
2270            .filter(|tile| tile.y == 2)
2271            .collect();
2272
2273        // When the issue occurred, another tile at location x = 32, y = 2
2274        // would be emitted.
2275        assert_eq!(row_tiles, [Tile::new(33, 2, 0, W)]);
2276    }
2277
2278    #[test]
2279    fn sloped_touching_top() {
2280        let lines = [Line {
2281            p0: Point { x: 0.0, y: 8.0 },
2282            p1: Point { x: 4.0, y: 0.0 },
2283        }];
2284
2285        let mut tiles = new_tiles();
2286        let expected = [
2287            Tile::new(0, 0, 0, R | B),
2288            Tile::new(1, 0, 0, W | L),
2289            Tile::new(0, 1, 0, W | T),
2290        ];
2291
2292        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2293    }
2294
2295    #[test]
2296    fn sloped_touching_bot() {
2297        let lines = [Line {
2298            p0: Point { x: 0.0, y: 0.0 },
2299            p1: Point { x: 4.0, y: 8.0 },
2300        }];
2301
2302        let mut tiles = new_tiles();
2303        let expected = [
2304            Tile::new(0, 0, 0, W | B),
2305            Tile::new(0, 1, 0, W | R | T),
2306            Tile::new(1, 1, 0, L),
2307        ];
2308
2309        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2310    }
2311
2312    //==============================================================================================
2313    // Same Tile Cases
2314    //==============================================================================================
2315    #[test]
2316    fn same_tile() {
2317        let lines = [Line {
2318            p0: Point { x: 1.0, y: 1.0 },
2319            p1: Point { x: 3.0, y: 3.0 },
2320        }];
2321
2322        let mut tiles = new_tiles();
2323        let expected = [Tile::new(0, 0, 0, 0)];
2324
2325        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2326    }
2327
2328    #[test]
2329    fn same_tile_left() {
2330        let lines = [Line {
2331            p0: Point { x: 0.0, y: 1.0 },
2332            p1: Point { x: 3.0, y: 1.0 },
2333        }];
2334
2335        let mut tiles = new_tiles();
2336        let expected = [Tile::new(0, 0, 0, 0)];
2337
2338        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2339    }
2340
2341    #[test]
2342    fn same_tile_top() {
2343        let lines = [Line {
2344            p0: Point { x: 1.0, y: 0.0 },
2345            p1: Point { x: 1.0, y: 3.0 },
2346        }];
2347
2348        let mut tiles = new_tiles();
2349        let expected = [Tile::new(0, 0, 0, W)];
2350
2351        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2352    }
2353
2354    #[test]
2355    fn same_tile_right() {
2356        let lines = [Line {
2357            p0: Point { x: 1.0, y: 1.0 },
2358            p1: Point { x: 4.0, y: 1.0 },
2359        }];
2360
2361        let mut tiles = new_tiles();
2362        let expected = [Tile::new(0, 0, 0, R), Tile::new(1, 0, 0, L)];
2363
2364        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2365    }
2366
2367    #[test]
2368    fn same_tile_bottom() {
2369        let lines = [
2370            Line {
2371                p0: Point { x: 1.0, y: 1.0 },
2372                p1: Point { x: 1.0, y: 4.0 },
2373            },
2374            Line {
2375                p0: Point { x: 1.0, y: 1.0 },
2376                p1: Point { x: 2.0, y: 4.0 },
2377            },
2378        ];
2379
2380        let mut tiles = new_tiles();
2381        let expected = [Tile::new(0, 0, 0, 0), Tile::new(0, 0, 1, 0)];
2382
2383        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2384    }
2385
2386    #[test]
2387    fn same_tile_top_left() {
2388        let lines = [
2389            Line {
2390                p0: Point { x: 0.0, y: 1.0 },
2391                p1: Point { x: 1.0, y: 0.0 },
2392            },
2393            Line {
2394                p0: Point { x: 0.0, y: 0.0001 },
2395                p1: Point { x: 0.0001, y: 0.0 },
2396            },
2397        ];
2398
2399        let mut tiles = new_tiles();
2400        let expected = [Tile::new(0, 0, 0, W), Tile::new(0, 0, 1, W)];
2401
2402        tiles.assert_tiles_match(&lines, VIEW_DIM, VIEW_DIM, &expected);
2403    }
2404
2405    //==============================================================================================
2406    // CulledWindings & Row Marking Logic
2407    //==============================================================================================
2408    #[test]
2409    fn test_culled_windings_new_and_reset() {
2410        let mut windings = CulledWindings::new(8);
2411        assert_eq!(windings.partial.len(), 2);
2412        assert_eq!(windings.coarse.len(), 2);
2413        assert_eq!(windings.active.len(), 1);
2414
2415        windings.coarse[0] = 1;
2416        windings.active[0] = 0xFF;
2417        windings.culled = true;
2418
2419        windings.reset(8);
2420        assert_eq!(windings.coarse[0], 0);
2421        assert_eq!(windings.active[0], 0);
2422
2423        windings.coarse[0] = 1;
2424        windings.active[0] = 0xFF;
2425        windings.culled = false;
2426
2427        windings.reset(8);
2428        assert_eq!(windings.coarse[0], 1);
2429        assert_eq!(windings.active[0], 0xFF);
2430
2431        windings.culled = true;
2432        windings.reset(12);
2433        assert_eq!(windings.coarse[0], 0);
2434        assert_eq!(windings.active[0], 0);
2435    }
2436
2437    #[test]
2438    fn test_mark_row_active() {
2439        let mut windings = CulledWindings::new(200);
2440        windings.mark_row_active(0);
2441        windings.mark_row_active(5);
2442        windings.mark_row_active(31);
2443        windings.mark_row_active(32);
2444        assert_eq!(windings.active[0], (1 << 0) | (1 << 5) | (1 << 31));
2445        assert_eq!(windings.active[1], 1 << 0);
2446    }
2447
2448    #[test]
2449    fn test_mark_row_range_single_word() {
2450        let mut windings = CulledWindings::new(200);
2451        windings.mark_row_range_active(5, 10);
2452        let expected_mask = ((1_u32 << 5) - 1) << 5;
2453        assert_eq!(windings.active[0], expected_mask);
2454        assert_eq!(windings.active[1], 0);
2455    }
2456
2457    #[test]
2458    fn test_mark_row_range_full_word() {
2459        let mut windings = CulledWindings::new(200);
2460        windings.mark_row_range_active(0, 32);
2461        assert_eq!(windings.active[0], u32::MAX);
2462        assert_eq!(windings.active[1], 0);
2463    }
2464
2465    #[test]
2466    fn test_mark_row_range_spanning_two_words() {
2467        let mut windings = CulledWindings::new(200);
2468        windings.mark_row_range_active(30, 35);
2469        assert_eq!(windings.active[0], (1 << 30) | (1 << 31));
2470        assert_eq!(windings.active[1], (1 << 0) | (1 << 1) | (1 << 2));
2471    }
2472
2473    #[test]
2474    fn test_mark_row_range_spanning_multiple_words() {
2475        let mut windings = CulledWindings::new(500);
2476        windings.mark_row_range_active(10, 80);
2477        assert_eq!(windings.active[0], u32::MAX << 10);
2478        assert_eq!(windings.active[1], u32::MAX);
2479        assert_eq!(windings.active[2], (1 << 16) - 1);
2480        assert_eq!(windings.active[3], 0);
2481    }
2482
2483    #[test]
2484    fn test_mark_row_range_empty_or_invalid() {
2485        let mut windings = CulledWindings::new(200);
2486        windings.mark_row_range_active(10, 10);
2487        windings.mark_row_range_active(15, 10);
2488        assert_eq!(windings.active[0], 0);
2489        assert_eq!(windings.active[1], 0);
2490    }
2491
2492    //==============================================================================================
2493    // Miscellaneous Cases
2494    //==============================================================================================
2495    #[test]
2496    // See https://github.com/LaurenzV/cpu-sparse-experiments/issues/46.
2497    fn infinite_loop() {
2498        let line = Line {
2499            p0: Point { x: 22.0, y: 552.0 },
2500            p1: Point { x: 224.0, y: 388.0 },
2501        };
2502
2503        let mut tiles = new_tiles();
2504        tiles.make_tiles_msaa(&[line], 600, 600);
2505        tiles.make_tiles_analytic_aa(Level::baseline(), &[line], 600, 600);
2506    }
2507
2508    #[test]
2509    // See https://github.com/linebender/vello/issues/1321
2510    fn overflow() {
2511        let line = Line {
2512            p0: Point {
2513                x: 59.60001,
2514                y: 40.78,
2515            },
2516            p1: Point {
2517                x: 520599.6,
2518                y: 100.18,
2519            },
2520        };
2521
2522        let mut tiles = new_tiles();
2523        tiles.make_tiles_analytic_aa(Level::baseline(), &[line], 200, 100);
2524        tiles.make_tiles_msaa(&[line], 200, 100);
2525    }
2526
2527    #[test]
2528    fn sort_test() {
2529        let mut lines: Vec<Line> = Vec::new();
2530        let mut tiles = Tiles::new(Level::baseline(), VIEW_DIM, VIEW_DIM);
2531
2532        let step = 4.0;
2533        let mut y = F_V_DIM - 10.0;
2534        while y > 10.0 {
2535            lines.push(Line {
2536                p0: Point {
2537                    x: F_V_DIM - 10.0,
2538                    y,
2539                },
2540                p1: Point { x: 10.0, y },
2541            });
2542
2543            lines.push(Line {
2544                p0: Point {
2545                    x: F_V_DIM - 12.0,
2546                    y,
2547                },
2548                p1: Point { x: 12.0, y },
2549            });
2550
2551            y -= step;
2552        }
2553
2554        tiles.make_tiles_msaa(&lines, VIEW_DIM, VIEW_DIM);
2555        assert!(tiles.tile_buf.first().unwrap().y > tiles.tile_buf.last().unwrap().y);
2556        tiles.sort_tiles();
2557        check_sorted(&tiles.tile_buf);
2558
2559        tiles.make_tiles_analytic_aa(Level::baseline(), &lines, VIEW_DIM, VIEW_DIM);
2560        assert!(tiles.tile_buf.first().unwrap().y > tiles.tile_buf.last().unwrap().y);
2561        tiles.sort_tiles();
2562        check_sorted(&tiles.tile_buf);
2563    }
2564
2565    fn check_sorted(buf: &[Tile]) {
2566        for i in 0..buf.len() - 1 {
2567            let current = buf[i];
2568            let next = buf[i + 1];
2569
2570            if current.y > next.y {
2571                panic!(
2572                    "Sort Failure [Y]: Tile[{}] (y={}) > Tile[{}] (y={})",
2573                    i,
2574                    current.y,
2575                    i + 1,
2576                    next.y
2577                );
2578            }
2579
2580            if current.y == next.y {
2581                if current.x > next.x {
2582                    panic!(
2583                        "Sort Failure [X]: at Row y={}, Tile[{}] (x={}) > Tile[{}] (x={})",
2584                        current.y,
2585                        i,
2586                        current.x,
2587                        i + 1,
2588                        next.x
2589                    );
2590                }
2591
2592                if current.x == next.x
2593                    && current.packed_winding_line_idx > next.packed_winding_line_idx
2594                {
2595                    panic!(
2596                        "Sort Failure [Payload]: at {}x{}, Tile[{}] (val={}) > Tile[{}] (val={})",
2597                        current.x,
2598                        current.y,
2599                        i,
2600                        current.packed_winding_line_idx,
2601                        i + 1,
2602                        next.packed_winding_line_idx
2603                    );
2604                }
2605            }
2606        }
2607    }
2608}