Skip to main content

alpha2_render/
graph.rs

1use std::fmt;
2use std::sync::atomic::{AtomicU32, Ordering};
3
4use crate::graph_cache::{
5    composite_cache_key, effect_cache_key_with_resources, transient_source_key,
6};
7use crate::{
8    BlendMode, Color, EffectId, EffectParams, EffectResources, EffectRuntime, LayerTransform,
9    RenderError, RenderSize, SourceView,
10};
11
12/// Stable handle to a node within one render graph.
13#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
14pub struct RenderNodeId {
15    pub(crate) graph: u32,
16    pub(crate) index: usize,
17}
18
19impl RenderNodeId {
20    /// Returns the node's insertion-order index.
21    #[must_use]
22    pub const fn index(self) -> usize {
23        self.index
24    }
25}
26
27/// Public category of a render graph node.
28#[derive(Copy, Clone, Debug, Eq, PartialEq)]
29pub enum RenderNodeKind {
30    /// Externally owned input texture.
31    Source,
32    /// Registered one-input effect.
33    Effect,
34    /// Registered two-input transition.
35    Transition,
36    /// Registered zero-input generator.
37    Generator,
38    /// Ordered layer composition.
39    Composite,
40}
41
42/// One owned layer reference in a graph composition node.
43#[derive(Copy, Clone, Debug)]
44pub struct GraphLayer {
45    /// Node whose result is sampled as this layer.
46    pub source: RenderNodeId,
47    /// Layer transform and opacity.
48    pub visual: LayerTransform,
49    /// Artistic blend operation.
50    pub blend_mode: BlendMode,
51}
52
53/// An insertion-ordered, acyclic render program.
54#[derive(Debug)]
55pub struct RenderGraph<'a> {
56    pub(crate) id: u32,
57    pub(crate) size: RenderSize,
58    pub(crate) nodes: Vec<RenderNode<'a>>,
59    pub(crate) cache_keys: Vec<RenderCacheKey>,
60    /// Whether each node's key is complete: no source under it lacks a
61    /// stable key.
62    pub(crate) stable: Vec<bool>,
63    /// Each node's cache intent; `None` for a node that is not a candidate.
64    pub(crate) intents: Vec<Option<crate::CacheIntent>>,
65    pub(crate) output: Option<RenderNodeId>,
66    /// The space this graph's composites blend in; `None` takes the
67    /// renderer's.
68    pub(crate) blend_space: Option<crate::BlendSpace>,
69}
70
71static NEXT_GRAPH_ID: AtomicU32 = AtomicU32::new(1);
72
73#[derive(Debug)]
74pub(crate) enum RenderNode<'a> {
75    Source(&'a wgpu::TextureView),
76    Effect {
77        effect: EffectId,
78        source: RenderNodeId,
79        params: EffectParams<'a>,
80        resources: EffectResources,
81        runtime: EffectRuntime,
82    },
83    Transition {
84        effect: EffectId,
85        from: RenderNodeId,
86        to: RenderNodeId,
87        params: EffectParams<'a>,
88        resources: EffectResources,
89        runtime: EffectRuntime,
90    },
91    Generator {
92        effect: EffectId,
93        params: EffectParams<'a>,
94        resources: EffectResources,
95        runtime: EffectRuntime,
96    },
97    Composite {
98        layers: Vec<GraphLayer>,
99        background: Option<Color>,
100    },
101}
102
103/// Deterministic 128-bit key for a source or rendered graph result.
104#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
105pub struct RenderCacheKey(pub(crate) u128);
106
107impl RenderCacheKey {
108    /// Creates a stable source key supplied by a decode or asset cache.
109    #[must_use]
110    pub const fn source(value: u64) -> Self {
111        Self(value as u128)
112    }
113
114    /// Creates a key from all 128 bits, as [`Self::value`] gives them back. A
115    /// key a caller derives for something other than a decoded source sets
116    /// high bits, so it never equals a [`Self::source`] key.
117    #[must_use]
118    pub const fn from_value(value: u128) -> Self {
119        Self(value)
120    }
121
122    /// Returns the stable 128-bit representation.
123    #[must_use]
124    pub const fn value(self) -> u128 {
125        self.0
126    }
127}
128
129/// Structural or execution failure in a render graph.
130#[derive(Copy, Clone, Debug, Eq, PartialEq)]
131#[non_exhaustive]
132pub enum GraphError {
133    /// A node references an ID not already present in the graph.
134    InvalidDependency,
135    /// No final output node was selected.
136    MissingOutput,
137    /// An external source cannot be returned as an owned render result.
138    SourceOutput,
139    /// A registered shader kind does not match its graph node.
140    EffectKindMismatch,
141    /// A rendering operation failed.
142    Render(RenderError),
143}
144
145impl fmt::Display for GraphError {
146    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::InvalidDependency => {
149                formatter.write_str("render graph dependency must name an earlier node")
150            }
151            Self::MissingOutput => formatter.write_str("render graph has no selected output"),
152            Self::SourceOutput => {
153                formatter.write_str("render graph output must be a rendered node")
154            }
155            Self::EffectKindMismatch => {
156                formatter.write_str("registered effect kind does not match its graph node")
157            }
158            Self::Render(error) => write!(formatter, "render graph execution failed: {error}"),
159        }
160    }
161}
162
163impl std::error::Error for GraphError {
164    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
165        match self {
166            Self::Render(error) => Some(error),
167            _ => None,
168        }
169    }
170}
171
172impl From<RenderError> for GraphError {
173    fn from(error: RenderError) -> Self {
174        Self::Render(error)
175    }
176}
177
178impl GraphError {
179    /// Collapses a graph failure into the renderer's error type, reporting
180    /// every structural fault as [`RenderError::InvalidGraph`].
181    #[must_use]
182    pub const fn into_render_error(self) -> RenderError {
183        match self {
184            Self::Render(error) => error,
185            Self::InvalidDependency
186            | Self::MissingOutput
187            | Self::SourceOutput
188            | Self::EffectKindMismatch => RenderError::InvalidGraph,
189        }
190    }
191}
192
193impl<'a> RenderGraph<'a> {
194    /// Creates an empty graph whose rendered nodes share one output size.
195    #[must_use]
196    pub fn new(size: RenderSize) -> Self {
197        Self {
198            id: NEXT_GRAPH_ID.fetch_add(1, Ordering::Relaxed),
199            size,
200            nodes: Vec::new(),
201            cache_keys: Vec::new(),
202            stable: Vec::new(),
203            intents: Vec::new(),
204            output: None,
205            blend_space: None,
206        }
207    }
208
209    /// Blends this graph's composites in `space` rather than in the
210    /// renderer's default space. A timeline frame takes it from its project,
211    /// so preview and export blend alike whatever renderer draws them.
212    pub fn set_blend_space(&mut self, space: crate::BlendSpace) {
213        self.blend_space = Some(space);
214    }
215
216    /// Adds an externally owned source texture.
217    pub fn add_source(&mut self, texture: SourceView<'a>) -> RenderNodeId {
218        let key = transient_source_key(self.id, self.nodes.len());
219        self.push(RenderNode::Source(texture.wgpu_view()), key, false)
220    }
221
222    /// Adds an externally owned texture with a cross-frame stable cache key.
223    ///
224    /// The key is the caller's promise that the texture's pixels depend on
225    /// nothing it leaves out: the source's content, the frame, and how it was
226    /// decoded and converted. Results drawn from it can then be cached.
227    pub fn add_source_with_key(
228        &mut self,
229        texture: SourceView<'a>,
230        key: RenderCacheKey,
231    ) -> RenderNodeId {
232        self.push(RenderNode::Source(texture.wgpu_view()), key, true)
233    }
234
235    /// Adds a registered one-input effect node.
236    ///
237    /// # Errors
238    /// Returns [`GraphError::InvalidDependency`] unless `source` is earlier.
239    pub fn add_effect(
240        &mut self,
241        effect: EffectId,
242        source: RenderNodeId,
243        params: EffectParams<'a>,
244        runtime: EffectRuntime,
245    ) -> Result<RenderNodeId, GraphError> {
246        self.add_effect_with_resources(effect, source, params, EffectResources::new(), runtime)
247    }
248
249    /// Adds a registered one-input effect node with external resources.
250    ///
251    /// The built-in Gaussian blur and glow are added as the internal passes
252    /// that draw them, and the returned node is the last of those.
253    ///
254    /// # Errors
255    /// Returns [`GraphError::InvalidDependency`] unless `source` is earlier;
256    /// for a built-in drawn as passes, also [`RenderError::InvalidEffectParams`]
257    /// when its parameters are not ones it declares or are out of range.
258    pub fn add_effect_with_resources(
259        &mut self,
260        effect: EffectId,
261        source: RenderNodeId,
262        params: EffectParams<'a>,
263        resources: EffectResources,
264        runtime: EffectRuntime,
265    ) -> Result<RenderNodeId, GraphError> {
266        self.validate_dependency(source)?;
267        // Some built-ins are drawn as several internal passes; they bind no resources.
268        if let Some(expanded) = self.add_expanded_effect(&effect, source, &params, runtime) {
269            if resources.iter().next().is_some() {
270                return Err(RenderError::InvalidEffectParams.into());
271            }
272            return expanded;
273        }
274        let key = effect_cache_key_with_resources(
275            b"effect",
276            self.size,
277            &effect,
278            &[self.cache_keys[source.index]],
279            &params,
280            &resources,
281            runtime,
282        );
283        let stable = self.stable[source.index];
284        Ok(self.push(
285            RenderNode::Effect {
286                effect,
287                source,
288                params,
289                resources,
290                runtime,
291            },
292            key,
293            stable,
294        ))
295    }
296
297    /// Adds a registered generator node: a picture drawn from its
298    /// parameters alone, at the graph's size.
299    pub fn add_generator(
300        &mut self,
301        effect: EffectId,
302        params: EffectParams<'a>,
303        runtime: EffectRuntime,
304    ) -> RenderNodeId {
305        self.add_generator_with_resources(effect, params, EffectResources::new(), runtime)
306    }
307
308    /// Adds a registered generator node that samples external resources,
309    /// bound by the names its definition declares, as an effect's are.
310    pub fn add_generator_with_resources(
311        &mut self,
312        effect: EffectId,
313        params: EffectParams<'a>,
314        resources: EffectResources,
315        runtime: EffectRuntime,
316    ) -> RenderNodeId {
317        let key = effect_cache_key_with_resources(
318            b"generator",
319            self.size,
320            &effect,
321            &[],
322            &params,
323            &resources,
324            runtime,
325        );
326        self.push(
327            RenderNode::Generator {
328                effect,
329                params,
330                resources,
331                runtime,
332            },
333            key,
334            true,
335        )
336    }
337
338    /// Adds a registered two-input transition node.
339    ///
340    /// # Errors
341    /// Returns [`GraphError::InvalidDependency`] unless both inputs are earlier.
342    pub fn add_transition(
343        &mut self,
344        effect: EffectId,
345        from: RenderNodeId,
346        to: RenderNodeId,
347        params: EffectParams<'a>,
348        runtime: EffectRuntime,
349    ) -> Result<RenderNodeId, GraphError> {
350        self.add_transition_with_resources(
351            effect,
352            from,
353            to,
354            params,
355            EffectResources::new(),
356            runtime,
357        )
358    }
359
360    /// Adds a registered two-input transition node with external resources.
361    ///
362    /// # Errors
363    /// Returns [`GraphError::InvalidDependency`] unless both inputs are earlier.
364    pub fn add_transition_with_resources(
365        &mut self,
366        effect: EffectId,
367        from: RenderNodeId,
368        to: RenderNodeId,
369        params: EffectParams<'a>,
370        resources: EffectResources,
371        runtime: EffectRuntime,
372    ) -> Result<RenderNodeId, GraphError> {
373        self.validate_dependency(from)?;
374        self.validate_dependency(to)?;
375        let key = effect_cache_key_with_resources(
376            b"transition",
377            self.size,
378            &effect,
379            &[self.cache_keys[from.index], self.cache_keys[to.index]],
380            &params,
381            &resources,
382            runtime,
383        );
384        let stable = self.stable[from.index] && self.stable[to.index];
385        Ok(self.push(
386            RenderNode::Transition {
387                effect,
388                from,
389                to,
390                params,
391                resources,
392                runtime,
393            },
394            key,
395            stable,
396        ))
397    }
398
399    /// Adds an ordered layer-composition node over a transparent target.
400    ///
401    /// # Errors
402    /// Returns [`GraphError::InvalidDependency`] for a missing layer source.
403    pub fn add_composite(&mut self, layers: Vec<GraphLayer>) -> Result<RenderNodeId, GraphError> {
404        self.push_composite(layers, None)
405    }
406
407    /// Adds an ordered layer-composition node over a solid background.
408    ///
409    /// `background` is the bottom layer: the first layer blends against it as
410    /// against any lower layer. Like [`crate::Renderer::render_solid`]'s
411    /// color, it is a premultiplied, sRGB-encoded storage value.
412    ///
413    /// # Errors
414    /// Returns [`GraphError::InvalidDependency`] for a missing layer source,
415    /// or [`RenderError::InvalidColor`] for a background out of range or not
416    /// premultiplied.
417    pub fn add_composite_over(
418        &mut self,
419        layers: Vec<GraphLayer>,
420        background: Color,
421    ) -> Result<RenderNodeId, GraphError> {
422        if !background.is_valid() {
423            return Err(GraphError::Render(RenderError::InvalidColor));
424        }
425        self.push_composite(layers, Some(background))
426    }
427
428    fn push_composite(
429        &mut self,
430        layers: Vec<GraphLayer>,
431        background: Option<Color>,
432    ) -> Result<RenderNodeId, GraphError> {
433        for layer in &layers {
434            self.validate_dependency(layer.source)?;
435        }
436        let key = composite_cache_key(self.size, &layers, background, &self.cache_keys);
437        let stable = layers.iter().all(|layer| self.stable[layer.source.index]);
438        Ok(self.push(RenderNode::Composite { layers, background }, key, stable))
439    }
440
441    /// Selects the graph result.
442    ///
443    /// # Errors
444    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
445    pub fn set_output(&mut self, output: RenderNodeId) -> Result<(), GraphError> {
446        self.validate_dependency(output)?;
447        self.output = Some(output);
448        Ok(())
449    }
450
451    /// Returns the number of nodes in insertion order.
452    #[must_use]
453    pub fn node_count(&self) -> usize {
454        self.nodes.len()
455    }
456
457    /// Returns the public category of a node.
458    #[must_use]
459    pub fn node_kind(&self, id: RenderNodeId) -> Option<RenderNodeKind> {
460        if id.graph == self.id {
461            self.nodes.get(id.index).map(RenderNode::kind)
462        } else {
463            None
464        }
465    }
466
467    /// Names `node` a cache candidate: its result is looked up before it is
468    /// drawn and kept after, as `intent` says, when its key is complete.
469    /// [`crate::CacheIntent::Bypass`] instead keeps it, everything it is drawn
470    /// from, and everything drawn from it out of the cache.
471    ///
472    /// # Errors
473    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
474    pub fn set_cache_intent(
475        &mut self,
476        node: RenderNodeId,
477        intent: crate::CacheIntent,
478    ) -> Result<(), GraphError> {
479        self.validate_dependency(node)?;
480        self.intents[node.index] = Some(intent);
481        Ok(())
482    }
483
484    /// Whether `node`'s result can be cached, and if not, why.
485    ///
486    /// # Errors
487    /// Returns [`GraphError::InvalidDependency`] for an unknown node.
488    pub fn cache_eligibility(
489        &self,
490        node: RenderNodeId,
491    ) -> Result<crate::CacheEligibility, GraphError> {
492        self.validate_dependency(node)?;
493        Ok(self.eligibility()[node.index])
494    }
495
496    /// Every node's eligibility. A bypass reaches down to what a node is
497    /// drawn from and up to what is drawn from it; a transient source only up.
498    pub(crate) fn eligibility(&self) -> Vec<crate::CacheEligibility> {
499        let count = self.nodes.len();
500        let mut bypassed: Vec<bool> = self
501            .intents
502            .iter()
503            .map(|intent| *intent == Some(crate::CacheIntent::Bypass))
504            .collect();
505        // Up: nodes are in dependency order, so inputs come first.
506        for index in 0..count {
507            if self.nodes[index]
508                .dependencies()
509                .iter()
510                .any(|input| bypassed[input.index])
511            {
512                bypassed[index] = true;
513            }
514        }
515        // Down: from each explicitly bypassed node to its inputs.
516        let mut down: Vec<bool> = self
517            .intents
518            .iter()
519            .map(|intent| *intent == Some(crate::CacheIntent::Bypass))
520            .collect();
521        for index in (0..count).rev() {
522            if down[index] {
523                for input in self.nodes[index].dependencies() {
524                    down[input.index] = true;
525                }
526            }
527        }
528        (0..count)
529            .map(|index| {
530                if bypassed[index] || down[index] {
531                    crate::CacheEligibility::Bypassed
532                } else if self.stable[index] {
533                    crate::CacheEligibility::Eligible
534                } else {
535                    crate::CacheEligibility::TransientSource
536                }
537            })
538            .collect()
539    }
540
541    fn push(
542        &mut self,
543        node: RenderNode<'a>,
544        cache_key: RenderCacheKey,
545        stable: bool,
546    ) -> RenderNodeId {
547        let id = RenderNodeId {
548            graph: self.id,
549            index: self.nodes.len(),
550        };
551        self.nodes.push(node);
552        self.cache_keys.push(cache_key);
553        self.stable.push(stable);
554        self.intents.push(None);
555        id
556    }
557
558    fn validate_dependency(&self, dependency: RenderNodeId) -> Result<(), GraphError> {
559        if dependency.graph == self.id && dependency.index < self.nodes.len() {
560            Ok(())
561        } else {
562            Err(GraphError::InvalidDependency)
563        }
564    }
565}
566
567impl RenderNodeKind {
568    /// The label a pass of this kind is timed under (see
569    /// [`crate::PassTiming::label`]).
570    pub(crate) const fn pass_label(self) -> &'static str {
571        match self {
572            Self::Source => "source",
573            Self::Effect => "effect",
574            Self::Transition => "transition",
575            Self::Generator => "generator",
576            Self::Composite => "composite",
577        }
578    }
579}
580
581impl RenderNode<'_> {
582    pub(crate) const fn kind(&self) -> RenderNodeKind {
583        match self {
584            Self::Source(_) => RenderNodeKind::Source,
585            Self::Effect { .. } => RenderNodeKind::Effect,
586            Self::Transition { .. } => RenderNodeKind::Transition,
587            Self::Generator { .. } => RenderNodeKind::Generator,
588            Self::Composite { .. } => RenderNodeKind::Composite,
589        }
590    }
591
592    pub(crate) fn dependencies(&self) -> Vec<RenderNodeId> {
593        match self {
594            Self::Source(_) | Self::Generator { .. } => Vec::new(),
595            Self::Effect { source, .. } => vec![*source],
596            Self::Transition { from, to, .. } => vec![*from, *to],
597            Self::Composite { layers, .. } => layers.iter().map(|layer| layer.source).collect(),
598        }
599    }
600}
601
602#[cfg(test)]
603mod tests {
604    use super::*;
605
606    #[test]
607    fn every_node_and_setting_refuses_a_node_of_another_graph_or_one_not_yet_added() {
608        // A node id carries its graph, so a dependency check that compared only indices
609        // would wire one graph's node into another; every method taking a node refuses it.
610        let size = RenderSize::new(16, 9).expect("non-zero size");
611        let effect = EffectId::new("test.effect".into(), 1);
612        let mut other = RenderGraph::new(size);
613        let foreign = other.add_generator(
614            effect.clone(),
615            EffectParams::new(),
616            EffectRuntime::default(),
617        );
618        let mut graph = RenderGraph::new(size);
619        let own = graph.add_generator(
620            effect.clone(),
621            EffectParams::new(),
622            EffectRuntime::default(),
623        );
624        // The same index as a node of this graph, but another graph's.
625        assert_eq!(foreign.index, own.index);
626        let unknown = RenderNodeId {
627            graph: graph.id,
628            index: 1,
629        };
630        let layer = |source| GraphLayer {
631            source,
632            visual: crate::LayerTransform {
633                position: [0.5, 0.5],
634                anchor: [0.5, 0.5],
635                scale: [1.0, 1.0],
636                rotation_degrees: 0.0,
637                opacity: 1.0,
638            },
639            blend_mode: crate::BlendMode::Normal,
640        };
641        for bad in [foreign, unknown] {
642            let invalid = Err(GraphError::InvalidDependency);
643            assert_eq!(
644                graph.add_effect(
645                    effect.clone(),
646                    bad,
647                    EffectParams::new(),
648                    EffectRuntime::default()
649                ),
650                invalid
651            );
652            assert_eq!(
653                graph.add_transition(
654                    effect.clone(),
655                    own,
656                    bad,
657                    EffectParams::new(),
658                    EffectRuntime::default()
659                ),
660                invalid
661            );
662            assert_eq!(
663                graph.add_transition(
664                    effect.clone(),
665                    bad,
666                    own,
667                    EffectParams::new(),
668                    EffectRuntime::default()
669                ),
670                invalid
671            );
672            assert_eq!(graph.add_composite(vec![layer(own), layer(bad)]), invalid);
673            assert_eq!(
674                graph.add_composite_over(vec![layer(bad)], Color::BLACK),
675                invalid
676            );
677            assert_eq!(graph.set_output(bad), Err(GraphError::InvalidDependency));
678            assert_eq!(
679                graph.set_cache_intent(bad, crate::CacheIntent::Pin),
680                Err(GraphError::InvalidDependency)
681            );
682            assert_eq!(
683                graph.cache_eligibility(bad),
684                Err(GraphError::InvalidDependency)
685            );
686            assert_eq!(graph.node_kind(bad), None);
687        }
688        assert_eq!(graph.node_count(), 1);
689        assert_eq!(graph.node_kind(own), Some(RenderNodeKind::Generator));
690        // An out-of-range background is refused before its layers are looked at.
691        assert_eq!(
692            graph.add_composite_over(vec![layer(foreign)], Color::rgba(1.5, 0.0, 0.0, 1.0)),
693            Err(GraphError::Render(RenderError::InvalidColor))
694        );
695        assert_eq!(graph.node_count(), 1);
696    }
697
698    #[test]
699    fn a_graph_compiles_only_once_an_output_is_chosen() {
700        let size = RenderSize::new(16, 9).expect("non-zero size");
701        let mut graph = RenderGraph::new(size);
702        let output = graph.add_composite(Vec::new()).expect("empty clear node");
703        assert_eq!(graph.compile().err(), Some(GraphError::MissingOutput));
704        assert_eq!(
705            GraphError::MissingOutput.into_render_error(),
706            RenderError::InvalidGraph
707        );
708        graph.set_output(output).expect("known output");
709        assert_eq!(
710            graph.compile().expect("compiles").execution_order(),
711            [output]
712        );
713    }
714
715    #[test]
716    fn quality_compilation_exposes_centered_temporal_schedule() {
717        let size = RenderSize::new(16, 9).expect("non-zero size");
718        let mut graph = RenderGraph::new(size);
719        let output = graph.add_composite(Vec::new()).expect("empty clear node");
720        graph.set_output(output).expect("known output");
721        let plan = graph
722            .compile_with_quality(crate::RenderQualityControls {
723                max_filter_samples: 12,
724                motion_blur_subframes: 4,
725            })
726            .expect("compiled");
727        assert_eq!(plan.temporal_offsets(), [-0.375, -0.125, 0.125, 0.375]);
728        assert_eq!(plan.filter_sample_limit(output), None);
729    }
730}