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    canvas: (u32, u32),
657) -> Result<InlinedNodes<'a>, crate::ModelError> {
658    use crate::ModelError;
659    use std::collections::{HashMap, HashSet};
660
661    if compositions.len() > MAX_COMPOSITIONS {
662        return Err(ModelError::TooManyCompositions {
663            found: compositions.len(),
664            maximum: MAX_COMPOSITIONS,
665        });
666    }
667    let mut ids = HashSet::with_capacity(compositions.len());
668    for composition in compositions {
669        if !ids.insert(&composition.id) {
670            return Err(ModelError::DuplicateCompositionId(composition.id.clone()));
671        }
672    }
673    let by_id: HashMap<&CompositionId, &Composition> = compositions
674        .iter()
675        .map(|composition| (&composition.id, composition))
676        .collect();
677    if let Some(source) = sources.iter().find(|source| {
678        source
679            .composition
680            .as_ref()
681            .is_some_and(|id| !by_id.contains_key(id))
682    }) {
683        return Err(ModelError::InvalidCompositionSource {
684            source: source.id.clone(),
685        });
686    }
687    for composition in compositions {
688        if composition.nodes.len() > MAX_COMPOSITION_NODES {
689            return Err(ModelError::TooManyCompositionNodes {
690                composition: composition.id.clone(),
691                found: composition.nodes.len(),
692                maximum: MAX_COMPOSITION_NODES,
693            });
694        }
695        let mut positions = HashMap::with_capacity(composition.nodes.len());
696        for (position, node) in composition.nodes.iter().enumerate() {
697            if node.id.as_str().contains('/') {
698                return Err(ModelError::InvalidNodeId {
699                    composition: composition.id.clone(),
700                    node: node.id.clone(),
701                });
702            }
703            if positions.insert(&node.id, position).is_some() {
704                return Err(ModelError::DuplicateNodeId {
705                    composition: composition.id.clone(),
706                    node: node.id.clone(),
707                });
708            }
709        }
710        validate_ports(composition, &positions)?;
711        for node in &composition.nodes {
712            validate_node(
713                composition,
714                node,
715                by_source,
716                &by_id,
717                effect_resources,
718                canvas,
719            )?;
720            for input in node.op.inputs() {
721                if let Some(target) = input.node()
722                    && !positions.contains_key(target)
723                {
724                    return Err(ModelError::UnknownNodeInput {
725                        composition: composition.id.clone(),
726                        node: node.id.clone(),
727                        input: input.name(),
728                        target: target.clone(),
729                    });
730                }
731            }
732        }
733        if let Some(output) = &composition.output
734            && !positions.contains_key(output)
735        {
736            return Err(ModelError::UnknownCompositionOutput {
737                composition: composition.id.clone(),
738                node: output.clone(),
739            });
740        }
741        if let Some(node) = cycle_member(composition, &positions) {
742            return Err(ModelError::CompositionCycle {
743                composition: composition.id.clone(),
744                node: node.clone(),
745            });
746        }
747    }
748    if let Some(composition) = reference_cycle_member(compositions, &by_id, by_source) {
749        return Err(ModelError::CompositionUsesItself {
750            composition: composition.clone(),
751        });
752    }
753    validate_expansion(compositions, &by_id, sources)
754}
755
756/// The nodes each composition draws once inlined, as [`MAX_INLINED_NODES`]
757/// counts them.
758pub(crate) type InlinedNodes<'a> = std::collections::HashMap<&'a CompositionId, u64>;
759
760/// The compositions nest at most [`MAX_COMPOSITION_DEPTH`] deep and draw at
761/// most [`MAX_INLINED_NODES`] nodes; returns the nodes each draws. The
762/// references are acyclic, so one walk in dependency order counts each
763/// composition once; it is iterative so that a long chain cannot exhaust
764/// the stack.
765fn validate_expansion<'a>(
766    compositions: &'a [Composition],
767    by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
768    sources: &'a [crate::MediaSource],
769) -> Result<InlinedNodes<'a>, crate::ModelError> {
770    use std::collections::HashMap;
771    let composition_sources: HashMap<&SourceId, &CompositionId> = sources
772        .iter()
773        .filter_map(|source| Some((&source.id, source.composition.as_ref()?)))
774        .collect();
775    // The composition a node draws in its place, if any.
776    let draws = |node: &'a CompositionNode| -> Option<&'a CompositionId> {
777        match &node.op {
778            NodeOp::Group { composition, .. } => Some(composition),
779            NodeOp::Media { source_id, .. } => composition_sources.get(source_id).copied(),
780            _ => None,
781        }
782        .filter(|id| by_id.contains_key(id))
783    };
784    // Depth and inlined node count of each composition walked.
785    let mut walked: HashMap<&CompositionId, (usize, u64)> = HashMap::new();
786    let mut total = 0_u64;
787    for root in compositions {
788        let mut stack = vec![(root, false)];
789        while let Some((composition, expanded)) = stack.pop() {
790            if walked.contains_key(&composition.id) {
791                continue;
792            }
793            if !expanded {
794                stack.push((composition, true));
795                stack.extend(
796                    composition
797                        .nodes
798                        .iter()
799                        .filter_map(draws)
800                        .filter(|id| !walked.contains_key(id))
801                        .map(|id| (by_id[id], false)),
802                );
803                continue;
804            }
805            let mut depth = 1;
806            let mut nodes = 0_u64;
807            for node in &composition.nodes {
808                let (inner, count) = draws(node).map_or((0, 1), |id| walked[id]);
809                depth = depth.max(inner + 1);
810                nodes = nodes.saturating_add(count);
811            }
812            walked.insert(&composition.id, (depth, nodes));
813        }
814        let (depth, nodes) = walked[&root.id];
815        if depth > MAX_COMPOSITION_DEPTH {
816            return Err(crate::ModelError::CompositionNestedTooDeep {
817                composition: root.id.clone(),
818                maximum: MAX_COMPOSITION_DEPTH,
819            });
820        }
821        total = total.saturating_add(nodes);
822        if total > MAX_INLINED_NODES as u64 {
823            return Err(crate::ModelError::TooManyInlinedNodes {
824                maximum: MAX_INLINED_NODES,
825            });
826        }
827    }
828    Ok(walked
829        .into_iter()
830        .map(|(id, (_, nodes))| (id, nodes))
831        .collect())
832}
833
834/// A composition's group inputs are named, distinct, and few enough, and its
835/// controls name distinct settings of its effect and transition nodes.
836fn validate_ports(
837    composition: &Composition,
838    positions: &std::collections::HashMap<&NodeId, usize>,
839) -> Result<(), crate::ModelError> {
840    use std::collections::HashSet;
841    let invalid = || crate::ModelError::InvalidCompositionPorts {
842        composition: composition.id.clone(),
843    };
844    let mut names = HashSet::new();
845    if composition.inputs.len() > MAX_GROUP_INPUTS
846        || composition
847            .inputs
848            .iter()
849            .any(|name| name.is_empty() || !names.insert(name))
850    {
851        return Err(invalid());
852    }
853    let mut controls = HashSet::new();
854    let mut settings = HashSet::new();
855    if composition.controls.len() > MAX_COMPOSITION_CONTROLS {
856        return Err(invalid());
857    }
858    for control in &composition.controls {
859        let node = positions
860            .get(&control.node)
861            .map(|position| &composition.nodes[*position]);
862        if control.name.is_empty()
863            || control.param.is_empty()
864            || !controls.insert(&control.name)
865            || !settings.insert((&control.node, &control.param))
866            || node.is_none_or(|node| node.op.effect().is_none())
867        {
868            return Err(invalid());
869        }
870    }
871    Ok(())
872}
873
874/// A composition that uses itself, through group nodes or composition
875/// sources, however indirectly: the first, in document order, that a
876/// depth-first walk finds on its own path.
877fn reference_cycle_member<'a>(
878    compositions: &'a [Composition],
879    by_id: &std::collections::HashMap<&'a CompositionId, &'a Composition>,
880    sources: &std::collections::HashMap<&SourceId, &'a crate::MediaSource>,
881) -> Option<&'a CompositionId> {
882    #[derive(Clone, Copy, PartialEq)]
883    enum Mark {
884        New,
885        Open,
886        Done,
887    }
888    let uses = |composition: &'a Composition| -> Vec<&'a CompositionId> {
889        composition
890            .nodes
891            .iter()
892            .filter_map(|node| match &node.op {
893                NodeOp::Group { composition, .. } => Some(composition),
894                NodeOp::Media { source_id, .. } => sources
895                    .get(source_id)
896                    .and_then(|source| source.composition.as_ref()),
897                _ => None,
898            })
899            .filter(|id| by_id.contains_key(id))
900            .collect()
901    };
902    let mut marks: std::collections::HashMap<&CompositionId, Mark> = compositions
903        .iter()
904        .map(|composition| (&composition.id, Mark::New))
905        .collect();
906    for root in compositions {
907        if marks[&root.id] != Mark::New {
908            continue;
909        }
910        let mut stack = vec![(&root.id, uses(root), 0_usize)];
911        marks.insert(&root.id, Mark::Open);
912        while let Some((current, next, at)) = stack.last_mut() {
913            let Some(target) = next.get(*at).copied() else {
914                marks.insert(current, Mark::Done);
915                stack.pop();
916                continue;
917            };
918            *at += 1;
919            match marks[target] {
920                Mark::Open => return Some(target),
921                Mark::New => {
922                    marks.insert(target, Mark::Open);
923                    stack.push((target, uses(by_id[target]), 0));
924                }
925                Mark::Done => {}
926            }
927        }
928    }
929    None
930}
931
932/// A node's shape problem as the model reports it.
933fn shape_error(
934    composition: &Composition,
935    node: &CompositionNode,
936    issue: crate::ShapeIssue,
937) -> crate::ModelError {
938    crate::ModelError::InvalidShape {
939        owner: crate::ShapeOwner::Node {
940            composition: composition.id.clone(),
941            node: node.id.clone(),
942        },
943        at: issue.at,
944        problem: issue.problem,
945    }
946}
947
948/// A node's layout problem as the model reports it.
949fn layout_error(
950    composition: &Composition,
951    node: &CompositionNode,
952    issue: crate::LayoutIssue,
953) -> crate::ModelError {
954    crate::ModelError::InvalidLayout {
955        owner: crate::ShapeOwner::Node {
956            composition: composition.id.clone(),
957            node: node.id.clone(),
958        },
959        at: issue.at,
960        problem: issue.problem,
961    }
962}
963
964fn validate_node(
965    composition: &Composition,
966    node: &CompositionNode,
967    sources: &std::collections::HashMap<&SourceId, &crate::MediaSource>,
968    compositions: &std::collections::HashMap<&CompositionId, &Composition>,
969    effect_resources: &std::collections::HashSet<&crate::EffectResourceId>,
970    canvas: (u32, u32),
971) -> Result<(), crate::ModelError> {
972    use crate::ModelError;
973
974    let invalid = || ModelError::InvalidNode {
975        composition: composition.id.clone(),
976        node: node.id.clone(),
977    };
978    let unit = |animation: &Animated<f32>| {
979        crate::animation::valid_scalar_values(animation, |value| (0.0..=1.0).contains(&value))
980    };
981    let shape_error = |issue| shape_error(composition, node, issue);
982    if node.bypass && !node.op.has_inputs() {
983        return Err(invalid());
984    }
985    let valid = match &node.op {
986        NodeOp::Clip {} | NodeOp::Matte { .. } => true,
987        NodeOp::Media { source_id, start } => {
988            let visual = sources.get(source_id).is_some_and(|source| {
989                matches!(
990                    source.kind,
991                    crate::MediaKind::Video
992                        | crate::MediaKind::Image
993                        | crate::MediaKind::Generated
994                        | crate::MediaKind::Composition
995                )
996            });
997            if !visual {
998                return Err(ModelError::InvalidNodeSource {
999                    composition: composition.id.clone(),
1000                    node: node.id.clone(),
1001                    source: source_id.clone(),
1002                });
1003            }
1004            start.ticks() >= 0
1005        }
1006        NodeOp::Generator { generator } => match generator.validate_on(canvas.0, canvas.1) {
1007            Ok(()) => true,
1008            Err(crate::GeneratorIssue::Shape(issue)) => return Err(shape_error(issue)),
1009            Err(crate::GeneratorIssue::Layout(issue)) => {
1010                return Err(layout_error(composition, node, issue));
1011            }
1012            Err(crate::GeneratorIssue::Invalid) => false,
1013        },
1014        NodeOp::Effect { effect, inputs, .. } => {
1015            effect.is_valid()
1016                && inputs.len() <= MAX_EFFECT_INPUTS
1017                && inputs
1018                    .keys()
1019                    .all(|name| crate::effect::valid_binding_name(name))
1020        }
1021        NodeOp::Transition {
1022            effect, progress, ..
1023        } => effect.is_valid() && unit(progress),
1024        NodeOp::Transform { transform, .. } => transform.validate(),
1025        NodeOp::Merge { layers } => {
1026            layers.len() <= MAX_MERGE_LAYERS && layers.iter().all(|layer| unit(&layer.opacity))
1027        }
1028        NodeOp::ShapeMask { masks, .. } => {
1029            crate::mask::validate_masks(masks).map_err(shape_error)?;
1030            masks.len() <= crate::MAX_CLIP_MASKS
1031        }
1032        NodeOp::Group {
1033            composition: target,
1034            inputs,
1035            controls,
1036        } => {
1037            let Some(target) = compositions.get(target) else {
1038                return Err(ModelError::UnknownGroupComposition {
1039                    composition: composition.id.clone(),
1040                    node: node.id.clone(),
1041                    target: target.clone(),
1042                });
1043            };
1044            inputs.len() == target.inputs.len()
1045                && controls.iter().all(|(name, value)| {
1046                    target.controls.iter().any(|control| &control.name == name)
1047                        && crate::effect::valid_param(value)
1048                })
1049        }
1050        NodeOp::GroupInput { index } => {
1051            usize::try_from(*index).is_ok_and(|index| index < composition.inputs.len())
1052        }
1053    };
1054    if !valid {
1055        return Err(invalid());
1056    }
1057    if let Some(effect) = node.op.effect()
1058        && let Some(resource) = effect
1059            .resources
1060            .values()
1061            .find(|resource| !effect_resources.contains(resource))
1062    {
1063        return Err(ModelError::UnknownNodeResource {
1064            composition: composition.id.clone(),
1065            node: node.id.clone(),
1066            resource: resource.clone(),
1067        });
1068    }
1069    Ok(())
1070}
1071
1072/// A node on a cycle, if the graph has one: the first node, in document
1073/// order, that a depth-first walk finds on its own path.
1074fn cycle_member<'a>(
1075    composition: &'a Composition,
1076    positions: &std::collections::HashMap<&'a NodeId, usize>,
1077) -> Option<&'a NodeId> {
1078    #[derive(Clone, Copy, PartialEq)]
1079    enum Mark {
1080        New,
1081        Open,
1082        Done,
1083    }
1084    let nodes = &composition.nodes;
1085    let mut marks = vec![Mark::New; nodes.len()];
1086    for root in 0..nodes.len() {
1087        if marks[root] != Mark::New {
1088            continue;
1089        }
1090        // Each entry is a node and how many of its inputs have been walked.
1091        let mut stack = vec![(root, 0_usize)];
1092        marks[root] = Mark::Open;
1093        while let Some((current, next)) = stack.last_mut() {
1094            let inputs = nodes[*current].op.inputs();
1095            let Some(input) = inputs.get(*next) else {
1096                marks[*current] = Mark::Done;
1097                stack.pop();
1098                continue;
1099            };
1100            *next += 1;
1101            let Some(target) = input.node().and_then(|id| positions.get(id)) else {
1102                continue;
1103            };
1104            match marks[*target] {
1105                Mark::Open => return Some(&nodes[*target].id),
1106                Mark::New => {
1107                    marks[*target] = Mark::Open;
1108                    stack.push((*target, 0));
1109                }
1110                Mark::Done => {}
1111            }
1112        }
1113    }
1114    None
1115}
1116
1117#[cfg(test)]
1118#[path = "composition_tests.rs"]
1119mod tests;