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