Skip to main content

ff_render/sink/
mod.rs

1use std::time::Duration;
2
3use ff_preview::FrameSink;
4
5use crate::graph::RenderGraph;
6
7// ── TextureHandle ─────────────────────────────────────────────────────────────
8
9/// A GPU texture together with its default view and dimensions.
10///
11/// Window systems (winit, egui) can blit this directly to the display
12/// surface without a CPU round-trip download.
13#[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
21// ── GpuFrameSink ─────────────────────────────────────────────────────────────
22
23/// A [`FrameSink`] that processes each frame through a [`RenderGraph`] before
24/// forwarding to a downstream sink.
25///
26/// When the `wgpu` feature is enabled and the graph was created with a GPU
27/// context, the GPU pipeline runs. On GPU error the unprocessed frame is
28/// forwarded as a fallback.
29///
30/// When the `wgpu` feature is **not** enabled (or the graph is CPU-only), the
31/// CPU fallback pipeline runs transparently.
32///
33/// # Example
34///
35/// ```ignore
36/// let ctx = Arc::new(RenderContext::init().await?);
37/// let graph = RenderGraph::new(ctx)
38///     .push(ColorGradeNode { brightness: 0.2, ..Default::default() });
39/// let sink = GpuFrameSink::new(graph, Box::new(RgbaSink::new()));
40/// runner.set_sink(Box::new(sink));
41/// ```
42pub struct GpuFrameSink {
43    graph: RenderGraph,
44    downstream: Box<dyn FrameSink>,
45}
46
47impl GpuFrameSink {
48    /// Construct a sink that applies `graph` to every incoming frame and
49    /// forwards the result to `downstream`.
50    #[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        // CPU fallback (also used when wgpu feature is disabled).
71        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// ── Tests ─────────────────────────────────────────────────────────────────────
81
82#[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        // Use a CPU-only graph so no GPU device is required.
103        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        // When wgpu feature is enabled, process_gpu will fail (no ctx) and
111        // fall back to process_cpu — which is what we want for this test.
112        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}