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