1use std::borrow::Cow;
2use std::fmt;
3use std::sync::atomic::{AtomicU32, Ordering};
4
5use crate::graph_cache::{
6 composite_cache_key, effect_cache_key_with_resources, named_inputs_key, transient_source_key,
7 vector_cache_key,
8};
9use crate::{
10 BlendMode, Color, EffectId, EffectParams, EffectResources, EffectRuntime, LayerTransform,
11 RenderError, RenderSize, SourceView,
12};
13
14#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
16pub struct RenderNodeId {
17 pub(crate) graph: u32,
18 pub(crate) index: usize,
19}
20
21impl RenderNodeId {
22 #[must_use]
24 pub const fn index(self) -> usize {
25 self.index
26 }
27}
28
29#[derive(Copy, Clone, Debug, Eq, PartialEq)]
31pub enum RenderNodeKind {
32 Source,
34 Effect,
36 Transition,
38 Generator,
40 Composite,
42 Vector,
44}
45
46#[derive(Clone, Debug, Default, Eq, PartialEq)]
48pub struct EffectInputs<'a> {
49 values: Vec<(Cow<'a, str>, RenderNodeId)>,
50}
51
52impl<'a> EffectInputs<'a> {
53 #[must_use]
55 pub const fn new() -> Self {
56 Self { values: Vec::new() }
57 }
58
59 #[must_use]
61 pub fn with(mut self, name: impl Into<Cow<'a, str>>, node: RenderNodeId) -> Self {
62 self.insert(name, node);
63 self
64 }
65
66 pub fn insert(&mut self, name: impl Into<Cow<'a, str>>, node: RenderNodeId) {
68 let name = name.into();
69 match self
70 .values
71 .binary_search_by(|(held, _)| held.as_ref().cmp(name.as_ref()))
72 {
73 Ok(index) => self.values[index].1 = node,
74 Err(index) => self.values.insert(index, (name, node)),
75 }
76 }
77
78 #[must_use]
80 pub fn is_empty(&self) -> bool {
81 self.values.is_empty()
82 }
83
84 #[must_use]
86 pub fn len(&self) -> usize {
87 self.values.len()
88 }
89
90 pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, RenderNodeId)> {
92 self.values
93 .iter()
94 .map(|(name, node)| (name.as_ref(), *node))
95 }
96}
97
98#[derive(Copy, Clone, Debug)]
100pub struct GraphLayer {
101 pub source: RenderNodeId,
103 pub visual: LayerTransform,
105 pub blend_mode: BlendMode,
107}
108
109#[derive(Debug)]
111pub struct RenderGraph<'a> {
112 pub(crate) id: u32,
113 pub(crate) size: RenderSize,
114 pub(crate) nodes: Vec<RenderNode<'a>>,
115 pub(crate) cache_keys: Vec<RenderCacheKey>,
116 pub(crate) stable: Vec<bool>,
118 pub(crate) intents: Vec<Option<crate::CacheIntent>>,
120 pub(crate) output: Option<RenderNodeId>,
121 pub(crate) blend_space: Option<crate::BlendSpace>,
123}
124
125static NEXT_GRAPH_ID: AtomicU32 = AtomicU32::new(1);
126
127#[derive(Debug)]
128pub(crate) enum RenderNode<'a> {
129 Source(SourceView<'a>),
130 Effect {
131 effect: EffectId,
132 source: RenderNodeId,
133 inputs: EffectInputs<'a>,
134 params: EffectParams<'a>,
135 resources: EffectResources,
136 runtime: EffectRuntime,
137 },
138 Transition {
139 effect: EffectId,
140 from: RenderNodeId,
141 to: RenderNodeId,
142 params: EffectParams<'a>,
143 resources: EffectResources,
144 runtime: EffectRuntime,
145 },
146 Generator {
147 effect: EffectId,
148 params: EffectParams<'a>,
149 resources: EffectResources,
150 runtime: EffectRuntime,
151 },
152 Composite {
153 layers: Vec<GraphLayer>,
154 background: Option<Color>,
155 },
156 Vector {
157 scene: std::sync::Arc<alpha2_geometry::VectorScene>,
158 },
159}
160
161#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
163pub struct RenderCacheKey(pub(crate) u128);
164
165impl RenderCacheKey {
166 #[must_use]
168 pub const fn source(value: u64) -> Self {
169 Self(value as u128)
170 }
171
172 #[must_use]
175 pub const fn from_value(value: u128) -> Self {
176 Self(value)
177 }
178
179 #[must_use]
181 pub const fn value(self) -> u128 {
182 self.0
183 }
184}
185
186#[derive(Copy, Clone, Debug, Eq, PartialEq)]
188#[non_exhaustive]
189pub enum GraphError {
190 InvalidDependency,
192 MissingOutput,
194 SourceOutput,
196 EffectKindMismatch,
198 Render(RenderError),
200}
201
202impl fmt::Display for GraphError {
203 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204 match self {
205 Self::InvalidDependency => {
206 formatter.write_str("render graph dependency must name an earlier node")
207 }
208 Self::MissingOutput => formatter.write_str("render graph has no selected output"),
209 Self::SourceOutput => {
210 formatter.write_str("render graph output must be a rendered node")
211 }
212 Self::EffectKindMismatch => {
213 formatter.write_str("registered effect kind does not match its graph node")
214 }
215 Self::Render(error) => write!(formatter, "render graph execution failed: {error}"),
216 }
217 }
218}
219
220impl std::error::Error for GraphError {
221 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
222 match self {
223 Self::Render(error) => Some(error),
224 _ => None,
225 }
226 }
227}
228
229impl From<RenderError> for GraphError {
230 fn from(error: RenderError) -> Self {
231 Self::Render(error)
232 }
233}
234
235impl GraphError {
236 #[must_use]
238 pub const fn into_render_error(self) -> RenderError {
239 match self {
240 Self::Render(error) => error,
241 Self::InvalidDependency
242 | Self::MissingOutput
243 | Self::SourceOutput
244 | Self::EffectKindMismatch => RenderError::InvalidGraph,
245 }
246 }
247}
248
249impl<'a> RenderGraph<'a> {
250 #[must_use]
252 pub fn new(size: RenderSize) -> Self {
253 Self {
254 id: NEXT_GRAPH_ID.fetch_add(1, Ordering::Relaxed),
255 size,
256 nodes: Vec::new(),
257 cache_keys: Vec::new(),
258 stable: Vec::new(),
259 intents: Vec::new(),
260 output: None,
261 blend_space: None,
262 }
263 }
264
265 #[must_use]
267 pub const fn size(&self) -> RenderSize {
268 self.size
269 }
270
271 pub fn set_blend_space(&mut self, space: crate::BlendSpace) {
274 self.blend_space = Some(space);
275 }
276
277 pub fn add_source(&mut self, texture: SourceView<'a>) -> RenderNodeId {
279 let key = transient_source_key(self.id, self.nodes.len());
280 self.push(RenderNode::Source(texture), key, false)
281 }
282
283 pub fn add_source_with_key(
288 &mut self,
289 texture: SourceView<'a>,
290 key: RenderCacheKey,
291 ) -> RenderNodeId {
292 self.push(RenderNode::Source(texture), key, true)
293 }
294
295 pub fn add_effect(
300 &mut self,
301 effect: EffectId,
302 source: RenderNodeId,
303 params: EffectParams<'a>,
304 runtime: EffectRuntime,
305 ) -> Result<RenderNodeId, GraphError> {
306 self.add_effect_with_resources(effect, source, params, EffectResources::new(), runtime)
307 }
308
309 pub fn add_effect_with_resources(
317 &mut self,
318 effect: EffectId,
319 source: RenderNodeId,
320 params: EffectParams<'a>,
321 resources: EffectResources,
322 runtime: EffectRuntime,
323 ) -> Result<RenderNodeId, GraphError> {
324 self.add_effect_with_inputs(
325 effect,
326 source,
327 EffectInputs::new(),
328 params,
329 resources,
330 runtime,
331 )
332 }
333
334 pub fn add_effect_with_inputs(
341 &mut self,
342 effect: EffectId,
343 source: RenderNodeId,
344 inputs: EffectInputs<'a>,
345 params: EffectParams<'a>,
346 resources: EffectResources,
347 runtime: EffectRuntime,
348 ) -> Result<RenderNodeId, GraphError> {
349 self.validate_dependency(source)?;
350 for (_, input) in inputs.iter() {
351 self.validate_dependency(input)?;
352 }
353 if !inputs.is_empty() && crate::gaussian_blur::expanded_passes(effect.as_str()).is_some() {
356 return Err(RenderError::InvalidEffectParams.into());
357 }
358 if let Some(expanded) = self.add_expanded_effect(&effect, source, ¶ms, runtime) {
359 if resources.iter().next().is_some() {
360 return Err(RenderError::InvalidEffectParams.into());
361 }
362 return expanded;
363 }
364 let source_key = self.cache_keys[source.index];
365 let input_keys: &[RenderCacheKey] = if inputs.is_empty() {
366 &[source_key]
367 } else {
368 &[source_key, named_inputs_key(&inputs, &self.cache_keys)]
369 };
370 let key = effect_cache_key_with_resources(
371 b"effect", self.size, &effect, input_keys, ¶ms, &resources, runtime,
372 );
373 let stable =
374 self.stable[source.index] && inputs.iter().all(|(_, input)| self.stable[input.index]);
375 Ok(self.push(
376 RenderNode::Effect {
377 effect,
378 source,
379 inputs,
380 params,
381 resources,
382 runtime,
383 },
384 key,
385 stable,
386 ))
387 }
388
389 pub fn add_generator(
391 &mut self,
392 effect: EffectId,
393 params: EffectParams<'a>,
394 runtime: EffectRuntime,
395 ) -> RenderNodeId {
396 self.add_generator_with_resources(effect, params, EffectResources::new(), runtime)
397 }
398
399 pub fn add_generator_with_resources(
401 &mut self,
402 effect: EffectId,
403 params: EffectParams<'a>,
404 resources: EffectResources,
405 runtime: EffectRuntime,
406 ) -> RenderNodeId {
407 let key = effect_cache_key_with_resources(
408 b"generator",
409 self.size,
410 &effect,
411 &[],
412 ¶ms,
413 &resources,
414 runtime,
415 );
416 self.push(
417 RenderNode::Generator {
418 effect,
419 params,
420 resources,
421 runtime,
422 },
423 key,
424 true,
425 )
426 }
427
428 pub fn add_vector(
434 &mut self,
435 scene: std::sync::Arc<alpha2_geometry::VectorScene>,
436 ) -> RenderNodeId {
437 let key = vector_cache_key(self.size, scene.key);
438 self.push(RenderNode::Vector { scene }, key, true)
439 }
440
441 pub fn add_transition(
446 &mut self,
447 effect: EffectId,
448 from: RenderNodeId,
449 to: RenderNodeId,
450 params: EffectParams<'a>,
451 runtime: EffectRuntime,
452 ) -> Result<RenderNodeId, GraphError> {
453 self.add_transition_with_resources(
454 effect,
455 from,
456 to,
457 params,
458 EffectResources::new(),
459 runtime,
460 )
461 }
462
463 pub fn add_transition_with_resources(
468 &mut self,
469 effect: EffectId,
470 from: RenderNodeId,
471 to: RenderNodeId,
472 params: EffectParams<'a>,
473 resources: EffectResources,
474 runtime: EffectRuntime,
475 ) -> Result<RenderNodeId, GraphError> {
476 self.validate_dependency(from)?;
477 self.validate_dependency(to)?;
478 let key = effect_cache_key_with_resources(
479 b"transition",
480 self.size,
481 &effect,
482 &[self.cache_keys[from.index], self.cache_keys[to.index]],
483 ¶ms,
484 &resources,
485 runtime,
486 );
487 let stable = self.stable[from.index] && self.stable[to.index];
488 Ok(self.push(
489 RenderNode::Transition {
490 effect,
491 from,
492 to,
493 params,
494 resources,
495 runtime,
496 },
497 key,
498 stable,
499 ))
500 }
501
502 pub fn add_composite(&mut self, layers: Vec<GraphLayer>) -> Result<RenderNodeId, GraphError> {
507 self.push_composite(layers, None)
508 }
509
510 pub fn add_composite_over(
519 &mut self,
520 layers: Vec<GraphLayer>,
521 background: Color,
522 ) -> Result<RenderNodeId, GraphError> {
523 if !background.is_valid() {
524 return Err(GraphError::Render(RenderError::InvalidColor));
525 }
526 self.push_composite(layers, Some(background))
527 }
528
529 fn push_composite(
530 &mut self,
531 layers: Vec<GraphLayer>,
532 background: Option<Color>,
533 ) -> Result<RenderNodeId, GraphError> {
534 for layer in &layers {
535 self.validate_dependency(layer.source)?;
536 }
537 let key = composite_cache_key(self.size, &layers, background, &self.cache_keys);
538 let stable = layers.iter().all(|layer| self.stable[layer.source.index]);
539 Ok(self.push(RenderNode::Composite { layers, background }, key, stable))
540 }
541
542 pub fn set_output(&mut self, output: RenderNodeId) -> Result<(), GraphError> {
547 self.validate_dependency(output)?;
548 self.output = Some(output);
549 Ok(())
550 }
551
552 #[must_use]
554 pub const fn output(&self) -> Option<RenderNodeId> {
555 self.output
556 }
557
558 #[must_use]
560 pub fn node_count(&self) -> usize {
561 self.nodes.len()
562 }
563
564 #[must_use]
566 pub fn node_kind(&self, id: RenderNodeId) -> Option<RenderNodeKind> {
567 if id.graph == self.id {
568 self.nodes.get(id.index).map(RenderNode::kind)
569 } else {
570 None
571 }
572 }
573
574 pub fn set_cache_intent(
580 &mut self,
581 node: RenderNodeId,
582 intent: crate::CacheIntent,
583 ) -> Result<(), GraphError> {
584 self.validate_dependency(node)?;
585 self.intents[node.index] = Some(intent);
586 Ok(())
587 }
588
589 pub fn cache_eligibility(
594 &self,
595 node: RenderNodeId,
596 ) -> Result<crate::CacheEligibility, GraphError> {
597 self.validate_dependency(node)?;
598 Ok(self.eligibility()[node.index])
599 }
600
601 pub(crate) fn eligibility(&self) -> Vec<crate::CacheEligibility> {
604 let count = self.nodes.len();
605 let mut bypassed: Vec<bool> = self
606 .intents
607 .iter()
608 .map(|intent| *intent == Some(crate::CacheIntent::Bypass))
609 .collect();
610 for index in 0..count {
612 if self.nodes[index]
613 .dependencies()
614 .iter()
615 .any(|input| bypassed[input.index])
616 {
617 bypassed[index] = true;
618 }
619 }
620 let mut down: Vec<bool> = self
622 .intents
623 .iter()
624 .map(|intent| *intent == Some(crate::CacheIntent::Bypass))
625 .collect();
626 for index in (0..count).rev() {
627 if down[index] {
628 for input in self.nodes[index].dependencies() {
629 down[input.index] = true;
630 }
631 }
632 }
633 (0..count)
634 .map(|index| {
635 if bypassed[index] || down[index] {
636 crate::CacheEligibility::Bypassed
637 } else if self.stable[index] {
638 crate::CacheEligibility::Eligible
639 } else {
640 crate::CacheEligibility::TransientSource
641 }
642 })
643 .collect()
644 }
645
646 fn push(
647 &mut self,
648 node: RenderNode<'a>,
649 cache_key: RenderCacheKey,
650 stable: bool,
651 ) -> RenderNodeId {
652 let id = RenderNodeId {
653 graph: self.id,
654 index: self.nodes.len(),
655 };
656 self.nodes.push(node);
657 self.cache_keys.push(cache_key);
658 self.stable.push(stable);
659 self.intents.push(None);
660 id
661 }
662
663 fn validate_dependency(&self, dependency: RenderNodeId) -> Result<(), GraphError> {
664 if dependency.graph == self.id && dependency.index < self.nodes.len() {
665 Ok(())
666 } else {
667 Err(GraphError::InvalidDependency)
668 }
669 }
670}
671
672impl RenderNodeKind {
673 pub(crate) const fn pass_label(self) -> &'static str {
675 match self {
676 Self::Source => "source",
677 Self::Effect => "effect",
678 Self::Transition => "transition",
679 Self::Generator => "generator",
680 Self::Composite => "composite",
681 Self::Vector => "vector",
682 }
683 }
684}
685
686impl RenderNode<'_> {
687 pub(crate) const fn kind(&self) -> RenderNodeKind {
688 match self {
689 Self::Source(_) => RenderNodeKind::Source,
690 Self::Effect { .. } => RenderNodeKind::Effect,
691 Self::Transition { .. } => RenderNodeKind::Transition,
692 Self::Generator { .. } => RenderNodeKind::Generator,
693 Self::Composite { .. } => RenderNodeKind::Composite,
694 Self::Vector { .. } => RenderNodeKind::Vector,
695 }
696 }
697
698 pub(crate) fn dependencies(&self) -> Vec<RenderNodeId> {
699 match self {
700 Self::Source(_) | Self::Generator { .. } | Self::Vector { .. } => Vec::new(),
701 Self::Effect { source, inputs, .. } => std::iter::once(*source)
702 .chain(inputs.iter().map(|(_, input)| input))
703 .collect(),
704 Self::Transition { from, to, .. } => vec![*from, *to],
705 Self::Composite { layers, .. } => layers.iter().map(|layer| layer.source).collect(),
706 }
707 }
708}
709
710#[cfg(test)]
711mod tests {
712 use super::*;
713
714 #[test]
715 fn every_node_and_setting_refuses_a_node_of_another_graph_or_one_not_yet_added() {
716 let size = RenderSize::new(16, 9).expect("non-zero size");
719 let effect = EffectId::new("test.effect".into(), 1);
720 let mut other = RenderGraph::new(size);
721 let foreign = other.add_generator(
722 effect.clone(),
723 EffectParams::new(),
724 EffectRuntime::default(),
725 );
726 let mut graph = RenderGraph::new(size);
727 let own = graph.add_generator(
728 effect.clone(),
729 EffectParams::new(),
730 EffectRuntime::default(),
731 );
732 assert_eq!(foreign.index, own.index);
734 let unknown = RenderNodeId {
735 graph: graph.id,
736 index: 1,
737 };
738 let layer = |source| GraphLayer {
739 source,
740 visual: crate::LayerTransform {
741 position: [0.5, 0.5],
742 anchor: [0.5, 0.5],
743 scale: [1.0, 1.0],
744 rotation_degrees: 0.0,
745 opacity: 1.0,
746 },
747 blend_mode: crate::BlendMode::Normal,
748 };
749 for bad in [foreign, unknown] {
750 let invalid = Err(GraphError::InvalidDependency);
751 assert_eq!(
752 graph.add_effect(
753 effect.clone(),
754 bad,
755 EffectParams::new(),
756 EffectRuntime::default()
757 ),
758 invalid
759 );
760 assert_eq!(
761 graph.add_transition(
762 effect.clone(),
763 own,
764 bad,
765 EffectParams::new(),
766 EffectRuntime::default()
767 ),
768 invalid
769 );
770 assert_eq!(
771 graph.add_transition(
772 effect.clone(),
773 bad,
774 own,
775 EffectParams::new(),
776 EffectRuntime::default()
777 ),
778 invalid
779 );
780 assert_eq!(graph.add_composite(vec![layer(own), layer(bad)]), invalid);
781 assert_eq!(
782 graph.add_composite_over(vec![layer(bad)], Color::BLACK),
783 invalid
784 );
785 assert_eq!(graph.set_output(bad), Err(GraphError::InvalidDependency));
786 assert_eq!(
787 graph.set_cache_intent(bad, crate::CacheIntent::Pin),
788 Err(GraphError::InvalidDependency)
789 );
790 assert_eq!(
791 graph.cache_eligibility(bad),
792 Err(GraphError::InvalidDependency)
793 );
794 assert_eq!(graph.node_kind(bad), None);
795 }
796 assert_eq!(graph.node_count(), 1);
797 assert_eq!(graph.node_kind(own), Some(RenderNodeKind::Generator));
798 assert_eq!(
800 graph.add_composite_over(vec![layer(foreign)], Color::rgba(1.5, 0.0, 0.0, 1.0)),
801 Err(GraphError::Render(RenderError::InvalidColor))
802 );
803 assert_eq!(graph.node_count(), 1);
804 }
805
806 fn generator(graph: &mut RenderGraph<'_>, id: &'static str) -> RenderNodeId {
807 graph.add_generator(
808 EffectId::from_static(id, 1),
809 EffectParams::new(),
810 EffectRuntime::default(),
811 )
812 }
813
814 fn effect_with_inputs(
815 graph: &mut RenderGraph<'static>,
816 source: RenderNodeId,
817 inputs: EffectInputs<'static>,
818 ) -> Result<RenderNodeId, GraphError> {
819 graph.add_effect_with_inputs(
820 EffectId::from_static("test.multi-input", 1),
821 source,
822 inputs,
823 EffectParams::new(),
824 EffectResources::new(),
825 EffectRuntime::default(),
826 )
827 }
828
829 #[test]
830 fn named_inputs_are_dependencies() {
831 let size = RenderSize::new(16, 9).expect("non-zero size");
833 let mut graph = RenderGraph::new(size);
834 let source = generator(&mut graph, "test.source");
835 let map = generator(&mut graph, "test.map");
836 let unused = generator(&mut graph, "test.unused");
837 let effect = effect_with_inputs(&mut graph, source, EffectInputs::new().with("map", map))
838 .expect("earlier nodes");
839 graph.set_output(effect).expect("known output");
840 let plan = graph.compile().expect("compiles");
841 assert_eq!(plan.execution_order(), [source, map, effect]);
842 assert_eq!(plan.last_use(map), Some(2));
843 assert_eq!(plan.last_use(unused), None);
844 let later = RenderNodeId {
845 graph: graph.id,
846 index: graph.node_count(),
847 };
848 assert_eq!(
849 effect_with_inputs(&mut graph, source, EffectInputs::new().with("map", later)),
850 Err(GraphError::InvalidDependency)
851 );
852 }
853
854 #[test]
855 fn named_inputs_on_a_built_in_drawn_as_passes_are_refused() {
856 let size = RenderSize::new(16, 9).expect("non-zero size");
857 let mut graph = RenderGraph::new(size);
858 let source = generator(&mut graph, "test.source");
859 let map = generator(&mut graph, "test.map");
860 let count = graph.node_count();
861 for id in [crate::builtins::GAUSSIAN_BLUR_ID, crate::builtins::GLOW_ID] {
862 assert_eq!(
863 graph.add_effect_with_inputs(
864 EffectId::from_static(id, 1),
865 source,
866 EffectInputs::new().with("map", map),
867 EffectParams::new(),
868 EffectResources::new(),
869 EffectRuntime::default(),
870 ),
871 Err(GraphError::Render(RenderError::InvalidEffectParams)),
872 "{id}"
873 );
874 }
875 assert_eq!(graph.node_count(), count, "no pass is left behind");
876 }
877
878 #[test]
879 fn a_named_input_is_part_of_the_cache_key() {
880 let size = RenderSize::new(16, 9).expect("non-zero size");
881 let key = |build: &dyn Fn(&mut RenderGraph<'static>, [RenderNodeId; 3]) -> RenderNodeId| {
882 let mut graph = RenderGraph::new(size);
883 let nodes = [
884 generator(&mut graph, "test.source"),
885 generator(&mut graph, "test.map"),
886 generator(&mut graph, "test.other-map"),
887 ];
888 let node = build(&mut graph, nodes);
889 graph.cache_keys[node.index]
890 };
891 let named = |name: &'static str, input: usize| {
892 key(&move |graph, nodes| {
893 effect_with_inputs(
894 graph,
895 nodes[0],
896 EffectInputs::new().with(name, nodes[input]),
897 )
898 .expect("earlier nodes")
899 })
900 };
901 assert_eq!(named("map", 1), named("map", 1));
902 assert_ne!(named("map", 1), named("map", 2));
903 assert_ne!(named("map", 1), named("mask", 1));
904 let none = key(&|graph, nodes| {
905 effect_with_inputs(graph, nodes[0], EffectInputs::new()).expect("earlier node")
906 });
907 assert_ne!(named("map", 1), none);
908 let plain = key(&|graph, nodes| {
909 graph
910 .add_effect_with_resources(
911 EffectId::from_static("test.multi-input", 1),
912 nodes[0],
913 EffectParams::new(),
914 EffectResources::new(),
915 EffectRuntime::default(),
916 )
917 .expect("earlier node")
918 });
919 assert_eq!(none, plain);
920 }
921
922 #[test]
923 fn a_graph_compiles_only_once_an_output_is_chosen() {
924 let size = RenderSize::new(16, 9).expect("non-zero size");
925 let mut graph = RenderGraph::new(size);
926 let output = graph.add_composite(Vec::new()).expect("empty clear node");
927 assert_eq!(graph.compile().err(), Some(GraphError::MissingOutput));
928 assert_eq!(
929 GraphError::MissingOutput.into_render_error(),
930 RenderError::InvalidGraph
931 );
932 graph.set_output(output).expect("known output");
933 assert_eq!(
934 graph.compile().expect("compiles").execution_order(),
935 [output]
936 );
937 }
938
939 #[test]
940 fn quality_compilation_exposes_centered_temporal_schedule() {
941 let size = RenderSize::new(16, 9).expect("non-zero size");
942 let mut graph = RenderGraph::new(size);
943 let output = graph.add_composite(Vec::new()).expect("empty clear node");
944 graph.set_output(output).expect("known output");
945 let plan = graph
946 .compile_with_quality(crate::RenderQualityControls {
947 max_filter_samples: 12,
948 motion_blur_subframes: 4,
949 })
950 .expect("compiled");
951 assert_eq!(plan.temporal_offsets(), [-0.375, -0.125, 0.125, 0.375]);
952 assert_eq!(plan.filter_sample_limit(output), None);
953 }
954}