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}