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,
37 Transition,
39 Generator,
41 Composite,
43 Vector,
45}
46
47#[derive(Clone, Debug, Default, Eq, PartialEq)]
51pub struct EffectInputs<'a> {
52 values: Vec<(Cow<'a, str>, RenderNodeId)>,
53}
54
55impl<'a> EffectInputs<'a> {
56 #[must_use]
58 pub const fn new() -> Self {
59 Self { values: Vec::new() }
60 }
61
62 #[must_use]
64 pub fn with(mut self, name: impl Into<Cow<'a, str>>, node: RenderNodeId) -> Self {
65 self.insert(name, node);
66 self
67 }
68
69 pub fn insert(&mut self, name: impl Into<Cow<'a, str>>, node: RenderNodeId) {
71 let name = name.into();
72 match self
73 .values
74 .binary_search_by(|(held, _)| held.as_ref().cmp(name.as_ref()))
75 {
76 Ok(index) => self.values[index].1 = node,
77 Err(index) => self.values.insert(index, (name, node)),
78 }
79 }
80
81 #[must_use]
83 pub fn is_empty(&self) -> bool {
84 self.values.is_empty()
85 }
86
87 #[must_use]
89 pub fn len(&self) -> usize {
90 self.values.len()
91 }
92
93 pub(crate) fn iter(&self) -> impl Iterator<Item = (&str, RenderNodeId)> {
95 self.values
96 .iter()
97 .map(|(name, node)| (name.as_ref(), *node))
98 }
99}
100
101#[derive(Copy, Clone, Debug)]
103pub struct GraphLayer {
104 pub source: RenderNodeId,
106 pub visual: LayerTransform,
108 pub blend_mode: BlendMode,
110}
111
112#[derive(Debug)]
114pub struct RenderGraph<'a> {
115 pub(crate) id: u32,
116 pub(crate) size: RenderSize,
117 pub(crate) nodes: Vec<RenderNode<'a>>,
118 pub(crate) cache_keys: Vec<RenderCacheKey>,
119 pub(crate) stable: Vec<bool>,
122 pub(crate) intents: Vec<Option<crate::CacheIntent>>,
124 pub(crate) output: Option<RenderNodeId>,
125 pub(crate) blend_space: Option<crate::BlendSpace>,
128}
129
130static NEXT_GRAPH_ID: AtomicU32 = AtomicU32::new(1);
131
132#[derive(Debug)]
133pub(crate) enum RenderNode<'a> {
134 Source(&'a wgpu::TextureView),
135 Effect {
136 effect: EffectId,
137 source: RenderNodeId,
138 inputs: EffectInputs<'a>,
139 params: EffectParams<'a>,
140 resources: EffectResources,
141 runtime: EffectRuntime,
142 },
143 Transition {
144 effect: EffectId,
145 from: RenderNodeId,
146 to: RenderNodeId,
147 params: EffectParams<'a>,
148 resources: EffectResources,
149 runtime: EffectRuntime,
150 },
151 Generator {
152 effect: EffectId,
153 params: EffectParams<'a>,
154 resources: EffectResources,
155 runtime: EffectRuntime,
156 },
157 Composite {
158 layers: Vec<GraphLayer>,
159 background: Option<Color>,
160 },
161 Vector {
162 scene: std::sync::Arc<alpha2_geometry::VectorScene>,
163 },
164}
165
166#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
168pub struct RenderCacheKey(pub(crate) u128);
169
170impl RenderCacheKey {
171 #[must_use]
173 pub const fn source(value: u64) -> Self {
174 Self(value as u128)
175 }
176
177 #[must_use]
181 pub const fn from_value(value: u128) -> Self {
182 Self(value)
183 }
184
185 #[must_use]
187 pub const fn value(self) -> u128 {
188 self.0
189 }
190}
191
192#[derive(Copy, Clone, Debug, Eq, PartialEq)]
194#[non_exhaustive]
195pub enum GraphError {
196 InvalidDependency,
198 MissingOutput,
200 SourceOutput,
202 EffectKindMismatch,
204 Render(RenderError),
206}
207
208impl fmt::Display for GraphError {
209 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self {
211 Self::InvalidDependency => {
212 formatter.write_str("render graph dependency must name an earlier node")
213 }
214 Self::MissingOutput => formatter.write_str("render graph has no selected output"),
215 Self::SourceOutput => {
216 formatter.write_str("render graph output must be a rendered node")
217 }
218 Self::EffectKindMismatch => {
219 formatter.write_str("registered effect kind does not match its graph node")
220 }
221 Self::Render(error) => write!(formatter, "render graph execution failed: {error}"),
222 }
223 }
224}
225
226impl std::error::Error for GraphError {
227 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
228 match self {
229 Self::Render(error) => Some(error),
230 _ => None,
231 }
232 }
233}
234
235impl From<RenderError> for GraphError {
236 fn from(error: RenderError) -> Self {
237 Self::Render(error)
238 }
239}
240
241impl GraphError {
242 #[must_use]
245 pub const fn into_render_error(self) -> RenderError {
246 match self {
247 Self::Render(error) => error,
248 Self::InvalidDependency
249 | Self::MissingOutput
250 | Self::SourceOutput
251 | Self::EffectKindMismatch => RenderError::InvalidGraph,
252 }
253 }
254}
255
256impl<'a> RenderGraph<'a> {
257 #[must_use]
259 pub fn new(size: RenderSize) -> Self {
260 Self {
261 id: NEXT_GRAPH_ID.fetch_add(1, Ordering::Relaxed),
262 size,
263 nodes: Vec::new(),
264 cache_keys: Vec::new(),
265 stable: Vec::new(),
266 intents: Vec::new(),
267 output: None,
268 blend_space: None,
269 }
270 }
271
272 #[must_use]
274 pub const fn size(&self) -> RenderSize {
275 self.size
276 }
277
278 pub fn set_blend_space(&mut self, space: crate::BlendSpace) {
282 self.blend_space = Some(space);
283 }
284
285 pub fn add_source(&mut self, texture: SourceView<'a>) -> RenderNodeId {
287 let key = transient_source_key(self.id, self.nodes.len());
288 self.push(RenderNode::Source(texture.wgpu_view()), key, false)
289 }
290
291 pub fn add_source_with_key(
297 &mut self,
298 texture: SourceView<'a>,
299 key: RenderCacheKey,
300 ) -> RenderNodeId {
301 self.push(RenderNode::Source(texture.wgpu_view()), key, true)
302 }
303
304 pub fn add_effect(
309 &mut self,
310 effect: EffectId,
311 source: RenderNodeId,
312 params: EffectParams<'a>,
313 runtime: EffectRuntime,
314 ) -> Result<RenderNodeId, GraphError> {
315 self.add_effect_with_resources(effect, source, params, EffectResources::new(), runtime)
316 }
317
318 pub fn add_effect_with_resources(
328 &mut self,
329 effect: EffectId,
330 source: RenderNodeId,
331 params: EffectParams<'a>,
332 resources: EffectResources,
333 runtime: EffectRuntime,
334 ) -> Result<RenderNodeId, GraphError> {
335 self.add_effect_with_inputs(
336 effect,
337 source,
338 EffectInputs::new(),
339 params,
340 resources,
341 runtime,
342 )
343 }
344
345 pub fn add_effect_with_inputs(
354 &mut self,
355 effect: EffectId,
356 source: RenderNodeId,
357 inputs: EffectInputs<'a>,
358 params: EffectParams<'a>,
359 resources: EffectResources,
360 runtime: EffectRuntime,
361 ) -> Result<RenderNodeId, GraphError> {
362 self.validate_dependency(source)?;
363 for (_, input) in inputs.iter() {
364 self.validate_dependency(input)?;
365 }
366 if !inputs.is_empty() && crate::gaussian_blur::expanded_passes(effect.as_str()).is_some() {
369 return Err(RenderError::InvalidEffectParams.into());
370 }
371 if let Some(expanded) = self.add_expanded_effect(&effect, source, ¶ms, runtime) {
372 if resources.iter().next().is_some() {
373 return Err(RenderError::InvalidEffectParams.into());
374 }
375 return expanded;
376 }
377 let source_key = self.cache_keys[source.index];
378 let input_keys: &[RenderCacheKey] = if inputs.is_empty() {
380 &[source_key]
381 } else {
382 &[source_key, named_inputs_key(&inputs, &self.cache_keys)]
383 };
384 let key = effect_cache_key_with_resources(
385 b"effect", self.size, &effect, input_keys, ¶ms, &resources, runtime,
386 );
387 let stable =
388 self.stable[source.index] && inputs.iter().all(|(_, input)| self.stable[input.index]);
389 Ok(self.push(
390 RenderNode::Effect {
391 effect,
392 source,
393 inputs,
394 params,
395 resources,
396 runtime,
397 },
398 key,
399 stable,
400 ))
401 }
402
403 pub fn add_generator(
406 &mut self,
407 effect: EffectId,
408 params: EffectParams<'a>,
409 runtime: EffectRuntime,
410 ) -> RenderNodeId {
411 self.add_generator_with_resources(effect, params, EffectResources::new(), runtime)
412 }
413
414 pub fn add_generator_with_resources(
417 &mut self,
418 effect: EffectId,
419 params: EffectParams<'a>,
420 resources: EffectResources,
421 runtime: EffectRuntime,
422 ) -> RenderNodeId {
423 let key = effect_cache_key_with_resources(
424 b"generator",
425 self.size,
426 &effect,
427 &[],
428 ¶ms,
429 &resources,
430 runtime,
431 );
432 self.push(
433 RenderNode::Generator {
434 effect,
435 params,
436 resources,
437 runtime,
438 },
439 key,
440 true,
441 )
442 }
443
444 pub fn add_vector(
452 &mut self,
453 scene: std::sync::Arc<alpha2_geometry::VectorScene>,
454 ) -> RenderNodeId {
455 let key = vector_cache_key(self.size, scene.key);
456 self.push(RenderNode::Vector { scene }, key, true)
457 }
458
459 pub fn add_transition(
464 &mut self,
465 effect: EffectId,
466 from: RenderNodeId,
467 to: RenderNodeId,
468 params: EffectParams<'a>,
469 runtime: EffectRuntime,
470 ) -> Result<RenderNodeId, GraphError> {
471 self.add_transition_with_resources(
472 effect,
473 from,
474 to,
475 params,
476 EffectResources::new(),
477 runtime,
478 )
479 }
480
481 pub fn add_transition_with_resources(
486 &mut self,
487 effect: EffectId,
488 from: RenderNodeId,
489 to: RenderNodeId,
490 params: EffectParams<'a>,
491 resources: EffectResources,
492 runtime: EffectRuntime,
493 ) -> Result<RenderNodeId, GraphError> {
494 self.validate_dependency(from)?;
495 self.validate_dependency(to)?;
496 let key = effect_cache_key_with_resources(
497 b"transition",
498 self.size,
499 &effect,
500 &[self.cache_keys[from.index], self.cache_keys[to.index]],
501 ¶ms,
502 &resources,
503 runtime,
504 );
505 let stable = self.stable[from.index] && self.stable[to.index];
506 Ok(self.push(
507 RenderNode::Transition {
508 effect,
509 from,
510 to,
511 params,
512 resources,
513 runtime,
514 },
515 key,
516 stable,
517 ))
518 }
519
520 pub fn add_composite(&mut self, layers: Vec<GraphLayer>) -> Result<RenderNodeId, GraphError> {
525 self.push_composite(layers, None)
526 }
527
528 pub fn add_composite_over(
539 &mut self,
540 layers: Vec<GraphLayer>,
541 background: Color,
542 ) -> Result<RenderNodeId, GraphError> {
543 if !background.is_valid() {
544 return Err(GraphError::Render(RenderError::InvalidColor));
545 }
546 self.push_composite(layers, Some(background))
547 }
548
549 fn push_composite(
550 &mut self,
551 layers: Vec<GraphLayer>,
552 background: Option<Color>,
553 ) -> Result<RenderNodeId, GraphError> {
554 for layer in &layers {
555 self.validate_dependency(layer.source)?;
556 }
557 let key = composite_cache_key(self.size, &layers, background, &self.cache_keys);
558 let stable = layers.iter().all(|layer| self.stable[layer.source.index]);
559 Ok(self.push(RenderNode::Composite { layers, background }, key, stable))
560 }
561
562 pub fn set_output(&mut self, output: RenderNodeId) -> Result<(), GraphError> {
567 self.validate_dependency(output)?;
568 self.output = Some(output);
569 Ok(())
570 }
571
572 #[must_use]
574 pub const fn output(&self) -> Option<RenderNodeId> {
575 self.output
576 }
577
578 #[must_use]
580 pub fn node_count(&self) -> usize {
581 self.nodes.len()
582 }
583
584 #[must_use]
586 pub fn node_kind(&self, id: RenderNodeId) -> Option<RenderNodeKind> {
587 if id.graph == self.id {
588 self.nodes.get(id.index).map(RenderNode::kind)
589 } else {
590 None
591 }
592 }
593
594 pub fn set_cache_intent(
602 &mut self,
603 node: RenderNodeId,
604 intent: crate::CacheIntent,
605 ) -> Result<(), GraphError> {
606 self.validate_dependency(node)?;
607 self.intents[node.index] = Some(intent);
608 Ok(())
609 }
610
611 pub fn cache_eligibility(
616 &self,
617 node: RenderNodeId,
618 ) -> Result<crate::CacheEligibility, GraphError> {
619 self.validate_dependency(node)?;
620 Ok(self.eligibility()[node.index])
621 }
622
623 pub(crate) fn eligibility(&self) -> Vec<crate::CacheEligibility> {
626 let count = self.nodes.len();
627 let mut bypassed: Vec<bool> = self
628 .intents
629 .iter()
630 .map(|intent| *intent == Some(crate::CacheIntent::Bypass))
631 .collect();
632 for index in 0..count {
634 if self.nodes[index]
635 .dependencies()
636 .iter()
637 .any(|input| bypassed[input.index])
638 {
639 bypassed[index] = true;
640 }
641 }
642 let mut down: Vec<bool> = self
644 .intents
645 .iter()
646 .map(|intent| *intent == Some(crate::CacheIntent::Bypass))
647 .collect();
648 for index in (0..count).rev() {
649 if down[index] {
650 for input in self.nodes[index].dependencies() {
651 down[input.index] = true;
652 }
653 }
654 }
655 (0..count)
656 .map(|index| {
657 if bypassed[index] || down[index] {
658 crate::CacheEligibility::Bypassed
659 } else if self.stable[index] {
660 crate::CacheEligibility::Eligible
661 } else {
662 crate::CacheEligibility::TransientSource
663 }
664 })
665 .collect()
666 }
667
668 fn push(
669 &mut self,
670 node: RenderNode<'a>,
671 cache_key: RenderCacheKey,
672 stable: bool,
673 ) -> RenderNodeId {
674 let id = RenderNodeId {
675 graph: self.id,
676 index: self.nodes.len(),
677 };
678 self.nodes.push(node);
679 self.cache_keys.push(cache_key);
680 self.stable.push(stable);
681 self.intents.push(None);
682 id
683 }
684
685 fn validate_dependency(&self, dependency: RenderNodeId) -> Result<(), GraphError> {
686 if dependency.graph == self.id && dependency.index < self.nodes.len() {
687 Ok(())
688 } else {
689 Err(GraphError::InvalidDependency)
690 }
691 }
692}
693
694impl RenderNodeKind {
695 pub(crate) const fn pass_label(self) -> &'static str {
698 match self {
699 Self::Source => "source",
700 Self::Effect => "effect",
701 Self::Transition => "transition",
702 Self::Generator => "generator",
703 Self::Composite => "composite",
704 Self::Vector => "vector",
705 }
706 }
707}
708
709impl RenderNode<'_> {
710 pub(crate) const fn kind(&self) -> RenderNodeKind {
711 match self {
712 Self::Source(_) => RenderNodeKind::Source,
713 Self::Effect { .. } => RenderNodeKind::Effect,
714 Self::Transition { .. } => RenderNodeKind::Transition,
715 Self::Generator { .. } => RenderNodeKind::Generator,
716 Self::Composite { .. } => RenderNodeKind::Composite,
717 Self::Vector { .. } => RenderNodeKind::Vector,
718 }
719 }
720
721 pub(crate) fn dependencies(&self) -> Vec<RenderNodeId> {
722 match self {
723 Self::Source(_) | Self::Generator { .. } | Self::Vector { .. } => Vec::new(),
724 Self::Effect { source, inputs, .. } => std::iter::once(*source)
725 .chain(inputs.iter().map(|(_, input)| input))
726 .collect(),
727 Self::Transition { from, to, .. } => vec![*from, *to],
728 Self::Composite { layers, .. } => layers.iter().map(|layer| layer.source).collect(),
729 }
730 }
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[test]
738 fn every_node_and_setting_refuses_a_node_of_another_graph_or_one_not_yet_added() {
739 let size = RenderSize::new(16, 9).expect("non-zero size");
742 let effect = EffectId::new("test.effect".into(), 1);
743 let mut other = RenderGraph::new(size);
744 let foreign = other.add_generator(
745 effect.clone(),
746 EffectParams::new(),
747 EffectRuntime::default(),
748 );
749 let mut graph = RenderGraph::new(size);
750 let own = graph.add_generator(
751 effect.clone(),
752 EffectParams::new(),
753 EffectRuntime::default(),
754 );
755 assert_eq!(foreign.index, own.index);
757 let unknown = RenderNodeId {
758 graph: graph.id,
759 index: 1,
760 };
761 let layer = |source| GraphLayer {
762 source,
763 visual: crate::LayerTransform {
764 position: [0.5, 0.5],
765 anchor: [0.5, 0.5],
766 scale: [1.0, 1.0],
767 rotation_degrees: 0.0,
768 opacity: 1.0,
769 },
770 blend_mode: crate::BlendMode::Normal,
771 };
772 for bad in [foreign, unknown] {
773 let invalid = Err(GraphError::InvalidDependency);
774 assert_eq!(
775 graph.add_effect(
776 effect.clone(),
777 bad,
778 EffectParams::new(),
779 EffectRuntime::default()
780 ),
781 invalid
782 );
783 assert_eq!(
784 graph.add_transition(
785 effect.clone(),
786 own,
787 bad,
788 EffectParams::new(),
789 EffectRuntime::default()
790 ),
791 invalid
792 );
793 assert_eq!(
794 graph.add_transition(
795 effect.clone(),
796 bad,
797 own,
798 EffectParams::new(),
799 EffectRuntime::default()
800 ),
801 invalid
802 );
803 assert_eq!(graph.add_composite(vec![layer(own), layer(bad)]), invalid);
804 assert_eq!(
805 graph.add_composite_over(vec![layer(bad)], Color::BLACK),
806 invalid
807 );
808 assert_eq!(graph.set_output(bad), Err(GraphError::InvalidDependency));
809 assert_eq!(
810 graph.set_cache_intent(bad, crate::CacheIntent::Pin),
811 Err(GraphError::InvalidDependency)
812 );
813 assert_eq!(
814 graph.cache_eligibility(bad),
815 Err(GraphError::InvalidDependency)
816 );
817 assert_eq!(graph.node_kind(bad), None);
818 }
819 assert_eq!(graph.node_count(), 1);
820 assert_eq!(graph.node_kind(own), Some(RenderNodeKind::Generator));
821 assert_eq!(
823 graph.add_composite_over(vec![layer(foreign)], Color::rgba(1.5, 0.0, 0.0, 1.0)),
824 Err(GraphError::Render(RenderError::InvalidColor))
825 );
826 assert_eq!(graph.node_count(), 1);
827 }
828
829 fn generator(graph: &mut RenderGraph<'_>, id: &'static str) -> RenderNodeId {
830 graph.add_generator(
831 EffectId::from_static(id, 1),
832 EffectParams::new(),
833 EffectRuntime::default(),
834 )
835 }
836
837 fn effect_with_inputs(
838 graph: &mut RenderGraph<'static>,
839 source: RenderNodeId,
840 inputs: EffectInputs<'static>,
841 ) -> Result<RenderNodeId, GraphError> {
842 graph.add_effect_with_inputs(
843 EffectId::from_static("test.multi-input", 1),
844 source,
845 inputs,
846 EffectParams::new(),
847 EffectResources::new(),
848 EffectRuntime::default(),
849 )
850 }
851
852 #[test]
853 fn named_inputs_are_dependencies() {
854 let size = RenderSize::new(16, 9).expect("non-zero size");
856 let mut graph = RenderGraph::new(size);
857 let source = generator(&mut graph, "test.source");
858 let map = generator(&mut graph, "test.map");
859 let unused = generator(&mut graph, "test.unused");
860 let effect = effect_with_inputs(&mut graph, source, EffectInputs::new().with("map", map))
861 .expect("earlier nodes");
862 graph.set_output(effect).expect("known output");
863 let plan = graph.compile().expect("compiles");
864 assert_eq!(plan.execution_order(), [source, map, effect]);
865 assert_eq!(plan.last_use(map), Some(2));
866 assert_eq!(plan.last_use(unused), None);
867 let later = RenderNodeId {
869 graph: graph.id,
870 index: graph.node_count(),
871 };
872 assert_eq!(
873 effect_with_inputs(&mut graph, source, EffectInputs::new().with("map", later)),
874 Err(GraphError::InvalidDependency)
875 );
876 }
877
878 #[test]
879 fn named_inputs_on_a_built_in_drawn_as_passes_are_refused() {
880 let size = RenderSize::new(16, 9).expect("non-zero size");
881 let mut graph = RenderGraph::new(size);
882 let source = generator(&mut graph, "test.source");
883 let map = generator(&mut graph, "test.map");
884 let count = graph.node_count();
885 for id in [crate::builtins::GAUSSIAN_BLUR_ID, crate::builtins::GLOW_ID] {
886 assert_eq!(
887 graph.add_effect_with_inputs(
888 EffectId::from_static(id, 1),
889 source,
890 EffectInputs::new().with("map", map),
891 EffectParams::new(),
892 EffectResources::new(),
893 EffectRuntime::default(),
894 ),
895 Err(GraphError::Render(RenderError::InvalidEffectParams)),
896 "{id}"
897 );
898 }
899 assert_eq!(graph.node_count(), count, "no pass is left behind");
900 }
901
902 #[test]
903 fn a_named_input_is_part_of_the_cache_key() {
904 let size = RenderSize::new(16, 9).expect("non-zero size");
905 let key = |build: &dyn Fn(&mut RenderGraph<'static>, [RenderNodeId; 3]) -> RenderNodeId| {
906 let mut graph = RenderGraph::new(size);
907 let nodes = [
908 generator(&mut graph, "test.source"),
909 generator(&mut graph, "test.map"),
910 generator(&mut graph, "test.other-map"),
911 ];
912 let node = build(&mut graph, nodes);
913 graph.cache_keys[node.index]
914 };
915 let named = |name: &'static str, input: usize| {
916 key(&move |graph, nodes| {
917 effect_with_inputs(
918 graph,
919 nodes[0],
920 EffectInputs::new().with(name, nodes[input]),
921 )
922 .expect("earlier nodes")
923 })
924 };
925 assert_eq!(named("map", 1), named("map", 1));
927 assert_ne!(named("map", 1), named("map", 2));
928 assert_ne!(named("map", 1), named("mask", 1));
929 let none = key(&|graph, nodes| {
930 effect_with_inputs(graph, nodes[0], EffectInputs::new()).expect("earlier node")
931 });
932 assert_ne!(named("map", 1), none);
933 let plain = key(&|graph, nodes| {
935 graph
936 .add_effect_with_resources(
937 EffectId::from_static("test.multi-input", 1),
938 nodes[0],
939 EffectParams::new(),
940 EffectResources::new(),
941 EffectRuntime::default(),
942 )
943 .expect("earlier node")
944 });
945 assert_eq!(none, plain);
946 }
947
948 #[test]
949 fn a_graph_compiles_only_once_an_output_is_chosen() {
950 let size = RenderSize::new(16, 9).expect("non-zero size");
951 let mut graph = RenderGraph::new(size);
952 let output = graph.add_composite(Vec::new()).expect("empty clear node");
953 assert_eq!(graph.compile().err(), Some(GraphError::MissingOutput));
954 assert_eq!(
955 GraphError::MissingOutput.into_render_error(),
956 RenderError::InvalidGraph
957 );
958 graph.set_output(output).expect("known output");
959 assert_eq!(
960 graph.compile().expect("compiles").execution_order(),
961 [output]
962 );
963 }
964
965 #[test]
966 fn quality_compilation_exposes_centered_temporal_schedule() {
967 let size = RenderSize::new(16, 9).expect("non-zero size");
968 let mut graph = RenderGraph::new(size);
969 let output = graph.add_composite(Vec::new()).expect("empty clear node");
970 graph.set_output(output).expect("known output");
971 let plan = graph
972 .compile_with_quality(crate::RenderQualityControls {
973 max_filter_samples: 12,
974 motion_blur_subframes: 4,
975 })
976 .expect("compiled");
977 assert_eq!(plan.temporal_offsets(), [-0.375, -0.125, 0.125, 0.375]);
978 assert_eq!(plan.filter_sample_limit(output), None);
979 }
980}