Skip to main content

alpha2_model/
composition.rs

1//! Composition graphs: a clip's picture built from connected nodes.
2//!
3//! A [`Composition`] is an acyclic graph of image operations stored once in
4//! the project. A clip that shows one refers to it with a
5//! [`ClipComposition`], which also says how the clip's timeline time maps
6//! onto the composition's own time. Every node outputs one image; a node
7//! takes its inputs by naming the nodes that produce them, so a node's
8//! output can feed any number of later nodes, and an input can be left
9//! unconnected while the graph is being built.
10//!
11//! A composition can also be used inside another: a [`NodeOp::Group`] node
12//! draws it, feeding its [`NodeOp::GroupInput`] nodes from the group node's
13//! inputs and setting the node settings it exposes as
14//! [`CompositionControl`]s, independently for each group node. A
15//! [`crate::MediaKind::Composition`] source shows one on the timeline, or in
16//! a media node. Compositions may not use themselves, however indirectly.
17//! A composition is inlined wherever it is drawn, so its uses multiply:
18//! nesting is bounded by [`MAX_COMPOSITION_DEPTH`] and what all compositions
19//! draw by [`MAX_INLINED_NODES`].
20//!
21//! What a graph means is checked in two steps. Saving checks its structure:
22//! that every name resolves, that nothing depends on itself, and that every
23//! value is in range. Rendering then checks what the chosen output needs:
24//! every input a node reachable from the output uses must be connected.
25//! Nodes that do not reach the output are kept and ignored.
26
27use crate::animation::opaque;
28use crate::{
29    Animated, ClipBlendMode, ClipEffect, CompositionId, EffectValue, Generator, MatteMode, NodeId,
30    ShapeMask, VisualProperties,
31};
32use alpha2_media::SourceId;
33use alpha2_time::MediaTime;
34use serde::{Deserialize, Serialize};
35use std::collections::BTreeMap;
36use std::collections::{HashMap, HashSet};
37
38/// Most compositions in one project.
39pub const MAX_COMPOSITIONS: usize = 10_000;
40/// Most nodes in one composition.
41pub const MAX_COMPOSITION_NODES: usize = 256;
42/// The most named inputs an effect node gives its effect: what an effect
43/// definition may declare.
44pub const MAX_EFFECT_INPUTS: usize = 8;
45/// Most layers in one merge node.
46pub const MAX_MERGE_LAYERS: usize = 32;
47/// Most inputs a composition takes as a group.
48pub const MAX_GROUP_INPUTS: usize = 16;
49/// Most controls a composition exposes as a group.
50pub const MAX_COMPOSITION_CONTROLS: usize = 64;
51/// Deepest nesting of compositions: a composition that draws no other has
52/// depth one, and one that draws others, through group nodes or
53/// composition-source media nodes, is one deeper than the deepest of them.
54/// Drawing a composition recurses once per level.
55pub const MAX_COMPOSITION_DEPTH: usize = 64;
56/// Most nodes the project's compositions draw, summed over every
57/// composition: each node counts one, except a group node or a media node
58/// reading a composition source, which counts the nodes of the composition
59/// it draws. A composition is inlined wherever it is drawn, so this bounds
60/// what compiling the compositions makes, however their uses multiply.
61pub const MAX_INLINED_NODES: usize = 65_536;
62/// Most composition nodes the clips on the timeline at one instant draw
63/// together: each clip counts the nodes its composition and its composition
64/// source draw once inlined, as [`MAX_INLINED_NODES`] counts them, and a
65/// clip showing neither counts none. [`MAX_INLINED_NODES`] bounds what the
66/// compositions compile to however often they are used; this bounds what
67/// their uses draw in one frame, so a frame never draws more than one
68/// composition of the largest size the project may hold.
69pub const MAX_FRAME_INLINED_NODES: usize = 65_536;
70
71/// An acyclic graph of image operations with one selected output.
72#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
73#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74#[serde(deny_unknown_fields)]
75pub struct Composition {
76    /// Stable composition identity.
77    pub id: CompositionId,
78    /// The graph's nodes. Their order has no meaning: a node may name a node
79    /// listed after it as its input.
80    #[serde(default)]
81    pub nodes: Vec<CompositionNode>,
82    /// The node whose image is the composition's picture. A composition
83    /// without one saves, but cannot be rendered.
84    #[serde(default)]
85    pub output: Option<NodeId>,
86    /// The images a group node gives it, by name, in port order: its
87    /// [`NodeOp::GroupInput`] nodes read them.
88    #[serde(default, skip_serializing_if = "Vec::is_empty")]
89    pub inputs: Vec<String>,
90    /// The node settings a group node may set, by name.
91    #[serde(default, skip_serializing_if = "Vec::is_empty")]
92    pub controls: Vec<CompositionControl>,
93}
94
95/// A node setting a composition exposes to the group nodes that use it: an
96/// effect or transition parameter of one of its nodes.
97#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
98#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
99#[serde(deny_unknown_fields)]
100pub struct CompositionControl {
101    /// The name a group node sets it by; unique in the composition.
102    pub name: String,
103    /// The effect or transition node.
104    pub node: NodeId,
105    /// The parameter of its effect.
106    pub param: String,
107}
108
109impl Composition {
110    /// Creates an empty composition with no output.
111    #[must_use]
112    pub const fn new(id: CompositionId) -> Self {
113        Self {
114            id,
115            nodes: Vec::new(),
116            output: None,
117            inputs: Vec::new(),
118            controls: Vec::new(),
119        }
120    }
121
122    /// The node named `id`.
123    #[must_use]
124    pub fn node(&self, id: &NodeId) -> Option<&CompositionNode> {
125        self.nodes.iter().find(|node| &node.id == id)
126    }
127}
128
129/// One image operation in a [`Composition`].
130#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
131#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
132#[serde(deny_unknown_fields)]
133pub struct CompositionNode {
134    /// Identity, unique within its composition.
135    pub id: NodeId,
136    /// Whether the node passes its [`NodeOp::bypass_input`] through
137    /// unchanged instead of operating. A node without inputs cannot be
138    /// bypassed.
139    #[serde(default)]
140    pub bypass: bool,
141    /// What the node does, with its inputs and settings.
142    pub op: NodeOp,
143}
144
145impl CompositionNode {
146    /// Creates a node that is not bypassed.
147    #[must_use]
148    pub const fn new(id: NodeId, op: NodeOp) -> Self {
149        Self {
150            id,
151            bypass: false,
152            op,
153        }
154    }
155
156    /// The inputs drawing the node reads, in input order: only its
157    /// [`NodeOp::bypass_input`] when it is bypassed (none for a merge without
158    /// layers), and every input otherwise. The renderer plans these, and
159    /// [`crate::Project::used_sources`] follows them.
160    #[must_use]
161    pub fn needed_inputs(&self) -> Vec<NodeInput<'_>> {
162        if self.bypass {
163            return self.op.bypass_input().into_iter().collect();
164        }
165        self.op.inputs()
166    }
167}
168
169/// The operation of a [`CompositionNode`].
170///
171/// Animated settings are keyed in composition time (see
172/// [`ClipComposition`]), not timeline time.
173#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
174#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
175#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
176pub enum NodeOp {
177    /// The picture of the clip showing the composition: its source at the
178    /// source time the clip's time map gives, or what its generated source
179    /// draws.
180    Clip {},
181    /// Another visual source, read at `start` plus the composition time.
182    /// A media node adds no sound to the mix.
183    Media {
184        /// The source; a video, image, or generated source.
185        source_id: SourceId,
186        /// Source time shown at composition time zero. Non-negative.
187        #[serde(default)]
188        start: MediaTime,
189    },
190    /// A picture the engine draws.
191    Generator {
192        /// What is drawn.
193        generator: Generator,
194    },
195    /// A registered effect: its input and the named images it samples
196    /// besides.
197    Effect {
198        /// The image the effect processes; a bypassed node passes it through.
199        #[serde(default)]
200        input: Option<NodeId>,
201        /// The images the effect samples besides its input, by the names its
202        /// definition declares (`sample_<name>` in its shader); at most
203        /// [`MAX_EFFECT_INPUTS`]. A name the definition does not declare, or
204        /// one it declares and this leaves out, fails the frames that draw
205        /// the node.
206        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
207        inputs: BTreeMap<String, Option<NodeId>>,
208        /// The versioned effect and its parameters.
209        effect: ClipEffect,
210    },
211    /// A registered two-input transition at an explicit progress.
212    Transition {
213        /// The image at progress zero.
214        #[serde(default)]
215        from: Option<NodeId>,
216        /// The image at progress one.
217        #[serde(default)]
218        to: Option<NodeId>,
219        /// The versioned transition and its parameters.
220        effect: ClipEffect,
221        /// Progress through the transition, in `0..=1`.
222        progress: Animated<f32>,
223    },
224    /// The input placed on the canvas by a transform and opacity.
225    Transform {
226        /// The image placed.
227        #[serde(default)]
228        input: Option<NodeId>,
229        /// Position, anchor, scale, rotation, and opacity.
230        #[serde(default)]
231        transform: VisualProperties,
232    },
233    /// Ordered layers composited over transparency.
234    Merge {
235        /// Layers from bottom to top.
236        #[serde(default)]
237        layers: Vec<MergeLayer>,
238    },
239    /// The input kept where a second image says.
240    Matte {
241        /// The image kept.
242        #[serde(default)]
243        input: Option<NodeId>,
244        /// The image whose alpha or luminance is coverage.
245        #[serde(default)]
246        matte: Option<NodeId>,
247        /// Which property of the matte image is coverage.
248        mode: MatteMode,
249    },
250    /// The input kept inside shapes.
251    ShapeMask {
252        /// The image masked.
253        #[serde(default)]
254        input: Option<NodeId>,
255        /// The shapes, combined in order; at most [`crate::MAX_CLIP_MASKS`].
256        /// Geometry is in the input image's normalized space.
257        masks: Vec<ShapeMask>,
258    },
259    /// Another composition drawn in this one's time, as a group: its
260    /// output, with its group inputs fed from `inputs` and its controls set
261    /// from `controls`.
262    Group {
263        /// The composition drawn.
264        composition: CompositionId,
265        /// The image for each of its inputs, in its port order.
266        #[serde(default)]
267        inputs: Vec<Option<NodeId>>,
268        /// Values for its controls, by name; a control not set keeps the
269        /// composition's own value. Keyed in composition time.
270        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
271        controls: BTreeMap<String, Animated<EffectValue>>,
272    },
273    /// In a composition used as a group, the image the group node gives its
274    /// input `index`.
275    GroupInput {
276        /// The input, in the composition's port order.
277        index: u32,
278    },
279}
280
281/// One layer of a [`NodeOp::Merge`].
282#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
283#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
284#[serde(deny_unknown_fields)]
285pub struct MergeLayer {
286    /// The layer's image.
287    #[serde(default)]
288    pub input: Option<NodeId>,
289    /// How the layer blends with the layers under it.
290    #[serde(default)]
291    pub blend_mode: ClipBlendMode,
292    /// The layer's opacity, in `0..=1`.
293    #[serde(default = "opaque")]
294    pub opacity: Animated<f32>,
295}
296
297impl MergeLayer {
298    /// An opaque, normally blended layer of `input`.
299    #[must_use]
300    pub const fn new(input: Option<NodeId>) -> Self {
301        Self {
302            input,
303            blend_mode: ClipBlendMode::Normal,
304            opacity: Animated::Constant { value: 1.0 },
305        }
306    }
307}
308
309/// What each composition's output reads: the group inputs and clip nodes a
310/// walk from its output reaches, along the inputs drawing each node reads,
311/// into the compositions its group nodes draw. A group draws what its nodes
312/// would inline, so a group node needs only the ports its composition reads:
313/// a port it never reads may be left unconnected, and what a connected one
314/// reads is not drawn.
315///
316/// Built once for a project's compositions, in dependency order, without
317/// recursion. A composition that uses itself (which validation refuses) is
318/// taken to read every port and draw the clip.
319#[derive(Clone, Debug, Default)]
320pub struct CompositionReads<'a> {
321    read: HashMap<&'a CompositionId, Reads>,
322}
323
324/// What one composition's output reads.
325#[derive(Clone, Debug)]
326struct Reads {
327    /// By group input index.
328    ports: Vec<bool>,
329    /// Whether it draws the picture of the clip showing it.
330    clip: bool,
331}
332
333impl<'a> CompositionReads<'a> {
334    /// What each of `compositions` reads; it resolves group nodes by ID.
335    #[must_use]
336    pub fn new(compositions: &HashMap<&'a CompositionId, &'a Composition>) -> Self {
337        let mut reads = Self::default();
338        let mut walking = HashSet::new();
339        let group = |node: &'a CompositionNode| match &node.op {
340            NodeOp::Group { composition, .. } => compositions.get(composition).copied(),
341            _ => None,
342        };
343        for root in compositions.values() {
344            let mut stack = vec![(*root, false)];
345            while let Some((composition, expanded)) = stack.pop() {
346                if reads.read.contains_key(&composition.id) {
347                    continue;
348                }
349                if !expanded {
350                    if !walking.insert(&composition.id) {
351                        continue;
352                    }
353                    stack.push((composition, true));
354                    stack.extend(
355                        composition
356                            .nodes
357                            .iter()
358                            .filter_map(group)
359                            .filter(|inner| {
360                                !reads.read.contains_key(&inner.id) && !walking.contains(&inner.id)
361                            })
362                            .map(|inner| (inner, false)),
363                    );
364                    continue;
365                }
366                walking.remove(&composition.id);
367                let read = reads.walk(composition);
368                reads.read.insert(&composition.id, read);
369            }
370        }
371        reads
372    }
373
374    /// Whether `composition`'s output reads its group input `port`; true for
375    /// a composition these reads do not know.
376    #[must_use]
377    pub fn reads_port(&self, composition: &CompositionId, port: usize) -> bool {
378        self.read
379            .get(composition)
380            .is_none_or(|read| read.ports.get(port).copied().unwrap_or(false))
381    }
382
383    /// Whether `composition`, shown by a clip, draws the clip's picture: its
384    /// output reaches a clip node, directly or in a group. True for a
385    /// composition these reads do not know.
386    #[must_use]
387    pub fn draws_clip(&self, composition: &CompositionId) -> bool {
388        self.read.get(composition).is_none_or(|read| read.clip)
389    }
390
391    /// The inputs drawing `node` reads, in input order: its
392    /// [`CompositionNode::needed_inputs`], of a group node only the ports its
393    /// composition reads.
394    #[must_use]
395    pub fn needed_inputs<'n>(&self, node: &'n CompositionNode) -> Vec<NodeInput<'n>> {
396        let mut needed = node.needed_inputs();
397        if let (false, NodeOp::Group { composition, .. }) = (node.bypass, &node.op) {
398            needed.retain(|input| match input {
399                NodeInput::Port(port, _) => self.reads_port(composition, *port),
400                _ => true,
401            });
402        }
403        needed
404    }
405
406    /// What `composition`'s output reaches, with the compositions its group
407    /// nodes draw already known.
408    fn walk(&self, composition: &'a Composition) -> Reads {
409        let nodes: HashMap<&NodeId, &CompositionNode> = composition
410            .nodes
411            .iter()
412            .map(|node| (&node.id, node))
413            .collect();
414        let mut read = Reads {
415            ports: vec![false; composition.inputs.len()],
416            clip: false,
417        };
418        let mut reached = HashSet::new();
419        let mut pending: Vec<&NodeId> = composition.output.iter().collect();
420        while let Some(id) = pending.pop() {
421            let Some(node) = nodes.get(id).filter(|_| reached.insert(id)) else {
422                continue;
423            };
424            match (node.bypass, &node.op) {
425                (false, NodeOp::GroupInput { index }) => {
426                    if let Some(port) = usize::try_from(*index)
427                        .ok()
428                        .and_then(|index| read.ports.get_mut(index))
429                    {
430                        *port = true;
431                    }
432                }
433                (false, NodeOp::Clip {}) => read.clip = true,
434                (false, NodeOp::Group { composition, .. }) => {
435                    read.clip |= self.draws_clip(composition);
436                }
437                _ => {}
438            }
439            pending.extend(
440                self.needed_inputs(node)
441                    .into_iter()
442                    .filter_map(NodeInput::node),
443            );
444        }
445        read
446    }
447}
448
449/// One named input of a node, as a node's inputs are listed.
450#[derive(Clone, Copy, Debug, Eq, PartialEq)]
451pub enum NodeInput<'a> {
452    /// A named input: `input`, `from`, `to`, or `matte`.
453    Named(&'static str, Option<&'a NodeId>),
454    /// The input of a merge layer, by layer index.
455    Layer(usize, Option<&'a NodeId>),
456    /// An input of a group node, by port index.
457    Port(usize, Option<&'a NodeId>),
458    /// A named input of an effect node.
459    EffectInput(&'a str, Option<&'a NodeId>),
460}
461
462impl<'a> NodeInput<'a> {
463    /// The connected node, if any.
464    #[must_use]
465    pub const fn node(self) -> Option<&'a NodeId> {
466        match self {
467            Self::Named(_, node)
468            | Self::Layer(_, node)
469            | Self::Port(_, node)
470            | Self::EffectInput(_, node) => node,
471        }
472    }
473
474    /// The input's name as a diagnostic shows it: `input`, `from`, `to`,
475    /// `matte`, `layers[i]`, `inputs[i]`, or `inputs.<name>`.
476    #[must_use]
477    pub fn name(self) -> String {
478        match self {
479            Self::Named(name, _) => name.to_owned(),
480            Self::Layer(index, _) => format!("layers[{index}]"),
481            Self::Port(index, _) => format!("inputs[{index}]"),
482            Self::EffectInput(name, _) => format!("inputs.{name}"),
483        }
484    }
485}
486
487impl NodeOp {
488    /// Every input of the operation, connected or not, in a fixed order.
489    #[must_use]
490    pub fn inputs(&self) -> Vec<NodeInput<'_>> {
491        match self {
492            Self::Clip {}
493            | Self::Media { .. }
494            | Self::Generator { .. }
495            | Self::GroupInput { .. } => Vec::new(),
496            Self::Group { inputs, .. } => inputs
497                .iter()
498                .enumerate()
499                .map(|(index, input)| NodeInput::Port(index, input.as_ref()))
500                .collect(),
501            Self::Effect { input, inputs, .. } => {
502                std::iter::once(NodeInput::Named("input", input.as_ref()))
503                    .chain(
504                        inputs
505                            .iter()
506                            .map(|(name, node)| NodeInput::EffectInput(name, node.as_ref())),
507                    )
508                    .collect()
509            }
510            Self::Transform { input, .. } | Self::ShapeMask { input, .. } => {
511                vec![NodeInput::Named("input", input.as_ref())]
512            }
513            Self::Transition { from, to, .. } => vec![
514                NodeInput::Named("from", from.as_ref()),
515                NodeInput::Named("to", to.as_ref()),
516            ],
517            Self::Matte { input, matte, .. } => vec![
518                NodeInput::Named("input", input.as_ref()),
519                NodeInput::Named("matte", matte.as_ref()),
520            ],
521            Self::Merge { layers } => layers
522                .iter()
523                .enumerate()
524                .map(|(index, layer)| NodeInput::Layer(index, layer.input.as_ref()))
525                .collect(),
526        }
527    }
528
529    /// The input a bypassed node passes through: `input` for one-input
530    /// operations and mattes, `from` for a transition, the bottom layer of
531    /// a merge, and a group's first input. `None` for an operation without
532    /// inputs, which cannot be bypassed, and for a merge without layers.
533    #[must_use]
534    pub fn bypass_input(&self) -> Option<NodeInput<'_>> {
535        match self {
536            Self::Clip {}
537            | Self::Media { .. }
538            | Self::Generator { .. }
539            | Self::GroupInput { .. } => None,
540            Self::Merge { layers } => layers
541                .first()
542                .map(|layer| NodeInput::Layer(0, layer.input.as_ref())),
543            _ => self.inputs().into_iter().next(),
544        }
545    }
546
547    /// Whether the operation takes inputs, and so can be bypassed.
548    #[must_use]
549    pub const fn has_inputs(&self) -> bool {
550        match self {
551            Self::Clip {}
552            | Self::Media { .. }
553            | Self::Generator { .. }
554            | Self::GroupInput { .. } => false,
555            Self::Group { inputs, .. } => !inputs.is_empty(),
556            _ => true,
557        }
558    }
559
560    /// The effect or transition invocation the operation runs, if any.
561    #[must_use]
562    pub const fn effect(&self) -> Option<&ClipEffect> {
563        match self {
564            Self::Effect { effect, .. } | Self::Transition { effect, .. } => Some(effect),
565            _ => None,
566        }
567    }
568}
569
570/// A clip's use of a [`Composition`], and how its time maps onto it.
571///
572/// Composition time is `offset + (t - clip start) * scale` for timeline time
573/// `t`, rounded down to a tick. Moving a clip keeps its offset, so its
574/// animation moves with it; trimming its start by `d` adds `d * scale` to the
575/// offset, so what remains of the animation stays where it was. Extending the
576/// start before composition time zero makes the offset negative: settings
577/// hold their first keyframe there, and media nodes show their first frame.
578#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
579#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
580#[serde(deny_unknown_fields)]
581pub struct ClipComposition {
582    /// The composition shown.
583    pub id: CompositionId,
584    /// Composition time at the clip's start.
585    #[serde(default)]
586    pub offset: MediaTime,
587    /// Composition time per timeline time.
588    #[serde(default)]
589    pub scale: TimeScale,
590}
591
592impl ClipComposition {
593    /// Shows `id` from its time zero at the timeline's rate.
594    #[must_use]
595    pub const fn new(id: CompositionId) -> Self {
596        Self {
597            id,
598            offset: MediaTime::ZERO,
599            scale: TimeScale::ONE,
600        }
601    }
602
603    /// The composition time at timeline time `time` of a clip starting at
604    /// `clip_start`, or `None` when it is not representable or the scale has
605    /// a zero denominator (no rate; validation refuses it).
606    #[must_use]
607    pub fn local_time(&self, clip_start: MediaTime, time: MediaTime) -> Option<MediaTime> {
608        let elapsed = i128::from(time.ticks()) - i128::from(clip_start.ticks());
609        let scaled = (elapsed * i128::from(self.scale.num))
610            .checked_div_euclid(i128::from(self.scale.den))?;
611        let local = i128::from(self.offset.ticks()).checked_add(scaled)?;
612        i64::try_from(local).ok().map(MediaTime)
613    }
614}
615
616/// A positive rational rate of one time against another.
617#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
618#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
619#[serde(deny_unknown_fields)]
620pub struct TimeScale {
621    /// Numerator; positive.
622    pub num: u32,
623    /// Denominator; positive.
624    pub den: u32,
625}
626
627impl TimeScale {
628    /// The same rate.
629    pub const ONE: Self = Self { num: 1, den: 1 };
630
631    /// Whether both terms are positive.
632    #[must_use]
633    pub const fn is_valid(self) -> bool {
634        self.num > 0 && self.den > 0
635    }
636}
637
638impl Default for TimeScale {
639    fn default() -> Self {
640        Self::ONE
641    }
642}
643
644/// Checks every composition's identity, node settings, and connections, and
645/// returns the nodes each draws once inlined.
646///
647/// `by_source` is `sources` by ID, which each media node is looked up in.
648pub(crate) fn validate_compositions<'a>(
649    compositions: &'a [Composition],
650    sources: &'a [crate::MediaSource],
651    by_source: &std::collections::HashMap<&SourceId, &crate::MediaSource>,
652    effect_resources: &std::collections::HashSet<&crate::EffectResourceId>,
653    canvas: (u32, u32),
654) -> Result<InlinedNodes<'a>, crate::ModelError> {
655    use crate::ModelError;
656    use std::collections::{HashMap, HashSet};
657
658    if compositions.len() > MAX_COMPOSITIONS {
659        return Err(ModelError::TooManyCompositions {
660            found: compositions.len(),
661            maximum: MAX_COMPOSITIONS,
662        });
663    }
664    let mut ids = HashSet::with_capacity(compositions.len());
665    for composition in compositions {
666        if !ids.insert(&composition.id) {
667            return Err(ModelError::DuplicateCompositionId(composition.id.clone()));
668        }
669    }
670    let by_id: HashMap<&CompositionId, &Composition> = compositions
671        .iter()
672        .map(|composition| (&composition.id, composition))
673        .collect();
674    if let Some(source) = sources.iter().find(|source| {
675        source
676            .composition
677            .as_ref()
678            .is_some_and(|id| !by_id.contains_key(id))
679    }) {
680        return Err(ModelError::InvalidCompositionSource {
681            source: source.id.clone(),
682        });
683    }
684    for composition in compositions {
685        if composition.nodes.len() > MAX_COMPOSITION_NODES {
686            return Err(ModelError::TooManyCompositionNodes {
687                composition: composition.id.clone(),
688                found: composition.nodes.len(),
689                maximum: MAX_COMPOSITION_NODES,
690            });
691        }
692        let mut positions = HashMap::with_capacity(composition.nodes.len());
693        for (position, node) in composition.nodes.iter().enumerate() {
694            if node.id.as_str().contains('/') {
695                return Err(ModelError::InvalidNodeId {
696                    composition: composition.id.clone(),
697                    node: node.id.clone(),
698                });
699            }
700            if positions.insert(&node.id, position).is_some() {
701                return Err(ModelError::DuplicateNodeId {
702                    composition: composition.id.clone(),
703                    node: node.id.clone(),
704                });
705            }
706        }
707        validate_ports(composition, &positions)?;
708        for node in &composition.nodes {
709            validate_node(
710                composition,
711                node,
712                by_source,
713                &by_id,
714                effect_resources,
715                canvas,
716            )?;
717            for input in node.op.inputs() {
718                if let Some(target) = input.node()
719                    && !positions.contains_key(target)
720                {
721                    return Err(ModelError::UnknownNodeInput {
722                        composition: composition.id.clone(),
723                        node: node.id.clone(),
724                        input: input.name(),
725                        target: target.clone(),
726                    });
727                }
728            }
729        }
730        if let Some(output) = &composition.output
731            && !positions.contains_key(output)
732        {
733            return Err(ModelError::UnknownCompositionOutput {
734                composition: composition.id.clone(),
735                node: output.clone(),
736            });
737        }
738        if let Some(node) = cycle_member(composition, &positions) {
739            return Err(ModelError::CompositionCycle {
740                composition: composition.id.clone(),
741                node: node.clone(),
742            });
743        }
744    }
745    if let Some(composition) = reference_cycle_member(compositions, &by_id, by_source) {
746        return Err(ModelError::CompositionUsesItself {
747            composition: composition.clone(),
748        });
749    }
750    validate_expansion(compositions, &by_id, sources)
751}
752
753/// The nodes each composition draws once inlined, as [`MAX_INLINED_NODES`]
754/// counts them.
755pub(crate) type InlinedNodes<'a> = std::collections::HashMap<&'a CompositionId, u64>;
756
757/// The compositions nest at most [`MAX_COMPOSITION_DEPTH`] deep and draw at
758/// most [`MAX_INLINED_NODES`] nodes; returns the nodes each draws. The
759/// references are acyclic, so one walk in dependency order counts each
760/// composition once; it is iterative so that a long chain cannot exhaust
761/// the stack.
762fn validate_expansion<'a>(
763    compositions: &'a [Composition],
764    by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
765    sources: &'a [crate::MediaSource],
766) -> Result<InlinedNodes<'a>, crate::ModelError> {
767    use std::collections::HashMap;
768    let composition_sources: HashMap<&SourceId, &CompositionId> = sources
769        .iter()
770        .filter_map(|source| Some((&source.id, source.composition.as_ref()?)))
771        .collect();
772    // The composition a node draws in its place, if any.
773    let draws = |node: &'a CompositionNode| -> Option<&'a CompositionId> {
774        match &node.op {
775            NodeOp::Group { composition, .. } => Some(composition),
776            NodeOp::Media { source_id, .. } => composition_sources.get(source_id).copied(),
777            _ => None,
778        }
779        .filter(|id| by_id.contains_key(id))
780    };
781    // Depth and inlined node count of each composition walked.
782    let mut walked: HashMap<&CompositionId, (usize, u64)> = HashMap::new();
783    let mut total = 0_u64;
784    for root in compositions {
785        let mut stack = vec![(root, false)];
786        while let Some((composition, expanded)) = stack.pop() {
787            if walked.contains_key(&composition.id) {
788                continue;
789            }
790            if !expanded {
791                stack.push((composition, true));
792                stack.extend(
793                    composition
794                        .nodes
795                        .iter()
796                        .filter_map(draws)
797                        .filter(|id| !walked.contains_key(id))
798                        .map(|id| (by_id[id], false)),
799                );
800                continue;
801            }
802            let mut depth = 1;
803            let mut nodes = 0_u64;
804            for node in &composition.nodes {
805                let (inner, count) = draws(node).map_or((0, 1), |id| walked[id]);
806                depth = depth.max(inner + 1);
807                nodes = nodes.saturating_add(count);
808            }
809            walked.insert(&composition.id, (depth, nodes));
810        }
811        let (depth, nodes) = walked[&root.id];
812        if depth > MAX_COMPOSITION_DEPTH {
813            return Err(crate::ModelError::CompositionNestedTooDeep {
814                composition: root.id.clone(),
815                maximum: MAX_COMPOSITION_DEPTH,
816            });
817        }
818        total = total.saturating_add(nodes);
819        if total > MAX_INLINED_NODES as u64 {
820            return Err(crate::ModelError::TooManyInlinedNodes {
821                maximum: MAX_INLINED_NODES,
822            });
823        }
824    }
825    Ok(walked
826        .into_iter()
827        .map(|(id, (_, nodes))| (id, nodes))
828        .collect())
829}
830
831/// A composition's group inputs are named, distinct, and few enough, and its
832/// controls name distinct settings of its effect and transition nodes.
833fn validate_ports(
834    composition: &Composition,
835    positions: &std::collections::HashMap<&NodeId, usize>,
836) -> Result<(), crate::ModelError> {
837    use std::collections::HashSet;
838    let invalid = || crate::ModelError::InvalidCompositionPorts {
839        composition: composition.id.clone(),
840    };
841    let mut names = HashSet::new();
842    if composition.inputs.len() > MAX_GROUP_INPUTS
843        || composition
844            .inputs
845            .iter()
846            .any(|name| name.is_empty() || !names.insert(name))
847    {
848        return Err(invalid());
849    }
850    let mut controls = HashSet::new();
851    let mut settings = HashSet::new();
852    if composition.controls.len() > MAX_COMPOSITION_CONTROLS {
853        return Err(invalid());
854    }
855    for control in &composition.controls {
856        let node = positions
857            .get(&control.node)
858            .map(|position| &composition.nodes[*position]);
859        if control.name.is_empty()
860            || control.param.is_empty()
861            || !controls.insert(&control.name)
862            || !settings.insert((&control.node, &control.param))
863            || node.is_none_or(|node| node.op.effect().is_none())
864        {
865            return Err(invalid());
866        }
867    }
868    Ok(())
869}
870
871/// A composition that uses itself, through group nodes or composition
872/// sources, however indirectly: the first, in document order, that a
873/// depth-first walk finds on its own path.
874fn reference_cycle_member<'a>(
875    compositions: &'a [Composition],
876    by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
877    sources: &std::collections::HashMap<&SourceId, &'a crate::MediaSource>,
878) -> Option<&'a CompositionId> {
879    #[derive(Clone, Copy, PartialEq)]
880    enum Mark {
881        New,
882        Open,
883        Done,
884    }
885    let uses = |composition: &'a Composition| -> Vec<&'a CompositionId> {
886        composition
887            .nodes
888            .iter()
889            .filter_map(|node| match &node.op {
890                NodeOp::Group { composition, .. } => Some(composition),
891                NodeOp::Media { source_id, .. } => sources
892                    .get(source_id)
893                    .and_then(|source| source.composition.as_ref()),
894                _ => None,
895            })
896            .filter(|id| by_id.contains_key(id))
897            .collect()
898    };
899    let mut marks: std::collections::HashMap<&CompositionId, Mark> = compositions
900        .iter()
901        .map(|composition| (&composition.id, Mark::New))
902        .collect();
903    for root in compositions {
904        if marks[&root.id] != Mark::New {
905            continue;
906        }
907        let mut stack = vec![(&root.id, uses(root), 0_usize)];
908        marks.insert(&root.id, Mark::Open);
909        while let Some((current, next, at)) = stack.last_mut() {
910            let Some(target) = next.get(*at).copied() else {
911                marks.insert(current, Mark::Done);
912                stack.pop();
913                continue;
914            };
915            *at += 1;
916            match marks[target] {
917                Mark::Open => return Some(target),
918                Mark::New => {
919                    marks.insert(target, Mark::Open);
920                    stack.push((target, uses(by_id[target]), 0));
921                }
922                Mark::Done => {}
923            }
924        }
925    }
926    None
927}
928
929/// A node's shape problem as the model reports it.
930fn shape_error(
931    composition: &Composition,
932    node: &CompositionNode,
933    issue: crate::ShapeIssue,
934) -> crate::ModelError {
935    crate::ModelError::InvalidShape {
936        owner: crate::ShapeOwner::Node {
937            composition: composition.id.clone(),
938            node: node.id.clone(),
939        },
940        at: issue.at,
941        problem: issue.problem,
942    }
943}
944
945/// A node's layout problem as the model reports it.
946fn layout_error(
947    composition: &Composition,
948    node: &CompositionNode,
949    issue: crate::LayoutIssue,
950) -> crate::ModelError {
951    crate::ModelError::InvalidLayout {
952        owner: crate::ShapeOwner::Node {
953            composition: composition.id.clone(),
954            node: node.id.clone(),
955        },
956        at: issue.at,
957        problem: issue.problem,
958    }
959}
960
961fn validate_node(
962    composition: &Composition,
963    node: &CompositionNode,
964    sources: &std::collections::HashMap<&SourceId, &crate::MediaSource>,
965    compositions: &std::collections::HashMap<&CompositionId, &Composition>,
966    effect_resources: &std::collections::HashSet<&crate::EffectResourceId>,
967    canvas: (u32, u32),
968) -> Result<(), crate::ModelError> {
969    use crate::ModelError;
970
971    let invalid = || ModelError::InvalidNode {
972        composition: composition.id.clone(),
973        node: node.id.clone(),
974    };
975    let unit = |animation: &Animated<f32>| {
976        crate::animation::valid_scalar_values(animation, |value| (0.0..=1.0).contains(&value))
977    };
978    let shape_error = |issue| shape_error(composition, node, issue);
979    if node.bypass && !node.op.has_inputs() {
980        return Err(invalid());
981    }
982    let valid = match &node.op {
983        NodeOp::Clip {} | NodeOp::Matte { .. } => true,
984        NodeOp::Media { source_id, start } => {
985            let visual = sources.get(source_id).is_some_and(|source| {
986                matches!(
987                    source.kind,
988                    crate::MediaKind::Video
989                        | crate::MediaKind::Image
990                        | crate::MediaKind::Generated
991                        | crate::MediaKind::Composition
992                )
993            });
994            if !visual {
995                return Err(ModelError::InvalidNodeSource {
996                    composition: composition.id.clone(),
997                    node: node.id.clone(),
998                    source: source_id.clone(),
999                });
1000            }
1001            start.ticks() >= 0
1002        }
1003        NodeOp::Generator { generator } => match generator.validate_on(canvas.0, canvas.1) {
1004            Ok(()) => true,
1005            Err(crate::GeneratorIssue::Shape(issue)) => return Err(shape_error(issue)),
1006            Err(crate::GeneratorIssue::Layout(issue)) => {
1007                return Err(layout_error(composition, node, issue));
1008            }
1009            Err(crate::GeneratorIssue::Invalid) => false,
1010        },
1011        NodeOp::Effect { effect, inputs, .. } => {
1012            effect.is_valid()
1013                && inputs.len() <= MAX_EFFECT_INPUTS
1014                && inputs
1015                    .keys()
1016                    .all(|name| crate::effect::valid_binding_name(name))
1017        }
1018        NodeOp::Transition {
1019            effect, progress, ..
1020        } => effect.is_valid() && unit(progress),
1021        NodeOp::Transform { transform, .. } => transform.validate(),
1022        NodeOp::Merge { layers } => {
1023            layers.len() <= MAX_MERGE_LAYERS && layers.iter().all(|layer| unit(&layer.opacity))
1024        }
1025        NodeOp::ShapeMask { masks, .. } => {
1026            crate::mask::validate_masks(masks).map_err(shape_error)?;
1027            masks.len() <= crate::MAX_CLIP_MASKS
1028        }
1029        NodeOp::Group {
1030            composition: target,
1031            inputs,
1032            controls,
1033        } => {
1034            let Some(target) = compositions.get(target) else {
1035                return Err(ModelError::UnknownGroupComposition {
1036                    composition: composition.id.clone(),
1037                    node: node.id.clone(),
1038                    target: target.clone(),
1039                });
1040            };
1041            inputs.len() == target.inputs.len()
1042                && controls.iter().all(|(name, value)| {
1043                    target.controls.iter().any(|control| &control.name == name)
1044                        && crate::effect::valid_param(value)
1045                })
1046        }
1047        NodeOp::GroupInput { index } => {
1048            usize::try_from(*index).is_ok_and(|index| index < composition.inputs.len())
1049        }
1050    };
1051    if !valid {
1052        return Err(invalid());
1053    }
1054    if let Some(effect) = node.op.effect()
1055        && let Some(resource) = effect
1056            .resources
1057            .values()
1058            .find(|resource| !effect_resources.contains(resource))
1059    {
1060        return Err(ModelError::UnknownNodeResource {
1061            composition: composition.id.clone(),
1062            node: node.id.clone(),
1063            resource: resource.clone(),
1064        });
1065    }
1066    Ok(())
1067}
1068
1069/// A node on a cycle, if the graph has one: the first node, in document
1070/// order, that a depth-first walk finds on its own path.
1071fn cycle_member<'a>(
1072    composition: &'a Composition,
1073    positions: &std::collections::HashMap<&'a NodeId, usize>,
1074) -> Option<&'a NodeId> {
1075    #[derive(Clone, Copy, PartialEq)]
1076    enum Mark {
1077        New,
1078        Open,
1079        Done,
1080    }
1081    let nodes = &composition.nodes;
1082    let mut marks = vec![Mark::New; nodes.len()];
1083    for root in 0..nodes.len() {
1084        if marks[root] != Mark::New {
1085            continue;
1086        }
1087        // Each entry is a node and how many of its inputs have been walked.
1088        let mut stack = vec![(root, 0_usize)];
1089        marks[root] = Mark::Open;
1090        while let Some((current, next)) = stack.last_mut() {
1091            let inputs = nodes[*current].op.inputs();
1092            let Some(input) = inputs.get(*next) else {
1093                marks[*current] = Mark::Done;
1094                stack.pop();
1095                continue;
1096            };
1097            *next += 1;
1098            let Some(target) = input.node().and_then(|id| positions.get(id)) else {
1099                continue;
1100            };
1101            match marks[*target] {
1102                Mark::Open => return Some(&nodes[*target].id),
1103                Mark::New => {
1104                    marks[*target] = Mark::Open;
1105                    stack.push((*target, 0));
1106                }
1107                Mark::Done => {}
1108            }
1109        }
1110    }
1111    None
1112}
1113
1114#[cfg(test)]
1115#[path = "composition_tests.rs"]
1116mod tests;