1use std::ffi::c_void;
55use std::fs::File;
56use std::io::IoSlice;
57use std::os::fd::OwnedFd;
58use std::os::unix::fs::MetadataExt;
59use std::sync::Arc;
60use std::sync::atomic::Ordering;
61use std::time::{Duration, Instant};
62
63use gbm::{BufferObject, Device as RawGbmDevice, Format as GbmFormat};
64use smithay::backend::allocator::dmabuf::Dmabuf;
65use smithay::backend::allocator::gbm::GbmDevice;
66use smithay::backend::egl::EGLDisplay;
67use x11rb::connection::{Connection, RequestConnection};
68use x11rb::protocol::dri3::ConnectionExt as Dri3Ext;
69use x11rb::protocol::render::{self, ConnectionExt as RenderExt};
70use x11rb::protocol::xfixes::ConnectionExt as XfixesExt;
71use x11rb::protocol::xproto::{self, ConnectionExt as XprotoExt, ImageFormat, ImageOrder};
72use x11rb::rust_connection::RustConnection;
73
74use super::{
75 Controls, RootDamage, captured_damage, clamp_offset, cursor_image_origin, frame_rects,
76 require_32bpp, resolve_dims, take_tick, wait_for_frame,
77};
78use crate::CaptureSettings;
79use crate::encoders::software::{EncodedStripe, FrameTiming, StripeState};
80use crate::encoders::{self, FrameEncoder, FrameSource};
81use crate::pace::FramePace;
82use crate::pipeline::{
83 Damage, EncoderQuality, decide_hw_fullframe, plan_refinement, refinement_coded,
84};
85use crate::recording_sink::RecordingSink;
86
87const POOL_N: usize = 3;
90
91const GEOMETRY_POLL_FRAMES: i32 = 30;
93
94const MAX_X_FAILURES: u32 = 5;
97
98struct GpuBuffer {
101 _bo: BufferObject<()>,
102 dmabuf: Dmabuf,
103 pixmap: xproto::Pixmap,
104 picture: render::Picture,
105}
106
107struct Sprite {
110 pixmap: xproto::Pixmap,
111 picture: render::Picture,
112 gc: xproto::Gcontext,
113 width: u16,
114 height: u16,
115}
116
117struct CursorSprite {
120 pixmap: xproto::Pixmap,
121 picture: render::Picture,
122 gc: xproto::Gcontext,
123 width: u16,
124 height: u16,
125 serial: u32,
126}
127
128struct XScreen {
130 conn: RustConnection,
131 root: xproto::Window,
132 depth: u8,
133 byte_order: ImageOrder,
134 gc: xproto::Gcontext,
135 damage: RootDamage,
139 rgb_format: render::Pictformat,
141 argb_format: render::Pictformat,
143 modifiers: Vec<u64>,
146 explicit: bool,
150 device: String,
151 semaphore_opcode: Option<u8>,
153}
154
155const SEMAPHORE_EXTENSION: &str = "SELKIES-SEMAPHORE";
157
158struct GpuCapture {
163 encoder: Option<FrameEncoder>,
166 buffers: Vec<GpuBuffer>,
167 cursor: Option<CursorSprite>,
168 cursor_drawn: Option<smithay::utils::Rectangle<i32, smithay::utils::Physical>>,
170 overlay: crate::encoders::overlay::OverlayState,
173 watermark: Option<Sprite>,
174 x: XScreen,
175 gbm: RawGbmDevice<File>,
176 _egl: Option<EGLDisplay>,
177 egl_display: *const c_void,
178 next: usize,
179 settings: CaptureSettings,
182 request: CaptureSettings,
185 cap_x: i16,
186 cap_y: i16,
187 root_w: u16,
188 root_h: u16,
189 blit_semaphore: Option<u32>,
192}
193
194fn prune_modifier(modifiers: &mut Vec<u64>, chosen: u64) {
198 let before = modifiers.len();
199 modifiers.retain(|&m| m != chosen);
200 if modifiers.len() == before {
201 modifiers.clear();
202 }
203}
204
205fn intel_compressed(modifier: u64) -> bool {
210 const INTEL: u64 = 0x01;
211 const X_TILED: u64 = 1;
212 const Y_TILED: u64 = 2;
213 const YF_TILED: u64 = 3;
214 const TILE4: u64 = 9;
215 let layout = modifier & 0x00ff_ffff_ffff_ffff;
216 modifier >> 56 == INTEL && !matches!(layout, X_TILED | Y_TILED | YF_TILED | TILE4)
217}
218
219fn declined(reason: &str) -> Option<GpuCapture> {
221 println!("[X11] Zero-copy capture (DRI3) unavailable: {reason}.");
222 crate::report::capture_declined("DRI3", reason);
223 None
224}
225
226fn x_err(what: &str, e: impl std::fmt::Display) -> String {
227 format!("{what}: {e}")
228}
229
230fn device_of_fd(fd: &OwnedFd) -> Result<u64, String> {
232 let file = File::from(
233 fd.try_clone()
234 .map_err(|e| x_err("dup of the DRI3 device", e))?,
235 );
236 file.metadata()
237 .map(|m| m.rdev())
238 .map_err(|e| x_err("fstat of the DRI3 device", e))
239}
240
241fn same_gpu(a: u64, b: u64) -> bool {
244 let gpu = |dev: u64| {
245 std::fs::canonicalize(format!(
246 "/sys/dev/char/{}:{}/device",
247 libc::major(dev),
248 libc::minor(dev)
249 ))
250 .ok()
251 };
252 a == b || gpu(a).is_some_and(|g| gpu(b) == Some(g))
253}
254
255impl XScreen {
256 fn open(node: i32) -> Result<(Self, OwnedFd), String> {
258 let (conn, screen_num) =
259 x11rb::connect(None).map_err(|e| x_err("X11 connect failed", e))?;
260 require_32bpp(&conn, screen_num)?;
261 let screen = &conn.setup().roots[screen_num];
262 let root = screen.root;
263 let depth = screen.root_depth;
264 let byte_order = conn.setup().image_byte_order;
265
266 let dri3 = conn
267 .dri3_query_version(1, 2)
268 .map_err(|e| x_err("DRI3 query", e))?
269 .reply()
270 .map_err(|e| x_err("the server has no DRI3", e))?;
271 if dri3.major_version < 1 {
272 return Err(format!(
273 "the server's DRI3 is {}.{}",
274 dri3.major_version, dri3.minor_version
275 ));
276 }
277 let explicit = dri3.major_version > 1 || dri3.minor_version >= 2;
278 conn.xfixes_query_version(5, 0)
279 .map_err(|e| x_err("XFixes query", e))?
280 .reply()
281 .map_err(|e| x_err("the server has no XFixes", e))?;
282 let formats = conn
283 .render_query_pict_formats()
284 .map_err(|e| x_err("Render query", e))?
285 .reply()
286 .map_err(|e| x_err("the server has no Render extension", e))?
287 .formats;
288 let direct =
289 |f: &render::Pictforminfo, d: u8| f.type_ == render::PictType::DIRECT && f.depth == d;
290 let rgb_format = formats
291 .iter()
292 .find(|f| direct(f, depth) && f.direct.red_shift == 16 && f.direct.blue_shift == 0)
293 .map(|f| f.id)
294 .ok_or_else(|| format!("no Render format for the depth-{depth} screen"))?;
295 let argb_format = formats
296 .iter()
297 .find(|f| direct(f, 32) && f.direct.alpha_mask == 0xff && f.direct.red_shift == 16)
298 .map(|f| f.id)
299 .ok_or("no premultiplied ARGB32 Render format for the cursor")?;
300
301 let opened = conn
302 .dri3_open(root, 0)
303 .map_err(|e| x_err("DRI3 open", e))?
304 .reply()
305 .map_err(|e| x_err("the server refused DRI3 open", e))?;
306 let device_fd = opened.device_fd;
307 let encode_path = format!("/dev/dri/renderD{}", 128 + node);
308 let encode_dev = std::fs::metadata(&encode_path)
309 .map(|m| m.rdev())
310 .map_err(|e| x_err(&format!("encode node {encode_path}"), e))?;
311 let server_dev = device_of_fd(&device_fd)?;
312 if !same_gpu(server_dev, encode_dev) {
313 return Err(format!(
314 "the X server draws on DRM device {}:{}, not on the GPU of encode node {encode_path}",
315 libc::major(server_dev),
316 libc::minor(server_dev)
317 ));
318 }
319
320 let mut modifiers = Vec::new();
321 if explicit {
322 let modifiers_reply = conn
323 .dri3_get_supported_modifiers(root, depth, 32)
324 .map_err(|e| x_err("DRI3 modifiers", e))?
325 .reply()
326 .map_err(|e| x_err("the server listed no buffer modifiers", e))?;
327 modifiers = if modifiers_reply.window_modifiers.is_empty() {
328 modifiers_reply.screen_modifiers
329 } else {
330 modifiers_reply.window_modifiers
331 };
332 modifiers.retain(|&m| !intel_compressed(m));
333 }
334
335 let gc = conn.generate_id().map_err(|e| x_err("GC id", e))?;
336 conn.create_gc(
337 gc,
338 root,
339 &xproto::CreateGCAux::new()
340 .subwindow_mode(xproto::SubwindowMode::INCLUDE_INFERIORS)
341 .graphics_exposures(0),
342 )
343 .map_err(|e| x_err("CreateGC", e))?
344 .check()
345 .map_err(|e| x_err("CreateGC", e))?;
346 let damage = RootDamage::create(&conn, root).ok_or("the server has no Damage extension")?;
347 let semaphore_opcode = conn
348 .extension_information(SEMAPHORE_EXTENSION)
349 .ok()
350 .flatten()
351 .map(|e| e.major_opcode);
352
353 Ok((
354 Self {
355 conn,
356 root,
357 depth,
358 byte_order,
359 gc,
360 damage,
361 rgb_format,
362 argb_format,
363 modifiers,
364 explicit,
365 device: encode_path,
366 semaphore_opcode,
367 },
368 device_fd,
369 ))
370 }
371
372 fn semaphore_request(
374 &self,
375 minor: u8,
376 fields: &[u32],
377 fds: Vec<OwnedFd>,
378 ) -> Result<x11rb::cookie::VoidCookie<'_, RustConnection>, String> {
379 let opcode = self
380 .semaphore_opcode
381 .ok_or("the server has no SELKIES-SEMAPHORE")?;
382 let mut req = vec![opcode, minor];
383 req.extend_from_slice(&(1 + fields.len() as u16).to_ne_bytes());
384 for f in fields {
385 req.extend_from_slice(&f.to_ne_bytes());
386 }
387 self.conn
388 .send_request_without_reply(&[IoSlice::new(&req)], fds)
389 .map_err(|e| x_err("SELKIES-SEMAPHORE", e))
390 }
391
392 fn import_semaphore(&self, fd: OwnedFd) -> Result<u32, String> {
395 let id = self
396 .conn
397 .generate_id()
398 .map_err(|e| x_err("semaphore id", e))?;
399 self.semaphore_request(1, &[id, self.root], vec![fd])?
400 .check()
401 .map_err(|e| format!("the server did not take the semaphore: {e}"))?;
402 Ok(id)
403 }
404
405 fn signal_semaphore(&self, id: u32) -> Result<(), String> {
409 self.semaphore_request(2, &[id], Vec::new())?
410 .check()
411 .map_err(|e| format!("the server did not signal the semaphore: {e}"))
412 }
413
414 fn free_semaphore(&self, id: u32) {
415 let _ = self.semaphore_request(3, &[id], Vec::new());
416 }
417
418 fn await_blit(&self, pixmap: xproto::Pixmap) -> Result<(), String> {
421 self.conn
422 .get_image(ImageFormat::Z_PIXMAP, pixmap, 0, 0, 1, 1, !0u32)
423 .map_err(|e| x_err("blit sync", e))?
424 .reply()
425 .map_err(|e| x_err("blit sync reply", e))?;
426 Ok(())
427 }
428
429 fn upload_sprite(&self, argb: &[u32], width: u16, height: u16) -> Result<Sprite, String> {
432 let conn = &self.conn;
433 let pixmap = conn
434 .generate_id()
435 .map_err(|e| x_err("sprite pixmap id", e))?;
436 conn.create_pixmap(32, pixmap, self.root, width, height)
437 .map_err(|e| x_err("sprite CreatePixmap", e))?;
438 let gc = conn.generate_id().map_err(|e| x_err("sprite GC id", e))?;
439 conn.create_gc(
440 gc,
441 pixmap,
442 &xproto::CreateGCAux::new().graphics_exposures(0),
443 )
444 .map_err(|e| x_err("sprite CreateGC", e))?;
445 let picture = conn
446 .generate_id()
447 .map_err(|e| x_err("sprite picture id", e))?;
448 conn.render_create_picture(
449 picture,
450 pixmap,
451 self.argb_format,
452 &render::CreatePictureAux::new(),
453 )
454 .map_err(|e| x_err("sprite CreatePicture", e))?;
455 let mut data = Vec::with_capacity(argb.len() * 4);
456 for px in argb {
457 let bytes = if self.byte_order == ImageOrder::LSB_FIRST {
458 px.to_le_bytes()
459 } else {
460 px.to_be_bytes()
461 };
462 data.extend_from_slice(&bytes);
463 }
464 conn.put_image(
465 ImageFormat::Z_PIXMAP,
466 pixmap,
467 gc,
468 width,
469 height,
470 0,
471 0,
472 0,
473 32,
474 &data,
475 )
476 .map_err(|e| x_err("sprite PutImage", e))?;
477 Ok(Sprite {
478 pixmap,
479 picture,
480 gc,
481 width,
482 height,
483 })
484 }
485
486 fn free_sprite(&self, s: &Sprite) {
487 let _ = self.conn.render_free_picture(s.picture);
488 let _ = self.conn.free_gc(s.gc);
489 let _ = self.conn.free_pixmap(s.pixmap);
490 }
491
492 fn free_buffer(&self, b: &GpuBuffer) {
493 let _ = self.conn.render_free_picture(b.picture);
494 let _ = self.conn.free_pixmap(b.pixmap);
495 }
496
497 fn free_cursor(&self, c: &CursorSprite) {
498 let _ = self.conn.render_free_picture(c.picture);
499 let _ = self.conn.free_gc(c.gc);
500 let _ = self.conn.free_pixmap(c.pixmap);
501 }
502}
503
504impl GpuCapture {
505 fn alloc_bo(&mut self, w: u16, h: u16) -> Result<BufferObject<()>, String> {
510 if !self.x.explicit {
511 let by_modifier = self.gbm.create_buffer_object_with_modifiers::<()>(
514 u32::from(w),
515 u32::from(h),
516 GbmFormat::Argb8888,
517 std::iter::once(gbm::Modifier::Linear),
518 );
519 return match by_modifier {
520 Ok(bo) => Ok(bo),
521 Err(first) => self
522 .gbm
523 .create_buffer_object::<()>(
524 u32::from(w),
525 u32::from(h),
526 GbmFormat::Argb8888,
527 gbm::BufferObjectFlags::RENDERING | gbm::BufferObjectFlags::LINEAR,
528 )
529 .map_err(|e| {
530 format!(
531 "linear GBM allocation {w}x{h}: {first:?} by modifier, {e:?} by flag"
532 )
533 }),
534 };
535 }
536 loop {
537 let bo = if self.x.modifiers.is_empty() {
538 crate::alloc_render_target(
539 &self.gbm,
540 u32::from(w),
541 u32::from(h),
542 GbmFormat::Argb8888,
543 )
544 } else {
545 self.gbm.create_buffer_object_with_modifiers::<()>(
550 u32::from(w),
551 u32::from(h),
552 GbmFormat::Argb8888,
553 self.x.modifiers.iter().map(|&m| gbm::Modifier::from(m)),
554 )
555 };
556 let bo = match bo {
557 Ok(bo) => bo,
558 Err(e) if !self.x.modifiers.is_empty() => {
559 crate::log::debug!(
560 "[X11] DRI3 capture: allocation with the server's modifiers failed ({e:?}); trying without"
561 );
562 self.x.modifiers.clear();
563 continue;
564 }
565 Err(e) => return Err(format!("GBM allocation {w}x{h}: {e:?}")),
566 };
567 if bo.plane_count() <= 1 {
568 return Ok(bo);
569 }
570 let chosen: u64 = bo.modifier().into();
571 if self.x.modifiers.is_empty() {
572 return Err(format!(
573 "the driver lays {w}x{h} out over {} planes even unconstrained",
574 bo.plane_count()
575 ));
576 }
577 crate::log::debug!(
578 "[X11] DRI3 capture: modifier {chosen:#x} lays the buffer out in {} planes; trying another",
579 bo.plane_count()
580 );
581 drop(bo);
582 prune_modifier(&mut self.x.modifiers, chosen);
583 }
584 }
585
586 fn alloc_buffers(&mut self, w: u16, h: u16) -> Result<(), String> {
588 let mut fresh = Vec::with_capacity(POOL_N);
589 for _ in 0..POOL_N {
590 let bo = self.alloc_bo(w, h)?;
591 let dmabuf = crate::create_dmabuf_from_bo(&bo);
592 let server_fd = bo
593 .fd()
594 .map_err(|e| format!("dmabuf fd for the server: {e:?}"))?;
595 let modifier: u64 = bo.modifier().into();
596 let pixmap = self
597 .x
598 .conn
599 .generate_id()
600 .map_err(|e| x_err("pixmap id", e))?;
601 if self.x.explicit {
602 self.x
603 .conn
604 .dri3_pixmap_from_buffers(
605 pixmap,
606 self.x.root,
607 w,
608 h,
609 bo.stride(),
610 0,
611 0,
612 0,
613 0,
614 0,
615 0,
616 0,
617 self.x.depth,
618 32,
619 modifier,
620 vec![server_fd],
621 )
622 .map_err(|e| x_err("PixmapFromBuffers", e))?
623 .check()
624 .map_err(|e| {
625 format!(
626 "the server did not import the buffer (modifier {modifier:#x}): {e}"
627 )
628 })?;
629 } else {
630 let stride = u16::try_from(bo.stride())
632 .map_err(|_| format!("a {w}-pixel row's stride overflows DRI3 1.0"))?;
633 self.x
634 .conn
635 .dri3_pixmap_from_buffer(
636 pixmap,
637 self.x.root,
638 bo.stride() * u32::from(h),
639 w,
640 h,
641 stride,
642 self.x.depth,
643 32,
644 server_fd,
645 )
646 .map_err(|e| x_err("PixmapFromBuffer", e))?
647 .check()
648 .map_err(|e| format!("the server did not import the linear buffer: {e}"))?;
649 }
650 let picture = self
651 .x
652 .conn
653 .generate_id()
654 .map_err(|e| x_err("picture id", e))?;
655 self.x
656 .conn
657 .render_create_picture(
658 picture,
659 pixmap,
660 self.x.rgb_format,
661 &render::CreatePictureAux::new(),
662 )
663 .map_err(|e| x_err("CreatePicture", e))?
664 .check()
665 .map_err(|e| x_err("CreatePicture", e))?;
666 fresh.push(GpuBuffer {
667 _bo: bo,
668 dmabuf,
669 pixmap,
670 picture,
671 });
672 }
673 self.free_buffers();
674 self.buffers = fresh;
675 self.next = 0;
676 Ok(())
677 }
678
679 fn free_buffers(&mut self) {
680 for b in self.buffers.drain(..) {
681 self.x.free_buffer(&b);
682 }
683 }
684
685 fn reshape(&mut self, w: u16, h: u16) -> Result<(), String> {
688 self.alloc_buffers(w, h)?;
689 self.settings.width = i32::from(w);
690 self.settings.height = i32::from(h);
691 let in_place = match self.encoder.as_mut() {
692 Some(FrameEncoder::Nvenc(enc)) => {
693 enc.reconfigure_resolution(&self.settings).map(|_| ())
694 }
695 Some(FrameEncoder::Vaapi(_)) => {
696 Err("a VA-API session is rebuilt at a new size".to_string())
697 }
698 Some(FrameEncoder::Vpx(_) | FrameEncoder::Hevc(_) | FrameEncoder::Av1(_)) => {
699 Err("a software session takes host frames".to_string())
700 }
701 #[cfg(target_arch = "aarch64")]
705 Some(FrameEncoder::Tegra(_)) => Err("a Tegra session takes host frames".to_string()),
706 Some(FrameEncoder::V4l2m2m(_)) => {
707 Err("a V4L2 M2M session takes host frames".to_string())
708 }
709 None => Err("no session to reconfigure".to_string()),
710 };
711 if let Err(e) = in_place {
712 crate::log::debug!("[X11] DRI3 capture: {e}");
713 self.rebuild_encoder()?;
714 }
715 Ok(())
716 }
717
718 fn enc(&mut self) -> &mut FrameEncoder {
719 self.encoder
720 .as_mut()
721 .expect("the DRI3 capture always carries an encoder between frames")
722 }
723
724 fn backend(&self) -> &'static str {
726 self.encoder
727 .as_ref()
728 .map(|e| e.backend_name())
729 .unwrap_or("none")
730 }
731
732 fn rebuild_encoder(&mut self) -> Result<(), String> {
735 self.detach_semaphore();
736 drop(self.encoder.take());
737 let source = FrameSource::Dmabuf {
738 egl_display: self.egl_display,
739 };
740 let mut settings = self.settings.clone();
741 match encoders::select_frame_encoder(&mut settings, source, None, "X11") {
742 Some(enc) if enc.is_hardware() => {
743 self.encoder = Some(enc);
744 self.settings = settings;
745 self.attach_semaphore();
746 Ok(())
747 }
748 Some(_) => Err("no hardware encoder is left for the zero-copy path".to_string()),
749 None => Err("no encoder could be rebuilt for the zero-copy path".to_string()),
750 }
751 }
752
753 fn attach_semaphore(&mut self) {
756 self.detach_semaphore();
757 if self.x.semaphore_opcode.is_none() {
758 return;
759 }
760 let Some(fd) = self.enc().blit_semaphore_fd() else {
761 return;
762 };
763 match fd.and_then(|fd| self.x.import_semaphore(fd)) {
764 Ok(id) => {
765 self.blit_semaphore = Some(id);
766 println!(
767 "[X11] DRI3 capture: the encoder waits for each blit on the GPU, behind a semaphore the server signals ({SEMAPHORE_EXTENSION})."
768 );
769 }
770 Err(e) => crate::log::debug!(
771 "[X11] DRI3 capture: no blit semaphore ({e}); each blit is waited for with a GetImage"
772 ),
773 }
774 }
775
776 fn detach_semaphore(&mut self) {
777 if let Some(id) = self.blit_semaphore.take() {
778 self.x.free_semaphore(id);
779 }
780 }
781
782 fn pointer(&self) -> Option<(i16, i16)> {
784 let reply = self.x.conn.query_pointer(self.x.root).ok()?.reply().ok()?;
785 Some((reply.root_x, reply.root_y))
786 }
787
788 fn grab(&mut self, with_cursor: bool) -> Result<(usize, bool), String> {
792 let idx = self.next;
793 self.next = (self.next + 1) % self.buffers.len();
794 let (w, h) = (self.settings.width as u16, self.settings.height as u16);
795 let dst = self.buffers[idx].pixmap;
796 self.x
797 .conn
798 .copy_area(
799 self.x.root,
800 dst,
801 self.x.gc,
802 self.cap_x,
803 self.cap_y,
804 0,
805 0,
806 w,
807 h,
808 )
809 .map_err(|e| x_err("CopyArea", e))?;
810 self.cursor_drawn = None;
811 if with_cursor {
812 self.composite_cursor(idx)?;
813 } else if let Some(c) = self.cursor.take() {
814 self.x.free_cursor(&c);
815 }
816 self.composite_watermark(idx)?;
817 if let Some(id) = self.blit_semaphore {
818 match self.x.signal_semaphore(id) {
819 Ok(()) => return Ok((idx, true)),
820 Err(e) => {
823 eprintln!(
824 "[X11] DRI3 capture: {e}; waiting for each blit with a GetImage from now on."
825 );
826 self.detach_semaphore();
827 }
828 }
829 }
830 self.x.await_blit(dst)?;
831 Ok((idx, false))
832 }
833
834 fn composite_watermark(&mut self, idx: usize) -> Result<(), String> {
838 let Some((pixels, w, h)) = self.overlay.sprite() else {
839 return Ok(());
840 };
841 if self.watermark.is_none() {
842 let argb: Vec<u32> = pixels
844 .as_chunks::<4>()
845 .0
846 .iter()
847 .map(|p| {
848 let a = u32::from(p[3]);
849 let pm = |c: u8| (u32::from(c) * a / 255) & 0xff;
850 (a << 24) | (pm(p[0]) << 16) | (pm(p[1]) << 8) | pm(p[2])
851 })
852 .collect();
853 self.watermark = Some(self.x.upload_sprite(&argb, w as u16, h as u16)?);
854 }
855 let (fw, fh) = (self.settings.width, self.settings.height);
856 self.overlay
857 .update_position(fw, fh, self.request.watermark_location_enum);
858 let (x, y) = self.overlay.position();
859 let sprite = self.watermark.as_ref().unwrap();
860 if x >= fw
861 || y >= fh
862 || x + i32::from(sprite.width) <= 0
863 || y + i32::from(sprite.height) <= 0
864 {
865 return Ok(());
866 }
867 self.x
868 .conn
869 .render_composite(
870 render::PictOp::OVER,
871 sprite.picture,
872 x11rb::NONE,
873 self.buffers[idx].picture,
874 0,
875 0,
876 0,
877 0,
878 x.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16,
879 y.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16,
880 sprite.width,
881 sprite.height,
882 )
883 .map_err(|e| x_err("watermark Composite", e))?;
884 Ok(())
885 }
886
887 fn composite_cursor(&mut self, idx: usize) -> Result<(), String> {
890 let Some(c) = self
891 .x
892 .conn
893 .xfixes_get_cursor_image()
894 .ok()
895 .and_then(|c| c.reply().ok())
896 else {
897 return Ok(());
898 };
899 if c.width == 0 || c.height == 0 {
900 return Ok(());
901 }
902 let stale = self.cursor.as_ref().is_none_or(|s| {
903 s.serial != c.cursor_serial || s.width != c.width || s.height != c.height
904 });
905 if stale {
906 if let Some(old) = self.cursor.take() {
907 self.x.free_cursor(&old);
908 }
909 let conn = &self.x.conn;
910 let pixmap = conn
911 .generate_id()
912 .map_err(|e| x_err("cursor pixmap id", e))?;
913 conn.create_pixmap(32, pixmap, self.x.root, c.width, c.height)
914 .map_err(|e| x_err("cursor CreatePixmap", e))?;
915 let gc = conn.generate_id().map_err(|e| x_err("cursor GC id", e))?;
916 conn.create_gc(
917 gc,
918 pixmap,
919 &xproto::CreateGCAux::new().graphics_exposures(0),
920 )
921 .map_err(|e| x_err("cursor CreateGC", e))?;
922 let picture = conn
923 .generate_id()
924 .map_err(|e| x_err("cursor picture id", e))?;
925 conn.render_create_picture(
926 picture,
927 pixmap,
928 self.x.argb_format,
929 &render::CreatePictureAux::new(),
930 )
931 .map_err(|e| x_err("cursor CreatePicture", e))?;
932 let mut data = Vec::with_capacity(c.cursor_image.len() * 4);
933 for px in &c.cursor_image {
934 let bytes = if self.x.byte_order == ImageOrder::LSB_FIRST {
935 px.to_le_bytes()
936 } else {
937 px.to_be_bytes()
938 };
939 data.extend_from_slice(&bytes);
940 }
941 conn.put_image(
942 ImageFormat::Z_PIXMAP,
943 pixmap,
944 gc,
945 c.width,
946 c.height,
947 0,
948 0,
949 0,
950 32,
951 &data,
952 )
953 .map_err(|e| x_err("cursor PutImage", e))?;
954 self.cursor = Some(CursorSprite {
955 pixmap,
956 picture,
957 gc,
958 width: c.width,
959 height: c.height,
960 serial: c.cursor_serial,
961 });
962 }
963 let sprite = self.cursor.as_ref().unwrap();
964 let (img_x, img_y) = cursor_image_origin(
965 c.x,
966 c.y,
967 c.xhot,
968 c.yhot,
969 i32::from(self.cap_x),
970 i32::from(self.cap_y),
971 );
972 let (fw, fh) = (self.settings.width, self.settings.height);
973 if img_x >= fw
974 || img_y >= fh
975 || img_x + i32::from(c.width) <= 0
976 || img_y + i32::from(c.height) <= 0
977 {
978 return Ok(());
979 }
980 self.cursor_drawn = Some(smithay::utils::Rectangle::new(
981 (img_x, img_y).into(),
982 (i32::from(c.width), i32::from(c.height)).into(),
983 ));
984 self.x
985 .conn
986 .render_composite(
987 render::PictOp::OVER,
988 sprite.picture,
989 x11rb::NONE,
990 self.buffers[idx].picture,
991 0,
992 0,
993 0,
994 0,
995 img_x.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16,
996 img_y.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16,
997 sprite.width,
998 sprite.height,
999 )
1000 .map_err(|e| x_err("cursor Composite", e))?;
1001 Ok(())
1002 }
1003}
1004
1005impl Drop for GpuCapture {
1006 fn drop(&mut self) {
1007 self.detach_semaphore();
1008 if let Some(c) = self.cursor.take() {
1009 self.x.free_cursor(&c);
1010 }
1011 if let Some(w) = self.watermark.take() {
1012 self.x.free_sprite(&w);
1013 }
1014 self.free_buffers();
1015 let _ = self.x.conn.free_gc(self.x.gc);
1016 let _ = self.x.conn.flush();
1017 }
1018}
1019
1020fn open(settings: &CaptureSettings) -> Option<GpuCapture> {
1027 if !settings.codec.is_video() {
1028 return declined("the codec is JPEG");
1029 }
1030 if settings.use_cpu || settings.encode_node_index == -1 {
1031 return declined("software encoding was requested");
1032 }
1033 let node = settings.encode_node_index.max(0);
1034 let driver = crate::get_gpu_driver(node);
1035 if driver.is_empty() {
1036 return declined(&format!("encode node {node} has no readable render node"));
1037 }
1038 let (x, device_fd) = match XScreen::open(node) {
1039 Ok(v) => v,
1040 Err(e) => return declined(&e),
1041 };
1042 crate::report::zero_copy_available(true);
1043
1044 let alloc_file = File::from(device_fd);
1045 let egl_file = match alloc_file.try_clone() {
1046 Ok(f) => f,
1047 Err(e) => return declined(&format!("dup of the DRI3 device: {e}")),
1048 };
1049 let gbm = match RawGbmDevice::new(alloc_file) {
1050 Ok(d) => d,
1051 Err(e) => return declined(&format!("no GBM device on {}: {e:?}", x.device)),
1052 };
1053 let wants_egl = crate::driver_selects_nvenc(&driver);
1055 let egl = GbmDevice::new(egl_file)
1056 .ok()
1057 .and_then(|dev| crate::egl_display(dev).ok());
1058 if wants_egl && egl.is_none() {
1059 return declined(&format!(
1060 "no EGL display on {} for the NVENC import",
1061 x.device
1062 ));
1063 }
1064 let egl_display = egl
1065 .as_ref()
1066 .map(|e| e.get_display_handle().handle)
1067 .unwrap_or(std::ptr::null());
1068
1069 let geo = match x
1070 .conn
1071 .get_geometry(x.root)
1072 .ok()
1073 .and_then(|c| c.reply().ok())
1074 {
1075 Some(g) => g,
1076 None => return declined("the root geometry could not be read"),
1077 };
1078 let mut watermark = crate::encoders::overlay::OverlayState::default();
1079 if !settings.watermark_path.is_empty() {
1080 watermark.load_watermark(&settings.watermark_path, 1.0);
1081 }
1082 let request = settings.clone();
1083 let mut live = settings.clone();
1084 let (w, h) = resolve_dims(geo.width, geo.height, &request);
1085 live.width = i32::from(w);
1086 live.height = i32::from(h);
1087
1088 let mut gpu = GpuCapture {
1089 encoder: None,
1090 buffers: Vec::new(),
1091 cursor: None,
1092 cursor_drawn: None,
1093 overlay: watermark,
1094 watermark: None,
1095 cap_x: clamp_offset(request.capture_x, geo.width),
1096 cap_y: clamp_offset(request.capture_y, geo.height),
1097 root_w: geo.width,
1098 root_h: geo.height,
1099 blit_semaphore: None,
1100 x,
1101 gbm,
1102 _egl: egl,
1103 egl_display,
1104 next: 0,
1105 settings: live,
1106 request,
1107 };
1108 if let Err(e) = gpu.alloc_buffers(w, h) {
1109 return declined(&e);
1110 }
1111 let mut live = gpu.settings.clone();
1112 gpu.encoder = match encoders::select_frame_encoder(
1113 &mut live,
1114 FrameSource::Dmabuf { egl_display },
1115 None,
1116 "X11",
1117 ) {
1118 Some(enc) if enc.is_hardware() => Some(enc),
1119 Some(_) => return declined("the session encodes in software"),
1120 None => return declined("no hardware encoder opened for this session"),
1121 };
1122 gpu.settings = live;
1123 gpu.attach_semaphore();
1124 println!(
1125 "[X11] Zero-copy capture (DRI3): {}x{} blitted by the X server into {} GPU buffers on {}, encoded in place on {}.",
1126 gpu.settings.width,
1127 gpu.settings.height,
1128 POOL_N,
1129 gpu.x.device,
1130 gpu.backend()
1131 );
1132 crate::report::capture("DRI3", true);
1133 crate::log_stream_settings("X11", &gpu.settings, 1, gpu.encoder.as_ref());
1134 Some(gpu)
1135}
1136
1137const UNDRAWN_CURSOR_REACH: i32 = 64;
1140
1141fn cursor_motion_rects(
1146 drawn: Option<(i32, i32, i32, i32)>,
1147 from: Option<(i16, i16)>,
1148 to: (i16, i16),
1149) -> Vec<xproto::Rectangle> {
1150 let rect = |x: i32, y: i32, w: i32, h: i32| xproto::Rectangle {
1151 x: x.clamp(i16::MIN.into(), i16::MAX.into()) as i16,
1152 y: y.clamp(i16::MIN.into(), i16::MAX.into()) as i16,
1153 width: w.clamp(0, u16::MAX.into()) as u16,
1154 height: h.clamp(0, u16::MAX.into()) as u16,
1155 };
1156 let around = |(px, py): (i16, i16)| {
1157 let span = 2 * UNDRAWN_CURSOR_REACH;
1158 rect(
1159 i32::from(px) - UNDRAWN_CURSOR_REACH,
1160 i32::from(py) - UNDRAWN_CURSOR_REACH,
1161 span,
1162 span,
1163 )
1164 };
1165 match (drawn, from) {
1166 (Some((x, y, w, h)), Some((fx, fy))) => {
1167 let (dx, dy) = (
1168 i32::from(to.0) - i32::from(fx),
1169 i32::from(to.1) - i32::from(fy),
1170 );
1171 vec![rect(x, y, w, h), rect(x + dx, y + dy, w, h)]
1172 }
1173 (_, from) => from.into_iter().chain([to]).map(around).collect(),
1174 }
1175}
1176
1177pub fn run_capture<F>(
1190 settings: CaptureSettings,
1191 controls: Arc<Controls>,
1192 encode_tid_tx: std::sync::mpsc::Sender<std::thread::ThreadId>,
1193 on_frame: &mut F,
1194) -> Option<Result<(), String>>
1195where
1196 F: FnMut(Vec<EncodedStripe>),
1197{
1198 let mut gpu = open(&settings)?;
1199 controls
1200 .codec
1201 .store(gpu.enc().codec().id(), Ordering::Relaxed);
1202
1203 let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
1204 let mut state = StripeState::default();
1205 let mut frame_counter: u16 = 0;
1206 let mut pending_force_idr = true;
1207 let mut pace = FramePace::windowed();
1209 let mut geometry_check = GEOMETRY_POLL_FRAMES;
1210 let mut x_failures = 0u32;
1211 let mut encode_errors = 0u32;
1212 let mut encoder_rebuilt = false;
1213 let mut delivered_any = false;
1217 let mut last_log = Instant::now();
1218 let mut sent_frames: u64 = 0;
1219 let mut damaged_frames: u64 = 0;
1220 let mut first_frame = true;
1221 let mut last_cursor = None;
1223 let mut last_pointer = None;
1224 let mut tid_sent = false;
1225
1226 while !controls.stop.load(Ordering::Relaxed) {
1227 let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1228 gpu.settings.video_link_kbps = controls.link_kbps.load(Ordering::Relaxed);
1229 let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
1230 let trigger = wait_for_frame(&gpu.x.conn, Some(&gpu.x.damage), &pace, frame_dur);
1231 let woke = Instant::now();
1232 let (dirty, changed) = take_tick(&gpu.x.damage, &gpu.x.conn, trigger);
1235 let mut is_dirty = dirty || first_frame;
1236 let mut changed = changed.filter(|rects| !first_frame && !rects.is_empty());
1238 first_frame = false;
1239 damaged_frames += u64::from(is_dirty);
1240 if controls.stop.load(Ordering::Relaxed) {
1241 break;
1242 }
1243
1244 for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
1245 state.take_report(report);
1246 if !gpu.enc().take_report(report) {
1247 pending_force_idr = true;
1248 }
1249 }
1250 if controls.force_idr.swap(false, Ordering::Relaxed)
1251 || recording_sink
1252 .as_ref()
1253 .is_some_and(|s| s.should_force_idr())
1254 {
1255 pending_force_idr = true;
1256 }
1257 if controls.rate_dirty.swap(false, Ordering::Acquire) {
1258 gpu.settings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
1259 gpu.settings.video_vbv_multiplier =
1260 f64::from(controls.vbv_mult_milli.load(Ordering::Relaxed)) / 1000.0;
1261 gpu.settings.target_fps =
1262 f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1263 let live = gpu.settings.clone();
1264 if let Err(e) = gpu.enc().reconfigure_rate(&live) {
1265 eprintln!(
1266 "[X11] DRI3 capture: rate reconfigure failed ({e}); rebuilding the encoder."
1267 );
1268 if let Err(e) = gpu.rebuild_encoder() {
1269 return Some(Err(format!("DRI3 capture ended: {e}")));
1270 }
1271 state = StripeState::default();
1272 pending_force_idr = true;
1273 }
1274 }
1275 if controls.tunables_dirty.swap(false, Ordering::Acquire)
1276 && let Some(t) = controls.tunables.lock().unwrap().take()
1277 {
1278 t.apply_to(&mut gpu.settings);
1279 }
1280
1281 let want_cursor = controls.capture_cursor.load(Ordering::Relaxed);
1282 let pointer = if want_cursor { gpu.pointer() } else { None };
1283 if let Some(now) = pointer
1284 && last_pointer != Some(now)
1285 && !is_dirty
1286 {
1287 is_dirty = true;
1288 let cursor = last_cursor.map(|r: smithay::utils::Rectangle<i32, _>| {
1289 (
1290 r.loc.x + i32::from(gpu.cap_x),
1291 r.loc.y + i32::from(gpu.cap_y),
1292 r.size.w,
1293 r.size.h,
1294 )
1295 });
1296 changed = Some(cursor_motion_rects(cursor, last_pointer, now));
1297 }
1298 let mut recheck_geometry = controls.region_dirty.swap(false, Ordering::Acquire);
1299 if recheck_geometry {
1300 let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1301 gpu.request.capture_x = nx;
1302 gpu.request.capture_y = ny;
1303 gpu.request.width = nw;
1304 gpu.request.height = nh;
1305 gpu.request.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1306 }
1307 geometry_check -= 1;
1308 if geometry_check <= 0 {
1309 geometry_check = GEOMETRY_POLL_FRAMES;
1310 recheck_geometry = true;
1311 }
1312 if recheck_geometry {
1313 match gpu
1314 .x
1315 .conn
1316 .get_geometry(gpu.x.root)
1317 .ok()
1318 .and_then(|c| c.reply().ok())
1319 {
1320 Some(g) => {
1321 gpu.root_w = g.width;
1322 gpu.root_h = g.height;
1323 }
1324 None => {
1325 x_failures += 1;
1326 if x_failures > MAX_X_FAILURES {
1327 return Some(Err(
1328 "DRI3 capture ended: the X server stopped answering".to_string()
1329 ));
1330 }
1331 continue;
1332 }
1333 }
1334 gpu.cap_x = clamp_offset(gpu.request.capture_x, gpu.root_w);
1335 gpu.cap_y = clamp_offset(gpu.request.capture_y, gpu.root_h);
1336 let (w, h) = resolve_dims(gpu.root_w, gpu.root_h, &gpu.request);
1337 if i32::from(w) != gpu.settings.width || i32::from(h) != gpu.settings.height {
1338 if let Err(e) = gpu.reshape(w, h) {
1339 return Some(Err(format!(
1340 "DRI3 capture could not follow the new geometry: {e}"
1341 )));
1342 }
1343 state = StripeState::default();
1344 pending_force_idr = true;
1345 }
1346 }
1347
1348 if gpu.enc().take_lost_cleanup() {
1349 state.lost_frame();
1350 }
1351 let quality = EncoderQuality::of(gpu.enc());
1352 let damage = match &changed {
1353 _ if !is_dirty => Damage::None,
1354 Some(rects) => captured_damage(
1355 rects,
1356 gpu.cap_x.into(),
1357 gpu.cap_y.into(),
1358 gpu.settings.width,
1359 gpu.settings.height,
1360 ),
1361 None => Damage::Unknown,
1362 };
1363 let mut decision = decide_hw_fullframe(
1364 &mut state,
1365 &gpu.settings,
1366 frame_counter,
1367 damage,
1368 false,
1369 pending_force_idr,
1370 quality,
1371 );
1372 let mut frame_changed = changed.as_ref().map(|rects| {
1375 frame_rects(
1376 rects,
1377 gpu.cap_x,
1378 gpu.cap_y,
1379 gpu.settings.width as u16,
1380 gpu.settings.height as u16,
1381 )
1382 });
1383 let planned = !decision.send;
1386 if planned {
1387 let cbr = gpu.settings.video_cbr_mode;
1388 let (refines, last) = (gpu.enc().refines(), gpu.enc().last_quality());
1389 let bound = gpu.enc().rate_bound(cbr);
1390 plan_refinement(
1391 &mut state,
1392 &gpu.settings,
1393 &mut decision,
1394 damage,
1395 frame_changed.as_deref(),
1396 refines,
1397 last,
1398 bound,
1399 );
1400 if !decision.send {
1401 gpu.enc().idle();
1402 }
1403 }
1404 pace.record_tick(trigger, frame_dur, woke, decision.send);
1407 let mut delivered = false;
1408 if decision.send {
1409 let grabbed_ns = crate::wayland::host::now_ns();
1413 let (idx, after_blit) = match gpu.grab(want_cursor) {
1414 Ok(i) => {
1415 x_failures = 0;
1416 i
1417 }
1418 Err(e) => {
1419 x_failures += 1;
1420 if x_failures > MAX_X_FAILURES {
1421 return Some(Err(format!("DRI3 capture ended: {e}")));
1422 }
1423 frame_counter = frame_counter.wrapping_add(1);
1424 continue;
1425 }
1426 };
1427 let dmabuf = gpu.buffers[idx].dmabuf.clone();
1428 if pointer.is_some() {
1429 last_pointer = pointer;
1430 }
1431 if gpu.cursor_drawn != last_cursor {
1432 let moved = gpu.cursor_drawn.into_iter().chain(last_cursor);
1433 frame_changed.get_or_insert_with(Vec::new).extend(moved);
1434 last_cursor = gpu.cursor_drawn;
1435 }
1436 let (refines, last) = (gpu.enc().refines(), gpu.enc().last_quality());
1437 let cbr = gpu.settings.video_cbr_mode;
1438 let bound = gpu.enc().rate_bound(cbr);
1439 if !planned {
1440 plan_refinement(
1441 &mut state,
1442 &gpu.settings,
1443 &mut decision,
1444 damage,
1445 frame_changed.as_deref(),
1446 refines,
1447 last,
1448 bound,
1449 );
1450 }
1451 decision.prepare(gpu.enc());
1452 let encode_start_ns = crate::wayland::host::now_ns();
1453 let result = if after_blit {
1454 gpu.enc().encode_dmabuf_after_blit(
1455 &dmabuf,
1456 u64::from(frame_counter),
1457 decision.target_qp,
1458 decision.force_idr,
1459 )
1460 } else {
1461 gpu.enc().encode_dmabuf(
1462 &dmabuf,
1463 u64::from(frame_counter),
1464 decision.target_qp,
1465 decision.force_idr,
1466 )
1467 };
1468 let coded = EncoderQuality::of(gpu.enc());
1469 let capped = gpu.enc().coded_capped();
1470 let bytes = result.as_ref().map_or(0, Vec::len);
1471 refinement_coded(
1472 &mut state,
1473 &gpu.settings,
1474 &decision,
1475 coded,
1476 bytes,
1477 bound,
1478 capped,
1479 );
1480 match result {
1481 Ok(data) if !data.is_empty() => {
1482 encode_errors = 0;
1483 let stripes = vec![EncodedStripe {
1484 data: Arc::new(data),
1485 codec: gpu.settings.codec,
1486 stripe_y_start: 0,
1487 stripe_height: gpu.settings.height,
1488 frame_id: i32::from(frame_counter),
1489 timing: FrameTiming {
1490 capture_ns: grabbed_ns,
1491 encode_start_ns,
1492 encode_end_ns: crate::wayland::host::now_ns(),
1493 },
1494 reference: gpu.enc().last_reference(),
1495 }];
1496 if let Some(sink) = &recording_sink {
1497 sink.write_frame(&stripes, gpu.settings.width, gpu.settings.height);
1498 }
1499 sent_frames += 1;
1500 delivered = true;
1501 delivered_any = true;
1502 on_frame(stripes);
1503 }
1504 Ok(_) => {
1505 encode_errors = 0;
1506 delivered_any = true;
1507 }
1508 Err(e) => {
1509 if !delivered_any {
1510 drop(gpu);
1512 declined(&format!(
1513 "the encoder could not read the server's buffer: {e}"
1514 ));
1515 return None;
1516 }
1517 encode_errors += 1;
1518 if encode_errors % crate::HW_ERROR_RECOVERY_THRESHOLD == 1 {
1519 eprintln!("[X11] hardware encode error on the DRI3 path: {e}");
1520 }
1521 if encode_errors >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1522 if encoder_rebuilt {
1523 return Some(Err(
1524 "the encoder failed repeatedly on the DRI3 path".to_string()
1525 ));
1526 }
1527 eprintln!(
1528 "[X11] rebuilding the encoder after repeated errors on the DRI3 path."
1529 );
1530 if let Err(e) = gpu.rebuild_encoder() {
1531 return Some(Err(format!("DRI3 capture ended: {e}")));
1532 }
1533 encoder_rebuilt = true;
1534 encode_errors = 0;
1535 state = StripeState::default();
1536 pending_force_idr = true;
1537 }
1538 }
1539 }
1540 }
1541 if delivered_any && !tid_sent {
1542 let _ = encode_tid_tx.send(std::thread::current().id());
1543 tid_sent = true;
1544 }
1545 pending_force_idr = (pending_force_idr || decision.force_idr) && !delivered;
1546 frame_counter = frame_counter.wrapping_add(1);
1547
1548 let elapsed = last_log.elapsed().as_secs_f64();
1549 if elapsed >= 1.0 {
1550 crate::log::debug!(
1551 "[X11] DRI3 {}x{} Encoder: {} EncFPS: {:.2} Damaged/s: {:.2}",
1552 gpu.settings.width,
1553 gpu.settings.height,
1554 gpu.backend(),
1555 sent_frames as f64 / elapsed,
1556 damaged_frames as f64 / elapsed
1557 );
1558 sent_frames = 0;
1559 damaged_frames = 0;
1560 last_log = Instant::now();
1561 }
1562 }
1563 Some(Ok(()))
1564}
1565
1566#[cfg(test)]
1567mod modifier_tests {
1568 use super::{intel_compressed, prune_modifier};
1569
1570 const CCS_CC: u64 = 0x0100_0000_0000_0008;
1571 const CCS: u64 = 0x0100_0000_0000_0006;
1572 const Y_TILED: u64 = 0x0100_0000_0000_0002;
1573
1574 #[test]
1577 fn the_single_plane_compressed_layouts_are_not_offered() {
1578 const X_TILED: u64 = 0x0100_0000_0000_0001;
1579 const TILE4: u64 = 0x0100_0000_0000_0009;
1580 const DG2_RC_CCS: u64 = 0x0100_0000_0000_000a;
1581 const DG2_RC_CCS_CC: u64 = 0x0100_0000_0000_000c;
1582 let mut mods = vec![0, X_TILED, TILE4, DG2_RC_CCS, DG2_RC_CCS_CC];
1583 mods.retain(|&m| !intel_compressed(m));
1584 assert_eq!(mods, vec![0, X_TILED, TILE4]);
1585 assert!(intel_compressed(CCS) && intel_compressed(CCS_CC));
1586 assert!(!intel_compressed(Y_TILED) && !intel_compressed(0x00ff_ffff_ffff_ffff));
1587 }
1588
1589 #[test]
1592 fn a_rejected_modifier_leaves_the_others() {
1593 let mut mods = vec![CCS_CC, CCS, Y_TILED];
1594 prune_modifier(&mut mods, CCS_CC);
1595 assert_eq!(mods, vec![CCS, Y_TILED]);
1596 prune_modifier(&mut mods, CCS);
1597 assert_eq!(mods, vec![Y_TILED]);
1598 }
1599
1600 #[test]
1603 fn a_choice_outside_the_list_spends_it() {
1604 let mut mods = vec![CCS, Y_TILED];
1605 prune_modifier(&mut mods, 0x00ff_ffff_ffff_ffff);
1606 assert!(mods.is_empty());
1607 }
1608}
1609
1610#[cfg(test)]
1611mod cursor_tests {
1612 use super::cursor_motion_rects;
1613
1614 #[test]
1617 fn a_moved_cursor_changes_where_it_was_and_where_it_goes() {
1618 let rects = cursor_motion_rects(Some((100, 200, 24, 24)), Some((110, 210)), (150, 190));
1619 let got: Vec<_> = rects
1620 .iter()
1621 .map(|r| (r.x, r.y, r.width, r.height))
1622 .collect();
1623 assert_eq!(got, vec![(100, 200, 24, 24), (140, 180, 24, 24)]);
1624 }
1625
1626 #[test]
1629 fn an_undrawn_cursor_changes_a_box_around_the_pointer() {
1630 let rects = cursor_motion_rects(None, None, (10, 20));
1631 let got: Vec<_> = rects
1632 .iter()
1633 .map(|r| (r.x, r.y, r.width, r.height))
1634 .collect();
1635 assert_eq!(got, vec![(-54, -44, 128, 128)]);
1636 assert_eq!(cursor_motion_rects(None, Some((0, 0)), (10, 20)).len(), 2);
1637 }
1638}
1639
1640#[cfg(test)]
1641mod gpu_tests {
1642 use super::super::gpu_test_support::{
1643 capped_crf_codes_again_what_it_coded_bound, decoded_mean, paint_root, painted_ycbcr,
1644 settings,
1645 };
1646 use super::*;
1647 use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
1648 use crate::webcam::decode::{Codec as DecCodec, Decoder, VideoDecoder};
1649
1650 fn encode_grabbed(
1653 gpu: &mut GpuCapture,
1654 (idx, after_blit): (usize, bool),
1655 i: u64,
1656 key: bool,
1657 ) -> Vec<u8> {
1658 let dmabuf = gpu.buffers[idx].dmabuf.clone();
1659 if after_blit {
1660 gpu.enc().encode_dmabuf_after_blit(&dmabuf, i, 25, key)
1661 } else {
1662 gpu.enc().encode_dmabuf(&dmabuf, i, 25, key)
1663 }
1664 .expect("encode in place")
1665 }
1666
1667 #[test]
1672 #[ignore]
1673 fn gpu_dri3_capped_crf_codes_again_what_it_coded_bound() {
1674 let Some([plain, (capped, sent, bytes, keys)]) = capped_crf_codes_again_what_it_coded_bound(
1675 |s, c, t, mut f| run_capture(s, c, t, &mut f),
1676 false,
1677 ) else {
1678 println!("the DRI3 path declined this host; nothing to capture");
1679 return;
1680 };
1681 println!(
1682 "DRI3: uncapped {:.2} dB ({} frames while still), capped and refreshed {capped:.2} dB, \
1683 {sent} frames of {bytes} bytes while still, {keys} key frames",
1684 plain.0, plain.1
1685 );
1686 assert_eq!(keys, 0, "a key frame");
1687 assert!(sent > 0, "nothing refreshed the still page");
1688 assert!(
1689 capped >= plain.0 - 1.0,
1690 "refreshed to {capped:.2} dB against {:.2}",
1691 plain.0
1692 );
1693 }
1694
1695 #[test]
1700 #[ignore]
1701 fn gpu_dri3_capped_crf_comes_to_the_paint_over_once_its_peak_lets_go() {
1702 let Some([plain, (capped, sent, bytes, keys)]) = capped_crf_codes_again_what_it_coded_bound(
1703 |s, c, t, mut f| run_capture(s, c, t, &mut f),
1704 true,
1705 ) else {
1706 println!("the DRI3 path declined this host; nothing to capture");
1707 return;
1708 };
1709 println!(
1710 "DRI3 paint-over: uncapped {:.2} dB ({} frames, {} bytes while still), capped \
1711 {capped:.2} dB ({sent} frames, {bytes} bytes), {keys} key frames",
1712 plain.0, plain.1, plain.2
1713 );
1714 assert_eq!(keys, 0, "a key frame");
1715 assert!(
1716 capped >= plain.0 - 0.5,
1717 "{capped:.2} dB against the uncapped paint-over's {:.2}",
1718 plain.0
1719 );
1720 }
1721
1722 #[test]
1725 #[ignore]
1726 fn gpu_dri3_counts_the_primary_node_of_the_encode_gpu_as_it() {
1727 let node = settings(crate::encoders::codec::Codec::H264)
1728 .encode_node_index
1729 .max(0);
1730 let render = format!("/dev/dri/renderD{}", 128 + node);
1731 let render_dev = std::fs::metadata(&render).expect("encode node").rdev();
1732 let primary = std::fs::read_dir(format!("/sys/class/drm/renderD{}/device/drm", 128 + node))
1733 .expect("the encode node's GPU in sysfs")
1734 .filter_map(|e| e.ok()?.file_name().into_string().ok())
1735 .find(|name| name.starts_with("card"))
1736 .expect("a primary node on the encode node's GPU");
1737 let primary_dev = std::fs::metadata(format!("/dev/dri/{primary}"))
1738 .expect("primary node")
1739 .rdev();
1740 assert_ne!(
1741 primary_dev, render_dev,
1742 "{primary} and {render} are distinct nodes"
1743 );
1744 assert!(
1745 same_gpu(primary_dev, render_dev),
1746 "{primary} is the GPU of {render}"
1747 );
1748 }
1749
1750 #[test]
1755 #[ignore]
1756 fn gpu_dri3_composites_a_watermark_without_a_readback() {
1757 const SCREEN: (u8, u8, u8) = (0x20, 0x40, 0x80);
1758 const MARK: (u8, u8, u8) = (0xff, 0x00, 0x00);
1759 const ALPHA: u8 = 128;
1760 if !paint_root(SCREEN) {
1761 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1762 return;
1763 }
1764 let path = std::env::temp_dir().join("pixelflux-dri3-watermark.png");
1765 image::RgbaImage::from_pixel(96, 96, image::Rgba([MARK.0, MARK.1, MARK.2, ALPHA]))
1766 .save(&path)
1767 .expect("write the watermark");
1768 let mut s = settings(crate::encoders::codec::Codec::H264);
1769 s.watermark_path = path.to_string_lossy().into_owned();
1770 let Some(mut gpu) = open(&s) else {
1771 println!(
1772 "the DRI3 path declined a watermarked session on this host; nothing to capture"
1773 );
1774 return;
1775 };
1776 let grabbed = gpu.grab(false).expect("blit and composite");
1777 let pkt = encode_grabbed(&mut gpu, grabbed, 0, true);
1778 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1779 assert!(
1780 dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
1781 "no picture"
1782 );
1783 let v = dec.frame().expect("decoded frame");
1784 let blend = |m: u8, bg: u8| {
1786 (f64::from(m) * f64::from(ALPHA) + f64::from(bg) * (255.0 - f64::from(ALPHA))) / 255.0
1787 };
1788 let want = crate::encoders::chroma_siting::ycbcr(
1789 [
1790 blend(MARK.0, SCREEN.0),
1791 blend(MARK.1, SCREEN.1),
1792 blend(MARK.2, SCREEN.2),
1793 ],
1794 crate::encoders::chroma_siting::BT709,
1795 );
1796 let (mx, my) = (48usize, 48usize);
1797 let got = [
1798 f64::from(v.y[my * v.y_stride + mx]),
1799 f64::from(v.u[(my / 2) * v.uv_stride + mx / 2]),
1800 f64::from(v.v[(my / 2) * v.uv_stride + mx / 2]),
1801 ];
1802 for i in 0..3 {
1803 assert!(
1804 (got[i] - want[i]).abs() <= 10.0,
1805 "watermark plane {i}: captured {:.1}, blended {:.1}",
1806 got[i],
1807 want[i]
1808 );
1809 }
1810 let _ = std::fs::remove_file(&path);
1811 println!("the server composited the watermark at ({mx},{my}) to Y/Cb/Cr {got:?}");
1812 }
1813
1814 #[test]
1821 #[ignore]
1822 fn gpu_dri3_zero_copy_encodes_the_painted_root() {
1823 const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1824 const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1825 if !paint_root(FIRST) {
1826 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1827 return;
1828 }
1829 let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1830 println!("the DRI3 path declined this session on this host; nothing to capture");
1831 return;
1832 };
1833 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1834
1835 let encode = |gpu: &mut GpuCapture, i: u64, key: bool| -> Vec<u8> {
1836 let grabbed = gpu.grab(false).expect("blit into the pool");
1837 encode_grabbed(gpu, grabbed, i, key)
1838 };
1839
1840 let pkt = encode(&mut gpu, 0, true);
1841 assert_eq!(
1842 parse_video_type(pkt[1]),
1843 Some((crate::encoders::codec::Codec::H264, FRAME_KEY))
1844 );
1845 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1846 let want = painted_ycbcr(FIRST);
1847 for i in 0..3 {
1848 assert!(
1849 (mean[i] - want[i]).abs() <= 8.0,
1850 "plane {i}: captured {:.1}, painted {:.1}",
1851 mean[i],
1852 want[i]
1853 );
1854 }
1855
1856 for i in 1..4u64 {
1857 let pkt = encode(&mut gpu, i, false);
1858 assert_eq!(
1859 parse_video_type(pkt[1]),
1860 Some((crate::encoders::codec::Codec::H264, FRAME_DELTA))
1861 );
1862 let _ = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1863 }
1864
1865 assert!(
1866 paint_root(SECOND),
1867 "the root was painted once, so a repaint must work"
1868 );
1869 let pkt = encode(&mut gpu, 4, false);
1870 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1871 let want = painted_ycbcr(SECOND);
1872 for i in 0..3 {
1873 assert!(
1874 (mean[i] - want[i]).abs() <= 8.0,
1875 "after repaint, plane {i}: captured {:.1}, painted {:.1}",
1876 mean[i],
1877 want[i]
1878 );
1879 }
1880 println!(
1881 "captured {}x{} through {} server-imported buffers on {} and encoded in place on {}",
1882 gpu.settings.width,
1883 gpu.settings.height,
1884 POOL_N,
1885 gpu.x.device,
1886 gpu.backend()
1887 );
1888 }
1889}