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