Skip to main content

fret_core/scene/
validate.rs

1use super::*;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum SceneValidationErrorKind {
5    TransformUnderflow,
6    OpacityUnderflow,
7    LayerUnderflow,
8    ClipUnderflow,
9    MaskUnderflow,
10    EffectUnderflow,
11    CompositeGroupUnderflow,
12    BackdropSourceGroupUnderflow,
13    NonFiniteTransform,
14    NonFiniteOpacity,
15    NonFiniteOpData,
16    UnbalancedTransformStack { remaining: usize },
17    UnbalancedOpacityStack { remaining: usize },
18    UnbalancedLayerStack { remaining: usize },
19    UnbalancedClipStack { remaining: usize },
20    UnbalancedMaskStack { remaining: usize },
21    UnbalancedEffectStack { remaining: usize },
22    UnbalancedCompositeGroupStack { remaining: usize },
23    UnbalancedBackdropSourceGroupStack { remaining: usize },
24    EffectMaskCrossing,
25    CompositeGroupMaskCrossing,
26}
27
28#[derive(Debug, Clone, Copy)]
29pub struct SceneValidationError {
30    pub index: usize,
31    pub op: SceneOp,
32    pub kind: SceneValidationErrorKind,
33}
34
35impl std::fmt::Display for SceneValidationError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(
38            f,
39            "scene validation error at op index {}: {:?}",
40            self.index, self.kind
41        )
42    }
43}
44
45impl std::error::Error for SceneValidationError {}
46
47impl SceneRecording {
48    #[allow(clippy::result_large_err)]
49    pub fn validate(&self) -> Result<(), SceneValidationError> {
50        fn px_is_finite(px: crate::Px) -> bool {
51            px.0.is_finite()
52        }
53
54        fn point_is_finite(p: Point) -> bool {
55            px_is_finite(p.x) && px_is_finite(p.y)
56        }
57
58        fn rect_is_finite(r: Rect) -> bool {
59            point_is_finite(r.origin) && px_is_finite(r.size.width) && px_is_finite(r.size.height)
60        }
61
62        fn corners_is_finite(c: Corners) -> bool {
63            px_is_finite(c.top_left)
64                && px_is_finite(c.top_right)
65                && px_is_finite(c.bottom_right)
66                && px_is_finite(c.bottom_left)
67        }
68
69        fn edges_is_finite(e: Edges) -> bool {
70            px_is_finite(e.top)
71                && px_is_finite(e.right)
72                && px_is_finite(e.bottom)
73                && px_is_finite(e.left)
74        }
75
76        fn color_is_finite(c: Color) -> bool {
77            c.r.is_finite() && c.g.is_finite() && c.b.is_finite() && c.a.is_finite()
78        }
79
80        fn paint_is_finite(p: Paint) -> bool {
81            match p {
82                Paint::Solid(c) => color_is_finite(c),
83                Paint::LinearGradient(g) => {
84                    if !g.start.x.0.is_finite()
85                        || !g.start.y.0.is_finite()
86                        || !g.end.x.0.is_finite()
87                        || !g.end.y.0.is_finite()
88                    {
89                        return false;
90                    }
91                    let n = usize::from(g.stop_count).min(MAX_STOPS);
92                    for s in g.stops.iter().take(n) {
93                        if !s.offset.is_finite() || !color_is_finite(s.color) {
94                            return false;
95                        }
96                    }
97                    true
98                }
99                Paint::RadialGradient(g) => {
100                    if !g.center.x.0.is_finite()
101                        || !g.center.y.0.is_finite()
102                        || !g.radius.width.0.is_finite()
103                        || !g.radius.height.0.is_finite()
104                    {
105                        return false;
106                    }
107                    let n = usize::from(g.stop_count).min(MAX_STOPS);
108                    for s in g.stops.iter().take(n) {
109                        if !s.offset.is_finite() || !color_is_finite(s.color) {
110                            return false;
111                        }
112                    }
113                    true
114                }
115                Paint::SweepGradient(g) => {
116                    if !g.center.x.0.is_finite()
117                        || !g.center.y.0.is_finite()
118                        || !g.start_angle_turns.is_finite()
119                        || !g.end_angle_turns.is_finite()
120                    {
121                        return false;
122                    }
123                    let n = usize::from(g.stop_count).min(MAX_STOPS);
124                    for s in g.stops.iter().take(n) {
125                        if !s.offset.is_finite() || !color_is_finite(s.color) {
126                            return false;
127                        }
128                    }
129                    true
130                }
131                Paint::Material { params, .. } => params.is_finite(),
132            }
133        }
134
135        fn paint_binding_is_finite(p: PaintBindingV1) -> bool {
136            paint_is_finite(p.paint)
137        }
138
139        fn text_shadow_is_finite(s: TextShadowV1) -> bool {
140            point_is_finite(s.offset) && color_is_finite(s.color)
141        }
142
143        fn mask_is_finite(m: Mask) -> bool {
144            match m {
145                Mask::LinearGradient(g) => {
146                    if !g.start.x.0.is_finite()
147                        || !g.start.y.0.is_finite()
148                        || !g.end.x.0.is_finite()
149                        || !g.end.y.0.is_finite()
150                    {
151                        return false;
152                    }
153                    let n = usize::from(g.stop_count).min(MAX_STOPS);
154                    for s in g.stops.iter().take(n) {
155                        if !s.offset.is_finite() || !color_is_finite(s.color) {
156                            return false;
157                        }
158                    }
159                    true
160                }
161                Mask::RadialGradient(g) => {
162                    if !g.center.x.0.is_finite()
163                        || !g.center.y.0.is_finite()
164                        || !g.radius.width.0.is_finite()
165                        || !g.radius.height.0.is_finite()
166                    {
167                        return false;
168                    }
169                    let n = usize::from(g.stop_count).min(MAX_STOPS);
170                    for s in g.stops.iter().take(n) {
171                        if !s.offset.is_finite() || !color_is_finite(s.color) {
172                            return false;
173                        }
174                    }
175                    true
176                }
177                Mask::Image { uv, .. } => uv_is_finite(uv),
178            }
179        }
180
181        fn uv_is_finite(uv: UvRect) -> bool {
182            uv.u0.is_finite() && uv.v0.is_finite() && uv.u1.is_finite() && uv.v1.is_finite()
183        }
184
185        let mut transform_depth: usize = 0;
186        let mut opacity_depth: usize = 0;
187        let mut layer_depth: usize = 0;
188        let mut clip_depth: usize = 0;
189        let mut mask_depth: usize = 0;
190        let mut effect_depth: usize = 0;
191        let mut effect_mask_depths: Vec<usize> = Vec::new();
192        let mut composite_group_depth: usize = 0;
193        let mut composite_group_mask_depths: Vec<usize> = Vec::new();
194        let mut backdrop_source_group_depth: usize = 0;
195
196        for (index, &op) in self.ops.iter().enumerate() {
197            match op {
198                SceneOp::PushTransform { transform } => {
199                    if !transform.a.is_finite()
200                        || !transform.b.is_finite()
201                        || !transform.c.is_finite()
202                        || !transform.d.is_finite()
203                        || !transform.tx.is_finite()
204                        || !transform.ty.is_finite()
205                    {
206                        return Err(SceneValidationError {
207                            index,
208                            op,
209                            kind: SceneValidationErrorKind::NonFiniteTransform,
210                        });
211                    }
212                    transform_depth = transform_depth.saturating_add(1);
213                }
214                SceneOp::PopTransform => {
215                    if transform_depth == 0 {
216                        return Err(SceneValidationError {
217                            index,
218                            op,
219                            kind: SceneValidationErrorKind::TransformUnderflow,
220                        });
221                    }
222                    transform_depth = transform_depth.saturating_sub(1);
223                }
224                SceneOp::PushOpacity { opacity } => {
225                    if !opacity.is_finite() {
226                        return Err(SceneValidationError {
227                            index,
228                            op,
229                            kind: SceneValidationErrorKind::NonFiniteOpacity,
230                        });
231                    }
232                    opacity_depth = opacity_depth.saturating_add(1);
233                }
234                SceneOp::PopOpacity => {
235                    if opacity_depth == 0 {
236                        return Err(SceneValidationError {
237                            index,
238                            op,
239                            kind: SceneValidationErrorKind::OpacityUnderflow,
240                        });
241                    }
242                    opacity_depth = opacity_depth.saturating_sub(1);
243                }
244                SceneOp::PushLayer { .. } => {
245                    layer_depth = layer_depth.saturating_add(1);
246                }
247                SceneOp::PopLayer => {
248                    if layer_depth == 0 {
249                        return Err(SceneValidationError {
250                            index,
251                            op,
252                            kind: SceneValidationErrorKind::LayerUnderflow,
253                        });
254                    }
255                    layer_depth = layer_depth.saturating_sub(1);
256                }
257                SceneOp::PushClipRect { .. }
258                | SceneOp::PushClipRRect { .. }
259                | SceneOp::PushClipPath { .. } => {
260                    let ok = match op {
261                        SceneOp::PushClipRect { rect } => rect_is_finite(rect),
262                        SceneOp::PushClipRRect { rect, corner_radii } => {
263                            rect_is_finite(rect) && corners_is_finite(corner_radii)
264                        }
265                        SceneOp::PushClipPath { bounds, origin, .. } => {
266                            rect_is_finite(bounds) && point_is_finite(origin)
267                        }
268                        _ => unreachable!(),
269                    };
270                    if !ok {
271                        return Err(SceneValidationError {
272                            index,
273                            op,
274                            kind: SceneValidationErrorKind::NonFiniteOpData,
275                        });
276                    }
277                    clip_depth = clip_depth.saturating_add(1);
278                }
279                SceneOp::PopClip => {
280                    if clip_depth == 0 {
281                        return Err(SceneValidationError {
282                            index,
283                            op,
284                            kind: SceneValidationErrorKind::ClipUnderflow,
285                        });
286                    }
287                    clip_depth = clip_depth.saturating_sub(1);
288                }
289                SceneOp::PushMask { bounds, mask } => {
290                    if !rect_is_finite(bounds) || !mask_is_finite(mask) {
291                        return Err(SceneValidationError {
292                            index,
293                            op,
294                            kind: SceneValidationErrorKind::NonFiniteOpData,
295                        });
296                    }
297                    mask_depth = mask_depth.saturating_add(1);
298                }
299                SceneOp::PopMask => {
300                    if mask_depth == 0 {
301                        return Err(SceneValidationError {
302                            index,
303                            op,
304                            kind: SceneValidationErrorKind::MaskUnderflow,
305                        });
306                    }
307                    mask_depth = mask_depth.saturating_sub(1);
308                }
309                SceneOp::PushEffect { bounds, chain, .. } => {
310                    if !rect_is_finite(bounds) {
311                        return Err(SceneValidationError {
312                            index,
313                            op,
314                            kind: SceneValidationErrorKind::NonFiniteOpData,
315                        });
316                    }
317
318                    for step in chain.iter() {
319                        let ok = match step {
320                            EffectStep::GaussianBlur {
321                                radius_px,
322                                downsample,
323                            } => px_is_finite(radius_px) && downsample > 0,
324                            EffectStep::DropShadowV1(s) => {
325                                px_is_finite(s.offset_px.x)
326                                    && px_is_finite(s.offset_px.y)
327                                    && px_is_finite(s.blur_radius_px)
328                                    && s.downsample > 0
329                                    && s.color.r.is_finite()
330                                    && s.color.g.is_finite()
331                                    && s.color.b.is_finite()
332                                    && s.color.a.is_finite()
333                            }
334                            EffectStep::BackdropWarpV1(w) => {
335                                px_is_finite(w.strength_px)
336                                    && px_is_finite(w.scale_px)
337                                    && w.scale_px.0 > 0.0
338                                    && w.phase.is_finite()
339                                    && px_is_finite(w.chromatic_aberration_px)
340                            }
341                            EffectStep::BackdropWarpV2(w) => {
342                                let base_ok = px_is_finite(w.base.strength_px)
343                                    && px_is_finite(w.base.scale_px)
344                                    && w.base.scale_px.0 > 0.0
345                                    && w.base.phase.is_finite()
346                                    && px_is_finite(w.base.chromatic_aberration_px);
347                                if !base_ok {
348                                    false
349                                } else {
350                                    match w.field {
351                                        BackdropWarpFieldV2::Procedural => true,
352                                        BackdropWarpFieldV2::ImageDisplacementMap {
353                                            uv, ..
354                                        } => {
355                                            uv.u0.is_finite()
356                                                && uv.v0.is_finite()
357                                                && uv.u1.is_finite()
358                                                && uv.v1.is_finite()
359                                        }
360                                    }
361                                }
362                            }
363                            EffectStep::NoiseV1(n) => {
364                                n.strength.is_finite()
365                                    && px_is_finite(n.scale_px)
366                                    && n.scale_px.0 > 0.0
367                                    && n.phase.is_finite()
368                            }
369                            EffectStep::ColorAdjust {
370                                saturation,
371                                brightness,
372                                contrast,
373                            } => {
374                                saturation.is_finite()
375                                    && brightness.is_finite()
376                                    && contrast.is_finite()
377                            }
378                            EffectStep::ColorMatrix { m } => m.iter().all(|v| v.is_finite()),
379                            EffectStep::AlphaThreshold { cutoff, soft } => {
380                                cutoff.is_finite() && soft.is_finite() && soft >= 0.0
381                            }
382                            EffectStep::Pixelate { scale } => scale > 0,
383                            EffectStep::Dither { .. } => true,
384                            EffectStep::CustomV1 {
385                                params,
386                                max_sample_offset_px,
387                                ..
388                            } => params.is_finite() && px_is_finite(max_sample_offset_px),
389                            EffectStep::CustomV2 {
390                                params,
391                                max_sample_offset_px,
392                                input_image,
393                                ..
394                            } => {
395                                let base_ok =
396                                    params.is_finite() && px_is_finite(max_sample_offset_px);
397                                if !base_ok {
398                                    false
399                                } else if let Some(input) = input_image {
400                                    input.uv.u0.is_finite()
401                                        && input.uv.v0.is_finite()
402                                        && input.uv.u1.is_finite()
403                                        && input.uv.v1.is_finite()
404                                } else {
405                                    true
406                                }
407                            }
408                            EffectStep::CustomV3 {
409                                params,
410                                max_sample_offset_px,
411                                user0,
412                                user1,
413                                sources,
414                                ..
415                            } => {
416                                let base_ok =
417                                    params.is_finite() && px_is_finite(max_sample_offset_px);
418                                if !base_ok {
419                                    false
420                                } else {
421                                    let input_ok = |input: Option<CustomEffectImageInputV1>| {
422                                        input.is_none_or(|input| {
423                                            input.uv.u0.is_finite()
424                                                && input.uv.v0.is_finite()
425                                                && input.uv.u1.is_finite()
426                                                && input.uv.v1.is_finite()
427                                        })
428                                    };
429                                    if !input_ok(user0) || !input_ok(user1) {
430                                        false
431                                    } else if let Some(req) = sources.pyramid {
432                                        req.max_levels > 0
433                                            && px_is_finite(req.max_radius_px)
434                                            && req.max_radius_px.0 >= 0.0
435                                    } else {
436                                        true
437                                    }
438                                }
439                            }
440                        };
441                        if !ok {
442                            return Err(SceneValidationError {
443                                index,
444                                op,
445                                kind: SceneValidationErrorKind::NonFiniteOpData,
446                            });
447                        }
448                    }
449
450                    effect_mask_depths.push(mask_depth);
451                    effect_depth = effect_depth.saturating_add(1);
452                }
453                SceneOp::PopEffect => {
454                    if effect_depth == 0 {
455                        return Err(SceneValidationError {
456                            index,
457                            op,
458                            kind: SceneValidationErrorKind::EffectUnderflow,
459                        });
460                    }
461                    let expected = effect_mask_depths.pop().unwrap_or(mask_depth);
462                    if expected != mask_depth {
463                        return Err(SceneValidationError {
464                            index,
465                            op,
466                            kind: SceneValidationErrorKind::EffectMaskCrossing,
467                        });
468                    }
469                    effect_depth = effect_depth.saturating_sub(1);
470                }
471                SceneOp::PushBackdropSourceGroupV1 {
472                    bounds, pyramid, ..
473                } => {
474                    let ok = rect_is_finite(bounds)
475                        && pyramid.is_none_or(|p| {
476                            p.max_levels > 0
477                                && px_is_finite(p.max_radius_px)
478                                && p.max_radius_px.0 >= 0.0
479                        });
480                    if !ok {
481                        return Err(SceneValidationError {
482                            index,
483                            op,
484                            kind: SceneValidationErrorKind::NonFiniteOpData,
485                        });
486                    }
487                    backdrop_source_group_depth = backdrop_source_group_depth.saturating_add(1);
488                }
489                SceneOp::PopBackdropSourceGroup => {
490                    if backdrop_source_group_depth == 0 {
491                        return Err(SceneValidationError {
492                            index,
493                            op,
494                            kind: SceneValidationErrorKind::BackdropSourceGroupUnderflow,
495                        });
496                    }
497                    backdrop_source_group_depth = backdrop_source_group_depth.saturating_sub(1);
498                }
499                SceneOp::PushCompositeGroup { desc } => {
500                    if !rect_is_finite(desc.bounds) {
501                        return Err(SceneValidationError {
502                            index,
503                            op,
504                            kind: SceneValidationErrorKind::NonFiniteOpData,
505                        });
506                    }
507                    composite_group_mask_depths.push(mask_depth);
508                    composite_group_depth = composite_group_depth.saturating_add(1);
509                }
510                SceneOp::PopCompositeGroup => {
511                    if composite_group_depth == 0 {
512                        return Err(SceneValidationError {
513                            index,
514                            op,
515                            kind: SceneValidationErrorKind::CompositeGroupUnderflow,
516                        });
517                    }
518                    let expected = composite_group_mask_depths.pop().unwrap_or(mask_depth);
519                    if expected != mask_depth {
520                        return Err(SceneValidationError {
521                            index,
522                            op,
523                            kind: SceneValidationErrorKind::CompositeGroupMaskCrossing,
524                        });
525                    }
526                    composite_group_depth = composite_group_depth.saturating_sub(1);
527                }
528                SceneOp::Quad {
529                    rect,
530                    background,
531                    border,
532                    border_paint,
533                    corner_radii,
534                    ..
535                } => {
536                    if !rect_is_finite(rect)
537                        || !paint_binding_is_finite(background)
538                        || !edges_is_finite(border)
539                        || !paint_binding_is_finite(border_paint)
540                        || !corners_is_finite(corner_radii)
541                    {
542                        return Err(SceneValidationError {
543                            index,
544                            op,
545                            kind: SceneValidationErrorKind::NonFiniteOpData,
546                        });
547                    }
548                }
549                SceneOp::StrokeRRect {
550                    rect,
551                    stroke,
552                    stroke_paint,
553                    corner_radii,
554                    style,
555                    ..
556                } => {
557                    let mut ok = rect_is_finite(rect)
558                        && edges_is_finite(stroke)
559                        && paint_binding_is_finite(stroke_paint)
560                        && corners_is_finite(corner_radii);
561                    if let Some(dash) = style.dash {
562                        ok = ok
563                            && dash.dash.0.is_finite()
564                            && dash.gap.0.is_finite()
565                            && dash.phase.0.is_finite();
566                    }
567                    if !ok {
568                        return Err(SceneValidationError {
569                            index,
570                            op,
571                            kind: SceneValidationErrorKind::NonFiniteOpData,
572                        });
573                    }
574                }
575                SceneOp::ShadowRRect {
576                    rect,
577                    corner_radii,
578                    offset,
579                    spread,
580                    blur_radius,
581                    color,
582                    ..
583                } => {
584                    if !rect_is_finite(rect)
585                        || !corners_is_finite(corner_radii)
586                        || !point_is_finite(offset)
587                        || !px_is_finite(spread)
588                        || !px_is_finite(blur_radius)
589                        || !color_is_finite(color)
590                    {
591                        return Err(SceneValidationError {
592                            index,
593                            op,
594                            kind: SceneValidationErrorKind::NonFiniteOpData,
595                        });
596                    }
597                }
598                SceneOp::Image { rect, opacity, .. } => {
599                    if !rect_is_finite(rect) || !opacity.is_finite() {
600                        return Err(SceneValidationError {
601                            index,
602                            op,
603                            kind: SceneValidationErrorKind::NonFiniteOpData,
604                        });
605                    }
606                }
607                SceneOp::ImageRegion {
608                    rect, uv, opacity, ..
609                } => {
610                    if !rect_is_finite(rect) || !uv_is_finite(uv) || !opacity.is_finite() {
611                        return Err(SceneValidationError {
612                            index,
613                            op,
614                            kind: SceneValidationErrorKind::NonFiniteOpData,
615                        });
616                    }
617                }
618                SceneOp::MaskImage {
619                    rect,
620                    uv,
621                    color,
622                    opacity,
623                    ..
624                } => {
625                    if !rect_is_finite(rect)
626                        || !uv_is_finite(uv)
627                        || !color_is_finite(color)
628                        || !opacity.is_finite()
629                    {
630                        return Err(SceneValidationError {
631                            index,
632                            op,
633                            kind: SceneValidationErrorKind::NonFiniteOpData,
634                        });
635                    }
636                }
637                SceneOp::SvgMaskIcon {
638                    rect,
639                    color,
640                    opacity,
641                    ..
642                } => {
643                    if !rect_is_finite(rect) || !color_is_finite(color) || !opacity.is_finite() {
644                        return Err(SceneValidationError {
645                            index,
646                            op,
647                            kind: SceneValidationErrorKind::NonFiniteOpData,
648                        });
649                    }
650                }
651                SceneOp::SvgImage { rect, opacity, .. } => {
652                    if !rect_is_finite(rect) || !opacity.is_finite() {
653                        return Err(SceneValidationError {
654                            index,
655                            op,
656                            kind: SceneValidationErrorKind::NonFiniteOpData,
657                        });
658                    }
659                }
660                SceneOp::Text {
661                    origin,
662                    paint,
663                    outline,
664                    shadow,
665                    ..
666                } => {
667                    if !point_is_finite(origin)
668                        || !paint_binding_is_finite(paint)
669                        || outline.is_some_and(|o| {
670                            !paint_binding_is_finite(o.paint) || !o.width_px.0.is_finite()
671                        })
672                        || shadow.is_some_and(|s| !text_shadow_is_finite(s))
673                    {
674                        return Err(SceneValidationError {
675                            index,
676                            op,
677                            kind: SceneValidationErrorKind::NonFiniteOpData,
678                        });
679                    }
680                }
681                SceneOp::Path { origin, paint, .. } => {
682                    if !point_is_finite(origin) || !paint_binding_is_finite(paint) {
683                        return Err(SceneValidationError {
684                            index,
685                            op,
686                            kind: SceneValidationErrorKind::NonFiniteOpData,
687                        });
688                    }
689                }
690                SceneOp::ViewportSurface { rect, opacity, .. } => {
691                    if !rect_is_finite(rect) || !opacity.is_finite() {
692                        return Err(SceneValidationError {
693                            index,
694                            op,
695                            kind: SceneValidationErrorKind::NonFiniteOpData,
696                        });
697                    }
698                }
699            }
700        }
701
702        if transform_depth != 0 {
703            return Err(SceneValidationError {
704                index: self.ops.len(),
705                op: SceneOp::PopTransform,
706                kind: SceneValidationErrorKind::UnbalancedTransformStack {
707                    remaining: transform_depth,
708                },
709            });
710        }
711        if opacity_depth != 0 {
712            return Err(SceneValidationError {
713                index: self.ops.len(),
714                op: SceneOp::PopOpacity,
715                kind: SceneValidationErrorKind::UnbalancedOpacityStack {
716                    remaining: opacity_depth,
717                },
718            });
719        }
720        if layer_depth != 0 {
721            return Err(SceneValidationError {
722                index: self.ops.len(),
723                op: SceneOp::PopLayer,
724                kind: SceneValidationErrorKind::UnbalancedLayerStack {
725                    remaining: layer_depth,
726                },
727            });
728        }
729        if clip_depth != 0 {
730            return Err(SceneValidationError {
731                index: self.ops.len(),
732                op: SceneOp::PopClip,
733                kind: SceneValidationErrorKind::UnbalancedClipStack {
734                    remaining: clip_depth,
735                },
736            });
737        }
738        if mask_depth != 0 {
739            return Err(SceneValidationError {
740                index: self.ops.len(),
741                op: SceneOp::PopMask,
742                kind: SceneValidationErrorKind::UnbalancedMaskStack {
743                    remaining: mask_depth,
744                },
745            });
746        }
747        if effect_depth != 0 {
748            return Err(SceneValidationError {
749                index: self.ops.len(),
750                op: SceneOp::PopEffect,
751                kind: SceneValidationErrorKind::UnbalancedEffectStack {
752                    remaining: effect_depth,
753                },
754            });
755        }
756        if composite_group_depth != 0 {
757            return Err(SceneValidationError {
758                index: self.ops.len(),
759                op: SceneOp::PopCompositeGroup,
760                kind: SceneValidationErrorKind::UnbalancedCompositeGroupStack {
761                    remaining: composite_group_depth,
762                },
763            });
764        }
765        if backdrop_source_group_depth != 0 {
766            return Err(SceneValidationError {
767                index: self.ops.len(),
768                op: SceneOp::PopBackdropSourceGroup,
769                kind: SceneValidationErrorKind::UnbalancedBackdropSourceGroupStack {
770                    remaining: backdrop_source_group_depth,
771                },
772            });
773        }
774
775        Ok(())
776    }
777}