Skip to main content

motion_core/
display_list.rs

1//! Validated device-space drawing commands, with explicit transforms and resources.
2
3use crate::drawing::validate_rect;
4use crate::validation::ValidationState;
5use crate::{
6    Color, DrawCommand, Point, Rect, SceneStats, Size, Transform, ValidationError, ValidationLimits,
7};
8
9/// Integer output dimensions; construction does not allocate pixel storage.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct PixelSize {
12    /// Output width, required to be positive during compilation/preflight.
13    pub width: u32,
14    /// Output height, required to be positive during compilation/preflight.
15    pub height: u32,
16}
17
18impl PixelSize {
19    /// Stores output dimensions; invalid zero dimensions fail preflight.
20    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    /// Returns centered contain mapping and its uniform logical-to-pixel scale.
36    /// Rejects zero output, nonpositive design sizes and unrepresentable mapped extents.
37    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/// Flat drawing commands with composed local-to-device transforms.
77///
78/// Fields remain inspectable/editable; consumers must preflight edited lists.
79/// This is not a PreparedCpuFrame or a renderer and allocates no output pixels.
80#[derive(Clone, Debug, PartialEq)]
81pub struct DisplayList {
82    /// Integer output size, independent of logical design dimensions.
83    pub output_size: PixelSize,
84    /// Optional floating background color; None leaves the output transparent.
85    pub background: Option<Color>,
86    /// Ordered fills and balanced isolated-layer boundaries.
87    pub commands: Vec<DrawCommand>,
88}
89
90/// Immutable command snapshot whose structure and properties have been checked.
91/// Clones share commands without exposing mutation. Validation covers structure,
92/// geometry and paint properties, not raster admission or device availability.
93/// Tighter consumers must check their limits.
94#[derive(Clone, Debug, PartialEq)]
95pub struct ValidatedDisplayList {
96    list: std::sync::Arc<DisplayList>,
97    stats: SceneStats,
98}
99
100impl ValidatedDisplayList {
101    /// Returns an explicitly editable copy, whose consumers must validate again.
102    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    /// Sets only the output background without invalidating command requirements.
107    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    /// Checks positive structural limits against retained validation requirements.
113    /// Rewalks commands only when their measured count/depth exceeds the new allowance,
114    /// preserving the same detailed diagnostic as editable-list preflight.
115    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    /// Read-only checked commands and output description.
125    pub fn display_list(&self) -> &DisplayList {
126        &self.list
127    }
128
129    /// Requirements measured by the original full validation.
130    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    /// Consumes an editable list into a shared immutable validated snapshot.
145    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    /// Validates all commands, including transparent/offscreen geometry.
157    ///
158    /// Limits count commands and every path verb, and layer nesting determines
159    /// output depth. This is not a pixel/allocation budget or backend probe.
160    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
177/// Incrementally validates commands and layer grammar before producing a display list.
178/// Discard the builder after an error; prior structural charges are not rolled back.
179pub struct Builder {
180    list: DisplayList,
181    state: ValidationState,
182    layers: ScopeStack,
183}
184
185impl Builder {
186    /// Creates an empty list with checked output dimensions and structural limits.
187    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    /// Validates and charges a command at the supplied diagnostic path before storing it.
201    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    /// Returns the list only when every layer and mask scope has been closed.
208    pub fn finish(self) -> Result<DisplayList, ValidationError> {
209        require_balanced(&self.layers)?;
210        Ok(self.list)
211    }
212
213    /// Freezes already checked commands without repeating structural traversal.
214    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
418/// Checks local and transformed path controls for finite, representable geometry.
419/// This does not flatten curves, charge structural units or determine pixel coverage.
420pub 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
461/// Maps all rectangle corners and checks finite coordinates and enclosing spans.
462/// Returns a conservative axis-aligned bound, not a geometric clipping region.
463pub 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}