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#[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 #[must_use]
22 pub const fn index(self) -> usize {
23 self.index
24 }
25}
26
27#[derive(Copy, Clone, Debug, Eq, PartialEq)]
29pub enum RenderNodeKind {
30 Source,
32 Effect,
34 Transition,
36 Generator,
38 Composite,
40}
41
42#[derive(Copy, Clone, Debug)]
44pub struct GraphLayer {
45 pub source: RenderNodeId,
47 pub visual: LayerTransform,
49 pub blend_mode: BlendMode,
51}
52
53#[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 pub(crate) stable: Vec<bool>,
63 pub(crate) intents: Vec<Option<crate::CacheIntent>>,
65 pub(crate) output: Option<RenderNodeId>,
66 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#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
105pub struct RenderCacheKey(pub(crate) u128);
106
107impl RenderCacheKey {
108 #[must_use]
110 pub const fn source(value: u64) -> Self {
111 Self(value as u128)
112 }
113
114 #[must_use]
118 pub const fn from_value(value: u128) -> Self {
119 Self(value)
120 }
121
122 #[must_use]
124 pub const fn value(self) -> u128 {
125 self.0
126 }
127}
128
129#[derive(Copy, Clone, Debug, Eq, PartialEq)]
131#[non_exhaustive]
132pub enum GraphError {
133 InvalidDependency,
135 MissingOutput,
137 SourceOutput,
139 EffectKindMismatch,
141 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 #[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 #[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 pub fn set_blend_space(&mut self, space: crate::BlendSpace) {
213 self.blend_space = Some(space);
214 }
215
216 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 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 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 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 if let Some(expanded) = self.add_expanded_effect(&effect, source, ¶ms, 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 ¶ms,
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 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 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 ¶ms,
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 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 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 ¶ms,
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 pub fn add_composite(&mut self, layers: Vec<GraphLayer>) -> Result<RenderNodeId, GraphError> {
404 self.push_composite(layers, None)
405 }
406
407 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 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 #[must_use]
453 pub fn node_count(&self) -> usize {
454 self.nodes.len()
455 }
456
457 #[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 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 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 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 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 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 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 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 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 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}