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