Skip to main content

alpha2_eval/
frame.rs

1use crate::{EffectParamValue, sample_effect_param};
2use alpha2_media::SourceId;
3use alpha2_model::{
4    BlendSpace, ClipBlendMode, ClipEffect, ClipId, EffectResourceId, Generator, Matte, Rgb,
5    ShapeMask, TextStyle, TrackId, Vec2,
6};
7use alpha2_time::MediaTime;
8
9/// A timestamp-specific composition description with no GPU handles.
10///
11/// Identities and authored effects are borrowed from the [`crate::ActiveIndex`]
12/// that produced the description, so evaluating a frame copies none of them.
13/// `'a` is the index's borrow; clone what must outlive it.
14#[derive(Clone, Debug, PartialEq)]
15pub struct FrameDesc<'a> {
16    /// Timestamp evaluated on the project timeline.
17    pub time: MediaTime,
18    /// Opaque color under every visual layer.
19    pub background: Rgb,
20    /// The space in which blend modes are evaluated.
21    pub blend_space: BlendSpace,
22    /// Active visual clips in composition order.
23    pub visual: Vec<ActiveClip<'a>>,
24    /// Active audio clips in mix order.
25    pub audio: Vec<ActiveClip<'a>>,
26    /// Active two-input transitions in track order.
27    pub transitions: Vec<ActiveTransition<'a>>,
28    /// The cue each caption track shows, in track order.
29    pub captions: Vec<ActiveCaption<'a>>,
30}
31
32impl FrameDesc<'_> {
33    /// Whether the frame draws `clip`'s picture anywhere: as itself, or,
34    /// for another clip's matte ([`ActiveClip::is_matte`]), into an active
35    /// clip that names it. A matte no active clip names is drawn nowhere, so
36    /// nothing of it is needed.
37    #[must_use]
38    pub fn draws(&self, clip: &ActiveClip<'_>) -> bool {
39        !clip.is_matte
40            || self.visual.iter().any(|other| {
41                other
42                    .matte
43                    .is_some_and(|matte| &matte.source == clip.clip_id)
44            })
45    }
46}
47
48/// The cue one caption track shows at a timestamp.
49#[derive(Clone, Copy, Debug, PartialEq)]
50pub struct ActiveCaption<'a> {
51    /// The caption track.
52    pub track_id: &'a TrackId,
53    /// The cue's text.
54    pub text: &'a str,
55    /// The track's style.
56    pub style: &'a TextStyle,
57    /// How many of the frame's [`FrameDesc::visual`] clips lie under the
58    /// caption, by track order: those on tracks before the caption's,
59    /// whether or not the frame draws them ([`FrameDesc::draws`]). It is
60    /// composited after the drawn clips among the first `layers_below` of
61    /// `visual` and before every drawn clip after them, so a clip that is
62    /// not drawn never moves a caption past a track.
63    pub layers_below: usize,
64}
65
66/// A visible or audible clip mapped to its source timestamp.
67#[derive(Clone, Copy, Debug, PartialEq)]
68pub struct ActiveClip<'a> {
69    /// Active clip identity.
70    pub clip_id: &'a ClipId,
71    /// Media source needed for this clip.
72    pub source_id: &'a SourceId,
73    /// Track containing the clip.
74    pub track_id: &'a TrackId,
75    /// Source timestamp the clip's time map selects (forward, reversed,
76    /// frozen, or looped).
77    pub source_time: MediaTime,
78    /// Evaluated visual presentation, present only for visual tracks.
79    pub visual: Option<EvaluatedVisual>,
80    /// The composition that builds the clip's picture, at the clip's
81    /// composition time; `None` shows the source as it is.
82    pub composition: Option<crate::ActiveComposition<'a>>,
83    /// Artistic blend operation used by visual composition.
84    pub blend_mode: ClipBlendMode,
85    /// Whether a runtime may keep and reuse the clip's rendered picture.
86    pub cache: alpha2_model::CacheIntent,
87    /// Shape masks, in order; their animated values are sampled at the
88    /// frame's time with [`crate::sample_scalar`] and [`crate::sample_vec2`].
89    pub masks: &'a [ShapeMask],
90    /// The clip whose picture sets where this one is visible.
91    pub matte: Option<&'a Matte>,
92    /// Whether this clip is another clip's matte, and so is drawn only into
93    /// that matte, never composited itself.
94    pub is_matte: bool,
95    /// What the engine draws for a generated source, which needs no frame
96    /// from the host.
97    pub generator: Option<&'a Generator>,
98    /// For a composition source, its composition at the clip's source time:
99    /// the clip's picture, which the host supplies no frame for.
100    pub source_composition: Option<crate::ActiveComposition<'a>>,
101}
102
103impl ActiveClip<'_> {
104    /// Whether the clip's own source frame is drawn: it is a host-supplied
105    /// source, and the clip shows it directly or its composition draws it.
106    #[must_use]
107    pub fn needs_source(&self) -> bool {
108        self.generator.is_none()
109            && self.source_composition.is_none()
110            && self
111                .composition
112                .is_none_or(|composition| composition.plan().uses_clip())
113    }
114}
115
116/// Visual properties evaluated at one timeline timestamp.
117#[derive(Copy, Clone, Debug, PartialEq)]
118pub struct EvaluatedVisual {
119    /// Normalized output translation.
120    pub position: Vec2,
121    /// Normalized source anchor.
122    pub anchor: Vec2,
123    /// Per-axis scale.
124    pub scale: Vec2,
125    /// Clockwise rotation in degrees.
126    pub rotation_degrees: f32,
127    /// Opacity from zero to one.
128    pub opacity: f32,
129}
130
131/// A source-frame request emitted for host prefetch and decode scheduling.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct FrameNeed {
134    /// Clip consumer that owns the sequential decoder cursor.
135    ///
136    /// Multiple clips may read the same source at unrelated timestamps. Hosts
137    /// must not make those clips share one seek position.
138    pub clip_id: ClipId,
139    /// The composition step that reads the source, when a media step does
140    /// rather than the clip itself: its [`crate::Step::node`] path, after a
141    /// `/` for a step of the clip's composition source (a node ID has no `/`,
142    /// so no path of the clip's own composition starts with one). With the
143    /// clip it names the consumer: two steps reading one source at different
144    /// times each keep their own cursor instead of making one seek back and
145    /// forth.
146    pub reader: Option<alpha2_model::NodeId>,
147    /// Source to decode or retrieve.
148    pub source_id: SourceId,
149    /// Timestamp requested on the source timeline.
150    pub media_time: MediaTime,
151}
152
153/// A transition evaluated at one timeline timestamp.
154#[derive(Clone, Copy, Debug, PartialEq)]
155pub struct ActiveTransition<'a> {
156    /// Clip sampled at progress zero.
157    pub from_clip: &'a ClipId,
158    /// Clip sampled at progress one.
159    pub to_clip: &'a ClipId,
160    /// Versioned shader invocation, with parameters sampled at the frame's
161    /// time.
162    pub effect: ActiveEffect<'a>,
163    /// Eased progress in the inclusive unit interval.
164    pub progress: f32,
165}
166
167/// One effect invocation at one timestamp: timeline time for a timeline
168/// transition, composition time for a composition step.
169#[derive(Clone, Copy, Debug, PartialEq)]
170pub struct ActiveEffect<'a> {
171    effect: &'a ClipEffect,
172    time: MediaTime,
173}
174
175impl<'a> ActiveEffect<'a> {
176    /// The invocation `effect` sampled at `time`.
177    #[must_use]
178    pub const fn new(effect: &'a ClipEffect, time: MediaTime) -> Self {
179        Self { effect, time }
180    }
181
182    /// Stable effect definition identifier.
183    #[must_use]
184    pub fn id(&self) -> &'a str {
185        &self.effect.id
186    }
187
188    /// Exact effect definition version.
189    #[must_use]
190    pub const fn version(&self) -> u32 {
191        self.effect.version
192    }
193
194    /// Every authored parameter, sampled at this timestamp, by name.
195    /// Parameters the invocation leaves out take their definition defaults.
196    pub fn params(&self) -> impl Iterator<Item = (&'a str, EffectParamValue)> + 'a {
197        let time = self.time;
198        self.effect
199            .params
200            .iter()
201            .map(move |(name, animation)| (name.as_str(), sample_effect_param(animation, time)))
202    }
203
204    /// One parameter sampled at this timestamp, if the invocation sets it.
205    #[must_use]
206    pub fn param(&self, name: &str) -> Option<EffectParamValue> {
207        self.effect
208            .params
209            .get(name)
210            .map(|animation| sample_effect_param(animation, self.time))
211    }
212
213    /// Named external resources matched against the effect definition.
214    pub fn resources(&self) -> impl Iterator<Item = (&'a str, &'a EffectResourceId)> + 'a {
215        self.effect
216            .resources
217            .iter()
218            .map(|(name, id)| (name.as_str(), id))
219    }
220
221    /// The authored invocation, with its parameters unevaluated.
222    #[must_use]
223    pub const fn authored(&self) -> &'a ClipEffect {
224        self.effect
225    }
226}