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