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: one source, and the named inputs its definition
35    /// declares.
36    Effect,
37    /// Registered two-input transition.
38    Transition,
39    /// Registered zero-input generator.
40    Generator,
41    /// Ordered layer composition.
42    Composite,
43    /// A vector scene: fills drawn from tile coverage.
44    Vector,
45}
46
47/// Named images one effect node samples besides its source, by the names its
48/// definition declares. Kept sorted by name, one node per name; a name is
49/// borrowed for `'a` or owned, as [`EffectParams`] names are.
50#[derive(Clone, Debug, Default, Eq, PartialEq)]
51pub struct EffectInputs<'a> {
52    values: Vec<(Cow<'a, str>, RenderNodeId)>,
53}
54
55impl<'a> EffectInputs<'a> {
56    /// Creates an empty set, without allocating.
57    #[must_use]
58    pub const fn new() -> Self {
59        Self { values: Vec::new() }
60    }
61
62    /// Adds or replaces one named input.
63    #[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    /// Adds or replaces one named input in place.
70    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    /// Whether no input is named.
82    #[must_use]
83    pub fn is_empty(&self) -> bool {
84        self.values.is_empty()
85    }
86
87    /// The number of named inputs.
88    #[must_use]
89    pub fn len(&self) -> usize {
90        self.values.len()
91    }
92
93    /// Every input by name, in name order.
94    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/// One owned layer reference in a graph composition node.
102#[derive(Copy, Clone, Debug)]
103pub struct GraphLayer {
104    /// Node whose result is sampled as this layer.
105    pub source: RenderNodeId,
106    /// Layer transform and opacity.
107    pub visual: LayerTransform,
108    /// Artistic blend operation.
109    pub blend_mode: BlendMode,
110}
111
112/// An insertion-ordered, acyclic render program.
113#[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    /// Whether each node's key is complete: no source under it lacks a
120    /// stable key.
121    pub(crate) stable: Vec<bool>,
122    /// Each node's cache intent; `None` for a node that is not a candidate.
123    pub(crate) intents: Vec<Option<crate::CacheIntent>>,
124    pub(crate) output: Option<RenderNodeId>,
125    /// The space this graph's composites blend in; `None` takes the
126    /// renderer's.
127    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/// Deterministic 128-bit key for a source or rendered graph result.
167#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
168pub struct RenderCacheKey(pub(crate) u128);
169
170impl RenderCacheKey {
171    /// Creates a stable source key supplied by a decode or asset cache.
172    #[must_use]
173    pub const fn source(value: u64) -> Self {
174        Self(value as u128)
175    }
176
177    /// Creates a key from all 128 bits, as [`Self::value`] gives them back. A
178    /// key a caller derives for something other than a decoded source sets
179    /// high bits, so it never equals a [`Self::source`] key.
180    #[must_use]
181    pub const fn from_value(value: u128) -> Self {
182        Self(value)
183    }
184
185    /// Returns the stable 128-bit representation.
186    #[must_use]
187    pub const fn value(self) -> u128 {
188        self.0
189    }
190}
191
192/// Structural or execution failure in a render graph.
193#[derive(Copy, Clone, Debug, Eq, PartialEq)]
194#[non_exhaustive]
195pub enum GraphError {
196    /// A node references an ID not already present in the graph.
197    InvalidDependency,
198    /// No final output node was selected.
199    MissingOutput,
200    /// An external source cannot be returned as an owned render result.
201    SourceOutput,
202    /// A registered shader kind does not match its graph node.
203    EffectKindMismatch,
204    /// A rendering operation failed.
205    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    /// Collapses a graph failure into the renderer's error type, reporting
243    /// every structural fault as [`RenderError::InvalidGraph`].
244    #[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    /// Creates an empty graph whose rendered nodes share one output size.
258    #[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    /// The size every rendered node of this graph has.
273    #[must_use]
274    pub const fn size(&self) -> RenderSize {
275        self.size
276    }
277
278    /// Blends this graph's composites in `space` rather than in the
279    /// renderer's default space. A timeline frame takes it from its project,
280    /// so preview and export blend alike whatever renderer draws them.
281    pub fn set_blend_space(&mut self, space: crate::BlendSpace) {
282        self.blend_space = Some(space);
283    }
284
285    /// Adds an externally owned source texture.
286    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    /// Adds an externally owned texture with a cross-frame stable cache key.
292    ///
293    /// The key is the caller's promise that the texture's pixels depend on
294    /// nothing it leaves out: the source's content, the frame, and how it was
295    /// decoded and converted. Results drawn from it can then be cached.
296    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    /// Adds a registered one-input effect node.
305    ///
306    /// # Errors
307    /// Returns [`GraphError::InvalidDependency`] unless `source` is earlier.
308    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    /// Adds a registered one-input effect node with external resources.
319    ///
320    /// The built-in Gaussian blur and glow are added as the internal passes
321    /// that draw them, and the returned node is the last of those.
322    ///
323    /// # Errors
324    /// Returns [`GraphError::InvalidDependency`] unless `source` is earlier;
325    /// for a built-in drawn as passes, also [`RenderError::InvalidEffectParams`]
326    /// when its parameters are not ones it declares or are out of range.
327    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    /// Adds a registered effect node that also samples `inputs`, bound by the
346    /// names its definition declares.
347    ///
348    /// # Errors
349    /// Returns [`GraphError::InvalidDependency`] unless `source` and every
350    /// input are earlier, [`RenderError::InvalidEffectParams`] for named
351    /// inputs on a built-in drawn as passes, and otherwise the errors of
352    /// [`Self::add_effect_with_resources`].
353    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        // Some built-ins are drawn as several internal passes; they bind no resources and
367        // declare no inputs. Checked before they are expanded, so no node is left behind.
368        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, &params, 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        // Without named inputs the key is the one `add_effect_with_resources` gives.
379        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, &params, &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    /// Adds a registered generator node: a picture drawn from its
404    /// parameters alone, at the graph's size.
405    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    /// Adds a registered generator node that samples external resources,
415    /// bound by the names its definition declares, as an effect's are.
416    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            &params,
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    /// Adds a vector scene: its fills drawn from tile coverage at the
445    /// graph's size, over transparency (see [`alpha2_geometry::VectorScene`]).
446    /// Its key is the scene's, so an unchanged scene is a cache hit; its
447    /// picture is premultiplied and sRGB-encoded, as every node's.
448    ///
449    /// The scene's coordinates are this graph's pixels: one built for
450    /// another size draws at that size's coordinates.
451    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    /// Adds a registered two-input transition node.
460    ///
461    /// # Errors
462    /// Returns [`GraphError::InvalidDependency`] unless both inputs are earlier.
463    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    /// Adds a registered two-input transition node with external resources.
482    ///
483    /// # Errors
484    /// Returns [`GraphError::InvalidDependency`] unless both inputs are earlier.
485    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            &params,
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    /// Adds an ordered layer-composition node over a transparent target.
521    ///
522    /// # Errors
523    /// Returns [`GraphError::InvalidDependency`] for a missing layer source.
524    pub fn add_composite(&mut self, layers: Vec<GraphLayer>) -> Result<RenderNodeId, GraphError> {
525        self.push_composite(layers, None)
526    }
527
528    /// Adds an ordered layer-composition node over a solid background.
529    ///
530    /// `background` is the bottom layer: the first layer blends against it as
531    /// against any lower layer. Like [`crate::Renderer::render_solid`]'s
532    /// color, it is a premultiplied, sRGB-encoded storage value.
533    ///
534    /// # Errors
535    /// Returns [`GraphError::InvalidDependency`] for a missing layer source,
536    /// or [`RenderError::InvalidColor`] for a background out of range or not
537    /// premultiplied.
538    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    /// Selects the graph result.
563    ///
564    /// # Errors
565    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
566    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    /// The selected graph result, if one was selected.
573    #[must_use]
574    pub const fn output(&self) -> Option<RenderNodeId> {
575        self.output
576    }
577
578    /// Returns the number of nodes in insertion order.
579    #[must_use]
580    pub fn node_count(&self) -> usize {
581        self.nodes.len()
582    }
583
584    /// Returns the public category of a node.
585    #[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    /// Names `node` a cache candidate: its result is looked up before it is
595    /// drawn and kept after, as `intent` says, when its key is complete.
596    /// [`crate::CacheIntent::Bypass`] instead keeps it, everything it is drawn
597    /// from, and everything drawn from it out of the cache.
598    ///
599    /// # Errors
600    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
601    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    /// Whether `node`'s result can be cached, and if not, why.
612    ///
613    /// # Errors
614    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
615    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    /// Every node's eligibility. A bypass reaches down to what a node is
624    /// drawn from and up to what is drawn from it; a transient source only up.
625    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        // Up: nodes are in dependency order, so inputs come first.
633        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        // Down: from each explicitly bypassed node to its inputs.
643        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    /// The label a pass of this kind is timed under (see
696    /// [`crate::PassTiming::label`]).
697    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        // A node id carries its graph, so a dependency check that compared only indices
740        // would wire one graph's node into another; every method taking a node refuses it.
741        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        // The same index as a node of this graph, but another graph's.
756        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        // An out-of-range background is refused before its layers are looked at.
822        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        // A node read only as a named input is drawn before the effect and kept until it.
855        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        // An input must be an earlier node of this graph, as a source must.
868        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        // Equal inputs give equal keys in another graph; another node or name another key.
926        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        // Without named inputs, the key is the one `add_effect_with_resources` gives.
934        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}