1use alpha2_eval::{
7 ActiveClip, ActiveComposition, ActiveEffect, ActiveTransition, EvaluatedVisual, FrameDesc,
8 GeneratorPlan, ShapesPlan, StepOp, sample_scalar, sample_visual,
9};
10use alpha2_media::{PixelLayout, SourceId, YuvEncoding};
11use alpha2_model::{
12 ClipBlendMode, EffectValue, Generator, MaskMode, MatteMode, Rgb, ShapeMask, TextStyle,
13};
14use alpha2_render::{
15 BlendMode, CacheIntent, Color, CpuImage, CpuRenderer, EffectCatalog, EffectId, EffectInputs,
16 EffectParams, EffectResources, EffectRuntime, FrameSubmitTimings, GENERATOR_EFFECT_ID,
17 GraphError, GraphLayer, LayerTransform, MASK_COMBINE_EFFECT_IDS, MASK_COMBINE_NAMES,
18 MAX_SHAPE_MASKS, Nv12PlaneTargets, ParamSpec, ParamValue, PendingSourceWork, PlaneTarget,
19 RenderCacheKey, RenderControls, RenderError, RenderGraph, RenderNodeId, RenderNodeKind,
20 RenderSize, RenderedTexture, Renderer, SourceView, TRACK_MATTE_EFFECT_ID, Yuv420pReadback,
21 Yuv420pTargets,
22};
23use alpha2_time::MediaTime;
24
25use crate::TextTexture;
26use crate::layout::draw::LaidOut;
27use std::collections::{HashMap, HashSet};
28use std::sync::Arc;
29use std::time::Duration;
30use web_time::Instant;
31
32pub trait FrameProvider {
38 fn texture_for(&self, source: &SourceId, time: MediaTime) -> Option<SourceView<'_>>;
46
47 fn source_key(&self, _source: &SourceId, _time: MediaTime) -> Option<RenderCacheKey> {
52 None
53 }
54
55 fn pending_source_work(
58 &self,
59 _source: &SourceId,
60 _time: MediaTime,
61 ) -> Option<&PendingSourceWork> {
62 None
63 }
64
65 fn text(&self, _key: u64) -> Option<TextTexture<'_>> {
68 None
69 }
70
71 fn layout(&self, _key: u128) -> Option<std::sync::Arc<crate::PreparedLayout>> {
74 None
75 }
76}
77
78#[derive(Clone, Debug, Default, PartialEq, Eq)]
80pub struct RenderReport {
81 pub missing_sources: Vec<SourceId>,
83}
84
85impl RenderReport {
86 #[must_use]
88 pub fn is_complete(&self) -> bool {
89 self.missing_sources.is_empty()
90 }
91}
92
93#[derive(Debug)]
95pub struct FrameRender<I = RenderedTexture> {
96 pub report: RenderReport,
98 pub texture: Option<I>,
100}
101
102impl<I> FrameRender<I> {
103 #[must_use]
105 pub fn is_complete(&self) -> bool {
106 self.texture.is_some()
107 }
108}
109
110#[derive(Debug)]
112pub struct FrameYuv420pRender {
113 pub report: RenderReport,
115 pub readback: Option<Yuv420pReadback>,
117}
118
119impl FrameYuv420pRender {
120 #[must_use]
122 pub fn is_complete(&self) -> bool {
123 self.readback.is_some()
124 }
125}
126
127#[derive(Debug)]
129#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
130pub struct FrameNv12Render {
131 pub report: RenderReport,
133 pub submitted: bool,
135}
136
137#[derive(Debug)]
139pub struct FrameGraph<'a> {
140 pub report: RenderReport,
142 pub graph: Option<RenderGraph<'a>>,
144 pub pending: Vec<&'a PendingSourceWork>,
146}
147
148pub trait FrameRenderer: EffectCatalog + RenderControls {
151 type Image;
153
154 #[must_use]
158 fn inspect_frame(&self, frame: &FrameDesc, frames: &dyn FrameProvider) -> RenderReport {
159 available_textures(frame, frames).0
160 }
161
162 fn build_frame_graph<'a>(
170 &self,
171 size: RenderSize,
172 frame: &'a FrameDesc<'a>,
173 frames: &'a dyn FrameProvider,
174 ) -> Result<FrameGraph<'a>, RenderError>;
175
176 fn render_frame_graph(
181 &self,
182 graph: &RenderGraph<'_>,
183 pending: &[&PendingSourceWork],
184 ) -> Result<Self::Image, RenderError>;
185
186 fn render_frame(
193 &self,
194 size: RenderSize,
195 frame: &FrameDesc,
196 frames: &dyn FrameProvider,
197 ) -> Result<FrameRender<Self::Image>, RenderError> {
198 let FrameGraph {
199 report,
200 graph,
201 pending,
202 } = self.build_frame_graph(size, frame, frames)?;
203 let texture = match graph {
204 Some(graph) => Some(self.render_frame_graph(&graph, &pending)?),
205 None => None,
206 };
207 Ok(FrameRender { report, texture })
208 }
209
210 fn render_frame_into_yuv420p(
217 &self,
218 size: RenderSize,
219 frame: &FrameDesc,
220 frames: &dyn FrameProvider,
221 encoding: YuvEncoding,
222 targets: &mut Yuv420pTargets<'_>,
223 ) -> Result<RenderReport, RenderError>;
224
225 fn render_frame_into_planes(
232 &self,
233 size: RenderSize,
234 frame: &FrameDesc,
235 frames: &dyn FrameProvider,
236 encoding: YuvEncoding,
237 layout: PixelLayout,
238 targets: &mut [PlaneTarget<'_>],
239 ) -> Result<RenderReport, RenderError>;
240}
241
242fn yuv420p_targets<'a>(
243 targets: &'a mut [PlaneTarget<'_>],
244) -> Result<Yuv420pTargets<'a>, RenderError> {
245 let [y, u, v] = targets else {
246 return Err(RenderError::TextureReadback(
247 alpha2_render::ReadbackFailure::Layout,
248 ));
249 };
250 Ok(Yuv420pTargets {
251 y: y.reborrow(),
252 u: u.reborrow(),
253 v: v.reborrow(),
254 })
255}
256
257pub trait GpuFrameRenderer: FrameRenderer<Image = RenderedTexture> {
259 fn render_frame_yuv420p(
266 &self,
267 size: RenderSize,
268 frame: &FrameDesc,
269 frames: &dyn FrameProvider,
270 encoding: YuvEncoding,
271 ) -> Result<FrameYuv420pRender, RenderError>;
272
273 fn render_frame_yuv420p_timed(
281 &self,
282 size: RenderSize,
283 frame: &FrameDesc,
284 frames: &dyn FrameProvider,
285 encoding: YuvEncoding,
286 timings: &mut FrameSubmitTimings,
287 ) -> Result<FrameYuv420pRender, RenderError>;
288
289 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
298 fn render_frame_nv12_timed(
299 &self,
300 size: RenderSize,
301 frame: &FrameDesc,
302 frames: &dyn FrameProvider,
303 planes: Nv12PlaneTargets<'_>,
304 encoding: YuvEncoding,
305 timings: &mut FrameSubmitTimings,
306 ) -> Result<FrameNv12Render, RenderError>;
307}
308
309fn build_frame_graph<'a>(
311 catalog: &dyn EffectCatalog,
312 size: RenderSize,
313 frame: &'a FrameDesc<'a>,
314 frames: &'a dyn FrameProvider,
315) -> Result<FrameGraph<'a>, RenderError> {
316 validate_project_effects(catalog, frame)?;
317 let (report, mut available) = available_textures(frame, frames);
318 if !report.is_complete() {
319 return Ok(FrameGraph {
320 report,
321 graph: None,
322 pending: Vec::new(),
323 });
324 }
325 attach_texts(&mut available, frames, size);
326 let captions = caption_layers(frame, frames, size)?;
327 let graph = frame_graph(catalog, size, frame, &available, &captions)?;
328 Ok(FrameGraph {
329 report,
330 graph: Some(graph),
331 pending: pending_source_work(&available, frames),
332 })
333}
334
335impl FrameRenderer for Renderer {
336 type Image = RenderedTexture;
337
338 fn build_frame_graph<'a>(
339 &self,
340 size: RenderSize,
341 frame: &'a FrameDesc<'a>,
342 frames: &'a dyn FrameProvider,
343 ) -> Result<FrameGraph<'a>, RenderError> {
344 build_frame_graph(self, size, frame, frames)
345 }
346
347 fn render_frame_graph(
348 &self,
349 graph: &RenderGraph<'_>,
350 pending: &[&PendingSourceWork],
351 ) -> Result<RenderedTexture, RenderError> {
352 self.render_graph(graph, pending)
353 }
354
355 fn render_frame_into_yuv420p(
356 &self,
357 size: RenderSize,
358 frame: &FrameDesc,
359 frames: &dyn FrameProvider,
360 encoding: YuvEncoding,
361 targets: &mut Yuv420pTargets<'_>,
362 ) -> Result<RenderReport, RenderError> {
363 let render = self.render_frame_yuv420p(size, frame, frames, encoding)?;
364 if let Some(readback) = render.readback {
365 #[cfg(target_arch = "wasm32")]
367 {
368 drop((readback, targets));
369 return Err(RenderError::TextureReadback(
370 alpha2_render::ReadbackFailure::NotMapped,
371 ));
372 }
373 #[cfg(not(target_arch = "wasm32"))]
374 {
375 readback.wait_ready()?;
376 readback.finish_into(targets)?;
377 }
378 }
379 Ok(render.report)
380 }
381
382 fn render_frame_into_planes(
383 &self,
384 size: RenderSize,
385 frame: &FrameDesc,
386 frames: &dyn FrameProvider,
387 encoding: YuvEncoding,
388 layout: PixelLayout,
389 targets: &mut [PlaneTarget<'_>],
390 ) -> Result<RenderReport, RenderError> {
391 if layout.is_yuv420p() {
392 let mut planes = yuv420p_targets(targets)?;
393 return self.render_frame_into_yuv420p(size, frame, frames, encoding, &mut planes);
394 }
395 let render = self.render_frame(size, frame, frames)?;
396 if let Some(texture) = render.texture {
397 let readback = self.begin_layout_readback(&texture, encoding, layout)?;
398 #[cfg(target_arch = "wasm32")]
400 {
401 drop((readback, targets));
402 return Err(RenderError::TextureReadback(
403 alpha2_render::ReadbackFailure::NotMapped,
404 ));
405 }
406 #[cfg(not(target_arch = "wasm32"))]
407 {
408 readback.wait_ready()?;
409 readback.finish_into_planes(targets)?;
410 }
411 }
412 Ok(render.report)
413 }
414}
415
416impl FrameRenderer for CpuRenderer {
417 type Image = Arc<CpuImage>;
418
419 fn build_frame_graph<'a>(
420 &self,
421 size: RenderSize,
422 frame: &'a FrameDesc<'a>,
423 frames: &'a dyn FrameProvider,
424 ) -> Result<FrameGraph<'a>, RenderError> {
425 build_frame_graph(self, size, frame, frames)
426 }
427
428 fn render_frame_graph(
429 &self,
430 graph: &RenderGraph<'_>,
431 _pending: &[&PendingSourceWork],
432 ) -> Result<Arc<CpuImage>, RenderError> {
433 self.execute_graph(graph)
434 .map_err(GraphError::into_render_error)
435 }
436
437 fn render_frame_into_yuv420p(
438 &self,
439 size: RenderSize,
440 frame: &FrameDesc,
441 frames: &dyn FrameProvider,
442 encoding: YuvEncoding,
443 targets: &mut Yuv420pTargets<'_>,
444 ) -> Result<RenderReport, RenderError> {
445 let FrameGraph { report, graph, .. } = self.build_frame_graph(size, frame, frames)?;
446 if let Some(graph) = graph {
447 self.render_graph_into_yuv420p(&graph, encoding, targets)?;
448 }
449 Ok(report)
450 }
451
452 fn render_frame_into_planes(
453 &self,
454 size: RenderSize,
455 frame: &FrameDesc,
456 frames: &dyn FrameProvider,
457 encoding: YuvEncoding,
458 layout: PixelLayout,
459 targets: &mut [PlaneTarget<'_>],
460 ) -> Result<RenderReport, RenderError> {
461 if layout.is_yuv420p() {
462 let mut planes = yuv420p_targets(targets)?;
463 return self.render_frame_into_yuv420p(size, frame, frames, encoding, &mut planes);
464 }
465 let render = self.render_frame(size, frame, frames)?;
466 if let Some(image) = render.texture {
467 alpha2_render::pack_cpu_image(&image, encoding, layout, targets)?;
468 }
469 Ok(render.report)
470 }
471}
472
473impl GpuFrameRenderer for Renderer {
474 fn render_frame_yuv420p(
475 &self,
476 size: RenderSize,
477 frame: &FrameDesc,
478 frames: &dyn FrameProvider,
479 encoding: YuvEncoding,
480 ) -> Result<FrameYuv420pRender, RenderError> {
481 render_frame_yuv420p_measured(self, size, frame, frames, encoding, None)
482 }
483
484 fn render_frame_yuv420p_timed(
485 &self,
486 size: RenderSize,
487 frame: &FrameDesc,
488 frames: &dyn FrameProvider,
489 encoding: YuvEncoding,
490 timings: &mut FrameSubmitTimings,
491 ) -> Result<FrameYuv420pRender, RenderError> {
492 render_frame_yuv420p_measured(self, size, frame, frames, encoding, Some(timings))
493 }
494
495 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
496 fn render_frame_nv12_timed(
497 &self,
498 size: RenderSize,
499 frame: &FrameDesc,
500 frames: &dyn FrameProvider,
501 planes: Nv12PlaneTargets<'_>,
502 encoding: YuvEncoding,
503 timings: &mut FrameSubmitTimings,
504 ) -> Result<FrameNv12Render, RenderError> {
505 let mut clock = StageClock::start(true);
506 let mut collected = FrameSubmitTimings::default();
507 let FrameGraph {
508 report,
509 graph,
510 pending,
511 } = self.build_frame_graph(size, frame, frames)?;
512 collected.graph_build = clock.lap();
513 let submitted = graph.is_some();
514 if let Some(graph) = graph {
515 self.render_graph_nv12_timed(&graph, &pending, planes, encoding, &mut collected)?;
516 }
517 *timings = collected;
518 Ok(FrameNv12Render { report, submitted })
519 }
520}
521
522fn render_frame_yuv420p_measured(
523 renderer: &Renderer,
524 size: RenderSize,
525 frame: &FrameDesc,
526 frames: &dyn FrameProvider,
527 encoding: YuvEncoding,
528 timings: Option<&mut FrameSubmitTimings>,
529) -> Result<FrameYuv420pRender, RenderError> {
530 let measure = timings.is_some();
531 let mut clock = StageClock::start(measure);
532 let mut collected = FrameSubmitTimings::default();
533 let FrameGraph {
534 report,
535 graph,
536 pending,
537 } = renderer.build_frame_graph(size, frame, frames)?;
538 collected.graph_build = clock.lap();
539 let readback = match graph {
540 Some(graph) if measure => {
541 Some(renderer.render_graph_yuv420p_timed(&graph, &pending, encoding, &mut collected)?)
542 }
543 Some(graph) => Some(renderer.render_graph_yuv420p(&graph, &pending, encoding)?),
544 None => None,
545 };
546 if let Some(timings) = timings {
547 *timings = collected;
548 }
549 Ok(FrameYuv420pRender { report, readback })
550}
551
552#[derive(Copy, Clone)]
554struct StageClock(Option<Instant>);
555
556impl StageClock {
557 fn start(enabled: bool) -> Self {
558 Self(enabled.then(Instant::now))
559 }
560
561 fn lap(&mut self) -> Duration {
562 let Some(mark) = self.0 else {
563 return Duration::ZERO;
564 };
565 let now = Instant::now();
566 self.0 = Some(now);
567 now.duration_since(mark)
568 }
569}
570
571fn validate_project_effects(
572 renderer: &dyn EffectCatalog,
573 frame: &FrameDesc,
574) -> Result<(), RenderError> {
575 require_effects(renderer, frame_effects(frame)).map_err(|_| RenderError::UnknownEffect)
576}
577
578fn frame_effects<'a>(frame: &'a FrameDesc) -> impl Iterator<Item = (&'a str, u32)> {
580 let composition_effects = frame
581 .visual
582 .iter()
583 .flat_map(|clip| clip.composition.into_iter().chain(clip.source_composition))
584 .flat_map(|composition| composition.plan().effects())
585 .map(|effect| (effect.id.as_str(), effect.version));
586 let transition_effects = frame
587 .transitions
588 .iter()
589 .map(|transition| (transition.effect.id(), transition.effect.version()));
590 composition_effects.chain(transition_effects)
591}
592
593pub fn check_frame_effects<C: EffectCatalog + ?Sized>(
599 renderer: &C,
600 frame: &FrameDesc,
601) -> Result<(), crate::EngineError> {
602 require_effects(renderer, frame_effects(frame))
603}
604
605pub(crate) fn require_effects<'a, C: EffectCatalog + ?Sized>(
606 renderer: &C,
607 effects: impl IntoIterator<Item = (&'a str, u32)>,
608) -> Result<(), crate::EngineError> {
609 for (effect, version) in effects {
610 if renderer.project_effect_id(effect, version).is_none() {
611 return Err(crate::EngineError::UnregisteredEffect {
612 effect: effect.to_owned(),
613 version,
614 });
615 }
616 }
617 Ok(())
618}
619
620fn pending_source_work<'a>(
621 available: &[(&ActiveClip, ClipFrames<'_>)],
622 frames: &'a dyn FrameProvider,
623) -> Vec<&'a PendingSourceWork> {
624 let mut work = Vec::new();
625 for (clip, textures) in available {
626 if textures.source.is_some() {
627 work.extend(frames.pending_source_work(clip.source_id, clip.source_time));
628 }
629 for composition in clip.composition.iter().chain(&clip.source_composition) {
630 for (_, source, time) in composition.media() {
631 work.extend(frames.pending_source_work(source, time));
632 }
633 }
634 }
635 work
636}
637
638#[derive(Copy, Clone, Debug)]
639struct Frame<'t> {
640 view: SourceView<'t>,
641 key: Option<RenderCacheKey>,
642}
643
644impl<'t> Frame<'t> {
645 fn add_to(self, graph: &mut RenderGraph<'t>) -> RenderNodeId {
646 match self.key {
647 Some(key) => graph.add_source_with_key(self.view, key),
648 None => graph.add_source(self.view),
649 }
650 }
651}
652
653#[derive(Copy, Clone, Debug)]
655struct PlacedText<'t> {
656 texture: TextTexture<'t>,
657 visual: LayerTransform,
658}
659
660impl<'t> PlacedText<'t> {
661 fn add_to(self, graph: &mut RenderGraph<'t>) -> Result<RenderNodeId, RenderError> {
662 let layer = text_layer(graph, self.texture, self.visual)?;
663 graph
664 .add_composite(vec![layer])
665 .map_err(GraphError::into_render_error)
666 }
667}
668
669fn keyed_source<'t>(
670 graph: &mut RenderGraph<'t>,
671 view: SourceView<'t>,
672 key: Option<RenderCacheKey>,
673) -> RenderNodeId {
674 match key {
675 Some(key) => graph.add_source_with_key(view, key),
676 None => graph.add_source(view),
677 }
678}
679
680pub(crate) fn text_layer<'t>(
686 graph: &mut RenderGraph<'t>,
687 texture: TextTexture<'t>,
688 visual: LayerTransform,
689) -> Result<GraphLayer, RenderError> {
690 let glyphs = keyed_source(graph, texture.texture, texture.cache_key);
691 if texture.shadows.is_empty() {
692 return Ok(GraphLayer {
693 source: glyphs,
694 visual,
695 blend_mode: BlendMode::Normal,
696 });
697 }
698 let placed = graph
699 .add_composite(vec![GraphLayer {
700 source: glyphs,
701 visual,
702 blend_mode: BlendMode::Normal,
703 }])
704 .map_err(GraphError::into_render_error)?;
705 let mut layers = Vec::with_capacity(texture.shadows.len() + 2);
706 if let Some((background, key)) = texture.background {
707 layers.push(GraphLayer {
708 source: keyed_source(graph, background, key),
709 visual,
710 blend_mode: BlendMode::Normal,
711 });
712 }
713 for shadow in texture.shadows.iter().rev() {
714 let cast = graph
715 .add_shadow(placed, shadow.offset, shadow.sigma, shadow.color)
716 .map_err(GraphError::into_render_error)?;
717 layers.push(identity_layer(cast));
718 }
719 layers.push(identity_layer(placed));
720 let look = graph
721 .add_composite(layers)
722 .map_err(GraphError::into_render_error)?;
723 Ok(identity_layer(look))
724}
725
726#[derive(Clone, Debug)]
727enum StepFrame<'t> {
728 Media(Frame<'t>),
729 Text(PlacedText<'t>),
730 Layout(LaidOut<'t>),
731}
732
733#[derive(Clone, Debug, Default)]
735struct ClipFrames<'t> {
736 layer: usize,
739 source: Option<Frame<'t>>,
741 text: Option<PlacedText<'t>>,
743 layout: Option<LaidOut<'t>>,
745 media: Vec<Option<StepFrame<'t>>>,
747 source_media: Vec<Option<StepFrame<'t>>>,
749}
750
751fn available_textures<'frame, 'eval, 'texture>(
753 frame: &'frame FrameDesc<'eval>,
754 frames: &'texture dyn FrameProvider,
755) -> (
756 RenderReport,
757 Vec<(&'frame ActiveClip<'eval>, ClipFrames<'texture>)>,
758) {
759 let mut report = RenderReport::default();
760 let mut available = Vec::with_capacity(frame.visual.len());
761 let mut lookup = |source: &SourceId, time| {
762 let texture = frames.texture_for(source, time);
763 if texture.is_none() && !report.missing_sources.contains(source) {
764 report.missing_sources.push(source.clone());
765 }
766 texture.map(|view| Frame {
767 view,
768 key: frames.source_key(source, time),
769 })
770 };
771 for (layer, clip) in frame.visual.iter().enumerate() {
773 if !frame.draws(clip) {
774 continue;
775 }
776 let mut textures = ClipFrames {
777 layer,
778 ..ClipFrames::default()
779 };
780 let mut complete = true;
781 if clip.needs_source() {
782 textures.source = lookup(clip.source_id, clip.source_time);
783 complete &= textures.source.is_some();
784 }
785 for (composition, media) in [
786 (clip.composition, &mut textures.media),
787 (clip.source_composition, &mut textures.source_media),
788 ] {
789 let Some(composition) = composition else {
790 continue;
791 };
792 *media = vec![None; composition.plan().steps.len()];
793 for (step, source, time) in composition.media() {
794 media[step] = lookup(source, time).map(StepFrame::Media);
795 complete &= media[step].is_some();
796 }
797 }
798 if complete {
799 available.push((clip, textures));
800 }
801 }
802 (report, available)
803}
804
805fn attach_texts<'t>(
808 available: &mut [(&ActiveClip<'_>, ClipFrames<'t>)],
809 frames: &'t dyn FrameProvider,
810 size: RenderSize,
811) {
812 let laid_out = |plan: Option<&GeneratorPlan>, time: MediaTime| {
813 let plan = plan?.layout()?;
814 let layout = frames.layout(crate::layout_key(plan.evaluate(time).key, size))?;
815 LaidOut::attach(layout, frames)
816 };
817 let lookup = |generator: &Generator, time: MediaTime| match generator {
818 Generator::Text { text, style } => frames
819 .text(alpha2_layout::animated_text_key(
820 text,
821 style,
822 crate::text::picture(size),
823 time,
824 ))
825 .map(|texture| PlacedText {
826 texture,
827 visual: text_placement(texture, style, size),
828 }),
829 _ => None,
830 };
831 for (clip, textures) in available {
832 textures.text = clip
833 .generator
834 .and_then(|generator| lookup(generator, clip.source_time));
835 textures.layout = laid_out(clip.plan, clip.source_time);
836 for (composition, media) in [
837 (clip.composition, &mut textures.media),
838 (clip.source_composition, &mut textures.source_media),
839 ] {
840 let Some(composition) = composition else {
841 continue;
842 };
843 for (step, slot) in composition.plan().steps.iter().zip(media.iter_mut()) {
844 match &step.op {
845 StepOp::Generator {
846 generator: Generator::Layout { .. },
847 start,
848 plan,
849 } => {
850 let time = MediaTime(
851 step.time(composition.time())
852 .ticks()
853 .saturating_add(start.ticks()),
854 );
855 *slot = laid_out(plan.as_deref(), time).map(StepFrame::Layout);
856 }
857 StepOp::Generator {
858 generator, start, ..
859 } => {
860 let time =
861 crate::text::generator_time(step.time(composition.time()), *start);
862 *slot = lookup(generator, time).map(StepFrame::Text);
863 }
864 _ => {}
865 }
866 }
867 }
868 }
869}
870
871fn text_placement(texture: TextTexture<'_>, style: &TextStyle, size: RenderSize) -> LayerTransform {
873 #[allow(clippy::cast_precision_loss)]
874 let scale = [
875 texture.width as f32 / size.width as f32,
876 texture.height as f32 / size.height as f32,
877 ];
878 LayerTransform {
879 position: [style.position.x, style.position.y],
880 anchor: [
881 box_anchor(style.anchor.x, texture.width, texture.margin[0]),
882 box_anchor(style.anchor.y, texture.height, texture.margin[1]),
883 ],
884 scale,
885 rotation_degrees: 0.0,
886 opacity: 1.0,
887 }
888}
889
890struct CaptionLayer<'a> {
892 layers_below: usize,
894 texture: TextTexture<'a>,
895 visual: LayerTransform,
896}
897
898fn caption_graph_layer<'a>(
899 graph: &mut RenderGraph<'a>,
900 caption: &CaptionLayer<'a>,
901) -> Result<GraphLayer, RenderError> {
902 text_layer(graph, caption.texture, caption.visual)
903}
904
905fn caption_layers<'a>(
906 frame: &FrameDesc,
907 frames: &'a dyn FrameProvider,
908 size: RenderSize,
909) -> Result<Vec<CaptionLayer<'a>>, RenderError> {
910 frame
911 .captions
912 .iter()
913 .map(|caption| {
914 let texture = frames
915 .text(alpha2_layout::animated_text_key(
916 caption.text,
917 caption.style,
918 crate::text::picture(size),
919 caption.local_time,
920 ))
921 .ok_or(RenderError::MissingText)?;
922 Ok(CaptionLayer {
923 layers_below: caption.layers_below,
924 visual: text_placement(texture, caption.style, size),
925 texture,
926 })
927 })
928 .collect()
929}
930
931#[allow(clippy::cast_precision_loss)]
934fn box_anchor(anchor: f32, extent: u32, margin: u32) -> f32 {
935 if margin == 0 {
936 return anchor;
937 }
938 let inner = extent.saturating_sub(margin * 2) as f32;
939 (margin as f32 + anchor * inner) / extent as f32
940}
941
942fn frame_runtime(frame: &FrameDesc, size: RenderSize) -> EffectRuntime {
943 #[allow(clippy::cast_precision_loss)]
944 let ratio = size.width as f32 / size.height as f32;
945 EffectRuntime {
946 time: frame.time,
947 ratio,
948 ..EffectRuntime::default()
949 }
950}
951
952fn frame_graph<'a>(
953 renderer: &dyn EffectCatalog,
954 size: RenderSize,
955 frame: &'a FrameDesc<'a>,
956 available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
957 captions: &[CaptionLayer<'a>],
958) -> Result<RenderGraph<'a>, RenderError> {
959 let mut graph = RenderGraph::new(size);
960 graph.set_blend_space(match frame.blend_space {
961 alpha2_model::BlendSpace::LinearLight => alpha2_render::BlendSpace::LinearLight,
962 alpha2_model::BlendSpace::SrgbEncoded => alpha2_render::BlendSpace::SrgbEncoded,
963 });
964 let mut nodes = HashMap::with_capacity(available.len());
965 let mut inputs = Vec::with_capacity(available.len() + frame.transitions.len());
966 let mut coverages = HashMap::new();
968 let runtime = frame_runtime(frame, size);
969 for (clip, textures) in available {
970 if clip.is_matte {
971 continue;
972 }
973 let mut layer = clip_layer(renderer, &mut graph, clip, textures, frame.time, runtime)?;
974 if let Some(matte) = clip.matte {
975 layer = apply_matte(
976 renderer,
977 &mut graph,
978 layer,
979 matte,
980 available,
981 (frame.time, runtime),
982 &mut coverages,
983 )?;
984 }
985 nodes.insert(clip.clip_id, inputs.len());
986 inputs.push(FrameInput {
987 kind: FrameInputKind::Layer(layer),
988 uses_backdrop: clip.blend_mode != ClipBlendMode::Normal,
989 });
990 }
991 let background = background_color(frame.background);
992 let mut suppressed = HashSet::new();
993 for transition in &frame.transitions {
994 let from = *nodes
995 .get(transition.from_clip)
996 .ok_or(RenderError::InvalidGraph)?;
997 let to = *nodes
998 .get(transition.to_clip)
999 .ok_or(RenderError::InvalidGraph)?;
1000 let output = inputs.len();
1001 inputs.push(FrameInput {
1002 kind: FrameInputKind::Transition {
1003 from,
1004 to,
1005 transition,
1006 },
1007 uses_backdrop: inputs[from].uses_backdrop || inputs[to].uses_backdrop,
1008 });
1009 nodes.insert(transition.to_clip, output);
1010 suppressed.insert(transition.from_clip);
1011 }
1012 let mut layers = Vec::with_capacity(available.len() + captions.len());
1013 let mut captions = captions.iter().peekable();
1014 for (clip, textures) in available {
1015 while let Some(caption) = captions.next_if(|caption| caption.layers_below <= textures.layer)
1017 {
1018 layers.push(caption_graph_layer(&mut graph, caption)?);
1019 }
1020 if clip.is_matte || suppressed.contains(clip.clip_id) {
1021 continue;
1022 }
1023 let index = *nodes.get(clip.clip_id).ok_or(RenderError::InvalidGraph)?;
1024 if let FrameInputKind::Layer(layer) = inputs[index].kind {
1025 layers.push(layer);
1026 continue;
1027 }
1028 let uses_backdrop = inputs[index].uses_backdrop;
1032 let backdrop = if uses_backdrop {
1033 layers.as_slice()
1034 } else {
1035 &[]
1036 };
1037 let source = compile_frame_input(
1039 &mut graph,
1040 &inputs,
1041 index,
1042 backdrop,
1043 uses_backdrop.then_some(background),
1044 runtime,
1045 renderer,
1046 )?;
1047 if uses_backdrop {
1048 layers.clear();
1049 }
1050 layers.push(identity_layer(source));
1051 }
1052 for caption in captions {
1053 layers.push(caption_graph_layer(&mut graph, caption)?);
1054 }
1055 let output = graph
1056 .add_composite_over(layers, background)
1057 .map_err(GraphError::into_render_error)?;
1058 graph
1059 .set_output(output)
1060 .map_err(GraphError::into_render_error)?;
1061 graph
1062 .set_cache_intent(output, CacheIntent::Auto)
1063 .map_err(GraphError::into_render_error)?;
1064 Ok(graph)
1065}
1066
1067const fn cache_intent(intent: alpha2_model::CacheIntent) -> CacheIntent {
1068 match intent {
1069 alpha2_model::CacheIntent::Auto => CacheIntent::Auto,
1070 alpha2_model::CacheIntent::Pin => CacheIntent::Pin,
1071 alpha2_model::CacheIntent::Bypass => CacheIntent::Bypass,
1072 }
1073}
1074
1075fn apply_matte<'a, 'c>(
1077 renderer: &dyn EffectCatalog,
1078 graph: &mut RenderGraph<'a>,
1079 mut layer: GraphLayer,
1080 matte: &'c alpha2_model::Matte,
1081 available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
1082 (time, runtime): (MediaTime, EffectRuntime),
1083 coverages: &mut HashMap<&'c alpha2_model::ClipId, RenderNodeId>,
1084) -> Result<GraphLayer, RenderError> {
1085 let source = available
1086 .iter()
1087 .find(|(candidate, _)| candidate.clip_id == &matte.source);
1088 match source {
1089 Some((source, textures)) => {
1090 let coverage = if let Some(coverage) = coverages.get(&matte.source) {
1091 *coverage
1092 } else {
1093 let matte_layer = clip_layer(renderer, graph, source, textures, time, runtime)?;
1094 let coverage = alone(graph, matte_layer)?;
1095 coverages.insert(&matte.source, coverage);
1096 coverage
1097 };
1098 layer = matted_layer(graph, layer, coverage, matte.mode, runtime)?;
1099 }
1100 None if is_inverted(matte.mode) => {}
1103 None => layer.visual.opacity = 0.0,
1104 }
1105 Ok(layer)
1106}
1107
1108fn background_color(background: Option<Rgb>) -> Color {
1109 background.map_or(Color::rgba(0.0, 0.0, 0.0, 0.0), |background| {
1110 Color::rgba(
1111 f64::from(background.red),
1112 f64::from(background.green),
1113 f64::from(background.blue),
1114 1.0,
1115 )
1116 })
1117}
1118
1119struct FrameInput<'a> {
1120 kind: FrameInputKind<'a>,
1121 uses_backdrop: bool,
1122}
1123
1124fn clip_layer<'a>(
1125 renderer: &dyn EffectCatalog,
1126 graph: &mut RenderGraph<'a>,
1127 clip: &'a ActiveClip<'a>,
1128 textures: &ClipFrames<'a>,
1129 time: MediaTime,
1130 runtime: EffectRuntime,
1131) -> Result<GraphLayer, RenderError> {
1132 let mut source = match clip.composition {
1133 None => clip_picture(renderer, graph, clip, textures, runtime)?,
1134 Some(composition) => plan_node(
1135 renderer,
1136 graph,
1137 composition,
1138 &textures.media,
1139 Some((clip, textures)),
1140 runtime,
1141 )?,
1142 };
1143 if !clip.masks.is_empty() {
1144 source = mask_node(graph, source, clip.masks, time, runtime)?;
1145 }
1146 let intent = cache_intent(clip.cache);
1149 if intent == CacheIntent::Bypass || graph.node_kind(source) != Some(RenderNodeKind::Source) {
1150 graph
1151 .set_cache_intent(source, intent)
1152 .map_err(GraphError::into_render_error)?;
1153 }
1154 Ok(GraphLayer {
1155 source,
1156 visual: layer_transform(clip.visual.ok_or(RenderError::InvalidVisual)?),
1157 blend_mode: blend_mode(clip.blend_mode),
1158 })
1159}
1160
1161fn clip_picture<'a>(
1162 renderer: &dyn EffectCatalog,
1163 graph: &mut RenderGraph<'a>,
1164 clip: &'a ActiveClip<'a>,
1165 textures: &ClipFrames<'a>,
1166 runtime: EffectRuntime,
1167) -> Result<RenderNodeId, RenderError> {
1168 if let Some(source) = clip.source_composition {
1169 return plan_node(
1171 renderer,
1172 graph,
1173 source,
1174 &textures.source_media,
1175 None,
1176 runtime,
1177 );
1178 }
1179 match (clip.generator, textures.source) {
1180 (Some(Generator::Text { .. }), _) => {
1181 textures.text.ok_or(RenderError::MissingText)?.add_to(graph)
1182 }
1183 (Some(Generator::Layout { .. }), _) => textures
1184 .layout
1185 .as_ref()
1186 .ok_or(RenderError::MissingText)?
1187 .add_to(graph),
1188 (Some(Generator::Shapes { .. }), _) => {
1189 let plan = clip
1190 .plan
1191 .and_then(GeneratorPlan::shapes)
1192 .ok_or(RenderError::InvalidGraph)?;
1193 Ok(shapes_node(graph, plan, clip.source_time))
1194 }
1195 (Some(generator), _) => Ok(generator_node(graph, generator, runtime)),
1196 (None, Some(frame)) => Ok(frame.add_to(graph)),
1197 (None, None) => Err(RenderError::InvalidGraph),
1198 }
1199}
1200
1201fn shapes_node(graph: &mut RenderGraph<'_>, plan: &ShapesPlan, time: MediaTime) -> RenderNodeId {
1203 let size = graph.size();
1204 let canvas = alpha2_geometry::CanvasSize::new(size.width, size.height);
1205 let aspect = f64::from(size.width) / f64::from(size.height);
1206 let items = plan.evaluate(time, aspect);
1207 graph.add_vector(std::sync::Arc::new(alpha2_geometry::build_scene(
1208 &items, canvas,
1209 )))
1210}
1211
1212fn generator_node(
1214 graph: &mut RenderGraph<'_>,
1215 generator: &alpha2_model::Generator,
1216 runtime: EffectRuntime,
1217) -> RenderNodeId {
1218 graph.add_generator(
1219 EffectId::from_static(GENERATOR_EFFECT_ID, 1),
1220 crate::generated::generator_params(generator),
1221 timeless(runtime),
1222 )
1223}
1224
1225fn plan_node<'a>(
1228 renderer: &dyn EffectCatalog,
1229 graph: &mut RenderGraph<'a>,
1230 composition: ActiveComposition<'a>,
1231 media: &[Option<StepFrame<'a>>],
1232 clip: Option<(&'a ActiveClip<'a>, &ClipFrames<'a>)>,
1233 runtime: EffectRuntime,
1234) -> Result<RenderNodeId, RenderError> {
1235 let plan = composition.plan();
1236 let time = composition.time();
1237 let mut built: Vec<RenderNodeId> = Vec::with_capacity(plan.steps.len());
1238 let mut picture = None;
1239 for (index, step) in plan.steps.iter().enumerate() {
1240 let at = step.time(time);
1241 let step_runtime = EffectRuntime {
1242 time: at,
1243 ..runtime
1244 };
1245 let node = match &step.op {
1246 StepOp::Clip => {
1248 if let Some(node) = picture {
1249 node
1250 } else {
1251 let (clip, textures) = clip.ok_or(RenderError::InvalidGraph)?;
1252 *picture.insert(clip_picture(renderer, graph, clip, textures, runtime)?)
1253 }
1254 }
1255 StepOp::Media { .. } => match media.get(index).and_then(Option::as_ref) {
1256 Some(StepFrame::Media(frame)) => frame.add_to(graph),
1257 _ => return Err(RenderError::InvalidGraph),
1258 },
1259 StepOp::Generator {
1260 generator: Generator::Text { .. },
1261 ..
1262 } => match media.get(index).and_then(Option::as_ref) {
1263 Some(StepFrame::Text(text)) => text.add_to(graph)?,
1264 _ => return Err(RenderError::MissingText),
1265 },
1266 StepOp::Generator {
1267 generator: Generator::Layout { .. },
1268 ..
1269 } => match media.get(index).and_then(Option::as_ref) {
1270 Some(StepFrame::Layout(layout)) => layout.add_to(graph)?,
1271 _ => return Err(RenderError::MissingText),
1272 },
1273 op => step_node(renderer, graph, op, &built, at, step_runtime)?,
1274 };
1275 built.push(node);
1276 }
1277 step_input(&built, plan.output)
1278}
1279
1280const _: () = assert!(alpha2_model::MAX_EFFECT_INPUTS == alpha2_render::MAX_EFFECT_INPUTS);
1282
1283fn step_input(built: &[RenderNodeId], step: usize) -> Result<RenderNodeId, RenderError> {
1284 built.get(step).copied().ok_or(RenderError::InvalidGraph)
1285}
1286
1287fn step_node<'a>(
1288 renderer: &dyn EffectCatalog,
1289 graph: &mut RenderGraph<'a>,
1290 op: &'a StepOp,
1291 built: &[RenderNodeId],
1292 time: MediaTime,
1293 runtime: EffectRuntime,
1294) -> Result<RenderNodeId, RenderError> {
1295 let input = |step: usize| step_input(built, step);
1296 let node = match op {
1297 StepOp::Clip | StepOp::Media { .. } => return Err(RenderError::InvalidGraph),
1298 StepOp::Generator {
1299 generator: Generator::Shapes { .. },
1300 start,
1301 plan,
1302 } => {
1303 let plan = plan
1304 .as_deref()
1305 .and_then(GeneratorPlan::shapes)
1306 .ok_or(RenderError::InvalidGraph)?;
1307 let at = MediaTime(time.ticks().saturating_add(start.ticks()));
1308 shapes_node(graph, plan, at)
1309 }
1310 StepOp::Generator { generator, .. } => generator_node(graph, generator, runtime),
1311 StepOp::Effect {
1312 input: from,
1313 inputs,
1314 effect,
1315 } => {
1316 let id = project_effect_id(renderer, &effect.id, effect.version)?;
1317 let active = ActiveEffect::new(effect, time);
1318 let params = effect_params(renderer, &id, active);
1319 let runtime = effect_runtime(renderer, &id, runtime);
1320 let mut named = EffectInputs::new();
1321 for (name, step) in inputs {
1322 named.insert(name.as_str(), input(*step)?);
1323 }
1324 graph.add_effect_with_inputs(
1325 id,
1326 input(*from)?,
1327 named,
1328 params,
1329 effect_resources(active),
1330 runtime,
1331 )
1332 }
1333 .map_err(GraphError::into_render_error)?,
1334 StepOp::Transition {
1335 from,
1336 to,
1337 effect,
1338 progress,
1339 } => {
1340 let id = project_effect_id(renderer, &effect.id, effect.version)?;
1341 let active = ActiveEffect::new(effect, time);
1342 let params = effect_params(renderer, &id, active);
1343 let runtime = EffectRuntime {
1344 progress: sample_scalar(progress, time).clamp(0.0, 1.0),
1345 ..effect_runtime(renderer, &id, runtime)
1346 };
1347 graph.add_transition_with_resources(
1348 id,
1349 input(*from)?,
1350 input(*to)?,
1351 params,
1352 effect_resources(active),
1353 runtime,
1354 )
1355 }
1356 .map_err(GraphError::into_render_error)?,
1357 StepOp::Transform {
1358 input: from,
1359 transform,
1360 } => graph
1361 .add_composite(vec![GraphLayer {
1362 source: input(*from)?,
1363 visual: layer_transform(sample_visual(transform, time, graph_aspect(graph))),
1364 blend_mode: BlendMode::Normal,
1365 }])
1366 .map_err(GraphError::into_render_error)?,
1367 StepOp::Merge { layers } => {
1368 let layers = layers
1369 .iter()
1370 .map(|layer| {
1371 let mut placed = identity_layer(input(layer.input)?);
1372 placed.visual.opacity = sample_scalar(&layer.opacity, time).clamp(0.0, 1.0);
1373 placed.blend_mode = blend_mode(layer.blend_mode);
1374 Ok(placed)
1375 })
1376 .collect::<Result<Vec<_>, RenderError>>()?;
1377 graph
1378 .add_composite(layers)
1379 .map_err(GraphError::into_render_error)?
1380 }
1381 StepOp::Matte {
1382 input: from,
1383 matte,
1384 mode,
1385 } => matte_node(graph, input(*from)?, input(*matte)?, *mode, runtime)?,
1386 StepOp::ShapeMask { input: from, masks } => {
1387 mask_node(graph, input(*from)?, masks, time, runtime)?
1388 }
1389 };
1390 Ok(node)
1391}
1392
1393fn graph_aspect(graph: &RenderGraph<'_>) -> f64 {
1395 let size = graph.size();
1396 f64::from(size.width) / f64::from(size.height)
1397}
1398
1399fn mask_node(
1403 graph: &mut RenderGraph<'_>,
1404 source: RenderNodeId,
1405 masks: &[ShapeMask],
1406 time: MediaTime,
1407 runtime: EffectRuntime,
1408) -> Result<RenderNodeId, RenderError> {
1409 let size = graph.size();
1410 let canvas = alpha2_geometry::CanvasSize::new(size.width, size.height);
1411 let aspect = graph_aspect(graph);
1412 let unit = size.width.min(size.height);
1413 let white = alpha2_model::Paint::Solid {
1414 color: alpha2_model::Rgba::new(1.0, 1.0, 1.0, 1.0),
1415 };
1416 let shown = &masks[..masks.len().min(MAX_SHAPE_MASKS)];
1417 if shown.is_empty() {
1418 return Ok(source);
1419 }
1420 let mut coverages = Vec::with_capacity(shown.len());
1421 for mask in shown {
1422 let outline = alpha2_eval::evaluate_geometry(&mask.shape, time, aspect);
1423 let items = [
1424 alpha2_geometry::EvaluatedItem::Geometry(outline),
1425 alpha2_geometry::EvaluatedItem::Fill {
1426 paint: white.clone(),
1427 opacity: 1.0,
1428 rule: mask.fill_rule,
1429 },
1430 ];
1431 let coverage = graph.add_vector(std::sync::Arc::new(alpha2_geometry::build_scene(
1432 &items, canvas,
1433 )));
1434 #[allow(clippy::cast_precision_loss)]
1435 let sigma = 0.3125 * sample_scalar(&mask.feather, time).max(0.0) * unit as f32;
1436 let coverage = graph
1437 .add_gaussian_pixels(coverage, sigma, timeless(runtime))
1438 .map_err(GraphError::into_render_error)?;
1439 coverages.push((coverage, mask.invert, mask.mode));
1440 }
1441 coverage_mask(graph, source, &coverages)
1442}
1443
1444pub(crate) fn coverage_mask(
1446 graph: &mut RenderGraph<'_>,
1447 source: RenderNodeId,
1448 coverages: &[(RenderNodeId, bool, MaskMode)],
1449) -> Result<RenderNodeId, RenderError> {
1450 if coverages.is_empty() || coverages.len() > MAX_SHAPE_MASKS {
1451 return Err(RenderError::InvalidGraph);
1452 }
1453 let size = graph.size();
1454 #[allow(clippy::cast_precision_loss)]
1455 let runtime = EffectRuntime {
1456 ratio: size.width as f32 / size.height as f32,
1457 ..EffectRuntime::default()
1458 };
1459 let mut inputs = EffectInputs::new();
1460 let mut params = EffectParams::with_capacity(coverages.len() * 2);
1461 for (&(coverage, invert, mode), names) in coverages.iter().zip(MASK_COMBINE_NAMES) {
1462 inputs.insert(names.input, coverage);
1463 params.insert(names.invert, ParamValue::Bool(invert));
1464 params.insert(
1465 names.mode,
1466 ParamValue::Enum(match mode {
1467 MaskMode::Add => 0,
1468 MaskMode::Subtract => 1,
1469 MaskMode::Intersect => 2,
1470 }),
1471 );
1472 }
1473 graph
1474 .add_effect_with_inputs(
1475 EffectId::from_static(MASK_COMBINE_EFFECT_IDS[coverages.len() - 1], 1),
1476 source,
1477 inputs,
1478 params,
1479 EffectResources::new(),
1480 runtime,
1481 )
1482 .map_err(GraphError::into_render_error)
1483}
1484
1485const fn is_inverted(mode: MatteMode) -> bool {
1486 matches!(mode, MatteMode::AlphaInverted | MatteMode::LumaInverted)
1487}
1488
1489fn matted_layer(
1492 graph: &mut RenderGraph<'_>,
1493 layer: GraphLayer,
1494 coverage: RenderNodeId,
1495 mode: MatteMode,
1496 runtime: EffectRuntime,
1497) -> Result<GraphLayer, RenderError> {
1498 let picture = alone(graph, layer)?;
1499 let matted = matte_node(graph, picture, coverage, mode, runtime)?;
1500 Ok(GraphLayer {
1501 blend_mode: layer.blend_mode,
1502 ..identity_layer(matted)
1503 })
1504}
1505
1506fn alone(graph: &mut RenderGraph<'_>, layer: GraphLayer) -> Result<RenderNodeId, RenderError> {
1507 graph
1508 .add_composite(vec![GraphLayer {
1509 blend_mode: BlendMode::Normal,
1510 ..layer
1511 }])
1512 .map_err(GraphError::into_render_error)
1513}
1514
1515fn effect_runtime(
1518 renderer: &dyn EffectCatalog,
1519 effect: &EffectId,
1520 runtime: EffectRuntime,
1521) -> EffectRuntime {
1522 if renderer.effect_reads_time(effect) {
1523 runtime
1524 } else {
1525 EffectRuntime {
1526 time: MediaTime::ZERO,
1527 ..runtime
1528 }
1529 }
1530}
1531
1532fn timeless(runtime: EffectRuntime) -> EffectRuntime {
1534 EffectRuntime {
1535 ratio: runtime.ratio,
1536 ..EffectRuntime::default()
1537 }
1538}
1539
1540fn matte_node(
1541 graph: &mut RenderGraph<'_>,
1542 picture: RenderNodeId,
1543 coverage: RenderNodeId,
1544 mode: MatteMode,
1545 runtime: EffectRuntime,
1546) -> Result<RenderNodeId, RenderError> {
1547 graph
1548 .add_transition_with_resources(
1549 EffectId::from_static(TRACK_MATTE_EFFECT_ID, 1),
1550 picture,
1551 coverage,
1552 EffectParams::new()
1553 .with(
1554 "luma",
1555 ParamValue::Bool(matches!(mode, MatteMode::Luma | MatteMode::LumaInverted)),
1556 )
1557 .with("inverted", ParamValue::Bool(is_inverted(mode))),
1558 EffectResources::new(),
1559 EffectRuntime {
1561 progress: 0.5,
1562 ..timeless(runtime)
1563 },
1564 )
1565 .map_err(GraphError::into_render_error)
1566}
1567
1568#[derive(Copy, Clone)]
1569enum FrameInputKind<'a> {
1570 Layer(GraphLayer),
1571 Transition {
1572 from: usize,
1573 to: usize,
1574 transition: &'a ActiveTransition<'a>,
1575 },
1576}
1577
1578fn compile_frame_input<'a>(
1579 graph: &mut RenderGraph<'a>,
1580 inputs: &[FrameInput<'a>],
1581 index: usize,
1582 backdrop: &[GraphLayer],
1583 background: Option<Color>,
1584 runtime: EffectRuntime,
1585 renderer: &dyn EffectCatalog,
1586) -> Result<RenderNodeId, RenderError> {
1587 if index >= inputs.len() {
1588 return Err(RenderError::InvalidGraph);
1589 }
1590 let mut compiled = vec![None; inputs.len()];
1591 let mut pending = vec![(index, false)];
1592 while let Some((current, expanded)) = pending.pop() {
1593 if compiled[current].is_some() {
1594 continue;
1595 }
1596 match inputs[current].kind {
1597 FrameInputKind::Layer(layer) => {
1598 let mut layers = Vec::with_capacity(backdrop.len() + 1);
1599 layers.extend_from_slice(backdrop);
1600 layers.push(layer);
1601 let composite = match background {
1602 Some(background) => graph.add_composite_over(layers, background),
1603 None => graph.add_composite(layers),
1604 };
1605 compiled[current] = Some(composite.map_err(GraphError::into_render_error)?);
1606 }
1607 FrameInputKind::Transition {
1608 from,
1609 to,
1610 transition,
1611 } => {
1612 if from >= inputs.len() || to >= inputs.len() {
1613 return Err(RenderError::InvalidGraph);
1614 }
1615 if !expanded {
1616 pending.push((current, true));
1617 if compiled[to].is_none() {
1618 pending.push((to, false));
1619 }
1620 if compiled[from].is_none() {
1621 pending.push((from, false));
1622 }
1623 continue;
1624 }
1625 let from = compiled[from].ok_or(RenderError::InvalidGraph)?;
1626 let to = compiled[to].ok_or(RenderError::InvalidGraph)?;
1627 let id = project_effect_id(
1628 renderer,
1629 transition.effect.id(),
1630 transition.effect.version(),
1631 )?;
1632 let params = effect_params(renderer, &id, transition.effect);
1633 let runtime = EffectRuntime {
1634 progress: transition.progress,
1635 ..effect_runtime(renderer, &id, runtime)
1636 };
1637 compiled[current] = Some(
1638 graph
1639 .add_transition_with_resources(
1640 id,
1641 from,
1642 to,
1643 params,
1644 effect_resources(transition.effect),
1645 runtime,
1646 )
1647 .map_err(GraphError::into_render_error)?,
1648 );
1649 }
1650 }
1651 }
1652 compiled[index].ok_or(RenderError::InvalidGraph)
1653}
1654
1655fn identity_layer(source: RenderNodeId) -> GraphLayer {
1656 GraphLayer {
1657 source,
1658 visual: LayerTransform {
1659 position: [0.5, 0.5],
1660 anchor: [0.5, 0.5],
1661 scale: [1.0, 1.0],
1662 rotation_degrees: 0.0,
1663 opacity: 1.0,
1664 },
1665 blend_mode: BlendMode::Normal,
1666 }
1667}
1668
1669const fn layer_transform(visual: EvaluatedVisual) -> LayerTransform {
1670 LayerTransform {
1671 position: [visual.position.x, visual.position.y],
1672 anchor: [visual.anchor.x, visual.anchor.y],
1673 scale: [visual.scale.x, visual.scale.y],
1674 rotation_degrees: visual.rotation_degrees,
1675 opacity: visual.opacity,
1676 }
1677}
1678
1679fn effect_params<'a>(
1684 renderer: &dyn EffectCatalog,
1685 id: &EffectId,
1686 effect: ActiveEffect<'a>,
1687) -> EffectParams<'a> {
1688 let mut params = EffectParams::with_capacity(effect.authored().params.len());
1689 for (name, sample) in effect.params() {
1690 let mut value = param_value(sample.value);
1691 if sample.interpolated
1692 && let Some(spec) = renderer
1693 .effect_definition(id)
1694 .and_then(|definition| definition.params.iter().find(|spec| spec.name == name))
1695 {
1696 value = clamp_to_range(spec, value);
1697 }
1698 params.insert(name, value);
1699 }
1700 params
1701}
1702
1703fn project_effect_id(
1704 renderer: &dyn EffectCatalog,
1705 id: &str,
1706 version: u32,
1707) -> Result<EffectId, RenderError> {
1708 renderer
1709 .project_effect_id(id, version)
1710 .cloned()
1711 .ok_or(RenderError::UnknownEffect)
1712}
1713
1714fn clamp_to_range(spec: &ParamSpec, value: ParamValue) -> ParamValue {
1715 let clamp = |value: f32| {
1716 let value = spec.min.map_or(value, |minimum| value.max(minimum));
1717 spec.max.map_or(value, |maximum| value.min(maximum))
1718 };
1719 match value {
1720 ParamValue::Float(value) => ParamValue::Float(clamp(value)),
1721 ParamValue::Vec2([x, y]) => ParamValue::Vec2([clamp(x), clamp(y)]),
1722 ParamValue::Vec4(values) => ParamValue::Vec4(values.map(clamp)),
1723 other => other,
1724 }
1725}
1726
1727fn effect_resources(effect: ActiveEffect<'_>) -> EffectResources {
1728 effect
1729 .resources()
1730 .fold(EffectResources::new(), |resources, (name, id)| {
1731 resources.with(name, id.as_str())
1732 })
1733}
1734
1735const fn param_value(value: EffectValue) -> ParamValue {
1736 match value {
1737 EffectValue::Float(value) => ParamValue::Float(value),
1738 EffectValue::Vec2(value) => ParamValue::Vec2(value),
1739 EffectValue::Vec4(value) => ParamValue::Vec4(value),
1740 EffectValue::Color(value) => ParamValue::Color(value),
1741 EffectValue::Enum(value) => ParamValue::Enum(value),
1742 EffectValue::Bool(value) => ParamValue::Bool(value),
1743 }
1744}
1745
1746const fn blend_mode(mode: ClipBlendMode) -> BlendMode {
1747 match mode {
1748 ClipBlendMode::Normal => BlendMode::Normal,
1749 ClipBlendMode::Multiply => BlendMode::Multiply,
1750 ClipBlendMode::Screen => BlendMode::Screen,
1751 ClipBlendMode::Overlay => BlendMode::Overlay,
1752 ClipBlendMode::Darken => BlendMode::Darken,
1753 ClipBlendMode::Lighten => BlendMode::Lighten,
1754 ClipBlendMode::ColorDodge => BlendMode::ColorDodge,
1755 ClipBlendMode::ColorBurn => BlendMode::ColorBurn,
1756 ClipBlendMode::HardLight => BlendMode::HardLight,
1757 ClipBlendMode::SoftLight => BlendMode::SoftLight,
1758 ClipBlendMode::Difference => BlendMode::Difference,
1759 ClipBlendMode::Exclusion => BlendMode::Exclusion,
1760 ClipBlendMode::Hue => BlendMode::Hue,
1761 ClipBlendMode::Saturation => BlendMode::Saturation,
1762 ClipBlendMode::Color => BlendMode::Color,
1763 ClipBlendMode::Luminosity => BlendMode::Luminosity,
1764 ClipBlendMode::PlusLighter => BlendMode::PlusLighter,
1765 }
1766}
1767
1768#[cfg(test)]
1769mod tests;