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 outlines are evaluated at the frame's
88 /// time with [`crate::evaluate_geometry`], their feathers with
89 /// [`crate::sample_scalar`].
90 pub masks: &'a [ShapeMask],
91 /// The clip whose picture sets where this one is visible.
92 pub matte: Option<&'a Matte>,
93 /// Whether this clip is another clip's matte, and so is drawn only into
94 /// that matte, never composited itself.
95 pub is_matte: bool,
96 /// What the engine draws for a generated source, which needs no frame
97 /// from the host. It is drawn at the clip's `source_time`.
98 pub generator: Option<&'a Generator>,
99 /// A shapes or layout generator's tree, its fixed parts evaluated once:
100 /// evaluate it at `source_time`.
101 pub plan: Option<&'a crate::GeneratorPlan>,
102 /// For a composition source, its composition at the clip's source time:
103 /// the clip's picture, which the host supplies no frame for.
104 pub source_composition: Option<crate::ActiveComposition<'a>>,
105}
106
107impl ActiveClip<'_> {
108 /// Whether the clip's own source frame is drawn: it is a host-supplied
109 /// source, and the clip shows it directly or its composition draws it.
110 #[must_use]
111 pub fn needs_source(&self) -> bool {
112 self.generator.is_none()
113 && self.source_composition.is_none()
114 && self
115 .composition
116 .is_none_or(|composition| composition.plan().uses_clip())
117 }
118}
119
120/// Visual properties evaluated at one timeline timestamp.
121#[derive(Copy, Clone, Debug, PartialEq)]
122pub struct EvaluatedVisual {
123 /// Normalized output translation.
124 pub position: Vec2,
125 /// Normalized source anchor.
126 pub anchor: Vec2,
127 /// Per-axis scale.
128 pub scale: Vec2,
129 /// Clockwise rotation in degrees.
130 pub rotation_degrees: f32,
131 /// Opacity from zero to one.
132 pub opacity: f32,
133}
134
135/// A source-frame request emitted for host prefetch and decode scheduling.
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct FrameNeed {
138 /// Clip consumer that owns the sequential decoder cursor.
139 ///
140 /// Multiple clips may read the same source at unrelated timestamps. Hosts
141 /// must not make those clips share one seek position.
142 pub clip_id: ClipId,
143 /// The composition step that reads the source, when a media step does
144 /// rather than the clip itself: its [`crate::Step::node`] path, after a
145 /// `/` for a step of the clip's composition source (a node ID has no `/`,
146 /// so no path of the clip's own composition starts with one). With the
147 /// clip it names the consumer: two steps reading one source at different
148 /// times each keep their own cursor instead of making one seek back and
149 /// forth.
150 pub reader: Option<alpha2_model::NodeId>,
151 /// Source to decode or retrieve.
152 pub source_id: SourceId,
153 /// Timestamp requested on the source timeline.
154 pub media_time: MediaTime,
155}
156
157/// A transition evaluated at one timeline timestamp.
158#[derive(Clone, Copy, Debug, PartialEq)]
159pub struct ActiveTransition<'a> {
160 /// Clip sampled at progress zero.
161 pub from_clip: &'a ClipId,
162 /// Clip sampled at progress one.
163 pub to_clip: &'a ClipId,
164 /// Versioned shader invocation, with parameters sampled at the frame's
165 /// time.
166 pub effect: ActiveEffect<'a>,
167 /// Eased progress in the inclusive unit interval.
168 pub progress: f32,
169}
170
171/// One effect invocation at one timestamp: timeline time for a timeline
172/// transition, composition time for a composition step.
173#[derive(Clone, Copy, Debug, PartialEq)]
174pub struct ActiveEffect<'a> {
175 effect: &'a ClipEffect,
176 time: MediaTime,
177}
178
179impl<'a> ActiveEffect<'a> {
180 /// The invocation `effect` sampled at `time`.
181 #[must_use]
182 pub const fn new(effect: &'a ClipEffect, time: MediaTime) -> Self {
183 Self { effect, time }
184 }
185
186 /// Stable effect definition identifier.
187 #[must_use]
188 pub fn id(&self) -> &'a str {
189 &self.effect.id
190 }
191
192 /// Exact effect definition version.
193 #[must_use]
194 pub const fn version(&self) -> u32 {
195 self.effect.version
196 }
197
198 /// Every authored parameter, sampled at this timestamp, by name.
199 /// Parameters the invocation leaves out take their definition defaults.
200 pub fn params(&self) -> impl Iterator<Item = (&'a str, EffectParamValue)> + 'a {
201 let time = self.time;
202 self.effect
203 .params
204 .iter()
205 .map(move |(name, animation)| (name.as_str(), sample_effect_param(animation, time)))
206 }
207
208 /// One parameter sampled at this timestamp, if the invocation sets it.
209 #[must_use]
210 pub fn param(&self, name: &str) -> Option<EffectParamValue> {
211 self.effect
212 .params
213 .get(name)
214 .map(|animation| sample_effect_param(animation, self.time))
215 }
216
217 /// Named external resources matched against the effect definition.
218 pub fn resources(&self) -> impl Iterator<Item = (&'a str, &'a EffectResourceId)> + 'a {
219 self.effect
220 .resources
221 .iter()
222 .map(|(name, id)| (name.as_str(), id))
223 }
224
225 /// The authored invocation, with its parameters unevaluated.
226 #[must_use]
227 pub const fn authored(&self) -> &'a ClipEffect {
228 self.effect
229 }
230}