1use crate::drawing::validate_rect;
4use crate::validation::ValidationState;
5use crate::{
6 Color, DrawCommand, Point, Rect, SceneStats, Size, Transform, ValidationError, ValidationLimits,
7};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct PixelSize {
12 pub width: u32,
14 pub height: u32,
16}
17
18impl PixelSize {
19 pub const fn new(width: u32, height: u32) -> Self {
21 Self { width, height }
22 }
23
24 fn validate(self) -> Result<(), ValidationError> {
25 if self.width == 0 || self.height == 0 {
26 return Err(ValidationError::invalid(
27 "display_list",
28 "size",
29 "Output dimensions must be positive",
30 ));
31 }
32 Ok(())
33 }
34
35 pub fn mapping(self, design: Size) -> Result<(Transform, f64), ValidationError> {
38 self.validate()?;
39 if !design.width.is_finite()
40 || !design.height.is_finite()
41 || design.width <= 0.0
42 || design.height <= 0.0
43 {
44 return Err(ValidationError::invalid(
45 "display_list",
46 "design_size",
47 "Design dimensions must be finite and positive",
48 ));
49 }
50 let width = f64::from(self.width);
51 let height = f64::from(self.height);
52 let scale = (width / design.width).min(height / design.height);
53 let mapped_width = design.width * scale;
54 let mapped_height = design.height * scale;
55 if !scale.is_finite()
56 || scale <= 0.0
57 || !mapped_width.is_finite()
58 || !mapped_height.is_finite()
59 || mapped_width == 0.0
60 || mapped_height == 0.0
61 {
62 return Err(ValidationError::invalid(
63 "display_list",
64 "transform",
65 "Design-to-device mapping must remain finite and nonzero",
66 ));
67 }
68 let transform = Transform::scale_non_uniform(scale, scale).then(Transform::translate(
69 (width - mapped_width) * 0.5,
70 (height - mapped_height) * 0.5,
71 ));
72 Ok((transform, scale))
73 }
74}
75
76#[derive(Clone, Debug, PartialEq)]
81pub struct DisplayList {
82 pub output_size: PixelSize,
84 pub background: Option<Color>,
86 pub commands: Vec<DrawCommand>,
88}
89
90#[derive(Clone, Debug, PartialEq)]
95pub struct ValidatedDisplayList {
96 list: std::sync::Arc<DisplayList>,
97 stats: SceneStats,
98}
99
100impl ValidatedDisplayList {
101 pub fn into_editable(self) -> DisplayList {
103 std::sync::Arc::try_unwrap(self.list).unwrap_or_else(|list| list.as_ref().clone())
104 }
105
106 pub fn with_background(mut self, background: Option<Color>) -> Self {
108 std::sync::Arc::make_mut(&mut self.list).background = background;
109 self
110 }
111
112 pub fn preflight(&self, limits: ValidationLimits) -> Result<SceneStats, ValidationError> {
116 ValidationState::new(limits)?;
117 if self.stats.nodes > limits.max_nodes || self.stats.max_depth > limits.max_depth {
118 self.list.preflight(limits)
119 } else {
120 Ok(self.stats)
121 }
122 }
123
124 pub fn display_list(&self) -> &DisplayList {
126 &self.list
127 }
128
129 pub fn stats(&self) -> SceneStats {
131 self.stats
132 }
133}
134
135impl std::ops::Deref for ValidatedDisplayList {
136 type Target = DisplayList;
137
138 fn deref(&self) -> &Self::Target {
139 self.display_list()
140 }
141}
142
143impl DisplayList {
144 pub fn validate(
146 self,
147 limits: ValidationLimits,
148 ) -> Result<ValidatedDisplayList, ValidationError> {
149 let stats = self.preflight(limits)?;
150 Ok(ValidatedDisplayList {
151 list: std::sync::Arc::new(self),
152 stats,
153 })
154 }
155
156 pub fn preflight(&self, limits: ValidationLimits) -> Result<SceneStats, ValidationError> {
161 self.output_size.validate()?;
162 let mut state = ValidationState::new(limits)?;
163 let mut layers = ScopeStack::default();
164 for (index, command) in self.commands.iter().enumerate() {
165 validate_command(
166 command,
167 &mut state,
168 &mut layers,
169 &format!("display_list.commands[{index}]"),
170 )?;
171 }
172 require_balanced(&layers)?;
173 Ok(state.stats)
174 }
175}
176
177pub struct Builder {
180 list: DisplayList,
181 state: ValidationState,
182 layers: ScopeStack,
183}
184
185impl Builder {
186 pub fn new(size: PixelSize, limits: ValidationLimits) -> Result<Self, ValidationError> {
188 size.validate()?;
189 Ok(Self {
190 list: DisplayList {
191 output_size: size,
192 background: None,
193 commands: Vec::new(),
194 },
195 state: ValidationState::new(limits)?,
196 layers: ScopeStack::default(),
197 })
198 }
199
200 pub fn push(&mut self, command: DrawCommand, path: &str) -> Result<(), ValidationError> {
202 validate_command(&command, &mut self.state, &mut self.layers, path)?;
203 self.list.commands.push(command);
204 Ok(())
205 }
206
207 pub fn finish(self) -> Result<DisplayList, ValidationError> {
209 require_balanced(&self.layers)?;
210 Ok(self.list)
211 }
212
213 pub fn finish_validated(self) -> Result<ValidatedDisplayList, ValidationError> {
215 require_balanced(&self.layers)?;
216 Ok(ValidatedDisplayList {
217 list: std::sync::Arc::new(self.list),
218 stats: self.state.stats,
219 })
220 }
221}
222
223#[derive(Clone, Copy)]
224enum Scope {
225 Layer { masked: bool },
226 Mask,
227}
228
229struct ScopeStack {
230 entries: [Scope; 128],
231 len: usize,
232}
233
234impl Default for ScopeStack {
235 fn default() -> Self {
236 Self {
237 entries: [Scope::Mask; 128],
238 len: 0,
239 }
240 }
241}
242
243impl ScopeStack {
244 fn last(&self) -> Option<Scope> {
245 self.len.checked_sub(1).map(|index| self.entries[index])
246 }
247}
248
249fn validate_command(
250 command: &DrawCommand,
251 state: &mut ValidationState,
252 layers: &mut ScopeStack,
253 path: &str,
254) -> Result<(), ValidationError> {
255 if let DrawCommand::FillRect { paint, .. } | DrawCommand::Fill { paint, .. } = command {
256 paint.validate().map_err(|mut error| {
257 error.path = path.to_owned();
258 error
259 })?;
260 }
261
262 if matches!(layers.last(), Some(Scope::Layer { masked: true }))
263 && !matches!(command, DrawCommand::PopLayer)
264 {
265 return Err(ValidationError::invalid(
266 path,
267 "masks",
268 "Only PopLayer may follow completed mask artwork",
269 ));
270 }
271
272 let depth = match command {
273 DrawCommand::PopLayer if !matches!(layers.last(), Some(Scope::Layer { .. })) => {
274 return Err(ValidationError::invalid(
275 path,
276 "layers",
277 "PopLayer requires an open layer",
278 ));
279 }
280 DrawCommand::EndMask if !matches!(layers.last(), Some(Scope::Mask)) => {
281 return Err(ValidationError::invalid(
282 path,
283 "masks",
284 "EndMask requires an open mask phase",
285 ));
286 }
287 DrawCommand::BeginMask { .. }
288 if !matches!(layers.last(), Some(Scope::Layer { masked: false })) =>
289 {
290 return Err(ValidationError::invalid(
291 path,
292 "masks",
293 "BeginMask requires an unmasked layer",
294 ));
295 }
296 DrawCommand::PopLayer | DrawCommand::EndMask => layers.len,
297 _ => layers.len + 1,
298 };
299 let verbs = match command {
300 DrawCommand::ParticleInstances { batch, transform } => {
301 batch.validate()?;
302 mapped_bounds(batch.bounds(), *transform, path)?;
303 batch.len()
304 }
305 DrawCommand::PushFilterLayer { bounds, filters } => {
306 validate_rect(*bounds, path, "bounds")?;
307 let mut current = *bounds;
308 for (index, filter) in filters.iter().enumerate() {
309 let path = format!("{path}.filters[{index}]");
310 filter.validate(&path)?;
311 for operation in 0..filter.structural_cost() {
312 state.enter_with(|| format!("{path}[{operation}]"), depth)?;
313 }
314 current = filter.extent(current, &path)?.0;
315 }
316 0
317 }
318 DrawCommand::PushLayer {
319 opacity,
320 bounds,
321 clip,
322 ..
323 }
324 | DrawCommand::PushRectLayer {
325 opacity,
326 bounds,
327 clip,
328 ..
329 }
330 | DrawCommand::PushShapeLayer {
331 opacity,
332 bounds,
333 clip,
334 ..
335 } => {
336 validate_rect(*bounds, path, "bounds")?;
337 if let DrawCommand::PushRectLayer { rect_clip, .. }
338 | DrawCommand::PushShapeLayer {
339 rect_clip: Some(rect_clip),
340 ..
341 } = command
342 {
343 let clip_path = format!("{path}.rect_clip");
344 mapped_bounds(rect_clip.bounds, rect_clip.transform, &clip_path)?;
345 state.enter(&clip_path, depth)?;
346 }
347 if let DrawCommand::PushShapeLayer { shape_clip, .. } = command {
348 let clip_path = format!("{path}.shape_clip");
349 let verbs = shape_clip.validate(&clip_path)?;
350 state.enter(&clip_path, depth)?;
351 for index in 0..verbs {
352 state.enter_with(|| format!("{clip_path}.verbs[{index}]"), depth)?;
353 }
354 }
355 if !opacity.is_finite() || !(0.0..=1.0).contains(opacity) {
356 return Err(ValidationError::invalid(
357 path,
358 "opacity",
359 "Layer opacity must be finite in 0..=1",
360 ));
361 }
362 if let Some(clip) = clip {
363 validate_path(&clip.path, clip.transform, &format!("{path}.clip"))?;
364 clip.path.verbs().len()
365 } else {
366 0
367 }
368 }
369 DrawCommand::PopLayer | DrawCommand::BeginMask { .. } | DrawCommand::EndMask => 0,
370 DrawCommand::FillRect {
371 bounds, transform, ..
372 } => {
373 mapped_bounds(*bounds, *transform, path)?;
374 0
375 }
376 DrawCommand::Fill {
377 path: geometry,
378 transform,
379 ..
380 } => {
381 validate_path(geometry, *transform, path)?;
382 geometry.verbs().len()
383 }
384 };
385 state.enter(path, depth)?;
386 for index in 0..verbs {
387 let prefix = if matches!(
388 command,
389 DrawCommand::PushLayer { .. }
390 | DrawCommand::PushRectLayer { .. }
391 | DrawCommand::PushShapeLayer { .. }
392 ) {
393 format!("{path}.clip")
394 } else {
395 path.to_owned()
396 };
397 state.enter_with(|| format!("{prefix}.verbs[{index}]"), depth)?;
398 }
399 match command {
400 DrawCommand::PushFilterLayer { .. }
401 | DrawCommand::PushLayer { .. }
402 | DrawCommand::PushRectLayer { .. }
403 | DrawCommand::PushShapeLayer { .. } => {
404 layers.entries[layers.len] = Scope::Layer { masked: false };
405 layers.len += 1;
406 }
407 DrawCommand::BeginMask { .. } => {
408 layers.entries[layers.len - 1] = Scope::Layer { masked: true };
409 layers.entries[layers.len] = Scope::Mask;
410 layers.len += 1;
411 }
412 DrawCommand::PopLayer | DrawCommand::EndMask => layers.len -= 1,
413 _ => {}
414 }
415 Ok(())
416}
417
418pub fn validate_path(
421 geometry: &crate::Path2D,
422 transform: Transform,
423 path: &str,
424) -> Result<(), ValidationError> {
425 geometry.validate()?;
426 if !transform.is_finite() {
427 return Err(ValidationError::invalid(
428 path,
429 "transform",
430 "Command transform must be finite",
431 ));
432 }
433 let mut hull: Option<Rect> = None;
434 for (index, verb) in geometry.verbs().iter().enumerate() {
435 for point in verb.points().into_iter().flatten() {
436 let point = transform.apply(point);
437 if !point.x.is_finite() || !point.y.is_finite() {
438 return Err(ValidationError::invalid(
439 format!("{path}.verbs[{index}]"),
440 "coordinates",
441 "Transformed path coordinates must remain finite",
442 ));
443 }
444 hull = Some(
445 hull.map_or(Rect::new(point.x, point.y, point.x, point.y), |rect| {
446 Rect::new(
447 rect.left.min(point.x),
448 rect.top.min(point.y),
449 rect.right.max(point.x),
450 rect.bottom.max(point.y),
451 )
452 }),
453 );
454 }
455 }
456 if let Some(hull) = hull {
457 validate_rect(hull, path, "coordinates")?;
458 }
459 Ok(())
460}
461
462pub fn mapped_bounds(
465 bounds: Rect,
466 transform: Transform,
467 path: &str,
468) -> Result<Rect, ValidationError> {
469 validate_rect(bounds, path, "bounds")?;
470 if !transform.is_finite() {
471 return Err(ValidationError::invalid(
472 path,
473 "transform",
474 "Command transform must be finite",
475 ));
476 }
477 let corners = [
478 Point::new(bounds.left, bounds.top),
479 Point::new(bounds.right, bounds.top),
480 Point::new(bounds.right, bounds.bottom),
481 Point::new(bounds.left, bounds.bottom),
482 ]
483 .map(|point| transform.apply(point));
484 if corners
485 .iter()
486 .any(|point| !point.x.is_finite() || !point.y.is_finite())
487 {
488 return Err(ValidationError::invalid(
489 path,
490 "transform",
491 "Mapped geometry must remain finite",
492 ));
493 }
494 let rect = Rect::new(
495 corners
496 .iter()
497 .map(|point| point.x)
498 .fold(f64::INFINITY, f64::min),
499 corners
500 .iter()
501 .map(|point| point.y)
502 .fold(f64::INFINITY, f64::min),
503 corners
504 .iter()
505 .map(|point| point.x)
506 .fold(f64::NEG_INFINITY, f64::max),
507 corners
508 .iter()
509 .map(|point| point.y)
510 .fold(f64::NEG_INFINITY, f64::max),
511 );
512 validate_rect(rect, path, "bounds")?;
513 Ok(rect)
514}
515
516fn require_balanced(layers: &ScopeStack) -> Result<(), ValidationError> {
517 if layers.len != 0 {
518 return Err(ValidationError::invalid(
519 "display_list",
520 "layers",
521 "Every layer and mask phase must close in nesting order",
522 ));
523 }
524 Ok(())
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use crate::{FillRule, Paint, Path2D};
531 use std::sync::Arc;
532
533 fn rect() -> DrawCommand {
534 DrawCommand::FillRect {
535 bounds: Rect::new(0.0, 0.0, 10.0, 20.0),
536 transform: Transform::translate(0.125, 0.25),
537 paint: Paint::Solid(Color::WHITE),
538 }
539 }
540
541 fn list(commands: Vec<DrawCommand>) -> DisplayList {
542 DisplayList {
543 output_size: PixelSize::new(200, 200),
544 background: None,
545 commands,
546 }
547 }
548
549 #[test]
550 fn contain_mapping_centers_design_space_and_rejects_invalid_dimensions() {
551 let (transform, scale) = PixelSize::new(200, 200)
552 .mapping(Size::new(100.0, 50.0))
553 .unwrap();
554 assert_eq!(scale, 2.0);
555 assert_eq!(transform.apply(Point::ORIGIN), Point::new(0.0, 50.0));
556 assert_eq!(
557 transform.apply(Point::new(100.0, 50.0)),
558 Point::new(200.0, 150.0)
559 );
560 for size in [
561 Size::new(0.0, 1.0),
562 Size::new(f64::NAN, 1.0),
563 Size::new(f64::from_bits(1), f64::MAX),
564 ] {
565 assert!(PixelSize::new(200, 200).mapping(size).is_err());
566 }
567 assert!(PixelSize::new(0, 100).mapping(Size::new(1.0, 1.0)).is_err());
568 }
569
570 #[test]
571 fn balanced_layers_have_exact_output_work_and_depth_limits() {
572 let commands = vec![
573 DrawCommand::PushLayer {
574 opacity: 0.5,
575 bounds: Rect::new(0.0, 0.0, 10.0, 20.0),
576 blend_mode: crate::drawing::BlendMode::SourceOver,
577 clip: None,
578 },
579 rect(),
580 DrawCommand::PopLayer,
581 ];
582 let list = list(commands);
583 let exact = ValidationLimits {
584 max_nodes: 3,
585 max_depth: 2,
586 };
587 assert_eq!(list.preflight(exact).unwrap().nodes, 3);
588 assert_eq!(
589 list.preflight(ValidationLimits {
590 max_nodes: 2,
591 ..exact
592 })
593 .unwrap_err()
594 .field,
595 "max_nodes"
596 );
597 assert_eq!(
598 list.preflight(ValidationLimits {
599 max_depth: 1,
600 ..exact
601 })
602 .unwrap_err()
603 .field,
604 "max_depth"
605 );
606 }
607
608 #[test]
609 fn edited_lists_reject_unbalanced_or_invalid_layers() {
610 assert_eq!(
611 list(vec![DrawCommand::PopLayer])
612 .preflight(ValidationLimits::default())
613 .unwrap_err()
614 .field,
615 "layers"
616 );
617 for opacity in [0.5, f64::NAN] {
618 let error = list(vec![DrawCommand::PushLayer {
619 opacity,
620 bounds: Rect::default(),
621 blend_mode: crate::drawing::BlendMode::SourceOver,
622 clip: None,
623 }])
624 .preflight(ValidationLimits::default())
625 .unwrap_err();
626 assert_eq!(
627 error.field,
628 if opacity.is_nan() {
629 "opacity"
630 } else {
631 "layers"
632 }
633 );
634 }
635 }
636
637 #[test]
638 fn transformed_controls_validate_even_when_transformed_curve_bounds_fit() {
639 let mut geometry = Path2D::new();
640 geometry.try_move_to(Point::ORIGIN).unwrap();
641 geometry
642 .try_quadratic_to(Point::new(1.0e308, 1.0), Point::new(0.0, 2.0))
643 .unwrap();
644 let command = DrawCommand::Fill {
645 path: Arc::new(geometry),
646 transform: Transform::scale_non_uniform(2.0, 1.0),
647 paint: Paint::Solid(Color::WHITE),
648 rule: FillRule::NonZero,
649 };
650 let error = list(vec![command])
651 .preflight(ValidationLimits::default())
652 .unwrap_err();
653 assert_eq!(error.field, "coordinates");
654 assert!(error.path.contains(".verbs[1]"));
655 }
656
657 #[test]
658 fn builder_charges_each_path_verb_before_retaining_output() {
659 let geometry = Path2D::from_rect(Rect::new(0.0, 0.0, 10.0, 20.0)).unwrap();
660 let command = DrawCommand::Fill {
661 path: Arc::new(geometry),
662 transform: Transform::IDENTITY,
663 paint: Paint::Solid(Color::WHITE),
664 rule: FillRule::NonZero,
665 };
666 let mut exact = Builder::new(
667 PixelSize::new(100, 100),
668 ValidationLimits {
669 max_nodes: 6,
670 max_depth: 1,
671 },
672 )
673 .unwrap();
674 exact.push(command.clone(), "native").unwrap();
675 assert_eq!(exact.finish().unwrap().commands.len(), 1);
676 let mut short = Builder::new(
677 PixelSize::new(100, 100),
678 ValidationLimits {
679 max_nodes: 5,
680 max_depth: 1,
681 },
682 )
683 .unwrap();
684 assert_eq!(
685 short.push(command, "native").unwrap_err().field,
686 "max_nodes"
687 );
688 assert!(short.list.commands.is_empty());
689 }
690}
691
692#[cfg(test)]
693mod validated_tests {
694 use super::*;
695
696 #[test]
697 fn immutable_snapshots_recheck_strict_budgets_and_preserve_diagnostics() {
698 let command = DrawCommand::FillRect {
699 bounds: Rect::new(0.0, 0.0, 2.0, 2.0),
700 transform: Transform::IDENTITY,
701 paint: Color::WHITE.into(),
702 };
703 let mut editable = DisplayList {
704 output_size: PixelSize::new(4, 4),
705 background: None,
706 commands: vec![command.clone(), command],
707 };
708 let validated = editable
709 .clone()
710 .validate(ValidationLimits::default())
711 .unwrap();
712 editable.commands.clear();
713 assert_eq!(validated.display_list().commands.len(), 2);
714 let cloned = validated.clone();
715 assert!(std::ptr::eq(
716 cloned.display_list(),
717 validated.display_list()
718 ));
719 let limits = ValidationLimits {
720 max_nodes: 1,
721 ..ValidationLimits::default()
722 };
723 assert_eq!(
724 validated.preflight(limits).unwrap_err(),
725 validated.display_list().preflight(limits).unwrap_err()
726 );
727 assert_eq!(
728 validated.preflight(ValidationLimits::default()).unwrap(),
729 validated.stats()
730 );
731 }
732}