Skip to main content

vello_cpu/coarse/
bucketer.rs

1// Copyright 2026 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use super::cmd::{DepthFill, LayerFill, LayerFillAttrs, PaintFill, PaintFillAttrs, RenderCmd};
5use super::depth::{DepthSegment, DepthState};
6use crate::coarse::depth;
7use crate::filter::context::FilterContext;
8use crate::kurbo::{Affine, Vec2};
9use crate::peniko::{BlendMode, Extend, ImageQuality, ImageSampler};
10use crate::record::RecordedFill;
11use crate::util::Span;
12use alloc::sync::Arc;
13use alloc::vec;
14use alloc::vec::Vec;
15use vello_common::encode::{EncodedImage, EncodedPaint};
16use vello_common::filter::FilterLayerPlacement;
17use vello_common::geometry::RectU16;
18use vello_common::mask::Mask;
19use vello_common::paint::{ImageSource, IndexedPaint, Paint};
20use vello_common::pixmap::Pixmap;
21use vello_common::record::{LayerClip, LayerProps, Node, RecordedLayer, RecordedLayerKind};
22use vello_common::strip::{Strip, visit_strip_fill_segments};
23use vello_common::tile::Tile;
24use vello_common::util::{Clear, RectExt, RetainVec, VecPool};
25
26/// State for a single row of strips.
27#[derive(Debug, Default)]
28pub(crate) struct RowState {
29    /// Normal render commands rendered in back-to-front with depth buffer read.
30    pub(crate) render_cmds: Vec<RenderCmd>,
31    /// Opaque fill commands rendered front-to-back with depth buffer read and write.
32    pub(crate) depth_cmds: Vec<DepthFill>,
33    /// State of the depth buffer for this row.
34    depth: DepthState,
35    /// Current layer depth.
36    pub(super) layer_depth: usize,
37    layer_stack: Vec<RowLayerState>,
38}
39
40/// Each recorded layer by itself already stores a bounding box.
41/// However, we want to take this one step further and track a horizontal bounding
42/// box of each row band individually. This allows us to save a lot of work when clearing buffers
43/// during fine rasterization.
44///
45/// For example, let's say that we are rendering a triangle with the points
46/// (0, 0), (500, 0) and (250, 250). The bounding box would be (0, 0) and (500, 500). However,
47/// the triangle gets much more narrow as we advance through the rows. If we just chose
48/// the coarse bounding box, we would always end up clearing a whole width of 500 pixels during
49/// fine rasterization. By tracking the bounding box per row, we can reduce the work to the
50/// absolute minimum necessary for each row.
51#[derive(Debug)]
52struct RowLayerState {
53    /// The index in the command stream where the `PushBuf` command of the corresponding
54    /// layer lives.
55    push_cmd_idx: usize,
56    /// The horizontal span of the layer.
57    span: Option<Span>,
58}
59
60impl RowState {
61    pub(super) fn new() -> Self {
62        Self::default()
63    }
64
65    fn clear(&mut self) {
66        self.render_cmds.clear();
67        self.depth_cmds.clear();
68        self.depth.reset();
69        self.layer_depth = 0;
70        self.layer_stack.clear();
71    }
72
73    #[inline]
74    pub(super) fn push_cmd(&mut self, cmd: RenderCmd) {
75        match cmd {
76            RenderCmd::PaintFill(cmd) => self.include_current_span(cmd.span),
77            RenderCmd::LayerFill(cmd) => self.include_current_span(cmd.span),
78            RenderCmd::PushBuf(_) | RenderCmd::PopBuf => {}
79        }
80
81        self.render_cmds.push(cmd);
82    }
83
84    #[inline]
85    pub(super) fn push_buf(&mut self) {
86        let push_cmd_idx = self.render_cmds.len();
87        self.render_cmds.push(RenderCmd::PushBuf(None));
88        self.layer_stack.push(RowLayerState {
89            push_cmd_idx,
90            span: None,
91        });
92        self.layer_depth += 1;
93    }
94
95    #[inline]
96    pub(super) fn pop_buf(&mut self) {
97        let layer = self.layer_stack.pop().unwrap();
98        match &mut self.render_cmds[layer.push_cmd_idx] {
99            RenderCmd::PushBuf(push_span) => *push_span = layer.span,
100            _ => unreachable!("layer stack must point to a PushBuf command"),
101        }
102
103        self.render_cmds.push(RenderCmd::PopBuf);
104
105        self.layer_depth -= 1;
106    }
107
108    #[inline]
109    pub(super) fn push_depth_fill(&mut self, cmd: DepthFill, draw_id: u32) {
110        let span = cmd.span();
111        self.depth.include_span(span, draw_id);
112        self.depth_cmds.push(cmd);
113    }
114
115    #[inline]
116    pub(crate) fn can_skip_depth(&self, span: Span, draw_id: u32) -> bool {
117        self.depth.can_skip(span, draw_id)
118    }
119
120    #[inline]
121    fn include_current_span(&mut self, span: Span) {
122        if let Some(layer) = self.layer_stack.last_mut() {
123            match &mut layer.span {
124                Some(layer_span) => layer_span.extend(span),
125                None => layer.span = Some(span),
126            }
127        }
128    }
129}
130
131impl Clear for RowState {
132    fn clear(&mut self) {
133        Self::clear(self);
134    }
135}
136
137fn debug_assert_tile_aligned(point: (u16, u16), description: &str) {
138    debug_assert_eq!(
139        point.0 % Tile::WIDTH,
140        0,
141        "{description} must be tile-width aligned",
142    );
143    debug_assert_eq!(
144        point.1 % Tile::HEIGHT,
145        0,
146        "{description} must be tile-height aligned",
147    );
148}
149
150/// A bucketer that groups commands into strip-row-sized buckets.
151#[derive(Debug)]
152pub(crate) struct CommandBucketer {
153    /// The viewport of the root/filter layer we are currently bucketing.
154    viewport: RectU16,
155    /// The currently active stack of clip bboxes (from layer clips), always anchored at the
156    /// (0, 0) origin, regardless of the viewport, tracked for two reasons:
157    /// - So we can clamp fill commands to the bbox and avoid unnecessary rendering work.
158    /// - In case we have a filter layer with a clip, it all happens in three steps:
159    ///   1) We first push a new intermediate layer with the clip.
160    ///   2) We composite the whole filter layer **cropped to the active clip bbox**. Note that
161    ///      the filter layer itself is _not_ affected by the clip bbox, as filter layers should
162    ///      always be fully rendered before applying clipping. However, by limiting the composition
163    ///      to the coarse clip bbox we might be able to save a lot of work if only a small part
164    ///      of the filter layer is visible.
165    ///   3) Pop the intermediate layer, which will take care of doing the fine-grained clipping
166    ///      (e.g. applying anti-aliasing from the clip path).
167    pub(super) clip_bboxes: Vec<RectU16>,
168    /// The actual render commands for each strip row.
169    ///
170    /// Since this is essentially a 2D-array, we use [`RetainVec`] to preserve inner allocations
171    /// upon resetting.
172    pub(super) rows: RetainVec<RowState>,
173    pub(crate) paint_fill_attrs: Vec<PaintFillAttrs>,
174    pub(crate) layer_fill_attrs: Vec<LayerFillAttrs>,
175    pub(crate) filter_paints: Vec<EncodedPaint>,
176    /// Keeping track of currently active layers to enable lazy layer pushing.
177    pub(super) active_layers: Vec<ActiveLayer>,
178    /// A vector pool for keeping track of occupied rows in a layer.
179    occupied_rows_pool: VecPool<usize>,
180    /// Scratch space used when replaying clip strips while popping clipped layers.
181    occupied_rows_bool_scratch: Vec<bool>,
182    /// A counter to assign monotonically increasing IDs to draws to enable depth buffer rendering.
183    pub(super) next_draw_id: u32,
184}
185
186impl CommandBucketer {
187    pub(crate) fn from_wh(width: u16, height: u16) -> Self {
188        Self::new(RectU16::new(0, 0, width, height))
189    }
190
191    pub(crate) fn new(mut viewport: RectU16) -> Self {
192        // It's _very_ important that we snap to tile coordinates. Fine rasterization assumes
193        // that the width is a multiple of the tile width, so if that's not the case bad things
194        // could happen!
195        viewport = viewport.snap_to_tile_coordinates();
196        let clip_bbox = RectU16::new(0, 0, viewport.width(), viewport.height());
197        // Note: `clip_bbox` is already snapped to tile coordinates because `viewport` is, so no
198        // need to `div_ceil` here.
199        let num_rows = usize::from(clip_bbox.height() / Tile::HEIGHT);
200
201        Self {
202            viewport,
203            clip_bboxes: vec![clip_bbox],
204            rows: RetainVec::with_len(num_rows, RowState::new),
205            paint_fill_attrs: Vec::new(),
206            layer_fill_attrs: Vec::new(),
207            filter_paints: Vec::new(),
208            active_layers: Vec::new(),
209            occupied_rows_pool: VecPool::default(),
210            occupied_rows_bool_scratch: vec![false; num_rows],
211            // It is important to start at 1, because the depth buffer uses 0 for "no entries yet".
212            next_draw_id: 1,
213        }
214    }
215
216    fn bbox_span(bbox: RectU16) -> Span {
217        // Bbox might be empty vertically but not horizontally. In this case,
218        // it should still be considered a zero-sized span, though.
219        // TODO: Discard empty layers in an earlier stage.
220        if bbox.is_empty() {
221            Span::new(bbox.x0, 0)
222        } else {
223            Span::new(bbox.x0, bbox.x1 - bbox.x0)
224        }
225    }
226
227    pub(crate) fn rows(&self) -> &[RowState] {
228        self.rows.as_slice()
229    }
230
231    pub(crate) fn width(&self) -> u16 {
232        self.clip_bboxes[0].width()
233    }
234
235    fn viewport_origin(&self) -> (u16, u16) {
236        (self.viewport.x0, self.viewport.y0)
237    }
238
239    pub(crate) fn reset(&mut self, mut viewport: RectU16) {
240        // See comments in `CommandBucketer::reset`.
241        viewport = viewport.snap_to_tile_coordinates();
242        let clip_bbox = RectU16::new(0, 0, viewport.width(), viewport.height());
243        let num_rows = usize::from(viewport.height() / Tile::HEIGHT);
244
245        self.rows.clear();
246        self.rows.resize_with(num_rows, RowState::new);
247        self.paint_fill_attrs.clear();
248        self.layer_fill_attrs.clear();
249        self.filter_paints.clear();
250        for layer in self.active_layers.drain(..) {
251            self.occupied_rows_pool.submit(layer.occupied_rows);
252        }
253        self.occupied_rows_bool_scratch.clear();
254        self.occupied_rows_bool_scratch.resize(num_rows, false);
255        self.next_draw_id = 1;
256        self.viewport = viewport;
257        self.clip_bboxes.truncate(1);
258        self.clip_bboxes[0] = clip_bbox;
259    }
260
261    fn next_draw_id(&mut self) -> u32 {
262        let draw_id = self.next_draw_id;
263        self.next_draw_id += 1;
264
265        draw_id
266    }
267
268    #[inline(always)]
269    pub(super) fn ensure_row_layers(&mut self, row_idx: usize) {
270        let layer_depth = self.rows[row_idx].layer_depth;
271        if layer_depth == self.active_layers.len() {
272            return;
273        }
274
275        for layer_idx in layer_depth..self.active_layers.len() {
276            self.rows[row_idx].push_buf();
277            self.active_layers[layer_idx].occupied_rows.push(row_idx);
278        }
279    }
280
281    pub(crate) fn bucket_commands(
282        &mut self,
283        nodes: &[Node],
284        draws: &[RecordedFill],
285        layers: &[RecordedLayer],
286        strips: &[Strip],
287        encoded_paints: &[EncodedPaint],
288        filter_ctx: &FilterContext,
289    ) {
290        // When rendering filter layers, we always anchor them so that the top-left of the bounding
291        // box lands at (0, 0), even if the bounding box's top-left is for example at (200, 200).
292        // This is to ensure that the pixmap is as small as possible. Otherwise, if we for example
293        // had a filter layer that draws something small in the bottom right but nowhere else, we
294        // would still need to allocate a full viewport-sized pixmap!
295        // Therefore, we need to keep track of this offset so that for example paints know that
296        // they should actually be sampled at (200, 200) instead of (0, 0).
297
298        debug_assert_tile_aligned(self.viewport_origin(), "viewport origin");
299
300        for node in nodes {
301            for RecordedFill {
302                thread_idx,
303                strip_range,
304                paint,
305                blend_mode,
306                mask,
307            } in node.draws_in(draws)
308            {
309                let draw_id = self.next_draw_id();
310                let attrs = PaintFillAttrs {
311                    paint: paint.clone(),
312                    blend_mode: *blend_mode,
313                    mask: mask.clone(),
314                    draw_id,
315                    thread_idx: *thread_idx,
316                    origin: self.viewport_origin(),
317                };
318                self.generate_fill(&strips[strip_range.clone()], &attrs, encoded_paints);
319            }
320
321            if let Some(id) = node.layer {
322                let layer = &layers[id as usize];
323                let props = &layer.props;
324
325                match &layer.kind {
326                    RecordedLayerKind::Regular => {
327                        // Regular layers are inlined and bucketed into the same command stream.
328                        self.push_layer(props);
329                        // TODO: Avoid recursion to prevent stack overflows for deeply nested
330                        // layers.
331                        self.bucket_commands(
332                            &layer.nodes,
333                            draws,
334                            layers,
335                            strips,
336                            encoded_paints,
337                            filter_ctx,
338                        );
339                        self.pop_layer(strips);
340                    }
341                    RecordedLayerKind::Filter { placement, .. } => {
342                        let needs_layer = props.blend_mode != BlendMode::default()
343                            || props.opacity != 1.0
344                            || props.mask.is_some()
345                            || props.clip_path.is_some();
346
347                        if needs_layer {
348                            self.push_layer(props);
349                        }
350
351                        // Note: At this point, we've already rasterized all dependent
352                        // filter layers, so this should never fail.
353                        if let Some(pixmap) = filter_ctx.filter_layer(id as usize) {
354                            self.generate_filter_layer_fill(
355                                pixmap,
356                                *placement,
357                                encoded_paints.len(),
358                            );
359                        }
360
361                        if needs_layer {
362                            self.pop_layer(strips);
363                        }
364                    }
365                }
366            }
367        }
368    }
369
370    pub(crate) fn push_layer(&mut self, props: &LayerProps) {
371        let parent_bbox = *self.clip_bboxes.last().unwrap();
372        let bbox = props
373            .clip_path
374            .as_ref()
375            .map(|clip| {
376                // Make sure to translate the clip path from viewport-space to local space, since
377                // `clip_bboxes` uses this coordinate system.
378                let clip_bbox = clip.bbox.relative_to_origin(self.viewport_origin());
379                clip_bbox.intersect(parent_bbox)
380            })
381            .unwrap_or(parent_bbox);
382
383        if props.clip_path.is_some() {
384            self.clip_bboxes.push(bbox);
385        }
386
387        self.active_layers.push(ActiveLayer {
388            // TODO: Masks are currently probably broken if they are used inside of a filter layer,
389            // since they aren't shifted and also only work if the mask has the same dimensions as
390            // our allocated filter layer.
391            mask: props.mask.clone(),
392            blend_mode: props.blend_mode,
393            opacity: props.opacity,
394            clip: props.clip_path.clone(),
395            span: Self::bbox_span(bbox),
396            occupied_rows: self.occupied_rows_pool.take(),
397        });
398
399        // If the blend mode is destructive, we need to eagerly push to all rows in the clip bbox,
400        // since even areas where we didn't draw anything need to be blended with the destructive
401        // blend mode.
402        if props.blend_mode.is_destructive() && !bbox.is_empty() {
403            let row_start = usize::from(bbox.y0 / Tile::HEIGHT);
404            let row_end = usize::from(bbox.y1.div_ceil(Tile::HEIGHT)).min(self.rows.len());
405            for row_idx in row_start..row_end {
406                self.ensure_row_layers(row_idx);
407            }
408        }
409    }
410
411    pub(crate) fn pop_layer(&mut self, strips: &[Strip]) {
412        let mut layer = self.active_layers.pop().unwrap();
413        let opacity = layer.opacity;
414        let blend_mode = layer.blend_mode;
415
416        // Two cases that need to be distinguished.
417        //
418        // If no clip was associated, we simply iterate over all rows that
419        // were lazily associated with some rendered contents and emit a `LayerFill`
420        // instructions across the whole width of the bounding box of the layer.
421        //
422        // If there _was_ a clip, things get trickier because the `LayerFill` commands
423        // need to be generated on a more fine-grained basis, and might in certain cases
424        // also require anti-aliasing.
425
426        if let Some(clip) = layer.clip {
427            let attrs_idx = self.layer_fill_attrs.len() as u32;
428            let draw_id = self.next_draw_id();
429
430            self.layer_fill_attrs.push(LayerFillAttrs {
431                blend_mode,
432                opacity,
433                mask: layer.mask.clone(),
434                draw_id,
435                thread_idx: clip.thread_idx,
436            });
437            self.clip_bboxes.pop();
438
439            // Note: The clip strips themselves are still in viewport space, not in local space.
440            // They will be converted to local space when calling `generate`.
441            let clip_strips = &strips[clip.strip_range];
442
443            // We need to make sure that we only emit `LayerFill` commands for rows that actually
444            // have been pushed into. If we don't have a destructive blend mode and only parts of
445            // the layer were painted, it can happen that the clip path itself covers a row
446            // that actually doesn't have an associated `PushBuf` command.
447            // For such rows, we don't want to emit any layer fill commands, because there is
448            // nothing to fill into in the first place!
449
450            // `occupied_rows` stores the indices of all rows that have been marked, but they
451            // are not sorted or anything. Therefore, we convert it into an indexable array
452            // such that in the `generate` closure, we can easily check whether the row is included
453            // or now.
454            for &row_idx in &layer.occupied_rows {
455                self.occupied_rows_bool_scratch[row_idx] = true;
456            }
457
458            // Only generate layer fill commands if it actually lies within a row that has
459            // been touched by the contents of the layer.
460            self.generate(
461                clip_strips,
462                |bucketer, fill| {
463                    if bucketer.occupied_rows_bool_scratch[fill.row_idx] {
464                        bucketer.rows[fill.row_idx].push_cmd(RenderCmd::LayerFill(LayerFill::new(
465                            fill.span, None, attrs_idx,
466                        )));
467                    }
468                },
469                |bucketer, fill| {
470                    if bucketer.occupied_rows_bool_scratch[fill.row_idx] {
471                        bucketer.rows[fill.row_idx].push_cmd(RenderCmd::LayerFill(LayerFill::new(
472                            fill.span,
473                            Some(fill.alpha_idx),
474                            attrs_idx,
475                        )));
476                    }
477                },
478            );
479
480            for row_idx in layer.occupied_rows.drain(..) {
481                self.rows[row_idx].pop_buf();
482                // Make sure to reset it so that by the end, the vector is all `false` again.
483                self.occupied_rows_bool_scratch[row_idx] = false;
484            }
485
486            self.occupied_rows_pool.submit(layer.occupied_rows);
487        } else {
488            let attrs_idx = self.layer_fill_attrs.len() as u32;
489            let draw_id = self.next_draw_id();
490
491            self.layer_fill_attrs.push(LayerFillAttrs {
492                blend_mode,
493                opacity,
494                mask: layer.mask.clone(),
495                draw_id,
496                thread_idx: 0,
497            });
498
499            for row_idx in layer.occupied_rows.drain(..) {
500                let row = &mut self.rows[row_idx];
501
502                // TODO: Instead of always pushing the full layer bbox across all rows, it
503                // would be nice to instead only emit the per-row bounding box.
504                row.push_cmd(RenderCmd::LayerFill(LayerFill::new(
505                    layer.span, None, attrs_idx,
506                )));
507                row.pop_buf();
508            }
509
510            self.occupied_rows_pool.submit(layer.occupied_rows);
511        }
512    }
513
514    pub(crate) fn generate_filter_layer_fill(
515        &mut self,
516        pixmap: Arc<Pixmap>,
517        placement: FilterLayerPlacement,
518        static_paint_count: usize,
519    ) {
520        let origin = self.viewport_origin();
521        let dest_bbox = placement.dest_bbox;
522        let src_sample_shift = placement.src_origin();
523
524        // Here, we want to determine the absolute transform that needs to be applied to the filter
525        // image for it to be placed correctly. On the one hand, we position it such that it starts
526        // at the top-left of `dest_bbox`, which is the intended location the filter layer should
527        // be composited into. On the other hand, we optionally apply a correction to ensure we
528        // are sampling from the right location of the filter pixmap (see
529        // `FilterLayerPlacement::new` for a more detailed description of how/what this offset
530        // represents).
531        let src_offset = (
532            i32::from(src_sample_shift.0) - i32::from(dest_bbox.x0),
533            i32::from(src_sample_shift.1) - i32::from(dest_bbox.y0),
534        );
535
536        // Now that we have determined the image transform, we next need to determine which
537        // strip rows in the bucketer should actually be emitted. Keep in mind that regardless
538        // of what `viewport_origin` is, when actually rendering we always shift the origin to
539        // (0, 0). (Note: We did _not_ do this for computing `src_offset` because the shift for the
540        // paint itself will be applied later when resolving the indexed paint)
541        let dest_bbox = dest_bbox.relative_to_origin(origin);
542        let clip_bbox = *self.clip_bboxes.last().unwrap();
543        // As noted in [`CommandBucketer::clip_bboxes`], we only need to composite the parts
544        // of the filter layer that actually lie within clip bounding box.
545        // `clip_bbox` already is in viewport-local coordinates, so we don't need to shift it
546        // like we did for `dest_bbox`.
547        let clipped_dest_bbox = dest_bbox.intersect(clip_bbox);
548        if clipped_dest_bbox.is_empty() {
549            return;
550        }
551
552        let draw_id = self.next_draw_id();
553        let span = Self::bbox_span(clipped_dest_bbox);
554        let paint_idx = static_paint_count + self.filter_paints.len();
555        self.filter_paints.push(EncodedPaint::Image(EncodedImage {
556            source: ImageSource::Pixmap(pixmap),
557            sampler: ImageSampler {
558                x_extend: Extend::Pad,
559                y_extend: Extend::Pad,
560                quality: ImageQuality::Low,
561                alpha: 1.0,
562            },
563            may_have_transparency: true,
564            transform: Affine::translate((f64::from(src_offset.0), f64::from(src_offset.1))),
565            x_advance: Vec2::new(1.0, 0.0),
566            y_advance: Vec2::new(0.0, 1.0),
567            tint: None,
568        }));
569        let attrs_idx = self.paint_fill_attrs.len() as u32;
570        self.paint_fill_attrs.push(PaintFillAttrs {
571            paint: Paint::Indexed(IndexedPaint::new(paint_idx)),
572            blend_mode: BlendMode::default(),
573            mask: None,
574            draw_id,
575            thread_idx: 0,
576            origin,
577        });
578        let row_start = usize::from(clipped_dest_bbox.y0 / Tile::HEIGHT);
579        let row_end = usize::from(clipped_dest_bbox.y1.div_ceil(Tile::HEIGHT));
580        for row_idx in row_start..row_end {
581            self.push_fill(GeneratedFill { row_idx, span }, attrs_idx, None);
582        }
583    }
584
585    pub(crate) fn generate_fill(
586        &mut self,
587        strip_buf: &[Strip],
588        attrs: &PaintFillAttrs,
589        encoded_paints: &[EncodedPaint],
590    ) {
591        if strip_buf.is_empty() {
592            return;
593        }
594
595        debug_assert_ne!(attrs.draw_id, 0, "fill draw IDs should start at 1");
596
597        let attrs_idx = self.paint_fill_attrs.len() as u32;
598        self.paint_fill_attrs.push(attrs.clone());
599
600        let draw_id =
601            // While in certain cases it _might_ be okay to use depth culling while inside of
602            // a layer, it can get very finicky with blend modes etc., so we just outright
603            // reject those for now.
604            (self.active_layers.is_empty()
605                && attrs.blend_mode == BlendMode::default()
606                && attrs.mask.is_none()
607                && !attrs.paint.may_have_transparency(encoded_paints))
608                .then_some(attrs.draw_id);
609
610        self.generate(
611            strip_buf,
612            |bucketer, fill| {
613                // `push_fill` already calls `ensure_row_layers` so no need to call it twice.
614                bucketer.push_fill(fill, attrs_idx, draw_id);
615            },
616            |bucketer, fill| {
617                let row_idx = fill.row_idx;
618                bucketer.ensure_row_layers(row_idx);
619                bucketer.rows[row_idx].push_cmd(RenderCmd::PaintFill(PaintFill::new(
620                    fill.span,
621                    Some(fill.alpha_idx),
622                    attrs_idx,
623                )));
624            },
625        );
626    }
627
628    pub(crate) fn generate<F, A>(
629        &mut self,
630        strip_buf: &[Strip],
631        mut fill_cmd: F,
632        mut alpha_fill_cmd: A,
633    ) where
634        F: FnMut(&mut Self, GeneratedFill),
635        A: FnMut(&mut Self, GeneratedAlphaFill),
636    {
637        if strip_buf.is_empty() {
638            return;
639        }
640
641        let clip_bbox = *self.clip_bboxes.last().unwrap();
642
643        // TODO: Don't emit layers with empty clip bboxes (and non-destructive blend modes)
644        // in the first place during recordings.
645        if clip_bbox.is_empty() {
646            return;
647        }
648
649        // Note: Those will always be aligned to tile coordinates.
650        let clip_x0 = clip_bbox.x0;
651        let clip_x1 = clip_bbox.x1;
652
653        debug_assert_tile_aligned((clip_x0, clip_bbox.y0), "clip start");
654        debug_assert_tile_aligned((clip_x1, clip_bbox.y1), "clip end");
655
656        let origin = self.viewport_origin();
657        debug_assert_tile_aligned(origin, "viewport origin");
658
659        // Note: the viewport of a filter layer is based on the bounds of its rendered contents.
660        // Therefore, those are always guaranteed to be within the viewport rect. However, this does not
661        // apply to any clip paths associated with the filter layer. Including those in the filter
662        // layer bbox could unnecessarily balloon the size of the pixmap we allocate if it is
663        // very large, since those don't actually contribute any visible output. However, this does
664        // mean that this method might be called with strips that do not lie within the viewport.
665        // Therefore, we need to make sure to clip those appropriately.
666
667        let origin_tile_x = origin.0 / Tile::WIDTH;
668        let origin_tile_y = origin.1 / Tile::HEIGHT;
669        let clip_scene_y0 = origin.1.saturating_add(clip_bbox.y0);
670        let clip_scene_y1 = origin.1.saturating_add(clip_bbox.y1);
671        // Convert to scene coordinates.
672        let clip_scene_x0 = origin.0.saturating_add(clip_x0);
673        let clip_scene_x1 = origin.0.saturating_add(clip_x1);
674
675        // Clip bounding box in tile units.
676        let tile_bounds = RectU16::new(
677            clip_scene_x0 / Tile::WIDTH,
678            clip_scene_y0 / Tile::HEIGHT,
679            clip_scene_x1 / Tile::WIDTH,
680            clip_scene_y1 / Tile::HEIGHT,
681        );
682
683        visit_strip_fill_segments(
684            strip_buf,
685            tile_bounds,
686            self,
687            |bucketer, segment| {
688                let row_idx = usize::from(segment.tile_y - origin_tile_y);
689                let x0 = (segment.tile_x0 - origin_tile_x) * Tile::WIDTH;
690                let x1 = (segment.tile_x1 - origin_tile_x) * Tile::WIDTH;
691                alpha_fill_cmd(
692                    bucketer,
693                    GeneratedAlphaFill {
694                        row_idx,
695                        span: Span::new(x0, x1 - x0),
696                        alpha_idx: segment.alpha_idx,
697                    },
698                );
699            },
700            |bucketer, segment| {
701                let row_idx = usize::from(segment.tile_y - origin_tile_y);
702                let x0 = (segment.tile_x0 - origin_tile_x) * Tile::WIDTH;
703                let x1 = (segment.tile_x1 - origin_tile_x) * Tile::WIDTH;
704                fill_cmd(
705                    bucketer,
706                    GeneratedFill {
707                        row_idx,
708                        span: Span::new(x0, x1 - x0),
709                    },
710                );
711            },
712        );
713    }
714
715    /// Note: If depth-culling should be disabled, pass `None` to `draw_id`.
716    fn push_fill(&mut self, fill: GeneratedFill, attrs_idx: u32, draw_id: Option<u32>) {
717        self.ensure_row_layers(fill.row_idx);
718        let row = &mut self.rows[fill.row_idx];
719        let draw_id = draw_id.filter(|_| row.layer_depth == 0);
720
721        let Some(draw_id) = draw_id else {
722            // If depth-culling is disabled, we can just push it as a single contiguous command.
723            row.push_cmd(RenderCmd::PaintFill(PaintFill::new(
724                fill.span, None, attrs_idx,
725            )));
726
727            return;
728        };
729
730        depth::split_opaque_span(fill.span, |segment| match segment {
731            DepthSegment::Regular(span) => {
732                row.push_cmd(RenderCmd::PaintFill(PaintFill::new(span, None, attrs_idx)));
733            }
734            DepthSegment::Opaque(bucket_range) => {
735                row.push_depth_fill(DepthFill::new(bucket_range, attrs_idx), draw_id);
736            }
737        });
738    }
739}
740
741/// Metadata about the currently active layer.
742#[derive(Debug, Clone)]
743pub(crate) struct ActiveLayer {
744    pub(crate) mask: Option<Mask>,
745    pub(crate) blend_mode: BlendMode,
746    pub(crate) opacity: f32,
747    pub(crate) clip: Option<LayerClip>,
748    pub(crate) span: Span,
749    /// Which rows have been drawn into and thus contain lazily-allocated `PushBuf` instructions.
750    pub(crate) occupied_rows: Vec<usize>,
751}
752
753/// A generic fill to allow using `generate_fill` to create either paint fills or blend fills.
754#[derive(Debug, Clone, Copy)]
755pub(crate) struct GeneratedFill {
756    pub(crate) row_idx: usize,
757    pub(crate) span: Span,
758}
759
760/// A generic alpha fill to allow using `generate_fill` to create either paint fills or blend fills.
761#[derive(Debug, Clone, Copy)]
762pub(crate) struct GeneratedAlphaFill {
763    pub(crate) row_idx: usize,
764    pub(crate) span: Span,
765    pub(crate) alpha_idx: u32,
766}
767
768#[cfg(test)]
769mod tests {
770    use super::LayerClip;
771    use crate::coarse::CommandBucketer;
772    use crate::coarse::cmd::{PaintFillAttrs, RenderCmd};
773    use crate::coarse::depth::{BucketRange, DEPTH_BUCKET_WIDTH};
774    use vello_common::color::palette::css::{BLUE, RED};
775    use vello_common::color::{AlphaColor, Srgb};
776    use vello_common::geometry::RectU16;
777    use vello_common::paint::{Paint, PremulColor};
778    use vello_common::peniko::{BlendMode, Compose, Mix};
779    use vello_common::record::LayerProps;
780    use vello_common::strip::Strip;
781    use vello_common::tile::Tile;
782
783    fn color(alpha: AlphaColor<Srgb>) -> PremulColor {
784        PremulColor::from_alpha_color(alpha)
785    }
786
787    fn fill_attrs(paint: Paint) -> PaintFillAttrs {
788        PaintFillAttrs {
789            paint,
790            blend_mode: BlendMode::default(),
791            mask: None,
792            draw_id: 1,
793            thread_idx: 0,
794            origin: (0, 0),
795        }
796    }
797
798    fn layer_props() -> LayerProps {
799        LayerProps {
800            blend_mode: BlendMode::default(),
801            opacity: 1.0,
802            mask: None,
803            clip_path: None,
804        }
805    }
806
807    fn clipped_layer_props(bbox: RectU16) -> LayerProps {
808        LayerProps {
809            blend_mode: BlendMode::default(),
810            opacity: 1.0,
811            mask: None,
812            clip_path: Some(LayerClip {
813                strip_range: 0..0,
814                thread_idx: 0,
815                bbox,
816            }),
817        }
818    }
819
820    fn destructive_clipped_layer_props(bbox: RectU16) -> LayerProps {
821        LayerProps {
822            blend_mode: BlendMode::new(Mix::Normal, Compose::Clear),
823            ..clipped_layer_props(bbox)
824        }
825    }
826
827    fn clipped_layer_props_with_strips(
828        bbox: RectU16,
829        strip_range: core::ops::Range<usize>,
830    ) -> LayerProps {
831        LayerProps {
832            blend_mode: BlendMode::default(),
833            opacity: 1.0,
834            mask: None,
835            clip_path: Some(LayerClip {
836                strip_range,
837                thread_idx: 0,
838                bbox,
839            }),
840        }
841    }
842
843    fn count_layer_fills(cmds: &[RenderCmd]) -> usize {
844        cmds.iter()
845            .filter(|cmd| matches!(cmd, RenderCmd::LayerFill(_)))
846            .count()
847    }
848
849    #[test]
850    fn opaque_fill_inside_layer_does_not_use_depth_write() {
851        let mut bucketer = CommandBucketer::from_wh(DEPTH_BUCKET_WIDTH, 4);
852        let strips = [
853            Strip::new(0, 0, 0, false),
854            Strip::new(DEPTH_BUCKET_WIDTH, 0, 0, true),
855        ];
856
857        bucketer.push_layer(&layer_props());
858        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
859
860        let row = &bucketer.rows()[0];
861        assert_eq!(row.depth_cmds.len(), 0);
862        assert_eq!(row.render_cmds.len(), 2);
863        assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
864        assert!(
865            matches!(row.render_cmds[1], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 0 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH)
866        );
867    }
868
869    #[test]
870    fn alpha_fill_is_clipped_to_active_layer_bbox() {
871        let mut bucketer = CommandBucketer::from_wh(8, 4);
872        let strips = [Strip::new(0, 0, 0, false), Strip::new(12, 0, 48, false)];
873
874        bucketer.push_layer(&clipped_layer_props(RectU16::new(4, 0, 8, 4)));
875        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
876
877        let row = &bucketer.rows()[0];
878        assert_eq!(row.render_cmds.len(), 2);
879        assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
880        assert!(matches!(
881            row.render_cmds[1],
882            RenderCmd::PaintFill(cmd)
883                if cmd.span.pixel_x() == 4
884                    && cmd.span.pixel_width() == 4
885                    && cmd.alpha_idx() == Some(u32::from(4 * Tile::HEIGHT))
886        ));
887    }
888
889    #[test]
890    fn disjoint_nested_clip_bounds_do_not_emit_commands() {
891        let mut bucketer = CommandBucketer::from_wh(16, 4);
892        let strips = [Strip::new(0, 0, 0, false), Strip::new(16, 0, 0, true)];
893
894        bucketer.push_layer(&clipped_layer_props(RectU16::new(0, 0, 4, 4)));
895        bucketer.push_layer(&clipped_layer_props(RectU16::new(8, 0, 12, 4)));
896        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
897
898        assert!(bucketer.rows().iter().all(|row| row.render_cmds.is_empty()));
899    }
900
901    #[test]
902    fn empty_destructive_clip_does_not_push_rows() {
903        let mut bucketer = CommandBucketer::from_wh(16, 4);
904
905        bucketer.push_layer(&clipped_layer_props(RectU16::new(0, 0, 4, 4)));
906        bucketer.push_layer(&destructive_clipped_layer_props(RectU16::new(8, 0, 12, 4)));
907
908        assert!(bucketer.rows().iter().all(|row| row.render_cmds.is_empty()));
909    }
910
911    #[test]
912    fn opaque_fill_uses_depth_write_when_possible() {
913        let end = DEPTH_BUCKET_WIDTH * 2 + 4;
914        let mut bucketer = CommandBucketer::from_wh(end, 4);
915        let strips = [Strip::new(4, 0, 0, false), Strip::new(end, 0, 0, true)];
916
917        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
918
919        let row = &bucketer.rows()[0];
920        assert_eq!(row.depth_cmds.len(), 1);
921        assert_eq!(row.depth_cmds[0].bucket_range(), BucketRange::new(1, 2));
922        assert_eq!(row.render_cmds.len(), 2);
923        assert!(
924            matches!(row.render_cmds[0], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 4 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH - 4)
925        );
926        assert!(
927            matches!(row.render_cmds[1], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == DEPTH_BUCKET_WIDTH * 2 && cmd.span.pixel_width() == 4)
928        );
929    }
930
931    #[test]
932    fn non_opaque_fill_uses_regular_commands() {
933        let mut bucketer = CommandBucketer::from_wh(DEPTH_BUCKET_WIDTH, 4);
934        let strips = [
935            Strip::new(0, 0, 0, false),
936            Strip::new(DEPTH_BUCKET_WIDTH, 0, 0, true),
937        ];
938
939        bucketer.generate_fill(
940            &strips,
941            &fill_attrs(Paint::Solid(color(BLUE.with_alpha(0.5)))),
942            &[],
943        );
944
945        let row = &bucketer.rows()[0];
946        assert_eq!(row.depth_cmds.len(), 0);
947        assert_eq!(row.render_cmds.len(), 1);
948        assert!(
949            matches!(row.render_cmds[0], RenderCmd::PaintFill(cmd) if cmd.span.pixel_x() == 0 && cmd.span.pixel_width() == DEPTH_BUCKET_WIDTH)
950        );
951    }
952
953    #[test]
954    fn clips_fills_correctly_inside_nonzero_origin_viewport() {
955        // Viewport spans scene (32, 32) to (96, 96). Local space is 64x64, the origin at (32, 32).
956        let mut bucketer = CommandBucketer::new(RectU16::new(32, 32, 96, 96));
957        // Clip bbox in scene coordinates: (40, 32)..(72, 96) => local (8, 0)..(40, 64).
958        bucketer.push_layer(&clipped_layer_props(RectU16::new(40, 32, 72, 96)));
959
960        // A 32px-wide alpha strip at scene (40, 32) => local (8, 0), fully inside the clip.
961        let strips = [
962            Strip::new(40, 32, 0, false),
963            Strip::new(72, 32, 32 * u32::from(Tile::HEIGHT), false),
964        ];
965        bucketer.generate_fill(&strips, &fill_attrs(Paint::Solid(color(RED))), &[]);
966
967        let row = &bucketer.rows()[0];
968
969        assert!(matches!(row.render_cmds[0], RenderCmd::PushBuf(_)));
970        // The fill inside should not have been clipped.
971        assert!(matches!(
972            row.render_cmds[1],
973            RenderCmd::PaintFill(cmd)
974                if cmd.span.pixel_x() == 8
975                    && cmd.span.pixel_width() == 32
976                    && cmd.alpha_idx() == Some(0)
977        ));
978    }
979
980    #[test]
981    fn culls_clip_strips_above_viewport_origin() {
982        // Viewport spans scene (0, 32) to (64, 96). Local space is 64x64, the origin at (0, 32).
983        let mut bucketer = CommandBucketer::new(RectU16::new(0, 32, 64, 96));
984
985        let alpha = u32::from(Tile::HEIGHT);
986        let strips = [
987            // Content: 16px alpha strip at scene (0, 32) => local row 0.
988            Strip::new(0, 32, 0, false),
989            Strip::new(16, 32, 16 * alpha, false),
990            // Clip strip above the viewport origin at scene y = 0, covering nothing visible.
991            Strip::new(0, 0, 16 * alpha, false),
992            Strip::new(16, 0, 32 * alpha, false),
993            // Clip strip at scene y = 32 => local row 0: the real coverage.
994            Strip::new(0, 32, 32 * alpha, false),
995            Strip::new(16, 32, 48 * alpha, false),
996        ];
997
998        // Clip bbox in scene coordinates: (0, 0)..(16, 40) => local (0, 0)..(16, 8).
999        bucketer.push_layer(&clipped_layer_props_with_strips(
1000            RectU16::new(0, 0, 16, 40),
1001            2..6,
1002        ));
1003        bucketer.generate_fill(&strips[0..2], &fill_attrs(Paint::Solid(color(RED))), &[]);
1004        bucketer.pop_layer(&strips);
1005
1006        let row = &bucketer.rows()[0];
1007        assert_eq!(
1008            count_layer_fills(&row.render_cmds),
1009            1,
1010            "row 0 must be composited exactly once, got {:?}",
1011            row.render_cmds
1012        );
1013    }
1014}