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 = "wgpu")]
59 {
60 match self.graph.process_gpu(rgba, width, height) {
61 Ok(processed) => {
62 self.downstream.push_frame(&processed, width, height, pts);
63 return;
64 }
65 Err(e) => {
66 log::warn!("GpuFrameSink GPU processing failed, using CPU fallback error={e}");
67 }
68 }
69 }
70 let processed = self.graph.process_cpu(rgba, width, height);
72 self.downstream.push_frame(&processed, width, height, pts);
73 }
74
75 fn flush(&mut self) {
76 self.downstream.flush();
77 }
78}
79
80#[cfg(test)]
83mod tests {
84 use super::*;
85 use std::sync::{Arc, Mutex};
86
87 use crate::nodes::ColorGradeNode;
88
89 struct CollectSink(Arc<Mutex<Vec<Vec<u8>>>>);
90
91 impl FrameSink for CollectSink {
92 fn push_frame(&mut self, rgba: &[u8], _w: u32, _h: u32, _pts: Duration) {
93 self.0
94 .lock()
95 .unwrap_or_else(std::sync::PoisonError::into_inner)
96 .push(rgba.to_vec());
97 }
98 }
99
100 #[test]
101 fn gpu_frame_sink_cpu_path_should_forward_processed_frame() {
102 let graph = RenderGraph::new_cpu().push_cpu(ColorGradeNode::new(0.5, 1.0, 1.0, 0.0, 0.0));
104
105 let collected = Arc::new(Mutex::new(Vec::new()));
106 let downstream = Box::new(CollectSink(Arc::clone(&collected)));
107 let mut sink = GpuFrameSink::new(graph, downstream);
108
109 let pts = Duration::from_millis(0);
110 sink.push_frame(&[128u8, 128, 128, 255], 1, 1, pts);
113
114 let guard = collected
115 .lock()
116 .unwrap_or_else(std::sync::PoisonError::into_inner);
117 assert_eq!(guard.len(), 1, "exactly one frame must be forwarded");
118 assert!(
119 guard[0][0] > 128,
120 "brightness +0.5 must increase R channel; got {}",
121 guard[0][0]
122 );
123 }
124
125 #[test]
126 fn gpu_frame_sink_flush_should_propagate_to_downstream() {
127 struct FlushTracker(Arc<Mutex<bool>>);
128 impl FrameSink for FlushTracker {
129 fn push_frame(&mut self, _: &[u8], _: u32, _: u32, _: Duration) {}
130 fn flush(&mut self) {
131 *self
132 .0
133 .lock()
134 .unwrap_or_else(std::sync::PoisonError::into_inner) = true;
135 }
136 }
137
138 let flushed = Arc::new(Mutex::new(false));
139 let mut sink = GpuFrameSink::new(
140 RenderGraph::new_cpu(),
141 Box::new(FlushTracker(Arc::clone(&flushed))),
142 );
143 sink.flush();
144 assert!(
145 *flushed
146 .lock()
147 .unwrap_or_else(std::sync::PoisonError::into_inner),
148 "flush must propagate to downstream"
149 );
150 }
151
152 #[test]
153 fn gpu_frame_sink_should_be_send() {
154 fn assert_send<T: Send>() {}
155 assert_send::<GpuFrameSink>();
156 }
157}