1use std::cell::RefCell;
24use std::collections::HashMap;
25use std::sync::atomic::{AtomicU64, Ordering};
26use std::sync::{Arc, Mutex, OnceLock};
27
28use crate::RustCaptureSettings;
29use crate::encoders::software::EncodedStripe;
30
31#[derive(Clone, Debug, Default, PartialEq)]
37pub struct StreamInfo {
38 pub backend: &'static str,
39 pub capture: &'static str,
40 pub zero_copy: bool,
41 pub zero_copy_available: bool,
42 pub capture_reason: String,
43 pub encoder: String,
44 pub hardware: bool,
45 pub encoder_reason: String,
46 pub codec: &'static str,
47 pub fullcolor: bool,
48 pub bit_depth: u32,
49 pub full_range: bool,
50 pub stripes: usize,
51 pub gpu: String,
52 pub driver: String,
53 pub encode_node: i32,
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
58pub struct StreamTotals {
59 pub frames: u64,
60 pub bytes: u64,
61 pub encode_ns: u64,
62 pub pipeline_ns: u64,
63}
64
65#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
68pub struct StreamPeaks {
69 pub pipeline_min_ns: u64,
70 pub pipeline_max_ns: u64,
71 pub frame_max_bytes: u64,
72}
73
74pub struct StreamReport {
76 info: Mutex<StreamInfo>,
77 frames: AtomicU64,
78 bytes: AtomicU64,
79 encode_ns: AtomicU64,
80 pipeline_ns: AtomicU64,
81 pipeline_min_ns: AtomicU64,
83 pipeline_max_ns: AtomicU64,
84 frame_max_bytes: AtomicU64,
85}
86
87impl Default for StreamReport {
88 fn default() -> Self {
89 Self {
90 info: Mutex::default(),
91 frames: AtomicU64::new(0),
92 bytes: AtomicU64::new(0),
93 encode_ns: AtomicU64::new(0),
94 pipeline_ns: AtomicU64::new(0),
95 pipeline_min_ns: AtomicU64::new(u64::MAX),
96 pipeline_max_ns: AtomicU64::new(0),
97 frame_max_bytes: AtomicU64::new(0),
98 }
99 }
100}
101
102impl StreamReport {
103 pub fn new(backend: &'static str) -> Arc<Self> {
104 let report = Self::default();
105 report.info.lock().unwrap().backend = backend;
106 Arc::new(report)
107 }
108
109 pub fn info(&self) -> StreamInfo {
110 self.info.lock().unwrap().clone()
111 }
112
113 pub fn totals(&self) -> StreamTotals {
114 StreamTotals {
115 frames: self.frames.load(Ordering::Relaxed),
116 bytes: self.bytes.load(Ordering::Relaxed),
117 encode_ns: self.encode_ns.load(Ordering::Relaxed),
118 pipeline_ns: self.pipeline_ns.load(Ordering::Relaxed),
119 }
120 }
121
122 pub fn take_peaks(&self) -> StreamPeaks {
124 let min = self.pipeline_min_ns.swap(u64::MAX, Ordering::Relaxed);
125 StreamPeaks {
126 pipeline_min_ns: if min == u64::MAX { 0 } else { min },
127 pipeline_max_ns: self.pipeline_max_ns.swap(0, Ordering::Relaxed),
128 frame_max_bytes: self.frame_max_bytes.swap(0, Ordering::Relaxed),
129 }
130 }
131
132 pub fn tally(&self, stripes: &[EncodedStripe]) {
135 let Some(first) = stripes.first() else { return };
136 let bytes: usize = stripes.iter().map(|s| s.data.len()).sum();
137 let timing = first.timing;
138 self.frames.fetch_add(1, Ordering::Relaxed);
139 self.bytes.fetch_add(bytes as u64, Ordering::Relaxed);
140 self.frame_max_bytes
141 .fetch_max(bytes as u64, Ordering::Relaxed);
142 if timing.encode_end_ns > 0 {
143 let encode = (timing.encode_end_ns - timing.encode_start_ns).max(0) as u64;
144 let pipeline = (timing.encode_end_ns - timing.capture_ns).max(0) as u64;
145 self.encode_ns.fetch_add(encode, Ordering::Relaxed);
146 self.pipeline_ns.fetch_add(pipeline, Ordering::Relaxed);
147 self.pipeline_min_ns.fetch_min(pipeline, Ordering::Relaxed);
148 self.pipeline_max_ns.fetch_max(pipeline, Ordering::Relaxed);
149 }
150 }
151
152 fn update(&self, change: impl FnOnce(&mut StreamInfo)) {
153 change(&mut self.info.lock().unwrap());
154 }
155}
156
157thread_local! {
158 static CURRENT: RefCell<Option<Arc<StreamReport>>> = const { RefCell::new(None) };
159}
160
161pub struct Scope(Option<Arc<StreamReport>>);
163
164impl Drop for Scope {
165 fn drop(&mut self) {
166 CURRENT.with(|current| *current.borrow_mut() = self.0.take());
167 }
168}
169
170pub fn enter(report: &Arc<StreamReport>) -> Scope {
174 Scope(CURRENT.with(|current| current.borrow_mut().replace(report.clone())))
175}
176
177fn record(change: impl FnOnce(&mut StreamInfo)) {
178 CURRENT.with(|current| {
179 if let Some(report) = current.borrow().as_ref() {
180 report.update(change);
181 }
182 });
183}
184
185pub fn capture(path: &'static str, zero_copy: bool) {
188 record(|info| {
189 info.capture = path;
190 info.zero_copy = zero_copy;
191 if zero_copy {
192 info.zero_copy_available = true;
193 info.capture_reason.clear();
194 }
195 });
196}
197
198pub fn zero_copy_available(available: bool) {
203 record(|info| info.zero_copy_available = available);
204}
205
206pub fn capture_declined(path: &str, reason: &str) {
208 record(|info| {
209 if !info.capture_reason.is_empty() {
210 info.capture_reason.push_str("; ");
211 }
212 info.capture_reason.push_str(&format!("{path}: {reason}"));
213 });
214}
215
216pub fn capture_reason(reason: &str) {
218 record(|info| info.capture_reason = reason.to_string());
219}
220
221pub fn hardware_encoder(gpu: &str, driver: &str, node: i32) {
223 record(|info| {
224 info.gpu = gpu.to_string();
225 info.driver = driver.to_string();
226 info.encode_node = node;
227 info.encoder_reason.clear();
228 });
229}
230
231pub fn encoder_reason(reason: &str) {
233 record(|info| info.encoder_reason = reason.to_string());
234}
235
236pub fn stream(
239 settings: &RustCaptureSettings,
240 stripes: usize,
241 backend: Option<(&str, bool)>,
242 fullcolor: bool,
243 full_range: bool,
244) {
245 record(|info| {
246 let (name, hardware) = match backend {
247 Some((name, hardware)) => (name, hardware),
248 None if settings.codec.is_video() => {
249 (crate::encoders::software_library(settings.codec), false)
250 }
251 None => ("turbojpeg", false),
252 };
253 info.encoder = name.to_string();
254 info.hardware = hardware;
255 if !hardware {
256 info.gpu.clear();
257 info.driver.clear();
258 info.encode_node = -1;
259 }
260 info.codec = settings.codec.name();
261 info.fullcolor = fullcolor;
262 info.bit_depth = 8;
263 info.full_range = full_range;
264 info.stripes = stripes;
265 });
266}
267
268pub fn bit_depth(depth: u32) {
270 record(|info| info.bit_depth = depth);
271}
272
273#[derive(Clone, Debug, Default)]
276pub struct Renderer {
277 pub kind: &'static str,
278 pub node: String,
279 pub gpu: String,
280 pub reason: String,
281}
282
283static RENDERER: OnceLock<Mutex<Renderer>> = OnceLock::new();
284
285pub fn set_renderer(renderer: Renderer) {
286 *RENDERER.get_or_init(Default::default).lock().unwrap() = renderer;
287}
288
289pub fn renderer() -> Renderer {
290 RENDERER
291 .get_or_init(Default::default)
292 .lock()
293 .unwrap()
294 .clone()
295}
296
297static WAYLAND: OnceLock<Mutex<HashMap<u32, Arc<StreamReport>>>> = OnceLock::new();
298
299pub fn wayland_reports() -> &'static Mutex<HashMap<u32, Arc<StreamReport>>> {
302 WAYLAND.get_or_init(Default::default)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
312 fn decisions_land_in_the_bound_report() {
313 capture_declined("NvFBC", "unbound");
314 let report = StreamReport::new("x11");
315 {
316 let _scope = enter(&report);
317 capture_declined("NvFBC", "the codec is JPEG");
318 capture_declined("DRI3", "no DRI3");
319 capture("XShm", false);
320 }
321 capture_declined("NvFBC", "unbound");
322 let info = report.info();
323 assert_eq!(info.backend, "x11");
324 assert_eq!(info.capture, "XShm");
325 assert_eq!(
326 info.capture_reason,
327 "NvFBC: the codec is JPEG; DRI3: no DRI3"
328 );
329 assert!(!info.zero_copy_available);
330 {
331 let _scope = enter(&report);
332 capture("DRI3", true);
333 }
334 assert!(report.info().zero_copy);
335 assert!(report.info().zero_copy_available);
336 assert!(report.info().capture_reason.is_empty());
337 }
338
339 #[test]
342 fn a_readback_says_whether_a_zero_copy_path_was_offered() {
343 let offered = StreamReport::new("x11");
344 {
345 let _scope = enter(&offered);
346 capture_declined("NvFBC", "the X server offers no NV-GLX");
347 zero_copy_available(true);
348 capture_declined("DRI3", "the encoder could not read the server's buffer");
349 capture("XShm", false);
350 }
351 assert!(!offered.info().zero_copy);
352 assert!(offered.info().zero_copy_available);
353 let host = StreamReport::new("wayland");
354 {
355 let _scope = enter(&host);
356 capture("dmabuf", true);
357 capture("readback", false);
358 zero_copy_available(false);
359 capture_reason("the host compositor delivers software frames only");
360 }
361 assert!(!host.info().zero_copy);
362 assert!(!host.info().zero_copy_available);
363 }
364
365 #[test]
369 fn peaks_cover_one_window_and_restart_on_read() {
370 use crate::encoders::codec::Codec;
371 use crate::encoders::reference::Reference;
372 use crate::encoders::software::FrameTiming;
373 let stripe = |bytes: usize, capture: i64, end: i64| EncodedStripe {
374 data: Arc::new(vec![0; bytes]),
375 codec: Codec::H264,
376 stripe_y_start: 0,
377 stripe_height: 16,
378 frame_id: 0,
379 timing: FrameTiming {
380 capture_ns: capture,
381 encode_start_ns: end - 1_000_000,
382 encode_end_ns: end,
383 },
384 reference: Reference::Untracked,
385 };
386 let report = StreamReport::new("x11");
387 report.tally(&[stripe(1000, 10_000_000, 14_000_000)]);
388 report.tally(&[
389 stripe(700, 20_000_000, 29_000_000),
390 stripe(800, 20_000_000, 29_000_000),
391 ]);
392 report.tally(&[stripe(300, 30_000_000, 36_000_000)]);
393 let peaks = report.take_peaks();
394 assert_eq!(
395 peaks,
396 StreamPeaks {
397 pipeline_min_ns: 4_000_000,
398 pipeline_max_ns: 9_000_000,
399 frame_max_bytes: 1500,
400 }
401 );
402 assert_eq!(report.totals().pipeline_ns, 19_000_000);
403 assert_eq!(report.take_peaks(), StreamPeaks::default());
404 report.tally(&[stripe(200, 0, 0)]);
405 assert_eq!(
406 report.take_peaks(),
407 StreamPeaks {
408 frame_max_bytes: 200,
409 ..StreamPeaks::default()
410 }
411 );
412 }
413
414 #[test]
416 fn scopes_restore_the_outer_report() {
417 let outer = StreamReport::new("wayland");
418 let inner = StreamReport::new("wayland");
419 let _outer_scope = enter(&outer);
420 {
421 let _inner_scope = enter(&inner);
422 encoder_reason("inner");
423 }
424 encoder_reason("outer");
425 assert_eq!(inner.info().encoder_reason, "inner");
426 assert_eq!(outer.info().encoder_reason, "outer");
427 }
428}