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::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 /// Pop the most recent clip.
222 ///
223 /// A pop with nothing to pop is ignored. A display list is recorded by a
224 /// widget tree that can be unbalanced, and an unbalanced pop must not be
225 /// able to lift a clip a sibling still relies on — nor to underflow the mask
226 /// context, which would be a panic on the frame path.
227 pub fn pop(&mut self) {
228 match self.entries.pop() {
229 Some(Entry::Scissor { restore }) => self.scissor = restore,
230 Some(Entry::Mask) => self.masks.pop_clip(),
231 None => {}
232 }
233 }
234
235 fn push_scissor(&mut self, device: Rect) {
236 let restore = self.scissor;
237 self.scissor = self.scissor.intersect(device_rect(device));
238 self.entries.push(Entry::Scissor { restore });
239 self.scissor_clips = self.scissor_clips.saturating_add(1);
240 }
241
242 /// Rasterize `path` under `transform` into a mask nested in the current
243 /// one.
244 ///
245 /// The mask is generated against the enclosing mask, so it is culled
246 /// against that mask's bounds — or the generator's viewport when there is
247 /// none — and the path goes through the [`ViewportSplit`] pre-pass against
248 /// that same rectangle before it reaches the flattener.
249 fn push_mask(
250 &mut self,
251 path: impl IntoIterator<Item = PathEl>,
252 transform: Affine,
253 generator: &mut StripGenerator,
254 ) {
255 let viewport = cull_viewport(generator, self.masks.get().as_ref());
256 self.masks.push_clip(
257 ViewportSplit::new(path, transform, viewport),
258 generator,
259 Fill::NonZero,
260 Affine::IDENTITY,
261 None,
262 );
263 self.entries.push(Entry::Mask);
264 self.mask_clips = self.mask_clips.saturating_add(1);
265 self.mask_strips = self
266 .mask_strips
267 .saturating_add(self.masks.get().map_or(0, |mask| mask.strips.len()));
268 }
269
270 /// Rewrite the strip run a draw just generated so nothing outside the
271 /// scissor survives.
272 ///
273 /// `strip_start` and `alpha_start` are `storage`'s two lengths from
274 /// immediately before the draw generated, so everything past them belongs to
275 /// this draw alone and can be replaced wholesale. The rewrite reclaims the
276 /// coverage it drops — the original bytes are truncated away and only the
277 /// kept ones re-appended — so a clipped frame's alpha buffer stays
278 /// proportional to what it actually paints.
279 ///
280 /// A run that needs no change is left exactly as it was, which is the whole
281 /// unclipped case and most of the clipped one.
282 pub fn clip_run(&mut self, storage: &mut StripStorage, strip_start: usize, alpha_start: usize) {
283 if self.scissor == UNCLIPPED {
284 return;
285 }
286 let Some(run) = storage.strips.get(strip_start..) else {
287 return;
288 };
289 if !spans(run).any(|span| self.clips(&span)) {
290 return;
291 }
292
293 self.strips.clear();
294 self.alphas.clear();
295
296 let base = alpha_start.min(storage.alphas.len());
297 let scissor = self.scissor;
298 for span in spans(run) {
299 if let Some(clipped) = Clipped::of(&span, scissor) {
300 self.emit(&span, &clipped, &storage.alphas, base);
301 }
302 }
303 self.close_run(base);
304
305 storage.strips.truncate(strip_start);
306 storage.alphas.truncate(base);
307 storage.strips.extend_from_slice(&self.strips);
308 storage.alphas.extend_from_slice(&self.alphas);
309 }
310
311 /// Whether `span` would come out of the scissor changed — dropped, trimmed,
312 /// or masked.
313 fn clips(&self, span: &Span) -> bool {
314 Clipped::of(span, self.scissor).is_none_or(|clipped| !clipped.covers_all_of(span))
315 }
316
317 /// Terminate the staged run with the sentinel strip the renderer's pairwise
318 /// walk reads the last span's extent off.
319 ///
320 /// A run that staged nothing gets no sentinel: a lone sentinel is not a run,
321 /// and the caller drops an empty range rather than recording a draw for it.
322 fn close_run(&mut self, base: usize) {
323 let Some(last) = self.strips.last() else {
324 return;
325 };
326 let y = last.y;
327 let end = alpha_index(base.saturating_add(self.alphas.len()));
328 self.strips.push(Strip::sentinel(y, end));
329 }
330
331 /// Append `span`, clipped, to the staged run.
332 fn emit(&mut self, span: &Span, clipped: &Clipped, alphas: &[u8], base: usize) {
333 // A solid span keeps its whole-tile interior solid; only a tile the
334 // scissor edge falls inside has to start carrying coverage. An alpha
335 // span already pays for coverage across its width, so splitting it would
336 // buy nothing.
337 if span.alpha_idx.is_none()
338 && clipped.rows_are_whole()
339 && let Some((x0, x1)) = clipped.interior_tiles()
340 {
341 self.emit_split_solid(span, clipped, (x0, x1), alphas, base);
342 return;
343 }
344
345 let (x0, x1) = clipped.tiles();
346 self.emit_masked(span, clipped, (x0, x1), alphas, base);
347 }
348
349 /// Emit a solid span as a left boundary tile, a solid interior, and a right
350 /// boundary tile — either boundary tile may be absent, and both are when the
351 /// scissor did not cut this span at all.
352 fn emit_split_solid(
353 &mut self,
354 span: &Span,
355 clipped: &Clipped,
356 interior: (u32, u32),
357 alphas: &[u8],
358 base: usize,
359 ) {
360 let (tile_x0, tile_x1) = clipped.tiles();
361 let (interior_x0, interior_x1) = interior;
362 if tile_x0 < interior_x0 {
363 self.emit_masked(span, clipped, (tile_x0, interior_x0), alphas, base);
364 }
365 self.emit_solid(
366 pixel(interior_x0),
367 span.y,
368 pixel(interior_x1.saturating_sub(interior_x0)),
369 base,
370 );
371 if interior_x1 < tile_x1 {
372 self.emit_masked(span, clipped, (interior_x1, tile_x1), alphas, base);
373 }
374 }
375
376 /// Emit `[x0, x1)` of `span` as coverage, with every pixel the scissor
377 /// excludes zeroed.
378 ///
379 /// `[x0, x1)` is always whole tiles inside the span's own extent, which is
380 /// what keeps the emitted strip tile-aligned in both position and width
381 /// however the scissor edge falls.
382 fn emit_masked(
383 &mut self,
384 span: &Span,
385 clipped: &Clipped,
386 extent: (u32, u32),
387 alphas: &[u8],
388 base: usize,
389 ) {
390 let (x0, x1) = extent;
391 let width = x1.saturating_sub(x0);
392 if width == 0 {
393 return;
394 }
395
396 let origin = u32::from(span.x);
397 let start = alpha_index(base.saturating_add(self.alphas.len()));
398 for column in 0..width {
399 let x = x0.saturating_add(column);
400 for row in 0..u32::from(Tile::HEIGHT) {
401 let value = match span.alpha_idx {
402 _ if !clipped.contains(x, row) => 0,
403 None => FULL_COVERAGE,
404 Some(idx) => coverage_at(alphas, idx, x.saturating_sub(origin), row),
405 };
406 self.alphas.push(value);
407 }
408 }
409 self.strips
410 .push(Strip::new(pixel(x0), span.y, start, false));
411 }
412
413 /// Stage a solid span of `width` pixels at `(x, y)`.
414 ///
415 /// A solid span is two strips: one of zero width opening it, and one
416 /// carrying the fill-gap flag closing it. That is how the encoding says
417 /// "fill from here to there, with no coverage to sample".
418 fn emit_solid(&mut self, x: u16, y: u16, width: u16, base: usize) {
419 if width == 0 {
420 return;
421 }
422 let index = alpha_index(base.saturating_add(self.alphas.len()));
423 self.strips.push(Strip::new(x, y, index, false));
424 self.strips
425 .push(Strip::new(x.saturating_add(width), y, index, true));
426 }
427}
428
429/// A span's surviving extent under a scissor.
430#[derive(Debug, Clone, Copy)]
431struct Clipped {
432 /// Left edge in pixels, inclusive.
433 x0: u32,
434 /// Right edge in pixels, exclusive.
435 x1: u32,
436 /// First surviving scanline of the strip row, `0..Tile::HEIGHT`.
437 row0: u32,
438 /// One past the last surviving scanline.
439 row1: u32,
440}
441
442impl Clipped {
443 /// `span` under `scissor`, or `None` when nothing of it survives.
444 fn of(span: &Span, scissor: RectU16) -> Option<Self> {
445 let top = u32::from(span.y);
446 let bottom = top.saturating_add(u32::from(Tile::HEIGHT));
447 let row0 = u32::from(scissor.y0).clamp(top, bottom) - top;
448 let row1 = u32::from(scissor.y1).clamp(top, bottom) - top;
449 if row0 >= row1 {
450 return None;
451 }
452
453 let x0 = u32::from(span.x).max(u32::from(scissor.x0));
454 let x1 = span.x1().min(u32::from(scissor.x1));
455 if x0 >= x1 {
456 return None;
457 }
458
459 Some(Self { x0, x1, row0, row1 })
460 }
461
462 /// Whether the whole span survived untouched.
463 fn covers_all_of(&self, span: &Span) -> bool {
464 self.rows_are_whole() && self.x0 == u32::from(span.x) && self.x1 == span.x1()
465 }
466
467 /// Whether every scanline of the strip row survived.
468 fn rows_are_whole(&self) -> bool {
469 self.row0 == 0 && self.row1 == u32::from(Tile::HEIGHT)
470 }
471
472 /// Whether device column `x`, scanline `row` of the strip row, is inside the
473 /// scissor.
474 fn contains(&self, x: u32, row: u32) -> bool {
475 x >= self.x0 && x < self.x1 && row >= self.row0 && row < self.row1
476 }
477
478 /// The whole tiles the surviving extent touches, in pixels.
479 fn tiles(&self) -> (u32, u32) {
480 (tile_floor(self.x0), tile_ceil(self.x1))
481 }
482
483 /// The whole tiles lying entirely inside the surviving extent, in pixels, or
484 /// `None` when the extent covers no whole tile.
485 fn interior_tiles(&self) -> Option<(u32, u32)> {
486 let x0 = tile_ceil(self.x0);
487 let x1 = tile_floor(self.x1);
488 (x0 < x1).then_some((x0, x1))
489 }
490}
491
492/// The spans a strip run describes, read exactly as the renderer reads them:
493/// each strip's own alpha-sampled extent, plus the solid extent filling the gap
494/// to the next strip when the winding between them says there is one.
495///
496/// Decoding through the same rule the renderer applies is what makes the rewrite
497/// faithful: a span this iterator does not report is a span nothing downstream
498/// would have drawn either.
499fn spans(run: &[Strip]) -> impl Iterator<Item = Span> + '_ {
500 run.windows(2)
501 .flat_map(|pair| {
502 let [strip, next] = pair else {
503 return [None, None];
504 };
505 if strip.is_sentinel() {
506 return [None, None];
507 }
508
509 let width = strip.width_to(next);
510 let alpha = (width > 0).then(|| Span {
511 y: strip.y,
512 x: strip.x,
513 width,
514 alpha_idx: Some(strip.alpha_idx()),
515 });
516
517 let gap = if next.fill_gap() && next.y == strip.y {
518 let x = strip.x.saturating_add(width);
519 let gap_width = next.x.saturating_sub(x);
520 (gap_width > 0).then_some(Span {
521 y: strip.y,
522 x,
523 width: gap_width,
524 alpha_idx: None,
525 })
526 } else {
527 None
528 };
529
530 [alpha, gap]
531 })
532 .flatten()
533}
534
535/// The coverage byte for `column`, `row` of the span starting at `alpha_idx`.
536///
537/// Coverage is stored column-major, `Tile::HEIGHT` bytes per pixel column — the
538/// same unit the strip shader reads it back in. A byte past the end of the
539/// buffer reads as uncovered rather than panicking: the frame path returns
540/// errors, and a short coverage buffer is not one of them.
541fn coverage_at(alphas: &[u8], alpha_idx: u32, column: u32, row: u32) -> u8 {
542 let offset = column
543 .saturating_mul(u32::from(Tile::HEIGHT))
544 .saturating_add(row);
545 let index = alpha_idx.saturating_add(offset) as usize;
546 alphas.get(index).copied().unwrap_or(0)
547}
548
549/// `pixels` as a strip coordinate, saturating at the grid strips address.
550fn pixel(pixels: u32) -> u16 {
551 u16::try_from(pixels).unwrap_or(u16::MAX)
552}
553
554/// `index` as a strip's alpha index, saturating below the flag bit the packed
555/// field reserves.
556fn alpha_index(index: usize) -> u32 {
557 u32::try_from(index)
558 .unwrap_or(MAX_ALPHA_INDEX)
559 .min(MAX_ALPHA_INDEX)
560}
561
562/// `x` rounded down to a tile boundary.
563fn tile_floor(x: u32) -> u32 {
564 x - x % u32::from(Tile::WIDTH)
565}
566
567/// `x` rounded up to a tile boundary.
568fn tile_ceil(x: u32) -> u32 {
569 tile_floor(x.saturating_add(u32::from(Tile::WIDTH) - 1))
570}
571
572/// Whether every corner radius describes a square corner.
573fn radii_are_square(radii: RoundedRectRadii) -> bool {
574 radii.top_left <= 0.0
575 && radii.top_right <= 0.0
576 && radii.bottom_right <= 0.0
577 && radii.bottom_left <= 0.0
578}
579
580/// A pixel-aligned device rectangle on the `u16` grid strips address.
581///
582/// The rectangle's edges are already whole numbers — that is what admitted it to
583/// the scissor path — but not necessarily small ones: a clip far outside the
584/// viewport is clamped rather than refused, which turns an enormous clip into
585/// "clips nothing" and an entirely negative one into "clips everything", both of
586/// which are what the geometry says.
587fn device_rect(rect: Rect) -> RectU16 {
588 let coordinate = |value: f64| value.clamp(0.0, f64::from(u16::MAX)) as u16;
589 RectU16::new(
590 coordinate(rect.x0),
591 coordinate(rect.y0),
592 coordinate(rect.x1),
593 coordinate(rect.y1),
594 )
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use vello_common::fearless_simd::Level;
601
602 fn generator() -> StripGenerator {
603 StripGenerator::new(64, 64, Level::baseline())
604 }
605
606 #[test]
607 fn a_run_decodes_into_its_alpha_and_solid_spans() {
608 // Four pixels of coverage at x = 0, a solid gap on to x = 12, then the
609 // sentinel closing the row.
610 let run = [
611 Strip::new(0, 0, 0, false),
612 Strip::new(12, 0, 16, true),
613 Strip::sentinel(0, 16),
614 ];
615 let decoded: Vec<Span> = spans(&run).collect();
616
617 assert_eq!(decoded.len(), 2);
618 assert_eq!((decoded[0].x, decoded[0].width), (0, 4));
619 assert_eq!(decoded[0].alpha_idx, Some(0));
620 assert_eq!((decoded[1].x, decoded[1].width), (4, 8));
621 assert_eq!(decoded[1].alpha_idx, None);
622 }
623
624 #[test]
625 fn a_span_the_scissor_misses_does_not_survive() {
626 let span = Span {
627 y: 0,
628 x: 0,
629 width: 8,
630 alpha_idx: None,
631 };
632 assert!(Clipped::of(&span, RectU16::new(16, 0, 32, 4)).is_none());
633 assert!(Clipped::of(&span, RectU16::new(0, 8, 32, 12)).is_none());
634 assert!(Clipped::of(&span, RectU16::new(0, 0, 8, 4)).is_some());
635 }
636
637 #[test]
638 fn tile_rounding_brackets_a_pixel_extent() {
639 assert_eq!(tile_floor(5), 4);
640 assert_eq!(tile_ceil(5), 8);
641 assert_eq!(tile_floor(8), 8);
642 assert_eq!(tile_ceil(8), 8);
643 }
644
645 #[test]
646 fn interior_tiles_are_only_the_whole_ones() {
647 let span = Span {
648 y: 0,
649 x: 0,
650 width: 32,
651 alpha_idx: None,
652 };
653 let wide = Clipped::of(&span, RectU16::new(5, 0, 19, 4)).expect("overlaps");
654 assert_eq!(wide.interior_tiles(), Some((8, 16)));
655
656 let narrow = Clipped::of(&span, RectU16::new(5, 0, 7, 4)).expect("overlaps");
657 assert_eq!(narrow.interior_tiles(), None);
658 }
659
660 #[test]
661 fn an_enormous_clip_rectangle_clamps_rather_than_wrapping() {
662 let huge = device_rect(Rect::new(-1e30, -1e30, 1e30, 1e30));
663 assert_eq!(huge, UNCLIPPED);
664
665 let behind = device_rect(Rect::new(-1e30, -1e30, -1e29, -1e29));
666 assert!(behind.is_empty());
667 }
668
669 #[test]
670 fn an_unbalanced_pop_leaves_the_stack_alone() {
671 let mut stack = ClipStack::new();
672 stack.pop();
673 stack.pop();
674
675 assert!(!stack.blocks_everything());
676 assert_eq!(stack.scissor_clips(), 0);
677 assert_eq!(stack.mask_clips(), 0);
678 }
679
680 #[test]
681 fn nested_scissors_intersect_and_unwind() {
682 let mut generator = generator();
683 let mut stack = ClipStack::new();
684
685 stack.push_rect(
686 Rect::new(0.0, 0.0, 40.0, 40.0),
687 Affine::IDENTITY,
688 &mut generator,
689 );
690 stack.push_rect(
691 Rect::new(20.0, 20.0, 60.0, 60.0),
692 Affine::IDENTITY,
693 &mut generator,
694 );
695 assert_eq!(stack.scissor, RectU16::new(20, 20, 40, 40));
696
697 stack.pop();
698 assert_eq!(stack.scissor, RectU16::new(0, 0, 40, 40));
699 stack.pop();
700 assert_eq!(stack.scissor, UNCLIPPED);
701
702 assert_eq!(stack.scissor_clips(), 2);
703 assert_eq!(stack.mask_strips(), 0);
704 }
705
706 #[test]
707 fn a_square_cornered_rounded_clip_still_scissors() {
708 let mut generator = generator();
709 let mut stack = ClipStack::new();
710
711 stack.push_rounded(
712 Rect::new(4.0, 4.0, 20.0, 20.0),
713 RoundedRectRadii::from_single_radius(0.0),
714 Affine::IDENTITY,
715 &mut generator,
716 );
717
718 assert_eq!(stack.scissor_clips(), 1);
719 assert_eq!(stack.mask_clips(), 0);
720 assert_eq!(stack.mask_strips(), 0);
721 }
722
723 #[test]
724 fn a_rounded_clip_rasterizes_a_mask() {
725 let mut generator = generator();
726 let mut stack = ClipStack::new();
727
728 stack.push_rounded(
729 Rect::new(4.0, 4.0, 40.0, 40.0),
730 RoundedRectRadii::from_single_radius(8.0),
731 Affine::IDENTITY,
732 &mut generator,
733 );
734
735 assert_eq!(stack.mask_clips(), 1);
736 assert!(stack.mask_strips() > 0);
737 assert!(stack.mask().is_some());
738
739 stack.pop();
740 assert!(stack.mask().is_none());
741 }
742}