Skip to main content

ez_ffmpeg/wgpu_filter/
params.rs

1//! Shared, thread-safe handles surfaced to users: live shader parameters
2//! and cumulative per-stage timing statistics.
3
4use std::marker::PhantomData;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7
8/// Cumulative per-stage timings, readable while the filter runs via
9/// [`crate::wgpu_filter::WgpuFrameFilter::stats_handle`].
10#[derive(Clone, Copy, Debug, Default)]
11pub struct WgpuFilterStats {
12    /// Number of frames fully processed (readback completed).
13    pub frames: u64,
14    /// CPU time spent uploading planes/uniforms and encoding GPU passes.
15    pub upload_secs: f64,
16    /// Wall time the pipeline thread spent blocked waiting for GPU results.
17    /// With `frames_in_flight > 1`, GPU work overlaps the next frame's CPU
18    /// work, so this is typically much smaller than the actual GPU time.
19    pub gpu_secs: f64,
20    /// CPU time spent copying mapped readback bytes into output frames.
21    pub download_secs: f64,
22    /// Hardware input frames imported zero-copy (dmabuf).
23    pub hw_import_frames: u64,
24    /// Hardware input frames downloaded to system memory instead — with
25    /// `hw_zero_copy_input` enabled, a nonzero count reveals the import
26    /// fallback engaging (a large per-frame perf cliff that a log line
27    /// alone cannot make visible mid-stream).
28    pub hw_download_frames: u64,
29    /// Wall time spent in `av_hwframe_transfer_data` downloads (previously
30    /// folded into `upload_secs`, which hid the fallback's cost).
31    pub hw_download_secs: f64,
32}
33
34/// Live handle for updating shader parameters from any thread while the
35/// filter runs inside a pipeline. Created via
36/// [`crate::wgpu_filter::WgpuFrameFilter::params_handle`].
37pub struct WgpuParamsHandle<P: bytemuck::Pod> {
38    pub(crate) bytes: Arc<Mutex<Vec<u8>>>,
39    pub(crate) dirty: Arc<AtomicBool>,
40    pub(crate) _marker: PhantomData<P>,
41}
42
43impl<P: bytemuck::Pod> WgpuParamsHandle<P> {
44    /// Replaces the parameter value; the new bytes are uploaded before the
45    /// next frame is rendered.
46    pub fn set(&self, value: P) {
47        // Tolerate a poisoned lock (the byte buffer is always left in a
48        // valid state) so a panicked pipeline thread cannot take the user's
49        // control thread down with it.
50        let mut bytes = self
51            .bytes
52            .lock()
53            .unwrap_or_else(std::sync::PoisonError::into_inner);
54        bytes.clear();
55        bytes.extend_from_slice(bytemuck::bytes_of(&value));
56        self.dirty.store(true, Ordering::Release);
57    }
58
59    /// Reads, modifies and writes back the parameter value as one atomic
60    /// step — the lock is held across the closure, so concurrent `set` /
61    /// `update` calls from other threads serialize instead of losing
62    /// writes (`update(|p| p.gain += 0.1)` never overwrites a value it
63    /// did not see). Because the lock is held, calling `set`/`update` on
64    /// any handle to the same filter from inside the closure deadlocks.
65    pub fn update(&self, f: impl FnOnce(&mut P)) {
66        let mut bytes = self
67            .bytes
68            .lock()
69            .unwrap_or_else(std::sync::PoisonError::into_inner);
70        // Unaligned read: `Vec<u8>` does not guarantee alignment for `P`.
71        let mut value: P = bytemuck::pod_read_unaligned(&bytes);
72        f(&mut value);
73        bytes.clear();
74        bytes.extend_from_slice(bytemuck::bytes_of(&value));
75        self.dirty.store(true, Ordering::Release);
76    }
77}
78
79/// Parameter state shared between the filter, its params handles, and the
80/// per-frame upload path.
81pub(crate) struct SharedParams {
82    pub(crate) bytes: Arc<Mutex<Vec<u8>>>,
83    pub(crate) dirty: Arc<AtomicBool>,
84    pub(crate) len: usize,
85}
86
87impl SharedParams {
88    pub(crate) fn new(initial: Vec<u8>) -> Self {
89        Self {
90            len: initial.len(),
91            bytes: Arc::new(Mutex::new(initial)),
92            dirty: Arc::new(AtomicBool::new(true)),
93        }
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[repr(C)]
102    #[derive(Clone, Copy, PartialEq, Debug, bytemuck::Pod, bytemuck::Zeroable)]
103    struct Pair {
104        a: f32,
105        b: f32,
106    }
107
108    #[test]
109    fn update_reads_modifies_and_marks_dirty() {
110        let shared = SharedParams::new(bytemuck::bytes_of(&Pair { a: 1.0, b: 2.0 }).to_vec());
111        let handle = WgpuParamsHandle::<Pair> {
112            bytes: Arc::clone(&shared.bytes),
113            dirty: Arc::clone(&shared.dirty),
114            _marker: PhantomData,
115        };
116        shared.dirty.store(false, Ordering::Release);
117        handle.update(|p| p.b += 40.0);
118        assert!(shared.dirty.load(Ordering::Acquire));
119        let bytes = shared
120            .bytes
121            .lock()
122            .unwrap_or_else(std::sync::PoisonError::into_inner);
123        let read: Pair = bytemuck::pod_read_unaligned(&bytes);
124        assert_eq!(read, Pair { a: 1.0, b: 42.0 });
125    }
126}