1use 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
33pub trait FrameProvider {
41 fn texture_for(&self, source: &SourceId, time: MediaTime) -> Option<SourceView<'_>>;
54
55 fn source_key(&self, _source: &SourceId, _time: MediaTime) -> Option<RenderCacheKey> {
62 None
63 }
64
65 fn pending_source_work(
68 &self,
69 _source: &SourceId,
70 _time: MediaTime,
71 ) -> Option<&PendingSourceWork> {
72 None
73 }
74
75 fn text(&self, _key: u64) -> Option<TextTexture<'_>> {
80 None
81 }
82
83 fn layout(&self, _key: u128) -> Option<std::sync::Arc<crate::PreparedLayout>> {
88 None
89 }
90}
91
92#[derive(Clone, Debug, Default, PartialEq, Eq)]
94pub struct RenderReport {
95 pub missing_sources: Vec<SourceId>,
97}
98
99impl RenderReport {
100 #[must_use]
102 pub fn is_complete(&self) -> bool {
103 self.missing_sources.is_empty()
104 }
105}
106
107#[derive(Debug)]
109pub struct FrameRender<I = RenderedTexture> {
110 pub report: RenderReport,
112 pub texture: Option<I>,
114}
115
116impl<I> FrameRender<I> {
117 #[must_use]
119 pub fn is_complete(&self) -> bool {
120 self.texture.is_some()
121 }
122}
123
124#[derive(Debug)]
126pub struct FrameYuv420pRender {
127 pub report: RenderReport,
129 pub readback: Option<Yuv420pReadback>,
131}
132
133impl FrameYuv420pRender {
134 #[must_use]
136 pub fn is_complete(&self) -> bool {
137 self.readback.is_some()
138 }
139}
140
141#[derive(Debug)]
143#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
144pub struct FrameNv12Render {
145 pub report: RenderReport,
147 pub submitted: bool,
149}
150
151#[derive(Debug)]
153pub struct FrameGraph<'a> {
154 pub report: RenderReport,
156 pub graph: Option<RenderGraph<'a>>,
158 pub pending: Vec<&'a PendingSourceWork>,
161}
162
163pub trait FrameRenderer: EffectCatalog + RenderControls {
169 type Image;
172
173 #[must_use]
179 fn inspect_frame(&self, frame: &FrameDesc, frames: &dyn FrameProvider) -> RenderReport {
180 available_textures(frame, frames).0
181 }
182
183 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 fn render_frame_graph(
206 &self,
207 graph: &RenderGraph<'_>,
208 pending: &[&PendingSourceWork],
209 ) -> Result<Self::Image, RenderError>;
210
211 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 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
256pub trait GpuFrameRenderer: FrameRenderer<Image = RenderedTexture> {
259 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 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 #[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
315fn 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 #[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#[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 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
534fn 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
550pub 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
563pub(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
579fn 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#[derive(Copy, Clone, Debug)]
602struct Frame<'t> {
603 view: SourceView<'t>,
604 key: Option<RenderCacheKey>,
605}
606
607impl<'t> Frame<'t> {
608 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#[derive(Copy, Clone, Debug)]
620struct PlacedText<'t> {
621 texture: TextTexture<'t>,
622 visual: LayerTransform,
623}
624
625impl<'t> PlacedText<'t> {
626 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#[derive(Clone, Debug)]
645enum StepFrame<'t> {
646 Media(Frame<'t>),
648 Text(PlacedText<'t>),
650 Layout(LaidOut<'t>),
652}
653
654#[derive(Clone, Debug, Default)]
656struct ClipFrames<'t> {
657 layer: usize,
662 source: Option<Frame<'t>>,
665 text: Option<PlacedText<'t>>,
667 layout: Option<LaidOut<'t>>,
669 media: Vec<Option<StepFrame<'t>>>,
672 source_media: Vec<Option<StepFrame<'t>>>,
674}
675
676fn 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 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
731fn 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 } => frames
746 .text(crate::text_key(text, style, crate::text::picture(size)))
747 .map(|texture| PlacedText {
748 texture,
749 visual: text_placement(texture, style, size),
750 }),
751 _ => None,
752 };
753 for (clip, textures) in available {
754 textures.text = clip.generator.and_then(lookup);
755 textures.layout = laid_out(clip.plan, clip.source_time);
756 for (composition, media) in [
757 (clip.composition, &mut textures.media),
758 (clip.source_composition, &mut textures.source_media),
759 ] {
760 let Some(composition) = composition else {
761 continue;
762 };
763 for (step, slot) in composition.plan().steps.iter().zip(media.iter_mut()) {
764 match &step.op {
765 StepOp::Generator {
766 generator: Generator::Layout { .. },
767 start,
768 plan,
769 } => {
770 let time = MediaTime(
771 step.time(composition.time())
772 .ticks()
773 .saturating_add(start.ticks()),
774 );
775 *slot = laid_out(plan.as_deref(), time).map(StepFrame::Layout);
776 }
777 StepOp::Generator { generator, .. } => {
778 *slot = lookup(generator).map(StepFrame::Text);
779 }
780 _ => {}
781 }
782 }
783 }
784 }
785}
786
787fn text_placement(texture: TextTexture<'_>, style: &TextStyle, size: RenderSize) -> LayerTransform {
791 #[allow(clippy::cast_precision_loss)]
792 let scale = [
793 texture.width as f32 / size.width as f32,
794 texture.height as f32 / size.height as f32,
795 ];
796 LayerTransform {
797 position: [style.position.x, style.position.y],
798 anchor: [
799 box_anchor(style.anchor.x, texture.width, texture.margin[0]),
800 box_anchor(style.anchor.y, texture.height, texture.margin[1]),
801 ],
802 scale,
803 rotation_degrees: 0.0,
804 opacity: 1.0,
805 }
806}
807
808struct CaptionLayer<'a> {
810 layers_below: usize,
812 texture: TextTexture<'a>,
813 visual: LayerTransform,
814}
815
816fn caption_graph_layer<'a>(graph: &mut RenderGraph<'a>, caption: &CaptionLayer<'a>) -> GraphLayer {
819 let source = match caption.texture.cache_key {
820 Some(key) => graph.add_source_with_key(caption.texture.texture, key),
821 None => graph.add_source(caption.texture.texture),
822 };
823 GraphLayer {
824 source,
825 visual: caption.visual,
826 blend_mode: BlendMode::Normal,
827 }
828}
829
830fn caption_layers<'a>(
833 frame: &FrameDesc,
834 frames: &'a dyn FrameProvider,
835 size: RenderSize,
836) -> Result<Vec<CaptionLayer<'a>>, RenderError> {
837 frame
838 .captions
839 .iter()
840 .map(|caption| {
841 let texture = frames
842 .text(crate::text_key(
843 caption.text,
844 caption.style,
845 crate::text::picture(size),
846 ))
847 .ok_or(RenderError::MissingText)?;
848 Ok(CaptionLayer {
849 layers_below: caption.layers_below,
850 visual: text_placement(texture, caption.style, size),
851 texture,
852 })
853 })
854 .collect()
855}
856
857#[allow(clippy::cast_precision_loss)]
861fn box_anchor(anchor: f32, extent: u32, margin: u32) -> f32 {
862 if margin == 0 {
863 return anchor;
864 }
865 let inner = extent.saturating_sub(margin * 2) as f32;
866 (margin as f32 + anchor * inner) / extent as f32
867}
868
869fn frame_runtime(frame: &FrameDesc, size: RenderSize) -> EffectRuntime {
871 #[allow(clippy::cast_precision_loss)]
872 let ratio = size.width as f32 / size.height as f32;
873 EffectRuntime {
874 time: frame.time,
875 ratio,
876 ..EffectRuntime::default()
877 }
878}
879
880fn frame_graph<'a>(
881 renderer: &dyn EffectCatalog,
882 size: RenderSize,
883 frame: &'a FrameDesc<'a>,
884 available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
885 captions: &[CaptionLayer<'a>],
886) -> Result<RenderGraph<'a>, RenderError> {
887 let mut graph = RenderGraph::new(size);
888 graph.set_blend_space(match frame.blend_space {
889 alpha2_model::BlendSpace::LinearLight => alpha2_render::BlendSpace::LinearLight,
890 alpha2_model::BlendSpace::SrgbEncoded => alpha2_render::BlendSpace::SrgbEncoded,
891 });
892 let mut nodes = HashMap::with_capacity(available.len());
893 let mut inputs = Vec::with_capacity(available.len() + frame.transitions.len());
894 let mut coverages = HashMap::new();
896 let runtime = frame_runtime(frame, size);
897 for (clip, textures) in available {
898 if clip.is_matte {
899 continue;
900 }
901 let mut layer = clip_layer(renderer, &mut graph, clip, textures, frame.time, runtime)?;
902 if let Some(matte) = clip.matte {
903 layer = apply_matte(
904 renderer,
905 &mut graph,
906 layer,
907 matte,
908 available,
909 (frame.time, runtime),
910 &mut coverages,
911 )?;
912 }
913 nodes.insert(clip.clip_id, inputs.len());
914 inputs.push(FrameInput {
915 kind: FrameInputKind::Layer(layer),
916 uses_backdrop: clip.blend_mode != ClipBlendMode::Normal,
917 });
918 }
919 let background = background_color(frame.background);
920 let mut suppressed = HashSet::new();
921 for transition in &frame.transitions {
922 let from = *nodes
923 .get(transition.from_clip)
924 .ok_or(RenderError::InvalidGraph)?;
925 let to = *nodes
926 .get(transition.to_clip)
927 .ok_or(RenderError::InvalidGraph)?;
928 let output = inputs.len();
929 inputs.push(FrameInput {
930 kind: FrameInputKind::Transition {
931 from,
932 to,
933 transition,
934 },
935 uses_backdrop: inputs[from].uses_backdrop || inputs[to].uses_backdrop,
936 });
937 nodes.insert(transition.to_clip, output);
938 suppressed.insert(transition.from_clip);
939 }
940 let mut layers = Vec::with_capacity(available.len() + captions.len());
941 let mut captions = captions.iter().peekable();
942 for (clip, textures) in available {
943 while let Some(caption) = captions.next_if(|caption| caption.layers_below <= textures.layer)
946 {
947 layers.push(caption_graph_layer(&mut graph, caption));
948 }
949 if clip.is_matte || suppressed.contains(clip.clip_id) {
950 continue;
951 }
952 let index = *nodes.get(clip.clip_id).ok_or(RenderError::InvalidGraph)?;
953 if let FrameInputKind::Layer(layer) = inputs[index].kind {
954 layers.push(layer);
955 continue;
956 }
957 let uses_backdrop = inputs[index].uses_backdrop;
961 let backdrop = if uses_backdrop {
962 layers.as_slice()
963 } else {
964 &[]
965 };
966 let source = compile_frame_input(
969 &mut graph,
970 &inputs,
971 index,
972 backdrop,
973 uses_backdrop.then_some(background),
974 runtime,
975 renderer,
976 )?;
977 if uses_backdrop {
978 layers.clear();
979 }
980 layers.push(identity_layer(source));
981 }
982 for caption in captions {
983 layers.push(caption_graph_layer(&mut graph, caption));
984 }
985 let output = graph
986 .add_composite_over(layers, background)
987 .map_err(GraphError::into_render_error)?;
988 graph
989 .set_output(output)
990 .map_err(GraphError::into_render_error)?;
991 graph
992 .set_cache_intent(output, CacheIntent::Auto)
993 .map_err(GraphError::into_render_error)?;
994 Ok(graph)
995}
996
997const fn cache_intent(intent: alpha2_model::CacheIntent) -> CacheIntent {
998 match intent {
999 alpha2_model::CacheIntent::Auto => CacheIntent::Auto,
1000 alpha2_model::CacheIntent::Pin => CacheIntent::Pin,
1001 alpha2_model::CacheIntent::Bypass => CacheIntent::Bypass,
1002 }
1003}
1004
1005fn apply_matte<'a, 'c>(
1007 renderer: &dyn EffectCatalog,
1008 graph: &mut RenderGraph<'a>,
1009 mut layer: GraphLayer,
1010 matte: &'c alpha2_model::Matte,
1011 available: &[(&'a ActiveClip<'a>, ClipFrames<'a>)],
1012 (time, runtime): (MediaTime, EffectRuntime),
1013 coverages: &mut HashMap<&'c alpha2_model::ClipId, RenderNodeId>,
1014) -> Result<GraphLayer, RenderError> {
1015 let source = available
1016 .iter()
1017 .find(|(candidate, _)| candidate.clip_id == &matte.source);
1018 match source {
1019 Some((source, textures)) => {
1020 let coverage = if let Some(coverage) = coverages.get(&matte.source) {
1021 *coverage
1022 } else {
1023 let matte_layer = clip_layer(renderer, graph, source, textures, time, runtime)?;
1024 let coverage = alone(graph, matte_layer)?;
1025 coverages.insert(&matte.source, coverage);
1026 coverage
1027 };
1028 layer = matted_layer(graph, layer, coverage, matte.mode, runtime)?;
1029 }
1030 None if is_inverted(matte.mode) => {}
1033 None => layer.visual.opacity = 0.0,
1034 }
1035 Ok(layer)
1036}
1037
1038fn background_color(background: Rgb) -> Color {
1040 Color::rgba(
1041 f64::from(background.red),
1042 f64::from(background.green),
1043 f64::from(background.blue),
1044 1.0,
1045 )
1046}
1047
1048struct FrameInput<'a> {
1049 kind: FrameInputKind<'a>,
1050 uses_backdrop: bool,
1051}
1052
1053fn clip_layer<'a>(
1054 renderer: &dyn EffectCatalog,
1055 graph: &mut RenderGraph<'a>,
1056 clip: &'a ActiveClip<'a>,
1057 textures: &ClipFrames<'a>,
1058 time: MediaTime,
1059 runtime: EffectRuntime,
1060) -> Result<GraphLayer, RenderError> {
1061 let mut source = match clip.composition {
1062 None => clip_picture(renderer, graph, clip, textures, runtime)?,
1063 Some(composition) => plan_node(
1064 renderer,
1065 graph,
1066 composition,
1067 &textures.media,
1068 Some((clip, textures)),
1069 runtime,
1070 )?,
1071 };
1072 if !clip.masks.is_empty() {
1073 source = mask_node(graph, source, clip.masks, time, runtime)?;
1074 }
1075 let intent = cache_intent(clip.cache);
1079 if intent == CacheIntent::Bypass || graph.node_kind(source) != Some(RenderNodeKind::Source) {
1080 graph
1081 .set_cache_intent(source, intent)
1082 .map_err(GraphError::into_render_error)?;
1083 }
1084 Ok(GraphLayer {
1085 source,
1086 visual: layer_transform(clip.visual.ok_or(RenderError::InvalidVisual)?),
1087 blend_mode: blend_mode(clip.blend_mode),
1088 })
1089}
1090
1091fn clip_picture<'a>(
1094 renderer: &dyn EffectCatalog,
1095 graph: &mut RenderGraph<'a>,
1096 clip: &'a ActiveClip<'a>,
1097 textures: &ClipFrames<'a>,
1098 runtime: EffectRuntime,
1099) -> Result<RenderNodeId, RenderError> {
1100 if let Some(source) = clip.source_composition {
1101 return plan_node(
1103 renderer,
1104 graph,
1105 source,
1106 &textures.source_media,
1107 None,
1108 runtime,
1109 );
1110 }
1111 match (clip.generator, textures.source) {
1112 (Some(Generator::Text { .. }), _) => {
1113 textures.text.ok_or(RenderError::MissingText)?.add_to(graph)
1114 }
1115 (Some(Generator::Layout { .. }), _) => textures
1116 .layout
1117 .as_ref()
1118 .ok_or(RenderError::MissingText)?
1119 .add_to(graph),
1120 (Some(Generator::Shapes { .. }), _) => {
1121 let plan = clip
1122 .plan
1123 .and_then(GeneratorPlan::shapes)
1124 .ok_or(RenderError::InvalidGraph)?;
1125 Ok(shapes_node(graph, plan, clip.source_time))
1126 }
1127 (Some(generator), _) => Ok(generator_node(graph, generator, runtime)),
1128 (None, Some(frame)) => Ok(frame.add_to(graph)),
1129 (None, None) => Err(RenderError::InvalidGraph),
1130 }
1131}
1132
1133fn shapes_node(graph: &mut RenderGraph<'_>, plan: &ShapesPlan, time: MediaTime) -> RenderNodeId {
1137 let size = graph.size();
1138 let canvas = alpha2_geometry::CanvasSize::new(size.width, size.height);
1139 let aspect = f64::from(size.width) / f64::from(size.height);
1140 let items = plan.evaluate(time, aspect);
1141 graph.add_vector(std::sync::Arc::new(alpha2_geometry::build_scene(
1142 &items, canvas,
1143 )))
1144}
1145
1146fn generator_node(
1149 graph: &mut RenderGraph<'_>,
1150 generator: &alpha2_model::Generator,
1151 runtime: EffectRuntime,
1152) -> RenderNodeId {
1153 graph.add_generator(
1154 EffectId::from_static(GENERATOR_EFFECT_ID, 1),
1155 crate::generated::generator_params(generator),
1156 timeless(runtime),
1157 )
1158}
1159
1160fn plan_node<'a>(
1164 renderer: &dyn EffectCatalog,
1165 graph: &mut RenderGraph<'a>,
1166 composition: ActiveComposition<'a>,
1167 media: &[Option<StepFrame<'a>>],
1168 clip: Option<(&'a ActiveClip<'a>, &ClipFrames<'a>)>,
1169 runtime: EffectRuntime,
1170) -> Result<RenderNodeId, RenderError> {
1171 let plan = composition.plan();
1172 let time = composition.time();
1173 let mut built: Vec<RenderNodeId> = Vec::with_capacity(plan.steps.len());
1174 let mut picture = None;
1175 for (index, step) in plan.steps.iter().enumerate() {
1176 let at = step.time(time);
1177 let step_runtime = EffectRuntime {
1178 time: at,
1179 ..runtime
1180 };
1181 let node = match &step.op {
1182 StepOp::Clip => {
1184 if let Some(node) = picture {
1185 node
1186 } else {
1187 let (clip, textures) = clip.ok_or(RenderError::InvalidGraph)?;
1188 *picture.insert(clip_picture(renderer, graph, clip, textures, runtime)?)
1189 }
1190 }
1191 StepOp::Media { .. } => match media.get(index).and_then(Option::as_ref) {
1192 Some(StepFrame::Media(frame)) => frame.add_to(graph),
1193 _ => return Err(RenderError::InvalidGraph),
1194 },
1195 StepOp::Generator {
1196 generator: Generator::Text { .. },
1197 ..
1198 } => match media.get(index).and_then(Option::as_ref) {
1199 Some(StepFrame::Text(text)) => text.add_to(graph)?,
1200 _ => return Err(RenderError::MissingText),
1201 },
1202 StepOp::Generator {
1203 generator: Generator::Layout { .. },
1204 ..
1205 } => match media.get(index).and_then(Option::as_ref) {
1206 Some(StepFrame::Layout(layout)) => layout.add_to(graph)?,
1207 _ => return Err(RenderError::MissingText),
1208 },
1209 op => step_node(renderer, graph, op, &built, at, step_runtime)?,
1210 };
1211 built.push(node);
1212 }
1213 step_input(&built, plan.output)
1214}
1215
1216const _: () = assert!(alpha2_model::MAX_EFFECT_INPUTS == alpha2_render::MAX_EFFECT_INPUTS);
1218
1219fn step_input(built: &[RenderNodeId], step: usize) -> Result<RenderNodeId, RenderError> {
1221 built.get(step).copied().ok_or(RenderError::InvalidGraph)
1222}
1223
1224fn step_node<'a>(
1226 renderer: &dyn EffectCatalog,
1227 graph: &mut RenderGraph<'a>,
1228 op: &'a StepOp,
1229 built: &[RenderNodeId],
1230 time: MediaTime,
1231 runtime: EffectRuntime,
1232) -> Result<RenderNodeId, RenderError> {
1233 let input = |step: usize| step_input(built, step);
1234 let node = match op {
1235 StepOp::Clip | StepOp::Media { .. } => return Err(RenderError::InvalidGraph),
1236 StepOp::Generator {
1237 generator: Generator::Shapes { .. },
1238 start,
1239 plan,
1240 } => {
1241 let plan = plan
1242 .as_deref()
1243 .and_then(GeneratorPlan::shapes)
1244 .ok_or(RenderError::InvalidGraph)?;
1245 let at = MediaTime(time.ticks().saturating_add(start.ticks()));
1246 shapes_node(graph, plan, at)
1247 }
1248 StepOp::Generator { generator, .. } => generator_node(graph, generator, runtime),
1249 StepOp::Effect {
1250 input: from,
1251 inputs,
1252 effect,
1253 } => {
1254 let id = project_effect_id(renderer, &effect.id, effect.version)?;
1255 let active = ActiveEffect::new(effect, time);
1256 let params = effect_params(renderer, &id, active);
1257 let runtime = effect_runtime(renderer, &id, runtime);
1258 let mut named = EffectInputs::new();
1259 for (name, step) in inputs {
1260 named.insert(name.as_str(), input(*step)?);
1261 }
1262 graph.add_effect_with_inputs(
1263 id,
1264 input(*from)?,
1265 named,
1266 params,
1267 effect_resources(active),
1268 runtime,
1269 )
1270 }
1271 .map_err(GraphError::into_render_error)?,
1272 StepOp::Transition {
1273 from,
1274 to,
1275 effect,
1276 progress,
1277 } => {
1278 let id = project_effect_id(renderer, &effect.id, effect.version)?;
1279 let active = ActiveEffect::new(effect, time);
1280 let params = effect_params(renderer, &id, active);
1281 let runtime = EffectRuntime {
1282 progress: sample_scalar(progress, time).clamp(0.0, 1.0),
1283 ..effect_runtime(renderer, &id, runtime)
1284 };
1285 graph.add_transition_with_resources(
1286 id,
1287 input(*from)?,
1288 input(*to)?,
1289 params,
1290 effect_resources(active),
1291 runtime,
1292 )
1293 }
1294 .map_err(GraphError::into_render_error)?,
1295 StepOp::Transform {
1296 input: from,
1297 transform,
1298 } => graph
1299 .add_composite(vec![GraphLayer {
1300 source: input(*from)?,
1301 visual: layer_transform(sample_visual(transform, time, graph_aspect(graph))),
1302 blend_mode: BlendMode::Normal,
1303 }])
1304 .map_err(GraphError::into_render_error)?,
1305 StepOp::Merge { layers } => {
1306 let layers = layers
1307 .iter()
1308 .map(|layer| {
1309 let mut placed = identity_layer(input(layer.input)?);
1310 placed.visual.opacity = sample_scalar(&layer.opacity, time).clamp(0.0, 1.0);
1311 placed.blend_mode = blend_mode(layer.blend_mode);
1312 Ok(placed)
1313 })
1314 .collect::<Result<Vec<_>, RenderError>>()?;
1315 graph
1316 .add_composite(layers)
1317 .map_err(GraphError::into_render_error)?
1318 }
1319 StepOp::Matte {
1320 input: from,
1321 matte,
1322 mode,
1323 } => matte_node(graph, input(*from)?, input(*matte)?, *mode, runtime)?,
1324 StepOp::ShapeMask { input: from, masks } => {
1325 mask_node(graph, input(*from)?, masks, time, runtime)?
1326 }
1327 };
1328 Ok(node)
1329}
1330
1331fn graph_aspect(graph: &RenderGraph<'_>) -> f64 {
1334 let size = graph.size();
1335 f64::from(size.width) / f64::from(size.height)
1336}
1337
1338fn mask_node(
1345 graph: &mut RenderGraph<'_>,
1346 source: RenderNodeId,
1347 masks: &[ShapeMask],
1348 time: MediaTime,
1349 runtime: EffectRuntime,
1350) -> Result<RenderNodeId, RenderError> {
1351 let size = graph.size();
1352 let canvas = alpha2_geometry::CanvasSize::new(size.width, size.height);
1353 let aspect = graph_aspect(graph);
1354 let unit = size.width.min(size.height);
1355 let white = alpha2_model::Paint::Solid {
1356 color: alpha2_model::Rgba::new(1.0, 1.0, 1.0, 1.0),
1357 };
1358 let shown = &masks[..masks.len().min(MAX_SHAPE_MASKS)];
1359 if shown.is_empty() {
1360 return Ok(source);
1361 }
1362 let mut coverages = Vec::with_capacity(shown.len());
1363 for mask in shown {
1364 let outline = alpha2_eval::evaluate_geometry(&mask.shape, time, aspect);
1365 let items = [
1366 alpha2_geometry::EvaluatedItem::Geometry(outline),
1367 alpha2_geometry::EvaluatedItem::Fill {
1368 paint: white.clone(),
1369 opacity: 1.0,
1370 rule: mask.fill_rule,
1371 },
1372 ];
1373 let coverage = graph.add_vector(std::sync::Arc::new(alpha2_geometry::build_scene(
1374 &items, canvas,
1375 )));
1376 #[allow(clippy::cast_precision_loss)]
1377 let sigma = 0.3125 * sample_scalar(&mask.feather, time).max(0.0) * unit as f32;
1378 let coverage = graph
1379 .add_gaussian_pixels(coverage, sigma, timeless(runtime))
1380 .map_err(GraphError::into_render_error)?;
1381 coverages.push((coverage, mask.invert, mask.mode));
1382 }
1383 coverage_mask(graph, source, &coverages)
1384}
1385
1386pub(crate) fn coverage_mask(
1390 graph: &mut RenderGraph<'_>,
1391 source: RenderNodeId,
1392 coverages: &[(RenderNodeId, bool, MaskMode)],
1393) -> Result<RenderNodeId, RenderError> {
1394 if coverages.is_empty() || coverages.len() > MAX_SHAPE_MASKS {
1395 return Err(RenderError::InvalidGraph);
1396 }
1397 let size = graph.size();
1398 #[allow(clippy::cast_precision_loss)]
1399 let runtime = EffectRuntime {
1400 ratio: size.width as f32 / size.height as f32,
1401 ..EffectRuntime::default()
1402 };
1403 let mut inputs = EffectInputs::new();
1404 let mut params = EffectParams::with_capacity(coverages.len() * 2);
1405 for (&(coverage, invert, mode), names) in coverages.iter().zip(MASK_COMBINE_NAMES) {
1406 inputs.insert(names.input, coverage);
1407 params.insert(names.invert, ParamValue::Bool(invert));
1408 params.insert(
1409 names.mode,
1410 ParamValue::Enum(match mode {
1411 MaskMode::Add => 0,
1412 MaskMode::Subtract => 1,
1413 MaskMode::Intersect => 2,
1414 }),
1415 );
1416 }
1417 graph
1418 .add_effect_with_inputs(
1419 EffectId::from_static(MASK_COMBINE_EFFECT_IDS[coverages.len() - 1], 1),
1420 source,
1421 inputs,
1422 params,
1423 EffectResources::new(),
1424 runtime,
1425 )
1426 .map_err(GraphError::into_render_error)
1427}
1428
1429const fn is_inverted(mode: MatteMode) -> bool {
1430 matches!(mode, MatteMode::AlphaInverted | MatteMode::LumaInverted)
1431}
1432
1433fn matted_layer(
1437 graph: &mut RenderGraph<'_>,
1438 layer: GraphLayer,
1439 coverage: RenderNodeId,
1440 mode: MatteMode,
1441 runtime: EffectRuntime,
1442) -> Result<GraphLayer, RenderError> {
1443 let picture = alone(graph, layer)?;
1444 let matted = matte_node(graph, picture, coverage, mode, runtime)?;
1445 Ok(GraphLayer {
1446 blend_mode: layer.blend_mode,
1447 ..identity_layer(matted)
1448 })
1449}
1450
1451fn alone(graph: &mut RenderGraph<'_>, layer: GraphLayer) -> Result<RenderNodeId, RenderError> {
1453 graph
1454 .add_composite(vec![GraphLayer {
1455 blend_mode: BlendMode::Normal,
1456 ..layer
1457 }])
1458 .map_err(GraphError::into_render_error)
1459}
1460
1461fn effect_runtime(
1467 renderer: &dyn EffectCatalog,
1468 effect: &EffectId,
1469 runtime: EffectRuntime,
1470) -> EffectRuntime {
1471 if renderer.effect_reads_time(effect) {
1472 runtime
1473 } else {
1474 EffectRuntime {
1475 time: MediaTime::ZERO,
1476 ..runtime
1477 }
1478 }
1479}
1480
1481fn timeless(runtime: EffectRuntime) -> EffectRuntime {
1485 EffectRuntime {
1486 ratio: runtime.ratio,
1487 ..EffectRuntime::default()
1488 }
1489}
1490
1491fn matte_node(
1493 graph: &mut RenderGraph<'_>,
1494 picture: RenderNodeId,
1495 coverage: RenderNodeId,
1496 mode: MatteMode,
1497 runtime: EffectRuntime,
1498) -> Result<RenderNodeId, RenderError> {
1499 graph
1500 .add_transition_with_resources(
1501 EffectId::from_static(TRACK_MATTE_EFFECT_ID, 1),
1502 picture,
1503 coverage,
1504 EffectParams::new()
1505 .with(
1506 "luma",
1507 ParamValue::Bool(matches!(mode, MatteMode::Luma | MatteMode::LumaInverted)),
1508 )
1509 .with("inverted", ParamValue::Bool(is_inverted(mode))),
1510 EffectResources::new(),
1511 EffectRuntime {
1513 progress: 0.5,
1514 ..timeless(runtime)
1515 },
1516 )
1517 .map_err(GraphError::into_render_error)
1518}
1519
1520#[derive(Copy, Clone)]
1521enum FrameInputKind<'a> {
1522 Layer(GraphLayer),
1523 Transition {
1524 from: usize,
1525 to: usize,
1526 transition: &'a ActiveTransition<'a>,
1527 },
1528}
1529
1530fn compile_frame_input<'a>(
1531 graph: &mut RenderGraph<'a>,
1532 inputs: &[FrameInput<'a>],
1533 index: usize,
1534 backdrop: &[GraphLayer],
1535 background: Option<Color>,
1536 runtime: EffectRuntime,
1537 renderer: &dyn EffectCatalog,
1538) -> Result<RenderNodeId, RenderError> {
1539 if index >= inputs.len() {
1540 return Err(RenderError::InvalidGraph);
1541 }
1542 let mut compiled = vec![None; inputs.len()];
1543 let mut pending = vec![(index, false)];
1544 while let Some((current, expanded)) = pending.pop() {
1545 if compiled[current].is_some() {
1546 continue;
1547 }
1548 match inputs[current].kind {
1549 FrameInputKind::Layer(layer) => {
1550 let mut layers = Vec::with_capacity(backdrop.len() + 1);
1551 layers.extend_from_slice(backdrop);
1552 layers.push(layer);
1553 let composite = match background {
1554 Some(background) => graph.add_composite_over(layers, background),
1555 None => graph.add_composite(layers),
1556 };
1557 compiled[current] = Some(composite.map_err(GraphError::into_render_error)?);
1558 }
1559 FrameInputKind::Transition {
1560 from,
1561 to,
1562 transition,
1563 } => {
1564 if from >= inputs.len() || to >= inputs.len() {
1565 return Err(RenderError::InvalidGraph);
1566 }
1567 if !expanded {
1568 pending.push((current, true));
1569 if compiled[to].is_none() {
1570 pending.push((to, false));
1571 }
1572 if compiled[from].is_none() {
1573 pending.push((from, false));
1574 }
1575 continue;
1576 }
1577 let from = compiled[from].ok_or(RenderError::InvalidGraph)?;
1578 let to = compiled[to].ok_or(RenderError::InvalidGraph)?;
1579 let id = project_effect_id(
1580 renderer,
1581 transition.effect.id(),
1582 transition.effect.version(),
1583 )?;
1584 let params = effect_params(renderer, &id, transition.effect);
1585 let runtime = EffectRuntime {
1586 progress: transition.progress,
1587 ..effect_runtime(renderer, &id, runtime)
1588 };
1589 compiled[current] = Some(
1590 graph
1591 .add_transition_with_resources(
1592 id,
1593 from,
1594 to,
1595 params,
1596 effect_resources(transition.effect),
1597 runtime,
1598 )
1599 .map_err(GraphError::into_render_error)?,
1600 );
1601 }
1602 }
1603 }
1604 compiled[index].ok_or(RenderError::InvalidGraph)
1605}
1606
1607fn identity_layer(source: RenderNodeId) -> GraphLayer {
1608 GraphLayer {
1609 source,
1610 visual: LayerTransform {
1611 position: [0.5, 0.5],
1612 anchor: [0.5, 0.5],
1613 scale: [1.0, 1.0],
1614 rotation_degrees: 0.0,
1615 opacity: 1.0,
1616 },
1617 blend_mode: BlendMode::Normal,
1618 }
1619}
1620
1621const fn layer_transform(visual: EvaluatedVisual) -> LayerTransform {
1624 LayerTransform {
1625 position: [visual.position.x, visual.position.y],
1626 anchor: [visual.anchor.x, visual.anchor.y],
1627 scale: [visual.scale.x, visual.scale.y],
1628 rotation_degrees: visual.rotation_degrees,
1629 opacity: visual.opacity,
1630 }
1631}
1632
1633fn effect_params<'a>(
1641 renderer: &dyn EffectCatalog,
1642 id: &EffectId,
1643 effect: ActiveEffect<'a>,
1644) -> EffectParams<'a> {
1645 let mut params = EffectParams::with_capacity(effect.authored().params.len());
1646 for (name, sample) in effect.params() {
1647 let mut value = param_value(sample.value);
1648 if sample.interpolated
1649 && let Some(spec) = renderer
1650 .effect_definition(id)
1651 .and_then(|definition| definition.params.iter().find(|spec| spec.name == name))
1652 {
1653 value = clamp_to_range(spec, value);
1654 }
1655 params.insert(name, value);
1656 }
1657 params
1658}
1659
1660fn project_effect_id(
1663 renderer: &dyn EffectCatalog,
1664 id: &str,
1665 version: u32,
1666) -> Result<EffectId, RenderError> {
1667 renderer
1668 .project_effect_id(id, version)
1669 .cloned()
1670 .ok_or(RenderError::UnknownEffect)
1671}
1672
1673fn clamp_to_range(spec: &ParamSpec, value: ParamValue) -> ParamValue {
1674 let clamp = |value: f32| {
1675 let value = spec.min.map_or(value, |minimum| value.max(minimum));
1676 spec.max.map_or(value, |maximum| value.min(maximum))
1677 };
1678 match value {
1679 ParamValue::Float(value) => ParamValue::Float(clamp(value)),
1680 ParamValue::Vec2([x, y]) => ParamValue::Vec2([clamp(x), clamp(y)]),
1681 ParamValue::Vec4(values) => ParamValue::Vec4(values.map(clamp)),
1682 other => other,
1683 }
1684}
1685
1686fn effect_resources(effect: ActiveEffect<'_>) -> EffectResources {
1687 effect
1688 .resources()
1689 .fold(EffectResources::new(), |resources, (name, id)| {
1690 resources.with(name, id.as_str())
1691 })
1692}
1693
1694const fn param_value(value: EffectValue) -> ParamValue {
1695 match value {
1696 EffectValue::Float(value) => ParamValue::Float(value),
1697 EffectValue::Vec2(value) => ParamValue::Vec2(value),
1698 EffectValue::Vec4(value) => ParamValue::Vec4(value),
1699 EffectValue::Color(value) => ParamValue::Color(value),
1700 EffectValue::Enum(value) => ParamValue::Enum(value),
1701 EffectValue::Bool(value) => ParamValue::Bool(value),
1702 }
1703}
1704
1705const fn blend_mode(mode: ClipBlendMode) -> BlendMode {
1706 match mode {
1707 ClipBlendMode::Normal => BlendMode::Normal,
1708 ClipBlendMode::Multiply => BlendMode::Multiply,
1709 ClipBlendMode::Screen => BlendMode::Screen,
1710 ClipBlendMode::Overlay => BlendMode::Overlay,
1711 ClipBlendMode::Darken => BlendMode::Darken,
1712 ClipBlendMode::Lighten => BlendMode::Lighten,
1713 ClipBlendMode::ColorDodge => BlendMode::ColorDodge,
1714 ClipBlendMode::ColorBurn => BlendMode::ColorBurn,
1715 ClipBlendMode::HardLight => BlendMode::HardLight,
1716 ClipBlendMode::SoftLight => BlendMode::SoftLight,
1717 ClipBlendMode::Difference => BlendMode::Difference,
1718 ClipBlendMode::Exclusion => BlendMode::Exclusion,
1719 ClipBlendMode::Hue => BlendMode::Hue,
1720 ClipBlendMode::Saturation => BlendMode::Saturation,
1721 ClipBlendMode::Color => BlendMode::Color,
1722 ClipBlendMode::Luminosity => BlendMode::Luminosity,
1723 }
1724}
1725
1726#[cfg(test)]
1727mod tests;