1use std::time::Duration;
2
3use ff_preview::FrameSink;
4
5use crate::graph::RenderGraph;
6
7#[cfg(feature = "wgpu")]
14pub struct TextureHandle {
15 pub texture: wgpu::Texture,
16 pub view: wgpu::TextureView,
17 pub width: u32,
18 pub height: u32,
19}
20
21pub struct GpuFrameSink {
43 graph: RenderGraph,
44 downstream: Box<dyn FrameSink>,
45}
46
47impl GpuFrameSink {
48 #[must_use]
51 pub fn new(graph: RenderGraph, downstream: Box<dyn FrameSink>) -> Self {
52 Self { graph, downstream }
53 }
54}
55
56impl FrameSink for GpuFrameSink {
57 fn push_frame(&mut self, rgba: &[u8], width: u32, height: u32, pts: Duration) {
58 #[cfg(feature = "display")]
62 {
63 if self.downstream.accepts_gpu_frame() {
64 match self.graph.process_gpu_to_texture(rgba, width, height) {
65 Ok(handle) => {
66 self.downstream.push_frame_gpu(
67 &handle.texture,
68 &handle.view,
69 handle.width,
70 handle.height,
71 pts,
72 );
73 return;
74 }
75 Err(e) => {
76 log::warn!("GpuFrameSink zero-copy path failed, using readback error={e}");
77 }
78 }
79 }
80 }
81 #[cfg(feature = "wgpu")]
82 {
83 match self.graph.process_gpu(rgba, width, height) {
84 Ok(processed) => {
85 self.downstream.push_frame(&processed, width, height, pts);
86 return;
87 }
88 Err(e) => {
89 log::warn!("GpuFrameSink GPU processing failed, using CPU fallback error={e}");
90 }
91 }
92 }
93 let processed = self.graph.process_cpu(rgba, width, height);
95 self.downstream.push_frame(&processed, width, height, pts);
96 }
97
98 fn flush(&mut self) {
99 self.downstream.flush();
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::sync::{Arc, Mutex};
107
108 use crate::nodes::ColorGradeNode;
109
110 struct CollectSink(Arc<Mutex<Vec<Vec<u8>>>>);
111
112 impl FrameSink for CollectSink {
113 fn push_frame(&mut self, rgba: &[u8], _w: u32, _h: u32, _pts: Duration) {
114 self.0
115 .lock()
116 .unwrap_or_else(std::sync::PoisonError::into_inner)
117 .push(rgba.to_vec());
118 }
119 }
120
121 #[test]
122 fn gpu_frame_sink_cpu_path_should_forward_processed_frame() {
123 let graph = RenderGraph::new_cpu().push_cpu(ColorGradeNode::new(0.5, 1.0, 1.0, 0.0, 0.0));
125
126 let collected = Arc::new(Mutex::new(Vec::new()));
127 let downstream = Box::new(CollectSink(Arc::clone(&collected)));
128 let mut sink = GpuFrameSink::new(graph, downstream);
129
130 let pts = Duration::from_millis(0);
131 sink.push_frame(&[128u8, 128, 128, 255], 1, 1, pts);
134
135 let guard = collected
136 .lock()
137 .unwrap_or_else(std::sync::PoisonError::into_inner);
138 assert_eq!(guard.len(), 1, "exactly one frame must be forwarded");
139 assert!(
140 guard[0][0] > 128,
141 "brightness +0.5 must increase R channel; got {}",
142 guard[0][0]
143 );
144 }
145
146 #[test]
147 fn gpu_frame_sink_flush_should_propagate_to_downstream() {
148 struct FlushTracker(Arc<Mutex<bool>>);
149 impl FrameSink for FlushTracker {
150 fn push_frame(&mut self, _: &[u8], _: u32, _: u32, _: Duration) {}
151 fn flush(&mut self) {
152 *self
153 .0
154 .lock()
155 .unwrap_or_else(std::sync::PoisonError::into_inner) = true;
156 }
157 }
158
159 let flushed = Arc::new(Mutex::new(false));
160 let mut sink = GpuFrameSink::new(
161 RenderGraph::new_cpu(),
162 Box::new(FlushTracker(Arc::clone(&flushed))),
163 );
164 sink.flush();
165 assert!(
166 *flushed
167 .lock()
168 .unwrap_or_else(std::sync::PoisonError::into_inner),
169 "flush must propagate to downstream"
170 );
171 }
172
173 #[test]
174 fn gpu_frame_sink_should_be_send() {
175 fn assert_send<T: Send>() {}
176 assert_send::<GpuFrameSink>();
177 }
178}
179
180#[cfg(all(test, feature = "display"))]
181mod display_tests {
182 use std::sync::{Arc, Mutex, PoisonError};
183 use std::time::Duration;
184
185 use ff_preview::FrameSink;
186
187 use super::GpuFrameSink;
188 use crate::context::RenderContext;
189 use crate::graph::RenderGraph;
190 use crate::nodes::ColorGradeNode;
191
192 fn ctx() -> Option<Arc<RenderContext>> {
194 match futures::executor::block_on(RenderContext::init()) {
195 Ok(ctx) => Some(Arc::new(ctx)),
196 Err(_) => None,
197 }
198 }
199
200 struct GpuCapture(Arc<Mutex<Option<(u32, u32)>>>);
202 impl FrameSink for GpuCapture {
203 fn push_frame(&mut self, _rgba: &[u8], _w: u32, _h: u32, _pts: Duration) {
204 unreachable!("the zero-copy path must not call the CPU push_frame");
205 }
206 fn accepts_gpu_frame(&self) -> bool {
207 true
208 }
209 fn push_frame_gpu(
210 &mut self,
211 _texture: &wgpu::Texture,
212 _view: &wgpu::TextureView,
213 width: u32,
214 height: u32,
215 _pts: Duration,
216 ) {
217 *self.0.lock().unwrap_or_else(PoisonError::into_inner) = Some((width, height));
218 }
219 }
220
221 struct CpuCapture(Arc<Mutex<bool>>);
223 impl FrameSink for CpuCapture {
224 fn push_frame(&mut self, _rgba: &[u8], _w: u32, _h: u32, _pts: Duration) {
225 *self.0.lock().unwrap_or_else(PoisonError::into_inner) = true;
226 }
227 }
228
229 #[test]
230 fn gpu_sink_should_forward_a_texture_without_cpu_readback() {
231 let Some(ctx) = ctx() else {
232 return;
233 };
234 let graph =
235 RenderGraph::new(Arc::clone(&ctx)).push(ColorGradeNode::new(0.0, 1.0, 1.0, 0.0, 0.0));
236 let got = Arc::new(Mutex::new(None));
237 let mut sink = GpuFrameSink::new(graph, Box::new(GpuCapture(Arc::clone(&got))));
238
239 let (w, h) = (16u32, 16u32);
240 sink.push_frame(&vec![128u8; (w * h * 4) as usize], w, h, Duration::ZERO);
241
242 assert_eq!(
243 *got.lock().unwrap_or_else(PoisonError::into_inner),
244 Some((w, h)),
245 "the GPU downstream must receive the composited texture"
246 );
247 assert_eq!(
248 ctx.readback_count(),
249 0,
250 "the zero-copy display path must perform no GPU-to-CPU readback"
251 );
252 }
253
254 #[test]
255 fn gpu_sink_cpu_downstream_should_use_readback_path() {
256 let Some(ctx) = ctx() else {
257 return;
258 };
259 let graph =
260 RenderGraph::new(Arc::clone(&ctx)).push(ColorGradeNode::new(0.0, 1.0, 1.0, 0.0, 0.0));
261 let got = Arc::new(Mutex::new(false));
262 let mut sink = GpuFrameSink::new(graph, Box::new(CpuCapture(Arc::clone(&got))));
263
264 let (w, h) = (16u32, 16u32);
265 sink.push_frame(&vec![128u8; (w * h * 4) as usize], w, h, Duration::ZERO);
266
267 assert!(
268 *got.lock().unwrap_or_else(PoisonError::into_inner),
269 "a CPU-only downstream must receive a CPU frame"
270 );
271 assert_eq!(
272 ctx.readback_count(),
273 1,
274 "a CPU-only downstream falls back to the readback path (proves the counter moves)"
275 );
276 }
277}