Skip to main content

alpha2_engine/
frame.rs

1//! Rendering of evaluated timeline frames.
2//!
3//! The renderer knows render graphs, not timelines. This module is where an
4//! evaluated [`FrameDesc`] becomes one: it checks that every clip's source
5//! frame has arrived, compiles clips, effect chains, and transitions into a
6//! [`RenderGraph`], and hands that graph to the renderer's submission paths.
7
8use alpha2_eval::{
9    ActiveClip, ActiveComposition, ActiveEffect, ActiveTransition, EvaluatedVisual, FrameDesc,
10    GeneratorPlan, ShapesPlan, StepOp, sample_scalar, sample_visual,
11};
12use alpha2_media::{SourceId, YuvEncoding};
13use alpha2_model::{
14    ClipBlendMode, EffectValue, Generator, MaskMode, MatteMode, Rgb, ShapeMask, TextStyle,
15};
16use alpha2_render::{
17    BlendMode, CacheIntent, Color, CpuImage, CpuRenderer, EffectCatalog, EffectId, EffectInputs,
18    EffectParams, EffectResources, EffectRuntime, FrameSubmitTimings, GENERATOR_EFFECT_ID,
19    GraphError, GraphLayer, LayerTransform, MASK_COMBINE_EFFECT_IDS, MASK_COMBINE_NAMES,
20    MAX_SHAPE_MASKS, Nv12PlaneTargets, ParamSpec, ParamValue, PendingSourceWork, RenderCacheKey,
21    RenderControls, RenderError, RenderGraph, RenderNodeId, RenderNodeKind, RenderSize,
22    RenderedTexture, Renderer, SourceView, TRACK_MATTE_EFFECT_ID, Yuv420pReadback, Yuv420pTargets,
23};
24use alpha2_time::MediaTime;
25
26use crate::TextTexture;
27use crate::layout::draw::LaidOut;
28use std::collections::{HashMap, HashSet};
29use std::sync::Arc;
30use std::time::Duration;
31use web_time::Instant;
32
33/// Supplies decoded source frames as GPU textures to the renderer.
34///
35/// Implementations own decode caches and the lifetime of their textures, and
36/// lend them as [`SourceView`]s. The pure model and evaluator crates never
37/// observe this platform-specific type. Textures must store premultiplied,
38/// sRGB-encoded RGBA in a non-sRGB GPU format, as returned by
39/// `Renderer::upload_rgba8` or `Renderer::render_yuv420p`.
40pub trait FrameProvider {
41    /// Returns a texture for `source` at the requested source timestamp.
42    ///
43    /// The texture is the frame presented at `time`: the one whose
44    /// presentation interval contains it, the last frame whose timestamp is
45    /// at or before `time` plus the stream's presentation slack, as
46    /// [`alpha2_media::Decoder::seek`] resolves a time. Before the stream's
47    /// first frame that frame is shown; past the last, the last. A host that
48    /// looks frames up by the requested time stores each under every time it
49    /// was asked for.
50    ///
51    /// `None` means the frame has not arrived yet; real-time runners may hold
52    /// their previous output while deterministic exporters wait and retry.
53    fn texture_for(&self, source: &SourceId, time: MediaTime) -> Option<SourceView<'_>>;
54
55    /// A key for the texture [`Self::texture_for`] returns: the host's
56    /// promise that the texture's pixels depend on nothing the key leaves
57    /// out: the source's content, the frame, and how it was decoded,
58    /// converted, and presented. Pictures drawn from keyed sources can be
59    /// kept and reused (see [`alpha2_render::Renderer::set_result_cache_budget`]);
60    /// the default, no key, keeps nothing drawn from the source.
61    fn source_key(&self, _source: &SourceId, _time: MediaTime) -> Option<RenderCacheKey> {
62        None
63    }
64
65    /// Returns presentation work that has not yet been submitted for this
66    /// physical source texture. The default covers already-ready textures.
67    fn pending_source_work(
68        &self,
69        _source: &SourceId,
70        _time: MediaTime,
71    ) -> Option<&PendingSourceWork> {
72        None
73    }
74
75    /// Returns the text [`crate::text_key`] names, a caption or a text
76    /// generator prepared by the host's [`crate::TextTextures`]. The default
77    /// holds none: a frame that shows text then fails with
78    /// [`RenderError::MissingText`].
79    fn text(&self, _key: u64) -> Option<TextTexture<'_>> {
80        None
81    }
82
83    /// Returns the layout [`crate::layout_key`] names, resolved and drawn
84    /// by the host's [`crate::TextTextures`], whose texts [`Self::text`]
85    /// lends. The default holds none: a frame that shows a layout then fails
86    /// with [`RenderError::MissingText`].
87    fn layout(&self, _key: u128) -> Option<std::sync::Arc<crate::PreparedLayout>> {
88        None
89    }
90}
91
92/// Outcome of one render attempt when sources may arrive asynchronously.
93#[derive(Clone, Debug, Default, PartialEq, Eq)]
94pub struct RenderReport {
95    /// Sources whose requested frame was unavailable.
96    pub missing_sources: Vec<SourceId>,
97}
98
99impl RenderReport {
100    /// Returns whether every requested source frame was available.
101    #[must_use]
102    pub fn is_complete(&self) -> bool {
103        self.missing_sources.is_empty()
104    }
105}
106
107/// Result of attempting to render one evaluated timeline frame.
108#[derive(Debug)]
109pub struct FrameRender<I = RenderedTexture> {
110    /// Availability information collected before anything is drawn.
111    pub report: RenderReport,
112    /// Rendered output, absent when one or more source frames are unavailable.
113    pub texture: Option<I>,
114}
115
116impl<I> FrameRender<I> {
117    /// Returns whether a texture was rendered from a complete source set.
118    #[must_use]
119    pub fn is_complete(&self) -> bool {
120        self.texture.is_some()
121    }
122}
123
124/// Result of rendering and scheduling one frame for planar YUV420 readback.
125#[derive(Debug)]
126pub struct FrameYuv420pRender {
127    /// Availability information collected before any GPU work is submitted.
128    pub report: RenderReport,
129    /// Submitted readback, absent when source frames are unavailable.
130    pub readback: Option<Yuv420pReadback>,
131}
132
133impl FrameYuv420pRender {
134    /// Returns whether a complete frame was submitted for readback.
135    #[must_use]
136    pub fn is_complete(&self) -> bool {
137        self.readback.is_some()
138    }
139}
140
141/// Result of rendering one frame into caller-owned NV12 plane textures.
142#[derive(Debug)]
143#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
144pub struct FrameNv12Render {
145    /// Availability information collected before any GPU work is submitted.
146    pub report: RenderReport,
147    /// Whether the pack was submitted; false when a source was missing.
148    pub submitted: bool,
149}
150
151/// One evaluated frame compiled into a render graph, not yet submitted.
152#[derive(Debug)]
153pub struct FrameGraph<'a> {
154    /// Availability information collected before the graph was built.
155    pub report: RenderReport,
156    /// The frame's graph, absent when source frames are unavailable.
157    pub graph: Option<RenderGraph<'a>>,
158    /// Presentation work behind the graph's source textures that has not
159    /// been submitted yet, for the submission to fold in.
160    pub pending: Vec<&'a PendingSourceWork>,
161}
162
163/// Timeline-frame rendering on whichever renderer draws the frames: the GPU
164/// [`Renderer`] or the [`alpha2_render::CpuRenderer`].
165///
166/// Implemented for both; import it to call these methods on one. The GPU's
167/// readback and hardware-encoder paths are [`GpuFrameRenderer`].
168pub trait FrameRenderer: EffectCatalog + RenderControls {
169    /// The picture a frame renders to: a [`RenderedTexture`] on the GPU, a
170    /// [`alpha2_render::CpuImage`] on the CPU.
171    type Image;
172
173    /// Resolves the textures required by an evaluated frame.
174    ///
175    /// This performs no fallback rendering: it is the common availability pass
176    /// used by a real-time player (which may reuse an older image) and a
177    /// deterministic exporter (which waits and retries).
178    #[must_use]
179    fn inspect_frame(&self, frame: &FrameDesc, frames: &dyn FrameProvider) -> RenderReport {
180        available_textures(frame, frames).0
181    }
182
183    /// Compiles one evaluator result into the render graph every render
184    /// method below submits, without drawing anything.
185    ///
186    /// The graph borrows the frame's effect and parameter names and the
187    /// provider's textures; an incomplete source set returns its report and
188    /// no graph.
189    ///
190    /// # Errors
191    /// Returns [`RenderError`] when an effect is unknown or a visual or
192    /// caption is invalid.
193    fn build_frame_graph<'a>(
194        &self,
195        size: RenderSize,
196        frame: &'a FrameDesc<'a>,
197        frames: &'a dyn FrameProvider,
198    ) -> Result<FrameGraph<'a>, RenderError>;
199
200    /// Draws a graph [`Self::build_frame_graph`] built, with the source work
201    /// behind it.
202    ///
203    /// # Errors
204    /// Returns [`RenderError`] when a pass fails.
205    fn render_frame_graph(
206        &self,
207        graph: &RenderGraph<'_>,
208        pending: &[&PendingSourceWork],
209    ) -> Result<Self::Image, RenderError>;
210
211    /// Renders one pure evaluator result using sources supplied by the host.
212    ///
213    /// Source availability is checked before anything is drawn. An
214    /// incomplete attempt returns its missing-source report with no partially
215    /// rendered picture, allowing real-time and deterministic runners to
216    /// apply their own hold or wait policy.
217    ///
218    /// # Errors
219    /// Returns [`RenderError`] when a visual or render target is invalid.
220    fn render_frame(
221        &self,
222        size: RenderSize,
223        frame: &FrameDesc,
224        frames: &dyn FrameProvider,
225    ) -> Result<FrameRender<Self::Image>, RenderError> {
226        let FrameGraph {
227            report,
228            graph,
229            pending,
230        } = self.build_frame_graph(size, frame, frames)?;
231        let texture = match graph {
232            Some(graph) => Some(self.render_frame_graph(&graph, &pending)?),
233            None => None,
234        };
235        Ok(FrameRender { report, texture })
236    }
237
238    /// Renders one evaluator result and converts it to planar YUV420 in
239    /// `targets`, caller-owned plane memory such as an encoder's frame.
240    ///
241    /// An incomplete source set writes nothing and returns its report. The
242    /// output size must be even for chroma subsampling.
243    ///
244    /// # Errors
245    /// Returns [`RenderError`] for an invalid graph or a target too small.
246    fn render_frame_into_yuv420p(
247        &self,
248        size: RenderSize,
249        frame: &FrameDesc,
250        frames: &dyn FrameProvider,
251        encoding: YuvEncoding,
252        targets: &mut Yuv420pTargets<'_>,
253    ) -> Result<RenderReport, RenderError>;
254}
255
256/// The GPU renderer's frame paths that end on the GPU: a YUV420 readback
257/// still to be mapped, and hardware-encoder NV12 planes.
258pub trait GpuFrameRenderer: FrameRenderer<Image = RenderedTexture> {
259    /// Renders one evaluator result, converts it to YUV420, and submits both
260    /// workloads in one command buffer.
261    ///
262    /// An incomplete source set returns without encoding or submitting work.
263    /// The output size must be even for chroma subsampling.
264    ///
265    /// # Errors
266    /// Returns [`RenderError`] for invalid graph, target, or YUV layout data.
267    fn render_frame_yuv420p(
268        &self,
269        size: RenderSize,
270        frame: &FrameDesc,
271        frames: &dyn FrameProvider,
272        encoding: YuvEncoding,
273    ) -> Result<FrameYuv420pRender, RenderError>;
274
275    /// Renders one frame for YUV420 readback and reports where the call went.
276    ///
277    /// This produces exactly what [`GpuFrameRenderer::render_frame_yuv420p`]
278    /// produces; the only difference is that it reads a clock between stages
279    /// and writes the spans to `timings`. An incomplete source set leaves
280    /// `timings` holding only the stages that ran.
281    ///
282    /// # Errors
283    /// Returns [`RenderError`] for invalid graph, target, or YUV layout data.
284    fn render_frame_yuv420p_timed(
285        &self,
286        size: RenderSize,
287        frame: &FrameDesc,
288        frames: &dyn FrameProvider,
289        encoding: YuvEncoding,
290        timings: &mut FrameSubmitTimings,
291    ) -> Result<FrameYuv420pRender, RenderError>;
292
293    /// Renders one frame and packs it straight into caller-owned NV12 plane
294    /// textures, with no readback.
295    ///
296    /// This is [`GpuFrameRenderer::render_frame_yuv420p_timed`] with the
297    /// staging buffer removed: the same sources, the same graph and the same
298    /// conversion, ending in two textures a hardware encoder can be fed from
299    /// instead of a mapped buffer. An incomplete source set submits nothing.
300    ///
301    /// # Errors
302    /// Returns [`RenderError`] for invalid graph or plane-target data.
303    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
304    fn render_frame_nv12_timed(
305        &self,
306        size: RenderSize,
307        frame: &FrameDesc,
308        frames: &dyn FrameProvider,
309        planes: Nv12PlaneTargets<'_>,
310        encoding: YuvEncoding,
311        timings: &mut FrameSubmitTimings,
312    ) -> Result<FrameNv12Render, RenderError>;
313}
314
315/// [`FrameRenderer::build_frame_graph`] for any renderer: the graph depends
316/// on the renderer only through what its catalog says about effects.
317fn build_frame_graph<'a>(
318    catalog: &dyn EffectCatalog,
319    size: RenderSize,
320    frame: &'a FrameDesc<'a>,
321    frames: &'a dyn FrameProvider,
322) -> Result<FrameGraph<'a>, RenderError> {
323    validate_project_effects(catalog, frame)?;
324    let (report, mut available) = available_textures(frame, frames);
325    if !report.is_complete() {
326        return Ok(FrameGraph {
327            report,
328            graph: None,
329            pending: Vec::new(),
330        });
331    }
332    attach_texts(&mut available, frames, size);
333    let captions = caption_layers(frame, frames, size)?;
334    let graph = frame_graph(catalog, size, frame, &available, &captions)?;
335    Ok(FrameGraph {
336        report,
337        graph: Some(graph),
338        pending: pending_source_work(&available, frames),
339    })
340}
341
342impl FrameRenderer for Renderer {
343    type Image = RenderedTexture;
344
345    fn build_frame_graph<'a>(
346        &self,
347        size: RenderSize,
348        frame: &'a FrameDesc<'a>,
349        frames: &'a dyn FrameProvider,
350    ) -> Result<FrameGraph<'a>, RenderError> {
351        build_frame_graph(self, size, frame, frames)
352    }
353
354    fn render_frame_graph(
355        &self,
356        graph: &RenderGraph<'_>,
357        pending: &[&PendingSourceWork],
358    ) -> Result<RenderedTexture, RenderError> {
359        self.render_graph(graph, pending)
360    }
361
362    fn render_frame_into_yuv420p(
363        &self,
364        size: RenderSize,
365        frame: &FrameDesc,
366        frames: &dyn FrameProvider,
367        encoding: YuvEncoding,
368        targets: &mut Yuv420pTargets<'_>,
369    ) -> Result<RenderReport, RenderError> {
370        let render = self.render_frame_yuv420p(size, frame, frames, encoding)?;
371        if let Some(readback) = render.readback {
372            // A browser maps a buffer only on its event loop, which a blocking call never
373            // yields to: there the readback of `render_frame_yuv420p` is awaited instead.
374            #[cfg(target_arch = "wasm32")]
375            {
376                drop((readback, targets));
377                return Err(RenderError::TextureReadback(
378                    alpha2_render::ReadbackFailure::NotMapped,
379                ));
380            }
381            #[cfg(not(target_arch = "wasm32"))]
382            {
383                readback.wait_ready()?;
384                readback.finish_into(targets)?;
385            }
386        }
387        Ok(render.report)
388    }
389}
390
391impl FrameRenderer for CpuRenderer {
392    type Image = Arc<CpuImage>;
393
394    fn build_frame_graph<'a>(
395        &self,
396        size: RenderSize,
397        frame: &'a FrameDesc<'a>,
398        frames: &'a dyn FrameProvider,
399    ) -> Result<FrameGraph<'a>, RenderError> {
400        build_frame_graph(self, size, frame, frames)
401    }
402
403    fn render_frame_graph(
404        &self,
405        graph: &RenderGraph<'_>,
406        _pending: &[&PendingSourceWork],
407    ) -> Result<Arc<CpuImage>, RenderError> {
408        self.execute_graph(graph)
409            .map_err(GraphError::into_render_error)
410    }
411
412    fn render_frame_into_yuv420p(
413        &self,
414        size: RenderSize,
415        frame: &FrameDesc,
416        frames: &dyn FrameProvider,
417        encoding: YuvEncoding,
418        targets: &mut Yuv420pTargets<'_>,
419    ) -> Result<RenderReport, RenderError> {
420        let FrameGraph { report, graph, .. } = self.build_frame_graph(size, frame, frames)?;
421        if let Some(graph) = graph {
422            self.render_graph_into_yuv420p(&graph, encoding, targets)?;
423        }
424        Ok(report)
425    }
426}
427
428impl GpuFrameRenderer for Renderer {
429    fn render_frame_yuv420p(
430        &self,
431        size: RenderSize,
432        frame: &FrameDesc,
433        frames: &dyn FrameProvider,
434        encoding: YuvEncoding,
435    ) -> Result<FrameYuv420pRender, RenderError> {
436        render_frame_yuv420p_measured(self, size, frame, frames, encoding, None)
437    }
438
439    fn render_frame_yuv420p_timed(
440        &self,
441        size: RenderSize,
442        frame: &FrameDesc,
443        frames: &dyn FrameProvider,
444        encoding: YuvEncoding,
445        timings: &mut FrameSubmitTimings,
446    ) -> Result<FrameYuv420pRender, RenderError> {
447        render_frame_yuv420p_measured(self, size, frame, frames, encoding, Some(timings))
448    }
449
450    #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
451    fn render_frame_nv12_timed(
452        &self,
453        size: RenderSize,
454        frame: &FrameDesc,
455        frames: &dyn FrameProvider,
456        planes: Nv12PlaneTargets<'_>,
457        encoding: YuvEncoding,
458        timings: &mut FrameSubmitTimings,
459    ) -> Result<FrameNv12Render, RenderError> {
460        let mut clock = StageClock::start(true);
461        let mut collected = FrameSubmitTimings::default();
462        let FrameGraph {
463            report,
464            graph,
465            pending,
466        } = self.build_frame_graph(size, frame, frames)?;
467        collected.graph_build = clock.lap();
468        let submitted = graph.is_some();
469        if let Some(graph) = graph {
470            self.render_graph_nv12_timed(&graph, &pending, planes, encoding, &mut collected)?;
471        }
472        *timings = collected;
473        Ok(FrameNv12Render { report, submitted })
474    }
475}
476
477fn render_frame_yuv420p_measured(
478    renderer: &Renderer,
479    size: RenderSize,
480    frame: &FrameDesc,
481    frames: &dyn FrameProvider,
482    encoding: YuvEncoding,
483    timings: Option<&mut FrameSubmitTimings>,
484) -> Result<FrameYuv420pRender, RenderError> {
485    let measure = timings.is_some();
486    let mut clock = StageClock::start(measure);
487    let mut collected = FrameSubmitTimings::default();
488    let FrameGraph {
489        report,
490        graph,
491        pending,
492    } = renderer.build_frame_graph(size, frame, frames)?;
493    collected.graph_build = clock.lap();
494    let readback = match graph {
495        Some(graph) if measure => {
496            Some(renderer.render_graph_yuv420p_timed(&graph, &pending, encoding, &mut collected)?)
497        }
498        Some(graph) => Some(renderer.render_graph_yuv420p(&graph, &pending, encoding)?),
499        None => None,
500    };
501    if let Some(timings) = timings {
502        *timings = collected;
503    }
504    Ok(FrameYuv420pRender { report, readback })
505}
506
507/// A lap timer that reads no clock unless a caller asked for timings.
508#[derive(Copy, Clone)]
509struct StageClock(Option<Instant>);
510
511impl StageClock {
512    fn start(enabled: bool) -> Self {
513        Self(enabled.then(Instant::now))
514    }
515
516    /// Returns the span since the previous lap and starts a new one.
517    fn lap(&mut self) -> Duration {
518        let Some(mark) = self.0 else {
519            return Duration::ZERO;
520        };
521        let now = Instant::now();
522        self.0 = Some(now);
523        now.duration_since(mark)
524    }
525}
526
527fn validate_project_effects(
528    renderer: &dyn EffectCatalog,
529    frame: &FrameDesc,
530) -> Result<(), RenderError> {
531    require_effects(renderer, frame_effects(frame)).map_err(|_| RenderError::UnknownEffect)
532}
533
534/// Every effect `frame` draws, by id and version: its compositions' steps
535/// and its transitions.
536fn frame_effects<'a>(frame: &'a FrameDesc) -> impl Iterator<Item = (&'a str, u32)> {
537    let composition_effects = frame
538        .visual
539        .iter()
540        .flat_map(|clip| clip.composition.into_iter().chain(clip.source_composition))
541        .flat_map(|composition| composition.plan().effects())
542        .map(|effect| (effect.id.as_str(), effect.version));
543    let transition_effects = frame
544        .transitions
545        .iter()
546        .map(|transition| (transition.effect.id(), transition.effect.version()));
547    composition_effects.chain(transition_effects)
548}
549
550/// Checks that `renderer` draws every effect `frame` uses, naming the first
551/// it does not. [`FrameRenderer`]'s methods check the same, but answer with
552/// a [`RenderError`], which cannot say which effect.
553///
554/// # Errors
555/// Returns [`crate::EngineError::UnregisteredEffect`].
556pub fn check_frame_effects<C: EffectCatalog + ?Sized>(
557    renderer: &C,
558    frame: &FrameDesc,
559) -> Result<(), crate::EngineError> {
560    require_effects(renderer, frame_effects(frame))
561}
562
563/// The first of `effects` `renderer` does not draw, as an error.
564pub(crate) fn require_effects<'a, C: EffectCatalog + ?Sized>(
565    renderer: &C,
566    effects: impl IntoIterator<Item = (&'a str, u32)>,
567) -> Result<(), crate::EngineError> {
568    for (effect, version) in effects {
569        if renderer.project_effect_id(effect, version).is_none() {
570            return Err(crate::EngineError::UnregisteredEffect {
571                effect: effect.to_owned(),
572                version,
573            });
574        }
575    }
576    Ok(())
577}
578
579/// Collects the pending presentation work behind each available clip's
580/// textures, in composition order, for the renderer to fold into its
581/// submission.
582fn pending_source_work<'a>(
583    available: &[(&ActiveClip, ClipFrames<'_>)],
584    frames: &'a dyn FrameProvider,
585) -> Vec<&'a PendingSourceWork> {
586    let mut work = Vec::new();
587    for (clip, textures) in available {
588        if textures.source.is_some() {
589            work.extend(frames.pending_source_work(clip.source_id, clip.source_time));
590        }
591        for composition in clip.composition.iter().chain(&clip.source_composition) {
592            for (_, source, time) in composition.media() {
593                work.extend(frames.pending_source_work(source, time));
594            }
595        }
596    }
597    work
598}
599
600/// A host-supplied frame and, when the host vouches for it, its key.
601#[derive(Copy, Clone, Debug)]
602struct Frame<'t> {
603    view: SourceView<'t>,
604    key: Option<RenderCacheKey>,
605}
606
607impl<'t> Frame<'t> {
608    /// The frame as a graph source: keyed when the host gave a key.
609    fn add_to(self, graph: &mut RenderGraph<'t>) -> RenderNodeId {
610        match self.key {
611            Some(key) => graph.add_source_with_key(self.view, key),
612            None => graph.add_source(self.view),
613        }
614    }
615}
616
617/// Text a generator draws: its prepared texture, placed on the
618/// generator's transparent, canvas-sized picture.
619#[derive(Copy, Clone, Debug)]
620struct PlacedText<'t> {
621    texture: TextTexture<'t>,
622    visual: LayerTransform,
623}
624
625impl<'t> PlacedText<'t> {
626    /// The generator's picture: the texture, keyed as its store keys it,
627    /// over transparency.
628    fn add_to(self, graph: &mut RenderGraph<'t>) -> Result<RenderNodeId, RenderError> {
629        let source = match self.texture.cache_key {
630            Some(key) => graph.add_source_with_key(self.texture.texture, key),
631            None => graph.add_source(self.texture.texture),
632        };
633        graph
634            .add_composite(vec![GraphLayer {
635                source,
636                visual: self.visual,
637                blend_mode: BlendMode::Normal,
638            }])
639            .map_err(GraphError::into_render_error)
640    }
641}
642
643/// What one composition step draws from outside the graph.
644#[derive(Clone, Debug)]
645enum StepFrame<'t> {
646    /// A media step's host-supplied frame.
647    Media(Frame<'t>),
648    /// A text generator step's text.
649    Text(PlacedText<'t>),
650    /// A layout generator step's prepared layout.
651    Layout(LaidOut<'t>),
652}
653
654/// The host-supplied frames one visual clip draws.
655#[derive(Clone, Debug, Default)]
656struct ClipFrames<'t> {
657    /// The clip's index in [`FrameDesc::visual`], its place in track order,
658    /// which a caption's [`alpha2_eval::ActiveCaption::layers_below`] counts
659    /// in: the clips drawn are a subset, so their positions among
660    /// themselves do not say which tracks a caption lies between.
661    layer: usize,
662    /// The clip's own source frame; `None` when the engine draws the source
663    /// or the clip's composition does not show it.
664    source: Option<Frame<'t>>,
665    /// The text of the clip's text generator source, once prepared.
666    text: Option<PlacedText<'t>>,
667    /// The clip's layout generator source, once prepared.
668    layout: Option<LaidOut<'t>>,
669    /// Each media step's frame and each text generator step's text, by step
670    /// index; `None` for other steps.
671    media: Vec<Option<StepFrame<'t>>>,
672    /// The same for the clip's composition source.
673    source_media: Vec<Option<StepFrame<'t>>>,
674}
675
676/// Each visual clip with the source frames it draws. A frame the host has
677/// not supplied is reported missing, and its clip left out.
678fn available_textures<'frame, 'eval, 'texture>(
679    frame: &'frame FrameDesc<'eval>,
680    frames: &'texture dyn FrameProvider,
681) -> (
682    RenderReport,
683    Vec<(&'frame ActiveClip<'eval>, ClipFrames<'texture>)>,
684) {
685    let mut report = RenderReport::default();
686    let mut available = Vec::with_capacity(frame.visual.len());
687    let mut lookup = |source: &SourceId, time| {
688        let texture = frames.texture_for(source, time);
689        if texture.is_none() && !report.missing_sources.contains(source) {
690            report.missing_sources.push(source.clone());
691        }
692        texture.map(|view| Frame {
693            view,
694            key: frames.source_key(source, time),
695        })
696    };
697    // A matte no active clip names is drawn nowhere; it needs no frame.
698    for (layer, clip) in frame.visual.iter().enumerate() {
699        if !frame.draws(clip) {
700            continue;
701        }
702        let mut textures = ClipFrames {
703            layer,
704            ..ClipFrames::default()
705        };
706        let mut complete = true;
707        if clip.needs_source() {
708            textures.source = lookup(clip.source_id, clip.source_time);
709            complete &= textures.source.is_some();
710        }
711        for (composition, media) in [
712            (clip.composition, &mut textures.media),
713            (clip.source_composition, &mut textures.source_media),
714        ] {
715            let Some(composition) = composition else {
716                continue;
717            };
718            *media = vec![None; composition.plan().steps.len()];
719            for (step, source, time) in composition.media() {
720                media[step] = lookup(source, time).map(StepFrame::Media);
721                complete &= media[step].is_some();
722            }
723        }
724        if complete {
725            available.push((clip, textures));
726        }
727    }
728    (report, available)
729}
730
731/// Each available clip's text and layout generators with their prepared
732/// text and layouts, the clip's own source and its compositions' steps. What
733/// `frames` does not hold is left out, for the graph to report.
734fn attach_texts<'t>(
735    available: &mut [(&ActiveClip<'_>, ClipFrames<'t>)],
736    frames: &'t dyn FrameProvider,
737    size: RenderSize,
738) {
739    let laid_out = |plan: Option<&GeneratorPlan>, time: MediaTime| {
740        let plan = plan?.layout()?;
741        let layout = frames.layout(crate::layout_key(plan.evaluate(time).key, size))?;
742        LaidOut::attach(layout, frames)
743    };
744    let lookup = |generator: &Generator| match generator {
745        Generator::Text { text, style } => {
746            frames
747                .text(crate::text_key(text, style, size))
748                .map(|texture| PlacedText {
749                    texture,
750                    visual: text_placement(texture, style, size),
751                })
752        }
753        _ => None,
754    };
755    for (clip, textures) in available {
756        textures.text = clip.generator.and_then(lookup);
757        textures.layout = laid_out(clip.plan, clip.source_time);
758        for (composition, media) in [
759            (clip.composition, &mut textures.media),
760            (clip.source_composition, &mut textures.source_media),
761        ] {
762            let Some(composition) = composition else {
763                continue;
764            };
765            for (step, slot) in composition.plan().steps.iter().zip(media.iter_mut()) {
766                match &step.op {
767                    StepOp::Generator {
768                        generator: Generator::Layout { .. },
769                        start,
770                        plan,
771                    } => {
772                        let time = MediaTime(
773                            step.time(composition.time())
774                                .ticks()
775                                .saturating_add(start.ticks()),
776                        );
777                        *slot = laid_out(plan.as_deref(), time).map(StepFrame::Layout);
778                    }
779                    StepOp::Generator { generator, .. } => {
780                        *slot = lookup(generator).map(StepFrame::Text);
781                    }
782                    _ => {}
783                }
784            }
785        }
786    }
787}
788
789/// Where prepared text lies on the canvas: its texture keeps its pixel
790/// size, and the anchor point of the text with its decorations lies at its
791/// position.
792fn text_placement(texture: TextTexture<'_>, style: &TextStyle, size: RenderSize) -> LayerTransform {
793    #[allow(clippy::cast_precision_loss)]
794    let scale = [
795        texture.width as f32 / size.width as f32,
796        texture.height as f32 / size.height as f32,
797    ];
798    LayerTransform {
799        position: [style.position.x, style.position.y],
800        anchor: [
801            box_anchor(style.anchor.x, texture.width, texture.margin[0]),
802            box_anchor(style.anchor.y, texture.height, texture.margin[1]),
803        ],
804        scale,
805        rotation_degrees: 0.0,
806        opacity: 1.0,
807    }
808}
809
810/// A prepared caption and where it goes in the layer stack.
811struct CaptionLayer<'a> {
812    /// How many of the frame's visual clips, drawn or not, lie under it.
813    layers_below: usize,
814    texture: TextTexture<'a>,
815    visual: LayerTransform,
816}
817
818/// A caption's texture as a layer of `graph`: keyed as its store keys it,
819/// so a frame showing the same caption over the same picture is found again.
820fn caption_graph_layer<'a>(graph: &mut RenderGraph<'a>, caption: &CaptionLayer<'a>) -> GraphLayer {
821    let source = match caption.texture.cache_key {
822        Some(key) => graph.add_source_with_key(caption.texture.texture, key),
823        None => graph.add_source(caption.texture.texture),
824    };
825    GraphLayer {
826        source,
827        visual: caption.visual,
828        blend_mode: BlendMode::Normal,
829    }
830}
831
832/// Each caption `frame` shows, from `frames`, placed on the canvas
833/// ([`text_placement`]).
834fn caption_layers<'a>(
835    frame: &FrameDesc,
836    frames: &'a dyn FrameProvider,
837    size: RenderSize,
838) -> Result<Vec<CaptionLayer<'a>>, RenderError> {
839    frame
840        .captions
841        .iter()
842        .map(|caption| {
843            let texture = frames
844                .text(crate::text_key(caption.text, caption.style, size))
845                .ok_or(RenderError::MissingText)?;
846            Ok(CaptionLayer {
847                layers_below: caption.layers_below,
848                visual: text_placement(texture, caption.style, size),
849                texture,
850            })
851        })
852        .collect()
853}
854
855/// `anchor`, a point of a text with its decorations, as a point of its
856/// texture, which has `margin` pixels around that for ink past it: so an
857/// accent or a descender never moves the text.
858#[allow(clippy::cast_precision_loss)]
859fn box_anchor(anchor: f32, extent: u32, margin: u32) -> f32 {
860    if margin == 0 {
861        return anchor;
862    }
863    let inner = extent.saturating_sub(margin * 2) as f32;
864    (margin as f32 + anchor * inner) / extent as f32
865}
866
867/// The runtime a frame's passes see: its time and the canvas's aspect ratio.
868fn frame_runtime(frame: &FrameDesc, size: RenderSize) -> EffectRuntime {
869    #[allow(clippy::cast_precision_loss)]
870    let ratio = size.width as f32 / size.height as f32;
871    EffectRuntime {
872        time: frame.time,
873        ratio,
874        ..EffectRuntime::default()
875    }
876}
877
878fn frame_graph<'a>(
879    renderer: &dyn EffectCatalog,
880    size: RenderSize,
881    frame: &'a FrameDesc<'a>,
882    available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
883    captions: &[CaptionLayer<'a>],
884) -> Result<RenderGraph<'a>, RenderError> {
885    let mut graph = RenderGraph::new(size);
886    graph.set_blend_space(match frame.blend_space {
887        alpha2_model::BlendSpace::LinearLight => alpha2_render::BlendSpace::LinearLight,
888        alpha2_model::BlendSpace::SrgbEncoded => alpha2_render::BlendSpace::SrgbEncoded,
889    });
890    let mut nodes = HashMap::with_capacity(available.len());
891    let mut inputs = Vec::with_capacity(available.len() + frame.transitions.len());
892    // Each matte's coverage, drawn once however many clips it mattes.
893    let mut coverages = HashMap::new();
894    let runtime = frame_runtime(frame, size);
895    for (clip, textures) in available {
896        if clip.is_matte {
897            continue;
898        }
899        let mut layer = clip_layer(renderer, &mut graph, clip, textures, frame.time, runtime)?;
900        if let Some(matte) = clip.matte {
901            layer = apply_matte(
902                renderer,
903                &mut graph,
904                layer,
905                matte,
906                available,
907                (frame.time, runtime),
908                &mut coverages,
909            )?;
910        }
911        nodes.insert(clip.clip_id, inputs.len());
912        inputs.push(FrameInput {
913            kind: FrameInputKind::Layer(layer),
914            uses_backdrop: clip.blend_mode != ClipBlendMode::Normal,
915        });
916    }
917    let background = background_color(frame.background);
918    let mut suppressed = HashSet::new();
919    for transition in &frame.transitions {
920        let from = *nodes
921            .get(transition.from_clip)
922            .ok_or(RenderError::InvalidGraph)?;
923        let to = *nodes
924            .get(transition.to_clip)
925            .ok_or(RenderError::InvalidGraph)?;
926        let output = inputs.len();
927        inputs.push(FrameInput {
928            kind: FrameInputKind::Transition {
929                from,
930                to,
931                transition,
932            },
933            uses_backdrop: inputs[from].uses_backdrop || inputs[to].uses_backdrop,
934        });
935        nodes.insert(transition.to_clip, output);
936        suppressed.insert(transition.from_clip);
937    }
938    let mut layers = Vec::with_capacity(available.len() + captions.len());
939    let mut captions = captions.iter().peekable();
940    for (clip, textures) in available {
941        // Captions on tracks before this clip's, by track order: a clip before it that is not
942        // drawn (a matte nothing names) does not move them.
943        while let Some(caption) = captions.next_if(|caption| caption.layers_below <= textures.layer)
944        {
945            layers.push(caption_graph_layer(&mut graph, caption));
946        }
947        if clip.is_matte || suppressed.contains(clip.clip_id) {
948            continue;
949        }
950        let index = *nodes.get(clip.clip_id).ok_or(RenderError::InvalidGraph)?;
951        if let FrameInputKind::Layer(layer) = inputs[index].kind {
952            layers.push(layer);
953            continue;
954        }
955        // Normal transitions operate on isolated, transformed layers. Artistic modes need the
956        // same backdrop on both sides: transition their composed scenes, then replace that
957        // backdrop exactly once.
958        let uses_backdrop = inputs[index].uses_backdrop;
959        let backdrop = if uses_backdrop {
960            layers.as_slice()
961        } else {
962            &[]
963        };
964        // The background is the bottom layer of that backdrop too, so an artistic mode blends
965        // against the same color inside a transition as outside one.
966        let source = compile_frame_input(
967            &mut graph,
968            &inputs,
969            index,
970            backdrop,
971            uses_backdrop.then_some(background),
972            runtime,
973            renderer,
974        )?;
975        if uses_backdrop {
976            layers.clear();
977        }
978        layers.push(identity_layer(source));
979    }
980    for caption in captions {
981        layers.push(caption_graph_layer(&mut graph, caption));
982    }
983    let output = graph
984        .add_composite_over(layers, background)
985        .map_err(GraphError::into_render_error)?;
986    graph
987        .set_output(output)
988        .map_err(GraphError::into_render_error)?;
989    graph
990        .set_cache_intent(output, CacheIntent::Auto)
991        .map_err(GraphError::into_render_error)?;
992    Ok(graph)
993}
994
995const fn cache_intent(intent: alpha2_model::CacheIntent) -> CacheIntent {
996    match intent {
997        alpha2_model::CacheIntent::Auto => CacheIntent::Auto,
998        alpha2_model::CacheIntent::Pin => CacheIntent::Pin,
999        alpha2_model::CacheIntent::Bypass => CacheIntent::Bypass,
1000    }
1001}
1002
1003/// `layer` kept where `matte` says, the matte clip drawn only into it.
1004fn apply_matte<'a, 'c>(
1005    renderer: &dyn EffectCatalog,
1006    graph: &mut RenderGraph<'a>,
1007    mut layer: GraphLayer,
1008    matte: &'c alpha2_model::Matte,
1009    available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
1010    (time, runtime): (MediaTime, EffectRuntime),
1011    coverages: &mut HashMap<&'c alpha2_model::ClipId, RenderNodeId>,
1012) -> Result<GraphLayer, RenderError> {
1013    let source = available
1014        .iter()
1015        .find(|(candidate, _)| candidate.clip_id == &matte.source);
1016    match source {
1017        Some((source, textures)) => {
1018            let coverage = if let Some(coverage) = coverages.get(&matte.source) {
1019                *coverage
1020            } else {
1021                let matte_layer = clip_layer(renderer, graph, source, textures, time, runtime)?;
1022                let coverage = alone(graph, matte_layer)?;
1023                coverages.insert(&matte.source, coverage);
1024                coverage
1025            };
1026            layer = matted_layer(graph, layer, coverage, matte.mode, runtime)?;
1027        }
1028        // An inactive matte covers nothing: the clip is hidden, or wholly visible when the
1029        // matte is inverted. A hidden clip keeps its node so a transition can still name it.
1030        None if is_inverted(matte.mode) => {}
1031        None => layer.visual.opacity = 0.0,
1032    }
1033    Ok(layer)
1034}
1035
1036/// The project background as an opaque, sRGB-encoded storage value.
1037fn background_color(background: Rgb) -> Color {
1038    Color::rgba(
1039        f64::from(background.red),
1040        f64::from(background.green),
1041        f64::from(background.blue),
1042        1.0,
1043    )
1044}
1045
1046struct FrameInput<'a> {
1047    kind: FrameInputKind<'a>,
1048    uses_backdrop: bool,
1049}
1050
1051fn clip_layer<'a>(
1052    renderer: &dyn EffectCatalog,
1053    graph: &mut RenderGraph<'a>,
1054    clip: &'a ActiveClip<'a>,
1055    textures: &ClipFrames<'a>,
1056    time: MediaTime,
1057    runtime: EffectRuntime,
1058) -> Result<GraphLayer, RenderError> {
1059    let mut source = match clip.composition {
1060        None => clip_picture(renderer, graph, clip, textures, runtime)?,
1061        Some(composition) => plan_node(
1062            renderer,
1063            graph,
1064            composition,
1065            &textures.media,
1066            Some((clip, textures)),
1067            runtime,
1068        )?,
1069    };
1070    if !clip.masks.is_empty() {
1071        source = mask_node(graph, source, clip.masks, time, runtime)?;
1072    }
1073    // The clip's finished picture is where reuse pays: everything above it
1074    // changes with the layer stack, everything below it is in its key. A bare
1075    // source frame is already the host's; only a bypass on it matters.
1076    let intent = cache_intent(clip.cache);
1077    if intent == CacheIntent::Bypass || graph.node_kind(source) != Some(RenderNodeKind::Source) {
1078        graph
1079            .set_cache_intent(source, intent)
1080            .map_err(GraphError::into_render_error)?;
1081    }
1082    Ok(GraphLayer {
1083        source,
1084        visual: layer_transform(clip.visual.ok_or(RenderError::InvalidVisual)?),
1085        blend_mode: blend_mode(clip.blend_mode),
1086    })
1087}
1088
1089/// The clip's own picture: its composition source, its generated source,
1090/// or its source frame.
1091fn clip_picture<'a>(
1092    renderer: &dyn EffectCatalog,
1093    graph: &mut RenderGraph<'a>,
1094    clip: &'a ActiveClip<'a>,
1095    textures: &ClipFrames<'a>,
1096    runtime: EffectRuntime,
1097) -> Result<RenderNodeId, RenderError> {
1098    if let Some(source) = clip.source_composition {
1099        // A composition source's plan has no clip steps.
1100        return plan_node(
1101            renderer,
1102            graph,
1103            source,
1104            &textures.source_media,
1105            None,
1106            runtime,
1107        );
1108    }
1109    match (clip.generator, textures.source) {
1110        (Some(Generator::Text { .. }), _) => {
1111            textures.text.ok_or(RenderError::MissingText)?.add_to(graph)
1112        }
1113        (Some(Generator::Layout { .. }), _) => textures
1114            .layout
1115            .as_ref()
1116            .ok_or(RenderError::MissingText)?
1117            .add_to(graph),
1118        (Some(Generator::Shapes { .. }), _) => {
1119            let plan = clip
1120                .plan
1121                .and_then(GeneratorPlan::shapes)
1122                .ok_or(RenderError::InvalidGraph)?;
1123            Ok(shapes_node(graph, plan, clip.source_time))
1124        }
1125        (Some(generator), _) => Ok(generator_node(graph, generator, runtime)),
1126        (None, Some(frame)) => Ok(frame.add_to(graph)),
1127        (None, None) => Err(RenderError::InvalidGraph),
1128    }
1129}
1130
1131/// A shapes generator's picture at `time`, in the picture's own time: its
1132/// tree evaluated there (its fixed parts shared) and drawn as a vector scene
1133/// at the graph's size. A frame that draws the same scene has the same key.
1134fn shapes_node(graph: &mut RenderGraph<'_>, plan: &ShapesPlan, time: MediaTime) -> RenderNodeId {
1135    let size = graph.size();
1136    let canvas = alpha2_geometry::CanvasSize::new(size.width, size.height);
1137    let aspect = f64::from(size.width) / f64::from(size.height);
1138    let items = plan.evaluate(time, aspect);
1139    graph.add_vector(std::sync::Arc::new(alpha2_geometry::build_scene(
1140        &items, canvas,
1141    )))
1142}
1143
1144/// A fill does not change over time, so its node has the same cache key in
1145/// every frame.
1146fn generator_node(
1147    graph: &mut RenderGraph<'_>,
1148    generator: &alpha2_model::Generator,
1149    runtime: EffectRuntime,
1150) -> RenderNodeId {
1151    graph.add_generator(
1152        EffectId::from_static(GENERATOR_EFFECT_ID, 1),
1153        crate::generated::generator_params(generator),
1154        timeless(runtime),
1155    )
1156}
1157
1158/// The node drawing a compiled composition's output: each step in order,
1159/// with its settings sampled at the composition time plus its offset. Its
1160/// clip steps draw `clip`'s picture; a plan without them takes none.
1161fn plan_node<'a>(
1162    renderer: &dyn EffectCatalog,
1163    graph: &mut RenderGraph<'a>,
1164    composition: ActiveComposition<'a>,
1165    media: &[Option<StepFrame<'a>>],
1166    clip: Option<(&'a ActiveClip<'a>, &ClipFrames<'a>)>,
1167    runtime: EffectRuntime,
1168) -> Result<RenderNodeId, RenderError> {
1169    let plan = composition.plan();
1170    let time = composition.time();
1171    let mut built: Vec<RenderNodeId> = Vec::with_capacity(plan.steps.len());
1172    let mut picture = None;
1173    for (index, step) in plan.steps.iter().enumerate() {
1174        let at = step.time(time);
1175        let step_runtime = EffectRuntime {
1176            time: at,
1177            ..runtime
1178        };
1179        let node = match &step.op {
1180            // Every clip step draws the same picture.
1181            StepOp::Clip => {
1182                if let Some(node) = picture {
1183                    node
1184                } else {
1185                    let (clip, textures) = clip.ok_or(RenderError::InvalidGraph)?;
1186                    *picture.insert(clip_picture(renderer, graph, clip, textures, runtime)?)
1187                }
1188            }
1189            StepOp::Media { .. } => match media.get(index).and_then(Option::as_ref) {
1190                Some(StepFrame::Media(frame)) => frame.add_to(graph),
1191                _ => return Err(RenderError::InvalidGraph),
1192            },
1193            StepOp::Generator {
1194                generator: Generator::Text { .. },
1195                ..
1196            } => match media.get(index).and_then(Option::as_ref) {
1197                Some(StepFrame::Text(text)) => text.add_to(graph)?,
1198                _ => return Err(RenderError::MissingText),
1199            },
1200            StepOp::Generator {
1201                generator: Generator::Layout { .. },
1202                ..
1203            } => match media.get(index).and_then(Option::as_ref) {
1204                Some(StepFrame::Layout(layout)) => layout.add_to(graph)?,
1205                _ => return Err(RenderError::MissingText),
1206            },
1207            op => step_node(renderer, graph, op, &built, at, step_runtime)?,
1208        };
1209        built.push(node);
1210    }
1211    step_input(&built, plan.output)
1212}
1213
1214// A node names at most as many inputs as an effect may declare.
1215const _: () = assert!(alpha2_model::MAX_EFFECT_INPUTS == alpha2_render::MAX_EFFECT_INPUTS);
1216
1217/// The node an earlier step drew.
1218fn step_input(built: &[RenderNodeId], step: usize) -> Result<RenderNodeId, RenderError> {
1219    built.get(step).copied().ok_or(RenderError::InvalidGraph)
1220}
1221
1222/// The node for a step that draws from its inputs, or from nothing.
1223fn step_node<'a>(
1224    renderer: &dyn EffectCatalog,
1225    graph: &mut RenderGraph<'a>,
1226    op: &'a StepOp,
1227    built: &[RenderNodeId],
1228    time: MediaTime,
1229    runtime: EffectRuntime,
1230) -> Result<RenderNodeId, RenderError> {
1231    let input = |step: usize| step_input(built, step);
1232    let node = match op {
1233        StepOp::Clip | StepOp::Media { .. } => return Err(RenderError::InvalidGraph),
1234        StepOp::Generator {
1235            generator: Generator::Shapes { .. },
1236            start,
1237            plan,
1238        } => {
1239            let plan = plan
1240                .as_deref()
1241                .and_then(GeneratorPlan::shapes)
1242                .ok_or(RenderError::InvalidGraph)?;
1243            let at = MediaTime(time.ticks().saturating_add(start.ticks()));
1244            shapes_node(graph, plan, at)
1245        }
1246        StepOp::Generator { generator, .. } => generator_node(graph, generator, runtime),
1247        StepOp::Effect {
1248            input: from,
1249            inputs,
1250            effect,
1251        } => {
1252            let id = project_effect_id(renderer, &effect.id, effect.version)?;
1253            let active = ActiveEffect::new(effect, time);
1254            let params = effect_params(renderer, &id, active);
1255            let runtime = effect_runtime(renderer, &id, runtime);
1256            let mut named = EffectInputs::new();
1257            for (name, step) in inputs {
1258                named.insert(name.as_str(), input(*step)?);
1259            }
1260            graph.add_effect_with_inputs(
1261                id,
1262                input(*from)?,
1263                named,
1264                params,
1265                effect_resources(active),
1266                runtime,
1267            )
1268        }
1269        .map_err(GraphError::into_render_error)?,
1270        StepOp::Transition {
1271            from,
1272            to,
1273            effect,
1274            progress,
1275        } => {
1276            let id = project_effect_id(renderer, &effect.id, effect.version)?;
1277            let active = ActiveEffect::new(effect, time);
1278            let params = effect_params(renderer, &id, active);
1279            let runtime = EffectRuntime {
1280                progress: sample_scalar(progress, time).clamp(0.0, 1.0),
1281                ..effect_runtime(renderer, &id, runtime)
1282            };
1283            graph.add_transition_with_resources(
1284                id,
1285                input(*from)?,
1286                input(*to)?,
1287                params,
1288                effect_resources(active),
1289                runtime,
1290            )
1291        }
1292        .map_err(GraphError::into_render_error)?,
1293        StepOp::Transform {
1294            input: from,
1295            transform,
1296        } => graph
1297            .add_composite(vec![GraphLayer {
1298                source: input(*from)?,
1299                visual: layer_transform(sample_visual(transform, time, graph_aspect(graph))),
1300                blend_mode: BlendMode::Normal,
1301            }])
1302            .map_err(GraphError::into_render_error)?,
1303        StepOp::Merge { layers } => {
1304            let layers = layers
1305                .iter()
1306                .map(|layer| {
1307                    let mut placed = identity_layer(input(layer.input)?);
1308                    placed.visual.opacity = sample_scalar(&layer.opacity, time).clamp(0.0, 1.0);
1309                    placed.blend_mode = blend_mode(layer.blend_mode);
1310                    Ok(placed)
1311                })
1312                .collect::<Result<Vec<_>, RenderError>>()?;
1313            graph
1314                .add_composite(layers)
1315                .map_err(GraphError::into_render_error)?
1316        }
1317        StepOp::Matte {
1318            input: from,
1319            matte,
1320            mode,
1321        } => matte_node(graph, input(*from)?, input(*matte)?, *mode, runtime)?,
1322        StepOp::ShapeMask { input: from, masks } => {
1323            mask_node(graph, input(*from)?, masks, time, runtime)?
1324        }
1325    };
1326    Ok(node)
1327}
1328
1329/// The width over the height of the graph's pictures: what a position
1330/// moving along a spatial curve is measured in.
1331fn graph_aspect(graph: &RenderGraph<'_>) -> f64 {
1332    let size = graph.size();
1333    f64::from(size.width) / f64::from(size.height)
1334}
1335
1336/// `source` kept inside `masks` at `time`. Each mask's outline, evaluated
1337/// in the picture's normalized space, is drawn as a vector scene of opaque
1338/// white under its fill rule; a feathered one is blurred by a Gaussian of
1339/// deviation `0.3125 * feather` (of the shorter side), whose 10 to 90
1340/// percent ramp is `0.8 * feather` wide. One pass then combines them in
1341/// order and keeps the source by the result.
1342fn mask_node(
1343    graph: &mut RenderGraph<'_>,
1344    source: RenderNodeId,
1345    masks: &[ShapeMask],
1346    time: MediaTime,
1347    runtime: EffectRuntime,
1348) -> Result<RenderNodeId, RenderError> {
1349    let size = graph.size();
1350    let canvas = alpha2_geometry::CanvasSize::new(size.width, size.height);
1351    let aspect = graph_aspect(graph);
1352    let unit = size.width.min(size.height);
1353    let white = alpha2_model::Paint::Solid {
1354        color: alpha2_model::Rgba::new(1.0, 1.0, 1.0, 1.0),
1355    };
1356    let shown = &masks[..masks.len().min(MAX_SHAPE_MASKS)];
1357    if shown.is_empty() {
1358        return Ok(source);
1359    }
1360    let mut coverages = Vec::with_capacity(shown.len());
1361    for mask in shown {
1362        let outline = alpha2_eval::evaluate_geometry(&mask.shape, time, aspect);
1363        let items = [
1364            alpha2_geometry::EvaluatedItem::Geometry(outline),
1365            alpha2_geometry::EvaluatedItem::Fill {
1366                paint: white.clone(),
1367                opacity: 1.0,
1368                rule: mask.fill_rule,
1369            },
1370        ];
1371        let coverage = graph.add_vector(std::sync::Arc::new(alpha2_geometry::build_scene(
1372            &items, canvas,
1373        )));
1374        #[allow(clippy::cast_precision_loss)]
1375        let sigma = 0.3125 * sample_scalar(&mask.feather, time).max(0.0) * unit as f32;
1376        let coverage = graph
1377            .add_gaussian_pixels(coverage, sigma, timeless(runtime))
1378            .map_err(GraphError::into_render_error)?;
1379        coverages.push((coverage, mask.invert, mask.mode));
1380    }
1381    coverage_mask(graph, source, &coverages)
1382}
1383
1384/// `source` kept by `coverages`, pictures on the same canvas whose alpha
1385/// says where: each inverted or not, combined in order by its mode, in one
1386/// pass. At most [`MAX_SHAPE_MASKS`].
1387pub(crate) fn coverage_mask(
1388    graph: &mut RenderGraph<'_>,
1389    source: RenderNodeId,
1390    coverages: &[(RenderNodeId, bool, MaskMode)],
1391) -> Result<RenderNodeId, RenderError> {
1392    if coverages.is_empty() || coverages.len() > MAX_SHAPE_MASKS {
1393        return Err(RenderError::InvalidGraph);
1394    }
1395    let size = graph.size();
1396    #[allow(clippy::cast_precision_loss)]
1397    let runtime = EffectRuntime {
1398        ratio: size.width as f32 / size.height as f32,
1399        ..EffectRuntime::default()
1400    };
1401    let mut inputs = EffectInputs::new();
1402    let mut params = EffectParams::with_capacity(coverages.len() * 2);
1403    for (&(coverage, invert, mode), names) in coverages.iter().zip(MASK_COMBINE_NAMES) {
1404        inputs.insert(names.input, coverage);
1405        params.insert(names.invert, ParamValue::Bool(invert));
1406        params.insert(
1407            names.mode,
1408            ParamValue::Enum(match mode {
1409                MaskMode::Add => 0,
1410                MaskMode::Subtract => 1,
1411                MaskMode::Intersect => 2,
1412            }),
1413        );
1414    }
1415    graph
1416        .add_effect_with_inputs(
1417            EffectId::from_static(MASK_COMBINE_EFFECT_IDS[coverages.len() - 1], 1),
1418            source,
1419            inputs,
1420            params,
1421            EffectResources::new(),
1422            runtime,
1423        )
1424        .map_err(GraphError::into_render_error)
1425}
1426
1427const fn is_inverted(mode: MatteMode) -> bool {
1428    matches!(mode, MatteMode::AlphaInverted | MatteMode::LumaInverted)
1429}
1430
1431/// `layer` kept where `matte`'s picture says, as an untransformed layer with
1432/// `layer`'s blend mode. Both are drawn alone onto the canvas first, so the
1433/// matte applies where the two land, whatever their transforms.
1434fn matted_layer(
1435    graph: &mut RenderGraph<'_>,
1436    layer: GraphLayer,
1437    coverage: RenderNodeId,
1438    mode: MatteMode,
1439    runtime: EffectRuntime,
1440) -> Result<GraphLayer, RenderError> {
1441    let picture = alone(graph, layer)?;
1442    let matted = matte_node(graph, picture, coverage, mode, runtime)?;
1443    Ok(GraphLayer {
1444        blend_mode: layer.blend_mode,
1445        ..identity_layer(matted)
1446    })
1447}
1448
1449/// `layer` drawn alone, as it covers the canvas.
1450fn alone(graph: &mut RenderGraph<'_>, layer: GraphLayer) -> Result<RenderNodeId, RenderError> {
1451    graph
1452        .add_composite(vec![GraphLayer {
1453            blend_mode: BlendMode::Normal,
1454            ..layer
1455        }])
1456        .map_err(GraphError::into_render_error)
1457}
1458
1459/// The runtime `effect` is drawn with: `runtime`, without the frame's time
1460/// when the effect cannot read it ([`Renderer::effect_reads_time`]). Its
1461/// result then has the same cache key in every frame that draws it from the
1462/// same inputs and parameters, and is found again there; an effect that
1463/// reads the time keeps a key of its own in each frame.
1464fn effect_runtime(
1465    renderer: &dyn EffectCatalog,
1466    effect: &EffectId,
1467    runtime: EffectRuntime,
1468) -> EffectRuntime {
1469    if renderer.effect_reads_time(effect) {
1470        runtime
1471    } else {
1472        EffectRuntime {
1473            time: MediaTime::ZERO,
1474            ..runtime
1475        }
1476    }
1477}
1478
1479/// The runtime of a pass the engine writes whose picture does not change
1480/// with time (a shape mask or a track matte; [`generator_node`] likewise):
1481/// without the frame's time, so its cache key is the same in every frame.
1482fn timeless(runtime: EffectRuntime) -> EffectRuntime {
1483    EffectRuntime {
1484        ratio: runtime.ratio,
1485        ..EffectRuntime::default()
1486    }
1487}
1488
1489/// `picture` kept where `coverage`, drawn on the same canvas, says.
1490fn matte_node(
1491    graph: &mut RenderGraph<'_>,
1492    picture: RenderNodeId,
1493    coverage: RenderNodeId,
1494    mode: MatteMode,
1495    runtime: EffectRuntime,
1496) -> Result<RenderNodeId, RenderError> {
1497    graph
1498        .add_transition_with_resources(
1499            EffectId::from_static(TRACK_MATTE_EFFECT_ID, 1),
1500            picture,
1501            coverage,
1502            EffectParams::new()
1503                .with(
1504                    "luma",
1505                    ParamValue::Bool(matches!(mode, MatteMode::Luma | MatteMode::LumaInverted)),
1506                )
1507                .with("inverted", ParamValue::Bool(is_inverted(mode))),
1508            EffectResources::new(),
1509            // The matte ignores progress and time; an interior progress runs its body.
1510            EffectRuntime {
1511                progress: 0.5,
1512                ..timeless(runtime)
1513            },
1514        )
1515        .map_err(GraphError::into_render_error)
1516}
1517
1518#[derive(Copy, Clone)]
1519enum FrameInputKind<'a> {
1520    Layer(GraphLayer),
1521    Transition {
1522        from: usize,
1523        to: usize,
1524        transition: &'a ActiveTransition<'a>,
1525    },
1526}
1527
1528fn compile_frame_input<'a>(
1529    graph: &mut RenderGraph<'a>,
1530    inputs: &[FrameInput<'a>],
1531    index: usize,
1532    backdrop: &[GraphLayer],
1533    background: Option<Color>,
1534    runtime: EffectRuntime,
1535    renderer: &dyn EffectCatalog,
1536) -> Result<RenderNodeId, RenderError> {
1537    if index >= inputs.len() {
1538        return Err(RenderError::InvalidGraph);
1539    }
1540    let mut compiled = vec![None; inputs.len()];
1541    let mut pending = vec![(index, false)];
1542    while let Some((current, expanded)) = pending.pop() {
1543        if compiled[current].is_some() {
1544            continue;
1545        }
1546        match inputs[current].kind {
1547            FrameInputKind::Layer(layer) => {
1548                let mut layers = Vec::with_capacity(backdrop.len() + 1);
1549                layers.extend_from_slice(backdrop);
1550                layers.push(layer);
1551                let composite = match background {
1552                    Some(background) => graph.add_composite_over(layers, background),
1553                    None => graph.add_composite(layers),
1554                };
1555                compiled[current] = Some(composite.map_err(GraphError::into_render_error)?);
1556            }
1557            FrameInputKind::Transition {
1558                from,
1559                to,
1560                transition,
1561            } => {
1562                if from >= inputs.len() || to >= inputs.len() {
1563                    return Err(RenderError::InvalidGraph);
1564                }
1565                if !expanded {
1566                    pending.push((current, true));
1567                    if compiled[to].is_none() {
1568                        pending.push((to, false));
1569                    }
1570                    if compiled[from].is_none() {
1571                        pending.push((from, false));
1572                    }
1573                    continue;
1574                }
1575                let from = compiled[from].ok_or(RenderError::InvalidGraph)?;
1576                let to = compiled[to].ok_or(RenderError::InvalidGraph)?;
1577                let id = project_effect_id(
1578                    renderer,
1579                    transition.effect.id(),
1580                    transition.effect.version(),
1581                )?;
1582                let params = effect_params(renderer, &id, transition.effect);
1583                let runtime = EffectRuntime {
1584                    progress: transition.progress,
1585                    ..effect_runtime(renderer, &id, runtime)
1586                };
1587                compiled[current] = Some(
1588                    graph
1589                        .add_transition_with_resources(
1590                            id,
1591                            from,
1592                            to,
1593                            params,
1594                            effect_resources(transition.effect),
1595                            runtime,
1596                        )
1597                        .map_err(GraphError::into_render_error)?,
1598                );
1599            }
1600        }
1601    }
1602    compiled[index].ok_or(RenderError::InvalidGraph)
1603}
1604
1605fn identity_layer(source: RenderNodeId) -> GraphLayer {
1606    GraphLayer {
1607        source,
1608        visual: LayerTransform {
1609            position: [0.5, 0.5],
1610            anchor: [0.5, 0.5],
1611            scale: [1.0, 1.0],
1612            rotation_degrees: 0.0,
1613            opacity: 1.0,
1614        },
1615        blend_mode: BlendMode::Normal,
1616    }
1617}
1618
1619/// Carries the evaluator's presentation values into the renderer's layer
1620/// transform unchanged.
1621const fn layer_transform(visual: EvaluatedVisual) -> LayerTransform {
1622    LayerTransform {
1623        position: [visual.position.x, visual.position.y],
1624        anchor: [visual.anchor.x, visual.anchor.y],
1625        scale: [visual.scale.x, visual.scale.y],
1626        rotation_degrees: visual.rotation_degrees,
1627        opacity: visual.opacity,
1628    }
1629}
1630
1631/// The invocation's parameters at the frame's time.
1632///
1633/// A value between two keyframes is clamped to its parameter's range: a
1634/// timing curve may overshoot, and the overshoot is part of the motion, not an
1635/// authoring error. An authored value (a constant, or the value on a
1636/// keyframe) is passed as written, and the renderer rejects one outside the
1637/// range.
1638fn effect_params<'a>(
1639    renderer: &dyn EffectCatalog,
1640    id: &EffectId,
1641    effect: ActiveEffect<'a>,
1642) -> EffectParams<'a> {
1643    let mut params = EffectParams::with_capacity(effect.authored().params.len());
1644    for (name, sample) in effect.params() {
1645        let mut value = param_value(sample.value);
1646        if sample.interpolated
1647            && let Some(spec) = renderer
1648                .effect_definition(id)
1649                .and_then(|definition| definition.params.iter().find(|spec| spec.name == name))
1650        {
1651            value = clamp_to_range(spec, value);
1652        }
1653        params.insert(name, value);
1654    }
1655    params
1656}
1657
1658/// The identity a project's effect is registered under, which the graph
1659/// holds without copying its text.
1660fn project_effect_id(
1661    renderer: &dyn EffectCatalog,
1662    id: &str,
1663    version: u32,
1664) -> Result<EffectId, RenderError> {
1665    renderer
1666        .project_effect_id(id, version)
1667        .cloned()
1668        .ok_or(RenderError::UnknownEffect)
1669}
1670
1671fn clamp_to_range(spec: &ParamSpec, value: ParamValue) -> ParamValue {
1672    let clamp = |value: f32| {
1673        let value = spec.min.map_or(value, |minimum| value.max(minimum));
1674        spec.max.map_or(value, |maximum| value.min(maximum))
1675    };
1676    match value {
1677        ParamValue::Float(value) => ParamValue::Float(clamp(value)),
1678        ParamValue::Vec2([x, y]) => ParamValue::Vec2([clamp(x), clamp(y)]),
1679        ParamValue::Vec4(values) => ParamValue::Vec4(values.map(clamp)),
1680        other => other,
1681    }
1682}
1683
1684fn effect_resources(effect: ActiveEffect<'_>) -> EffectResources {
1685    effect
1686        .resources()
1687        .fold(EffectResources::new(), |resources, (name, id)| {
1688            resources.with(name, id.as_str())
1689        })
1690}
1691
1692const fn param_value(value: EffectValue) -> ParamValue {
1693    match value {
1694        EffectValue::Float(value) => ParamValue::Float(value),
1695        EffectValue::Vec2(value) => ParamValue::Vec2(value),
1696        EffectValue::Vec4(value) => ParamValue::Vec4(value),
1697        EffectValue::Color(value) => ParamValue::Color(value),
1698        EffectValue::Enum(value) => ParamValue::Enum(value),
1699        EffectValue::Bool(value) => ParamValue::Bool(value),
1700    }
1701}
1702
1703const fn blend_mode(mode: ClipBlendMode) -> BlendMode {
1704    match mode {
1705        ClipBlendMode::Normal => BlendMode::Normal,
1706        ClipBlendMode::Multiply => BlendMode::Multiply,
1707        ClipBlendMode::Screen => BlendMode::Screen,
1708        ClipBlendMode::Overlay => BlendMode::Overlay,
1709        ClipBlendMode::Darken => BlendMode::Darken,
1710        ClipBlendMode::Lighten => BlendMode::Lighten,
1711        ClipBlendMode::ColorDodge => BlendMode::ColorDodge,
1712        ClipBlendMode::ColorBurn => BlendMode::ColorBurn,
1713        ClipBlendMode::HardLight => BlendMode::HardLight,
1714        ClipBlendMode::SoftLight => BlendMode::SoftLight,
1715        ClipBlendMode::Difference => BlendMode::Difference,
1716        ClipBlendMode::Exclusion => BlendMode::Exclusion,
1717        ClipBlendMode::Hue => BlendMode::Hue,
1718        ClipBlendMode::Saturation => BlendMode::Saturation,
1719        ClipBlendMode::Color => BlendMode::Color,
1720        ClipBlendMode::Luminosity => BlendMode::Luminosity,
1721    }
1722}
1723
1724#[cfg(test)]
1725mod tests;