Skip to main content

alpha2_render/
graph.rs

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/// Stable handle to a node within one render graph.
15#[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    /// Returns the node's insertion-order index.
23    #[must_use]
24    pub const fn index(self) -> usize {
25        self.index
26    }
27}
28
29/// Public category of a render graph node.
30#[derive(Copy, Clone, Debug, Eq, PartialEq)]
31pub enum RenderNodeKind {
32    /// Externally owned input texture.
33    Source,
34    /// Registered effect with one source and any declared named inputs.
35    Effect,
36    /// Registered two-input transition.
37    Transition,
38    /// Registered zero-input generator.
39    Generator,
40    /// Ordered layer composition.
41    Composite,
42    /// A vector scene: fills drawn from tile coverage.
43    Vector,
44}
45
46/// Named images an effect node samples besides its source, sorted by name, one node per name.
47#[derive(Clone, Debug, Default, Eq, PartialEq)]
48pub struct EffectInputs<'a> {
49    values: Vec<(Cow<'a, str>, RenderNodeId)>,
50}
51
52impl<'a> EffectInputs<'a> {
53    /// Creates an empty set, without allocating.
54    #[must_use]
55    pub const fn new() -> Self {
56        Self { values: Vec::new() }
57    }
58
59    /// Adds or replaces one named input.
60    #[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    /// Adds or replaces one named input in place.
67    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    /// Whether no input is named.
79    #[must_use]
80    pub fn is_empty(&self) -> bool {
81        self.values.is_empty()
82    }
83
84    /// The number of named inputs.
85    #[must_use]
86    pub fn len(&self) -> usize {
87        self.values.len()
88    }
89
90    /// Every input by name, in name order.
91    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/// One owned layer reference in a graph composition node.
99#[derive(Copy, Clone, Debug)]
100pub struct GraphLayer {
101    /// Node whose result is sampled as this layer.
102    pub source: RenderNodeId,
103    /// Layer transform and opacity.
104    pub visual: LayerTransform,
105    /// Artistic blend operation.
106    pub blend_mode: BlendMode,
107}
108
109/// An insertion-ordered, acyclic render program.
110#[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    /// Whether each node's key is complete (no unkeyed source beneath it).
117    pub(crate) stable: Vec<bool>,
118    /// Each node's cache intent; `None` when not a candidate.
119    pub(crate) intents: Vec<Option<crate::CacheIntent>>,
120    pub(crate) output: Option<RenderNodeId>,
121    /// Blend space for this graph's composites; `None` uses the renderer's.
122    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/// Deterministic 128-bit key for a source or rendered graph result.
162#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
163pub struct RenderCacheKey(pub(crate) u128);
164
165impl RenderCacheKey {
166    /// Creates a stable source key supplied by a decode or asset cache.
167    #[must_use]
168    pub const fn source(value: u64) -> Self {
169        Self(value as u128)
170    }
171
172    /// Creates a key from all 128 bits. Keys not derived from a decoded source set high bits, so
173    /// they never equal a [`Self::source`] key.
174    #[must_use]
175    pub const fn from_value(value: u128) -> Self {
176        Self(value)
177    }
178
179    /// Returns the stable 128-bit representation.
180    #[must_use]
181    pub const fn value(self) -> u128 {
182        self.0
183    }
184}
185
186/// Structural or execution failure in a render graph.
187#[derive(Copy, Clone, Debug, Eq, PartialEq)]
188#[non_exhaustive]
189pub enum GraphError {
190    /// A node references an ID not already present in the graph.
191    InvalidDependency,
192    /// No final output node was selected.
193    MissingOutput,
194    /// An external source cannot be returned as an owned render result.
195    SourceOutput,
196    /// A registered shader kind does not match its graph node.
197    EffectKindMismatch,
198    /// A rendering operation failed.
199    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    /// Converts to [`RenderError`], reporting structural faults as [`RenderError::InvalidGraph`].
237    #[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    /// Creates an empty graph whose rendered nodes share one output size.
251    #[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    /// The size every rendered node of this graph has.
266    #[must_use]
267    pub const fn size(&self) -> RenderSize {
268        self.size
269    }
270
271    /// Blends this graph's composites in `space` instead of the renderer default, so preview and
272    /// export match.
273    pub fn set_blend_space(&mut self, space: crate::BlendSpace) {
274        self.blend_space = Some(space);
275    }
276
277    /// Adds an externally owned source texture.
278    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    /// Adds an external texture with a stable cache key.
284    ///
285    /// The key must cover everything the pixels depend on (content, frame, decode and conversion);
286    /// results drawn from it can then be cached.
287    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    /// Adds a registered one-input effect node.
296    ///
297    /// # Errors
298    /// Returns [`GraphError::InvalidDependency`] unless `source` is earlier.
299    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    /// Adds a registered one-input effect node with external resources.
310    ///
311    /// Built-in Gaussian blur and glow expand into internal passes; the last is returned.
312    ///
313    /// # Errors
314    /// [`GraphError::InvalidDependency`] unless `source` is earlier; for an expanded built-in, also
315    /// [`RenderError::InvalidEffectParams`] for unknown or out-of-range parameters.
316    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    /// Adds a registered effect node that also samples `inputs` by their declared names.
335    ///
336    /// # Errors
337    /// [`GraphError::InvalidDependency`] unless `source` and every input are earlier,
338    /// [`RenderError::InvalidEffectParams`] for named inputs on an expanded built-in, and otherwise
339    /// the errors of [`Self::add_effect_with_resources`].
340    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        // Expanded built-ins take no resources or inputs; check before expanding so no node is
354        // left.
355        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, &params, 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, &params, &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    /// Adds a registered generator node drawn from its parameters at the graph's size.
390    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    /// Adds a registered generator node that samples external resources by declared name.
400    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            &params,
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    /// Adds a vector scene drawn from tile coverage over transparency
429    /// (see [`alpha2_geometry::VectorScene`]).
430    ///
431    /// Keyed by the scene, so an unchanged scene is a cache hit. Scene coordinates are this graph's
432    /// pixels.
433    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    /// Adds a registered two-input transition node.
442    ///
443    /// # Errors
444    /// Returns [`GraphError::InvalidDependency`] unless both inputs are earlier.
445    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    /// Adds a registered two-input transition node with external resources.
464    ///
465    /// # Errors
466    /// Returns [`GraphError::InvalidDependency`] unless both inputs are earlier.
467    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            &params,
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    /// Adds an ordered layer-composition node over a transparent target.
503    ///
504    /// # Errors
505    /// Returns [`GraphError::InvalidDependency`] for a missing layer source.
506    pub fn add_composite(&mut self, layers: Vec<GraphLayer>) -> Result<RenderNodeId, GraphError> {
507        self.push_composite(layers, None)
508    }
509
510    /// Adds an ordered layer-composition node over a solid background.
511    ///
512    /// `background` acts as the bottom layer and is a premultiplied sRGB storage value, like
513    /// [`crate::Renderer::render_solid`]'s color.
514    ///
515    /// # Errors
516    /// [`GraphError::InvalidDependency`] for a missing layer source, or
517    /// [`RenderError::InvalidColor`] for an out-of-range or non-premultiplied background.
518    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    /// Selects the graph result.
543    ///
544    /// # Errors
545    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
546    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    /// The selected graph result, if one was selected.
553    #[must_use]
554    pub const fn output(&self) -> Option<RenderNodeId> {
555        self.output
556    }
557
558    /// Returns the number of nodes in insertion order.
559    #[must_use]
560    pub fn node_count(&self) -> usize {
561        self.nodes.len()
562    }
563
564    /// Returns the public category of a node.
565    #[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    /// Makes `node` a cache candidate under `intent`, used when its key is complete.
575    /// [`crate::CacheIntent::Bypass`] excludes the node, its inputs and its dependents.
576    ///
577    /// # Errors
578    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
579    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    /// Whether `node`'s result can be cached, and if not, why.
590    ///
591    /// # Errors
592    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
593    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    /// Every node's eligibility. A bypass spreads down to inputs and up to dependents; a transient
602    /// source only up.
603    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        // Up: nodes are in dependency order, so inputs come first.
611        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        // Down: from each explicitly bypassed node to its inputs.
621        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    /// Label for pass timing ([`crate::PassTiming::label`]).
674    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        // Node ids carry their graph; every method must refuse another graph's node with the same
717        // index.
718        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        // The same index as a node of this graph, but another graph's.
733        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        // An out-of-range background is refused before its layers are looked at.
799        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        // A node read only as a named input is drawn before the effect.
832        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}