1use std::ffi::c_void;
48use std::fs::File;
49use std::io::IoSlice;
50use std::os::fd::OwnedFd;
51use std::os::unix::fs::MetadataExt;
52use std::sync::Arc;
53use std::sync::atomic::Ordering;
54use std::time::{Duration, Instant};
55
56use gbm::{BufferObject, Device as RawGbmDevice, Format as GbmFormat};
57use smithay::backend::allocator::dmabuf::Dmabuf;
58use smithay::backend::allocator::gbm::GbmDevice;
59use smithay::backend::egl::EGLDisplay;
60use x11rb::connection::{Connection, RequestConnection};
61use x11rb::protocol::dri3::ConnectionExt as Dri3Ext;
62use x11rb::protocol::render::{self, ConnectionExt as RenderExt};
63use x11rb::protocol::xfixes::ConnectionExt as XfixesExt;
64use x11rb::protocol::xproto::{self, ConnectionExt as XprotoExt, ImageFormat, ImageOrder};
65use x11rb::rust_connection::RustConnection;
66
67use super::{
68 Controls, RootDamage, captured_damage, clamp_offset, cursor_image_origin, require_32bpp,
69 resolve_dims, wait_for_frame,
70};
71use crate::RustCaptureSettings;
72use crate::encoders::software::{EncodedStripe, FrameTiming, StripeState};
73use crate::encoders::{self, FrameEncoder, FrameSource};
74use crate::pace::{FramePace, TickTrigger};
75use crate::pipeline::{Damage, EncoderQuality, decide_hw_fullframe};
76use crate::recording_sink::RecordingSink;
77
78const POOL_N: usize = 3;
81
82const GEOMETRY_POLL_FRAMES: i32 = 30;
84
85const MAX_X_FAILURES: u32 = 5;
88
89struct GpuBuffer {
92 _bo: BufferObject<()>,
93 dmabuf: Dmabuf,
94 pixmap: xproto::Pixmap,
95 picture: render::Picture,
96}
97
98struct Sprite {
101 pixmap: xproto::Pixmap,
102 picture: render::Picture,
103 gc: xproto::Gcontext,
104 width: u16,
105 height: u16,
106}
107
108struct CursorSprite {
111 pixmap: xproto::Pixmap,
112 picture: render::Picture,
113 gc: xproto::Gcontext,
114 width: u16,
115 height: u16,
116 serial: u32,
117}
118
119struct XScreen {
121 conn: RustConnection,
122 root: xproto::Window,
123 depth: u8,
124 byte_order: ImageOrder,
125 gc: xproto::Gcontext,
126 damage: RootDamage,
130 rgb_format: render::Pictformat,
132 argb_format: render::Pictformat,
134 modifiers: Vec<u64>,
137 device: String,
138 semaphore_opcode: Option<u8>,
140}
141
142const SEMAPHORE_EXTENSION: &str = "SELKIES-SEMAPHORE";
144
145struct GpuCapture {
150 encoder: Option<FrameEncoder>,
153 buffers: Vec<GpuBuffer>,
154 cursor: Option<CursorSprite>,
155 overlay: crate::encoders::overlay::OverlayState,
158 watermark: Option<Sprite>,
159 x: XScreen,
160 gbm: RawGbmDevice<File>,
161 _egl: Option<EGLDisplay>,
162 egl_display: *const c_void,
163 next: usize,
164 settings: RustCaptureSettings,
167 request: RustCaptureSettings,
170 cap_x: i16,
171 cap_y: i16,
172 root_w: u16,
173 root_h: u16,
174 blit_semaphore: Option<u32>,
177}
178
179fn prune_modifier(modifiers: &mut Vec<u64>, chosen: u64) {
183 let before = modifiers.len();
184 modifiers.retain(|&m| m != chosen);
185 if modifiers.len() == before {
186 modifiers.clear();
187 }
188}
189
190fn intel_compressed(modifier: u64) -> bool {
195 const INTEL: u64 = 0x01;
196 const X_TILED: u64 = 1;
197 const Y_TILED: u64 = 2;
198 const YF_TILED: u64 = 3;
199 const TILE4: u64 = 9;
200 let layout = modifier & 0x00ff_ffff_ffff_ffff;
201 modifier >> 56 == INTEL && !matches!(layout, X_TILED | Y_TILED | YF_TILED | TILE4)
202}
203
204fn declined(reason: &str) -> Option<GpuCapture> {
206 println!("[X11] Zero-copy capture (DRI3) unavailable: {reason}; capturing through XShm.");
207 crate::report::capture_declined("DRI3", reason);
208 None
209}
210
211fn x_err(what: &str, e: impl std::fmt::Display) -> String {
212 format!("{what}: {e}")
213}
214
215fn device_of_fd(fd: &OwnedFd) -> Result<u64, String> {
217 let file = File::from(
218 fd.try_clone()
219 .map_err(|e| x_err("dup of the DRI3 device", e))?,
220 );
221 file.metadata()
222 .map(|m| m.rdev())
223 .map_err(|e| x_err("fstat of the DRI3 device", e))
224}
225
226fn same_gpu(a: u64, b: u64) -> bool {
229 let gpu = |dev: u64| {
230 std::fs::canonicalize(format!(
231 "/sys/dev/char/{}:{}/device",
232 libc::major(dev),
233 libc::minor(dev)
234 ))
235 .ok()
236 };
237 a == b || gpu(a).is_some_and(|g| gpu(b) == Some(g))
238}
239
240impl XScreen {
241 fn open(node: i32) -> Result<(Self, OwnedFd), String> {
243 let (conn, screen_num) =
244 x11rb::connect(None).map_err(|e| x_err("X11 connect failed", e))?;
245 require_32bpp(&conn, screen_num)?;
246 let screen = &conn.setup().roots[screen_num];
247 let root = screen.root;
248 let depth = screen.root_depth;
249 let byte_order = conn.setup().image_byte_order;
250
251 let dri3 = conn
252 .dri3_query_version(1, 2)
253 .map_err(|e| x_err("DRI3 query", e))?
254 .reply()
255 .map_err(|e| x_err("the server has no DRI3", e))?;
256 if dri3.major_version < 1 || (dri3.major_version == 1 && dri3.minor_version < 2) {
257 return Err(format!(
258 "the server's DRI3 is {}.{}, and buffers with modifiers need 1.2",
259 dri3.major_version, dri3.minor_version
260 ));
261 }
262 conn.xfixes_query_version(5, 0)
263 .map_err(|e| x_err("XFixes query", e))?
264 .reply()
265 .map_err(|e| x_err("the server has no XFixes", e))?;
266 let formats = conn
267 .render_query_pict_formats()
268 .map_err(|e| x_err("Render query", e))?
269 .reply()
270 .map_err(|e| x_err("the server has no Render extension", e))?
271 .formats;
272 let direct =
273 |f: &render::Pictforminfo, d: u8| f.type_ == render::PictType::DIRECT && f.depth == d;
274 let rgb_format = formats
275 .iter()
276 .find(|f| direct(f, depth) && f.direct.red_shift == 16 && f.direct.blue_shift == 0)
277 .map(|f| f.id)
278 .ok_or_else(|| format!("no Render format for the depth-{depth} screen"))?;
279 let argb_format = formats
280 .iter()
281 .find(|f| direct(f, 32) && f.direct.alpha_mask == 0xff && f.direct.red_shift == 16)
282 .map(|f| f.id)
283 .ok_or("no premultiplied ARGB32 Render format for the cursor")?;
284
285 let opened = conn
286 .dri3_open(root, 0)
287 .map_err(|e| x_err("DRI3 open", e))?
288 .reply()
289 .map_err(|e| x_err("the server refused DRI3 open", e))?;
290 let device_fd = opened.device_fd;
291 let encode_path = format!("/dev/dri/renderD{}", 128 + node);
292 let encode_dev = std::fs::metadata(&encode_path)
293 .map(|m| m.rdev())
294 .map_err(|e| x_err(&format!("encode node {encode_path}"), e))?;
295 let server_dev = device_of_fd(&device_fd)?;
296 if !same_gpu(server_dev, encode_dev) {
297 return Err(format!(
298 "the X server draws on DRM device {}:{}, not on the GPU of encode node {encode_path}",
299 libc::major(server_dev),
300 libc::minor(server_dev)
301 ));
302 }
303
304 let modifiers_reply = conn
305 .dri3_get_supported_modifiers(root, depth, 32)
306 .map_err(|e| x_err("DRI3 modifiers", e))?
307 .reply()
308 .map_err(|e| x_err("the server listed no buffer modifiers", e))?;
309 let mut modifiers = if modifiers_reply.window_modifiers.is_empty() {
310 modifiers_reply.screen_modifiers
311 } else {
312 modifiers_reply.window_modifiers
313 };
314 modifiers.retain(|&m| !intel_compressed(m));
315
316 let gc = conn.generate_id().map_err(|e| x_err("GC id", e))?;
317 conn.create_gc(
318 gc,
319 root,
320 &xproto::CreateGCAux::new()
321 .subwindow_mode(xproto::SubwindowMode::INCLUDE_INFERIORS)
322 .graphics_exposures(0),
323 )
324 .map_err(|e| x_err("CreateGC", e))?
325 .check()
326 .map_err(|e| x_err("CreateGC", e))?;
327 let damage = RootDamage::create(&conn, root).ok_or("the server has no Damage extension")?;
328 let semaphore_opcode = conn
329 .extension_information(SEMAPHORE_EXTENSION)
330 .ok()
331 .flatten()
332 .map(|e| e.major_opcode);
333
334 Ok((
335 Self {
336 conn,
337 root,
338 depth,
339 byte_order,
340 gc,
341 damage,
342 rgb_format,
343 argb_format,
344 modifiers,
345 device: encode_path,
346 semaphore_opcode,
347 },
348 device_fd,
349 ))
350 }
351
352 fn semaphore_request(
354 &self,
355 minor: u8,
356 fields: &[u32],
357 fds: Vec<OwnedFd>,
358 ) -> Result<x11rb::cookie::VoidCookie<'_, RustConnection>, String> {
359 let opcode = self
360 .semaphore_opcode
361 .ok_or("the server has no SELKIES-SEMAPHORE")?;
362 let mut req = vec![opcode, minor];
363 req.extend_from_slice(&(1 + fields.len() as u16).to_ne_bytes());
364 for f in fields {
365 req.extend_from_slice(&f.to_ne_bytes());
366 }
367 self.conn
368 .send_request_without_reply(&[IoSlice::new(&req)], fds)
369 .map_err(|e| x_err("SELKIES-SEMAPHORE", e))
370 }
371
372 fn import_semaphore(&self, fd: OwnedFd) -> Result<u32, String> {
375 let id = self
376 .conn
377 .generate_id()
378 .map_err(|e| x_err("semaphore id", e))?;
379 self.semaphore_request(1, &[id, self.root], vec![fd])?
380 .check()
381 .map_err(|e| format!("the server did not take the semaphore: {e}"))?;
382 Ok(id)
383 }
384
385 fn signal_semaphore(&self, id: u32) -> Result<(), String> {
389 self.semaphore_request(2, &[id], Vec::new())?
390 .check()
391 .map_err(|e| format!("the server did not signal the semaphore: {e}"))
392 }
393
394 fn free_semaphore(&self, id: u32) {
395 let _ = self.semaphore_request(3, &[id], Vec::new());
396 }
397
398 fn await_blit(&self, pixmap: xproto::Pixmap) -> Result<(), String> {
401 self.conn
402 .get_image(ImageFormat::Z_PIXMAP, pixmap, 0, 0, 1, 1, !0u32)
403 .map_err(|e| x_err("blit sync", e))?
404 .reply()
405 .map_err(|e| x_err("blit sync reply", e))?;
406 Ok(())
407 }
408
409 fn upload_sprite(&self, argb: &[u32], width: u16, height: u16) -> Result<Sprite, String> {
412 let conn = &self.conn;
413 let pixmap = conn
414 .generate_id()
415 .map_err(|e| x_err("sprite pixmap id", e))?;
416 conn.create_pixmap(32, pixmap, self.root, width, height)
417 .map_err(|e| x_err("sprite CreatePixmap", e))?;
418 let gc = conn.generate_id().map_err(|e| x_err("sprite GC id", e))?;
419 conn.create_gc(
420 gc,
421 pixmap,
422 &xproto::CreateGCAux::new().graphics_exposures(0),
423 )
424 .map_err(|e| x_err("sprite CreateGC", e))?;
425 let picture = conn
426 .generate_id()
427 .map_err(|e| x_err("sprite picture id", e))?;
428 conn.render_create_picture(
429 picture,
430 pixmap,
431 self.argb_format,
432 &render::CreatePictureAux::new(),
433 )
434 .map_err(|e| x_err("sprite CreatePicture", e))?;
435 let mut data = Vec::with_capacity(argb.len() * 4);
436 for px in argb {
437 let bytes = if self.byte_order == ImageOrder::LSB_FIRST {
438 px.to_le_bytes()
439 } else {
440 px.to_be_bytes()
441 };
442 data.extend_from_slice(&bytes);
443 }
444 conn.put_image(
445 ImageFormat::Z_PIXMAP,
446 pixmap,
447 gc,
448 width,
449 height,
450 0,
451 0,
452 0,
453 32,
454 &data,
455 )
456 .map_err(|e| x_err("sprite PutImage", e))?;
457 Ok(Sprite {
458 pixmap,
459 picture,
460 gc,
461 width,
462 height,
463 })
464 }
465
466 fn free_sprite(&self, s: &Sprite) {
467 let _ = self.conn.render_free_picture(s.picture);
468 let _ = self.conn.free_gc(s.gc);
469 let _ = self.conn.free_pixmap(s.pixmap);
470 }
471
472 fn free_buffer(&self, b: &GpuBuffer) {
473 let _ = self.conn.render_free_picture(b.picture);
474 let _ = self.conn.free_pixmap(b.pixmap);
475 }
476
477 fn free_cursor(&self, c: &CursorSprite) {
478 let _ = self.conn.render_free_picture(c.picture);
479 let _ = self.conn.free_gc(c.gc);
480 let _ = self.conn.free_pixmap(c.pixmap);
481 }
482}
483
484impl GpuCapture {
485 fn alloc_bo(&mut self, w: u16, h: u16) -> Result<BufferObject<()>, String> {
490 loop {
491 let bo = if self.x.modifiers.is_empty() {
492 crate::alloc_render_target(&self.gbm, w as u32, h as u32, GbmFormat::Argb8888)
493 } else {
494 self.gbm.create_buffer_object_with_modifiers::<()>(
499 w as u32,
500 h as u32,
501 GbmFormat::Argb8888,
502 self.x.modifiers.iter().map(|&m| gbm::Modifier::from(m)),
503 )
504 };
505 let bo = match bo {
506 Ok(bo) => bo,
507 Err(e) if !self.x.modifiers.is_empty() => {
508 crate::log::debug!(
509 "[X11] DRI3 capture: allocation with the server's modifiers failed ({e:?}); trying without"
510 );
511 self.x.modifiers.clear();
512 continue;
513 }
514 Err(e) => return Err(format!("GBM allocation {w}x{h}: {e:?}")),
515 };
516 if bo.plane_count() <= 1 {
517 return Ok(bo);
518 }
519 let chosen: u64 = bo.modifier().into();
520 if self.x.modifiers.is_empty() {
521 return Err(format!(
522 "the driver lays {w}x{h} out over {} planes even unconstrained",
523 bo.plane_count()
524 ));
525 }
526 crate::log::debug!(
527 "[X11] DRI3 capture: modifier {chosen:#x} lays the buffer out in {} planes; trying another",
528 bo.plane_count()
529 );
530 drop(bo);
531 prune_modifier(&mut self.x.modifiers, chosen);
532 }
533 }
534
535 fn alloc_buffers(&mut self, w: u16, h: u16) -> Result<(), String> {
537 let mut fresh = Vec::with_capacity(POOL_N);
538 for _ in 0..POOL_N {
539 let bo = self.alloc_bo(w, h)?;
540 let dmabuf = crate::create_dmabuf_from_bo(&bo);
541 let server_fd = bo
542 .fd()
543 .map_err(|e| format!("dmabuf fd for the server: {e:?}"))?;
544 let modifier: u64 = bo.modifier().into();
545 let pixmap = self
546 .x
547 .conn
548 .generate_id()
549 .map_err(|e| x_err("pixmap id", e))?;
550 self.x
551 .conn
552 .dri3_pixmap_from_buffers(
553 pixmap,
554 self.x.root,
555 w,
556 h,
557 bo.stride(),
558 0,
559 0,
560 0,
561 0,
562 0,
563 0,
564 0,
565 self.x.depth,
566 32,
567 modifier,
568 vec![server_fd],
569 )
570 .map_err(|e| x_err("PixmapFromBuffers", e))?
571 .check()
572 .map_err(|e| {
573 format!("the server did not import the buffer (modifier {modifier:#x}): {e}")
574 })?;
575 let picture = self
576 .x
577 .conn
578 .generate_id()
579 .map_err(|e| x_err("picture id", e))?;
580 self.x
581 .conn
582 .render_create_picture(
583 picture,
584 pixmap,
585 self.x.rgb_format,
586 &render::CreatePictureAux::new(),
587 )
588 .map_err(|e| x_err("CreatePicture", e))?
589 .check()
590 .map_err(|e| x_err("CreatePicture", e))?;
591 fresh.push(GpuBuffer {
592 _bo: bo,
593 dmabuf,
594 pixmap,
595 picture,
596 });
597 }
598 self.free_buffers();
599 self.buffers = fresh;
600 self.next = 0;
601 Ok(())
602 }
603
604 fn free_buffers(&mut self) {
605 for b in self.buffers.drain(..) {
606 self.x.free_buffer(&b);
607 }
608 }
609
610 fn reshape(&mut self, w: u16, h: u16) -> Result<(), String> {
613 self.alloc_buffers(w, h)?;
614 self.settings.width = w as i32;
615 self.settings.height = h as i32;
616 let in_place = match self.encoder.as_mut() {
617 Some(FrameEncoder::Nvenc(enc)) => {
618 enc.reconfigure_resolution(&self.settings).map(|_| ())
619 }
620 Some(FrameEncoder::Vaapi(_)) => {
621 Err("a VA-API session is rebuilt at a new size".to_string())
622 }
623 Some(FrameEncoder::Vpx(_) | FrameEncoder::Hevc(_) | FrameEncoder::Av1(_)) => {
624 Err("a software session takes host frames".to_string())
625 }
626 #[cfg(target_arch = "aarch64")]
630 Some(FrameEncoder::Tegra(_)) => Err("a Tegra session takes host frames".to_string()),
631 Some(FrameEncoder::V4l2m2m(_)) => {
632 Err("a V4L2 M2M session takes host frames".to_string())
633 }
634 None => Err("no session to reconfigure".to_string()),
635 };
636 if let Err(e) = in_place {
637 crate::log::debug!("[X11] DRI3 capture: {e}");
638 self.rebuild_encoder()?;
639 }
640 Ok(())
641 }
642
643 fn enc(&mut self) -> &mut FrameEncoder {
644 self.encoder
645 .as_mut()
646 .expect("the DRI3 capture always carries an encoder between frames")
647 }
648
649 fn backend(&self) -> &'static str {
651 self.encoder
652 .as_ref()
653 .map(|e| e.backend_name())
654 .unwrap_or("none")
655 }
656
657 fn rebuild_encoder(&mut self) -> Result<(), String> {
660 self.detach_semaphore();
661 drop(self.encoder.take());
662 let source = FrameSource::Dmabuf {
663 egl_display: self.egl_display,
664 };
665 let mut settings = self.settings.clone();
666 match encoders::select_frame_encoder(&mut settings, source, None, "X11") {
667 Some(enc) if enc.is_hardware() => {
668 self.encoder = Some(enc);
669 self.settings = settings;
670 self.attach_semaphore();
671 Ok(())
672 }
673 Some(_) => Err("no hardware encoder is left for the zero-copy path".to_string()),
674 None => Err("no encoder could be rebuilt for the zero-copy path".to_string()),
675 }
676 }
677
678 fn attach_semaphore(&mut self) {
681 self.detach_semaphore();
682 if self.x.semaphore_opcode.is_none() {
683 return;
684 }
685 let Some(fd) = self.enc().blit_semaphore_fd() else {
686 return;
687 };
688 match fd.and_then(|fd| self.x.import_semaphore(fd)) {
689 Ok(id) => {
690 self.blit_semaphore = Some(id);
691 println!(
692 "[X11] DRI3 capture: the encoder waits for each blit on the GPU, behind a semaphore the server signals ({SEMAPHORE_EXTENSION})."
693 );
694 }
695 Err(e) => crate::log::debug!(
696 "[X11] DRI3 capture: no blit semaphore ({e}); each blit is waited for with a GetImage"
697 ),
698 }
699 }
700
701 fn detach_semaphore(&mut self) {
702 if let Some(id) = self.blit_semaphore.take() {
703 self.x.free_semaphore(id);
704 }
705 }
706
707 fn grab(&mut self, with_cursor: bool) -> Result<(usize, bool), String> {
711 let idx = self.next;
712 self.next = (self.next + 1) % self.buffers.len();
713 let (w, h) = (self.settings.width as u16, self.settings.height as u16);
714 let dst = self.buffers[idx].pixmap;
715 self.x
716 .conn
717 .copy_area(
718 self.x.root,
719 dst,
720 self.x.gc,
721 self.cap_x,
722 self.cap_y,
723 0,
724 0,
725 w,
726 h,
727 )
728 .map_err(|e| x_err("CopyArea", e))?;
729 if with_cursor {
730 self.composite_cursor(idx)?;
731 } else if let Some(c) = self.cursor.take() {
732 self.x.free_cursor(&c);
733 }
734 self.composite_watermark(idx)?;
735 if let Some(id) = self.blit_semaphore {
736 match self.x.signal_semaphore(id) {
737 Ok(()) => return Ok((idx, true)),
738 Err(e) => {
741 eprintln!(
742 "[X11] DRI3 capture: {e}; waiting for each blit with a GetImage from now on."
743 );
744 self.detach_semaphore();
745 }
746 }
747 }
748 self.x.await_blit(dst)?;
749 Ok((idx, false))
750 }
751
752 fn composite_watermark(&mut self, idx: usize) -> Result<(), String> {
756 let Some((pixels, w, h)) = self.overlay.sprite() else {
757 return Ok(());
758 };
759 if self.watermark.is_none() {
760 let argb: Vec<u32> = pixels
762 .as_chunks::<4>()
763 .0
764 .iter()
765 .map(|p| {
766 let a = p[3] as u32;
767 let pm = |c: u8| (c as u32 * a / 255) & 0xff;
768 (a << 24) | (pm(p[0]) << 16) | (pm(p[1]) << 8) | pm(p[2])
769 })
770 .collect();
771 self.watermark = Some(self.x.upload_sprite(&argb, w as u16, h as u16)?);
772 }
773 let (fw, fh) = (self.settings.width, self.settings.height);
774 self.overlay
775 .update_position(fw, fh, self.request.watermark_location_enum);
776 let (x, y) = self.overlay.position();
777 let sprite = self.watermark.as_ref().unwrap();
778 if x >= fw || y >= fh || x + sprite.width as i32 <= 0 || y + sprite.height as i32 <= 0 {
779 return Ok(());
780 }
781 self.x
782 .conn
783 .render_composite(
784 render::PictOp::OVER,
785 sprite.picture,
786 x11rb::NONE,
787 self.buffers[idx].picture,
788 0,
789 0,
790 0,
791 0,
792 x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
793 y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
794 sprite.width,
795 sprite.height,
796 )
797 .map_err(|e| x_err("watermark Composite", e))?;
798 Ok(())
799 }
800
801 fn composite_cursor(&mut self, idx: usize) -> Result<(), String> {
804 let Some(c) = self
805 .x
806 .conn
807 .xfixes_get_cursor_image()
808 .ok()
809 .and_then(|c| c.reply().ok())
810 else {
811 return Ok(());
812 };
813 if c.width == 0 || c.height == 0 {
814 return Ok(());
815 }
816 let stale = self.cursor.as_ref().is_none_or(|s| {
817 s.serial != c.cursor_serial || s.width != c.width || s.height != c.height
818 });
819 if stale {
820 if let Some(old) = self.cursor.take() {
821 self.x.free_cursor(&old);
822 }
823 let conn = &self.x.conn;
824 let pixmap = conn
825 .generate_id()
826 .map_err(|e| x_err("cursor pixmap id", e))?;
827 conn.create_pixmap(32, pixmap, self.x.root, c.width, c.height)
828 .map_err(|e| x_err("cursor CreatePixmap", e))?;
829 let gc = conn.generate_id().map_err(|e| x_err("cursor GC id", e))?;
830 conn.create_gc(
831 gc,
832 pixmap,
833 &xproto::CreateGCAux::new().graphics_exposures(0),
834 )
835 .map_err(|e| x_err("cursor CreateGC", e))?;
836 let picture = conn
837 .generate_id()
838 .map_err(|e| x_err("cursor picture id", e))?;
839 conn.render_create_picture(
840 picture,
841 pixmap,
842 self.x.argb_format,
843 &render::CreatePictureAux::new(),
844 )
845 .map_err(|e| x_err("cursor CreatePicture", e))?;
846 let mut data = Vec::with_capacity(c.cursor_image.len() * 4);
847 for px in &c.cursor_image {
848 let bytes = if self.x.byte_order == ImageOrder::LSB_FIRST {
849 px.to_le_bytes()
850 } else {
851 px.to_be_bytes()
852 };
853 data.extend_from_slice(&bytes);
854 }
855 conn.put_image(
856 ImageFormat::Z_PIXMAP,
857 pixmap,
858 gc,
859 c.width,
860 c.height,
861 0,
862 0,
863 0,
864 32,
865 &data,
866 )
867 .map_err(|e| x_err("cursor PutImage", e))?;
868 self.cursor = Some(CursorSprite {
869 pixmap,
870 picture,
871 gc,
872 width: c.width,
873 height: c.height,
874 serial: c.cursor_serial,
875 });
876 }
877 let sprite = self.cursor.as_ref().unwrap();
878 let (img_x, img_y) = cursor_image_origin(
879 c.x,
880 c.y,
881 c.xhot,
882 c.yhot,
883 self.cap_x as i32,
884 self.cap_y as i32,
885 );
886 let (fw, fh) = (self.settings.width, self.settings.height);
887 if img_x >= fw || img_y >= fh || img_x + c.width as i32 <= 0 || img_y + c.height as i32 <= 0
888 {
889 return Ok(());
890 }
891 self.x
892 .conn
893 .render_composite(
894 render::PictOp::OVER,
895 sprite.picture,
896 x11rb::NONE,
897 self.buffers[idx].picture,
898 0,
899 0,
900 0,
901 0,
902 img_x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
903 img_y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
904 sprite.width,
905 sprite.height,
906 )
907 .map_err(|e| x_err("cursor Composite", e))?;
908 Ok(())
909 }
910}
911
912impl Drop for GpuCapture {
913 fn drop(&mut self) {
914 self.detach_semaphore();
915 if let Some(c) = self.cursor.take() {
916 self.x.free_cursor(&c);
917 }
918 if let Some(w) = self.watermark.take() {
919 self.x.free_sprite(&w);
920 }
921 self.free_buffers();
922 let _ = self.x.conn.free_gc(self.x.gc);
923 let _ = self.x.conn.flush();
924 }
925}
926
927fn open(settings: &RustCaptureSettings) -> Option<GpuCapture> {
934 if !settings.codec.is_video() {
935 return declined("the codec is JPEG");
936 }
937 if settings.use_cpu || settings.encode_node_index == -1 {
938 return declined("software encoding was requested");
939 }
940 let node = settings.encode_node_index.max(0);
941 let driver = crate::get_gpu_driver(node);
942 if driver.is_empty() {
943 return declined(&format!("encode node {node} has no readable render node"));
944 }
945 let (x, device_fd) = match XScreen::open(node) {
946 Ok(v) => v,
947 Err(e) => return declined(&e),
948 };
949 crate::report::zero_copy_available(true);
950
951 let alloc_file = File::from(device_fd);
952 let egl_file = match alloc_file.try_clone() {
953 Ok(f) => f,
954 Err(e) => return declined(&format!("dup of the DRI3 device: {e}")),
955 };
956 let gbm = match RawGbmDevice::new(alloc_file) {
957 Ok(d) => d,
958 Err(e) => return declined(&format!("no GBM device on {}: {e:?}", x.device)),
959 };
960 let wants_egl = crate::driver_selects_nvenc(&driver);
962 let egl = GbmDevice::new(egl_file)
963 .ok()
964 .and_then(|dev| unsafe { EGLDisplay::new(dev) }.ok());
965 if wants_egl && egl.is_none() {
966 return declined(&format!(
967 "no EGL display on {} for the NVENC import",
968 x.device
969 ));
970 }
971 let egl_display = egl
972 .as_ref()
973 .map(|e| e.get_display_handle().handle)
974 .unwrap_or(std::ptr::null());
975
976 let geo = match x
977 .conn
978 .get_geometry(x.root)
979 .ok()
980 .and_then(|c| c.reply().ok())
981 {
982 Some(g) => g,
983 None => return declined("the root geometry could not be read"),
984 };
985 let mut watermark = crate::encoders::overlay::OverlayState::default();
986 if !settings.watermark_path.is_empty() {
987 watermark.load_watermark(&settings.watermark_path, 1.0);
988 }
989 let request = settings.clone();
990 let mut live = settings.clone();
991 let (w, h) = resolve_dims(geo.width, geo.height, &request);
992 live.width = w as i32;
993 live.height = h as i32;
994
995 let mut gpu = GpuCapture {
996 encoder: None,
997 buffers: Vec::new(),
998 cursor: None,
999 overlay: watermark,
1000 watermark: None,
1001 cap_x: clamp_offset(request.capture_x, geo.width),
1002 cap_y: clamp_offset(request.capture_y, geo.height),
1003 root_w: geo.width,
1004 root_h: geo.height,
1005 blit_semaphore: None,
1006 x,
1007 gbm,
1008 _egl: egl,
1009 egl_display,
1010 next: 0,
1011 settings: live,
1012 request,
1013 };
1014 if let Err(e) = gpu.alloc_buffers(w, h) {
1015 return declined(&e);
1016 }
1017 let mut live = gpu.settings.clone();
1018 gpu.encoder = match encoders::select_frame_encoder(
1019 &mut live,
1020 FrameSource::Dmabuf { egl_display },
1021 None,
1022 "X11",
1023 ) {
1024 Some(enc) if enc.is_hardware() => Some(enc),
1025 Some(_) => return declined("the session encodes in software"),
1026 None => return declined("no hardware encoder opened for this session"),
1027 };
1028 gpu.settings = live;
1029 gpu.attach_semaphore();
1030 println!(
1031 "[X11] Zero-copy capture (DRI3): {}x{} blitted by the X server into {} GPU buffers on {}, encoded in place on {}.",
1032 gpu.settings.width,
1033 gpu.settings.height,
1034 POOL_N,
1035 gpu.x.device,
1036 gpu.backend()
1037 );
1038 crate::report::capture("DRI3", true);
1039 crate::log_stream_settings("X11", &gpu.settings, 1, gpu.encoder.as_ref());
1040 Some(gpu)
1041}
1042
1043pub fn run_capture<F>(
1056 settings: RustCaptureSettings,
1057 controls: Arc<Controls>,
1058 encode_tid_tx: std::sync::mpsc::Sender<std::thread::ThreadId>,
1059 on_frame: &mut F,
1060) -> Option<Result<(), String>>
1061where
1062 F: FnMut(Vec<EncodedStripe>),
1063{
1064 let mut gpu = open(&settings)?;
1065 controls
1066 .codec
1067 .store(gpu.enc().codec().id(), Ordering::Relaxed);
1068
1069 let recording_sink = RecordingSink::try_bind(&settings.recording_socket, settings.target_fps);
1070 let mut state = StripeState::default();
1071 let mut frame_counter: u16 = 0;
1072 let mut pending_force_idr = true;
1073 let mut pace = FramePace::default();
1074 let mut geometry_check = GEOMETRY_POLL_FRAMES;
1075 let mut x_failures = 0u32;
1076 let mut encode_errors = 0u32;
1077 let mut encoder_rebuilt = false;
1078 let mut delivered_any = false;
1082 let mut last_log = Instant::now();
1083 let mut sent_frames: u64 = 0;
1084 let mut damaged_frames: u64 = 0;
1085 let mut first_frame = true;
1086 let mut tid_sent = false;
1087
1088 while !controls.stop.load(Ordering::Relaxed) {
1089 let fps = f64::from_bits(controls.fps_bits.load(Ordering::Relaxed)).max(1.0);
1090 let frame_dur = Duration::from_secs_f64(1.0 / fps.max(1.0));
1091 let trigger = wait_for_frame(&gpu.x.conn, Some(&gpu.x.damage), &pace, frame_dur);
1092 let woke = Instant::now();
1093 let is_dirty = trigger == TickTrigger::Damage || first_frame;
1096 let changed = gpu
1098 .x
1099 .damage
1100 .take(&gpu.x.conn)
1101 .filter(|rects| !first_frame && !rects.is_empty());
1102 first_frame = false;
1103 damaged_frames += is_dirty as u64;
1104 if controls.stop.load(Ordering::Relaxed) {
1105 break;
1106 }
1107
1108 for report in std::mem::take(&mut *controls.reference_reports.lock().unwrap()) {
1109 if !gpu.enc().take_report(report) {
1110 pending_force_idr = true;
1111 }
1112 }
1113 if controls.force_idr.swap(false, Ordering::Relaxed)
1114 || recording_sink
1115 .as_ref()
1116 .is_some_and(|s| s.should_force_idr())
1117 {
1118 pending_force_idr = true;
1119 }
1120 if controls.rate_dirty.swap(false, Ordering::Acquire) {
1121 gpu.settings.video_bitrate_kbps = controls.bitrate_kbps.load(Ordering::Relaxed);
1122 gpu.settings.video_vbv_multiplier =
1123 controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1124 gpu.settings.target_fps = fps;
1125 let live = gpu.settings.clone();
1126 if let Err(e) = gpu.enc().reconfigure_rate(&live) {
1127 eprintln!(
1128 "[X11] DRI3 capture: rate reconfigure failed ({e}); rebuilding the encoder."
1129 );
1130 if let Err(e) = gpu.rebuild_encoder() {
1131 return Some(Err(format!("DRI3 capture ended: {e}")));
1132 }
1133 state = StripeState::default();
1134 pending_force_idr = true;
1135 }
1136 }
1137 if controls.tunables_dirty.swap(false, Ordering::Acquire)
1138 && let Some(t) = controls.tunables.lock().unwrap().take()
1139 {
1140 t.apply_to(&mut gpu.settings);
1141 }
1142
1143 let want_cursor = controls.capture_cursor.load(Ordering::Relaxed);
1144 let mut recheck_geometry = controls.region_dirty.swap(false, Ordering::Acquire);
1145 if recheck_geometry {
1146 let (nx, ny, nw, nh) = *controls.region.lock().unwrap();
1147 gpu.request.capture_x = nx;
1148 gpu.request.capture_y = ny;
1149 gpu.request.width = nw;
1150 gpu.request.height = nh;
1151 gpu.request.auto_adjust_screen_capture_size = nw <= 0 || nh <= 0;
1152 }
1153 geometry_check -= 1;
1154 if geometry_check <= 0 {
1155 geometry_check = GEOMETRY_POLL_FRAMES;
1156 recheck_geometry = true;
1157 }
1158 if recheck_geometry {
1159 match gpu
1160 .x
1161 .conn
1162 .get_geometry(gpu.x.root)
1163 .ok()
1164 .and_then(|c| c.reply().ok())
1165 {
1166 Some(g) => {
1167 gpu.root_w = g.width;
1168 gpu.root_h = g.height;
1169 }
1170 None => {
1171 x_failures += 1;
1172 if x_failures > MAX_X_FAILURES {
1173 return Some(Err(
1174 "DRI3 capture ended: the X server stopped answering".to_string()
1175 ));
1176 }
1177 continue;
1178 }
1179 }
1180 gpu.cap_x = clamp_offset(gpu.request.capture_x, gpu.root_w);
1181 gpu.cap_y = clamp_offset(gpu.request.capture_y, gpu.root_h);
1182 let (w, h) = resolve_dims(gpu.root_w, gpu.root_h, &gpu.request);
1183 if w as i32 != gpu.settings.width || h as i32 != gpu.settings.height {
1184 if let Err(e) = gpu.reshape(w, h) {
1185 return Some(Err(format!(
1186 "DRI3 capture could not follow the new geometry: {e}"
1187 )));
1188 }
1189 state = StripeState::default();
1190 pending_force_idr = true;
1191 }
1192 }
1193
1194 let quality = EncoderQuality::of(gpu.enc());
1195 let decision = decide_hw_fullframe(
1196 &mut state,
1197 &gpu.settings,
1198 frame_counter,
1199 match changed {
1200 _ if !is_dirty => Damage::None,
1201 Some(rects) => captured_damage(
1202 &rects,
1203 gpu.cap_x.into(),
1204 gpu.cap_y.into(),
1205 gpu.settings.width,
1206 gpu.settings.height,
1207 ),
1208 None => Damage::Unknown,
1209 },
1210 false,
1211 pending_force_idr,
1212 quality,
1213 );
1214 if decision.send {
1217 pace.ticked(trigger, frame_dur, woke, false);
1218 } else {
1219 pace.defer(woke + frame_dur);
1220 }
1221 let mut delivered = false;
1222 if decision.send {
1223 let grabbed_ns = crate::wayland::host::now_ns();
1227 let (idx, after_blit) = match gpu.grab(want_cursor) {
1228 Ok(i) => {
1229 x_failures = 0;
1230 i
1231 }
1232 Err(e) => {
1233 x_failures += 1;
1234 if x_failures > MAX_X_FAILURES {
1235 return Some(Err(format!("DRI3 capture ended: {e}")));
1236 }
1237 frame_counter = frame_counter.wrapping_add(1);
1238 continue;
1239 }
1240 };
1241 let dmabuf = gpu.buffers[idx].dmabuf.clone();
1242 decision.prepare(gpu.enc());
1243 let encode_start_ns = crate::wayland::host::now_ns();
1244 let result = if after_blit {
1245 gpu.enc().encode_dmabuf_after_blit(
1246 &dmabuf,
1247 frame_counter as u64,
1248 decision.target_qp,
1249 decision.force_idr,
1250 )
1251 } else {
1252 gpu.enc().encode_dmabuf(
1253 &dmabuf,
1254 frame_counter as u64,
1255 decision.target_qp,
1256 decision.force_idr,
1257 )
1258 };
1259 match result {
1260 Ok(data) if !data.is_empty() => {
1261 encode_errors = 0;
1262 let stripes = vec![EncodedStripe {
1263 data: Arc::new(data),
1264 codec: gpu.settings.codec,
1265 stripe_y_start: 0,
1266 stripe_height: gpu.settings.height,
1267 frame_id: frame_counter as i32,
1268 timing: FrameTiming {
1269 capture_ns: grabbed_ns,
1270 encode_start_ns,
1271 encode_end_ns: crate::wayland::host::now_ns(),
1272 },
1273 reference: gpu.enc().last_reference(),
1274 }];
1275 if let Some(sink) = &recording_sink {
1276 sink.write_frame(&stripes, gpu.settings.width, gpu.settings.height);
1277 }
1278 sent_frames += 1;
1279 delivered = true;
1280 delivered_any = true;
1281 on_frame(stripes);
1282 }
1283 Ok(_) => {
1284 encode_errors = 0;
1285 delivered_any = true;
1286 }
1287 Err(e) => {
1288 if !delivered_any {
1289 drop(gpu);
1291 declined(&format!(
1292 "the encoder could not read the server's buffer: {e}"
1293 ));
1294 return None;
1295 }
1296 encode_errors += 1;
1297 if encode_errors % crate::HW_ERROR_RECOVERY_THRESHOLD == 1 {
1298 eprintln!("[X11] hardware encode error on the DRI3 path: {e}");
1299 }
1300 if encode_errors >= crate::HW_ERROR_RECOVERY_THRESHOLD {
1301 if encoder_rebuilt {
1302 return Some(Err(
1303 "the encoder failed repeatedly on the DRI3 path".to_string()
1304 ));
1305 }
1306 eprintln!(
1307 "[X11] rebuilding the encoder after repeated errors on the DRI3 path."
1308 );
1309 if let Err(e) = gpu.rebuild_encoder() {
1310 return Some(Err(format!("DRI3 capture ended: {e}")));
1311 }
1312 encoder_rebuilt = true;
1313 encode_errors = 0;
1314 state = StripeState::default();
1315 pending_force_idr = true;
1316 }
1317 }
1318 }
1319 }
1320 if delivered_any && !tid_sent {
1321 let _ = encode_tid_tx.send(std::thread::current().id());
1322 tid_sent = true;
1323 }
1324 pending_force_idr = (pending_force_idr || decision.force_idr) && !delivered;
1325 frame_counter = frame_counter.wrapping_add(1);
1326
1327 let elapsed = last_log.elapsed().as_secs_f64();
1328 if elapsed >= 1.0 {
1329 crate::log::debug!(
1330 "[X11] DRI3 {}x{} Encoder: {} EncFPS: {:.2} Damaged/s: {:.2}",
1331 gpu.settings.width,
1332 gpu.settings.height,
1333 gpu.backend(),
1334 sent_frames as f64 / elapsed,
1335 damaged_frames as f64 / elapsed
1336 );
1337 sent_frames = 0;
1338 damaged_frames = 0;
1339 last_log = Instant::now();
1340 }
1341 }
1342 Some(Ok(()))
1343}
1344
1345#[cfg(test)]
1346mod modifier_tests {
1347 use super::{intel_compressed, prune_modifier};
1348
1349 const CCS_CC: u64 = 0x0100_0000_0000_0008;
1350 const CCS: u64 = 0x0100_0000_0000_0006;
1351 const Y_TILED: u64 = 0x0100_0000_0000_0002;
1352
1353 #[test]
1356 fn the_single_plane_compressed_layouts_are_not_offered() {
1357 const X_TILED: u64 = 0x0100_0000_0000_0001;
1358 const TILE4: u64 = 0x0100_0000_0000_0009;
1359 const DG2_RC_CCS: u64 = 0x0100_0000_0000_000a;
1360 const DG2_RC_CCS_CC: u64 = 0x0100_0000_0000_000c;
1361 let mut mods = vec![0, X_TILED, TILE4, DG2_RC_CCS, DG2_RC_CCS_CC];
1362 mods.retain(|&m| !intel_compressed(m));
1363 assert_eq!(mods, vec![0, X_TILED, TILE4]);
1364 assert!(intel_compressed(CCS) && intel_compressed(CCS_CC));
1365 assert!(!intel_compressed(Y_TILED) && !intel_compressed(0x00ff_ffff_ffff_ffff));
1366 }
1367
1368 #[test]
1371 fn a_rejected_modifier_leaves_the_others() {
1372 let mut mods = vec![CCS_CC, CCS, Y_TILED];
1373 prune_modifier(&mut mods, CCS_CC);
1374 assert_eq!(mods, vec![CCS, Y_TILED]);
1375 prune_modifier(&mut mods, CCS);
1376 assert_eq!(mods, vec![Y_TILED]);
1377 }
1378
1379 #[test]
1382 fn a_choice_outside_the_list_spends_it() {
1383 let mut mods = vec![CCS, Y_TILED];
1384 prune_modifier(&mut mods, 0x00ff_ffff_ffff_ffff);
1385 assert!(mods.is_empty());
1386 }
1387}
1388
1389#[cfg(test)]
1390mod gpu_tests {
1391 use super::super::gpu_test_support::{decoded_mean, paint_root, painted_ycbcr, settings};
1392 use super::*;
1393 use crate::encoders::codec::{FRAME_DELTA, FRAME_KEY, VIDEO_HEADER_LEN, parse_video_type};
1394 use crate::webcam::decode::{Codec as DecCodec, Decoder, VideoDecoder};
1395
1396 fn encode_grabbed(
1399 gpu: &mut GpuCapture,
1400 (idx, after_blit): (usize, bool),
1401 i: u64,
1402 key: bool,
1403 ) -> Vec<u8> {
1404 let dmabuf = gpu.buffers[idx].dmabuf.clone();
1405 if after_blit {
1406 gpu.enc().encode_dmabuf_after_blit(&dmabuf, i, 25, key)
1407 } else {
1408 gpu.enc().encode_dmabuf(&dmabuf, i, 25, key)
1409 }
1410 .expect("encode in place")
1411 }
1412
1413 #[test]
1416 #[ignore]
1417 fn gpu_dri3_counts_the_primary_node_of_the_encode_gpu_as_it() {
1418 let node = settings(crate::encoders::codec::Codec::H264)
1419 .encode_node_index
1420 .max(0);
1421 let render = format!("/dev/dri/renderD{}", 128 + node);
1422 let render_dev = std::fs::metadata(&render).expect("encode node").rdev();
1423 let primary = std::fs::read_dir(format!("/sys/class/drm/renderD{}/device/drm", 128 + node))
1424 .expect("the encode node's GPU in sysfs")
1425 .filter_map(|e| e.ok()?.file_name().into_string().ok())
1426 .find(|name| name.starts_with("card"))
1427 .expect("a primary node on the encode node's GPU");
1428 let primary_dev = std::fs::metadata(format!("/dev/dri/{primary}"))
1429 .expect("primary node")
1430 .rdev();
1431 assert_ne!(
1432 primary_dev, render_dev,
1433 "{primary} and {render} are distinct nodes"
1434 );
1435 assert!(
1436 same_gpu(primary_dev, render_dev),
1437 "{primary} is the GPU of {render}"
1438 );
1439 }
1440
1441 #[test]
1446 #[ignore]
1447 fn gpu_dri3_composites_a_watermark_without_a_readback() {
1448 const SCREEN: (u8, u8, u8) = (0x20, 0x40, 0x80);
1449 const MARK: (u8, u8, u8) = (0xff, 0x00, 0x00);
1450 const ALPHA: u8 = 128;
1451 if !paint_root(SCREEN) {
1452 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1453 return;
1454 }
1455 let path = std::env::temp_dir().join("pixelflux-dri3-watermark.png");
1456 image::RgbaImage::from_pixel(96, 96, image::Rgba([MARK.0, MARK.1, MARK.2, ALPHA]))
1457 .save(&path)
1458 .expect("write the watermark");
1459 let mut s = settings(crate::encoders::codec::Codec::H264);
1460 s.watermark_path = path.to_string_lossy().into_owned();
1461 let Some(mut gpu) = open(&s) else {
1462 println!(
1463 "the DRI3 path declined a watermarked session on this host; nothing to capture"
1464 );
1465 return;
1466 };
1467 let grabbed = gpu.grab(false).expect("blit and composite");
1468 let pkt = encode_grabbed(&mut gpu, grabbed, 0, true);
1469 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1470 assert!(
1471 dec.decode(&pkt[VIDEO_HEADER_LEN..]).expect("decode"),
1472 "no picture"
1473 );
1474 let v = dec.frame().expect("decoded frame");
1475 let blend =
1477 |m: u8, bg: u8| (m as f64 * ALPHA as f64 + bg as f64 * (255.0 - ALPHA as f64)) / 255.0;
1478 let want = crate::encoders::chroma_siting::ycbcr(
1479 [
1480 blend(MARK.0, SCREEN.0),
1481 blend(MARK.1, SCREEN.1),
1482 blend(MARK.2, SCREEN.2),
1483 ],
1484 crate::encoders::chroma_siting::BT709,
1485 );
1486 let (mx, my) = (48usize, 48usize);
1487 let got = [
1488 v.y[my * v.y_stride + mx] as f64,
1489 v.u[(my / 2) * v.uv_stride + mx / 2] as f64,
1490 v.v[(my / 2) * v.uv_stride + mx / 2] as f64,
1491 ];
1492 for i in 0..3 {
1493 assert!(
1494 (got[i] - want[i]).abs() <= 10.0,
1495 "watermark plane {i}: captured {:.1}, blended {:.1}",
1496 got[i],
1497 want[i]
1498 );
1499 }
1500 let _ = std::fs::remove_file(&path);
1501 println!("the server composited the watermark at ({mx},{my}) to Y/Cb/Cr {got:?}");
1502 }
1503
1504 #[test]
1511 #[ignore]
1512 fn gpu_dri3_zero_copy_encodes_the_painted_root() {
1513 const FIRST: (u8, u8, u8) = (0x20, 0x40, 0xc0);
1514 const SECOND: (u8, u8, u8) = (0xd0, 0x50, 0x18);
1515 if !paint_root(FIRST) {
1516 println!("no X root this host can paint (xsetroot, $DISPLAY); nothing to capture");
1517 return;
1518 }
1519 let Some(mut gpu) = open(&settings(crate::encoders::codec::Codec::H264)) else {
1520 println!("the DRI3 path declined this session on this host; nothing to capture");
1521 return;
1522 };
1523 let mut dec = VideoDecoder::new(DecCodec::H264).expect("H.264 decoder");
1524
1525 let encode = |gpu: &mut GpuCapture, i: u64, key: bool| -> Vec<u8> {
1526 let grabbed = gpu.grab(false).expect("blit into the pool");
1527 encode_grabbed(gpu, grabbed, i, key)
1528 };
1529
1530 let pkt = encode(&mut gpu, 0, true);
1531 assert_eq!(
1532 parse_video_type(pkt[1]),
1533 Some((crate::encoders::codec::Codec::H264, FRAME_KEY))
1534 );
1535 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1536 let want = painted_ycbcr(FIRST);
1537 for i in 0..3 {
1538 assert!(
1539 (mean[i] - want[i]).abs() <= 8.0,
1540 "plane {i}: captured {:.1}, painted {:.1}",
1541 mean[i],
1542 want[i]
1543 );
1544 }
1545
1546 for i in 1..4u64 {
1547 let pkt = encode(&mut gpu, i, false);
1548 assert_eq!(
1549 parse_video_type(pkt[1]),
1550 Some((crate::encoders::codec::Codec::H264, FRAME_DELTA))
1551 );
1552 let _ = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1553 }
1554
1555 assert!(
1556 paint_root(SECOND),
1557 "the root was painted once, so a repaint must work"
1558 );
1559 let pkt = encode(&mut gpu, 4, false);
1560 let mean = decoded_mean(&mut dec, &pkt[VIDEO_HEADER_LEN..]);
1561 let want = painted_ycbcr(SECOND);
1562 for i in 0..3 {
1563 assert!(
1564 (mean[i] - want[i]).abs() <= 8.0,
1565 "after repaint, plane {i}: captured {:.1}, painted {:.1}",
1566 mean[i],
1567 want[i]
1568 );
1569 }
1570 println!(
1571 "captured {}x{} through {} server-imported buffers on {} and encoded in place on {}",
1572 gpu.settings.width,
1573 gpu.settings.height,
1574 POOL_N,
1575 gpu.x.device,
1576 gpu.backend()
1577 );
1578 }
1579}