Skip to main content

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::encode::EncodedPaint;
8use crate::filter_effects::Filter;
9use crate::kurbo::{Affine, Rect};
10use crate::mask::Mask;
11use crate::paint::{Paint, PremulColor};
12use crate::peniko::{BlendMode, Compose, Mix};
13use crate::render_graph::{DependencyKind, LayerId, RenderGraph, RenderNodeKind};
14use crate::{strip::Strip, tile::Tile};
15use alloc::vec;
16use alloc::{boxed::Box, vec::Vec};
17#[cfg(debug_assertions)]
18use alloc::{format, string::String};
19use core::ops::Range;
20use hashbrown::HashMap;
21#[cfg(not(feature = "std"))]
22use peniko::kurbo::common::FloatFuncs as _;
23
24#[derive(Debug)]
25struct Layer {
26    /// The layer's ID.
27    layer_id: LayerId,
28    /// Whether the layer has a clip associated with it.
29    clip: bool,
30    /// The blend mode with which this layer should be blended into
31    /// the previous layer.
32    blend_mode: BlendMode,
33    /// An opacity to apply to the whole layer before blending it
34    /// into the backdrop.
35    opacity: f32,
36    /// A mask to apply to the layer before blending it back into
37    /// the backdrop.
38    mask: Option<Mask>,
39    /// A filter effect to apply to the layer before other operations.
40    filter: Option<Filter>,
41    /// Bounding box of wide tiles containing geometry.
42    /// Starts with inverted bounds, shrinks to actual content during drawing.
43    wtile_bbox: WideTilesBbox,
44}
45
46impl Layer {
47    /// Whether the layer actually requires allocating a new scratch buffer
48    /// for drawing its contents.
49    fn needs_buf(&self) -> bool {
50        self.blend_mode.mix != Mix::Normal
51            || self.blend_mode.compose != Compose::SrcOver
52            || self.opacity != 1.0
53            || self.mask.is_some()
54            || self.filter.is_some()
55            || !self.clip
56    }
57}
58
59/// `MODE_CPU` allows compile time optimizations to be applied to wide tile draw command generation
60/// specific to `vello_cpu`.
61pub const MODE_CPU: u8 = 0;
62/// `MODE_HYBRID` allows compile time optimizations to be applied to wide tile draw command
63/// generation specific for `vello_hybrid`.
64pub const MODE_HYBRID: u8 = 1;
65
66/// A container for wide tiles.
67#[derive(Debug)]
68pub struct Wide<const MODE: u8 = MODE_CPU> {
69    /// The width of the container.
70    pub width: u16,
71    /// The height of the container.
72    pub height: u16,
73    /// The wide tiles in the container.
74    pub tiles: Vec<WideTile<MODE>>,
75    /// Shared command properties, referenced by index from fill and clip commands.
76    pub attrs: CommandAttrs,
77    /// The stack of layers.
78    layer_stack: Vec<Layer>,
79    /// The stack of active clip regions.
80    clip_stack: Vec<Clip>,
81    /// Stack of filter layer node IDs for render graph dependency tracking.
82    /// Initialized with node 0 (the root node representing the final output).
83    /// As layers with filters are pushed, their node IDs are added to this stack.
84    filter_node_stack: Vec<usize>,
85    /// Count of nested filtered layers with clip paths.
86    /// When > 0, command generation uses full viewport bounds instead of clip bounds
87    /// to ensure filter effects can process the full layer before applying the clip.
88    clipped_filter_layer_depth: u32,
89}
90
91/// A clip region.
92#[derive(Debug)]
93struct Clip {
94    /// The intersected bounding box after clip
95    pub clip_bbox: WideTilesBbox,
96    /// The rendered path in sparse strip representation
97    pub strips: Box<[Strip]>,
98    /// The index of the thread that owns the alpha buffer.
99    /// Always 0 in single-threaded mode.
100    pub thread_idx: u8,
101}
102
103/// An axis-aligned bounding box represented by top-left and bottom-right corners.
104///
105/// The coordinates are stored as `[x0, y0, x1, y1]` in wide tile coordinates,
106/// where `(x0, y0)` is the top-left corner and `(x1, y1)` is the bottom-right corner.
107#[derive(Debug, Clone, Copy)]
108pub struct WideTilesBbox {
109    /// The bounding box coordinates.
110    pub bbox: [u16; 4],
111}
112
113impl WideTilesBbox {
114    /// Create a new bounding box.
115    pub fn new(bbox: [u16; 4]) -> Self {
116        Self { bbox }
117    }
118
119    /// Get the x0 coordinate of the bounding box.
120    #[inline(always)]
121    pub fn x0(&self) -> u16 {
122        self.bbox[0]
123    }
124
125    /// Get the y0 coordinate of the bounding box.
126    #[inline(always)]
127    pub fn y0(&self) -> u16 {
128        self.bbox[1]
129    }
130
131    /// Get the x1 coordinate of the bounding box.
132    #[inline(always)]
133    pub fn x1(&self) -> u16 {
134        self.bbox[2]
135    }
136
137    /// Get the y1 coordinate of the bounding box.
138    #[inline(always)]
139    pub fn y1(&self) -> u16 {
140        self.bbox[3]
141    }
142
143    /// Get the width of the bounding box (x1 - x0).
144    #[inline(always)]
145    pub fn width_tiles(&self) -> u16 {
146        self.x1().saturating_sub(self.x0())
147    }
148
149    /// Get the width of the bounding box in pixels.
150    #[inline(always)]
151    pub fn width_px(&self) -> u16 {
152        self.width_tiles() * WideTile::WIDTH
153    }
154
155    /// Get the height of the bounding box (y1 - y0).
156    #[inline(always)]
157    pub fn height_tiles(&self) -> u16 {
158        self.y1().saturating_sub(self.y0())
159    }
160
161    /// Get the height of the bounding box in pixels.
162    #[inline(always)]
163    pub fn height_px(&self) -> u16 {
164        self.height_tiles() * Tile::HEIGHT
165    }
166
167    /// Check if a point (x, y) is contained within this bounding box.
168    ///
169    /// Returns `true` if x0 <= x < x1 and y0 <= y < y1.
170    #[inline(always)]
171    pub fn contains(&self, x: u16, y: u16) -> bool {
172        let [x0, y0, x1, y1] = self.bbox;
173        (x >= x0) & (x < x1) & (y >= y0) & (y < y1)
174    }
175
176    /// Create an empty bounding box (zero area).
177    #[inline(always)]
178    pub(crate) fn empty() -> Self {
179        Self::new([0, 0, 0, 0])
180    }
181
182    /// Calculate the intersection of this bounding box with another.
183    #[inline(always)]
184    pub(crate) fn intersect(self, other: Self) -> Self {
185        Self::new([
186            self.x0().max(other.x0()),
187            self.y0().max(other.y0()),
188            self.x1().min(other.x1()),
189            self.y1().min(other.y1()),
190        ])
191    }
192
193    /// Update this bounding box to include another bounding box (union in place).
194    #[inline(always)]
195    pub(crate) fn union(&mut self, other: Self) {
196        if !other.is_inverted() {
197            if self.is_inverted() {
198                // If self is empty, just copy other
199                self.bbox = other.bbox;
200            } else {
201                // Otherwise compute the union
202                self.bbox[0] = self.bbox[0].min(other.x0());
203                self.bbox[1] = self.bbox[1].min(other.y0());
204                self.bbox[2] = self.bbox[2].max(other.x1());
205                self.bbox[3] = self.bbox[3].max(other.y1());
206            }
207        }
208    }
209
210    /// Create an inverted bounding box for incremental updates.
211    #[inline(always)]
212    pub(crate) fn inverted() -> Self {
213        Self::new([u16::MAX, u16::MAX, 0, 0])
214    }
215
216    /// Check if the bbox is still in its inverted state (no updates yet).
217    #[inline(always)]
218    pub(crate) fn is_inverted(self) -> bool {
219        self.bbox[0] == u16::MAX && self.bbox[1] == u16::MAX
220    }
221
222    /// Update the bbox to include the given tile coordinates.
223    #[inline(always)]
224    pub(crate) fn include_tile(&mut self, wtile_x: u16, wtile_y: u16) {
225        self.bbox[0] = self.bbox[0].min(wtile_x);
226        self.bbox[1] = self.bbox[1].min(wtile_y);
227        self.bbox[2] = self.bbox[2].max(wtile_x + 1);
228        self.bbox[3] = self.bbox[3].max(wtile_y + 1);
229    }
230
231    /// Scale this bounding box by the given scale factors.
232    ///
233    /// Multiplies each coordinate by the corresponding scale factor to convert
234    /// from one coordinate system to another.
235    #[inline(always)]
236    pub fn scale(&self, scale_x: u16, scale_y: u16) -> [u32; 4] {
237        [
238            u32::from(self.x0()) * u32::from(scale_x),
239            u32::from(self.y0()) * u32::from(scale_y),
240            u32::from(self.x1()) * u32::from(scale_x),
241            u32::from(self.y1()) * u32::from(scale_y),
242        ]
243    }
244
245    /// Expands the bounding box outward by the given pixel amounts in each direction.
246    ///
247    /// Pixel values are converted to tile coordinates (rounding up) and clamped to the
248    /// valid range `[0, max_x)` × `[0, max_y)`. The result is a new bounding box in
249    /// wide tile coordinates.
250    pub fn expand_by_pixels(&self, expansion: Rect, max_x: u16, max_y: u16) -> Self {
251        // The expansion rect is centered at origin:
252        // - Negative coordinates (x0, y0) represent left/top expansion
253        // - Positive coordinates (x1, y1) represent right/bottom expansion
254        let left_px = (-expansion.x0).max(0.0).ceil() as u16;
255        let top_px = (-expansion.y0).max(0.0).ceil() as u16;
256        let right_px = expansion.x1.max(0.0).ceil() as u16;
257        let bottom_px = expansion.y1.max(0.0).ceil() as u16;
258
259        // Convert pixel expansion to tile expansion (round up)
260        let left_tiles = left_px.div_ceil(WideTile::WIDTH);
261        let top_tiles = top_px.div_ceil(Tile::HEIGHT);
262        let right_tiles = right_px.div_ceil(WideTile::WIDTH);
263        let bottom_tiles = bottom_px.div_ceil(Tile::HEIGHT);
264
265        Self::new([
266            self.x0().saturating_sub(left_tiles),
267            self.y0().saturating_sub(top_tiles),
268            (self.x1() + right_tiles).min(max_x),
269            (self.y1() + bottom_tiles).min(max_y),
270        ])
271    }
272}
273
274impl Wide<MODE_CPU> {
275    /// Create a new container for wide tiles.
276    pub fn new(width: u16, height: u16) -> Self {
277        Self::new_internal(width, height)
278    }
279}
280
281impl Wide<MODE_HYBRID> {
282    /// Create a new container for wide tiles.
283    pub fn new(width: u16, height: u16) -> Self {
284        Self::new_internal(width, height)
285    }
286}
287
288impl<const MODE: u8> Wide<MODE> {
289    /// Create a new container for wide tiles.
290    fn new_internal(width: u16, height: u16) -> Self {
291        let width_tiles = width.div_ceil(WideTile::WIDTH);
292        let height_tiles = height.div_ceil(Tile::HEIGHT);
293        let mut tiles = Vec::with_capacity(usize::from(width_tiles) * usize::from(height_tiles));
294
295        for h in 0..height_tiles {
296            for w in 0..width_tiles {
297                tiles.push(WideTile::<MODE>::new_internal(
298                    w * WideTile::WIDTH,
299                    h * Tile::HEIGHT,
300                ));
301            }
302        }
303
304        Self {
305            tiles,
306            width,
307            height,
308            attrs: CommandAttrs::default(),
309            layer_stack: vec![],
310            clip_stack: vec![],
311            // Start with root node 0.
312            filter_node_stack: vec![0],
313            clipped_filter_layer_depth: 0,
314        }
315    }
316
317    /// Whether there are any existing layers that haven't been popped yet.
318    pub fn has_layers(&self) -> bool {
319        !self.layer_stack.is_empty()
320    }
321
322    /// Reset all tiles in the container.
323    pub fn reset(&mut self) {
324        for tile in &mut self.tiles {
325            tile.bg = PremulColor::from_alpha_color(TRANSPARENT);
326            tile.cmds.clear();
327            tile.layer_ids.truncate(1);
328            tile.layer_cmd_ranges.clear();
329            tile.layer_cmd_ranges
330                .insert(0, LayerCommandRanges::default());
331        }
332        self.attrs.clear();
333        self.layer_stack.clear();
334        self.clip_stack.clear();
335        self.filter_node_stack.truncate(1);
336        self.clipped_filter_layer_depth = 0;
337    }
338
339    /// Return the number of horizontal tiles.
340    pub fn width_tiles(&self) -> u16 {
341        self.width.div_ceil(WideTile::WIDTH)
342    }
343
344    /// Return the number of vertical tiles.
345    pub fn height_tiles(&self) -> u16 {
346        self.height.div_ceil(Tile::HEIGHT)
347    }
348
349    /// Get the wide tile at a certain index.
350    ///
351    /// Panics if the index is out-of-range.
352    pub fn get(&self, x: u16, y: u16) -> &WideTile<MODE> {
353        assert!(
354            x < self.width_tiles() && y < self.height_tiles(),
355            "attempted to access out-of-bounds wide tile"
356        );
357
358        &self.tiles[usize::from(y) * usize::from(self.width_tiles()) + usize::from(x)]
359    }
360
361    /// Get mutable access to the wide tile at a certain index.
362    ///
363    /// Panics if the index is out-of-range.
364    pub fn get_mut(&mut self, x: u16, y: u16) -> &mut WideTile<MODE> {
365        assert!(
366            x < self.width_tiles() && y < self.height_tiles(),
367            "attempted to access out-of-bounds wide tile"
368        );
369
370        let idx = usize::from(y) * usize::from(self.width_tiles()) + usize::from(x);
371        &mut self.tiles[idx]
372    }
373
374    /// Return a reference to all wide tiles.
375    pub fn tiles(&self) -> &[WideTile<MODE>] {
376        self.tiles.as_slice()
377    }
378
379    /// Get the current layer Id.
380    #[inline(always)]
381    pub fn get_current_layer_id(&self) -> LayerId {
382        self.layer_stack.last().map_or(0, |l| l.layer_id)
383    }
384
385    /// Update the bounding box of the current layer to include the given tile.
386    /// Should be called whenever a command is generated for a tile.
387    #[inline]
388    fn update_current_layer_bbox(&mut self, wtile_x: u16, wtile_y: u16) {
389        if let Some(layer) = self.layer_stack.last_mut() {
390            layer.wtile_bbox.include_tile(wtile_x, wtile_y);
391        }
392    }
393
394    /// Generate wide tile commands from the strip buffer.
395    ///
396    /// This method processes a buffer of strips that represent a path, applies the fill rule,
397    /// and generates appropriate drawing commands for each affected wide tile.
398    ///
399    /// # Algorithm overview:
400    /// 1. For each strip in the buffer:
401    ///    - Calculate its position and width in pixels
402    ///    - Determine which wide tiles the strip intersects
403    ///    - Generate alpha fill commands for the intersected wide tiles
404    /// 2. For active fill regions (determined by fill rule):
405    ///    - Generate solid fill commands for the regions between strips
406    pub fn generate(
407        &mut self,
408        strip_buf: &[Strip],
409        paint: Paint,
410        blend_mode: BlendMode,
411        thread_idx: u8,
412        mask: Option<Mask>,
413        encoded_paints: &[EncodedPaint],
414    ) {
415        if strip_buf.is_empty() {
416            return;
417        }
418
419        let alpha_base_idx = strip_buf[0].alpha_idx();
420
421        // Create shared attributes for all commands from this path
422        let attrs_idx = self.attrs.fill.len() as u32;
423        self.attrs.fill.push(FillAttrs {
424            thread_idx,
425            paint,
426            blend_mode,
427            mask,
428            alpha_base_idx,
429        });
430
431        // Get current clip bounding box or full viewport if no clip is active
432        let bbox = self.active_bbox();
433
434        // Save current_layer_id to avoid borrowing issues
435        let current_layer_id = self.get_current_layer_id();
436
437        for i in 0..strip_buf.len() - 1 {
438            let strip = &strip_buf[i];
439
440            debug_assert!(
441                strip.y < self.height,
442                "Strips below the viewport should have been culled prior to this stage."
443            );
444
445            // Don't render strips that are outside the viewport width
446            if strip.x >= self.width {
447                continue;
448            }
449
450            let next_strip = &strip_buf[i + 1];
451            let x0 = strip.x;
452            let strip_y = strip.strip_y();
453
454            // Skip strips outside the current clip bounding box
455            if strip_y < bbox.y0() {
456                continue;
457            }
458            if strip_y >= bbox.y1() {
459                // The rest of our strips must be outside the clip, so we can break early.
460                break;
461            }
462
463            // Calculate the width of the strip in columns
464            let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
465            let next_col = next_strip.alpha_idx() / u32::from(Tile::HEIGHT);
466            // Can potentially be 0 if strip only changes winding without covering pixels
467            let strip_width = next_col.saturating_sub(col) as u16;
468            let x1 = x0.saturating_add(strip_width);
469
470            // Calculate which wide tiles this strip intersects
471            let wtile_x0 = (x0 / WideTile::WIDTH).max(bbox.x0());
472            // It's possible that a strip extends into a new wide tile, but we don't actually
473            // have as many wide tiles (e.g. because the pixmap width is only 512, but
474            // strip ends at 513), so take the minimum between the rounded values and `width_tiles`.
475            let wtile_x1 = x1
476                .div_ceil(WideTile::WIDTH)
477                .min(bbox.x1())
478                .min(WideTile::MAX_WIDE_TILE_COORD);
479
480            // Adjust column starting position if needed to respect clip boundaries
481            let mut x = x0;
482            let clip_x = bbox.x0() * WideTile::WIDTH;
483            if clip_x > x {
484                col += u32::from(clip_x - x);
485                x = clip_x;
486            }
487
488            // Generate alpha fill commands for each wide tile intersected by this strip
489            for wtile_x in wtile_x0..wtile_x1 {
490                let x_wtile_rel = x % WideTile::WIDTH;
491                // Restrict the width of the fill to the width of the wide tile
492                let width = x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
493                let cmd = CmdAlphaFill {
494                    x: x_wtile_rel,
495                    width,
496                    alpha_offset: col * u32::from(Tile::HEIGHT) - alpha_base_idx,
497                    attrs_idx,
498                };
499                x += width;
500                col += u32::from(width);
501                self.get_mut(wtile_x, strip_y).strip(cmd, current_layer_id);
502                self.update_current_layer_bbox(wtile_x, strip_y);
503            }
504
505            // Determine if the region between this strip and the next should be filled.
506            let active_fill = next_strip.fill_gap();
507
508            // If region should be filled and both strips are on the same row,
509            // generate fill commands for the region between them
510            if active_fill && strip_y == next_strip.strip_y() {
511                // Clamp the fill to the clip bounding box
512                x = x1.max(bbox.x0() * WideTile::WIDTH);
513                let x2 = next_strip.x.min(
514                    self.width
515                        .checked_next_multiple_of(WideTile::WIDTH)
516                        .unwrap_or(u16::MAX),
517                );
518                let wfxt0 = (x1 / WideTile::WIDTH).max(bbox.x0());
519                let wfxt1 = x2
520                    .div_ceil(WideTile::WIDTH)
521                    .min(bbox.x1())
522                    .min(WideTile::MAX_WIDE_TILE_COORD);
523
524                // Compute fill hint based on paint type
525                let fill_attrs = &self.attrs.fill[attrs_idx as usize];
526                let fill_hint = if fill_attrs.mask.is_none() {
527                    match &fill_attrs.paint {
528                        Paint::Solid(s) if s.is_opaque() => FillHint::OpaqueSolid(*s),
529                        Paint::Indexed(idx) => {
530                            if let Some(EncodedPaint::Image(img)) = encoded_paints.get(idx.index())
531                                && !img.may_have_opacities
532                                && img.sampler.alpha == 1.0
533                            {
534                                FillHint::OpaqueImage
535                            } else {
536                                FillHint::None
537                            }
538                        }
539                        _ => FillHint::None,
540                    }
541                } else {
542                    FillHint::None
543                };
544
545                // Generate fill commands for each wide tile in the fill region
546                for wtile_x in wfxt0..wfxt1 {
547                    let x_wtile_rel = x % WideTile::WIDTH;
548                    let width = x2.min(
549                        (wtile_x
550                            .checked_add(1)
551                            .unwrap_or(WideTile::MAX_WIDE_TILE_COORD))
552                            * WideTile::WIDTH,
553                    ) - x;
554                    x += width;
555                    self.get_mut(wtile_x, strip_y).fill(
556                        x_wtile_rel,
557                        width,
558                        attrs_idx,
559                        current_layer_id,
560                        fill_hint,
561                    );
562                    // TODO: This bbox update might be redundant since filled regions are always
563                    // bounded by strip regions (which already update the bbox). Consider removing
564                    // this in a follow-up with proper benchmarks to verify correctness.
565                    self.update_current_layer_bbox(wtile_x, strip_y);
566                }
567            }
568        }
569    }
570
571    /// Push a new layer with the given properties.
572    ///
573    /// Rendering will be directed to the layer storage identified by `layer_id`.
574    /// This is used for filter effects that require access to a fully-rendered layer.
575    ///
576    /// If `filter` is Some, builds render graph nodes for filter effects.
577    pub fn push_layer(
578        &mut self,
579        layer_id: LayerId,
580        clip_path: Option<impl Into<Box<[Strip]>>>,
581        blend_mode: BlendMode,
582        mask: Option<Mask>,
583        opacity: f32,
584        filter: Option<Filter>,
585        transform: Affine,
586        render_graph: &mut RenderGraph,
587        thread_idx: u8,
588    ) {
589        // Some explanations about what is going on here: We support the concept of
590        // layers, where a user can push a new layer (with certain properties), draw some
591        // stuff, and finally pop the layer, as part of which the layer as a whole will be
592        // blended into the previous layer.
593        // There are 3 "straightforward" properties that can be set for each layer:
594        // 1) The blend mode that should be used to blend the layer into the backdrop.
595        // 2) A mask that will be applied to the whole layer in the very end before blending.
596        // 3) An optional opacity that will be applied to the whole layer before blending (this
597        //    could in theory be simulated with an alpha mask, but since it's a common operation and
598        //    we only have a single opacity, this can easily be optimized.
599        //
600        // Finally, you can also add a clip path to the layer. However, clipping has its own
601        // more complicated logic for pushing/popping buffers where drawing is also suppressed
602        // in clipped-out wide tiles. Because of this, in case we have one of the above properties
603        // AND a clipping path, we will actually end up pushing two buffers, the first one handles
604        // the three properties and the second one is just for clip paths. That is a bit wasteful
605        // and I believe it should be possible to process them all in just one go, but for now
606        // this is good enough, and it allows us to implement blending without too deep changes to
607        // the original clipping implementation.
608
609        // Build render graph node ONLY if we have a filter.
610        // The render graph tracks dependencies and execution order for filter effects.
611        if let Some(filter) = &filter {
612            // Create a FilterLayer node that combines render + filter + other operations
613            let child_node = render_graph.add_node(RenderNodeKind::FilterLayer {
614                layer_id,
615                filter: filter.clone(),
616                // Bounding box starts inverted and will be updated in pop_layer with actual bounds
617                wtile_bbox: WideTilesBbox::inverted(),
618                transform,
619            });
620
621            // Connect to parent node if there is one
622            if let Some(&parent_node) = self.filter_node_stack.last() {
623                render_graph.add_edge(
624                    child_node,
625                    parent_node,
626                    DependencyKind::Sequential { layer_id },
627                );
628            }
629
630            // Push this filter node onto the stack so subsequent filters depend on it
631            self.filter_node_stack.push(child_node);
632        }
633
634        let has_filter = filter.is_some();
635        let has_clip = clip_path.is_some();
636        let layer_kind = if has_filter {
637            LayerKind::Filtered(layer_id)
638        } else {
639            LayerKind::Regular(layer_id)
640        };
641
642        // Filtered layers with clipping require special handling: normally, tiles with
643        // zero-winding clips suppress all drawing. However, filters need the full layer
644        // content rendered (including zero-clipped areas) before applying the clip as a mask.
645        // When this flag is true, we generate explicit drawing commands instead of just counters.
646        let in_clipped_filter_layer = has_filter && has_clip;
647
648        // Increment the depth counter so that active_bbox() returns the full viewport
649        // instead of the clipped bbox. This ensures command generation covers all tiles,
650        // allowing the filter to process the entire layer before the clip is applied.
651        if in_clipped_filter_layer {
652            self.clipped_filter_layer_depth += 1;
653        }
654
655        let layer = Layer {
656            layer_id,
657            clip: has_clip,
658            blend_mode,
659            opacity,
660            mask,
661            filter,
662            wtile_bbox: WideTilesBbox::inverted(),
663        };
664
665        // In case we do blending, masking, opacity, or filtering, push one buffer per wide tile.
666        //
667        // Layers require buffers for different reasons:
668        // - Blending, opacity, and masking: Need to composite results with the backdrop
669        // - Filtering: Content must be rendered to a buffer before applying filter effects
670        //
671        // The layer_kind parameter distinguishes how buffers are managed:
672        // - Regular layers use the local blend_buf stack
673        // - Filtered layers are materialized in persistent layer storage for filter processing
674        // - Clip layers have special handling for clipping operations
675        if layer.needs_buf() {
676            for x in 0..self.width_tiles() {
677                for y in 0..self.height_tiles() {
678                    let tile = self.get_mut(x, y);
679                    tile.push_buf(layer_kind);
680                    // Mark tiles that are in a clipped filter layer so they generate
681                    // explicit clip commands for proper filter processing.
682                    tile.in_clipped_filter_layer = in_clipped_filter_layer;
683                }
684            }
685        }
686
687        // If we have a clip path, push another buffer in the affected wide tiles.
688        // Note that it is important that we FIRST push the buffer for blending etc. and
689        // only then for clipping, otherwise we will use the empty clip buffer as the backdrop
690        // for blending!
691        if let Some(clip) = clip_path {
692            self.push_clip(clip, layer_id, thread_idx);
693        }
694
695        self.layer_stack.push(layer);
696    }
697
698    /// Pop a previously pushed layer.
699    ///
700    /// This method finalizes the layer by:
701    /// - Expanding the bounding box if filter effects are present
702    /// - Updating the parent layer's bounding box to include this layer's bounds
703    /// - Completing render graph nodes for filter effects
704    /// - Generating filter commands for each tile
705    /// - Popping any associated clip
706    /// - Applying mask, opacity, and blend mode operations if needed
707    pub fn pop_layer(&mut self, render_graph: &mut RenderGraph) {
708        // This method basically unwinds everything we did in `push_layer`.
709        let mut layer = self.layer_stack.pop().unwrap();
710
711        if let Some(filter) = &layer.filter {
712            // Update render graph node with final bounding box
713            if let Some(node_id) = self.filter_node_stack.pop() {
714                // Get the transform from the FilterLayer node and scale the expansion by it
715                if let Some(node) = render_graph.nodes.get_mut(node_id)
716                    && let RenderNodeKind::FilterLayer {
717                        wtile_bbox,
718                        transform,
719                        ..
720                    } = &mut node.kind
721                {
722                    // Calculate expansion in device/pixel space, accounting for the full transform.
723                    // This ensures that rotated filters (e.g., drop shadows) have correct bounds.
724                    let expansion = filter.bounds_expansion(transform);
725                    let expanded_bbox = layer.wtile_bbox.expand_by_pixels(
726                        expansion,
727                        self.width_tiles(),
728                        self.height_tiles(),
729                    );
730                    let clip_bbox = self.active_bbox();
731                    let final_bbox = expanded_bbox.intersect(clip_bbox);
732
733                    // Update both the local layer and the render graph node
734                    layer.wtile_bbox = final_bbox;
735                    *wtile_bbox = final_bbox;
736                }
737                // Record this node in execution order (children before parents)
738                render_graph.record_node_for_execution(node_id);
739            }
740
741            // Generate filter commands for each tile (used for non-graph path rendering)
742            // Apply filter BEFORE clipping (per SVG spec: filter → clip → mask → opacity → blend)
743            for x in 0..self.width_tiles() {
744                for y in 0..self.height_tiles() {
745                    self.get_mut(x, y).filter(layer.layer_id, filter.clone());
746                }
747            }
748        }
749
750        // Union this layer's bbox into the parent layer's bbox.
751        // This ensures the parent knows about all tiles used by this child layer,
752        // which is important for filter effects that may expand beyond the original content bounds.
753        if let Some(parent_layer) = self.layer_stack.last_mut() {
754            parent_layer.wtile_bbox.union(layer.wtile_bbox);
755        }
756
757        if layer.clip {
758            self.pop_clip();
759        }
760
761        let needs_buf = layer.needs_buf();
762
763        if needs_buf {
764            for x in 0..self.width_tiles() {
765                for y in 0..self.height_tiles() {
766                    let t = self.get_mut(x, y);
767
768                    if let Some(mask) = layer.mask.clone() {
769                        t.mask(mask);
770                    }
771                    t.opacity(layer.opacity);
772                    t.blend(layer.blend_mode);
773                    t.pop_buf();
774                }
775            }
776        }
777
778        let in_clipped_filter_layer = layer.filter.is_some() && layer.clip;
779        // Decrement the depth counter after popping a filtered layer with clip
780        if in_clipped_filter_layer {
781            self.clipped_filter_layer_depth -= 1;
782        }
783    }
784
785    /// Adds a clipping region defined by the provided strips.
786    ///
787    /// This method takes a vector of strips representing a clip path, calculates the
788    /// intersection with the current clip region, and updates the clip stack.
789    ///
790    /// # Algorithm overview:
791    /// 1. Calculate bounding box of the clip path
792    /// 2. Intersect with current clip bounding box
793    /// 3. For each tile in the intersected bounding box:
794    ///    - If covered by zero winding: `push_zero_clip`
795    ///    - If fully covered by non-zero winding: do nothing (clip is a no-op)
796    ///    - If partially covered: `push_clip`
797    pub fn push_clip(
798        &mut self,
799        strips: impl Into<Box<[Strip]>>,
800        layer_id: LayerId,
801        thread_idx: u8,
802    ) {
803        let strips = strips.into();
804        let n_strips = strips.len();
805
806        // Calculate the bounding box of the clip path in strip coordinates
807        let path_bbox = if n_strips <= 1 {
808            WideTilesBbox::empty()
809        } else {
810            // Calculate the y range from first to last strip in wide tile coordinates
811            let wtile_y0 = strips[0].strip_y();
812            let wtile_y1 = strips[n_strips.saturating_sub(1)].strip_y() + 1;
813
814            // Calculate the x range by examining all strips in wide tile coordinates
815            let mut wtile_x0 = strips[0].x / WideTile::WIDTH;
816            let mut wtile_x1 = wtile_x0;
817            for i in 0..n_strips.saturating_sub(1) {
818                let strip = &strips[i];
819                let next_strip = &strips[i + 1];
820                let width =
821                    ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
822                let x = strip.x;
823                wtile_x0 = wtile_x0.min(x / WideTile::WIDTH);
824                wtile_x1 = wtile_x1.max((x + width).div_ceil(WideTile::WIDTH));
825            }
826            WideTilesBbox::new([wtile_x0, wtile_y0, wtile_x1, wtile_y1])
827        };
828
829        let parent_bbox = self.active_bbox();
830        // Determine which tiles need clip processing:
831        // - For clipped filter layers: active_bbox() returns the full viewport, so parent_bbox
832        //   already covers all tiles. We need to process all of them because the filter needs
833        //   the entire layer rendered, and tiles outside the clip path must get `PushZeroClip`
834        //   commands to properly suppress their content after filtering.
835        // - For normal clips: Intersect with the path bounds to only process tiles that are
836        //   actually affected by the clip path, avoiding unnecessary work.
837        let clip_bbox = if self.clipped_filter_layer_depth > 0 {
838            // Use parent_bbox as-is (full viewport) to process all tiles
839            parent_bbox
840        } else {
841            // Optimize by processing only the intersection of parent and path bounds
842            parent_bbox.intersect(path_bbox)
843        };
844
845        let mut cur_wtile_x = clip_bbox.x0();
846        let mut cur_wtile_y = clip_bbox.y0();
847
848        // Process strips to determine the clipping state for each wide tile
849        for i in 0..n_strips.saturating_sub(1) {
850            let strip = &strips[i];
851            let strip_y = strip.strip_y();
852
853            // Skip strips before current wide tile row
854            if strip_y < cur_wtile_y {
855                continue;
856            }
857
858            // Process wide tiles in rows before this strip's row
859            // These wide tiles are all zero-winding (outside the path)
860            while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
861                for wtile_x in cur_wtile_x..clip_bbox.x1() {
862                    self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id);
863                }
864                // Reset x to the left edge of the clip bounding box
865                cur_wtile_x = clip_bbox.x0();
866                // Move to the next row
867                cur_wtile_y += 1;
868            }
869
870            // If we've reached the bottom of the clip bounding box, stop processing.
871            // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
872            // is zero-area (see issue 1072).
873            if cur_wtile_y >= clip_bbox.y1() {
874                break;
875            }
876
877            // Process wide tiles to the left of this strip in the same row
878            let x = strip.x;
879            let wtile_x_clamped = (x / WideTile::WIDTH).min(clip_bbox.x1());
880            if cur_wtile_x < wtile_x_clamped {
881                // If winding is zero or doesn't match fill rule, these wide tiles are outside the path
882                let is_inside = strip.fill_gap();
883                if !is_inside {
884                    for wtile_x in cur_wtile_x..wtile_x_clamped {
885                        self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id);
886                    }
887                }
888                // If winding is nonzero, then wide tiles covered entirely
889                // by sparse fill are no-op (no clipping is applied).
890                cur_wtile_x = wtile_x_clamped;
891            }
892
893            // Process wide tiles covered by the strip - these need actual clipping
894            let next_strip = &strips[i + 1];
895            let width =
896                ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
897            let wtile_x1 = (x + width).div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
898            if cur_wtile_x < wtile_x1 {
899                for wtile_x in cur_wtile_x..wtile_x1 {
900                    self.get_mut(wtile_x, cur_wtile_y).push_clip(layer_id);
901                }
902                cur_wtile_x = wtile_x1;
903            }
904        }
905
906        // Process any remaining wide tiles in the bounding box (all zero-winding)
907        while cur_wtile_y < clip_bbox.y1() {
908            for wtile_x in cur_wtile_x..clip_bbox.x1() {
909                self.get_mut(wtile_x, cur_wtile_y).push_zero_clip(layer_id);
910            }
911            cur_wtile_x = clip_bbox.x0();
912            cur_wtile_y += 1;
913        }
914
915        self.clip_stack.push(Clip {
916            clip_bbox,
917            strips,
918            thread_idx,
919        });
920    }
921
922    /// Get the bounding box of the current clip region or the entire viewport if no clip regions are active.
923    fn active_bbox(&self) -> WideTilesBbox {
924        // When in a clipped filter layer, use full viewport to allow
925        // filter to process the complete layer before applying clip as mask
926        if self.clipped_filter_layer_depth > 0 {
927            return self.full_viewport_bbox();
928        }
929
930        self.clip_stack
931            .last()
932            .map(|top| top.clip_bbox)
933            .unwrap_or_else(|| self.full_viewport_bbox())
934    }
935
936    /// Returns the bounding box covering the entire viewport in wide tile coordinates.
937    fn full_viewport_bbox(&self) -> WideTilesBbox {
938        WideTilesBbox::new([0, 0, self.width_tiles(), self.height_tiles()])
939    }
940
941    /// Removes the most recently added clip region.
942    ///
943    /// This is the inverse operation of `push_clip`, carefully undoing all the clipping
944    /// operations while also handling any rendering needed for the clip region itself.
945    ///
946    /// # Algorithm overview:
947    /// 1. Retrieve the top clip from the stack
948    /// 2. For each wide tile in the clip's bounding box:
949    ///    - If covered by zero winding: `pop_zero_clip`
950    ///    - If fully covered by non-zero winding: do nothing (was no-op)
951    ///    - If partially covered: render the clip and `pop_clip`
952    ///
953    /// This operation must be symmetric with `push_clip` to maintain a balanced clip stack.
954    fn pop_clip(&mut self) {
955        let Clip {
956            clip_bbox,
957            strips,
958            thread_idx,
959        } = self.clip_stack.pop().unwrap();
960        let n_strips = strips.len();
961
962        if n_strips == 0 {
963            return;
964        }
965
966        // Compute base alpha index and create shared clip attributes
967        let alpha_base_idx = strips[0].alpha_idx();
968        let clip_attrs_idx = self.attrs.clip.len() as u32;
969        self.attrs.clip.push(ClipAttrs {
970            thread_idx,
971            alpha_base_idx,
972        });
973
974        let mut cur_wtile_x = clip_bbox.x0();
975        let mut cur_wtile_y = clip_bbox.y0();
976        let mut pop_pending = false;
977
978        // Process each strip to determine the clipping state for each tile
979        for i in 0..n_strips.saturating_sub(1) {
980            let strip = &strips[i];
981            let strip_y = strip.strip_y();
982
983            // Skip strips before current tile row
984            if strip_y < cur_wtile_y {
985                continue;
986            }
987
988            // Process tiles in rows before this strip's row
989            // These tiles all had zero-winding clips
990            while cur_wtile_y < strip_y.min(clip_bbox.y1()) {
991                // Handle any pending clip pop from previous iteration
992                if core::mem::take(&mut pop_pending) {
993                    self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
994                    cur_wtile_x += 1;
995                }
996
997                // Pop zero clips for all remaining tiles in this row
998                for wtile_x in cur_wtile_x..clip_bbox.x1() {
999                    self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
1000                }
1001                cur_wtile_x = clip_bbox.x0();
1002                cur_wtile_y += 1;
1003            }
1004
1005            // If we've reached the bottom of the clip bounding box, stop processing
1006            // Note that we are explicitly checking >= instead of ==, so that we abort if the clipping box
1007            // is zero-area (see issue 1072).
1008            if cur_wtile_y >= clip_bbox.y1() {
1009                break;
1010            }
1011
1012            // Process tiles to the left of this strip in the same row
1013            let x0 = strip.x;
1014            let wtile_x_clamped = (x0 / WideTile::WIDTH).min(clip_bbox.x1());
1015            if cur_wtile_x < wtile_x_clamped {
1016                // Handle any pending clip pop from previous iteration
1017                if core::mem::take(&mut pop_pending) {
1018                    self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1019                    cur_wtile_x += 1;
1020                }
1021
1022                // Pop zero clips for tiles that had zero winding or didn't match fill rule
1023                // TODO: The winding check is probably not needed; if there was a fill,
1024                // the logic below should have advanced wtile_x.
1025                let is_inside = strip.fill_gap();
1026                if !is_inside {
1027                    for wtile_x in cur_wtile_x..wtile_x_clamped {
1028                        self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
1029                    }
1030                }
1031                cur_wtile_x = wtile_x_clamped;
1032            }
1033
1034            // Process tiles covered by the strip - render clip content and pop
1035            let next_strip = &strips[i + 1];
1036            let strip_width =
1037                ((next_strip.alpha_idx() - strip.alpha_idx()) / u32::from(Tile::HEIGHT)) as u16;
1038            let mut clipped_x1 = x0 + strip_width;
1039            let wtile_x0 = (x0 / WideTile::WIDTH).max(clip_bbox.x0());
1040            let wtile_x1 = clipped_x1.div_ceil(WideTile::WIDTH).min(clip_bbox.x1());
1041
1042            // Calculate starting position and column for alpha mask
1043            let mut x = x0;
1044            let mut col = strip.alpha_idx() / u32::from(Tile::HEIGHT);
1045            let clip_x = clip_bbox.x0() * WideTile::WIDTH;
1046            if clip_x > x {
1047                col += u32::from(clip_x - x);
1048                x = clip_x;
1049                clipped_x1 = clip_x.max(clipped_x1);
1050            }
1051
1052            // Render clip strips for each affected tile and mark for popping
1053            for wtile_x in wtile_x0..wtile_x1 {
1054                // If we've moved past tile_x and have a pending pop, do it now
1055                if cur_wtile_x < wtile_x && core::mem::take(&mut pop_pending) {
1056                    self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1057                }
1058
1059                // Calculate the portion of the strip that affects this tile
1060                let x_rel = x % WideTile::WIDTH;
1061                let width = clipped_x1.min((wtile_x + 1) * WideTile::WIDTH) - x;
1062
1063                // Create clip strip command for rendering the partial coverage
1064                let cmd = CmdClipAlphaFill {
1065                    x: x_rel,
1066                    width,
1067                    alpha_offset: col * u32::from(Tile::HEIGHT) - alpha_base_idx,
1068                    attrs_idx: clip_attrs_idx,
1069                };
1070                x += width;
1071                col += u32::from(width);
1072
1073                // Apply the clip strip command and update state
1074                self.get_mut(wtile_x, cur_wtile_y).clip_strip(cmd);
1075                cur_wtile_x = wtile_x;
1076
1077                // Only request a pop if the x coordinate is actually inside the bounds.
1078                if cur_wtile_x < clip_bbox.x1() {
1079                    pop_pending = true;
1080                }
1081            }
1082
1083            // Handle fill regions between strips based on fill rule
1084            let is_inside = next_strip.fill_gap();
1085            if is_inside && strip_y == next_strip.strip_y() {
1086                if cur_wtile_x >= clip_bbox.x1() {
1087                    continue;
1088                }
1089
1090                let x2 = next_strip.x;
1091                let clipped_x2 = x2.min((cur_wtile_x + 1) * WideTile::WIDTH);
1092                let width = clipped_x2.saturating_sub(clipped_x1);
1093
1094                // If there's a gap, fill it. Only do this if the fill wouldn't cover the
1095                // whole tile, as such clips are skipped by the `push_clip` function. See
1096                // <https://github.com/linebender/vello/blob/de0659e4df9842c8857153841a2b4ba6f1020bb0/sparse_strips/vello_common/src/coarse.rs#L504-L516>
1097                if width > 0 && width < WideTile::WIDTH {
1098                    let x_rel = clipped_x1 % WideTile::WIDTH;
1099                    self.get_mut(cur_wtile_x, cur_wtile_y)
1100                        .clip_fill(x_rel, width);
1101                }
1102
1103                // If the next strip is a sentinel, skip the fill
1104                // It's a sentinel in the row if there is non-zero winding for the sparse fill
1105                // Look more into this in the strip.rs render function
1106                if x2 == u16::MAX {
1107                    continue;
1108                }
1109
1110                // If fill extends to next tile, pop current and handle next
1111                if x2 > (cur_wtile_x + 1) * WideTile::WIDTH {
1112                    if core::mem::take(&mut pop_pending) {
1113                        self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1114                    }
1115
1116                    let width2 = x2 % WideTile::WIDTH;
1117                    cur_wtile_x = x2 / WideTile::WIDTH;
1118
1119                    // If the strip is outside the clipping box, we don't need to do any
1120                    // filling, so we continue (also to prevent out-of-bounds access).
1121                    if cur_wtile_x >= clip_bbox.x1() {
1122                        continue;
1123                    }
1124
1125                    if width2 > 0 {
1126                        // An important thing to note: Note that we are only applying
1127                        // `clip_fill` to the wide tile that is actually covered by the next
1128                        // strip, and not the ones in-between! For example, if the first strip
1129                        // is in wide tile 1 and the second in wide tile 4, we will do a clip
1130                        // fill in wide tile 1 and 4, but not in 2 and 3. The reason for this is
1131                        // that any tile in-between is fully covered and thus no clipping is
1132                        // necessary at all. See also the `push_clip` function, where we don't
1133                        // push a new buffer for such tiles.
1134                        self.get_mut(cur_wtile_x, cur_wtile_y).clip_fill(0, width2);
1135                    }
1136                }
1137            }
1138        }
1139
1140        // Handle any pending clip pop from the last iteration
1141        if core::mem::take(&mut pop_pending) {
1142            self.get_mut(cur_wtile_x, cur_wtile_y).pop_clip();
1143            cur_wtile_x += 1;
1144        }
1145
1146        // Process any remaining tiles in the bounding box (all zero-winding)
1147        while cur_wtile_y < clip_bbox.y1() {
1148            for wtile_x in cur_wtile_x..clip_bbox.x1() {
1149                self.get_mut(wtile_x, cur_wtile_y).pop_zero_clip();
1150            }
1151            cur_wtile_x = clip_bbox.x0();
1152            cur_wtile_y += 1;
1153        }
1154    }
1155}
1156
1157/// A wide tile.
1158#[derive(Debug)]
1159pub struct WideTile<const MODE: u8 = MODE_CPU> {
1160    /// The x coordinate of the wide tile.
1161    pub x: u16,
1162    /// The y coordinate of the wide tile.
1163    pub y: u16,
1164    /// The background of the tile.
1165    pub bg: PremulColor,
1166    /// The draw commands of the tile.
1167    pub cmds: Vec<Cmd>,
1168    /// The number of zero-winding clips.
1169    pub n_zero_clip: usize,
1170    /// The number of non-zero-winding clips.
1171    pub n_clip: usize,
1172    /// The number of pushed buffers.
1173    pub n_bufs: usize,
1174    /// True when this tile is in a filtered layer with clipping applied.
1175    /// When set, clip operations generate explicit commands instead of just
1176    /// tracking counters, allowing filters to process clipped content correctly.
1177    pub in_clipped_filter_layer: bool,
1178    /// Maps layer Id to command ranges for this tile.
1179    pub layer_cmd_ranges: HashMap<LayerId, LayerCommandRanges>,
1180    /// Vector of layer IDs this tile participates in.
1181    pub layer_ids: Vec<LayerKind>,
1182}
1183
1184impl WideTile {
1185    /// The width of a wide tile in pixels.
1186    pub const WIDTH: u16 = 256;
1187    /// The maximum coordinate of a wide tile.
1188    pub const MAX_WIDE_TILE_COORD: u16 = u16::MAX / Self::WIDTH;
1189}
1190
1191impl WideTile<MODE_CPU> {
1192    /// Create a new wide tile.
1193    pub fn new(width: u16, height: u16) -> Self {
1194        Self::new_internal(width, height)
1195    }
1196}
1197
1198impl WideTile<MODE_HYBRID> {
1199    /// Create a new wide tile.
1200    pub fn new(width: u16, height: u16) -> Self {
1201        Self::new_internal(width, height)
1202    }
1203}
1204
1205impl<const MODE: u8> WideTile<MODE> {
1206    /// Create a new wide tile.
1207    fn new_internal(x: u16, y: u16) -> Self {
1208        let mut layer_cmd_ranges = HashMap::new();
1209        layer_cmd_ranges.insert(0, LayerCommandRanges::default());
1210        Self {
1211            x,
1212            y,
1213            bg: PremulColor::from_alpha_color(TRANSPARENT),
1214            cmds: vec![],
1215            n_zero_clip: 0,
1216            n_clip: 0,
1217            n_bufs: 0,
1218            in_clipped_filter_layer: false,
1219            layer_cmd_ranges,
1220            layer_ids: vec![LayerKind::Regular(0)],
1221        }
1222    }
1223
1224    /// Fill a rectangular region with a paint.
1225    ///
1226    /// Generates fill commands unless the tile is in a zero-clip region (fully clipped out).
1227    /// For clipped filter layers, commands are always generated since filters need the full
1228    /// layer content rendered before applying the clip as a mask.
1229    ///
1230    /// The `fill_hint` parameter is pre-computed by the caller based on paint type:
1231    /// - `OpaqueSolid(color)`: Paint is an opaque solid color, can replace background
1232    /// - `OpaqueImage`: Paint is an opaque image, can clear previous commands
1233    /// - `None`: No optimization available
1234    pub(crate) fn fill(
1235        &mut self,
1236        x: u16,
1237        width: u16,
1238        attrs_idx: u32,
1239        current_layer_id: LayerId,
1240        fill_hint: FillHint,
1241    ) {
1242        if !self.is_zero_clip() || self.in_clipped_filter_layer {
1243            match MODE {
1244                MODE_CPU => {
1245                    // Check if we can apply overdraw elimination optimization.
1246                    // This requires filling the entire tile width with no clip/buffer stack.
1247                    //
1248                    // Note that we could be more aggressive in optimizing a whole-tile opaque fill
1249                    // even with a clip stack. It would be valid to elide all drawing commands from
1250                    // the enclosing clip push up to the fill. Further, we could extend the clip
1251                    // push command to include a background color, rather than always starting with
1252                    // a transparent buffer. Lastly, a sequence of push(bg); strip/fill; pop could
1253                    // be replaced with strip/fill with the color (the latter is true even with a
1254                    // non-opaque color).
1255                    //
1256                    // However, the extra cost of tracking such optimizations may outweigh the
1257                    // benefit, especially in hybrid mode with GPU painting.
1258                    let can_override =
1259                        x == 0 && width == WideTile::WIDTH && self.n_clip == 0 && self.n_bufs == 0;
1260
1261                    if can_override {
1262                        match fill_hint {
1263                            FillHint::OpaqueSolid(color) => {
1264                                self.cmds.clear();
1265                                self.bg = color;
1266                                if let Some(ranges) =
1267                                    self.layer_cmd_ranges.get_mut(&current_layer_id)
1268                                {
1269                                    ranges.clear();
1270                                }
1271                                return;
1272                            }
1273                            FillHint::OpaqueImage => {
1274                                // Opaque image: clear previous commands but still emit the fill.
1275                                self.cmds.clear();
1276                                self.bg = PremulColor::from_alpha_color(TRANSPARENT);
1277                                if let Some(ranges) =
1278                                    self.layer_cmd_ranges.get_mut(&current_layer_id)
1279                                {
1280                                    ranges.clear();
1281                                }
1282                                // Fall through to emit the fill command below, as opposed to
1283                                // solid paints where we have a return statement.
1284                            }
1285                            FillHint::None => {}
1286                        }
1287                    }
1288
1289                    self.record_fill_cmd(current_layer_id, self.cmds.len());
1290                    self.cmds.push(Cmd::Fill(CmdFill {
1291                        x,
1292                        width,
1293                        attrs_idx,
1294                    }));
1295                }
1296                MODE_HYBRID => {
1297                    self.record_fill_cmd(current_layer_id, self.cmds.len());
1298                    self.cmds.push(Cmd::Fill(CmdFill {
1299                        x,
1300                        width,
1301                        attrs_idx,
1302                    }));
1303                }
1304                _ => unreachable!(),
1305            }
1306        }
1307    }
1308
1309    /// Fill a region using an alpha mask from a strip.
1310    ///
1311    /// Generates alpha fill commands unless the tile is in a zero-clip region (fully clipped out).
1312    /// For clipped filter layers, commands are always generated since filters need the full
1313    /// layer content rendered before applying the clip as a mask.
1314    pub(crate) fn strip(&mut self, cmd_strip: CmdAlphaFill, current_layer_id: LayerId) {
1315        if !self.is_zero_clip() || self.in_clipped_filter_layer {
1316            self.record_fill_cmd(current_layer_id, self.cmds.len());
1317            self.cmds.push(Cmd::AlphaFill(cmd_strip));
1318        }
1319    }
1320
1321    /// Adds a new clip region to the current wide tile.
1322    ///
1323    /// Pushes a clip buffer unless the tile is in a zero-clip region (fully clipped out).
1324    /// For clipped filter layers, clip buffers are always pushed since filters need explicit
1325    /// clip state to process the full layer before applying the clip as a mask.
1326    pub fn push_clip(&mut self, layer_id: LayerId) {
1327        if !self.is_zero_clip() || self.in_clipped_filter_layer {
1328            self.push_buf(LayerKind::Clip(layer_id));
1329            self.n_clip += 1;
1330        }
1331    }
1332
1333    /// Removes the most recently added clip region from the current wide tile.
1334    ///
1335    /// Pops a clip buffer unless the tile is in a zero-clip region (fully clipped out).
1336    /// For clipped filter layers, clip buffers are always popped since filters need explicit
1337    /// clip state to process the full layer before applying the clip as a mask.
1338    pub fn pop_clip(&mut self) {
1339        if !self.is_zero_clip() || self.in_clipped_filter_layer {
1340            self.pop_buf();
1341            self.n_clip -= 1;
1342        }
1343    }
1344
1345    /// Adds a zero-winding clip region to the stack.
1346    ///
1347    /// Zero-winding clips represent tiles completely outside the clip path.
1348    /// Normally these just increment a counter to suppress drawing, but for
1349    /// clipped filter layers we generate explicit commands so filters can
1350    /// process the entire layer before applying the clip as a mask.
1351    pub fn push_zero_clip(&mut self, layer_id: LayerId) {
1352        if self.in_clipped_filter_layer {
1353            // Generate explicit command for filter processing
1354            self.cmds.push(Cmd::PushZeroClip(layer_id));
1355        }
1356        self.n_zero_clip += 1;
1357    }
1358
1359    /// Removes the most recently added zero-winding clip region.
1360    pub fn pop_zero_clip(&mut self) {
1361        if self.in_clipped_filter_layer {
1362            // Generate explicit command for filter processing
1363            self.cmds.push(Cmd::PopZeroClip);
1364        }
1365        self.n_zero_clip -= 1;
1366    }
1367
1368    /// Checks if the current clip region is a zero-winding clip.
1369    pub fn is_zero_clip(&mut self) -> bool {
1370        self.n_zero_clip > 0
1371    }
1372
1373    /// Applies a clip strip operation with the given parameters.
1374    ///
1375    /// Note: Unlike content operations (`strip`, `push_clip`, etc.), clip operations don't need
1376    /// the `|| self.in_clipped_filter_layer` check. Filter effects need full layer *content*
1377    /// rendered (even in zero-clip areas).
1378    pub fn clip_strip(&mut self, cmd_clip_strip: CmdClipAlphaFill) {
1379        if (!self.is_zero_clip()) && !matches!(self.cmds.last(), Some(Cmd::PushBuf(_))) {
1380            self.cmds.push(Cmd::ClipStrip(cmd_clip_strip));
1381        }
1382    }
1383
1384    /// Applies a clip fill operation at the specified position and width.
1385    pub fn clip_fill(&mut self, x: u16, width: u16) {
1386        if (!self.is_zero_clip()) && !matches!(self.cmds.last(), Some(Cmd::PushBuf(_))) {
1387            self.cmds.push(Cmd::ClipFill(CmdClipFill { x, width }));
1388        }
1389    }
1390
1391    /// Records the fill command for a specific layer.
1392    pub fn record_fill_cmd(&mut self, layer_id: LayerId, cmd_idx: usize) {
1393        self.layer_cmd_ranges.entry(layer_id).and_modify(|ranges| {
1394            ranges.full_range.end = cmd_idx + 1;
1395            if ranges.render_range.is_empty() {
1396                ranges.render_range = cmd_idx..cmd_idx + 1;
1397            } else {
1398                ranges.render_range.end = cmd_idx + 1;
1399            }
1400        });
1401    }
1402
1403    /// Push a buffer for a new layer.
1404    ///
1405    /// Different layer kinds are handled differently:
1406    /// - Regular layers: Use local `blend_buf` stack for temporary storage
1407    /// - Filtered layers: Materialized in persistent layer storage for filter processing
1408    /// - Clip layers: Special handling for clipping operations
1409    pub fn push_buf(&mut self, layer_kind: LayerKind) {
1410        let top_layer = layer_kind.id();
1411        if matches!(layer_kind, LayerKind::Filtered(_)) {
1412            self.layer_cmd_ranges.insert(
1413                top_layer,
1414                LayerCommandRanges {
1415                    full_range: self.cmds.len()..self.cmds.len() + 1,
1416                    // Start with empty render_range; will be updated by `record_fill_cmd` and `pop_buf`.
1417                    render_range: self.cmds.len() + 1..self.cmds.len() + 1,
1418                },
1419            );
1420        } else if matches!(layer_kind, LayerKind::Clip(_)) {
1421            self.layer_cmd_ranges.entry(top_layer).and_modify(|ranges| {
1422                ranges.full_range.end = self.cmds.len() + 1;
1423                // Start with empty render_range; will be updated by `record_fill_cmd` and `pop_buf`.
1424                ranges.render_range = self.cmds.len() + 1..self.cmds.len() + 1;
1425            });
1426        }
1427        self.cmds.push(Cmd::PushBuf(layer_kind));
1428        self.layer_ids.push(layer_kind);
1429        self.n_bufs += 1;
1430    }
1431
1432    /// Pop the most recent buffer.
1433    pub fn pop_buf(&mut self) {
1434        let top_layer = self.layer_ids.pop().unwrap();
1435        let mut next_layer = *self.layer_ids.last().unwrap();
1436
1437        if matches!(self.cmds.last(), Some(&Cmd::PushBuf(_))) {
1438            // Optimization: If no drawing happened between the last `PushBuf`,
1439            // we can just pop it instead.
1440            self.cmds.pop();
1441        } else {
1442            self.layer_cmd_ranges
1443                .entry(top_layer.id())
1444                .and_modify(|ranges| {
1445                    ranges.full_range.end = self.cmds.len() + 1;
1446                });
1447            if top_layer.id() == next_layer.id() {
1448                next_layer = *self
1449                    .layer_ids
1450                    .get(self.layer_ids.len().saturating_sub(2))
1451                    .unwrap();
1452            }
1453
1454            self.layer_cmd_ranges
1455                .entry(next_layer.id())
1456                .and_modify(|ranges| {
1457                    ranges.full_range.end = self.cmds.len() + 1;
1458                    ranges.render_range.end = self.cmds.len() + 1;
1459                });
1460            self.cmds.push(Cmd::PopBuf);
1461        }
1462        self.n_bufs -= 1;
1463    }
1464
1465    /// Apply an opacity to the whole buffer.
1466    pub fn opacity(&mut self, opacity: f32) {
1467        if opacity != 1.0 {
1468            self.cmds.push(Cmd::Opacity(opacity));
1469        }
1470    }
1471
1472    /// Apply a filter effect to the whole buffer.
1473    pub fn filter(&mut self, layer_id: LayerId, filter: Filter) {
1474        self.cmds.push(Cmd::Filter(layer_id, filter));
1475    }
1476
1477    /// Apply a mask to the whole buffer.
1478    pub fn mask(&mut self, mask: Mask) {
1479        self.cmds.push(Cmd::Mask(mask));
1480    }
1481
1482    /// Blend the current buffer into the previous buffer in the stack.
1483    pub fn blend(&mut self, blend_mode: BlendMode) {
1484        // Optimization: If no drawing happened since the last `PushBuf` and the blend mode
1485        // is not destructive, we do not need to do any blending at all.
1486        if !matches!(self.cmds.last(), Some(&Cmd::PushBuf(_))) || blend_mode.is_destructive() {
1487            self.cmds.push(Cmd::Blend(blend_mode));
1488        }
1489    }
1490}
1491
1492/// Debug utilities for wide tiles.
1493///
1494/// These methods are only available in debug builds (`debug_assertions`).
1495/// They provide introspection into the command buffer for debugging and logging purposes.
1496#[cfg(debug_assertions)]
1497impl<const MODE: u8> WideTile<MODE> {
1498    /// Lists all commands in this wide tile with their indices and names.
1499    ///
1500    /// Returns a formatted string with each command on a new line, showing its index
1501    /// and human-readable name. This is useful for debugging and understanding the
1502    /// command sequence.
1503    ///
1504    /// # Example
1505    ///
1506    /// ```ignore
1507    /// let commands = wide_tile.list_commands();
1508    /// println!("{}", commands);
1509    /// // Output:
1510    /// // 0: PushBuf(Regular)
1511    /// // 1: FillPath
1512    /// // 2: PushZeroClip
1513    /// // 3: FillPath
1514    /// // 4: PopBuf
1515    /// ```
1516    #[allow(dead_code, reason = "useful for debugging")]
1517    pub fn list_commands(&self) -> String {
1518        self.cmds
1519            .iter()
1520            .enumerate()
1521            .map(|(i, cmd)| format!("{}: {}", i, cmd.name()))
1522            .collect::<Vec<_>>()
1523            .join("\n")
1524    }
1525}
1526
1527/// Optimization hint for fill operations, computed in `Wide::generate` and passed to `WideTile::fill`.
1528///
1529/// This enum communicates whether a fill operation can benefit from overdraw elimination:
1530/// - For opaque solid colors: we can set the background color directly and skip the fill
1531/// - For opaque images: we can clear previous commands but still need to emit the fill
1532#[derive(Debug, Clone, Copy)]
1533pub enum FillHint {
1534    /// No optimization possible, emit fill command normally.
1535    None,
1536    /// Paint is an opaque solid color - can replace background if conditions are met.
1537    OpaqueSolid(PremulColor),
1538    /// Paint is an opaque image - can clear previous commands if conditions are met.
1539    OpaqueImage,
1540}
1541
1542/// Distinguishes between different types of layers and their storage strategies.
1543///
1544/// Each layer kind determines how the layer's content is stored and processed:
1545/// - Regular layers are blended on-the-fly using a temporary buffer stack
1546/// - Filtered layers are materialized in persistent storage for filter processing
1547/// - Clip layers are special buffers used for clipping operations
1548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1549pub enum LayerKind {
1550    /// Regular layer using local `blend_buf` stack for temporary storage.
1551    Regular(LayerId),
1552    /// Filtered layer materialized in persistent `layer_manager` storage.
1553    Filtered(LayerId),
1554    /// Clip layer for clipping operations.
1555    Clip(LayerId),
1556}
1557
1558impl LayerKind {
1559    /// Get the underlying layer ID.
1560    ///
1561    /// All layer kinds contain a layer ID that uniquely identifies the layer.
1562    pub fn id(&self) -> LayerId {
1563        match self {
1564            Self::Regular(id) | Self::Filtered(id) | Self::Clip(id) => *id,
1565        }
1566    }
1567}
1568
1569/// A drawing command for wide tiles.
1570///
1571/// Commands are executed in order to render the final image. They include
1572/// drawing operations (`Fill`, `AlphaFill`), layer management (`PushBuf`, `PopBuf`),
1573/// clipping operations (`ClipFill`, `ClipStrip`), and post-processing effects
1574/// (`Filter`, `Blend`, `Opacity`, `Mask`).
1575#[derive(Debug, PartialEq)]
1576pub enum Cmd {
1577    /// Fill a rectangular region with a solid color or paint.
1578    Fill(CmdFill),
1579    /// Fill a region with a paint, modulated by an alpha mask.
1580    AlphaFill(CmdAlphaFill),
1581    /// Pushes a new buffer for drawing.
1582    /// Regular layers use the local `blend_buf` stack.
1583    /// Filtered layers are materialized in persistent layer storage.
1584    PushBuf(LayerKind),
1585    /// Pops the most recent buffer and blends it into the previous buffer.
1586    PopBuf,
1587    /// A fill command within a clipping region.
1588    ///
1589    /// This command will blend the contents of the current buffer within the clip fill region
1590    /// into the previous buffer in the stack.
1591    ClipFill(CmdClipFill),
1592    /// A fill command with alpha mask within a clipping region.
1593    ///
1594    /// This command will blend the contents of the current buffer within the clip fill region
1595    /// into the previous buffer in the stack, with an additional alpha mask.
1596    ClipStrip(CmdClipAlphaFill),
1597    /// Marks entry into a zero-winding clip region for a clipped filter layer.
1598    ///
1599    /// Zero-winding clips represent tiles completely outside the clip path. For clipped
1600    /// filter layers, this command allows the filter to process the full layer content
1601    /// before applying the clip as a mask (per SVG spec: filter → clip → mask → blend).
1602    PushZeroClip(LayerId),
1603    /// Marks exit from a zero-winding clip region for a clipped filter layer.
1604    PopZeroClip,
1605    /// Apply a filter effect to a layer's contents.
1606    ///
1607    /// This command applies a filter (e.g., blur, drop shadow) to the specified layer's
1608    /// rendered content. Per the SVG specification, filters are applied before clipping,
1609    /// masking, blending, and opacity operations.
1610    Filter(LayerId, Filter),
1611    /// Blend the current buffer into the previous buffer.
1612    ///
1613    /// This command blends the contents of the current buffer into the previous buffer
1614    /// using the specified blend mode (e.g., multiply, screen, overlay).
1615    Blend(BlendMode),
1616    /// Apply uniform opacity to the current buffer.
1617    ///
1618    /// Multiplies the alpha channel of all pixels in the buffer by the given opacity value.
1619    Opacity(f32),
1620    /// Apply a mask to the current buffer.
1621    ///
1622    /// Modulates the alpha channel of the buffer using the provided mask.
1623    Mask(Mask),
1624}
1625
1626#[cfg(debug_assertions)]
1627impl Cmd {
1628    /// Returns a human-readable name for this command.
1629    ///
1630    /// This is useful for debugging, logging, and displaying command information
1631    /// in a user-friendly format. To get detailed paint information, use `name_with_attrs`
1632    /// which can look up the paint from the command attributes.
1633    ///
1634    /// **Note:** This method is only available in debug builds (`debug_assertions`).
1635    pub fn name(&self) -> &'static str {
1636        match self {
1637            Self::Fill(_) => "FillPath",
1638            Self::AlphaFill(_) => "AlphaFillPath",
1639            Self::PushBuf(layer_kind) => match layer_kind {
1640                LayerKind::Regular(_) => "PushBuf(Regular)",
1641                LayerKind::Filtered(_) => "PushBuf(Filtered)",
1642                LayerKind::Clip(_) => "PushBuf(Clip)",
1643            },
1644            Self::PopBuf => "PopBuf",
1645            Self::ClipFill(_) => "ClipPathFill",
1646            Self::ClipStrip(_) => "ClipPathStrip",
1647            Self::PushZeroClip(_) => "PushZeroClip",
1648            Self::PopZeroClip => "PopZeroClip",
1649            Self::Filter(_, _) => "Filter",
1650            Self::Blend(_) => "Blend",
1651            Self::Opacity(_) => "Opacity",
1652            Self::Mask(_) => "Mask",
1653        }
1654    }
1655
1656    /// Returns a human-readable name for this command with detailed paint information.
1657    ///
1658    /// This variant looks up paint details from the command attributes for fill commands.
1659    ///
1660    /// **Note:** This method is only available in debug builds (`debug_assertions`).
1661    pub fn name_with_attrs(
1662        &self,
1663        fill_attrs: &[FillAttrs],
1664        encoded_paints: &[EncodedPaint],
1665    ) -> String {
1666        match self {
1667            Self::Fill(cmd) => {
1668                if let Some(attrs) = fill_attrs.get(cmd.attrs_idx as usize) {
1669                    format!("FillPath({})", paint_name(&attrs.paint, encoded_paints))
1670                } else {
1671                    format!("FillPath(attrs_idx={})", cmd.attrs_idx)
1672                }
1673            }
1674            Self::AlphaFill(cmd) => {
1675                if let Some(attrs) = fill_attrs.get(cmd.attrs_idx as usize) {
1676                    format!(
1677                        "AlphaFillPath({})",
1678                        paint_name(&attrs.paint, encoded_paints)
1679                    )
1680                } else {
1681                    format!("AlphaFillPath(attrs_idx={})", cmd.attrs_idx)
1682                }
1683            }
1684            _ => self.name().into(),
1685        }
1686    }
1687}
1688
1689/// Returns a human-readable description of a paint.
1690#[cfg(debug_assertions)]
1691fn paint_name(paint: &Paint, encoded_paints: &[EncodedPaint]) -> String {
1692    match paint {
1693        Paint::Solid(color) => {
1694            let rgba = color.as_premul_rgba8();
1695            format!(
1696                "Solid(#{:02x}{:02x}{:02x}{:02x})",
1697                rgba.r, rgba.g, rgba.b, rgba.a
1698            )
1699        }
1700        Paint::Indexed(idx) => {
1701            let index = idx.index();
1702            if let Some(encoded) = encoded_paints.get(index) {
1703                let kind = match encoded {
1704                    EncodedPaint::Gradient(g) => match &g.kind {
1705                        crate::encode::EncodedKind::Linear(_) => "LinearGradient",
1706                        crate::encode::EncodedKind::Radial(_) => "RadialGradient",
1707                        crate::encode::EncodedKind::Sweep(_) => "SweepGradient",
1708                    },
1709                    EncodedPaint::Image(_) => "Image",
1710                    EncodedPaint::BlurredRoundedRect(_) => "BlurredRoundedRect",
1711                };
1712                format!("{}[{}]", kind, index)
1713            } else {
1714                format!("Indexed({})", index)
1715            }
1716        }
1717    }
1718}
1719
1720/// Shared attributes for alpha fill commands.
1721#[derive(Debug, Clone, PartialEq)]
1722pub struct FillAttrs {
1723    /// The index of the thread that owns the alpha buffer
1724    /// containing the mask values at `alpha_idx`.
1725    /// Always 0 in single-threaded mode.
1726    pub thread_idx: u8,
1727    /// The paint (color, gradient, etc.) to fill the region with.
1728    // TODO: Store premultiplied colors as indexed paints as well, to reduce
1729    // memory overhead? Or get rid of indexed paints and inline all paints?
1730    pub paint: Paint,
1731    /// The blend mode to apply before drawing the contents.
1732    pub blend_mode: BlendMode,
1733    /// A mask to apply to the command.
1734    pub mask: Option<Mask>,
1735    /// Base index into the alpha buffer for this path's commands.
1736    /// Commands store a relative offset that is added to this base.
1737    alpha_base_idx: u32,
1738}
1739
1740impl FillAttrs {
1741    /// Compute the absolute alpha buffer index from a relative offset.
1742    pub fn alpha_idx(&self, offset: u32) -> u32 {
1743        self.alpha_base_idx + offset
1744    }
1745}
1746
1747/// Shared attributes for clip alpha fill commands.
1748#[derive(Debug, Clone, PartialEq, Eq)]
1749pub struct ClipAttrs {
1750    /// The index of the thread that owns the alpha buffer
1751    /// containing the mask values at `alpha_idx`.
1752    /// Always 0 in single-threaded mode.
1753    pub thread_idx: u8,
1754    /// Base index into the alpha buffer for this clip path's commands.
1755    /// Commands store a relative offset that is added to this base.
1756    alpha_base_idx: u32,
1757}
1758
1759impl ClipAttrs {
1760    /// Compute the absolute alpha buffer index from a relative offset.
1761    pub fn alpha_idx(&self, offset: u32) -> u32 {
1762        self.alpha_base_idx + offset
1763    }
1764}
1765
1766/// Container for shared command attributes.
1767///
1768/// This struct holds the shared attributes for fill and clip commands,
1769/// allowing them to be passed together to functions that need both.
1770#[derive(Debug, Default, Clone)]
1771pub struct CommandAttrs {
1772    /// Shared attributes for fill commands, indexed by `attrs_idx` in `CmdFill`/`CmdAlphaFill`.
1773    pub fill: Vec<FillAttrs>,
1774    /// Shared attributes for clip commands, indexed by `attrs_idx` in `CmdClipAlphaFill`.
1775    pub clip: Vec<ClipAttrs>,
1776}
1777
1778impl CommandAttrs {
1779    /// Clear all attributes.
1780    pub fn clear(&mut self) {
1781        self.fill.clear();
1782        self.clip.clear();
1783    }
1784}
1785
1786/// Fill a consecutive horizontal region of a wide tile.
1787///
1788/// This command fills a rectangular region with the specified paint.
1789/// The region starts at x-coordinate `x` and extends for `width` pixels
1790/// horizontally, spanning the full height of the wide tile.
1791#[derive(Debug, Clone, PartialEq, Eq)]
1792pub struct CmdFill {
1793    /// The horizontal start position relative to the wide tile's left edge, in pixels.
1794    pub x: u16,
1795    /// The width of the filled region in pixels.
1796    pub width: u16,
1797    /// Index into the command attributes array.
1798    pub attrs_idx: u32,
1799}
1800
1801/// Fill a consecutive horizontal region with an alpha mask.
1802///
1803/// Similar to `CmdFill`, but modulates the paint by an alpha mask stored
1804/// in a separate buffer. This is used for anti-aliased edges and partial
1805/// coverage from path rasterization.
1806#[derive(Debug, Clone, PartialEq, Eq)]
1807pub struct CmdAlphaFill {
1808    /// The horizontal start position relative to the wide tile's left edge, in pixels.
1809    pub x: u16,
1810    /// The width of the filled region in pixels.
1811    pub width: u16,
1812    /// Relative offset to the alpha buffer location.
1813    /// Use `FillAttrs::alpha_idx(alpha_offset)` to compute the absolute index.
1814    pub alpha_offset: u32,
1815    /// Index into the command attributes array.
1816    pub attrs_idx: u32,
1817}
1818
1819/// Fill operation within a clipping region.
1820///
1821/// This command copies a horizontal region from the top of the clip buffer stack
1822/// to the next buffer on the stack, effectively rendering the clipped content.
1823/// Unlike `CmdFill`, this doesn't fill with a paint but transfers existing content.
1824#[derive(Debug, PartialEq, Eq)]
1825pub struct CmdClipFill {
1826    /// The horizontal start position relative to the wide tile's left edge, in pixels.
1827    pub x: u16,
1828    /// The width of the region to copy in pixels.
1829    pub width: u16,
1830}
1831
1832/// Alpha-masked fill operation within a clipping region.
1833///
1834/// This command composites a horizontal region from the top of the clip buffer stack
1835/// to the next buffer, modulated by an alpha mask. This is used for anti-aliased
1836/// clip edges.
1837#[derive(Debug, PartialEq, Eq)]
1838pub struct CmdClipAlphaFill {
1839    /// The horizontal start position relative to the wide tile's left edge, in pixels.
1840    pub x: u16,
1841    /// The width of the region to composite in pixels.
1842    pub width: u16,
1843    /// Relative offset to the alpha buffer location.
1844    /// Use `ClipAttrs::alpha_idx(alpha_offset)` to compute the absolute index.
1845    pub alpha_offset: u32,
1846    /// Index into the clip attributes array.
1847    pub attrs_idx: u32,
1848}
1849
1850trait BlendModeExt {
1851    /// Whether a blend mode might cause destructive changes in the backdrop.
1852    /// This disallows certain optimizations (like for example inlining a blend mode
1853    /// or only applying a blend mode to the current clipping area).
1854    fn is_destructive(&self) -> bool;
1855}
1856
1857impl BlendModeExt for BlendMode {
1858    fn is_destructive(&self) -> bool {
1859        matches!(
1860            self.compose,
1861            Compose::Clear
1862                | Compose::Copy
1863                | Compose::SrcIn
1864                | Compose::DestIn
1865                | Compose::SrcOut
1866                | Compose::DestAtop
1867        )
1868    }
1869}
1870
1871/// Ranges of commands for a specific layer in a specific tile.
1872///
1873/// This structure tracks two different ranges of commands:
1874/// - The full range includes all layer operations (push, draw, pop)
1875/// - The render range includes only the actual drawing commands
1876#[derive(Debug, Clone, Default)]
1877pub struct LayerCommandRanges {
1878    /// Full range including `PushBuf`, all commands, and `PopBuf`.
1879    pub full_range: Range<usize>,
1880    /// Range containing only fill commands (`Fill`, `AlphaFill`).
1881    /// This is the range to replace when sampling from a filtered layer.
1882    pub render_range: Range<usize>,
1883}
1884
1885impl LayerCommandRanges {
1886    /// Clear the full range and render range.
1887    #[inline]
1888    pub fn clear(&mut self) {
1889        self.full_range = 0..0;
1890        self.render_range = 0..0;
1891    }
1892}
1893
1894#[cfg(test)]
1895mod tests {
1896    use crate::coarse::{FillHint, LayerKind, MODE_CPU, Wide, WideTile};
1897    use crate::kurbo::Affine;
1898    use crate::peniko::{BlendMode, Compose, Mix};
1899    use crate::render_graph::RenderGraph;
1900    use crate::strip::Strip;
1901    use alloc::{boxed::Box, vec};
1902
1903    #[test]
1904    fn optimize_empty_layers() {
1905        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1906        wide.push_buf(LayerKind::Regular(0));
1907        wide.pop_buf();
1908
1909        assert!(wide.cmds.is_empty());
1910    }
1911
1912    #[test]
1913    fn basic_layer() {
1914        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1915        wide.push_buf(LayerKind::Regular(0));
1916        wide.fill(0, 10, 0, 0, FillHint::None);
1917        wide.fill(10, 10, 0, 0, FillHint::None);
1918        wide.pop_buf();
1919
1920        assert_eq!(wide.cmds.len(), 4);
1921    }
1922
1923    #[test]
1924    fn dont_inline_blend_with_two_fills() {
1925        let blend_mode = BlendMode::new(Mix::Lighten, Compose::SrcOver);
1926
1927        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1928        wide.push_buf(LayerKind::Regular(0));
1929        wide.fill(0, 10, 0, 0, FillHint::None);
1930        wide.fill(10, 10, 0, 0, FillHint::None);
1931        wide.blend(blend_mode);
1932        wide.pop_buf();
1933
1934        assert_eq!(wide.cmds.len(), 5);
1935    }
1936
1937    #[test]
1938    fn dont_inline_destructive_blend() {
1939        let blend_mode = BlendMode::new(Mix::Lighten, Compose::Clear);
1940
1941        let mut wide = WideTile::<MODE_CPU>::new(0, 0);
1942        wide.push_buf(LayerKind::Regular(0));
1943        wide.fill(0, 10, 0, 0, FillHint::None);
1944        wide.blend(blend_mode);
1945        wide.pop_buf();
1946
1947        assert_eq!(wide.cmds.len(), 4);
1948    }
1949
1950    #[test]
1951    fn tile_coordinates() {
1952        let wide = Wide::<MODE_CPU>::new(1000, 258);
1953
1954        let tile_1 = wide.get(1, 3);
1955        assert_eq!(tile_1.x, 256);
1956        assert_eq!(tile_1.y, 12);
1957
1958        let tile_2 = wide.get(2, 15);
1959        assert_eq!(tile_2.x, 512);
1960        assert_eq!(tile_2.y, 60);
1961    }
1962
1963    #[test]
1964    fn reset_clears_layer_and_clip_stacks() {
1965        type ClipPath = Option<Box<[Strip]>>;
1966
1967        let mut wide = Wide::<MODE_CPU>::new(1000, 258);
1968        let mut render_graph = RenderGraph::new();
1969        let no_clip_path: ClipPath = None;
1970        wide.push_layer(
1971            1,
1972            no_clip_path,
1973            BlendMode::default(),
1974            None,
1975            0.5,
1976            None,
1977            Affine::IDENTITY,
1978            &mut render_graph,
1979            0,
1980        );
1981
1982        assert_eq!(wide.layer_stack.len(), 1);
1983        assert_eq!(wide.clip_stack.len(), 0);
1984
1985        let strip = Strip::new(2, 2, 0, true);
1986        let clip_path = Some(vec![strip].into_boxed_slice());
1987        wide.push_layer(
1988            2,
1989            clip_path,
1990            BlendMode::default(),
1991            None,
1992            0.09,
1993            None,
1994            Affine::IDENTITY,
1995            &mut render_graph,
1996            0,
1997        );
1998
1999        assert_eq!(wide.layer_stack.len(), 2);
2000        assert_eq!(wide.clip_stack.len(), 1);
2001
2002        wide.reset();
2003
2004        assert_eq!(wide.layer_stack.len(), 0);
2005        assert_eq!(wide.clip_stack.len(), 0);
2006    }
2007}