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 if !transform.is_finite() {
426 return Err(ValidationError::invalid(
427 path,
428 "transform",
429 "Command transform must be finite",
430 ));
431 }
432 let mut hull: Option<Rect> = None;
433 for (index, verb) in geometry.verbs().iter().enumerate() {
434 for point in verb.points().into_iter().flatten() {
435 let point = transform.apply(point);
436 if !point.x.is_finite() || !point.y.is_finite() {
437 return Err(ValidationError::invalid(
438 format!("{path}.verbs[{index}]"),
439 "coordinates",
440 "Transformed path coordinates must remain finite",
441 ));
442 }
443 hull = Some(
444 hull.map_or(Rect::new(point.x, point.y, point.x, point.y), |rect| {
445 Rect::new(
446 rect.left.min(point.x),
447 rect.top.min(point.y),
448 rect.right.max(point.x),
449 rect.bottom.max(point.y),
450 )
451 }),
452 );
453 }
454 }
455 if let Some(hull) = hull {
456 validate_rect(hull, path, "coordinates")?;
457 }
458 Ok(())
459}
460
461pub fn mapped_bounds(
464 bounds: Rect,
465 transform: Transform,
466 path: &str,
467) -> Result<Rect, ValidationError> {
468 validate_rect(bounds, path, "bounds")?;
469 if !transform.is_finite() {
470 return Err(ValidationError::invalid(
471 path,
472 "transform",
473 "Command transform must be finite",
474 ));
475 }
476 let corners = [
477 Point::new(bounds.left, bounds.top),
478 Point::new(bounds.right, bounds.top),
479 Point::new(bounds.right, bounds.bottom),
480 Point::new(bounds.left, bounds.bottom),
481 ]
482 .map(|point| transform.apply(point));
483 if corners
484 .iter()
485 .any(|point| !point.x.is_finite() || !point.y.is_finite())
486 {
487 return Err(ValidationError::invalid(
488 path,
489 "transform",
490 "Mapped geometry must remain finite",
491 ));
492 }
493 let rect = Rect::new(
494 corners
495 .iter()
496 .map(|point| point.x)
497 .fold(f64::INFINITY, f64::min),
498 corners
499 .iter()
500 .map(|point| point.y)
501 .fold(f64::INFINITY, f64::min),
502 corners
503 .iter()
504 .map(|point| point.x)
505 .fold(f64::NEG_INFINITY, f64::max),
506 corners
507 .iter()
508 .map(|point| point.y)
509 .fold(f64::NEG_INFINITY, f64::max),
510 );
511 validate_rect(rect, path, "bounds")?;
512 Ok(rect)
513}
514
515fn require_balanced(layers: &ScopeStack) -> Result<(), ValidationError> {
516 if layers.len != 0 {
517 return Err(ValidationError::invalid(
518 "display_list",
519 "layers",
520 "Every layer and mask phase must close in nesting order",
521 ));
522 }
523 Ok(())
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::{FillRule, Paint, Path2D};
530 use std::sync::Arc;
531
532 fn rect() -> DrawCommand {
533 DrawCommand::FillRect {
534 bounds: Rect::new(0.0, 0.0, 10.0, 20.0),
535 transform: Transform::translate(0.125, 0.25),
536 paint: Paint::Solid(Color::WHITE),
537 }
538 }
539
540 fn list(commands: Vec<DrawCommand>) -> DisplayList {
541 DisplayList {
542 output_size: PixelSize::new(200, 200),
543 background: None,
544 commands,
545 }
546 }
547
548 #[test]
549 fn contain_mapping_centers_design_space_and_rejects_invalid_dimensions() {
550 let (transform, scale) = PixelSize::new(200, 200)
551 .mapping(Size::new(100.0, 50.0))
552 .unwrap();
553 assert_eq!(scale, 2.0);
554 assert_eq!(transform.apply(Point::ORIGIN), Point::new(0.0, 50.0));
555 assert_eq!(
556 transform.apply(Point::new(100.0, 50.0)),
557 Point::new(200.0, 150.0)
558 );
559 for size in [
560 Size::new(0.0, 1.0),
561 Size::new(f64::NAN, 1.0),
562 Size::new(f64::from_bits(1), f64::MAX),
563 ] {
564 assert!(PixelSize::new(200, 200).mapping(size).is_err());
565 }
566 assert!(PixelSize::new(0, 100).mapping(Size::new(1.0, 1.0)).is_err());
567 }
568
569 #[test]
570 fn balanced_layers_have_exact_output_work_and_depth_limits() {
571 let commands = vec![
572 DrawCommand::PushLayer {
573 opacity: 0.5,
574 bounds: Rect::new(0.0, 0.0, 10.0, 20.0),
575 blend_mode: crate::drawing::BlendMode::SourceOver,
576 clip: None,
577 },
578 rect(),
579 DrawCommand::PopLayer,
580 ];
581 let list = list(commands);
582 let exact = ValidationLimits {
583 max_nodes: 3,
584 max_depth: 2,
585 };
586 assert_eq!(list.preflight(exact).unwrap().nodes, 3);
587 assert_eq!(
588 list.preflight(ValidationLimits {
589 max_nodes: 2,
590 ..exact
591 })
592 .unwrap_err()
593 .field,
594 "max_nodes"
595 );
596 assert_eq!(
597 list.preflight(ValidationLimits {
598 max_depth: 1,
599 ..exact
600 })
601 .unwrap_err()
602 .field,
603 "max_depth"
604 );
605 }
606
607 #[test]
608 fn edited_lists_reject_unbalanced_or_invalid_layers() {
609 assert_eq!(
610 list(vec![DrawCommand::PopLayer])
611 .preflight(ValidationLimits::default())
612 .unwrap_err()
613 .field,
614 "layers"
615 );
616 for opacity in [0.5, f64::NAN] {
617 let error = list(vec![DrawCommand::PushLayer {
618 opacity,
619 bounds: Rect::default(),
620 blend_mode: crate::drawing::BlendMode::SourceOver,
621 clip: None,
622 }])
623 .preflight(ValidationLimits::default())
624 .unwrap_err();
625 assert_eq!(
626 error.field,
627 if opacity.is_nan() {
628 "opacity"
629 } else {
630 "layers"
631 }
632 );
633 }
634 }
635
636 #[test]
637 fn transformed_controls_validate_even_when_transformed_curve_bounds_fit() {
638 let mut geometry = Path2D::new();
639 geometry.try_move_to(Point::ORIGIN).unwrap();
640 geometry
641 .try_quadratic_to(Point::new(1.0e308, 1.0), Point::new(0.0, 2.0))
642 .unwrap();
643 let command = DrawCommand::Fill {
644 path: Arc::new(geometry),
645 transform: Transform::scale_non_uniform(2.0, 1.0),
646 paint: Paint::Solid(Color::WHITE),
647 rule: FillRule::NonZero,
648 };
649 let error = list(vec![command])
650 .preflight(ValidationLimits::default())
651 .unwrap_err();
652 assert_eq!(error.field, "coordinates");
653 assert!(error.path.contains(".verbs[1]"));
654 }
655
656 #[test]
657 fn builder_charges_each_path_verb_before_retaining_output() {
658 let geometry = Path2D::from_rect(Rect::new(0.0, 0.0, 10.0, 20.0)).unwrap();
659 let command = DrawCommand::Fill {
660 path: Arc::new(geometry),
661 transform: Transform::IDENTITY,
662 paint: Paint::Solid(Color::WHITE),
663 rule: FillRule::NonZero,
664 };
665 let mut exact = Builder::new(
666 PixelSize::new(100, 100),
667 ValidationLimits {
668 max_nodes: 6,
669 max_depth: 1,
670 },
671 )
672 .unwrap();
673 exact.push(command.clone(), "native").unwrap();
674 assert_eq!(exact.finish().unwrap().commands.len(), 1);
675 let mut short = Builder::new(
676 PixelSize::new(100, 100),
677 ValidationLimits {
678 max_nodes: 5,
679 max_depth: 1,
680 },
681 )
682 .unwrap();
683 assert_eq!(
684 short.push(command, "native").unwrap_err().field,
685 "max_nodes"
686 );
687 assert!(short.list.commands.is_empty());
688 }
689}
690
691#[cfg(test)]
692mod validated_tests {
693 use super::*;
694
695 #[test]
696 fn immutable_snapshots_recheck_strict_budgets_and_preserve_diagnostics() {
697 let command = DrawCommand::FillRect {
698 bounds: Rect::new(0.0, 0.0, 2.0, 2.0),
699 transform: Transform::IDENTITY,
700 paint: Color::WHITE.into(),
701 };
702 let mut editable = DisplayList {
703 output_size: PixelSize::new(4, 4),
704 background: None,
705 commands: vec![command.clone(), command],
706 };
707 let validated = editable
708 .clone()
709 .validate(ValidationLimits::default())
710 .unwrap();
711 editable.commands.clear();
712 assert_eq!(validated.display_list().commands.len(), 2);
713 let cloned = validated.clone();
714 assert!(std::ptr::eq(
715 cloned.display_list(),
716 validated.display_list()
717 ));
718 let limits = ValidationLimits {
719 max_nodes: 1,
720 ..ValidationLimits::default()
721 };
722 assert_eq!(
723 validated.preflight(limits).unwrap_err(),
724 validated.display_list().preflight(limits).unwrap_err()
725 );
726 assert_eq!(
727 validated.preflight(ValidationLimits::default()).unwrap(),
728 validated.stats()
729 );
730 }
731}