1#[cfg(feature = "wgpu")]
2mod graph_inner;
3
4use crate::nodes::RenderNodeCpu;
5
6#[cfg(feature = "wgpu")]
7use crate::error::RenderError;
8
9#[cfg(feature = "wgpu")]
10use crate::context::RenderContext;
11#[cfg(feature = "wgpu")]
12use crate::nodes::RenderNode;
13#[cfg(feature = "wgpu")]
14use std::sync::Arc;
15
16pub struct RenderGraph {
37 cpu_nodes: Vec<Box<dyn RenderNodeCpu>>,
39 #[cfg(feature = "wgpu")]
40 gpu_nodes: Vec<Box<dyn RenderNode>>,
41 #[cfg(feature = "wgpu")]
43 ctx: Option<Arc<RenderContext>>,
44}
45
46impl RenderGraph {
47 #[cfg(feature = "wgpu")]
53 #[must_use]
54 pub fn new(ctx: Arc<RenderContext>) -> Self {
55 Self {
56 cpu_nodes: Vec::new(),
57 gpu_nodes: Vec::new(),
58 ctx: Some(ctx),
59 }
60 }
61
62 #[must_use]
68 pub fn new_cpu() -> Self {
69 Self {
70 cpu_nodes: Vec::new(),
71 #[cfg(feature = "wgpu")]
72 gpu_nodes: Vec::new(),
73 #[cfg(feature = "wgpu")]
74 ctx: None,
75 }
76 }
77
78 #[cfg(feature = "wgpu")]
84 #[must_use]
85 pub fn push(mut self, node: impl RenderNode + 'static) -> Self {
86 self.gpu_nodes.push(Box::new(node));
87 self
88 }
89
90 #[cfg(not(feature = "wgpu"))]
97 #[must_use]
98 pub fn push(mut self, node: impl RenderNodeCpu + 'static) -> Self {
99 self.cpu_nodes.push(Box::new(node));
100 self
101 }
102
103 #[must_use]
105 pub fn push_cpu(mut self, node: impl RenderNodeCpu + 'static) -> Self {
106 self.cpu_nodes.push(Box::new(node));
107 self
108 }
109
110 #[cfg(feature = "wgpu")]
121 pub fn process_gpu(&self, rgba: &[u8], w: u32, h: u32) -> Result<Vec<u8>, RenderError> {
122 let ctx = self.ctx.as_ref().ok_or_else(|| RenderError::Composite {
123 message: "process_gpu called on a CPU-only RenderGraph (no RenderContext)".to_string(),
124 })?;
125 graph_inner::run_gpu(&self.gpu_nodes, ctx, rgba, w, h)
126 }
127
128 #[must_use]
134 pub fn process_cpu(&self, rgba: &[u8], w: u32, h: u32) -> Vec<u8> {
135 let mut out = rgba.to_vec();
136
137 for node in &self.cpu_nodes {
138 node.process_cpu(&mut out, w, h);
139 }
140
141 #[cfg(feature = "wgpu")]
142 for node in &self.gpu_nodes {
143 node.process_cpu(&mut out, w, h);
144 }
145
146 out
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::nodes::ColorGradeNode;
154
155 #[test]
156 fn render_graph_empty_cpu_should_return_input_unchanged() {
157 let graph = RenderGraph::new_cpu();
158 let rgba = vec![100u8, 150, 200, 255];
159 let result = graph.process_cpu(&rgba, 1, 1);
160 assert_eq!(result, rgba, "empty graph must return input unchanged");
161 }
162
163 #[test]
164 fn render_graph_push_cpu_color_grade_should_brighten() {
165 let graph = RenderGraph::new_cpu().push_cpu(ColorGradeNode::new(0.5, 1.0, 1.0, 0.0, 0.0));
166 let rgba = vec![128u8, 128, 128, 255];
167 let result = graph.process_cpu(&rgba, 1, 1);
168 assert!(
169 result[0] > 128,
170 "brightness +0.5 must increase R; got {}",
171 result[0]
172 );
173 }
174
175 #[test]
176 fn render_graph_multiple_cpu_nodes_should_chain() {
177 let graph = RenderGraph::new_cpu()
179 .push_cpu(ColorGradeNode::new(0.1, 1.0, 1.0, 0.0, 0.0))
180 .push_cpu(ColorGradeNode::new(0.1, 1.0, 1.0, 0.0, 0.0));
181 let single = RenderGraph::new_cpu().push_cpu(ColorGradeNode::new(0.2, 1.0, 1.0, 0.0, 0.0));
182
183 let rgba = vec![100u8, 100, 100, 255];
184 let chained = graph.process_cpu(&rgba, 1, 1);
185 let single_result = single.process_cpu(&rgba, 1, 1);
186
187 let diff = (chained[0] as i32 - single_result[0] as i32).abs();
189 assert!(
190 diff <= 2,
191 "chained vs single brightness boost must be close; got chained={} single={}",
192 chained[0],
193 single_result[0]
194 );
195 }
196}