Skip to main content

alpha2_eval/
composition.rs

1//! Composition graphs resolved into the steps that draw their output.
2//!
3//! A composition is compiled once, when the index is built: the nodes its
4//! output reaches are put in dependency order, bypassed nodes are replaced by
5//! the input they pass through, and every input is resolved to the step that
6//! produces it. A group node is replaced by the steps of the composition it
7//! draws, its group inputs resolved to the group node's inputs and its
8//! controls set on the nodes they expose; a media node that reads a
9//! composition source is replaced by that composition's steps, shifted in
10//! time by the node's start. The compiled steps are what the flat graph
11//! would be: a group draws exactly what its nodes would inline. Evaluating a
12//! frame then only samples settings at the clip's composition time, plus
13//! each step's offset.
14//!
15//! Inlining copies: each step holds its own node's settings and path, so a
16//! composition drawn many times holds its settings many times. Validation
17//! bounds the steps ([`MAX_INLINED_NODES`]), and the planner bounds what
18//! they hold: the plans of one project hold at most [`MAX_PLAN_BYTES`].
19
20use alpha2_media::SourceId;
21use alpha2_model::{
22    Animated, ClipBlendMode, ClipEffect, Composition, CompositionId, CompositionReads, EffectValue,
23    Generator, Keyframe, MAX_COMPOSITION_DEPTH, MAX_INLINED_NODES, MaskShape, MatteMode, MediaKind,
24    MediaSource, NodeId, NodeOp, Paint, ShapeMask, VisualProperties,
25};
26use alpha2_time::MediaTime;
27use std::collections::HashMap;
28use std::sync::Arc;
29
30/// Most bytes the compiled composition plans of one project hold together,
31/// as [`Step`]s estimate them: each step, its node path, and the heap its
32/// settings use (keyframes, parameter and resource names, masks, layers,
33/// gradient stops). A project's plans are those of every composition a clip
34/// or composition source shows; past this, none of them is drawn
35/// ([`PlanError::TooLarge`]). The steps are what validation counts, at most
36/// [`MAX_INLINED_NODES`]; this bounds how large each may be, which inlining
37/// multiplies.
38pub const MAX_PLAN_BYTES: u64 = 256 * 1024 * 1024;
39
40/// A composition's output, drawn by steps in dependency order.
41#[derive(Clone, Debug, PartialEq)]
42pub struct CompositionPlan {
43    /// The composition.
44    pub id: CompositionId,
45    /// Steps in dependency order; each step's inputs are earlier steps.
46    pub steps: Vec<Step>,
47    /// The step whose image is the composition's picture.
48    pub output: usize,
49}
50
51impl CompositionPlan {
52    /// The effect and transition invocations the steps run, in step order.
53    pub fn effects(&self) -> impl Iterator<Item = &ClipEffect> {
54        self.steps.iter().filter_map(|step| match &step.op {
55            StepOp::Effect { effect, .. } | StepOp::Transition { effect, .. } => Some(effect),
56            _ => None,
57        })
58    }
59
60    /// Whether a step draws the picture of the clip showing the plan.
61    #[must_use]
62    pub fn uses_clip(&self) -> bool {
63        self.steps
64            .iter()
65            .any(|step| matches!(step.op, StepOp::Clip))
66    }
67}
68
69/// One node of a composition, with its inputs resolved to earlier steps.
70#[derive(Clone, Debug, PartialEq)]
71pub struct Step {
72    /// The node this step draws: its id, after the ids of the group and
73    /// media nodes it was inlined through, joined by `/`.
74    pub node: NodeId,
75    /// What it draws.
76    pub op: StepOp,
77    /// Added to the plan's time to sample this step: the starts of the media
78    /// nodes whose composition sources it was inlined through.
79    pub offset: MediaTime,
80}
81
82impl Step {
83    /// The time this step's settings are sampled at, when the plan is drawn
84    /// at `time`.
85    #[must_use]
86    pub fn time(&self, time: MediaTime) -> MediaTime {
87        MediaTime(time.ticks().saturating_add(self.offset.ticks()))
88    }
89}
90
91/// The operation of a [`Step`]. Inputs are indices of earlier steps.
92#[derive(Clone, Debug, PartialEq)]
93pub enum StepOp {
94    /// The picture of the clip showing the composition.
95    Clip,
96    /// A host-supplied source at `start` plus the composition time.
97    Media {
98        /// The source.
99        source_id: SourceId,
100        /// Source time at composition time zero.
101        start: MediaTime,
102    },
103    /// A picture the engine draws: a generator node, or a media node naming
104    /// a generated source.
105    Generator(Generator),
106    /// A registered one-input effect.
107    Effect {
108        /// The processed image.
109        input: usize,
110        /// The effect.
111        effect: ClipEffect,
112    },
113    /// A registered transition at an explicit progress.
114    Transition {
115        /// The image at progress zero.
116        from: usize,
117        /// The image at progress one.
118        to: usize,
119        /// The transition.
120        effect: ClipEffect,
121        /// Progress in `0..=1`.
122        progress: Animated<f32>,
123    },
124    /// The input placed by a transform.
125    Transform {
126        /// The placed image.
127        input: usize,
128        /// The transform.
129        transform: VisualProperties,
130    },
131    /// Layers composited over transparency, bottom first.
132    Merge {
133        /// The layers.
134        layers: Vec<StepLayer>,
135    },
136    /// The input kept where a second image says.
137    Matte {
138        /// The kept image.
139        input: usize,
140        /// The coverage image.
141        matte: usize,
142        /// Which property of the coverage image counts.
143        mode: MatteMode,
144    },
145    /// The input kept inside shapes.
146    ShapeMask {
147        /// The masked image.
148        input: usize,
149        /// The shapes.
150        masks: Vec<ShapeMask>,
151    },
152}
153
154/// One layer of a [`StepOp::Merge`].
155#[derive(Clone, Debug, PartialEq)]
156pub struct StepLayer {
157    /// The layer's image.
158    pub input: usize,
159    /// Its blend mode.
160    pub blend_mode: ClipBlendMode,
161    /// Its opacity.
162    pub opacity: Animated<f32>,
163}
164
165/// Why a composition's output cannot be drawn.
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub enum PlanError {
168    /// The composition selects no output.
169    NoOutput,
170    /// A node the output needs has an unconnected input.
171    MissingInput {
172        /// The node.
173        node: NodeId,
174        /// The input: `input`, `from`, `to`, `matte`, `layers[i]`, or
175        /// `inputs[i]`.
176        input: String,
177    },
178    /// A group input read where no group node feeds it: in a composition
179    /// drawn as a clip's composition or as a source.
180    UnboundGroupInput {
181        /// The node.
182        node: NodeId,
183    },
184    /// A clip node in a composition drawn as a source, which shows no clip.
185    ClipWithoutClip {
186        /// The node.
187        node: NodeId,
188    },
189    /// The group or media node draws compositions nested deeper than
190    /// [`alpha2_model::MAX_COMPOSITION_DEPTH`]. Validation refuses such a
191    /// project, so only an unvalidated one reaches this.
192    NestedTooDeep {
193        /// The node.
194        node: NodeId,
195    },
196    /// The output needs more than [`alpha2_model::MAX_INLINED_NODES`] steps,
197    /// which validation refuses, or the plans of the project hold more than
198    /// [`MAX_PLAN_BYTES`].
199    TooLarge,
200}
201
202/// What compiled plans may still hold, in bytes: one for a plan compiled on
203/// its own, shared by the plans of one project.
204#[derive(Debug)]
205pub(crate) struct PlanBudget {
206    left: u64,
207    exceeded: bool,
208}
209
210impl PlanBudget {
211    pub(crate) const fn new(bytes: u64) -> Self {
212        Self {
213            left: bytes,
214            exceeded: false,
215        }
216    }
217
218    /// Whether a plan needed more than was left.
219    pub(crate) const fn exceeded(&self) -> bool {
220        self.exceeded
221    }
222}
223
224/// What planning looks up: a project's compositions and sources by ID, and
225/// what each composition reads. Built once for all the plans of a project.
226pub(crate) struct Lookup<'a> {
227    compositions: HashMap<&'a CompositionId, &'a Composition>,
228    sources: HashMap<&'a SourceId, &'a MediaSource>,
229    reads: CompositionReads<'a>,
230}
231
232impl<'a> Lookup<'a> {
233    pub(crate) fn new(compositions: &'a [Composition], sources: &'a [MediaSource]) -> Self {
234        let compositions = compositions
235            .iter()
236            .map(|composition| (&composition.id, composition))
237            .collect();
238        let reads = CompositionReads::new(&compositions);
239        Self {
240            compositions,
241            sources: sources.iter().map(|source| (&source.id, source)).collect(),
242            reads,
243        }
244    }
245
246    /// What each composition reads.
247    pub(crate) const fn reads(&self) -> &CompositionReads<'a> {
248        &self.reads
249    }
250}
251
252/// Compiles `composition`'s output into steps, as a clip's composition: its
253/// clip nodes draw the clip's picture. `compositions` resolves group nodes
254/// and `sources` media nodes, which may name generated or composition
255/// sources.
256///
257/// The project must be valid ([`alpha2_model::Project::validate`]): every
258/// input names a node, there is no cycle, and no composition uses itself.
259///
260/// The plan holds at most [`MAX_PLAN_BYTES`]; an index holds every plan of
261/// a project within that together.
262///
263/// # Errors
264/// Returns [`PlanError`] when there is no output, the output needs an input
265/// that is not connected or a group input no group node feeds, or the plan
266/// nests too deep or grows too large.
267pub fn plan_composition(
268    composition: &Composition,
269    compositions: &[Composition],
270    sources: &[MediaSource],
271) -> Result<CompositionPlan, PlanError> {
272    plan(
273        composition,
274        &Lookup::new(compositions, sources),
275        true,
276        &mut PlanBudget::new(MAX_PLAN_BYTES),
277    )
278}
279
280/// [`plan_composition`] with what `lookup` holds, within what `budget` has
281/// left, taking what the plan holds from it.
282pub(crate) fn plan_composition_within(
283    composition: &Composition,
284    lookup: &Lookup<'_>,
285    budget: &mut PlanBudget,
286) -> Result<CompositionPlan, PlanError> {
287    plan(composition, lookup, true, budget)
288}
289
290/// Compiles `composition` as a composition source draws it: like
291/// [`plan_composition`], but a clip node or group input is an error, since
292/// no clip or group node feeds it.
293///
294/// # Errors
295/// Returns [`PlanError`] as [`plan_composition`] does, and for a clip node
296/// or group input the output needs.
297pub fn plan_source_composition(
298    composition: &Composition,
299    compositions: &[Composition],
300    sources: &[MediaSource],
301) -> Result<CompositionPlan, PlanError> {
302    plan(
303        composition,
304        &Lookup::new(compositions, sources),
305        false,
306        &mut PlanBudget::new(MAX_PLAN_BYTES),
307    )
308}
309
310/// [`plan_source_composition`] with what `lookup` holds, within what
311/// `budget` has left, taking what the plan holds from it.
312pub(crate) fn plan_source_composition_within(
313    composition: &Composition,
314    lookup: &Lookup<'_>,
315    budget: &mut PlanBudget,
316) -> Result<CompositionPlan, PlanError> {
317    plan(composition, lookup, false, budget)
318}
319
320fn plan<'a>(
321    composition: &'a Composition,
322    lookup: &Lookup<'a>,
323    clip_allowed: bool,
324    budget: &mut PlanBudget,
325) -> Result<CompositionPlan, PlanError> {
326    let mut planner = Planner {
327        lookup,
328        steps: Vec::new(),
329        budget,
330    };
331    let scope = Scope {
332        composition,
333        inputs: Vec::new(),
334        controls: HashMap::new(),
335        offset: MediaTime::ZERO,
336        prefix: String::new(),
337        clip_allowed,
338        group_inputs_bound: false,
339        depth: 1,
340    };
341    let output = planner.output(&scope)?;
342    Ok(CompositionPlan {
343        id: composition.id.clone(),
344        steps: planner.steps,
345        output,
346    })
347}
348
349/// One composition as it is being drawn: inline in another, or on its own.
350struct Scope<'a> {
351    composition: &'a Composition,
352    /// The step each group input reads: none for one its composition does
353    /// not read.
354    inputs: Vec<Option<usize>>,
355    /// Values its group node sets, by node and parameter.
356    controls: HashMap<(&'a NodeId, &'a str), &'a Animated<EffectValue>>,
357    /// Added to the plan's time for its steps.
358    offset: MediaTime,
359    /// Prepended to its node ids in the steps.
360    prefix: String,
361    /// Whether a clip node draws the clip's picture here.
362    clip_allowed: bool,
363    /// Whether a group node feeds its group inputs.
364    group_inputs_bound: bool,
365    /// How deeply it is nested: one for the plan's own composition.
366    depth: usize,
367}
368
369struct Planner<'a, 'b> {
370    lookup: &'b Lookup<'a>,
371    steps: Vec<Step>,
372    budget: &'b mut PlanBudget,
373}
374
375impl<'a> Planner<'a, '_> {
376    /// The step drawing `scope`'s output.
377    fn output(&mut self, scope: &Scope<'a>) -> Result<usize, PlanError> {
378        let output = scope
379            .composition
380            .output
381            .as_ref()
382            .ok_or(PlanError::NoOutput)?;
383        let nodes: HashMap<&NodeId, &'a alpha2_model::CompositionNode> = scope
384            .composition
385            .nodes
386            .iter()
387            .map(|node| (&node.id, node))
388            .collect();
389        self.resolve(scope, &nodes, output)
390    }
391
392    /// The step drawing node `id` of `scope`, planning it and what it needs
393    /// first.
394    ///
395    /// Validation has ruled out cycles, so the walk ends; it is iterative so
396    /// that a long chain cannot exhaust the stack. Nesting recurses once per
397    /// group or composition source, at most
398    /// [`alpha2_model::MAX_COMPOSITION_DEPTH`] deep, and the steps number at
399    /// most [`alpha2_model::MAX_INLINED_NODES`]: validation bounds both, and
400    /// the planner refuses to go further for a project that skipped it.
401    fn resolve(
402        &mut self,
403        scope: &Scope<'a>,
404        nodes: &HashMap<&NodeId, &'a alpha2_model::CompositionNode>,
405        id: &'a NodeId,
406    ) -> Result<usize, PlanError> {
407        let mut done: HashMap<&NodeId, usize> = HashMap::new();
408        // Each entry is a node whose needed inputs are pushed before it is revisited.
409        let mut pending = vec![(id, false)];
410        while let Some((current, expanded)) = pending.pop() {
411            if done.contains_key(current) {
412                continue;
413            }
414            let node = nodes[current];
415            let needed = needed_inputs(self.lookup.reads(), &scope.prefix, node)?;
416            if !expanded {
417                pending.push((current, true));
418                pending.extend(
419                    needed
420                        .iter()
421                        .rev()
422                        .filter(|input| !done.contains_key(**input))
423                        .map(|input| (*input, false)),
424                );
425                continue;
426            }
427            let step = if node.bypass {
428                done[needed[0]]
429            } else {
430                self.node_step(scope, node, &done)?
431            };
432            done.insert(current, step);
433        }
434        Ok(done[id])
435    }
436
437    /// The step (or inlined steps) drawing `node`, its inputs planned.
438    fn node_step(
439        &mut self,
440        scope: &Scope<'a>,
441        node: &'a alpha2_model::CompositionNode,
442        done: &HashMap<&NodeId, usize>,
443    ) -> Result<usize, PlanError> {
444        let at = |input: &Option<NodeId>| {
445            input
446                .as_ref()
447                .map(|id| done[id])
448                .expect("needed inputs are connected and planned")
449        };
450        let id = || scoped(&scope.prefix, &node.id);
451        let op = match &node.op {
452            NodeOp::Clip {} => {
453                if !scope.clip_allowed {
454                    return Err(PlanError::ClipWithoutClip { node: id() });
455                }
456                StepOp::Clip
457            }
458            NodeOp::GroupInput { index } => {
459                // Validation keeps the index within the composition's ports.
460                return usize::try_from(*index)
461                    .ok()
462                    .and_then(|index| scope.inputs.get(index).copied().flatten())
463                    .filter(|_| scope.group_inputs_bound)
464                    .ok_or_else(|| PlanError::UnboundGroupInput { node: id() });
465            }
466            NodeOp::Group {
467                composition,
468                inputs,
469                controls,
470            } => {
471                // Only the ports its composition reads are needed, and planned.
472                let reads = self.lookup.reads();
473                let inputs = inputs
474                    .iter()
475                    .enumerate()
476                    .map(|(port, input)| reads.reads_port(composition, port).then(|| at(input)))
477                    .collect();
478                return self.group(scope, &id(), composition, inputs, controls);
479            }
480            NodeOp::Media { source_id, start } => {
481                let source = self.lookup.sources.get(source_id).copied();
482                if let Some(composition) = source.and_then(|source| source.composition.as_ref()) {
483                    return self.composition_source(scope, &id(), composition, *start);
484                }
485                let generator = source
486                    .filter(|source| source.kind == MediaKind::Generated)
487                    .and_then(|source| source.generator.clone());
488                generator.map_or_else(
489                    || StepOp::Media {
490                        source_id: source_id.clone(),
491                        start: *start,
492                    },
493                    StepOp::Generator,
494                )
495            }
496            NodeOp::Generator { generator } => StepOp::Generator(generator.clone()),
497            NodeOp::Effect { input, effect } => StepOp::Effect {
498                input: at(input),
499                effect: controlled(scope, &node.id, effect),
500            },
501            NodeOp::Transition {
502                from,
503                to,
504                effect,
505                progress,
506            } => StepOp::Transition {
507                from: at(from),
508                to: at(to),
509                effect: controlled(scope, &node.id, effect),
510                progress: progress.clone(),
511            },
512            NodeOp::Transform { input, transform } => StepOp::Transform {
513                input: at(input),
514                transform: transform.clone(),
515            },
516            NodeOp::Merge { layers } => StepOp::Merge {
517                layers: layers
518                    .iter()
519                    .map(|layer| StepLayer {
520                        input: at(&layer.input),
521                        blend_mode: layer.blend_mode,
522                        opacity: layer.opacity.clone(),
523                    })
524                    .collect(),
525            },
526            NodeOp::Matte { input, matte, mode } => StepOp::Matte {
527                input: at(input),
528                matte: at(matte),
529                mode: *mode,
530            },
531            NodeOp::ShapeMask { input, masks } => StepOp::ShapeMask {
532                input: at(input),
533                masks: masks.clone(),
534            },
535        };
536        self.push(Step {
537            node: id(),
538            op,
539            offset: scope.offset,
540        })
541    }
542
543    /// Adds `step` to the plan, within [`MAX_INLINED_NODES`] steps and what
544    /// the budget has left.
545    fn push(&mut self, step: Step) -> Result<usize, PlanError> {
546        if self.steps.len() >= MAX_INLINED_NODES {
547            return Err(PlanError::TooLarge);
548        }
549        let bytes = step_bytes(&step);
550        if bytes > self.budget.left {
551            self.budget.exceeded = true;
552            return Err(PlanError::TooLarge);
553        }
554        self.budget.left -= bytes;
555        self.steps.push(step);
556        Ok(self.steps.len() - 1)
557    }
558
559    /// The steps of the composition a group node draws: its group inputs
560    /// read `inputs`, and the controls it sets change its nodes' settings.
561    fn group(
562        &mut self,
563        scope: &Scope<'a>,
564        node: &NodeId,
565        composition: &CompositionId,
566        inputs: Vec<Option<usize>>,
567        controls: &'a std::collections::BTreeMap<String, Animated<EffectValue>>,
568    ) -> Result<usize, PlanError> {
569        let target = self.composition(composition);
570        let depth = nested(scope, node)?;
571        let controls = target
572            .controls
573            .iter()
574            .filter_map(|control| {
575                controls
576                    .get(&control.name)
577                    .map(|value| ((&control.node, control.param.as_str()), value))
578            })
579            .collect();
580        self.output(&Scope {
581            composition: target,
582            inputs,
583            controls,
584            offset: scope.offset,
585            prefix: format!("{node}/"),
586            clip_allowed: scope.clip_allowed,
587            group_inputs_bound: true,
588            depth,
589        })
590    }
591
592    /// The steps of a composition a media node reads through a composition
593    /// source, `start` later in time.
594    fn composition_source(
595        &mut self,
596        scope: &Scope<'a>,
597        node: &NodeId,
598        composition: &CompositionId,
599        start: MediaTime,
600    ) -> Result<usize, PlanError> {
601        let depth = nested(scope, node)?;
602        self.output(&Scope {
603            composition: self.composition(composition),
604            inputs: Vec::new(),
605            controls: HashMap::new(),
606            offset: MediaTime(scope.offset.ticks().saturating_add(start.ticks())),
607            prefix: format!("{node}/"),
608            clip_allowed: false,
609            group_inputs_bound: false,
610            depth,
611        })
612    }
613
614    fn composition(&self, id: &CompositionId) -> &'a Composition {
615        self.lookup
616            .compositions
617            .get(id)
618            .copied()
619            .expect("validation guarantees referenced compositions exist")
620    }
621}
622
623/// The depth of a composition `node` draws inside `scope`, if within
624/// [`MAX_COMPOSITION_DEPTH`].
625fn nested(scope: &Scope<'_>, node: &NodeId) -> Result<usize, PlanError> {
626    let depth = scope.depth + 1;
627    if depth > MAX_COMPOSITION_DEPTH {
628        return Err(PlanError::NestedTooDeep { node: node.clone() });
629    }
630    Ok(depth)
631}
632
633/// `id` after `prefix`.
634fn scoped(prefix: &str, id: &NodeId) -> NodeId {
635    if prefix.is_empty() {
636        return id.clone();
637    }
638    NodeId::new(format!("{prefix}{id}")).expect("a prefixed id is not empty")
639}
640
641/// `effect` with the parameters its group node sets.
642fn controlled(scope: &Scope<'_>, node: &NodeId, effect: &ClipEffect) -> ClipEffect {
643    let mut effect = effect.clone();
644    for ((target, param), value) in &scope.controls {
645        if *target == node {
646            effect.params.insert((*param).to_owned(), (*value).clone());
647        }
648    }
649    effect
650}
651
652/// What `step` holds, as [`MAX_PLAN_BYTES`] counts it.
653fn step_bytes(step: &Step) -> u64 {
654    let heap = match &step.op {
655        StepOp::Clip | StepOp::Matte { .. } => 0,
656        StepOp::Media { source_id, .. } => source_id.as_str().len(),
657        StepOp::Generator(generator) => generator_bytes(generator),
658        StepOp::Effect { effect, .. } => effect_bytes(effect),
659        StepOp::Transition {
660            effect, progress, ..
661        } => effect_bytes(effect) + keyframe_bytes(progress),
662        StepOp::Transform { transform, .. } => transform_bytes(transform),
663        StepOp::Merge { layers } => layers
664            .iter()
665            .map(|layer| size_of::<StepLayer>() + keyframe_bytes(&layer.opacity))
666            .sum(),
667        StepOp::ShapeMask { masks, .. } => masks.iter().map(mask_bytes).sum(),
668    };
669    u64::try_from(size_of::<Step>() + step.node.as_str().len() + heap).unwrap_or(u64::MAX)
670}
671
672/// The heap an animated value's keyframes take; none for a constant.
673fn keyframe_bytes<T>(value: &Animated<T>) -> usize {
674    match value {
675        Animated::Constant { .. } => 0,
676        Animated::Keyframes { keyframes } => keyframes.len() * size_of::<Keyframe<T>>(),
677    }
678}
679
680fn effect_bytes(effect: &ClipEffect) -> usize {
681    effect.id.len()
682        + effect
683            .params
684            .iter()
685            .map(|(name, value)| {
686                name.len() + size_of::<Animated<EffectValue>>() + keyframe_bytes(value)
687            })
688            .sum::<usize>()
689        + effect
690            .resources
691            .iter()
692            .map(|(name, id)| name.len() + id.as_str().len())
693            .sum::<usize>()
694}
695
696fn transform_bytes(transform: &VisualProperties) -> usize {
697    keyframe_bytes(&transform.position)
698        + keyframe_bytes(&transform.scale)
699        + keyframe_bytes(&transform.rotation_degrees)
700        + keyframe_bytes(&transform.opacity)
701}
702
703fn mask_bytes(mask: &ShapeMask) -> usize {
704    let corner = match &mask.shape {
705        MaskShape::Rectangle { corner_radius } => keyframe_bytes(corner_radius),
706        MaskShape::Ellipse => 0,
707    };
708    size_of::<ShapeMask>()
709        + keyframe_bytes(&mask.center)
710        + keyframe_bytes(&mask.size)
711        + keyframe_bytes(&mask.rotation_degrees)
712        + keyframe_bytes(&mask.feather)
713        + corner
714}
715
716fn generator_bytes(generator: &Generator) -> usize {
717    let paint = |paint: &Paint| match paint {
718        Paint::Solid { .. } => 0,
719        Paint::LinearGradient { stops, .. }
720        | Paint::RadialGradient { stops, .. }
721        | Paint::ConicGradient { stops, .. } => {
722            stops.len() * size_of::<alpha2_model::GradientStop>()
723        }
724    };
725    match generator {
726        Generator::Fill { paint: fill } => paint(fill),
727        Generator::Shape { fill, stroke, .. } => {
728            fill.as_ref().map_or(0, paint)
729                + stroke.as_ref().map_or(0, |stroke| paint(&stroke.paint))
730        }
731        Generator::Text { text, style } => {
732            text.len() + style.font_family.as_ref().map_or(0, String::len)
733        }
734    }
735}
736
737/// The nodes a node needs, in input order: its
738/// [`CompositionReads::needed_inputs`], each connected. A missing one is
739/// named as the steps name the node: after `prefix`, the group or media node
740/// drawing its composition, like every plan error.
741fn needed_inputs<'a>(
742    reads: &CompositionReads<'_>,
743    prefix: &str,
744    node: &'a alpha2_model::CompositionNode,
745) -> Result<Vec<&'a NodeId>, PlanError> {
746    let needed = reads.needed_inputs(node);
747    if node.bypass && needed.is_empty() {
748        // A merge without layers has nothing to pass through.
749        return Err(PlanError::MissingInput {
750            node: scoped(prefix, &node.id),
751            input: "layers[0]".into(),
752        });
753    }
754    needed
755        .into_iter()
756        .map(|input| {
757            input.node().ok_or_else(|| PlanError::MissingInput {
758                node: scoped(prefix, &node.id),
759                input: input.name(),
760            })
761        })
762        .collect()
763}
764
765/// A clip's compiled composition and how its time maps onto it.
766#[derive(Clone, Debug)]
767pub(crate) struct IndexedComposition {
768    pub(crate) plan: Arc<Result<CompositionPlan, PlanError>>,
769    pub(crate) reference: alpha2_model::ClipComposition,
770}
771
772/// A clip's composition at one timeline timestamp.
773#[derive(Clone, Copy, Debug, PartialEq)]
774pub struct ActiveComposition<'a> {
775    plan: &'a CompositionPlan,
776    time: MediaTime,
777}
778
779impl<'a> ActiveComposition<'a> {
780    /// `plan` at composition time `time`.
781    #[must_use]
782    pub const fn new(plan: &'a CompositionPlan, time: MediaTime) -> Self {
783        Self { plan, time }
784    }
785
786    /// The compiled composition.
787    #[must_use]
788    pub const fn plan(&self) -> &'a CompositionPlan {
789        self.plan
790    }
791
792    /// The composition time at which settings are sampled.
793    #[must_use]
794    pub const fn time(&self) -> MediaTime {
795        self.time
796    }
797
798    /// Each host-supplied source a step reads, with the source time it
799    /// reads, by step index.
800    pub fn media(&self) -> impl Iterator<Item = (usize, &'a SourceId, MediaTime)> + 'a {
801        let time = self.time;
802        self.plan
803            .steps
804            .iter()
805            .enumerate()
806            .filter_map(move |(index, step)| match &step.op {
807                StepOp::Media { source_id, start } => Some((
808                    index,
809                    source_id,
810                    MediaTime(start.ticks().saturating_add(step.time(time).ticks())),
811                )),
812                _ => None,
813            })
814    }
815}
816
817#[cfg(test)]
818#[path = "composition_tests.rs"]
819mod tests;