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