Skip to main content

vello_common/
record.rs

1// Copyright 2026 the Vello Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! Recording rendering commands.
5//!
6//! Vello CPU and Vello Hybrid share a recording stage before their pipelines diverge. Their
7//! pipelines can be split into roughly three parts:
8//!
9//! 1. Record rendering commands into a scene-graph-like structure.
10//! 2. Turn the complete recording into renderer-specific work. Vello CPU buckets commands by
11//!    strip row, while Vello Hybrid schedules draws and layer operations
12//!    across intermediate textures and render passes.
13//! 3. Execute that work using CPU fine rasterization or GPU render passes, respectively.
14//!
15//! The reasons for completing the recording before renderer-specific planning differ somewhat
16//! between the two renderers:
17//!
18//! - Vello CPU can inline regular layers with blends, opacity, masks, or clips into their parent
19//!   command stream. However, filter layers must instead be rendered separately because
20//!   spatial filters might sample neighboring pixels. The complete layer must be rendered before
21//!   the filter can be applied and its result composited into the parent.
22//!
23//! - Vello Hybrid needs to render *every* layer separately. Its scheduler therefore
24//!   needs the complete layer hierarchy and bounds before it can allocate intermediate textures and
25//!   order draws, filters, and composition operations.
26//!
27//! - In both renderers, filter layers might have different dimensions than the main viewport. A
28//!   large blur, for example, might require rendering shapes that would normally be culled because
29//!   they exceed the viewport.
30//!
31//! The recording stage allows us to serialize the whole scene into a graph that is enrichened
32//! with metadata, allowing each renderer to "do their own thing" while providing a common
33//! intermediate representation.
34
35use crate::filter::{FilterData, FilterLayerPlacement};
36use crate::geometry::{RectU16, SizeU16};
37use crate::mask::Mask;
38use crate::peniko::BlendMode;
39use crate::strip::Strip;
40use crate::util::RectExt;
41use alloc::vec::Vec;
42use core::ops::Range;
43use smallvec::SmallVec;
44
45/// A drawable object that can report its bounding box.
46pub trait Drawable {
47    /// Return the **tile-aligned** bounding box of the given object, if it
48    /// has one.
49    fn bbox(&self, strips: &[Strip]) -> Option<RectU16>;
50}
51
52/// A node in the recorded render graph.
53#[derive(Debug)]
54pub struct Node {
55    /// A contiguous (possibly empty) batch of draw commands indexing [`CommandRecorder::draws`].
56    pub draws: Range<u32>,
57    /// An optional layer composition, invoked after `draws`.
58    pub layer: Option<u32>,
59}
60
61impl Node {
62    /// Return the draw commands referenced by this node.
63    pub fn draws_in<'a, D>(&self, draws: &'a [D]) -> &'a [D] {
64        &draws[self.draws.start as usize..self.draws.end as usize]
65    }
66}
67
68/// Metadata and child nodes for a recorded layer.
69#[derive(Debug)]
70pub struct RecordedLayer {
71    /// Properties of the layer.
72    pub props: LayerProps,
73    /// The child nodes of the layer.
74    pub nodes: SmallVec<[Node; 2]>,
75    /// The kind of recorded layer.
76    pub kind: RecordedLayerKind,
77    /// Nesting depth of the layer.
78    pub depth: usize,
79    /// Tile-aligned bounding box of the layer.
80    ///
81    /// **IMPORTANT**: This field only indicates the bounding box of visible contents directly
82    /// in this layer. It does not mean that any child layer is also strictly contained within
83    /// those bounds.
84    ///
85    /// For example:
86    ///
87    /// ```text
88    /// Root surface
89    /// └── L0 — regular layer, clipped to 50 × 50
90    ///     └── L1 — filter layer with a 100 × 100 bbox
91    /// ```
92    ///
93    /// This is a completely valid constellation. L1 needs to be rendered at its full resolution,
94    /// but since the parent layer has a clip path, we can constrain it's visible region. However,
95    /// the child layer must remain unaffected by this.
96    pub bbox: RectU16,
97}
98
99/// Properties for a recorded layer.
100#[derive(Debug)]
101pub struct LayerProps {
102    /// Blend mode used when compositing the layer.
103    pub blend_mode: BlendMode,
104    /// Opacity applied when compositing the layer.
105    pub opacity: f32,
106    /// Optional mask applied when compositing the layer.
107    pub mask: Option<Mask>,
108    /// Optional clip path applied when compositing the layer.
109    pub clip_path: Option<LayerClip>,
110}
111
112/// Clip path associated with a recorded layer.
113#[derive(Debug, Clone)]
114pub struct LayerClip {
115    /// Range of strips representing the clip path.
116    pub strip_range: Range<usize>,
117    /// Index of the thread-local strip storage containing the strips.
118    pub thread_idx: u8,
119    /// Tile-aligned bounds of the clip path.
120    pub bbox: RectU16,
121}
122
123/// Additional metadata for regular and filter layers.
124#[derive(Debug)]
125pub enum RecordedLayerKind {
126    /// A regular layer.
127    Regular,
128    /// A filter layer.
129    Filter {
130        /// Static data about the filter itself.
131        filter_data: FilterData,
132        /// Data about how to place the filter layer, which can only be determined once its
133        /// contents have been recorded.
134        placement: FilterLayerPlacement,
135    },
136}
137
138impl RecordedLayer {
139    fn regular(props: LayerProps, depth: usize) -> Self {
140        Self {
141            props,
142            nodes: SmallVec::new(),
143            kind: RecordedLayerKind::Regular,
144            depth,
145            // Will be initialized once we call `pop_layer`.
146            bbox: RectU16::ZERO,
147        }
148    }
149
150    fn filter(props: LayerProps, filter_plan: FilterData, depth: usize) -> Self {
151        Self {
152            props,
153            nodes: SmallVec::new(),
154            kind: RecordedLayerKind::Filter {
155                filter_data: filter_plan,
156                // Will be initialized once we call `pop_layer`.
157                placement: FilterLayerPlacement::EMPTY,
158            },
159            depth,
160            // Will be initialized once we call `pop_layer`.
161            bbox: RectU16::ZERO,
162        }
163    }
164}
165
166// TODO: Rename this: https://github.com/linebender/vello/pull/1746#discussion_r3611799919
167/// Recorder for a scene description.
168#[derive(Debug)]
169pub struct CommandRecorder<D> {
170    /// Tile-aligned dimensions of the root scene.
171    pub scene_size: SizeU16,
172    /// The nodes of the root layer.
173    pub nodes: Vec<Node>,
174    /// Flat storage for all draw commands that are part of the recording.
175    pub draws: Vec<D>,
176    /// Data about recorded layers, indexed by their ID.
177    pub layers: Vec<RecordedLayer>,
178    /// IDs of recorded filter layers in creation order.
179    pub filter_layers: Vec<u32>,
180    /// Whether the root is the target of a non-default blending operation.
181    pub root_is_blend_target: bool,
182    /// Maximum layer depth across the whole layer graph.
183    pub max_layer_depth: usize,
184    /// The largest dimensions of any recorded layer.
185    pub largest_layer_size: Option<SizeU16>,
186    /// The largest dimensions of any recorded filter layer.
187    pub largest_filter_layer_size: Option<SizeU16>,
188    /// Whether there exists at least one layer that uses a non-default blend mode.
189    pub has_non_default_blend: bool,
190    /// The layer whose command stream is currently the base.
191    ///
192    /// This is `None` if there is no active layer and we are recording into the root layer instead.
193    active_layer: Option<u32>,
194    /// Stack of currently pushed layers.
195    layer_stack: Vec<OpenLayer>,
196}
197
198impl<D> Default for CommandRecorder<D> {
199    fn default() -> Self {
200        Self {
201            scene_size: SizeU16::ZERO,
202            nodes: Vec::new(),
203            draws: Vec::new(),
204            layers: Vec::new(),
205            filter_layers: Vec::new(),
206            root_is_blend_target: false,
207            max_layer_depth: 0,
208            largest_layer_size: None,
209            largest_filter_layer_size: None,
210            has_non_default_blend: false,
211            active_layer: None,
212            layer_stack: Vec::new(),
213        }
214    }
215}
216
217#[derive(Debug)]
218struct OpenLayer {
219    id: u32,
220    /// The bounding box of the contents recorded into this layer.
221    bbox: RectU16,
222    parent_layer: Option<u32>,
223}
224
225impl<D> CommandRecorder<D> {
226    /// Create a new command recorder.
227    pub fn new(width: u16, height: u16) -> Self {
228        Self {
229            scene_size: snapped_scene_size(width, height),
230            ..Self::default()
231        }
232    }
233
234    /// Whether any layers are currently open.
235    pub fn has_layers(&self) -> bool {
236        !self.layer_stack.is_empty()
237    }
238
239    /// Reset the command recorder.
240    #[inline]
241    pub fn reset(&mut self, width: u16, height: u16) {
242        self.scene_size = snapped_scene_size(width, height);
243        self.nodes.clear();
244        self.draws.clear();
245
246        self.layers.clear();
247        self.filter_layers.clear();
248        self.root_is_blend_target = false;
249        self.max_layer_depth = 0;
250        self.largest_layer_size = None;
251        self.largest_filter_layer_size = None;
252        self.has_non_default_blend = false;
253        self.active_layer = None;
254        self.layer_stack.clear();
255    }
256
257    /// Push a new layer.
258    #[inline]
259    pub fn push_layer(&mut self, props: LayerProps, filter_plan: Option<FilterData>) {
260        if let Some(filter_plan) = filter_plan {
261            self.push_filter_layer(props, filter_plan);
262            return;
263        }
264
265        self.push_regular_layer(props);
266    }
267
268    fn push_regular_layer(&mut self, props: LayerProps) {
269        let depth = self.layer_stack.len() + 1;
270        self.push_recorded_layer(RecordedLayer::regular(props, depth));
271    }
272
273    fn push_filter_layer(&mut self, props: LayerProps, filter_plan: FilterData) {
274        let depth = self.layer_stack.len() + 1;
275        let id = self.push_recorded_layer(RecordedLayer::filter(props, filter_plan, depth));
276
277        self.filter_layers.push(id);
278    }
279
280    fn push_recorded_layer(&mut self, layer: RecordedLayer) -> u32 {
281        let parent_layer = self.active_layer;
282        self.max_layer_depth = self.max_layer_depth.max(layer.depth);
283
284        if layer.props.blend_mode != BlendMode::default() {
285            self.has_non_default_blend = true;
286
287            if parent_layer.is_none() {
288                self.root_is_blend_target = true;
289            }
290        }
291
292        let id = self.push_layer_metadata(layer);
293        self.push_layer_node(id);
294        self.active_layer = Some(id);
295        self.layer_stack.push(OpenLayer {
296            id,
297            // Will be populated as we record commands.
298            bbox: RectU16::INVERTED,
299            parent_layer,
300        });
301
302        id
303    }
304
305    /// Pop the currently active layer.
306    pub fn pop_layer(&mut self) -> PoppedLayer {
307        let layer = self.layer_stack.pop().unwrap();
308        let id = layer.id;
309
310        let (popped_layer, bbox_in_parent) = {
311            let recorded_layer = &mut self.layers[id as usize];
312            match &mut recorded_layer.kind {
313                RecordedLayerKind::Regular => {
314                    let mut bbox = layer.bbox;
315
316                    // Turn the potentially still-inverted bbox into a zero-sized one.
317                    if bbox.is_empty() {
318                        bbox = RectU16::ZERO;
319                    }
320
321                    if let Some(clip_path) = &recorded_layer.props.clip_path {
322                        bbox = bbox.intersect(clip_path.bbox);
323                    }
324
325                    recorded_layer.bbox = bbox;
326
327                    let layer_size = bbox.into();
328                    self.largest_layer_size = Some(
329                        self.largest_layer_size
330                            .map_or(layer_size, |current| current.max(layer_size)),
331                    );
332
333                    (PoppedLayer::Regular, bbox)
334                }
335                RecordedLayerKind::Filter {
336                    filter_data: filter_plan,
337                    placement,
338                } => {
339                    *placement = FilterLayerPlacement::new(layer.bbox, filter_plan);
340                    recorded_layer.bbox = placement.pixmap_bbox;
341
342                    let filter_size = placement.pixmap_bbox.into();
343                    self.largest_layer_size = Some(
344                        self.largest_layer_size
345                            .map_or(filter_size, |current| current.max(filter_size)),
346                    );
347                    self.largest_filter_layer_size = Some(
348                        self.largest_filter_layer_size
349                            .map_or(filter_size, |current| current.max(filter_size)),
350                    );
351
352                    (PoppedLayer::Filter, placement.dest_bbox)
353                }
354            }
355        };
356
357        // Update the parent bbox as well.
358        self.active_layer = layer.parent_layer;
359        self.record_bbox(|| Some(bbox_in_parent));
360
361        popped_layer
362    }
363
364    #[inline]
365    fn active_node_mut(&mut self) -> Option<&mut Node> {
366        if let Some(id) = self.active_layer {
367            self.layers[id as usize].nodes.last_mut()
368        } else {
369            self.nodes.last_mut()
370        }
371    }
372
373    fn push_node(&mut self, node: Node) {
374        if let Some(id) = self.active_layer {
375            self.layers[id as usize].nodes.push(node);
376        } else {
377            self.nodes.push(node);
378        }
379    }
380
381    fn push_layer_node(&mut self, layer_id: u32) {
382        let draw_idx = self.draws.len() as u32;
383
384        match self.active_node_mut() {
385            Some(node) if node.layer.is_none() => {
386                node.layer = Some(layer_id);
387            }
388            _ => self.push_node(Node {
389                draws: draw_idx..draw_idx,
390                layer: Some(layer_id),
391            }),
392        }
393    }
394
395    fn push_layer_metadata(&mut self, layer: RecordedLayer) -> u32 {
396        let id = self.layers.len() as u32;
397        self.layers.push(layer);
398        id
399    }
400
401    fn record_bbox(&mut self, bbox: impl FnOnce() -> Option<RectU16>) {
402        let Some(layer) = self.layer_stack.last_mut() else {
403            return;
404        };
405
406        let Some(bbox) = bbox().and_then(|b| if b.is_empty() { None } else { Some(b) }) else {
407            return;
408        };
409
410        layer.bbox.union(bbox);
411    }
412}
413
414fn snapped_scene_size(width: u16, height: u16) -> SizeU16 {
415    RectU16::new(0, 0, width, height)
416        .snap_to_tile_coordinates()
417        .into()
418}
419
420impl<D: Drawable> CommandRecorder<D> {
421    /// Push a draw command.
422    #[inline]
423    pub fn push_draw(&mut self, draw: D, strips: &[Strip]) {
424        self.record_bbox(|| draw.bbox(strips));
425        let draw_idx = self.draws.len() as u32;
426        self.draws.push(draw);
427
428        match self.active_node_mut() {
429            Some(node) if node.layer.is_none() && node.draws.end == draw_idx => {
430                node.draws.end += 1;
431            }
432            _ => {
433                self.push_node(Node {
434                    draws: draw_idx..draw_idx + 1,
435                    layer: None,
436                });
437            }
438        };
439    }
440}
441
442/// Kind of layer returned by [`CommandRecorder::pop_layer`].
443#[derive(Debug, Clone, Copy, PartialEq, Eq)]
444pub enum PoppedLayer {
445    /// A regular layer.
446    Regular,
447    /// A filter layer.
448    Filter,
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use crate::filter_effects::{Filter, FilterPrimitive};
455    use crate::geometry::PaddingU16;
456    use crate::kurbo::Affine;
457    use crate::peniko::Mix;
458    use crate::tile::Tile;
459
460    const DEFAULT_SIZE: u16 = 10;
461
462    #[derive(Debug)]
463    struct TestDraw;
464
465    impl Drawable for TestDraw {
466        fn bbox(&self, _strips: &[Strip]) -> Option<RectU16> {
467            Some(RectU16::new(0, 0, 64, 4))
468        }
469    }
470
471    #[derive(Debug)]
472    struct EmptyDraw;
473
474    impl Drawable for EmptyDraw {
475        fn bbox(&self, _strips: &[Strip]) -> Option<RectU16> {
476            None
477        }
478    }
479
480    fn layer_props() -> LayerProps {
481        LayerProps {
482            blend_mode: BlendMode::default(),
483            opacity: 1.0,
484            mask: None,
485            clip_path: None,
486        }
487    }
488
489    fn blended_layer_props() -> LayerProps {
490        LayerProps {
491            blend_mode: Mix::Multiply.into(),
492            ..layer_props()
493        }
494    }
495
496    fn filter_data(filter_padding: PaddingU16, source_padding: PaddingU16) -> FilterData {
497        FilterData {
498            filter: Filter::from_primitive(FilterPrimitive::Offset { dx: 0.0, dy: 0.0 }),
499            transform: Affine::IDENTITY,
500            filter_padding,
501            source_padding,
502        }
503    }
504
505    fn assert_cmds(cmds: &[Node], expected: &[(Range<u32>, Option<u32>)]) {
506        assert_eq!(cmds.len(), expected.len());
507
508        for (cmd, (draws, layer)) in cmds.iter().zip(expected) {
509            assert_eq!(&cmd.draws, draws);
510            assert_eq!(cmd.layer, *layer);
511        }
512    }
513
514    fn layer_cmds(recorder: &CommandRecorder<TestDraw>, id: usize) -> &[Node] {
515        &recorder.layers[id].nodes
516    }
517
518    #[test]
519    fn scene_size_is_tile_aligned() {
520        let mut recorder = CommandRecorder::<TestDraw>::new(10, 10);
521        assert_eq!(recorder.scene_size, SizeU16::new(12));
522
523        recorder.reset(13, 7);
524        assert_eq!(recorder.scene_size, SizeU16::from_wh(16, 8));
525
526        recorder.reset(Tile::WIDTH * 5, Tile::HEIGHT * 3);
527        assert_eq!(
528            recorder.scene_size,
529            SizeU16::from_wh(Tile::WIDTH * 5, Tile::HEIGHT * 3)
530        );
531    }
532
533    #[test]
534    fn filter_placement_padding_expands_bbox() {
535        let placement = FilterLayerPlacement::new(
536            RectU16::new(8, 8, 16, 20),
537            &filter_data(PaddingU16::new(2, 4, 6, 8), PaddingU16::ZERO),
538        );
539
540        // Since we are tile-aligned, values are expanded to a multiple of tile-size.
541        assert_eq!(placement.pixmap_bbox, RectU16::new(4, 4, 24, 28));
542        assert_eq!(placement.dest_bbox, RectU16::new(4, 4, 24, 28));
543        assert_eq!(placement.src_origin(), (0, 0));
544    }
545
546    #[test]
547    fn filter_placement_with_source_shift() {
548        let placement = FilterLayerPlacement::new(
549            RectU16::new(8, 12, 20, 24),
550            &filter_data(PaddingU16::new(6, 2, 4, 6), PaddingU16::new(10, 16, 0, 0)),
551        );
552
553        // Bbox expanded with padding is [8 - 6, 12 - 2, 20 + 4, 24 + 6]
554        // = [2, 10, 24, 30], snappding this gives us [0, 8, 24, 32].
555        assert_eq!(placement.pixmap_bbox, RectU16::new(0, 8, 24, 32));
556        // Account for source origin using saturing sub of 10 horizontally and
557        // 16 vertically.
558        assert_eq!(placement.dest_bbox, RectU16::new(0, 0, 14, 16));
559        // Source origin is now 10 - 0 = 10 and 16 - 8 = 8.
560        assert_eq!(placement.src_origin(), (10, 8));
561    }
562
563    #[test]
564    fn layer_behavior() {
565        let mut recorder = CommandRecorder::<TestDraw>::new(DEFAULT_SIZE, DEFAULT_SIZE);
566
567        recorder.push_layer(
568            layer_props(),
569            Some(filter_data(PaddingU16::ZERO, PaddingU16::ZERO)),
570        );
571        recorder.push_layer(
572            layer_props(),
573            Some(filter_data(PaddingU16::ZERO, PaddingU16::ZERO)),
574        );
575        recorder.push_layer(layer_props(), None);
576
577        recorder.push_draw(TestDraw, &[]);
578
579        assert_eq!(recorder.pop_layer(), PoppedLayer::Regular);
580        assert_eq!(recorder.pop_layer(), PoppedLayer::Filter);
581
582        recorder.push_layer(layer_props(), None);
583        recorder.push_draw(TestDraw, &[]);
584        assert_eq!(recorder.pop_layer(), PoppedLayer::Regular);
585        assert_eq!(recorder.pop_layer(), PoppedLayer::Filter);
586
587        assert_cmds(&recorder.nodes, &[(0..0, Some(0))]);
588        assert_cmds(
589            layer_cmds(&recorder, 0),
590            &[(0..0, Some(1)), (1..1, Some(3))],
591        );
592        assert_cmds(layer_cmds(&recorder, 1), &[(0..0, Some(2))]);
593        assert_cmds(layer_cmds(&recorder, 2), &[(0..1, None)]);
594        assert_cmds(layer_cmds(&recorder, 3), &[(1..2, None)]);
595        assert_eq!(recorder.draws.len(), 2);
596        assert_eq!(recorder.filter_layers, [0, 1]);
597        assert_eq!(
598            recorder
599                .layers
600                .iter()
601                .map(|layer| layer.depth)
602                .collect::<Vec<_>>(),
603            [1, 2, 3, 2]
604        );
605        assert_eq!(recorder.max_layer_depth, 3);
606        assert!(!recorder.has_non_default_blend);
607        assert_eq!(recorder.largest_layer_size, Some(SizeU16::from_wh(64, 4)));
608        assert_eq!(
609            recorder.largest_filter_layer_size,
610            Some(SizeU16::from_wh(64, 4))
611        );
612    }
613
614    #[test]
615    fn draw_batches_are_split_by_layers() {
616        let mut recorder = CommandRecorder::<TestDraw>::new(DEFAULT_SIZE, DEFAULT_SIZE);
617
618        recorder.push_draw(TestDraw, &[]);
619        recorder.push_draw(TestDraw, &[]);
620        recorder.push_layer(layer_props(), None);
621        recorder.push_draw(TestDraw, &[]);
622        recorder.pop_layer();
623        recorder.push_draw(TestDraw, &[]);
624
625        assert_cmds(&recorder.nodes, &[(0..2, Some(0)), (3..4, None)]);
626        assert_cmds(layer_cmds(&recorder, 0), &[(2..3, None)]);
627    }
628
629    #[test]
630    fn node_resolves_draw_range() {
631        let draws = [0, 1, 2, 3];
632        let node = Node {
633            draws: 1..3,
634            layer: None,
635        };
636
637        assert_eq!(node.draws_in(&draws), &[1, 2]);
638    }
639
640    #[test]
641    fn empty_draws_do_not_affect_layer_bounds() {
642        let mut recorder = CommandRecorder::<EmptyDraw>::new(DEFAULT_SIZE, DEFAULT_SIZE);
643        recorder.push_layer(layer_props(), None);
644        recorder.push_draw(EmptyDraw, &[]);
645        recorder.pop_layer();
646
647        assert!(recorder.layers[0].bbox.is_empty());
648    }
649
650    #[test]
651    fn disjoint_layer_bounds_are_empty_but_not_inverted() {
652        let mut recorder = CommandRecorder::<TestDraw>::new(DEFAULT_SIZE, DEFAULT_SIZE);
653        let mut props = layer_props();
654        props.clip_path = Some(LayerClip {
655            strip_range: 0..0,
656            thread_idx: 0,
657            bbox: RectU16::new(8, 8, 12, 12),
658        });
659
660        recorder.push_layer(props, None);
661        recorder.push_draw(TestDraw, &[]);
662        recorder.pop_layer();
663
664        assert_eq!(recorder.layers[0].bbox, RectU16::new(8, 8, 12, 8));
665    }
666
667    #[test]
668    fn blend_metadata_distinguishes_root_and_nested_targets() {
669        let mut recorder = CommandRecorder::<TestDraw>::new(DEFAULT_SIZE, DEFAULT_SIZE);
670
671        recorder.push_layer(layer_props(), None);
672        recorder.push_layer(blended_layer_props(), None);
673
674        assert!(!recorder.root_is_blend_target);
675        assert!(recorder.has_non_default_blend);
676        assert_eq!(recorder.max_layer_depth, 2);
677
678        recorder.pop_layer();
679        recorder.pop_layer();
680        recorder.push_layer(blended_layer_props(), None);
681
682        assert!(recorder.root_is_blend_target);
683    }
684
685    #[test]
686    fn reset_clears_all_metadata() {
687        let mut recorder = CommandRecorder::<TestDraw>::new(DEFAULT_SIZE, DEFAULT_SIZE);
688
689        recorder.push_layer(blended_layer_props(), None);
690        recorder.push_layer(
691            layer_props(),
692            Some(filter_data(PaddingU16::ZERO, PaddingU16::ZERO)),
693        );
694        recorder.push_draw(TestDraw, &[]);
695        recorder.pop_layer();
696        recorder.pop_layer();
697
698        assert!(recorder.root_is_blend_target);
699        assert!(recorder.has_non_default_blend);
700        assert_eq!(recorder.max_layer_depth, 2);
701        assert!(recorder.largest_layer_size.is_some());
702        assert!(recorder.largest_filter_layer_size.is_some());
703
704        recorder.reset(13, 7);
705
706        assert_eq!(recorder.scene_size, SizeU16::from_wh(16, 8));
707        assert!(!recorder.root_is_blend_target);
708        assert!(!recorder.has_non_default_blend);
709        assert_eq!(recorder.max_layer_depth, 0);
710        assert!(recorder.largest_layer_size.is_none());
711        assert!(recorder.largest_filter_layer_size.is_none());
712        assert!(recorder.filter_layers.is_empty());
713    }
714}