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