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