vello_common/
coarse.rs

1// Copyright 2025 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Generating and processing wide tiles.
5
6use crate::color::palette::css::TRANSPARENT;
7use crate::mask::Mask;
8use crate::paint::{Paint, PremulColor};
9use crate::peniko::{BlendMode, Compose, Mix};
10use crate::{strip::Strip, tile::Tile};
11use alloc::vec;
12use alloc::{boxed::Box, vec::Vec};
13
14#[derive(Debug)]
15struct Layer {
16    /// Whether the layer has a clip associated with it.
17    clip: bool,
18    /// The blend mode with which this layer should be blended into
19    /// the previous layer.
20    blend_mode: BlendMode,
21    /// An opacity to apply to the whole layer before blending it
22    /// into the backdrop.
23    opacity: f32,
24    /// A mask to apply to the layer before blending it back into
25    /// the backdrop.
26    mask: Option<Mask>,
27}
28
29impl Layer {
30    /// Whether the layer actually requires allocating a new scratch buffer
31    /// for drawing its contents.
32    fn needs_buf(&self) -> bool {
33        self.blend_mode.mix != Mix::Normal
34            || self.blend_mode.compose != Compose::SrcOver
35            || self.opacity != 1.0
36            || self.mask.is_some()
37            || !self.clip
38    }
39}
40
41/// `MODE_CPU` allows compile time optimizations to be applied to wide tile draw command generation
42/// specific to `vello_cpu`.
43pub const MODE_CPU: u8 = 0;
44/// `MODE_HYBRID` allows compile time optimizations to be applied to wide tile draw command
45/// generation specific for `vello_hybrid`.
46pub const MODE_HYBRID: u8 = 1;
47
48/// A container for wide tiles.
49#[derive(Debug)]
50pub struct Wide<const MODE: u8 = MODE_CPU> {
51    /// The width of the container.
52    pub width: u16,
53    /// The height of the container.
54    pub height: u16,
55    /// The wide tiles in the container.
56    pub tiles: Vec<WideTile<MODE>>,
57    /// The stack of layers.
58    layer_stack: Vec<Layer>,
59    /// The stack of active clip regions.
60    clip_stack: Vec<Clip>,
61}
62
63/// A clip region.
64#[derive(Debug)]
65struct Clip {
66    /// The intersected bounding box after clip
67    pub clip_bbox: Bbox,
68    /// The rendered path in sparse strip representation
69    pub strips: Box<[Strip]>,
70    #[cfg(feature = "multithreading")]
71    pub thread_idx: u8,
72}
73
74/// A bounding box
75///
76/// The first two values represent the x0 and y0 coordinates, respectively.
77/// The last two values represent the x1 and y1 coordinates, respectively.
78///  x0, y0 — the top-left corner of the bounding box,
79///  x1, y1 — the bottom-right corner of the bounding box.   
80#[derive(Debug, Clone)]
81struct Bbox {
82    pub bbox: [u16; 4],
83}
84
85impl Bbox {
86    pub(crate) fn new(bbox: [u16; 4]) -> Self {
87        Self { bbox }
88    }
89
90    /// Get the x0 coordinate of the bounding box.
91    #[inline]
92    pub(crate) fn x0(&self) -> u16 {
93        self.bbox[0]
94    }
95
96    /// Get the y0 coordinate of the bounding box.
97    #[inline]
98    pub(crate) fn y0(&self) -> u16 {
99        self.bbox[1]
100    }
101
102    /// Get the x1 coordinate of the bounding box.
103    #[inline]
104    pub(crate) fn x1(&self) -> u16 {
105        self.bbox[2]
106    }
107
108    /// Get the y1 coordinate of the bounding box.
109    #[inline]
110    pub(crate) fn y1(&self) -> u16 {
111        self.bbox[3]
112    }
113
114    /// Create an empty bounding box (zero area).
115    #[inline]
116    pub(crate) fn empty() -> Self {
117        Self::new([0, 0, 0, 0])
118    }
119
120    /// Calculate the intersection of this bounding box with another.
121    #[inline]
122    pub(crate) fn intersect(&self, other: &Self) -> Self {
123        Self::new([
124            self.x0().max(other.x0()),
125            self.y0().max(other.y0()),
126            self.x1().min(other.x1()),
127            self.y1().min(other.y1()),
128        ])
129    }
130}
131
132impl Wide<MODE_CPU> {
133    /// Create a new container for wide tiles.
134    pub fn new(width: u16, height: u16) -> Self {
135        Self::new_internal(width, height)
136    }
137}
138
139impl Wide<MODE_HYBRID> {
140    /// Create a new container for wide tiles.
141    pub fn new(width: u16, height: u16) -> Self {
142        Self::new_internal(width, height)
143    }
144}
145
146impl<const MODE: u8> Wide<MODE> {
147    /// Create a new container for wide tiles.
148    fn new_internal(width: u16, height: u16) -> Self {
149        let width_tiles = width.div_ceil(WideTile::WIDTH);
150        let height_tiles = height.div_ceil(Tile::HEIGHT);
151        let mut tiles = Vec::with_capacity(usize::from(width_tiles) * usize::from(height_tiles));
152
153        for h in 0..height_tiles {
154            for w in 0..width_tiles {
155                tiles.push(WideTile::<MODE>::new_internal(
156                    w * WideTile::WIDTH,
157                    h * Tile::HEIGHT,
158                ));
159            }
160        }
161
162        Self {
163            tiles,
164            width,
165            height,
166            layer_stack: vec![],
167            clip_stack: vec![],
168        }
169    }
170
171    /// Whether there are any existing layers that haven't been popped yet.
172    pub fn has_layers(&self) -> bool {
173        !self.layer_stack.is_empty()
174    }
175
176    /// Reset all tiles in the container.
177    pub fn reset(&mut self) {
178        for tile in &mut self.tiles {
179            tile.bg = PremulColor::from_alpha_color(TRANSPARENT);
180            tile.cmds.clear();
181        }
182        self.layer_stack.clear();
183        self.clip_stack.clear();
184    }
185
186    /// Return the number of horizontal tiles.
187    pub fn width_tiles(&self) -> u16 {
188        self.width.div_ceil(WideTile::WIDTH)
189    }
190
191    /// Return the number of vertical tiles.
192    pub fn height_tiles(&self) -> u16 {
193        self.height.div_ceil(Tile::HEIGHT)
194    }
195
196    /// Get the wide tile at a certain index.
197    ///
198    /// Panics if the index is out-of-range.
199    pub fn get(&self, x: u16, y: u16) -> &WideTile<MODE> {
200        assert!(
201            x < self.width_tiles() && y < self.height_tiles(),
202            "attempted to access out-of-bounds wide tile"
203        );
204
205        &self.tiles[usize::from(y) * usize::from(self.width_tiles()) + usize::from(x)]
206    }
207
208    /// Get mutable access to the wide tile at a certain index.
209    ///
210    /// Panics if the index is out-of-range.
211    pub fn get_mut(&mut self, x: u16, y: u16) -> &mut WideTile<MODE> {
212        assert!(
213            x < self.width_tiles() && y < self.height_tiles(),
214            "attempted to access out-of-bounds wide tile"
215        );
216
217        let idx = usize::from(y) * usize::from(self.width_tiles()) + usize::from(x);
218        &mut self.tiles[idx]
219    }
220
221    /// Return a reference to all wide tiles.
222    pub fn tiles(&self) -> &[WideTile<MODE>] {
223        self.tiles.as_slice()
224    }
225
226    /// Generate wide tile commands from the strip buffer.
227    ///
228    /// This method processes a buffer of strips that represent a path, applies the fill rule,
229    /// and generates appropriate drawing commands for each affected wide tile.
230    ///
231    /// # Algorithm overview:
232    /// 1. For each strip in the buffer:
233    ///    - Calculate its position and width in pixels
234    ///    - Determine which wide tiles the strip intersects
235    ///    - Generate alpha fill commands for the intersected wide tiles
236    /// 2. For active fill regions (determined by fill rule):
237    ///    - Generate solid fill commands for the regions between strips
238    pub fn generate(&mut self, strip_buf: &[Strip], paint: Paint, thread_idx: u8) {
239        if strip_buf.is_empty() {
240            return;
241        }
242
243        // Prevent unused warning.
244        let _ = thread_idx;
245
246        // Get current clip bounding box or full viewport if no clip is active
247        let bbox = self.get_bbox();
248
249        for i in 0..strip_buf.len() - 1 {
250            let strip = &strip_buf[i];
251
252            debug_assert!(
253                strip.y < self.height,
254                "Strips below the viewport should have been culled prior to this stage."
255            );
256
257            // Don't render strips that are outside the viewport width
258            if strip.x >= self.width {
259                continue;
260            }
261
262            let next_strip = &strip_buf[i + 1];
263            let x0 = strip.x;
264            let strip_y = strip.strip_y();
265
266            // Skip strips outside the current clip bounding box
267            if strip_y < bbox.y0() {
268                continue;
269            }
270            if strip_y >= bbox.y1() {
271                // The rest of our strips must be outside the clip, so we can break early.
272                break;
273            }
274
275            // Calculate the width of the strip in columns
276            let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
277            let next_col = next_strip.alpha_idx() / u32::from(Tile::HEIGHT);
278            // Can potentially be 0 if strip only changes winding without covering pixels
279            let strip_width = next_col.saturating_sub(col) as u16;
280            let x1 = x0.saturating_add(strip_width);
281
282            // Calculate which wide tiles this strip intersects
283            let wtile_x0 = (x0 / WideTile::WIDTH).max(bbox.x0());
284            // It's possible that a strip extends into a new wide tile, but we don't actually
285            // have as many wide tiles (e.g. because the pixmap width is only 512, but
286            // strip ends at 513), so take the minimum between the rounded values and `width_tiles`.
287            let wtile_x1 = x1
288                .div_ceil(WideTile::WIDTH)
289                .min(bbox.x1())
290                .min(WideTile::MAX_WIDE_TILE_COORD);
291
292            // Adjust column starting position if needed to respect clip boundaries
293            let mut x = x0;
294            let clip_x = bbox.x0() * WideTile::WIDTH;
295            if clip_x > x {
296                col += u32::from(clip_x - x);
297                x = clip_x;
298            }
299
300            // Generate alpha fill commands for each wide tile intersected by this strip
301            for wtile_x in wtile_x0..wtile_x1 {
302                let x_wtile_rel = x % WideTile::WIDTH;
303                // Restrict the width of the fill to the width of the wide tile
304                let width = x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
305                let cmd = CmdAlphaFill {
306                    x: x_wtile_rel,
307                    width,
308                    alpha_idx: (col * u32::from(Tile::HEIGHT)) as usize,
309                    #[cfg(feature = "multithreading")]
310                    thread_idx,
311                    paint: paint.clone(),
312                    blend_mode: None,
313                };
314                x += width;
315                col += u32::from(width);
316                self.get_mut(wtile_x, strip_y).strip(cmd);
317            }
318
319            // Determine if the region between this strip and the next should be filled.
320            let active_fill = next_strip.fill_gap();
321
322            // If region should be filled and both strips are on the same row,
323            // generate fill commands for the region between them
324            if active_fill && strip_y == next_strip.strip_y() {
325                // Clamp the fill to the clip bounding box
326                x = x1.max(bbox.x0() * WideTile::WIDTH);
327                let x2 = next_strip.x.min(
328                    self.width
329                        .checked_next_multiple_of(WideTile::WIDTH)
330                        .unwrap_or(u16::MAX),
331                );
332                let wfxt0 = (x1 / WideTile::WIDTH).max(bbox.x0());
333                let wfxt1 = x2
334                    .div_ceil(WideTile::WIDTH)
335                    .min(bbox.x1())
336                    .min(WideTile::MAX_WIDE_TILE_COORD);
337
338                // Generate fill commands for each wide tile in the fill region
339                for wtile_x in wfxt0..wfxt1 {
340                    let x_wtile_rel = x % WideTile::WIDTH;
341                    let width = x2.min(
342                        (wtile_x
343                            .checked_add(1)
344                            .unwrap_or(WideTile::MAX_WIDE_TILE_COORD))
345                            * WideTile::WIDTH,
346                    ) - x;
347                    x += width;
348                    self.get_mut(wtile_x, strip_y)
349                        .fill(x_wtile_rel, width, paint.clone());
350                }
351            }
352        }
353    }
354
355    /// Push a new layer with the given properties.
356    pub fn push_layer(
357        &mut self,
358        clip_path: Option<impl Into<Box<[Strip]>>>,
359        blend_mode: BlendMode,
360        mask: Option<Mask>,
361        opacity: f32,
362        thread_idx: u8,
363    ) {
364        // Some explanations about what is going on here: We support the concept of
365        // layers, where a user can push a new layer (with certain properties), draw some
366        // stuff, and finally pop the layer, as part of which the layer as a whole will be
367        // blended into the previous layer.
368        // There are 3 "straightforward" properties that can be set for each layer:
369        // 1) The blend mode that should be used to blend the layer into the backdrop.
370        // 2) A mask that will be applied to the whole layer in the very end before blending.
371        // 3) An optional opacity that will be applied to the whole layer before blending (this
372        //    could in theory be simulated with an alpha mask, but since it's a common operation and
373        //    we only have a single opacity, this can easily be optimized.
374        //
375        // Finally, you can also add a clip path to the layer. However, clipping has its own
376        // more complicated logic for pushing/popping buffers where drawing is also suppressed
377        // in clipped-out wide tiles. Because of this, in case we have one of the above properties
378        // AND a clipping path, we will actually end up pushing two buffers, the first one handles
379        // the three properties and the second one is just for clip paths. That is a bit wasteful
380        // and I believe it should be possible to process them all in just one go, but for now
381        // this is good enough, and it allows us to implement blending without too deep changes to
382        // the original clipping implementation.
383
384        let layer = Layer {
385            clip: clip_path.is_some(),
386            blend_mode,
387            opacity,
388            mask,
389        };
390
391        let needs_buf = layer.needs_buf();
392
393        // In case we do blending, masking or opacity, push one buffer per wide tile.
394        if needs_buf {
395            for x in 0..self.width_tiles() {
396                for y in 0..self.height_tiles() {
397                    self.get_mut(x, y).push_buf();
398                }
399            }
400        }
401
402        // If we have a clip path, push another buffer in the affected wide tiles.
403        // Note that it is important that we FIRST push the buffer for blending etc. and
404        // only then for clipping, otherwise we will use the empty clip buffer as the backdrop
405        // for blending!
406        if let Some(clip) = clip_path {
407            self.push_clip(clip, thread_idx);
408        }
409
410        self.layer_stack.push(layer);
411    }
412
413    /// Pop a previously pushed layer.
414    pub fn pop_layer(&mut self) {
415        // This method basically unwinds everything we did in `push_layer`.
416
417        let layer = self.layer_stack.pop().unwrap();
418
419        if layer.clip {
420            self.pop_clip();
421        }
422
423        let needs_buf = layer.needs_buf();
424
425        if needs_buf {
426            for x in 0..self.width_tiles() {
427                for y in 0..self.height_tiles() {
428                    let t = self.get_mut(x, y);
429
430                    if let Some(mask) = layer.mask.clone() {
431                        t.mask(mask);
432                    }
433                    t.opacity(layer.opacity);
434                    t.blend(layer.blend_mode);
435                    t.pop_buf();
436                }
437            }
438        }
439    }
440
441    /// Adds a clipping region defined by the provided strips.
442    ///
443    /// This method takes a vector of strips representing a clip path, calculates the
444    /// intersection with the current clip region, and updates the clip stack.
445    ///
446    /// # Algorithm overview:
447    /// 1. Calculate bounding box of the clip path
448    /// 2. Intersect with current clip bounding box
449    /// 3. For each tile in the intersected bounding box:
450    ///    - If covered by zero winding: `push_zero_clip`
451    ///    - If fully covered by non-zero winding: do nothing (clip is a no-op)
452    ///    - If partially covered: `push_clip`
453    pub fn push_clip(&mut self, strips: impl Into<Box<[Strip]>>, thread_idx: u8) {
454        let strips = strips.into();
455        let n_strips = strips.len();
456
457        // Calculate the bounding box of the clip path in strip coordinates
458        let path_bbox = if n_strips <= 1 {
459            Bbox::empty()
460        } else {
461            // Calculate the y range from first to last strip in wide tile coordinates
462            let wtile_y0 = strips[0].strip_y();
463            let wtile_y1 = strips[n_strips.saturating_sub(1)].strip_y() + 1;
464
465            // Calculate the x range by examining all strips in wide tile coordinates
466            let mut wtile_x0 = strips[0].x / WideTile::WIDTH;
467            let mut wtile_x1 = wtile_x0;
468            for i in 0..n_strips.saturating_sub(1) {
469                let strip = &strips[i];
470                let next_strip = &strips[i + 1];
471                let width =
472                    ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
473                let x = strip.x;
474                wtile_x0 = wtile_x0.min(x / WideTile::WIDTH);
475                wtile_x1 = wtile_x1.max((x + width).div_ceil(WideTile::WIDTH));
476            }
477            Bbox::new([wtile_x0, wtile_y0, wtile_x1, wtile_y1])
478        };
479
480        let parent_bbox = self.get_bbox();
481        // Calculate the intersection of the parent clip bounding box and the path bounding box.
482        let clip_bbox = parent_bbox.intersect(&path_bbox);
483
484        let mut cur_wtile_x = clip_bbox.x0();
485        let mut cur_wtile_y = clip_bbox.y0();
486
487        // Process strips to determine the clipping state for each wide tile
488        for i in 0..n_strips.saturating_sub(1) {
489            let strip = &strips[i];
490            let strip_y = strip.strip_y();
491
492            // Skip strips before current wide tile row
493            if strip_y < cur_wtile_y {
494                continue;
495            }
496
497            // Process wide tiles in rows before this strip's row
498            // These wide tiles are all zero-winding (outside the path)
499            while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
500                for wtile_x in cur_wtile_x..clip_bbox.x1() {
501                    self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
502                }
503                // Reset x to the left edge of the clip bounding box
504                cur_wtile_x = clip_bbox.x0();
505                // Move to the next row
506                cur_wtile_y += 1;
507            }
508
509            // If we've reached the bottom of the clip bounding box, stop processing.
510            // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
511            // is zero-area (see issue 1072).
512            if cur_wtile_y >= clip_bbox.y1() {
513                break;
514            }
515
516            // Process wide tiles to the left of this strip in the same row
517            let x = strip.x;
518            let wtile_x_clamped = (x / WideTile::WIDTH).min(clip_bbox.x1());
519            if cur_wtile_x < wtile_x_clamped {
520                // If winding is zero or doesn't match fill rule, these wide tiles are outside the path
521                let is_inside = strip.fill_gap();
522                if !is_inside {
523                    for wtile_x in cur_wtile_x..wtile_x_clamped {
524                        self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
525                    }
526                }
527                // If winding is nonzero, then wide tiles covered entirely
528                // by sparse fill are no-op (no clipping is applied).
529                cur_wtile_x = wtile_x_clamped;
530            }
531
532            // Process wide tiles covered by the strip - these need actual clipping
533            let next_strip = &strips[i + 1];
534            let width =
535                ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
536            let wtile_x1 = (x + width).div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
537            if cur_wtile_x < wtile_x1 {
538                for wtile_x in cur_wtile_x..wtile_x1 {
539                    self.get_mut(wtile_x, cur_wtile_y).push_clip();
540                }
541                cur_wtile_x = wtile_x1;
542            }
543        }
544
545        // Process any remaining wide tiles in the bounding box (all zero-winding)
546        while cur_wtile_y < clip_bbox.y1() {
547            for wtile_x in cur_wtile_x..clip_bbox.x1() {
548                self.get_mut(wtile_x, cur_wtile_y).push_zero_clip();
549            }
550            cur_wtile_x = clip_bbox.x0();
551            cur_wtile_y += 1;
552        }
553
554        // Prevent unused warning.
555        let _ = thread_idx;
556
557        self.clip_stack.push(Clip {
558            clip_bbox,
559            strips,
560            #[cfg(feature = "multithreading")]
561            thread_idx,
562        });
563    }
564
565    /// Get the bounding box of the current clip region or the entire viewport if no clip regions are active.
566    fn get_bbox(&self) -> Bbox {
567        if let Some(top) = self.clip_stack.last() {
568            top.clip_bbox.clone()
569        } else {
570            // Convert pixel dimensions to wide tile coordinates
571            Bbox::new([0, 0, self.width_tiles(), self.height_tiles()])
572        }
573    }
574
575    /// Removes the most recently added clip region.
576    ///
577    /// This is the inverse operation of `push_clip`, carefully undoing all the clipping
578    /// operations while also handling any rendering needed for the clip region itself.
579    ///
580    /// # Algorithm overview:
581    /// 1. Retrieve the top clip from the stack
582    /// 2. For each wide tile in the clip's bounding box:
583    ///    - If covered by zero winding: `pop_zero_clip`
584    ///    - If fully covered by non-zero winding: do nothing (was no-op)
585    ///    - If partially covered: render the clip and `pop_clip`
586    ///
587    /// This operation must be symmetric with `push_clip` to maintain a balanced clip stack.
588    fn pop_clip(&mut self) {
589        let Clip {
590            clip_bbox,
591            strips,
592            #[cfg(feature = "multithreading")]
593            thread_idx,
594        } = self.clip_stack.pop().unwrap();
595        let n_strips = strips.len();
596
597        let mut cur_wtile_x = clip_bbox.x0();
598        let mut cur_wtile_y = clip_bbox.y0();
599        let mut pop_pending = false;
600
601        // Process each strip to determine the clipping state for each tile
602        for i in 0..n_strips.saturating_sub(1) {
603            let strip = &strips[i];
604            let strip_y = strip.strip_y();
605
606            // Skip strips before current tile row
607            if strip_y < cur_wtile_y {
608                continue;
609            }
610
611            // Process tiles in rows before this strip's row
612            // These tiles all had zero-winding clips
613            while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
614                // Handle any pending clip pop from previous iteration
615                if core::mem::take(&mut pop_pending) {
616                    self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
617                    cur_wtile_x += 1;
618                }
619
620                // Pop zero clips for all remaining tiles in this row
621                for wtile_x in cur_wtile_x..clip_bbox.x1() {
622                    self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
623                }
624                cur_wtile_x = clip_bbox.x0();
625                cur_wtile_y += 1;
626            }
627
628            // If we've reached the bottom of the clip bounding box, stop processing
629            // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
630            // is zero-area (see issue 1072).
631            if cur_wtile_y >= clip_bbox.y1() {
632                break;
633            }
634
635            // Process tiles to the left of this strip in the same row
636            let x0 = strip.x;
637            let wtile_x_clamped = (x0 / WideTile::WIDTH).min(clip_bbox.x1());
638            if cur_wtile_x < wtile_x_clamped {
639                // Handle any pending clip pop from previous iteration
640                if core::mem::take(&mut pop_pending) {
641                    self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
642                    cur_wtile_x += 1;
643                }
644
645                // Pop zero clips for tiles that had zero winding or didn't match fill rule
646                // TODO: The winding check is probably not needed; if there was a fill,
647                // the logic below should have advanced wtile_x.
648                let is_inside = strip.fill_gap();
649                if !is_inside {
650                    for wtile_x in cur_wtile_x..wtile_x_clamped {
651                        self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
652                    }
653                }
654                cur_wtile_x = wtile_x_clamped;
655            }
656
657            // Process tiles covered by the strip - render clip content and pop
658            let next_strip = &strips[i + 1];
659            let strip_width =
660                ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
661            let mut clipped_x1 = x0 + strip_width;
662            let wtile_x0 = (x0 / WideTile::WIDTH).max(clip_bbox.x0());
663            let wtile_x1 = clipped_x1.div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
664
665            // Calculate starting position and column for alpha mask
666            let mut x = x0;
667            let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
668            let clip_x = clip_bbox.x0() * WideTile::WIDTH;
669            if clip_x > x {
670                col += u32::from(clip_x - x);
671                x = clip_x;
672                clipped_x1 = clip_x.max(clipped_x1);
673            }
674
675            // Render clip strips for each affected tile and mark for popping
676            for wtile_x in wtile_x0..wtile_x1 {
677                // If we've moved past tile_x and have a pending pop, do it now
678                if cur_wtile_x < wtile_x && core::mem::take(&mut pop_pending) {
679                    self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
680                }
681
682                // Calculate the portion of the strip that affects this tile
683                let x_rel = u32::from(x % WideTile::WIDTH);
684                let width = clipped_x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
685
686                // Create clip strip command for rendering the partial coverage
687                let cmd = CmdClipAlphaFill {
688                    x: x_rel,
689                    width: u32::from(width),
690                    alpha_idx: col as usize * Tile::HEIGHT as usize,
691                    #[cfg(feature = "multithreading")]
692                    thread_idx,
693                };
694                x += width;
695                col += u32::from(width);
696
697                // Apply the clip strip command and update state
698                self.get_mut(wtile_x, cur_wtile_y).clip_strip(cmd);
699                cur_wtile_x = wtile_x;
700
701                // Only request a pop if the x coordinate is actually inside the bounds.
702                if cur_wtile_x < clip_bbox.x1() {
703                    pop_pending = true;
704                }
705            }
706
707            // Handle fill regions between strips based on fill rule
708            let is_inside = next_strip.fill_gap();
709            if is_inside && strip_y == next_strip.strip_y() {
710                if cur_wtile_x >= clip_bbox.x1() {
711                    continue;
712                }
713
714                let x2 = next_strip.x;
715                let clipped_x2 = x2.min((cur_wtile_x + 1) * WideTile::WIDTH);
716                let width = clipped_x2.saturating_sub(clipped_x1);
717
718                // If there's a gap, fill it. Only do this if the fill wouldn't cover the
719                // whole tile, as such clips are skipped by the `push_clip` function. See
720                // <https://github.com/linebender/vello/blob/de0659e4df9842c8857153841a2b4ba6f1020bb0/sparse_strips/vello_common/src/coarse.rs#L504-L516>
721                if width > 0 && width < WideTile::WIDTH {
722                    let x_rel = u32::from(clipped_x1 % WideTile::WIDTH);
723                    self.get_mut(cur_wtile_x, cur_wtile_y)
724                        .clip_fill(x_rel, u32::from(width));
725                }
726
727                // If the next strip is a sentinel, skip the fill
728                // It's a sentinel in the row if there is non-zero winding for the sparse fill
729                // Look more into this in the strip.rs render function
730                if x2 == u16::MAX {
731                    continue;
732                }
733
734                // If fill extends to next tile, pop current and handle next
735                if x2 > (cur_wtile_x + 1) * WideTile::WIDTH {
736                    if core::mem::take(&mut pop_pending) {
737                        self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
738                    }
739
740                    let width2 = x2 % WideTile::WIDTH;
741                    cur_wtile_x = x2 / WideTile::WIDTH;
742
743                    // If the strip is outside the clipping box, we don't need to do any
744                    // filling, so we continue (also to prevent out-of-bounds access).
745                    if cur_wtile_x >= clip_bbox.x1() {
746                        continue;
747                    }
748
749                    if width2 > 0 {
750                        // An important thing to note: Note that we are only applying
751                        // `clip_fill` to the wide tile that is actually covered by the next
752                        // strip, and not the ones in-between! For example, if the first strip
753                        // is in wide tile 1 and the second in wide tile 4, we will do a clip
754                        // fill in wide tile 1 and 4, but not in 2 and 3. The reason for this is
755                        // that any tile in-between is fully covered and thus no clipping is
756                        // necessary at all. See also the `push_clip` function, where we don't
757                        // push a new buffer for such tiles.
758                        self.get_mut(cur_wtile_x, cur_wtile_y)
759                            .clip_fill(0, u32::from(width2));
760                    }
761                }
762            }
763        }
764
765        // Handle any pending clip pop from the last iteration
766        if core::mem::take(&mut pop_pending) {
767            self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
768            cur_wtile_x += 1;
769        }
770
771        // Process any remaining tiles in the bounding box (all zero-winding)
772        while cur_wtile_y < clip_bbox.y1() {
773            for wtile_x in cur_wtile_x..clip_bbox.x1() {
774                self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
775            }
776            cur_wtile_x = clip_bbox.x0();
777            cur_wtile_y += 1;
778        }
779    }
780}
781
782/// A wide tile.
783#[derive(Debug)]
784pub struct WideTile<const MODE: u8 = MODE_CPU> {
785    /// The x coordinate of the wide tile.
786    pub x: u16,
787    /// The y coordinate of the wide tile.
788    pub y: u16,
789    /// The background of the tile.
790    pub bg: PremulColor,
791    /// The draw commands of the tile.
792    pub cmds: Vec<Cmd>,
793    /// The number of zero-winding clips.
794    pub n_zero_clip: usize,
795    /// The number of non-zero-winding clips.
796    pub n_clip: usize,
797    /// The number of pushed buffers.
798    pub n_bufs: usize,
799}
800
801impl WideTile {
802    /// The width of a wide tile in pixels.
803    pub const WIDTH: u16 = 256;
804    /// The maximum coordinate of a wide tile.
805    pub const MAX_WIDE_TILE_COORD: u16 = u16::MAX / Self::WIDTH;
806}
807
808impl WideTile<MODE_CPU> {
809    /// Create a new wide tile.
810    pub fn new(width: u16, height: u16) -> Self {
811        Self::new_internal(width, height)
812    }
813}
814
815impl WideTile<MODE_HYBRID> {
816    /// Create a new wide tile.
817    pub fn new(width: u16, height: u16) -> Self {
818        Self::new_internal(width, height)
819    }
820}
821
822impl<const MODE: u8> WideTile<MODE> {
823    /// Create a new wide tile.
824    fn new_internal(x: u16, y: u16) -> Self {
825        Self {
826            x,
827            y,
828            bg: PremulColor::from_alpha_color(TRANSPARENT),
829            cmds: vec![],
830            n_zero_clip: 0,
831            n_clip: 0,
832            n_bufs: 0,
833        }
834    }
835
836    pub(crate) fn fill(&mut self, x: u16, width: u16, paint: Paint) {
837        if !self.is_zero_clip() {
838            match MODE {
839                MODE_CPU => {
840                    let bg = if let Paint::Solid(s) = &paint {
841                        // Note that we could be more aggressive in optimizing a whole-tile opaque fill
842                        // even with a clip stack. It would be valid to elide all drawing commands from
843                        // the enclosing clip push up to the fill. Further, we could extend the clip
844                        // push command to include a background color, rather than always starting with
845                        // a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could
846                        // be replaced with strip/fill with the color (the latter is true even with a
847                        // non-opaque color).
848                        //
849                        // However, the extra cost of tracking such optimizations may outweigh the
850                        // benefit, especially in hybrid mode with GPU painting.
851                        let can_override = x == 0
852                            && width == WideTile::WIDTH
853                            && s.is_opaque()
854                            && self.n_clip == 0
855                            && self.n_bufs == 0;
856                        can_override.then_some(*s)
857                    } else {
858                        // TODO: Implement for indexed paints.
859                        None
860                    };
861
862                    if let Some(bg) = bg {
863                        self.cmds.clear();
864                        self.bg = bg;
865                    } else {
866                        self.cmds.push(Cmd::Fill(CmdFill {
867                            x,
868                            width,
869                            paint,
870                            blend_mode: None,
871                        }));
872                    }
873                }
874                MODE_HYBRID => {
875                    self.cmds.push(Cmd::Fill(CmdFill {
876                        x,
877                        width,
878                        paint,
879                        blend_mode: None,
880                    }));
881                }
882                _ => unreachable!(),
883            }
884        }
885    }
886
887    pub(crate) fn strip(&mut self, cmd_strip: CmdAlphaFill) {
888        if !self.is_zero_clip() {
889            self.cmds.push(Cmd::AlphaFill(cmd_strip));
890        }
891    }
892
893    /// Adds a new clip region to the current wide tile.
894    pub fn push_clip(&mut self) {
895        if !self.is_zero_clip() {
896            self.push_buf();
897            self.n_clip += 1;
898        }
899    }
900
901    /// Removes the most recently added clip region from the current wide tile.
902    pub fn pop_clip(&mut self) {
903        if !self.is_zero_clip() {
904            self.pop_buf();
905            self.n_clip -= 1;
906        }
907    }
908
909    /// Adds a zero-winding clip region to the stack.
910    pub fn push_zero_clip(&mut self) {
911        self.n_zero_clip += 1;
912    }
913
914    /// Removes the most recently added zero-winding clip region.
915    pub fn pop_zero_clip(&mut self) {
916        self.n_zero_clip -= 1;
917    }
918
919    /// Checks if the current clip region is a zero-winding clip.
920    pub fn is_zero_clip(&mut self) -> bool {
921        self.n_zero_clip > 0
922    }
923
924    /// Applies a clip strip operation with the given parameters.
925    pub fn clip_strip(&mut self, cmd_clip_strip: CmdClipAlphaFill) {
926        if !self.is_zero_clip() && !matches!(self.cmds.last(), Some(Cmd::PushBuf)) {
927            self.cmds.push(Cmd::ClipStrip(cmd_clip_strip));
928        }
929    }
930
931    /// Applies a clip fill operation at the specified position and width.
932    pub fn clip_fill(&mut self, x: u32, width: u32) {
933        if !self.is_zero_clip() && !matches!(self.cmds.last(), Some(Cmd::PushBuf)) {
934            self.cmds.push(Cmd::ClipFill(CmdClipFill { x, width }));
935        }
936    }
937
938    /// Push a buffer.
939    pub fn push_buf(&mut self) {
940        self.cmds.push(Cmd::PushBuf);
941        self.n_bufs += 1;
942    }
943
944    /// Pop the most recent buffer.
945    pub fn pop_buf(&mut self) {
946        if matches!(self.cmds.last(), Some(&Cmd::PushBuf)) {
947            // Optimization: If no drawing happened between the last `PushBuf`,
948            // we can just pop it instead.
949            self.cmds.pop();
950        } else {
951            self.cmds.push(Cmd::PopBuf);
952        }
953
954        self.n_bufs -= 1;
955    }
956
957    /// Apply an opacity to the whole buffer.
958    pub fn opacity(&mut self, opacity: f32) {
959        if opacity != 1.0 {
960            self.cmds.push(Cmd::Opacity(opacity));
961        }
962    }
963
964    /// Apply a mask to the whole buffer.
965    pub fn mask(&mut self, mask: Mask) {
966        self.cmds.push(Cmd::Mask(mask));
967    }
968
969    /// Blend the current buffer into the previous buffer in the stack.
970    pub fn blend(&mut self, blend_mode: BlendMode) {
971        // Optimization: If no drawing happened since the last `PushBuf` and the blend mode
972        // is not destructive, we do not need to do any blending at all.
973        if !matches!(self.cmds.last(), Some(&Cmd::PushBuf)) || blend_mode.is_destructive() {
974            self.cmds.push(Cmd::Blend(blend_mode));
975        }
976    }
977}
978
979/// A drawing command.
980#[derive(Debug, PartialEq)]
981pub enum Cmd {
982    /// A fill command.
983    Fill(CmdFill),
984    /// A fill command with alpha mask.
985    AlphaFill(CmdAlphaFill),
986    /// Pushes a new buffer for drawing.
987    PushBuf,
988    /// Pops the most recent buffer.
989    PopBuf,
990    /// A fill command within a clipping region.
991    ///
992    /// This command will blend the contents of the current buffer within the clip fill region
993    /// into the previous buffer in the stack.
994    ClipFill(CmdClipFill),
995    /// A fill command with alpha mask within a clipping region.
996    ///
997    /// This command will blend the contents of the current buffer within the clip fill region
998    /// into the previous buffer in the stack, with an additional alpha mask.
999    ClipStrip(CmdClipAlphaFill),
1000    /// Apply a blend.
1001    ///
1002    /// This command will blend the contents of the current buffer into the previous buffer in
1003    /// the stack.
1004    Blend(BlendMode),
1005    /// Apply an opacity mask to the current buffer.
1006    Opacity(f32),
1007    /// Apply a mask to the current buffer.
1008    Mask(Mask),
1009}
1010
1011/// Fill a consecutive region of a wide tile.
1012#[derive(Debug, Clone, PartialEq)]
1013pub struct CmdFill {
1014    /// The horizontal start position of the command in pixels.
1015    pub x: u16,
1016    /// The width of the command in pixels.
1017    pub width: u16,
1018    /// The paint that should be used to fill the area.
1019    pub paint: Paint,
1020    /// The blend mode to apply before drawing the contents.
1021    pub blend_mode: Option<BlendMode>,
1022}
1023
1024/// Fill a consecutive region of a wide tile with an alpha mask.
1025#[derive(Debug, Clone, PartialEq)]
1026pub struct CmdAlphaFill {
1027    /// The horizontal start position of the command in pixels.
1028    pub x: u16,
1029    /// The width of the command in pixels.
1030    pub width: u16,
1031    /// The start index into the alpha buffer of the command.
1032    pub alpha_idx: usize,
1033    /// The index of the thread that contains the alpha values
1034    /// pointed to by `alpha_idx`.
1035    #[cfg(feature = "multithreading")]
1036    pub thread_idx: u8,
1037    /// The paint that should be used to fill the area.
1038    pub paint: Paint,
1039    /// A blend mode to apply before drawing the contents.
1040    pub blend_mode: Option<BlendMode>,
1041}
1042
1043/// Same as fill, but copies top of clip stack to next on stack.
1044#[derive(Debug, PartialEq, Eq)]
1045pub struct CmdClipFill {
1046    /// The horizontal start position of the command in pixels.
1047    pub x: u32,
1048    /// The width of the command in pixels.
1049    pub width: u32,
1050}
1051
1052/// Same as strip, but composites top of clip stack to next on stack.
1053#[derive(Debug, PartialEq, Eq)]
1054pub struct CmdClipAlphaFill {
1055    /// The horizontal start position of the command in pixels.
1056    pub x: u32,
1057    /// The width of the command in pixels.
1058    pub width: u32,
1059    /// The index of the thread that contains the alpha values
1060    /// pointed to by `alpha_idx`.
1061    #[cfg(feature = "multithreading")]
1062    pub thread_idx: u8,
1063    /// The start index into the alpha buffer of the command.
1064    pub alpha_idx: usize,
1065}
1066
1067trait BlendModeExt {
1068    /// Whether a blend mode might cause destructive changes in the backdrop.
1069    /// This disallows certain optimizations (like for example inlining a blend mode
1070    /// or only applying a blend mode to the current clipping area).
1071    fn is_destructive(&self) -> bool;
1072}
1073
1074impl BlendModeExt for BlendMode {
1075    fn is_destructive(&self) -> bool {
1076        matches!(
1077            self.compose,
1078            Compose::Clear
1079                | Compose::Copy
1080                | Compose::SrcIn
1081                | Compose::DestIn
1082                | Compose::SrcOut
1083                | Compose::DestAtop
1084        )
1085    }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use crate::coarse::{MODE_CPU, Wide, WideTile};
1091    use crate::color::AlphaColor;
1092    use crate::color::palette::css::TRANSPARENT;
1093    use crate::paint::{Paint, PremulColor};
1094    use crate::peniko::{BlendMode, Compose, Mix};
1095    use crate::strip::Strip;
1096    use alloc::{boxed::Box, vec};
1097
1098    #[test]
1099    fn optimize_empty_layers() {
1100        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1101        wide.push_buf();
1102        wide.pop_buf();
1103
1104        assert!(wide.cmds.is_empty());
1105    }
1106
1107    #[test]
1108    fn basic_layer() {
1109        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1110        wide.push_buf();
1111        wide.fill(
1112            0,
1113            10,
1114            Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
1115        );
1116        wide.fill(
1117            10,
1118            10,
1119            Paint::Solid(PremulColor::from_alpha_color(TRANSPARENT)),
1120        );
1121        wide.pop_buf();
1122
1123        assert_eq!(wide.cmds.len(), 4);
1124    }
1125
1126    #[test]
1127    fn dont_inline_blend_with_two_fills() {
1128        let paint = Paint::Solid(PremulColor::from_alpha_color(AlphaColor::from_rgba8(
1129            30, 30, 30, 255,
1130        )));
1131        let blend_mode = BlendMode::new(Mix::Lighten, Compose::SrcOver);
1132
1133        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1134        wide.push_buf();
1135        wide.fill(0, 10, paint.clone());
1136        wide.fill(10, 10, paint.clone());
1137        wide.blend(blend_mode);
1138        wide.pop_buf();
1139
1140        assert_eq!(wide.cmds.len(), 5);
1141    }
1142
1143    #[test]
1144    fn dont_inline_destructive_blend() {
1145        let paint = Paint::Solid(PremulColor::from_alpha_color(AlphaColor::from_rgba8(
1146            30, 30, 30, 255,
1147        )));
1148        let blend_mode = BlendMode::new(Mix::Lighten, Compose::Clear);
1149
1150        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1151        wide.push_buf();
1152        wide.fill(0, 10, paint.clone());
1153        wide.blend(blend_mode);
1154        wide.pop_buf();
1155
1156        assert_eq!(wide.cmds.len(), 4);
1157    }
1158
1159    #[test]
1160    fn tile_coordinates() {
1161        let wide = Wide::<MODE_CPU>::new(1000, 258);
1162
1163        let tile_1 = wide.get(1, 3);
1164        assert_eq!(tile_1.x, 256);
1165        assert_eq!(tile_1.y, 12);
1166
1167        let tile_2 = wide.get(2, 15);
1168        assert_eq!(tile_2.x, 512);
1169        assert_eq!(tile_2.y, 60);
1170    }
1171
1172    #[test]
1173    fn reset_clears_layer_and_clip_stacks() {
1174        type ClipPath = Option<Box<[Strip]>>;
1175
1176        let mut wide = Wide::<MODE_CPU>::new(1000, 258);
1177        let no_clip_path: ClipPath = None;
1178        wide.push_layer(no_clip_path, BlendMode::default(), None, 0.5, 0);
1179
1180        assert_eq!(wide.layer_stack.len(), 1);
1181        assert_eq!(wide.clip_stack.len(), 0);
1182
1183        let strip = Strip::new(2, 2, 0, true);
1184        let clip_path = Some(vec![strip].into_boxed_slice());
1185        wide.push_layer(clip_path, BlendMode::default(), None, 0.09, 0);
1186
1187        assert_eq!(wide.layer_stack.len(), 2);
1188        assert_eq!(wide.clip_stack.len(), 1);
1189
1190        wide.reset();
1191
1192        assert_eq!(wide.layer_stack.len(), 0);
1193        assert_eq!(wide.clip_stack.len(), 0);
1194    }
1195}