1use alpha2_eval::{
9 ActiveClip, ActiveComposition, ActiveEffect, ActiveTransition, EvaluatedVisual, FrameDesc,
10 StepOp, sample_scalar, sample_vec2, sample_visual,
11};
12use alpha2_media::{SourceId, YuvEncoding};
13use alpha2_model::{
14 ClipBlendMode, EffectValue, Generator, MaskMode, MaskShape, MatteMode, Rgb, ShapeMask,
15 TextStyle,
16};
17use alpha2_render::{
18 BlendMode, CacheIntent, Color, EffectId, EffectParams, EffectResources, EffectRuntime,
19 FrameSubmitTimings, GENERATOR_EFFECT_ID, GraphError, GraphLayer, LayerTransform,
20 MAX_SHAPE_MASKS, Nv12PlaneTargets, ParamSpec, ParamValue, PendingSourceWork, RenderCacheKey,
21 RenderError, RenderGraph, RenderNodeId, RenderNodeKind, RenderSize, RenderedTexture, Renderer,
22 SHAPE_MASK_EFFECT_ID, SHAPE_MASK_PARAMS, SourceView, TRACK_MATTE_EFFECT_ID, Yuv420pReadback,
23};
24use alpha2_time::MediaTime;
25
26use crate::TextTexture;
27use std::collections::{HashMap, HashSet};
28use std::time::Duration;
29use web_time::Instant;
30
31pub trait FrameProvider {
39 fn texture_for(&self, source: &SourceId, time: MediaTime) -> Option<SourceView<'_>>;
52
53 fn source_key(&self, _source: &SourceId, _time: MediaTime) -> Option<RenderCacheKey> {
60 None
61 }
62
63 fn pending_source_work(
66 &self,
67 _source: &SourceId,
68 _time: MediaTime,
69 ) -> Option<&PendingSourceWork> {
70 None
71 }
72
73 fn text(&self, _key: u64) -> Option<TextTexture<'_>> {
78 None
79 }
80}
81
82#[derive(Clone, Debug, Default, PartialEq, Eq)]
84pub struct RenderReport {
85 pub missing_sources: Vec<SourceId>,
87}
88
89impl RenderReport {
90 #[must_use]
92 pub fn is_complete(&self) -> bool {
93 self.missing_sources.is_empty()
94 }
95}
96
97#[derive(Debug)]
99pub struct FrameRender {
100 pub report: RenderReport,
102 pub texture: Option<RenderedTexture>,
104}
105
106impl FrameRender {
107 #[must_use]
109 pub fn is_complete(&self) -> bool {
110 self.texture.is_some()
111 }
112}
113
114#[derive(Debug)]
116pub struct FrameYuv420pRender {
117 pub report: RenderReport,
119 pub readback: Option<Yuv420pReadback>,
121}
122
123impl FrameYuv420pRender {
124 #[must_use]
126 pub fn is_complete(&self) -> bool {
127 self.readback.is_some()
128 }
129}
130
131#[derive(Debug)]
133#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
134pub struct FrameNv12Render {
135 pub report: RenderReport,
137 pub submitted: bool,
139}
140
141#[derive(Debug)]
143pub struct FrameGraph<'a> {
144 pub report: RenderReport,
146 pub graph: Option<RenderGraph<'a>>,
148 pub pending: Vec<&'a PendingSourceWork>,
151}
152
153pub trait FrameRenderer {
157 #[must_use]
163 fn inspect_frame(&self, frame: &FrameDesc, frames: &dyn FrameProvider) -> RenderReport;
164
165 fn build_frame_graph<'a>(
176 &self,
177 size: RenderSize,
178 frame: &'a FrameDesc<'a>,
179 frames: &'a dyn FrameProvider,
180 ) -> Result<FrameGraph<'a>, RenderError>;
181
182 fn render_frame(
192 &self,
193 size: RenderSize,
194 frame: &FrameDesc,
195 frames: &dyn FrameProvider,
196 ) -> Result<FrameRender, RenderError>;
197
198 fn render_frame_yuv420p(
207 &self,
208 size: RenderSize,
209 frame: &FrameDesc,
210 frames: &dyn FrameProvider,
211 encoding: YuvEncoding,
212 ) -> Result<FrameYuv420pRender, RenderError>;
213
214 fn render_frame_yuv420p_timed(
224 &self,
225 size: RenderSize,
226 frame: &FrameDesc,
227 frames: &dyn FrameProvider,
228 encoding: YuvEncoding,
229 timings: &mut FrameSubmitTimings,
230 ) -> Result<FrameYuv420pRender, RenderError>;
231
232 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
243 fn render_frame_nv12_timed(
244 &self,
245 size: RenderSize,
246 frame: &FrameDesc,
247 frames: &dyn FrameProvider,
248 planes: Nv12PlaneTargets<'_>,
249 encoding: YuvEncoding,
250 timings: &mut FrameSubmitTimings,
251 ) -> Result<FrameNv12Render, RenderError>;
252}
253
254impl FrameRenderer for Renderer {
255 fn inspect_frame(&self, frame: &FrameDesc, frames: &dyn FrameProvider) -> RenderReport {
256 available_textures(frame, frames).0
257 }
258
259 fn build_frame_graph<'a>(
260 &self,
261 size: RenderSize,
262 frame: &'a FrameDesc<'a>,
263 frames: &'a dyn FrameProvider,
264 ) -> Result<FrameGraph<'a>, RenderError> {
265 validate_project_effects(self, frame)?;
266 let (report, mut available) = available_textures(frame, frames);
267 if !report.is_complete() {
268 return Ok(FrameGraph {
269 report,
270 graph: None,
271 pending: Vec::new(),
272 });
273 }
274 attach_texts(&mut available, frames, size);
275 let captions = caption_layers(frame, frames, size)?;
276 let graph = frame_graph(self, size, frame, &available, &captions)?;
277 Ok(FrameGraph {
278 report,
279 graph: Some(graph),
280 pending: pending_source_work(&available, frames),
281 })
282 }
283
284 fn render_frame(
285 &self,
286 size: RenderSize,
287 frame: &FrameDesc,
288 frames: &dyn FrameProvider,
289 ) -> Result<FrameRender, RenderError> {
290 let FrameGraph {
291 report,
292 graph,
293 pending,
294 } = self.build_frame_graph(size, frame, frames)?;
295 let texture = match graph {
296 Some(graph) => Some(self.render_graph(&graph, &pending)?),
297 None => None,
298 };
299 Ok(FrameRender { report, texture })
300 }
301
302 fn render_frame_yuv420p(
303 &self,
304 size: RenderSize,
305 frame: &FrameDesc,
306 frames: &dyn FrameProvider,
307 encoding: YuvEncoding,
308 ) -> Result<FrameYuv420pRender, RenderError> {
309 render_frame_yuv420p_measured(self, size, frame, frames, encoding, None)
310 }
311
312 fn render_frame_yuv420p_timed(
313 &self,
314 size: RenderSize,
315 frame: &FrameDesc,
316 frames: &dyn FrameProvider,
317 encoding: YuvEncoding,
318 timings: &mut FrameSubmitTimings,
319 ) -> Result<FrameYuv420pRender, RenderError> {
320 render_frame_yuv420p_measured(self, size, frame, frames, encoding, Some(timings))
321 }
322
323 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
324 fn render_frame_nv12_timed(
325 &self,
326 size: RenderSize,
327 frame: &FrameDesc,
328 frames: &dyn FrameProvider,
329 planes: Nv12PlaneTargets<'_>,
330 encoding: YuvEncoding,
331 timings: &mut FrameSubmitTimings,
332 ) -> Result<FrameNv12Render, RenderError> {
333 let mut clock = StageClock::start(true);
334 let mut collected = FrameSubmitTimings::default();
335 let FrameGraph {
336 report,
337 graph,
338 pending,
339 } = self.build_frame_graph(size, frame, frames)?;
340 collected.graph_build = clock.lap();
341 let submitted = graph.is_some();
342 if let Some(graph) = graph {
343 self.render_graph_nv12_timed(&graph, &pending, planes, encoding, &mut collected)?;
344 }
345 *timings = collected;
346 Ok(FrameNv12Render { report, submitted })
347 }
348}
349
350fn render_frame_yuv420p_measured(
351 renderer: &Renderer,
352 size: RenderSize,
353 frame: &FrameDesc,
354 frames: &dyn FrameProvider,
355 encoding: YuvEncoding,
356 timings: Option<&mut FrameSubmitTimings>,
357) -> Result<FrameYuv420pRender, RenderError> {
358 let measure = timings.is_some();
359 let mut clock = StageClock::start(measure);
360 let mut collected = FrameSubmitTimings::default();
361 let FrameGraph {
362 report,
363 graph,
364 pending,
365 } = renderer.build_frame_graph(size, frame, frames)?;
366 collected.graph_build = clock.lap();
367 let readback = match graph {
368 Some(graph) if measure => {
369 Some(renderer.render_graph_yuv420p_timed(&graph, &pending, encoding, &mut collected)?)
370 }
371 Some(graph) => Some(renderer.render_graph_yuv420p(&graph, &pending, encoding)?),
372 None => None,
373 };
374 if let Some(timings) = timings {
375 *timings = collected;
376 }
377 Ok(FrameYuv420pRender { report, readback })
378}
379
380#[derive(Copy, Clone)]
382struct StageClock(Option<Instant>);
383
384impl StageClock {
385 fn start(enabled: bool) -> Self {
386 Self(enabled.then(Instant::now))
387 }
388
389 fn lap(&mut self) -> Duration {
391 let Some(mark) = self.0 else {
392 return Duration::ZERO;
393 };
394 let now = Instant::now();
395 self.0 = Some(now);
396 now.duration_since(mark)
397 }
398}
399
400fn validate_project_effects(renderer: &Renderer, frame: &FrameDesc) -> Result<(), RenderError> {
401 require_effects(renderer, frame_effects(frame)).map_err(|_| RenderError::UnknownEffect)
402}
403
404fn frame_effects<'a>(frame: &'a FrameDesc) -> impl Iterator<Item = (&'a str, u32)> {
407 let composition_effects = frame
408 .visual
409 .iter()
410 .flat_map(|clip| clip.composition.into_iter().chain(clip.source_composition))
411 .flat_map(|composition| composition.plan().effects())
412 .map(|effect| (effect.id.as_str(), effect.version));
413 let transition_effects = frame
414 .transitions
415 .iter()
416 .map(|transition| (transition.effect.id(), transition.effect.version()));
417 composition_effects.chain(transition_effects)
418}
419
420pub(crate) fn check_frame_effects(
427 renderer: &Renderer,
428 frame: &FrameDesc,
429) -> Result<(), crate::EngineError> {
430 require_effects(renderer, frame_effects(frame))
431}
432
433pub(crate) fn require_effects<'a>(
435 renderer: &Renderer,
436 effects: impl IntoIterator<Item = (&'a str, u32)>,
437) -> Result<(), crate::EngineError> {
438 for (effect, version) in effects {
439 if renderer.project_effect_id(effect, version).is_none() {
440 return Err(crate::EngineError::UnregisteredEffect {
441 effect: effect.to_owned(),
442 version,
443 });
444 }
445 }
446 Ok(())
447}
448
449fn pending_source_work<'a>(
453 available: &[(&ActiveClip, ClipFrames<'_>)],
454 frames: &'a dyn FrameProvider,
455) -> Vec<&'a PendingSourceWork> {
456 let mut work = Vec::new();
457 for (clip, textures) in available {
458 if textures.source.is_some() {
459 work.extend(frames.pending_source_work(clip.source_id, clip.source_time));
460 }
461 for composition in clip.composition.iter().chain(&clip.source_composition) {
462 for (_, source, time) in composition.media() {
463 work.extend(frames.pending_source_work(source, time));
464 }
465 }
466 }
467 work
468}
469
470#[derive(Copy, Clone, Debug)]
472struct Frame<'t> {
473 view: SourceView<'t>,
474 key: Option<RenderCacheKey>,
475}
476
477impl<'t> Frame<'t> {
478 fn add_to(self, graph: &mut RenderGraph<'t>) -> RenderNodeId {
480 match self.key {
481 Some(key) => graph.add_source_with_key(self.view, key),
482 None => graph.add_source(self.view),
483 }
484 }
485}
486
487#[derive(Copy, Clone, Debug)]
490struct PlacedText<'t> {
491 texture: TextTexture<'t>,
492 visual: LayerTransform,
493}
494
495impl<'t> PlacedText<'t> {
496 fn add_to(self, graph: &mut RenderGraph<'t>) -> Result<RenderNodeId, RenderError> {
499 let source = match self.texture.cache_key {
500 Some(key) => graph.add_source_with_key(self.texture.texture, key),
501 None => graph.add_source(self.texture.texture),
502 };
503 graph
504 .add_composite(vec![GraphLayer {
505 source,
506 visual: self.visual,
507 blend_mode: BlendMode::Normal,
508 }])
509 .map_err(GraphError::into_render_error)
510 }
511}
512
513#[derive(Copy, Clone, Debug)]
515enum StepFrame<'t> {
516 Media(Frame<'t>),
518 Text(PlacedText<'t>),
520}
521
522#[derive(Clone, Debug, Default)]
524struct ClipFrames<'t> {
525 layer: usize,
530 source: Option<Frame<'t>>,
533 text: Option<PlacedText<'t>>,
535 media: Vec<Option<StepFrame<'t>>>,
538 source_media: Vec<Option<StepFrame<'t>>>,
540}
541
542fn available_textures<'frame, 'eval, 'texture>(
545 frame: &'frame FrameDesc<'eval>,
546 frames: &'texture dyn FrameProvider,
547) -> (
548 RenderReport,
549 Vec<(&'frame ActiveClip<'eval>, ClipFrames<'texture>)>,
550) {
551 let mut report = RenderReport::default();
552 let mut available = Vec::with_capacity(frame.visual.len());
553 let mut lookup = |source: &SourceId, time| {
554 let texture = frames.texture_for(source, time);
555 if texture.is_none() && !report.missing_sources.contains(source) {
556 report.missing_sources.push(source.clone());
557 }
558 texture.map(|view| Frame {
559 view,
560 key: frames.source_key(source, time),
561 })
562 };
563 for (layer, clip) in frame.visual.iter().enumerate() {
565 if !frame.draws(clip) {
566 continue;
567 }
568 let mut textures = ClipFrames {
569 layer,
570 ..ClipFrames::default()
571 };
572 let mut complete = true;
573 if clip.needs_source() {
574 textures.source = lookup(clip.source_id, clip.source_time);
575 complete &= textures.source.is_some();
576 }
577 for (composition, media) in [
578 (clip.composition, &mut textures.media),
579 (clip.source_composition, &mut textures.source_media),
580 ] {
581 let Some(composition) = composition else {
582 continue;
583 };
584 *media = vec![None; composition.plan().steps.len()];
585 for (step, source, time) in composition.media() {
586 media[step] = lookup(source, time).map(StepFrame::Media);
587 complete &= media[step].is_some();
588 }
589 }
590 if complete {
591 available.push((clip, textures));
592 }
593 }
594 (report, available)
595}
596
597fn attach_texts<'t>(
601 available: &mut [(&ActiveClip<'_>, ClipFrames<'t>)],
602 frames: &'t dyn FrameProvider,
603 size: RenderSize,
604) {
605 let lookup = |generator: &Generator| match generator {
606 Generator::Text { text, style } => {
607 frames
608 .text(crate::text_key(text, style, size))
609 .map(|texture| PlacedText {
610 texture,
611 visual: text_placement(texture, style, size),
612 })
613 }
614 _ => None,
615 };
616 for (clip, textures) in available {
617 textures.text = clip.generator.and_then(lookup);
618 for (composition, media) in [
619 (clip.composition, &mut textures.media),
620 (clip.source_composition, &mut textures.source_media),
621 ] {
622 let Some(composition) = composition else {
623 continue;
624 };
625 for (step, slot) in composition.plan().steps.iter().zip(media.iter_mut()) {
626 if let StepOp::Generator(generator) = &step.op {
627 *slot = lookup(generator).map(StepFrame::Text);
628 }
629 }
630 }
631 }
632}
633
634fn text_placement(texture: TextTexture<'_>, style: &TextStyle, size: RenderSize) -> LayerTransform {
638 #[allow(clippy::cast_precision_loss)]
639 let scale = [
640 texture.width as f32 / size.width as f32,
641 texture.height as f32 / size.height as f32,
642 ];
643 LayerTransform {
644 position: [style.position.x, style.position.y],
645 anchor: [
646 box_anchor(style.anchor.x, texture.width, texture.margin[0]),
647 box_anchor(style.anchor.y, texture.height, texture.margin[1]),
648 ],
649 scale,
650 rotation_degrees: 0.0,
651 opacity: 1.0,
652 }
653}
654
655struct CaptionLayer<'a> {
657 layers_below: usize,
659 texture: TextTexture<'a>,
660 visual: LayerTransform,
661}
662
663fn caption_graph_layer<'a>(graph: &mut RenderGraph<'a>, caption: &CaptionLayer<'a>) -> GraphLayer {
666 let source = match caption.texture.cache_key {
667 Some(key) => graph.add_source_with_key(caption.texture.texture, key),
668 None => graph.add_source(caption.texture.texture),
669 };
670 GraphLayer {
671 source,
672 visual: caption.visual,
673 blend_mode: BlendMode::Normal,
674 }
675}
676
677fn caption_layers<'a>(
680 frame: &FrameDesc,
681 frames: &'a dyn FrameProvider,
682 size: RenderSize,
683) -> Result<Vec<CaptionLayer<'a>>, RenderError> {
684 frame
685 .captions
686 .iter()
687 .map(|caption| {
688 let texture = frames
689 .text(crate::text_key(caption.text, caption.style, size))
690 .ok_or(RenderError::MissingText)?;
691 Ok(CaptionLayer {
692 layers_below: caption.layers_below,
693 visual: text_placement(texture, caption.style, size),
694 texture,
695 })
696 })
697 .collect()
698}
699
700#[allow(clippy::cast_precision_loss)]
704fn box_anchor(anchor: f32, extent: u32, margin: u32) -> f32 {
705 if margin == 0 {
706 return anchor;
707 }
708 let inner = extent.saturating_sub(margin * 2) as f32;
709 (margin as f32 + anchor * inner) / extent as f32
710}
711
712fn frame_runtime(frame: &FrameDesc, size: RenderSize) -> EffectRuntime {
714 #[allow(clippy::cast_precision_loss)]
715 let ratio = size.width as f32 / size.height as f32;
716 EffectRuntime {
717 time: frame.time,
718 ratio,
719 ..EffectRuntime::default()
720 }
721}
722
723fn frame_graph<'a>(
724 renderer: &Renderer,
725 size: RenderSize,
726 frame: &'a FrameDesc<'a>,
727 available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
728 captions: &[CaptionLayer<'a>],
729) -> Result<RenderGraph<'a>, RenderError> {
730 let mut graph = RenderGraph::new(size);
731 graph.set_blend_space(match frame.blend_space {
732 alpha2_model::BlendSpace::LinearLight => alpha2_render::BlendSpace::LinearLight,
733 alpha2_model::BlendSpace::SrgbEncoded => alpha2_render::BlendSpace::SrgbEncoded,
734 });
735 let mut nodes = HashMap::with_capacity(available.len());
736 let mut inputs = Vec::with_capacity(available.len() + frame.transitions.len());
737 let mut coverages = HashMap::new();
739 let runtime = frame_runtime(frame, size);
740 for (clip, textures) in available {
741 if clip.is_matte {
742 continue;
743 }
744 let mut layer = clip_layer(renderer, &mut graph, clip, textures, frame.time, runtime)?;
745 if let Some(matte) = clip.matte {
746 layer = apply_matte(
747 renderer,
748 &mut graph,
749 layer,
750 matte,
751 available,
752 (frame.time, runtime),
753 &mut coverages,
754 )?;
755 }
756 nodes.insert(clip.clip_id, inputs.len());
757 inputs.push(FrameInput {
758 kind: FrameInputKind::Layer(layer),
759 uses_backdrop: clip.blend_mode != ClipBlendMode::Normal,
760 });
761 }
762 let background = background_color(frame.background);
763 let mut suppressed = HashSet::new();
764 for transition in &frame.transitions {
765 let from = *nodes
766 .get(transition.from_clip)
767 .ok_or(RenderError::InvalidGraph)?;
768 let to = *nodes
769 .get(transition.to_clip)
770 .ok_or(RenderError::InvalidGraph)?;
771 let output = inputs.len();
772 inputs.push(FrameInput {
773 kind: FrameInputKind::Transition {
774 from,
775 to,
776 transition,
777 },
778 uses_backdrop: inputs[from].uses_backdrop || inputs[to].uses_backdrop,
779 });
780 nodes.insert(transition.to_clip, output);
781 suppressed.insert(transition.from_clip);
782 }
783 let mut layers = Vec::with_capacity(available.len() + captions.len());
784 let mut captions = captions.iter().peekable();
785 for (clip, textures) in available {
786 while let Some(caption) = captions.next_if(|caption| caption.layers_below <= textures.layer)
789 {
790 layers.push(caption_graph_layer(&mut graph, caption));
791 }
792 if clip.is_matte || suppressed.contains(clip.clip_id) {
793 continue;
794 }
795 let index = *nodes.get(clip.clip_id).ok_or(RenderError::InvalidGraph)?;
796 if let FrameInputKind::Layer(layer) = inputs[index].kind {
797 layers.push(layer);
798 continue;
799 }
800 let uses_backdrop = inputs[index].uses_backdrop;
804 let backdrop = if uses_backdrop {
805 layers.as_slice()
806 } else {
807 &[]
808 };
809 let source = compile_frame_input(
812 &mut graph,
813 &inputs,
814 index,
815 backdrop,
816 uses_backdrop.then_some(background),
817 runtime,
818 renderer,
819 )?;
820 if uses_backdrop {
821 layers.clear();
822 }
823 layers.push(identity_layer(source));
824 }
825 for caption in captions {
826 layers.push(caption_graph_layer(&mut graph, caption));
827 }
828 let output = graph
829 .add_composite_over(layers, background)
830 .map_err(GraphError::into_render_error)?;
831 graph
832 .set_output(output)
833 .map_err(GraphError::into_render_error)?;
834 graph
835 .set_cache_intent(output, CacheIntent::Auto)
836 .map_err(GraphError::into_render_error)?;
837 Ok(graph)
838}
839
840const fn cache_intent(intent: alpha2_model::CacheIntent) -> CacheIntent {
841 match intent {
842 alpha2_model::CacheIntent::Auto => CacheIntent::Auto,
843 alpha2_model::CacheIntent::Pin => CacheIntent::Pin,
844 alpha2_model::CacheIntent::Bypass => CacheIntent::Bypass,
845 }
846}
847
848fn apply_matte<'a, 'c>(
850 renderer: &Renderer,
851 graph: &mut RenderGraph<'a>,
852 mut layer: GraphLayer,
853 matte: &'c alpha2_model::Matte,
854 available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
855 (time, runtime): (MediaTime, EffectRuntime),
856 coverages: &mut HashMap<&'c alpha2_model::ClipId, RenderNodeId>,
857) -> Result<GraphLayer, RenderError> {
858 let source = available
859 .iter()
860 .find(|(candidate, _)| candidate.clip_id == &matte.source);
861 match source {
862 Some((source, textures)) => {
863 let coverage = if let Some(coverage) = coverages.get(&matte.source) {
864 *coverage
865 } else {
866 let matte_layer = clip_layer(renderer, graph, source, textures, time, runtime)?;
867 let coverage = alone(graph, matte_layer)?;
868 coverages.insert(&matte.source, coverage);
869 coverage
870 };
871 layer = matted_layer(graph, layer, coverage, matte.mode, runtime)?;
872 }
873 None if is_inverted(matte.mode) => {}
876 None => layer.visual.opacity = 0.0,
877 }
878 Ok(layer)
879}
880
881fn background_color(background: Rgb) -> Color {
883 Color::rgba(
884 f64::from(background.red),
885 f64::from(background.green),
886 f64::from(background.blue),
887 1.0,
888 )
889}
890
891struct FrameInput<'a> {
892 kind: FrameInputKind<'a>,
893 uses_backdrop: bool,
894}
895
896fn clip_layer<'a>(
897 renderer: &Renderer,
898 graph: &mut RenderGraph<'a>,
899 clip: &'a ActiveClip<'a>,
900 textures: &ClipFrames<'a>,
901 time: MediaTime,
902 runtime: EffectRuntime,
903) -> Result<GraphLayer, RenderError> {
904 let mut source = match clip.composition {
905 None => clip_picture(renderer, graph, clip, textures, runtime)?,
906 Some(composition) => plan_node(
907 renderer,
908 graph,
909 composition,
910 &textures.media,
911 Some((clip, textures)),
912 runtime,
913 )?,
914 };
915 if !clip.masks.is_empty() {
916 source = graph
917 .add_effect_with_resources(
918 EffectId::from_static(SHAPE_MASK_EFFECT_ID, 1),
919 source,
920 mask_params(clip.masks, time),
921 EffectResources::new(),
922 timeless(runtime),
923 )
924 .map_err(GraphError::into_render_error)?;
925 }
926 let intent = cache_intent(clip.cache);
930 if intent == CacheIntent::Bypass || graph.node_kind(source) != Some(RenderNodeKind::Source) {
931 graph
932 .set_cache_intent(source, intent)
933 .map_err(GraphError::into_render_error)?;
934 }
935 Ok(GraphLayer {
936 source,
937 visual: layer_transform(clip.visual.ok_or(RenderError::InvalidVisual)?),
938 blend_mode: blend_mode(clip.blend_mode),
939 })
940}
941
942fn clip_picture<'a>(
945 renderer: &Renderer,
946 graph: &mut RenderGraph<'a>,
947 clip: &'a ActiveClip<'a>,
948 textures: &ClipFrames<'a>,
949 runtime: EffectRuntime,
950) -> Result<RenderNodeId, RenderError> {
951 if let Some(source) = clip.source_composition {
952 return plan_node(
954 renderer,
955 graph,
956 source,
957 &textures.source_media,
958 None,
959 runtime,
960 );
961 }
962 match (clip.generator, textures.source) {
963 (Some(Generator::Text { .. }), _) => {
964 textures.text.ok_or(RenderError::MissingText)?.add_to(graph)
965 }
966 (Some(generator), _) => Ok(generator_node(graph, generator, runtime)),
967 (None, Some(frame)) => Ok(frame.add_to(graph)),
968 (None, None) => Err(RenderError::InvalidGraph),
969 }
970}
971
972fn generator_node(
975 graph: &mut RenderGraph<'_>,
976 generator: &alpha2_model::Generator,
977 runtime: EffectRuntime,
978) -> RenderNodeId {
979 graph.add_generator(
980 EffectId::from_static(GENERATOR_EFFECT_ID, 1),
981 crate::generated::generator_params(generator),
982 timeless(runtime),
983 )
984}
985
986fn plan_node<'a>(
990 renderer: &Renderer,
991 graph: &mut RenderGraph<'a>,
992 composition: ActiveComposition<'a>,
993 media: &[Option<StepFrame<'a>>],
994 clip: Option<(&'a ActiveClip<'a>, &ClipFrames<'a>)>,
995 runtime: EffectRuntime,
996) -> Result<RenderNodeId, RenderError> {
997 let plan = composition.plan();
998 let time = composition.time();
999 let mut built: Vec<RenderNodeId> = Vec::with_capacity(plan.steps.len());
1000 let mut picture = None;
1001 for (index, step) in plan.steps.iter().enumerate() {
1002 let at = step.time(time);
1003 let step_runtime = EffectRuntime {
1004 time: at,
1005 ..runtime
1006 };
1007 let node = match &step.op {
1008 StepOp::Clip => {
1010 if let Some(node) = picture {
1011 node
1012 } else {
1013 let (clip, textures) = clip.ok_or(RenderError::InvalidGraph)?;
1014 *picture.insert(clip_picture(renderer, graph, clip, textures, runtime)?)
1015 }
1016 }
1017 StepOp::Media { .. } => match media.get(index).copied().flatten() {
1018 Some(StepFrame::Media(frame)) => frame.add_to(graph),
1019 _ => return Err(RenderError::InvalidGraph),
1020 },
1021 StepOp::Generator(Generator::Text { .. }) => {
1022 match media.get(index).copied().flatten() {
1023 Some(StepFrame::Text(text)) => text.add_to(graph)?,
1024 _ => return Err(RenderError::MissingText),
1025 }
1026 }
1027 op => step_node(renderer, graph, op, &built, at, step_runtime)?,
1028 };
1029 built.push(node);
1030 }
1031 step_input(&built, plan.output)
1032}
1033
1034fn step_input(built: &[RenderNodeId], step: usize) -> Result<RenderNodeId, RenderError> {
1036 built.get(step).copied().ok_or(RenderError::InvalidGraph)
1037}
1038
1039fn step_node<'a>(
1041 renderer: &Renderer,
1042 graph: &mut RenderGraph<'a>,
1043 op: &'a StepOp,
1044 built: &[RenderNodeId],
1045 time: MediaTime,
1046 runtime: EffectRuntime,
1047) -> Result<RenderNodeId, RenderError> {
1048 let input = |step: usize| step_input(built, step);
1049 let node = match op {
1050 StepOp::Clip | StepOp::Media { .. } => return Err(RenderError::InvalidGraph),
1051 StepOp::Generator(generator) => generator_node(graph, generator, runtime),
1052 StepOp::Effect {
1053 input: from,
1054 effect,
1055 } => {
1056 let id = project_effect_id(renderer, &effect.id, effect.version)?;
1057 let active = ActiveEffect::new(effect, time);
1058 let params = effect_params(renderer, &id, active);
1059 let runtime = effect_runtime(renderer, &id, runtime);
1060 graph.add_effect_with_resources(
1061 id,
1062 input(*from)?,
1063 params,
1064 effect_resources(active),
1065 runtime,
1066 )
1067 }
1068 .map_err(GraphError::into_render_error)?,
1069 StepOp::Transition {
1070 from,
1071 to,
1072 effect,
1073 progress,
1074 } => {
1075 let id = project_effect_id(renderer, &effect.id, effect.version)?;
1076 let active = ActiveEffect::new(effect, time);
1077 let params = effect_params(renderer, &id, active);
1078 let runtime = EffectRuntime {
1079 progress: sample_scalar(progress, time).clamp(0.0, 1.0),
1080 ..effect_runtime(renderer, &id, runtime)
1081 };
1082 graph.add_transition_with_resources(
1083 id,
1084 input(*from)?,
1085 input(*to)?,
1086 params,
1087 effect_resources(active),
1088 runtime,
1089 )
1090 }
1091 .map_err(GraphError::into_render_error)?,
1092 StepOp::Transform {
1093 input: from,
1094 transform,
1095 } => graph
1096 .add_composite(vec![GraphLayer {
1097 source: input(*from)?,
1098 visual: layer_transform(sample_visual(transform, time)),
1099 blend_mode: BlendMode::Normal,
1100 }])
1101 .map_err(GraphError::into_render_error)?,
1102 StepOp::Merge { layers } => {
1103 let layers = layers
1104 .iter()
1105 .map(|layer| {
1106 let mut placed = identity_layer(input(layer.input)?);
1107 placed.visual.opacity = sample_scalar(&layer.opacity, time).clamp(0.0, 1.0);
1108 placed.blend_mode = blend_mode(layer.blend_mode);
1109 Ok(placed)
1110 })
1111 .collect::<Result<Vec<_>, RenderError>>()?;
1112 graph
1113 .add_composite(layers)
1114 .map_err(GraphError::into_render_error)?
1115 }
1116 StepOp::Matte {
1117 input: from,
1118 matte,
1119 mode,
1120 } => matte_node(graph, input(*from)?, input(*matte)?, *mode, runtime)?,
1121 StepOp::ShapeMask { input: from, masks } => graph
1122 .add_effect_with_resources(
1123 EffectId::from_static(SHAPE_MASK_EFFECT_ID, 1),
1124 input(*from)?,
1125 mask_params(masks, time),
1126 EffectResources::new(),
1127 timeless(runtime),
1128 )
1129 .map_err(GraphError::into_render_error)?,
1130 };
1131 Ok(node)
1132}
1133
1134fn mask_params(masks: &[ShapeMask], time: MediaTime) -> EffectParams<'static> {
1138 let shown = masks.len().min(MAX_SHAPE_MASKS);
1139 let mut params = EffectParams::with_capacity(1 + shown * 8);
1140 #[allow(clippy::cast_precision_loss)]
1141 params.insert("count", ParamValue::Float(masks.len() as f32));
1142 for (mask, names) in masks.iter().zip(SHAPE_MASK_PARAMS) {
1143 let center = sample_vec2(&mask.center, time);
1144 let size = sample_vec2(&mask.size, time);
1145 let (ellipse, radius) = match &mask.shape {
1146 MaskShape::Ellipse => (true, 0.0),
1147 MaskShape::Rectangle { corner_radius } => (false, sample_scalar(corner_radius, time)),
1148 };
1149 params.insert(names.center, ParamValue::Vec2([center.x, center.y]));
1150 params.insert(
1151 names.size,
1152 ParamValue::Vec2([size.x.max(0.0), size.y.max(0.0)]),
1153 );
1154 params.insert(
1155 names.rotation,
1156 ParamValue::Float(sample_scalar(&mask.rotation_degrees, time).to_radians()),
1157 );
1158 params.insert(names.radius, ParamValue::Float(radius.max(0.0)));
1159 params.insert(
1160 names.feather,
1161 ParamValue::Float(sample_scalar(&mask.feather, time).max(0.0)),
1162 );
1163 params.insert(names.ellipse, ParamValue::Bool(ellipse));
1164 params.insert(names.invert, ParamValue::Bool(mask.invert));
1165 params.insert(
1166 names.mode,
1167 ParamValue::Enum(match mask.mode {
1168 MaskMode::Add => 0,
1169 MaskMode::Subtract => 1,
1170 MaskMode::Intersect => 2,
1171 }),
1172 );
1173 }
1174 params
1175}
1176
1177const fn is_inverted(mode: MatteMode) -> bool {
1178 matches!(mode, MatteMode::AlphaInverted | MatteMode::LumaInverted)
1179}
1180
1181fn matted_layer(
1185 graph: &mut RenderGraph<'_>,
1186 layer: GraphLayer,
1187 coverage: RenderNodeId,
1188 mode: MatteMode,
1189 runtime: EffectRuntime,
1190) -> Result<GraphLayer, RenderError> {
1191 let picture = alone(graph, layer)?;
1192 let matted = matte_node(graph, picture, coverage, mode, runtime)?;
1193 Ok(GraphLayer {
1194 blend_mode: layer.blend_mode,
1195 ..identity_layer(matted)
1196 })
1197}
1198
1199fn alone(graph: &mut RenderGraph<'_>, layer: GraphLayer) -> Result<RenderNodeId, RenderError> {
1201 graph
1202 .add_composite(vec![GraphLayer {
1203 blend_mode: BlendMode::Normal,
1204 ..layer
1205 }])
1206 .map_err(GraphError::into_render_error)
1207}
1208
1209fn effect_runtime(renderer: &Renderer, effect: &EffectId, runtime: EffectRuntime) -> EffectRuntime {
1215 if renderer.effect_reads_time(effect) {
1216 runtime
1217 } else {
1218 EffectRuntime {
1219 time: MediaTime::ZERO,
1220 ..runtime
1221 }
1222 }
1223}
1224
1225fn timeless(runtime: EffectRuntime) -> EffectRuntime {
1229 EffectRuntime {
1230 ratio: runtime.ratio,
1231 ..EffectRuntime::default()
1232 }
1233}
1234
1235fn matte_node(
1237 graph: &mut RenderGraph<'_>,
1238 picture: RenderNodeId,
1239 coverage: RenderNodeId,
1240 mode: MatteMode,
1241 runtime: EffectRuntime,
1242) -> Result<RenderNodeId, RenderError> {
1243 graph
1244 .add_transition_with_resources(
1245 EffectId::from_static(TRACK_MATTE_EFFECT_ID, 1),
1246 picture,
1247 coverage,
1248 EffectParams::new()
1249 .with(
1250 "luma",
1251 ParamValue::Bool(matches!(mode, MatteMode::Luma | MatteMode::LumaInverted)),
1252 )
1253 .with("inverted", ParamValue::Bool(is_inverted(mode))),
1254 EffectResources::new(),
1255 EffectRuntime {
1257 progress: 0.5,
1258 ..timeless(runtime)
1259 },
1260 )
1261 .map_err(GraphError::into_render_error)
1262}
1263
1264#[derive(Copy, Clone)]
1265enum FrameInputKind<'a> {
1266 Layer(GraphLayer),
1267 Transition {
1268 from: usize,
1269 to: usize,
1270 transition: &'a ActiveTransition<'a>,
1271 },
1272}
1273
1274fn compile_frame_input<'a>(
1275 graph: &mut RenderGraph<'a>,
1276 inputs: &[FrameInput<'a>],
1277 index: usize,
1278 backdrop: &[GraphLayer],
1279 background: Option<Color>,
1280 runtime: EffectRuntime,
1281 renderer: &Renderer,
1282) -> Result<RenderNodeId, RenderError> {
1283 if index >= inputs.len() {
1284 return Err(RenderError::InvalidGraph);
1285 }
1286 let mut compiled = vec![None; inputs.len()];
1287 let mut pending = vec![(index, false)];
1288 while let Some((current, expanded)) = pending.pop() {
1289 if compiled[current].is_some() {
1290 continue;
1291 }
1292 match inputs[current].kind {
1293 FrameInputKind::Layer(layer) => {
1294 let mut layers = Vec::with_capacity(backdrop.len() + 1);
1295 layers.extend_from_slice(backdrop);
1296 layers.push(layer);
1297 let composite = match background {
1298 Some(background) => graph.add_composite_over(layers, background),
1299 None => graph.add_composite(layers),
1300 };
1301 compiled[current] = Some(composite.map_err(GraphError::into_render_error)?);
1302 }
1303 FrameInputKind::Transition {
1304 from,
1305 to,
1306 transition,
1307 } => {
1308 if from >= inputs.len() || to >= inputs.len() {
1309 return Err(RenderError::InvalidGraph);
1310 }
1311 if !expanded {
1312 pending.push((current, true));
1313 if compiled[to].is_none() {
1314 pending.push((to, false));
1315 }
1316 if compiled[from].is_none() {
1317 pending.push((from, false));
1318 }
1319 continue;
1320 }
1321 let from = compiled[from].ok_or(RenderError::InvalidGraph)?;
1322 let to = compiled[to].ok_or(RenderError::InvalidGraph)?;
1323 let id = project_effect_id(
1324 renderer,
1325 transition.effect.id(),
1326 transition.effect.version(),
1327 )?;
1328 let params = effect_params(renderer, &id, transition.effect);
1329 let runtime = EffectRuntime {
1330 progress: transition.progress,
1331 ..effect_runtime(renderer, &id, runtime)
1332 };
1333 compiled[current] = Some(
1334 graph
1335 .add_transition_with_resources(
1336 id,
1337 from,
1338 to,
1339 params,
1340 effect_resources(transition.effect),
1341 runtime,
1342 )
1343 .map_err(GraphError::into_render_error)?,
1344 );
1345 }
1346 }
1347 }
1348 compiled[index].ok_or(RenderError::InvalidGraph)
1349}
1350
1351fn identity_layer(source: RenderNodeId) -> GraphLayer {
1352 GraphLayer {
1353 source,
1354 visual: LayerTransform {
1355 position: [0.5, 0.5],
1356 anchor: [0.5, 0.5],
1357 scale: [1.0, 1.0],
1358 rotation_degrees: 0.0,
1359 opacity: 1.0,
1360 },
1361 blend_mode: BlendMode::Normal,
1362 }
1363}
1364
1365const fn layer_transform(visual: EvaluatedVisual) -> LayerTransform {
1368 LayerTransform {
1369 position: [visual.position.x, visual.position.y],
1370 anchor: [visual.anchor.x, visual.anchor.y],
1371 scale: [visual.scale.x, visual.scale.y],
1372 rotation_degrees: visual.rotation_degrees,
1373 opacity: visual.opacity,
1374 }
1375}
1376
1377fn effect_params<'a>(
1385 renderer: &Renderer,
1386 id: &EffectId,
1387 effect: ActiveEffect<'a>,
1388) -> EffectParams<'a> {
1389 let mut params = EffectParams::with_capacity(effect.authored().params.len());
1390 for (name, sample) in effect.params() {
1391 let mut value = param_value(sample.value);
1392 if sample.interpolated
1393 && let Some(spec) = renderer
1394 .effect_definition(id)
1395 .and_then(|definition| definition.params.iter().find(|spec| spec.name == name))
1396 {
1397 value = clamp_to_range(spec, value);
1398 }
1399 params.insert(name, value);
1400 }
1401 params
1402}
1403
1404fn project_effect_id(renderer: &Renderer, id: &str, version: u32) -> Result<EffectId, RenderError> {
1407 renderer
1408 .project_effect_id(id, version)
1409 .cloned()
1410 .ok_or(RenderError::UnknownEffect)
1411}
1412
1413fn clamp_to_range(spec: &ParamSpec, value: ParamValue) -> ParamValue {
1414 let clamp = |value: f32| {
1415 let value = spec.min.map_or(value, |minimum| value.max(minimum));
1416 spec.max.map_or(value, |maximum| value.min(maximum))
1417 };
1418 match value {
1419 ParamValue::Float(value) => ParamValue::Float(clamp(value)),
1420 ParamValue::Vec2([x, y]) => ParamValue::Vec2([clamp(x), clamp(y)]),
1421 ParamValue::Vec4(values) => ParamValue::Vec4(values.map(clamp)),
1422 other => other,
1423 }
1424}
1425
1426fn effect_resources(effect: ActiveEffect<'_>) -> EffectResources {
1427 effect
1428 .resources()
1429 .fold(EffectResources::new(), |resources, (name, id)| {
1430 resources.with(name, id.as_str())
1431 })
1432}
1433
1434const fn param_value(value: EffectValue) -> ParamValue {
1435 match value {
1436 EffectValue::Float(value) => ParamValue::Float(value),
1437 EffectValue::Vec2(value) => ParamValue::Vec2(value),
1438 EffectValue::Vec4(value) => ParamValue::Vec4(value),
1439 EffectValue::Color(value) => ParamValue::Color(value),
1440 EffectValue::Enum(value) => ParamValue::Enum(value),
1441 EffectValue::Bool(value) => ParamValue::Bool(value),
1442 }
1443}
1444
1445const fn blend_mode(mode: ClipBlendMode) -> BlendMode {
1446 match mode {
1447 ClipBlendMode::Normal => BlendMode::Normal,
1448 ClipBlendMode::Multiply => BlendMode::Multiply,
1449 ClipBlendMode::Screen => BlendMode::Screen,
1450 ClipBlendMode::Overlay => BlendMode::Overlay,
1451 ClipBlendMode::Darken => BlendMode::Darken,
1452 ClipBlendMode::Lighten => BlendMode::Lighten,
1453 ClipBlendMode::ColorDodge => BlendMode::ColorDodge,
1454 ClipBlendMode::ColorBurn => BlendMode::ColorBurn,
1455 ClipBlendMode::HardLight => BlendMode::HardLight,
1456 ClipBlendMode::SoftLight => BlendMode::SoftLight,
1457 ClipBlendMode::Difference => BlendMode::Difference,
1458 ClipBlendMode::Exclusion => BlendMode::Exclusion,
1459 ClipBlendMode::Hue => BlendMode::Hue,
1460 ClipBlendMode::Saturation => BlendMode::Saturation,
1461 ClipBlendMode::Color => BlendMode::Color,
1462 ClipBlendMode::Luminosity => BlendMode::Luminosity,
1463 }
1464}
1465
1466#[cfg(test)]
1467mod tests;