Skip to main content

frust_engine/compile/
clip.rs

1//! The compiler's clip stack, and the two shapes a clip lowers to.
2//!
3//! `frust_scene` has one clip stack, not two: `PushClip` and `PushClipRounded`
4//! both push onto it and one `PopClip` pops either. [`ClipStack`] is that stack
5//! inside the compiler, and its whole point is that neither shape ever needs an
6//! intermediate texture — a clip is either a device-space rectangle intersected
7//! into the enclosing one, or a coverage mask the strip generator already knows
8//! how to intersect a draw against.
9//!
10//! **Scissor.** A rectangular clip whose composed transform leaves it
11//! axis-aligned and whose edges land on whole device pixels is kept as a plain
12//! [`RectU16`], intersected with the enclosing scissor. Nothing is rasterized
13//! for it at all: once a draw's strips are generated, [`ClipStack::clip_run`]
14//! rewrites that run so no coverage outside the rectangle survives. The
15//! admission rule is [`fast_rect`]'s, deliberately — the rectangle a clip can be
16//! answered by scissoring is exactly the rectangle a fill can be answered by
17//! writing strip coverage directly.
18//!
19//! **Mask.** Every other clip — rounded corners, a rotation or skew in the
20//! transform, an edge on a half pixel — is rasterized once into a coverage mask
21//! held by [`ClipContext`], and subsequent draws are generated with that mask as
22//! their clip path. Nesting is that library's own: a mask pushed inside another
23//! is generated against the enclosing one, so the top of the stack is always the
24//! full intersection.
25//!
26//! The two compose in either order. A mask is applied by the strip generator
27//! while a draw's coverage is produced; a scissor is applied to the result. A
28//! draw under both is clipped by both, and neither lowering has to know the
29//! other happened.
30//!
31//! # Rewriting a strip run
32//!
33//! Scissoring reaches into the sparse-strip encoding rather than into geometry,
34//! so it has to respect that encoding's alignment rules. A strip's `x` and its
35//! width are both whole tiles (`vello_common`'s own `visit_strip_fill_segments`
36//! asserts as much), and one strip row covers `Tile::HEIGHT` scanlines at once.
37//! A scissor edge is only pixel-aligned, so it generally falls *inside* a tile
38//! and inside a row.
39//!
40//! The run is therefore rewritten by masking coverage rather than by moving
41//! edges: a span crossing the scissor boundary keeps its tile-aligned extent and
42//! gets a fresh run of coverage bytes with every pixel outside the rectangle set
43//! to zero. The interior of a solid span stays solid — only the tile an edge
44//! falls inside is turned into coverage — so a full-screen fill under a
45//! scrolling clip does not become a full-screen alpha buffer.
46
47use kurbo::{Affine, PathEl, Rect, RoundedRect, RoundedRectRadii, Shape};
48use peniko::Fill;
49
50use vello_common::clip::{ClipContext, PathDataRef};
51use vello_common::geometry::RectU16;
52use vello_common::strip::Strip;
53use vello_common::strip_generator::{StripGenerator, StripStorage};
54use vello_common::tile::Tile;
55
56use super::{FLATTEN_TOLERANCE, fast_rect};
57
58/// The scissor of a stack that clips nothing.
59///
60/// Deliberately the whole `u16` grid rather than the viewport: the strip
61/// pipeline addresses a viewport snapped up to whole tiles, and a draw is
62/// allowed to carry coverage into that snapped margin. Starting from the
63/// viewport would quietly clip that margin away on any frame that pushed a clip,
64/// making a clip's presence change what an unclipped draw looks like.
65const UNCLIPPED: RectU16 = RectU16::new(0, 0, u16::MAX, u16::MAX);
66
67/// Coverage of a fully covered pixel.
68const FULL_COVERAGE: u8 = 255;
69
70/// The largest alpha index a [`Strip`] can carry: its packed field reserves the
71/// top bit for the fill-gap flag.
72const MAX_ALPHA_INDEX: u32 = u32::MAX >> 1;
73
74/// One entry of the stack, naming what its matching pop has to undo.
75#[derive(Debug)]
76enum Entry {
77    /// A rectangle intersected into the scissor, carrying the scissor to put
78    /// back. Intersection is not invertible, so the previous value is stored
79    /// rather than recomputed.
80    Scissor { restore: RectU16 },
81    /// A coverage mask, popped from the mask context.
82    Mask,
83}
84
85/// One span of a draw's strip run, in the form the renderer reads back out: an
86/// alpha-sampled span, or a solid one filling the gap to the next strip.
87#[derive(Debug, Clone, Copy)]
88struct Span {
89    /// Top scanline of the strip row, a multiple of `Tile::HEIGHT`.
90    y: u16,
91    /// Left edge in pixels, a multiple of `Tile::WIDTH`.
92    x: u16,
93    /// Width in pixels, a multiple of `Tile::WIDTH`.
94    width: u16,
95    /// First coverage byte of the span, or `None` when it is solid.
96    alpha_idx: Option<u32>,
97}
98
99impl Span {
100    /// Right edge in pixels, exclusive.
101    fn x1(&self) -> u32 {
102        u32::from(self.x) + u32::from(self.width)
103    }
104}
105
106/// The compiler's clip stack: scissor rectangles and coverage masks, one stack.
107///
108/// Retained across frames like the strip generator it works beside — the mask
109/// context and the rewrite scratch are exactly the buffers a steady-state frame
110/// should be reusing. [`ClipStack::reset`] is what keeps it from carrying state
111/// between frames.
112#[derive(Debug)]
113pub struct ClipStack {
114    /// One entry per unpopped push, in push order.
115    entries: Vec<Entry>,
116    /// The coverage masks, nested by `vello_common`'s own clip context.
117    masks: ClipContext,
118    /// The intersection of every scissor currently on the stack.
119    scissor: RectU16,
120    /// Rewritten strips, staged here before replacing a draw's own run.
121    strips: Vec<Strip>,
122    /// Coverage for `strips`, offset from the run's own alpha start.
123    alphas: Vec<u8>,
124    /// Clips lowered to a scissor this frame.
125    scissor_clips: u32,
126    /// Clips lowered to a coverage mask this frame.
127    mask_clips: u32,
128    /// Strips this frame's coverage masks cost.
129    mask_strips: usize,
130}
131
132impl Default for ClipStack {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl ClipStack {
139    /// An empty stack, clipping nothing.
140    pub fn new() -> Self {
141        Self {
142            entries: Vec::new(),
143            masks: ClipContext::new(),
144            scissor: UNCLIPPED,
145            strips: Vec::new(),
146            alphas: Vec::new(),
147            scissor_clips: 0,
148            mask_clips: 0,
149            mask_strips: 0,
150        }
151    }
152
153    /// Drop every clip and counter, keeping the buffers.
154    pub fn reset(&mut self) {
155        self.entries.clear();
156        self.masks.reset();
157        self.scissor = UNCLIPPED;
158        self.scissor_clips = 0;
159        self.mask_clips = 0;
160        self.mask_strips = 0;
161    }
162
163    /// Clips lowered to a scissor this frame.
164    pub fn scissor_clips(&self) -> u32 {
165        self.scissor_clips
166    }
167
168    /// Clips lowered to a coverage mask this frame.
169    pub fn mask_clips(&self) -> u32 {
170        self.mask_clips
171    }
172
173    /// Strips this frame's coverage masks cost — zero for a frame whose clips
174    /// all scissored.
175    pub fn mask_strips(&self) -> usize {
176        self.mask_strips
177    }
178
179    /// The mask a draw generated now has to be clipped against, or `None` when
180    /// no mask is on the stack.
181    pub fn mask(&self) -> Option<PathDataRef<'_>> {
182        self.masks.get()
183    }
184
185    /// Whether the current scissor admits nothing, so a draw need not be
186    /// generated at all.
187    pub fn blocks_everything(&self) -> bool {
188        self.scissor.is_empty()
189    }
190
191    /// Push a rectangular clip: a scissor when the composed transform admits
192    /// one, a coverage mask otherwise.
193    pub fn push_rect(&mut self, rect: Rect, transform: Affine, generator: &mut StripGenerator) {
194        match fast_rect(rect, transform) {
195            Some(device) => self.push_scissor(device),
196            None => self.push_mask(rect.path_elements(FLATTEN_TOLERANCE), transform, generator),
197        }
198    }
199
200    /// Push a clip with rounded corners.
201    ///
202    /// Radii that are all square describe a plain rectangle, so such a clip
203    /// takes the rectangular path and can still scissor: a caller spelling an
204    /// unrounded clip through the rounded command pays nothing for the spelling.
205    pub fn push_rounded(
206        &mut self,
207        rect: Rect,
208        radii: RoundedRectRadii,
209        transform: Affine,
210        generator: &mut StripGenerator,
211    ) {
212        if radii_are_square(radii) {
213            self.push_rect(rect, transform, generator);
214            return;
215        }
216        let shape = RoundedRect::from_rect(rect, radii);
217        self.push_mask(shape.path_elements(FLATTEN_TOLERANCE), transform, generator);
218    }
219
220    /// Pop the most recent clip.
221    ///
222    /// A pop with nothing to pop is ignored. A display list is recorded by a
223    /// widget tree that can be unbalanced, and an unbalanced pop must not be
224    /// able to lift a clip a sibling still relies on — nor to underflow the mask
225    /// context, which would be a panic on the frame path.
226    pub fn pop(&mut self) {
227        match self.entries.pop() {
228            Some(Entry::Scissor { restore }) => self.scissor = restore,
229            Some(Entry::Mask) => self.masks.pop_clip(),
230            None => {}
231        }
232    }
233
234    fn push_scissor(&mut self, device: Rect) {
235        let restore = self.scissor;
236        self.scissor = self.scissor.intersect(device_rect(device));
237        self.entries.push(Entry::Scissor { restore });
238        self.scissor_clips = self.scissor_clips.saturating_add(1);
239    }
240
241    fn push_mask(
242        &mut self,
243        path: impl IntoIterator<Item = PathEl>,
244        transform: Affine,
245        generator: &mut StripGenerator,
246    ) {
247        self.masks
248            .push_clip(path, generator, Fill::NonZero, transform, None);
249        self.entries.push(Entry::Mask);
250        self.mask_clips = self.mask_clips.saturating_add(1);
251        self.mask_strips = self
252            .mask_strips
253            .saturating_add(self.masks.get().map_or(0, |mask| mask.strips.len()));
254    }
255
256    /// Rewrite the strip run a draw just generated so nothing outside the
257    /// scissor survives.
258    ///
259    /// `strip_start` and `alpha_start` are `storage`'s two lengths from
260    /// immediately before the draw generated, so everything past them belongs to
261    /// this draw alone and can be replaced wholesale. The rewrite reclaims the
262    /// coverage it drops — the original bytes are truncated away and only the
263    /// kept ones re-appended — so a clipped frame's alpha buffer stays
264    /// proportional to what it actually paints.
265    ///
266    /// A run that needs no change is left exactly as it was, which is the whole
267    /// unclipped case and most of the clipped one.
268    pub fn clip_run(&mut self, storage: &mut StripStorage, strip_start: usize, alpha_start: usize) {
269        if self.scissor == UNCLIPPED {
270            return;
271        }
272        let Some(run) = storage.strips.get(strip_start..) else {
273            return;
274        };
275        if !spans(run).any(|span| self.clips(&span)) {
276            return;
277        }
278
279        self.strips.clear();
280        self.alphas.clear();
281
282        let base = alpha_start.min(storage.alphas.len());
283        let scissor = self.scissor;
284        for span in spans(run) {
285            if let Some(clipped) = Clipped::of(&span, scissor) {
286                self.emit(&span, &clipped, &storage.alphas, base);
287            }
288        }
289        self.close_run(base);
290
291        storage.strips.truncate(strip_start);
292        storage.alphas.truncate(base);
293        storage.strips.extend_from_slice(&self.strips);
294        storage.alphas.extend_from_slice(&self.alphas);
295    }
296
297    /// Whether `span` would come out of the scissor changed — dropped, trimmed,
298    /// or masked.
299    fn clips(&self, span: &Span) -> bool {
300        Clipped::of(span, self.scissor).is_none_or(|clipped| !clipped.covers_all_of(span))
301    }
302
303    /// Terminate the staged run with the sentinel strip the renderer's pairwise
304    /// walk reads the last span's extent off.
305    ///
306    /// A run that staged nothing gets no sentinel: a lone sentinel is not a run,
307    /// and the caller drops an empty range rather than recording a draw for it.
308    fn close_run(&mut self, base: usize) {
309        let Some(last) = self.strips.last() else {
310            return;
311        };
312        let y = last.y;
313        let end = alpha_index(base.saturating_add(self.alphas.len()));
314        self.strips.push(Strip::sentinel(y, end));
315    }
316
317    /// Append `span`, clipped, to the staged run.
318    fn emit(&mut self, span: &Span, clipped: &Clipped, alphas: &[u8], base: usize) {
319        // A solid span keeps its whole-tile interior solid; only a tile the
320        // scissor edge falls inside has to start carrying coverage. An alpha
321        // span already pays for coverage across its width, so splitting it would
322        // buy nothing.
323        if span.alpha_idx.is_none()
324            && clipped.rows_are_whole()
325            && let Some((x0, x1)) = clipped.interior_tiles()
326        {
327            self.emit_split_solid(span, clipped, (x0, x1), alphas, base);
328            return;
329        }
330
331        let (x0, x1) = clipped.tiles();
332        self.emit_masked(span, clipped, (x0, x1), alphas, base);
333    }
334
335    /// Emit a solid span as a left boundary tile, a solid interior, and a right
336    /// boundary tile — either boundary tile may be absent, and both are when the
337    /// scissor did not cut this span at all.
338    fn emit_split_solid(
339        &mut self,
340        span: &Span,
341        clipped: &Clipped,
342        interior: (u32, u32),
343        alphas: &[u8],
344        base: usize,
345    ) {
346        let (tile_x0, tile_x1) = clipped.tiles();
347        let (interior_x0, interior_x1) = interior;
348        if tile_x0 < interior_x0 {
349            self.emit_masked(span, clipped, (tile_x0, interior_x0), alphas, base);
350        }
351        self.emit_solid(
352            pixel(interior_x0),
353            span.y,
354            pixel(interior_x1.saturating_sub(interior_x0)),
355            base,
356        );
357        if interior_x1 < tile_x1 {
358            self.emit_masked(span, clipped, (interior_x1, tile_x1), alphas, base);
359        }
360    }
361
362    /// Emit `[x0, x1)` of `span` as coverage, with every pixel the scissor
363    /// excludes zeroed.
364    ///
365    /// `[x0, x1)` is always whole tiles inside the span's own extent, which is
366    /// what keeps the emitted strip tile-aligned in both position and width
367    /// however the scissor edge falls.
368    fn emit_masked(
369        &mut self,
370        span: &Span,
371        clipped: &Clipped,
372        extent: (u32, u32),
373        alphas: &[u8],
374        base: usize,
375    ) {
376        let (x0, x1) = extent;
377        let width = x1.saturating_sub(x0);
378        if width == 0 {
379            return;
380        }
381
382        let origin = u32::from(span.x);
383        let start = alpha_index(base.saturating_add(self.alphas.len()));
384        for column in 0..width {
385            let x = x0.saturating_add(column);
386            for row in 0..u32::from(Tile::HEIGHT) {
387                let value = match span.alpha_idx {
388                    _ if !clipped.contains(x, row) => 0,
389                    None => FULL_COVERAGE,
390                    Some(idx) => coverage_at(alphas, idx, x.saturating_sub(origin), row),
391                };
392                self.alphas.push(value);
393            }
394        }
395        self.strips
396            .push(Strip::new(pixel(x0), span.y, start, false));
397    }
398
399    /// Stage a solid span of `width` pixels at `(x, y)`.
400    ///
401    /// A solid span is two strips: one of zero width opening it, and one
402    /// carrying the fill-gap flag closing it. That is how the encoding says
403    /// "fill from here to there, with no coverage to sample".
404    fn emit_solid(&mut self, x: u16, y: u16, width: u16, base: usize) {
405        if width == 0 {
406            return;
407        }
408        let index = alpha_index(base.saturating_add(self.alphas.len()));
409        self.strips.push(Strip::new(x, y, index, false));
410        self.strips
411            .push(Strip::new(x.saturating_add(width), y, index, true));
412    }
413}
414
415/// A span's surviving extent under a scissor.
416#[derive(Debug, Clone, Copy)]
417struct Clipped {
418    /// Left edge in pixels, inclusive.
419    x0: u32,
420    /// Right edge in pixels, exclusive.
421    x1: u32,
422    /// First surviving scanline of the strip row, `0..Tile::HEIGHT`.
423    row0: u32,
424    /// One past the last surviving scanline.
425    row1: u32,
426}
427
428impl Clipped {
429    /// `span` under `scissor`, or `None` when nothing of it survives.
430    fn of(span: &Span, scissor: RectU16) -> Option<Self> {
431        let top = u32::from(span.y);
432        let bottom = top.saturating_add(u32::from(Tile::HEIGHT));
433        let row0 = u32::from(scissor.y0).clamp(top, bottom) - top;
434        let row1 = u32::from(scissor.y1).clamp(top, bottom) - top;
435        if row0 >= row1 {
436            return None;
437        }
438
439        let x0 = u32::from(span.x).max(u32::from(scissor.x0));
440        let x1 = span.x1().min(u32::from(scissor.x1));
441        if x0 >= x1 {
442            return None;
443        }
444
445        Some(Self { x0, x1, row0, row1 })
446    }
447
448    /// Whether the whole span survived untouched.
449    fn covers_all_of(&self, span: &Span) -> bool {
450        self.rows_are_whole() && self.x0 == u32::from(span.x) && self.x1 == span.x1()
451    }
452
453    /// Whether every scanline of the strip row survived.
454    fn rows_are_whole(&self) -> bool {
455        self.row0 == 0 && self.row1 == u32::from(Tile::HEIGHT)
456    }
457
458    /// Whether device column `x`, scanline `row` of the strip row, is inside the
459    /// scissor.
460    fn contains(&self, x: u32, row: u32) -> bool {
461        x >= self.x0 && x < self.x1 && row >= self.row0 && row < self.row1
462    }
463
464    /// The whole tiles the surviving extent touches, in pixels.
465    fn tiles(&self) -> (u32, u32) {
466        (tile_floor(self.x0), tile_ceil(self.x1))
467    }
468
469    /// The whole tiles lying entirely inside the surviving extent, in pixels, or
470    /// `None` when the extent covers no whole tile.
471    fn interior_tiles(&self) -> Option<(u32, u32)> {
472        let x0 = tile_ceil(self.x0);
473        let x1 = tile_floor(self.x1);
474        (x0 < x1).then_some((x0, x1))
475    }
476}
477
478/// The spans a strip run describes, read exactly as the renderer reads them:
479/// each strip's own alpha-sampled extent, plus the solid extent filling the gap
480/// to the next strip when the winding between them says there is one.
481///
482/// Decoding through the same rule the renderer applies is what makes the rewrite
483/// faithful: a span this iterator does not report is a span nothing downstream
484/// would have drawn either.
485fn spans(run: &[Strip]) -> impl Iterator<Item = Span> + '_ {
486    run.windows(2)
487        .flat_map(|pair| {
488            let [strip, next] = pair else {
489                return [None, None];
490            };
491            if strip.is_sentinel() {
492                return [None, None];
493            }
494
495            let width = strip.width_to(next);
496            let alpha = (width > 0).then(|| Span {
497                y: strip.y,
498                x: strip.x,
499                width,
500                alpha_idx: Some(strip.alpha_idx()),
501            });
502
503            let gap = if next.fill_gap() && next.y == strip.y {
504                let x = strip.x.saturating_add(width);
505                let gap_width = next.x.saturating_sub(x);
506                (gap_width > 0).then_some(Span {
507                    y: strip.y,
508                    x,
509                    width: gap_width,
510                    alpha_idx: None,
511                })
512            } else {
513                None
514            };
515
516            [alpha, gap]
517        })
518        .flatten()
519}
520
521/// The coverage byte for `column`, `row` of the span starting at `alpha_idx`.
522///
523/// Coverage is stored column-major, `Tile::HEIGHT` bytes per pixel column — the
524/// same unit the strip shader reads it back in. A byte past the end of the
525/// buffer reads as uncovered rather than panicking: the frame path returns
526/// errors, and a short coverage buffer is not one of them.
527fn coverage_at(alphas: &[u8], alpha_idx: u32, column: u32, row: u32) -> u8 {
528    let offset = column
529        .saturating_mul(u32::from(Tile::HEIGHT))
530        .saturating_add(row);
531    let index = alpha_idx.saturating_add(offset) as usize;
532    alphas.get(index).copied().unwrap_or(0)
533}
534
535/// `pixels` as a strip coordinate, saturating at the grid strips address.
536fn pixel(pixels: u32) -> u16 {
537    u16::try_from(pixels).unwrap_or(u16::MAX)
538}
539
540/// `index` as a strip's alpha index, saturating below the flag bit the packed
541/// field reserves.
542fn alpha_index(index: usize) -> u32 {
543    u32::try_from(index)
544        .unwrap_or(MAX_ALPHA_INDEX)
545        .min(MAX_ALPHA_INDEX)
546}
547
548/// `x` rounded down to a tile boundary.
549fn tile_floor(x: u32) -> u32 {
550    x - x % u32::from(Tile::WIDTH)
551}
552
553/// `x` rounded up to a tile boundary.
554fn tile_ceil(x: u32) -> u32 {
555    tile_floor(x.saturating_add(u32::from(Tile::WIDTH) - 1))
556}
557
558/// Whether every corner radius describes a square corner.
559fn radii_are_square(radii: RoundedRectRadii) -> bool {
560    radii.top_left <= 0.0
561        && radii.top_right <= 0.0
562        && radii.bottom_right <= 0.0
563        && radii.bottom_left <= 0.0
564}
565
566/// A pixel-aligned device rectangle on the `u16` grid strips address.
567///
568/// The rectangle's edges are already whole numbers — that is what admitted it to
569/// the scissor path — but not necessarily small ones: a clip far outside the
570/// viewport is clamped rather than refused, which turns an enormous clip into
571/// "clips nothing" and an entirely negative one into "clips everything", both of
572/// which are what the geometry says.
573fn device_rect(rect: Rect) -> RectU16 {
574    let coordinate = |value: f64| value.clamp(0.0, f64::from(u16::MAX)) as u16;
575    RectU16::new(
576        coordinate(rect.x0),
577        coordinate(rect.y0),
578        coordinate(rect.x1),
579        coordinate(rect.y1),
580    )
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use vello_common::fearless_simd::Level;
587
588    fn generator() -> StripGenerator {
589        StripGenerator::new(64, 64, Level::baseline())
590    }
591
592    #[test]
593    fn a_run_decodes_into_its_alpha_and_solid_spans() {
594        // Four pixels of coverage at x = 0, a solid gap on to x = 12, then the
595        // sentinel closing the row.
596        let run = [
597            Strip::new(0, 0, 0, false),
598            Strip::new(12, 0, 16, true),
599            Strip::sentinel(0, 16),
600        ];
601        let decoded: Vec<Span> = spans(&run).collect();
602
603        assert_eq!(decoded.len(), 2);
604        assert_eq!((decoded[0].x, decoded[0].width), (0, 4));
605        assert_eq!(decoded[0].alpha_idx, Some(0));
606        assert_eq!((decoded[1].x, decoded[1].width), (4, 8));
607        assert_eq!(decoded[1].alpha_idx, None);
608    }
609
610    #[test]
611    fn a_span_the_scissor_misses_does_not_survive() {
612        let span = Span {
613            y: 0,
614            x: 0,
615            width: 8,
616            alpha_idx: None,
617        };
618        assert!(Clipped::of(&span, RectU16::new(16, 0, 32, 4)).is_none());
619        assert!(Clipped::of(&span, RectU16::new(0, 8, 32, 12)).is_none());
620        assert!(Clipped::of(&span, RectU16::new(0, 0, 8, 4)).is_some());
621    }
622
623    #[test]
624    fn tile_rounding_brackets_a_pixel_extent() {
625        assert_eq!(tile_floor(5), 4);
626        assert_eq!(tile_ceil(5), 8);
627        assert_eq!(tile_floor(8), 8);
628        assert_eq!(tile_ceil(8), 8);
629    }
630
631    #[test]
632    fn interior_tiles_are_only_the_whole_ones() {
633        let span = Span {
634            y: 0,
635            x: 0,
636            width: 32,
637            alpha_idx: None,
638        };
639        let wide = Clipped::of(&span, RectU16::new(5, 0, 19, 4)).expect("overlaps");
640        assert_eq!(wide.interior_tiles(), Some((8, 16)));
641
642        let narrow = Clipped::of(&span, RectU16::new(5, 0, 7, 4)).expect("overlaps");
643        assert_eq!(narrow.interior_tiles(), None);
644    }
645
646    #[test]
647    fn an_enormous_clip_rectangle_clamps_rather_than_wrapping() {
648        let huge = device_rect(Rect::new(-1e30, -1e30, 1e30, 1e30));
649        assert_eq!(huge, UNCLIPPED);
650
651        let behind = device_rect(Rect::new(-1e30, -1e30, -1e29, -1e29));
652        assert!(behind.is_empty());
653    }
654
655    #[test]
656    fn an_unbalanced_pop_leaves_the_stack_alone() {
657        let mut stack = ClipStack::new();
658        stack.pop();
659        stack.pop();
660
661        assert!(!stack.blocks_everything());
662        assert_eq!(stack.scissor_clips(), 0);
663        assert_eq!(stack.mask_clips(), 0);
664    }
665
666    #[test]
667    fn nested_scissors_intersect_and_unwind() {
668        let mut generator = generator();
669        let mut stack = ClipStack::new();
670
671        stack.push_rect(
672            Rect::new(0.0, 0.0, 40.0, 40.0),
673            Affine::IDENTITY,
674            &mut generator,
675        );
676        stack.push_rect(
677            Rect::new(20.0, 20.0, 60.0, 60.0),
678            Affine::IDENTITY,
679            &mut generator,
680        );
681        assert_eq!(stack.scissor, RectU16::new(20, 20, 40, 40));
682
683        stack.pop();
684        assert_eq!(stack.scissor, RectU16::new(0, 0, 40, 40));
685        stack.pop();
686        assert_eq!(stack.scissor, UNCLIPPED);
687
688        assert_eq!(stack.scissor_clips(), 2);
689        assert_eq!(stack.mask_strips(), 0);
690    }
691
692    #[test]
693    fn a_square_cornered_rounded_clip_still_scissors() {
694        let mut generator = generator();
695        let mut stack = ClipStack::new();
696
697        stack.push_rounded(
698            Rect::new(4.0, 4.0, 20.0, 20.0),
699            RoundedRectRadii::from_single_radius(0.0),
700            Affine::IDENTITY,
701            &mut generator,
702        );
703
704        assert_eq!(stack.scissor_clips(), 1);
705        assert_eq!(stack.mask_clips(), 0);
706        assert_eq!(stack.mask_strips(), 0);
707    }
708
709    #[test]
710    fn a_rounded_clip_rasterizes_a_mask() {
711        let mut generator = generator();
712        let mut stack = ClipStack::new();
713
714        stack.push_rounded(
715            Rect::new(4.0, 4.0, 40.0, 40.0),
716            RoundedRectRadii::from_single_radius(8.0),
717            Affine::IDENTITY,
718            &mut generator,
719        );
720
721        assert_eq!(stack.mask_clips(), 1);
722        assert!(stack.mask_strips() > 0);
723        assert!(stack.mask().is_some());
724
725        stack.pop();
726        assert!(stack.mask().is_none());
727    }
728}