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}