1#![allow(dead_code)]
55
56use std::fs::File;
57use std::sync::Arc;
58use std::thread;
59use std::time::{Duration, Instant};
60
61use gbm::{BufferObject, BufferObjectFlags, Device as RawGbmDevice, Format as GbmFormat};
62use pyo3::prelude::*;
63use pyo3::types::{PyAny, PyModule};
64
65use smithay::backend::egl::fence::EGLFence;
66use smithay::desktop::utils::{OutputPresentationFeedback, send_frames_surface_tree};
67use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
68use smithay::wayland::cursor_shape::CursorShapeManagerState;
69use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
70use smithay::wayland::image_copy_capture::{CaptureFailureReason, ImageCopyCaptureState};
71use smithay::wayland::presentation::{PresentationState, Refresh};
72use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
73use smithay::wayland::selection::wlr_data_control::DataControlState;
74use smithay::wayland::single_pixel_buffer::SinglePixelBufferState;
75use smithay::wayland::viewporter::ViewporterState;
76use smithay::{
77 backend::{
78 allocator::{
79 Fourcc, Modifier,
80 dmabuf::{Dmabuf, DmabufFlags},
81 gbm::GbmDevice,
82 },
83 drm::DrmNode,
84 egl::{EGLContext, EGLDisplay},
85 input::{Axis, AxisSource, KeyState, Keycode},
86 renderer::{
87 Bind, ExportMem, Frame as _, ImportAll, ImportDma, ImportEgl, ImportMem, Renderer as _,
88 damage::OutputDamageTracker,
89 element::{
90 AsRenderElements, Element, RenderElement, Wrap,
91 memory::MemoryRenderBufferRenderElement, surface::WaylandSurfaceRenderElement,
92 },
93 gles::GlesRenderer,
94 pixman::PixmanRenderer,
95 sync::SyncPoint,
96 },
97 },
98 desktop::{LayerMap, PopupManager, layer_map_for_output},
99 desktop::{Space, space::SpaceRenderElements},
100 input::{
101 SeatState,
102 keyboard::{FilterResult, XkbConfig},
103 pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent, RelativeMotionEvent},
104 },
105 output::{Mode as OutputMode, Output, PhysicalProperties, Scale as OutputScale, Subpixel},
106 reexports::{
107 calloop::{
108 EventLoop, Interest, Mode, PostAction,
109 generic::Generic,
110 timer::{TimeoutAction, Timer},
111 },
112 pixman,
113 wayland_server::{Display, DisplayHandle},
114 },
115 utils::{Clock, Physical, Point, Rectangle, Scale, Transform},
116 wayland::selection::primary_selection::PrimarySelectionState,
117 wayland::shell::wlr_layer::WlrLayerShellState,
118 wayland::xdg_activation::XdgActivationState,
119 wayland::{
120 compositor::{CompositorState, with_states},
121 dmabuf::{DmabufFeedbackBuilder, DmabufState},
122 foreign_toplevel_list::ForeignToplevelListState,
123 fractional_scale::FractionalScaleManagerState,
124 output::OutputManagerState,
125 pointer_constraints::PointerConstraintsState,
126 pointer_warp::PointerWarpManager,
127 relative_pointer::RelativePointerManagerState,
128 seat::WaylandFocus,
129 selection::data_device::DataDeviceState,
130 shell::xdg::XdgShellState,
131 shell::xdg::decoration::XdgDecorationState,
132 shm::ShmState,
133 socket::ListeningSocketSource,
134 },
135};
136
137pub mod encoders;
139pub mod log;
141
142pub mod computer_use;
144pub mod pace;
146pub mod recorder;
148pub mod recording_sink;
150pub mod report;
152pub mod uinput;
154pub mod wayland;
156
157#[cfg(test)]
158mod cleanup_bench;
159pub mod nvgpufilter;
161pub mod pipeline;
163pub mod pipewire;
165pub mod x11;
167
168pub mod webcam;
169
170pub use encoders::nvenc;
171pub use encoders::software::StripeState;
172
173fn get_process_rss_bytes() -> usize {
174 if let Ok(contents) = std::fs::read_to_string("/proc/self/statm")
175 && let Some(rss_pages) = contents.split_whitespace().nth(1)
176 && let Ok(pages) = rss_pages.parse::<usize>()
177 {
178 return pages * 4096;
179 }
180 0
181}
182
183fn shm_usage_in(dir: &str) -> u64 {
186 use std::os::unix::fs::MetadataExt;
187 let mut total_size = 0;
188 if let Ok(entries) = std::fs::read_dir(dir) {
189 for entry in entries.flatten() {
190 if let Ok(metadata) = entry.metadata() {
191 total_size += metadata.blocks() * 512;
192 }
193 }
194 }
195 total_size
196}
197
198fn get_shm_usage_bytes() -> u64 {
199 shm_usage_in("/dev/shm")
200}
201
202use encoders::overlay::OverlayState;
203use encoders::reference::Reference;
204use encoders::software::MAX_STRIPE_CAPACITY;
205use encoders::{Codec, FrameEncoder, FrameSource};
206
207use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1;
208
209use pace::{FramePace, TickTrigger};
210use wayland::cursor::{Cursor, CursorJob};
211use wayland::frontend::{AppState, ClientState, FocusTarget, next_serial, wayland_time};
212
213smithay::backend::renderer::element::render_elements! {
214 pub CompositionElements<R, E> where R: ImportAll + ImportMem;
215 Space=SpaceRenderElements<R, E>,
216 Window=Wrap<E>,
217 Cursor=MemoryRenderBufferRenderElement<R>,
218 Surface=WaylandSurfaceRenderElement<R>,
219}
220
221fn push_layer_elements<R>(
226 renderer: &mut R,
227 elements: &mut Vec<CompositionElements<R, WaylandSurfaceRenderElement<R>>>,
228 layer_map: &LayerMap,
229 target_layer: smithay::wayland::shell::wlr_layer::Layer,
230 scale: f64,
231) where
232 R: smithay::backend::renderer::Renderer + ImportAll + ImportMem,
233 R::TextureId: Clone + 'static,
234{
235 for surface in layer_map.layers().rev() {
236 if surface.layer() == target_layer
237 && let Some(geo) = layer_map.layer_geometry(surface)
238 {
239 elements.extend(
240 AsRenderElements::<R>::render_elements::<WaylandSurfaceRenderElement<R>>(
241 surface,
242 renderer,
243 geo.loc.to_physical_precise_round(scale),
244 Scale::from(scale),
245 1.0,
246 )
247 .into_iter()
248 .map(CompositionElements::Surface),
249 );
250 }
251 }
252}
253
254pub(crate) fn alloc_render_target<T: std::os::fd::AsFd>(
262 gbm: &RawGbmDevice<T>,
263 width: u32,
264 height: u32,
265 format: GbmFormat,
266) -> std::io::Result<BufferObject<()>> {
267 let aligned = |bo: &BufferObject<()>| {
268 Into::<u64>::into(bo.modifier()) != 0 || bo.stride().is_multiple_of(64)
269 };
270 let bo = gbm.create_buffer_object::<()>(width, height, format, BufferObjectFlags::RENDERING)?;
271 if aligned(&bo) {
272 return Ok(bo);
273 }
274 match gbm.create_buffer_object::<()>(
275 width,
276 height,
277 format,
278 BufferObjectFlags::RENDERING | BufferObjectFlags::SCANOUT,
279 ) {
280 Ok(scanout) if aligned(&scanout) => Ok(scanout),
281 _ => Ok(bo),
282 }
283}
284
285pub(crate) fn create_dmabuf_from_bo(bo: &BufferObject<()>) -> Dmabuf {
293 let fd = bo.fd().expect("Failed to get FD from GBM BO");
294 let modifier = bo.modifier();
295 let stride = bo.stride();
296 let width = bo.width();
297 let height = bo.height();
298
299 let drm_modifier = Modifier::from(Into::<u64>::into(modifier));
300
301 let mut builder = Dmabuf::builder(
302 (width as i32, height as i32),
303 Fourcc::Argb8888,
304 drm_modifier,
305 DmabufFlags::empty(),
306 );
307
308 builder.add_plane(fd, 0, stride);
309 builder.build().expect("Failed to build Dmabuf from GBM BO")
310}
311
312#[derive(Clone, Debug, PartialEq)]
321pub struct RustCaptureSettings {
322 pub width: i32,
323 pub height: i32,
324 pub scale: f64,
325 pub capture_x: i32,
326 pub capture_y: i32,
327 pub target_fps: f64,
328 pub jpeg_quality: i32,
329 pub paint_over_jpeg_quality: i32,
330 pub use_paint_over_quality: bool,
331 pub paint_over_trigger_frames: u32,
332 pub damage_block_threshold: u32,
333 pub damage_block_duration: u32,
334 pub codec: Codec,
335 pub video_crf: i32,
336 pub video_paintover_crf: i32,
337 pub video_paintover_burst_frames: i32,
338 pub video_fullcolor: bool,
339 pub video_bit_depth: i32,
342 pub video_fullframe: bool,
343 pub video_streaming_mode: bool,
344 pub capture_cursor: bool,
345 pub cursor_size_cap: i32,
348 pub watermark_path: String,
349 pub watermark_location_enum: i32,
350 pub encode_node_index: i32,
351 pub use_cpu: bool,
352 pub debug_logging: bool,
353 pub auto_adjust_screen_capture_size: bool,
354 pub recording_socket: String,
355 pub wayland_host_display: String,
358 pub omit_stripe_headers: bool,
361 pub acknowledge_references: bool,
366 pub video_reference_frames: u32,
371 pub video_cbr_mode: bool,
372 pub video_bitrate_kbps: i32,
373 pub video_vbv_multiplier: f64,
379 pub keyframe_interval_s: f64,
382 pub video_min_qp: i32,
387 pub video_max_qp: i32,
388}
389
390#[derive(Clone, Copy, Debug)]
396pub struct LiveTunables {
397 pub jpeg_quality: i32,
398 pub paint_over_jpeg_quality: i32,
399 pub use_paint_over_quality: bool,
400 pub paint_over_trigger_frames: u32,
401 pub video_crf: i32,
402 pub video_paintover_crf: i32,
403 pub video_paintover_burst_frames: i32,
404 pub video_streaming_mode: bool,
405 pub keyframe_interval_s: f64,
406 pub capture_cursor: bool,
407 pub cursor_size_cap: i32,
408}
409
410impl LiveTunables {
411 pub fn from_settings(s: &RustCaptureSettings) -> Self {
413 Self {
414 jpeg_quality: s.jpeg_quality,
415 paint_over_jpeg_quality: s.paint_over_jpeg_quality,
416 use_paint_over_quality: s.use_paint_over_quality,
417 paint_over_trigger_frames: s.paint_over_trigger_frames,
418 video_crf: s.video_crf,
419 video_paintover_crf: s.video_paintover_crf,
420 video_paintover_burst_frames: s.video_paintover_burst_frames,
421 video_streaming_mode: s.video_streaming_mode,
422 keyframe_interval_s: s.keyframe_interval_s,
423 capture_cursor: s.capture_cursor,
424 cursor_size_cap: s.cursor_size_cap,
425 }
426 }
427
428 pub fn apply_to(&self, s: &mut RustCaptureSettings) {
430 s.jpeg_quality = self.jpeg_quality;
431 s.paint_over_jpeg_quality = self.paint_over_jpeg_quality;
432 s.use_paint_over_quality = self.use_paint_over_quality;
433 s.paint_over_trigger_frames = self.paint_over_trigger_frames;
434 s.video_crf = self.video_crf;
435 s.video_paintover_crf = self.video_paintover_crf;
436 s.video_paintover_burst_frames = self.video_paintover_burst_frames;
437 s.video_streaming_mode = self.video_streaming_mode;
438 s.keyframe_interval_s = self.keyframe_interval_s;
439 s.cursor_size_cap = self.cursor_size_cap;
440 s.capture_cursor = self.capture_cursor;
441 }
442}
443
444impl Default for RustCaptureSettings {
445 fn default() -> Self {
446 Self {
447 width: 1024,
448 height: 768,
449 scale: 1.0,
450 capture_x: 0,
451 capture_y: 0,
452 target_fps: 60.0,
453 jpeg_quality: 75,
454 paint_over_jpeg_quality: 95,
455 use_paint_over_quality: true,
456 paint_over_trigger_frames: 15,
457 damage_block_threshold: 10,
458 damage_block_duration: 30,
459 codec: Codec::Jpeg,
460 video_crf: 25,
461 video_paintover_crf: 18,
462 video_paintover_burst_frames: 5,
463 video_fullcolor: false,
464 video_bit_depth: 8,
465 video_fullframe: false,
466 video_streaming_mode: false,
467 capture_cursor: false,
468 cursor_size_cap: 128,
469 watermark_path: String::new(),
470 watermark_location_enum: 0,
471 encode_node_index: -2,
472 use_cpu: false,
473 debug_logging: false,
474 auto_adjust_screen_capture_size: false,
475 recording_socket: String::new(),
476 wayland_host_display: String::new(),
477 omit_stripe_headers: false,
478 acknowledge_references: false,
479 video_reference_frames: 0,
480 video_cbr_mode: false,
481 video_bitrate_kbps: 4000,
482 video_vbv_multiplier: 0.0,
483 keyframe_interval_s: 0.0,
484 video_min_qp: 0,
485 video_max_qp: 0,
486 }
487 }
488}
489
490pub(crate) fn extract_settings(settings: &Bound<'_, PyAny>) -> PyResult<RustCaptureSettings> {
508 let watermark_path_obj = settings.getattr("watermark_path")?;
509 let watermark_path = if let Ok(s) = watermark_path_obj.extract::<String>() {
510 s
511 } else if let Ok(b) = watermark_path_obj.extract::<Vec<u8>>() {
512 String::from_utf8_lossy(&b).into_owned()
513 } else {
514 String::new()
515 };
516
517 let scale = settings
518 .getattr("scale")
519 .ok()
520 .and_then(|x| x.extract().ok())
521 .unwrap_or(1.0);
522
523 let sanitize_dim = |v: i32| -> i32 { if v <= 0 { 0 } else { v.min(MAX_CAPTURE_DIM) } };
528 let sanitize_fps = |v: f64| -> f64 {
529 if v.is_finite() && v > 0.0 {
530 v.min(MAX_FPS)
531 } else {
532 DEFAULT_FPS
533 }
534 };
535 let sanitize_scale = |v: f64| -> f64 {
536 if v.is_finite() && v > 0.0 {
537 v.min(MAX_SCALE)
538 } else {
539 1.0
540 }
541 };
542
543 Ok(RustCaptureSettings {
544 width: sanitize_dim(settings.getattr("capture_width")?.extract()?),
545 height: sanitize_dim(settings.getattr("capture_height")?.extract()?),
546 scale: sanitize_scale(scale),
547 capture_x: settings.getattr("capture_x")?.extract()?,
548 capture_y: settings.getattr("capture_y")?.extract()?,
549 target_fps: sanitize_fps(settings.getattr("target_fps")?.extract()?),
550 jpeg_quality: settings
551 .getattr("jpeg_quality")?
552 .extract::<i32>()?
553 .clamp(1, 100),
554 paint_over_jpeg_quality: settings
555 .getattr("paint_over_jpeg_quality")?
556 .extract::<i32>()?
557 .clamp(1, 100),
558 use_paint_over_quality: settings.getattr("use_paint_over_quality")?.extract()?,
559 paint_over_trigger_frames: settings.getattr("paint_over_trigger_frames")?.extract()?,
560 damage_block_threshold: settings.getattr("damage_block_threshold")?.extract()?,
561 damage_block_duration: settings.getattr("damage_block_duration")?.extract()?,
562 codec: {
563 let name: String = settings.getattr("codec")?.extract()?;
564 Codec::parse(&name).ok_or_else(|| {
565 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
566 "unknown codec '{name}': expected jpeg, h264, h265, vp8, vp9, or av1"
567 ))
568 })?
569 },
570 video_crf: settings.getattr("video_crf")?.extract()?,
571 video_paintover_crf: settings.getattr("video_paintover_crf")?.extract()?,
572 video_paintover_burst_frames: settings
573 .getattr("video_paintover_burst_frames")?
574 .extract()?,
575 video_fullcolor: settings.getattr("video_fullcolor")?.extract()?,
576 video_bit_depth: settings
577 .getattr("video_bit_depth")
578 .ok()
579 .and_then(|v| v.extract::<i32>().ok())
580 .unwrap_or(8),
581 video_fullframe: settings.getattr("video_fullframe")?.extract()?,
582 video_streaming_mode: settings.getattr("video_streaming_mode")?.extract()?,
583 capture_cursor: settings.getattr("capture_cursor")?.extract()?,
584 cursor_size_cap: settings
585 .getattr("cursor_size_cap")
586 .ok()
587 .and_then(|v| v.extract::<i32>().ok())
588 .unwrap_or(RustCaptureSettings::default().cursor_size_cap),
589 watermark_path,
590 watermark_location_enum: settings.getattr("watermark_location_enum")?.extract()?,
591 encode_node_index: settings.getattr("encode_node_index")?.extract()?,
592 use_cpu: settings.getattr("use_cpu")?.extract()?,
593 debug_logging: {
594 let on: bool = settings.getattr("debug_logging")?.extract()?;
595 crate::log::set_debug(on);
596 on
597 },
598 auto_adjust_screen_capture_size: settings
599 .getattr("auto_adjust_screen_capture_size")
600 .ok()
601 .and_then(|v| v.extract::<bool>().ok())
602 .unwrap_or(false),
603 recording_socket: settings
604 .getattr("recording_socket")
605 .ok()
606 .and_then(|v| v.extract::<String>().ok())
607 .unwrap_or_default(),
608 wayland_host_display: settings
609 .getattr("wayland_host_display")
610 .ok()
611 .and_then(|v| v.extract::<String>().ok())
612 .unwrap_or_default(),
613 omit_stripe_headers: settings
614 .getattr("omit_stripe_headers")
615 .ok()
616 .and_then(|v| v.extract::<bool>().ok())
617 .unwrap_or(false),
618 acknowledge_references: settings
619 .getattr("acknowledge_references")
620 .ok()
621 .and_then(|v| v.extract::<bool>().ok())
622 .unwrap_or(false),
623 video_reference_frames: settings
624 .getattr("video_reference_frames")
625 .ok()
626 .and_then(|v| v.extract::<u32>().ok())
627 .unwrap_or(0),
628 video_cbr_mode: settings.getattr("video_cbr_mode")?.extract()?,
629 video_bitrate_kbps: settings.getattr("video_bitrate_kbps")?.extract()?,
630 video_vbv_multiplier: settings
631 .getattr("video_vbv_multiplier")
632 .ok()
633 .and_then(|v| v.extract::<f64>().ok())
634 .unwrap_or(0.0),
635 keyframe_interval_s: settings
636 .getattr("keyframe_interval_s")
637 .ok()
638 .and_then(|v| v.extract::<f64>().ok())
639 .unwrap_or(0.0),
640 video_min_qp: settings
641 .getattr("video_min_qp")
642 .ok()
643 .and_then(|v| v.extract::<i32>().ok())
644 .unwrap_or(0),
645 video_max_qp: settings
646 .getattr("video_max_qp")
647 .ok()
648 .and_then(|v| v.extract::<i32>().ok())
649 .unwrap_or(0),
650 })
651}
652
653pub type OutputDesc = (u32, i32, i32, i32, i32, f64, bool);
656
657pub type WindowDesc = (u32, String, String, u32, bool);
660
661pub enum ThreadCommand {
668 StartCapture {
672 display_id: u32,
673 callback: Option<Py<PyAny>>,
674 settings: RustCaptureSettings,
675 },
676 StopCapture {
678 display_id: u32,
679 },
680 CreateOutput {
684 id: u32,
685 width: i32,
686 height: i32,
687 x: i32,
688 y: i32,
689 scale: f64,
690 reply: std::sync::mpsc::Sender<bool>,
691 },
692 CreateView {
696 id: u32,
697 owner: u32,
698 x: i32,
699 y: i32,
700 width: i32,
701 height: i32,
702 reply: std::sync::mpsc::Sender<bool>,
703 },
704 ResizeOutput {
707 id: u32,
708 width: i32,
709 height: i32,
710 scale: f64,
711 reply: std::sync::mpsc::Sender<bool>,
712 },
713 DestroyOutput {
716 id: u32,
717 reply: std::sync::mpsc::Sender<bool>,
718 },
719 RepositionOutput {
724 id: u32,
725 x: i32,
726 y: i32,
727 reply: std::sync::mpsc::Sender<bool>,
728 },
729 ListOutputs {
731 reply: std::sync::mpsc::Sender<Vec<OutputDesc>>,
732 },
733 OutputCapacity {
737 reply: std::sync::mpsc::Sender<i64>,
738 },
739 MoveWindowToOutput {
741 window_id: u32,
742 output_id: u32,
743 reply: std::sync::mpsc::Sender<bool>,
744 },
745 ListWindows {
747 reply: std::sync::mpsc::Sender<Vec<WindowDesc>>,
748 },
749 SetCursorCallback(Option<Py<PyAny>>),
750 SetClipboardCallback(Py<PyAny>),
751 SetClipboard {
754 entries: Vec<(String, Vec<u8>)>,
755 },
756 KeyboardKey {
757 scancode: u32,
758 state: u32,
759 },
760 KeyboardKeys {
764 events: Vec<(u32, u32)>,
765 },
766 SetKeymapString(String),
770 SetXkbLayout {
774 rules: String,
775 model: String,
776 layout: String,
777 variant: String,
778 options: String,
779 reply: std::sync::mpsc::Sender<bool>,
780 },
781 BindKeysyms {
786 keysyms: Vec<u32>,
787 reply: std::sync::mpsc::Sender<Vec<(u32, u32)>>,
788 },
789 SetKeymapOverlay {
795 binds: Vec<(u32, u32)>,
796 },
797 GetKeyboardState {
800 reply: std::sync::mpsc::Sender<(Vec<u32>, u32)>,
801 },
802 GetXkbKeymap {
805 reply: std::sync::mpsc::Sender<String>,
806 },
807 Barrier {
812 reply: std::sync::mpsc::Sender<()>,
813 },
814 PointerMotion {
817 x: f64,
818 y: f64,
819 motion: u64,
820 },
821 PointerRelativeMotion {
822 dx: f64,
823 dy: f64,
824 motion: u64,
825 },
826 PointerButton {
830 btn: u32,
831 state: u32,
832 },
833 PointerAxis {
834 x: f64,
835 y: f64,
836 },
837 PointerScroll {
840 dx: f64,
841 dy: f64,
842 },
843 PointerScrollEnd,
846 UpdateCursorConfig {
847 render_on_framebuffer: bool,
848 },
849 SetCursorSize {
853 size: i32,
854 reply: std::sync::mpsc::Sender<bool>,
855 },
856 RequestIdr {
859 display_id: u32,
860 },
861 InvalidateReference {
864 display_id: u32,
865 frame_id: u16,
866 },
867 AcknowledgeReference {
870 display_id: u32,
871 frame_id: u16,
872 held: bool,
873 },
874 UpdateRate {
877 display_id: u32,
878 bitrate_kbps: Option<i32>,
879 vbv_multiplier: Option<f64>,
880 fps: Option<f64>,
881 },
882 UpdateTunables {
885 display_id: u32,
886 tunables: LiveTunables,
887 },
888 CuScreenshot {
891 display_id: u32,
892 resp: std::sync::mpsc::Sender<Result<Vec<u8>, String>>,
893 },
894 CuCursorPosition {
895 resp: std::sync::mpsc::Sender<(f64, f64)>,
896 },
897 CuGetInfo {
898 display_id: u32,
899 resp: std::sync::mpsc::Sender<(i32, i32, f64)>,
900 },
901}
902
903pub(crate) fn get_gpu_driver(card_index: i32) -> String {
922 if !std::path::Path::new(&format!("/dev/dri/renderD{}", 128 + card_index)).exists() {
923 return String::new();
924 }
925 let path = format!("/sys/class/drm/renderD{}/device/driver", 128 + card_index);
926 match std::fs::read_link(&path) {
927 Ok(link_path) => link_path.to_string_lossy().to_lowercase(),
928 Err(_) => String::new(),
929 }
930}
931
932pub(crate) fn driver_selects_nvenc(encode_driver: &str) -> bool {
937 encode_driver.is_empty() || encode_driver.contains("nvidia")
938}
939
940struct CardIdentity {
945 driver: String,
946 pci_vendor: Option<u32>,
947 compatibles: Vec<String>,
948}
949
950fn read_card_identity(device: &std::path::Path) -> CardIdentity {
957 let mut id = CardIdentity {
958 driver: String::new(),
959 pci_vendor: None,
960 compatibles: Vec::new(),
961 };
962 if let Ok(uevent) = std::fs::read_to_string(device.join("uevent")) {
963 for line in uevent.lines() {
964 if let Some(v) = line.strip_prefix("DRIVER=") {
965 id.driver = v.trim().to_lowercase();
966 } else if let Some(v) = line.strip_prefix("PCI_ID=") {
967 id.pci_vendor = v
968 .split(':')
969 .next()
970 .and_then(|h| u32::from_str_radix(h, 16).ok());
971 } else if line.starts_with("OF_COMPATIBLE_")
972 && !line.starts_with("OF_COMPATIBLE_N")
973 && let Some(v) = line.split_once('=').map(|x| x.1)
974 {
975 id.compatibles.push(v.trim().to_lowercase());
976 }
977 }
978 }
979 if id.pci_vendor.is_none() {
980 id.pci_vendor = std::fs::read_to_string(device.join("vendor"))
981 .ok()
982 .and_then(|v| u32::from_str_radix(v.trim().trim_start_matches("0x"), 16).ok());
983 }
984 if id.compatibles.is_empty()
985 && let Ok(modalias) = std::fs::read_to_string(device.join("modalias"))
986 {
987 let modalias = modalias.trim();
988 if let Some(rest) = modalias.strip_prefix("of:") {
989 id.compatibles
990 .extend(rest.split('C').skip(1).map(|c| c.to_lowercase()));
991 }
992 }
993 if id.driver.is_empty() {
994 id.driver = std::fs::read_link(device.join("driver"))
995 .map(|p| {
996 p.file_name()
997 .map(|n| n.to_string_lossy().to_lowercase())
998 .unwrap_or_default()
999 })
1000 .unwrap_or_default();
1001 }
1002 id
1003}
1004
1005const VENDOR_PCI_IDS: &[(&str, &[u32])] = &[
1009 ("nvidia", &[0x10de, 0x12d2]),
1010 ("amd", &[0x1002, 0x1022]),
1011 ("ati", &[0x1002]),
1012 ("intel", &[0x8086, 0x8087]),
1013 ("arm", &[0x13b5]),
1014 ("qualcomm", &[0x5143, 0x17cb]),
1015 ("broadcom", &[0x14e4]),
1016 ("apple", &[0x106b]),
1017 ("mediatek", &[0x14c3]),
1018 ("samsung", &[0x144d]),
1019 ("vmware", &[0x15ad]),
1020 ("microsoft", &[0x1414]),
1021 ("virtio", &[0x1af4]),
1022];
1023
1024const OF_PREFIX_ALIASES: &[(&str, &str)] = &[
1027 ("mali", "arm"),
1028 ("qualcomm", "qcom"),
1029 ("adreno", "qcom"),
1030 ("broadcom", "brcm"),
1031 ("videocore", "brcm"),
1032 ("imagination", "img"),
1033 ("powervr", "img"),
1034];
1035
1036fn card_matches_token(token: &str, id: &CardIdentity) -> bool {
1042 if !id.driver.is_empty() && token == id.driver {
1043 return true;
1044 }
1045 if let Some(vid) = id.pci_vendor {
1046 if u32::from_str_radix(token.trim_start_matches("0x"), 16) == Ok(vid) {
1047 return true;
1048 }
1049 if let Some((_, ids)) = VENDOR_PCI_IDS.iter().find(|(n, _)| *n == token)
1050 && ids.contains(&vid)
1051 {
1052 return true;
1053 }
1054 }
1055 if !id.compatibles.is_empty() {
1056 let prefix = OF_PREFIX_ALIASES
1057 .iter()
1058 .find(|(n, _)| *n == token)
1059 .map(|(_, p)| *p)
1060 .unwrap_or(token);
1061 let want = format!("{prefix},");
1062 if id.compatibles.iter().any(|c| c.starts_with(&want)) {
1063 return true;
1064 }
1065 }
1066 false
1067}
1068
1069fn parse_auto_gpu(value: &str) -> Option<Option<String>> {
1075 let value = value.to_lowercase();
1076 match value.as_str() {
1077 "" | "false" | "0" | "off" | "no" => None,
1078 "true" | "1" | "on" | "yes" => Some(None),
1079 token => Some(Some(token.to_string())),
1080 }
1081}
1082
1083fn auto_render_node(auto_gpu: &str) -> Option<String> {
1086 auto_select_render_node(parse_auto_gpu(auto_gpu)?.as_deref())
1087}
1088
1089fn render_node_index(path: &str) -> Option<i32> {
1091 Some(path.strip_prefix("/dev/dri/renderD")?.parse::<i32>().ok()? - 128)
1092}
1093
1094fn first_render_node_index() -> Option<i32> {
1097 render_node_index(&auto_select_render_node(None)?)
1098}
1099
1100fn auto_select_render_node(token: Option<&str>) -> Option<String> {
1117 let mut cards: Vec<(u32, std::path::PathBuf)> = std::fs::read_dir("/sys/class/drm")
1118 .into_iter()
1119 .flatten()
1120 .flatten()
1121 .filter_map(|e| {
1122 let num = e
1123 .file_name()
1124 .into_string()
1125 .ok()?
1126 .strip_prefix("card")?
1127 .parse::<u32>()
1128 .ok()?;
1129 Some((num, e.path()))
1130 })
1131 .collect();
1132 cards.sort_by_key(|(n, _)| *n);
1133 for (_, path) in &cards {
1134 if let Some(t) = token
1135 && !card_matches_token(t, &read_card_identity(&path.join("device")))
1136 {
1137 continue;
1138 }
1139 if let Ok(drm_entries) = std::fs::read_dir(path.join("device/drm")) {
1140 for de in drm_entries.flatten() {
1141 let name = de.file_name().into_string().unwrap_or_default();
1142 if name.starts_with("renderD") {
1143 let dev = format!("/dev/dri/{}", name);
1144 if std::path::Path::new(&dev).exists() {
1145 return Some(dev);
1146 }
1147 }
1148 }
1149 }
1150 }
1151 if token.is_some() {
1152 return None;
1153 }
1154 let mut nodes: Vec<String> = std::fs::read_dir("/dev/dri")
1155 .ok()?
1156 .flatten()
1157 .filter_map(|e| e.file_name().into_string().ok())
1158 .filter(|n| n.starts_with("renderD"))
1159 .collect();
1160 nodes.sort();
1161 nodes.first().map(|n| format!("/dev/dri/{}", n))
1162}
1163
1164pub struct WlFrame {
1168 id: usize,
1170 buf: Vec<u8>,
1171 frame_id: u16,
1172 damage: Vec<Rectangle<i32, Physical>>,
1173 is_animated: bool,
1174 captured_ns: i64,
1176}
1177
1178struct WlPoolInner {
1180 free: Vec<(usize, Vec<u8>)>,
1181 slot: Option<WlFrame>,
1182}
1183
1184pub struct WlFramePool {
1192 inner: Mutex<WlPoolInner>,
1193 cv: Condvar,
1194 stop: AtomicBool,
1195}
1196
1197impl WlFramePool {
1198 fn new(n: usize, buf_len: usize) -> Self {
1202 Self {
1203 inner: Mutex::new(WlPoolInner {
1204 free: (0..n).map(|i| (i, vec![0u8; buf_len])).collect(),
1205 slot: None,
1206 }),
1207 cv: Condvar::new(),
1208 stop: AtomicBool::new(false),
1209 }
1210 }
1211
1212 fn try_begin(&self) -> Option<(usize, Vec<u8>)> {
1217 let mut g = self.inner.lock().unwrap();
1218 if g.slot.is_some() {
1219 return None;
1220 }
1221 g.free.pop()
1222 }
1223
1224 fn publish(&self, frame: WlFrame) {
1226 let mut g = self.inner.lock().unwrap();
1227 debug_assert!(g.slot.is_none());
1228 g.slot = Some(frame);
1229 drop(g);
1230 self.cv.notify_all();
1231 }
1232
1233 fn cancel(&self, id: usize, buf: Vec<u8>) {
1235 self.inner.lock().unwrap().free.push((id, buf));
1236 }
1237
1238 fn take(&self) -> Option<WlFrame> {
1241 let mut g = self.inner.lock().unwrap();
1242 loop {
1243 if let Some(f) = g.slot.take() {
1244 return Some(f);
1245 }
1246 if self.stop.load(Ordering::Acquire) {
1247 return None;
1248 }
1249 let (gg, _) = self.cv.wait_timeout(g, WL_POOL_WAKE_QUANTUM).unwrap();
1250 g = gg;
1251 }
1252 }
1253
1254 fn recycle(&self, id: usize, buf: Vec<u8>) {
1257 self.inner.lock().unwrap().free.push((id, buf));
1258 }
1259
1260 fn shutdown(&self) {
1263 let g = self.inner.lock().unwrap();
1264 self.stop.store(true, Ordering::Release);
1265 drop(g);
1266 self.cv.notify_all();
1267 }
1268}
1269
1270pub struct WlEncodeControls {
1278 rate_dirty: AtomicBool,
1279 bitrate_kbps: AtomicI32,
1280 vbv_mult_milli: AtomicI32,
1281 fps_bits: AtomicU64,
1282 force_idr: AtomicBool,
1283 reference_reports: Mutex<Vec<encoders::reference::ReferenceReport>>,
1284 tunables_dirty: AtomicBool,
1287 tunables: Mutex<Option<LiveTunables>>,
1288}
1289
1290impl WlEncodeControls {
1291 fn new() -> Self {
1292 Self {
1293 rate_dirty: AtomicBool::new(false),
1294 bitrate_kbps: AtomicI32::new(0),
1295 vbv_mult_milli: AtomicI32::new(0),
1296 fps_bits: AtomicU64::new(0),
1297 force_idr: AtomicBool::new(false),
1298 reference_reports: Mutex::new(Vec::new()),
1299 tunables_dirty: AtomicBool::new(false),
1300 tunables: Mutex::new(None),
1301 }
1302 }
1303}
1304
1305const WL_POOL_SURFACES: usize = 2;
1308const WL_POOL_WAKE_QUANTUM: Duration = Duration::from_millis(20);
1310const WL_CONTENT_HOLD: Duration = Duration::from_millis(500);
1317
1318const MAX_CAPTURE_DIM: i32 = 16384;
1322const MAX_FPS: f64 = 1000.0;
1323const IDLE_FRAME_INTERVAL: Duration = Duration::from_millis(250);
1327const DEFAULT_FPS: f64 = 60.0;
1328
1329fn frame_period(fps: f64) -> Duration {
1333 let fps = if fps.is_finite() && fps > 0.0 {
1334 fps.min(MAX_FPS)
1335 } else {
1336 DEFAULT_FPS
1337 };
1338 Duration::from_secs_f64(1.0 / fps)
1339}
1340
1341fn capture_period(cap: &wayland::frontend::WlCapture) -> Duration {
1343 frame_period(cap.settings.target_fps)
1344}
1345const MAX_SCALE: f64 = 8.0;
1346pub(crate) const SCROLL_V120_PER_UNIT: f64 = 12.0;
1350
1351pub struct WlEncodeStats {
1355 frames: AtomicU32,
1356 stripes: AtomicU32,
1357 n_stripes: AtomicU32,
1358 desc: Mutex<String>,
1359}
1360
1361impl WlEncodeStats {
1362 fn new() -> Self {
1363 Self {
1364 frames: AtomicU32::new(0),
1365 stripes: AtomicU32::new(0),
1366 n_stripes: AtomicU32::new(1),
1367 desc: Mutex::new(String::new()),
1368 }
1369 }
1370}
1371
1372struct WlEncodeConfig {
1376 settings: RustCaptureSettings,
1377 display_id: u32,
1379 use_gpu: bool,
1381 try_gpu: bool,
1383 prior: Option<FrameEncoder>,
1386 predecessor: Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
1391 recording_sink: Option<std::sync::Weak<crate::recording_sink::RecordingSink>>,
1394 deliver_tx: std::sync::mpsc::SyncSender<Vec<EncodedStripe>>,
1395 controls: Arc<WlEncodeControls>,
1396 stats: Arc<WlEncodeStats>,
1397 report: Arc<report::StreamReport>,
1398}
1399
1400fn build_readback_encoders(
1407 settings: &mut RustCaptureSettings,
1408 try_gpu: bool,
1409 rgba: bool,
1410 prior: Option<FrameEncoder>,
1411) -> Option<FrameEncoder> {
1412 let mut attempt = settings.clone();
1413 if !try_gpu {
1414 attempt.use_cpu = true;
1415 }
1416 let encoder =
1417 encoders::select_frame_encoder(&mut attempt, FrameSource::Host { rgba }, prior, "Wayland");
1418 settings.codec = attempt.codec;
1419 encoder
1420}
1421
1422fn wayland_encode_loop(pool: &WlFramePool, cfg: WlEncodeConfig) -> Option<FrameEncoder> {
1452 crate::boost_thread_priority(-10);
1453 let _report = report::enter(&cfg.report);
1454 let mut settings = cfg.settings;
1455 let inherited = cfg.predecessor.and_then(|h| h.join().ok().flatten());
1456 let mut video_encoder = build_readback_encoders(
1457 &mut settings,
1458 cfg.try_gpu,
1459 cfg.use_gpu,
1460 cfg.prior.or(inherited),
1461 );
1462 if cfg.try_gpu && video_encoder.is_none() {
1463 println!(
1464 "[Wayland] Readback encode: no hardware encoder opened; encoding in software ({}).",
1465 encoders::software_library(settings.codec)
1466 );
1467 }
1468 let n_stripes = wayland_stripe_count(&settings, video_encoder.is_some());
1469 cfg.stats
1470 .n_stripes
1471 .store(n_stripes as u32, Ordering::Relaxed);
1472 *cfg.stats.desc.lock().unwrap() = encoder_desc(&settings, video_encoder.as_ref(), false);
1473 set_wayland_active_codec(cfg.display_id, Some(settings.codec));
1474 log_stream_settings("Wayland", &settings, n_stripes, video_encoder.as_ref());
1475
1476 let width = settings.width;
1477 let height = settings.height;
1478 let mut stripes: Vec<StripeState> = Vec::with_capacity(MAX_STRIPE_CAPACITY);
1479 let mut stripes_carrying: f32 = 1.0;
1481 let mut hw_state = StripeState::default();
1482 let mut hw_error_streak: u32 = 0;
1484 let mut hw_rebuilt = false;
1485
1486 while let Some(mut f) = pool.take() {
1487 if cfg.controls.tunables_dirty.swap(false, Ordering::Acquire)
1488 && let Some(t) = cfg.controls.tunables.lock().unwrap().take()
1489 {
1490 t.apply_to(&mut settings);
1491 }
1492 if cfg.controls.rate_dirty.swap(false, Ordering::Acquire) {
1493 settings.video_bitrate_kbps = cfg.controls.bitrate_kbps.load(Ordering::Relaxed);
1494 settings.video_vbv_multiplier =
1495 cfg.controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1496 let fps = f64::from_bits(cfg.controls.fps_bits.load(Ordering::Relaxed));
1497 if fps > 0.0 {
1498 settings.target_fps = fps;
1499 }
1500 if let Some(enc) = video_encoder.as_mut()
1501 && let Err(e) = enc.reconfigure_rate(&settings)
1502 {
1503 eprintln!("[Wayland] rate reconfigure failed: {e}");
1507 hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
1508 }
1509 }
1510
1511 let recording_sink = cfg.recording_sink.as_ref().and_then(|w| w.upgrade());
1515 for report in std::mem::take(&mut *cfg.controls.reference_reports.lock().unwrap()) {
1516 let taken = match (video_encoder.as_mut(), report) {
1517 (Some(encoder), report) => encoder.take_report(report),
1518 (None, encoders::reference::ReferenceReport::Lost(frame_id)) => {
1519 encoders::software::invalidate_reference(&mut stripes, frame_id)
1520 }
1521 (None, encoders::reference::ReferenceReport::Held(frame_id)) => {
1522 encoders::software::acknowledge_reference(&mut stripes, frame_id, true);
1523 true
1524 }
1525 (None, encoders::reference::ReferenceReport::Sent(frame_id)) => {
1526 encoders::software::acknowledge_reference(&mut stripes, frame_id, false);
1527 true
1528 }
1529 };
1530 if !taken {
1531 cfg.controls.force_idr.store(true, Ordering::Relaxed);
1532 }
1533 }
1534 let requested_idr = cfg.controls.force_idr.swap(false, Ordering::Relaxed)
1535 || recording_sink
1536 .as_ref()
1537 .is_some_and(|s| s.should_force_idr());
1538
1539 let mut out: Vec<EncodedStripe> = Vec::new();
1540 if let Some(ref mut encoder) = video_encoder {
1541 let decision = crate::pipeline::decide_hw_fullframe(
1542 &mut hw_state,
1543 &settings,
1544 f.frame_id,
1545 crate::pipeline::Damage::of_rects(&f.damage, width, height),
1546 f.is_animated,
1547 requested_idr,
1548 crate::pipeline::EncoderQuality::of(encoder),
1549 );
1550 if decision.send || encoder.holds_frame() {
1551 let w = width as u32;
1552 let force_idr = decision.force_idr;
1553 decision.prepare(encoder);
1554 let encode_start_ns = wayland::host::now_ns();
1559 let outcome = if decision.send {
1560 encoder.encode_host(
1561 &f.buf,
1562 (w * 4) as usize,
1563 cfg.use_gpu,
1564 f.frame_id as u64,
1565 decision.target_qp,
1566 force_idr,
1567 )
1568 } else {
1569 encoder.push_held(f.frame_id as u64)
1570 };
1571 match outcome {
1572 Ok(data) => {
1573 hw_error_streak = 0;
1574 hw_rebuilt = false;
1575 if !data.is_empty() {
1576 let encode_end_ns = wayland::host::now_ns();
1577 for (data, id, reference) in encoder.delivered_units(data, f.frame_id) {
1578 out.push(EncodedStripe {
1579 data: Arc::new(data),
1580 codec: settings.codec,
1581 stripe_y_start: 0,
1582 stripe_height: height,
1583 frame_id: id as i32,
1584 timing: FrameTiming {
1585 capture_ns: f.captured_ns,
1586 encode_start_ns,
1587 encode_end_ns,
1588 },
1589 reference,
1590 });
1591 }
1592 }
1593 }
1594 Err(e) => {
1595 if hw_error_streak.is_multiple_of(HW_ERROR_RECOVERY_THRESHOLD) {
1598 eprintln!("[Wayland] HW encode error: {e}");
1599 }
1600 hw_error_streak = hw_error_streak.saturating_add(1);
1601 if hw_error_streak >= HW_ERROR_RECOVERY_THRESHOLD {
1602 hw_error_streak = 0;
1608 let try_gpu = !hw_rebuilt;
1609 if try_gpu {
1610 eprintln!(
1611 "[Wayland] rebuilding readback HW encoder after repeated encode errors."
1612 );
1613 } else {
1614 eprintln!(
1615 "[Wayland] readback HW encoder unrecoverable; demoting to software encoding ({}).",
1616 encoders::software_library(settings.codec)
1617 );
1618 }
1619 drop(video_encoder.take());
1623 video_encoder =
1624 build_readback_encoders(&mut settings, try_gpu, cfg.use_gpu, None);
1625 if !try_gpu {
1626 report::encoder_reason(
1627 "the hardware encoder failed repeatedly and was given up",
1628 );
1629 }
1630 hw_rebuilt = try_gpu && video_encoder.is_some();
1631 cfg.controls.force_idr.store(true, Ordering::Relaxed);
1632 let n = wayland_stripe_count(&settings, video_encoder.is_some());
1633 cfg.stats.n_stripes.store(n as u32, Ordering::Relaxed);
1634 *cfg.stats.desc.lock().unwrap() =
1635 encoder_desc(&settings, video_encoder.as_ref(), false);
1636 set_wayland_active_codec(cfg.display_id, Some(settings.codec));
1637 log_stream_settings("Wayland", &settings, n, video_encoder.as_ref());
1638 }
1639 }
1640 }
1641 }
1642 } else {
1643 let mut damage = std::mem::take(&mut f.damage);
1644 if f.is_animated {
1645 damage.push(Rectangle::new((0, 0).into(), (width, height).into()));
1646 }
1647 let force_idr_all = requested_idr
1648 || (settings.codec.is_video()
1649 && crate::pipeline::periodic_idr_due(&settings, f.frame_id));
1650 let encode_start_ns = wayland::host::now_ns();
1651 out = encoders::software::encode_cpu(
1652 &mut stripes,
1653 &mut stripes_carrying,
1654 &f.buf,
1655 width,
1656 height,
1657 &damage,
1658 &settings,
1659 f.frame_id,
1660 cfg.use_gpu,
1661 false,
1662 force_idr_all,
1663 );
1664 FrameTiming::stamp(&mut out, f.captured_ns, encode_start_ns);
1665 }
1666
1667 let WlFrame { id, buf, .. } = f;
1668 pool.recycle(id, buf);
1669 if requested_idr && out.is_empty() {
1672 cfg.controls.force_idr.store(true, Ordering::Relaxed);
1673 }
1674 if !out.is_empty() {
1675 cfg.stats.frames.fetch_add(1, Ordering::Relaxed);
1676 cfg.stats
1677 .stripes
1678 .fetch_add(out.len() as u32, Ordering::Relaxed);
1679 if let Some(ref socket) = recording_sink {
1680 socket.write_frame(&out, settings.width, settings.height);
1681 }
1682 crate::recorder::wayland_tap(cfg.display_id, &out);
1683 let _ = cfg.deliver_tx.send(out);
1684 }
1685 }
1686 crate::log::debug!(
1687 "[Wayland] Encode thread exiting (hw={}, stripes={}).",
1688 video_encoder.is_some(),
1689 stripes.len()
1690 );
1691 video_encoder
1692}
1693
1694fn encoder_desc(
1697 settings: &RustCaptureSettings,
1698 video_encoder: Option<&FrameEncoder>,
1699 zero_copy: bool,
1700) -> String {
1701 if !settings.codec.is_video() {
1702 return format!("JPEG Q:{}", settings.jpeg_quality);
1703 }
1704 let copy_mode = if zero_copy { "ZeroCopy" } else { "Readback" };
1705 let backend = match video_encoder {
1706 Some(enc) if enc.is_hardware() => format!("{} ({})", enc.backend_name(), copy_mode),
1707 Some(enc) => format!("CPU {}", enc.backend_name()),
1708 None => format!("CPU {}", encoders::software_library(Codec::H264)),
1709 };
1710 let is_444 = encoders::session_fullcolor(video_encoder, settings);
1711 let cs_str = if is_444 { "CS_IN:I444" } else { "CS_IN:I420" };
1712 let range_str = if encoders::session_full_range(video_encoder, settings) {
1713 "FR"
1714 } else {
1715 "LR"
1716 };
1717 let frame_str = if video_encoder.is_some() || settings.video_fullframe {
1718 "FF"
1719 } else {
1720 "Striped"
1721 };
1722 format!(
1723 "{} ({}) {} {} {} {}",
1724 settings.codec.display(),
1725 backend,
1726 cs_str,
1727 range_str,
1728 frame_str,
1729 encoders::rate_desc(
1730 settings,
1731 video_encoder.and_then(FrameEncoder::fixed_rate_control)
1732 )
1733 )
1734}
1735
1736fn wayland_stripe_count(settings: &RustCaptureSettings, fullframe_encoder: bool) -> usize {
1742 crate::encoders::software::stripe_count(
1743 settings.height,
1744 settings.codec,
1745 fullframe_encoder || settings.video_fullframe,
1746 )
1747}
1748
1749pub(crate) fn log_stream_settings(
1753 tag: &str,
1754 settings: &RustCaptureSettings,
1755 n_stripes: usize,
1756 video_encoder: Option<&FrameEncoder>,
1757) {
1758 let backend = video_encoder.map(|enc| (enc.backend_name(), enc.is_hardware()));
1759 let fixed_rate = video_encoder.and_then(FrameEncoder::fixed_rate_control);
1760 let holds = video_encoder.map_or_else(
1761 || encoders::software::stripes_hold_quantizer(settings),
1762 FrameEncoder::holds_quantizer,
1763 );
1764 let fullcolor = encoders::session_fullcolor(video_encoder, settings);
1765 let full_range = encoders::session_full_range(video_encoder, settings);
1766 log_stream_settings_of(
1767 tag, settings, n_stripes, backend, fixed_rate, holds, fullcolor, full_range,
1768 );
1769 report::bit_depth(encoders::session_bit_depth(video_encoder, settings));
1770}
1771
1772#[allow(clippy::too_many_arguments)]
1778pub(crate) fn log_stream_settings_of(
1779 tag: &str,
1780 settings: &RustCaptureSettings,
1781 n_stripes: usize,
1782 backend: Option<(&str, bool)>,
1783 fixed_rate: Option<&str>,
1784 holds: bool,
1785 fullcolor: bool,
1786 full_range: bool,
1787) {
1788 report::stream(settings, n_stripes, backend, fullcolor, full_range);
1789 println!(
1790 "{}",
1791 stream_settings_line(
1792 tag, settings, n_stripes, backend, fixed_rate, holds, fullcolor, full_range
1793 )
1794 );
1795}
1796
1797#[allow(clippy::too_many_arguments)]
1800fn stream_settings_line(
1801 tag: &str,
1802 settings: &RustCaptureSettings,
1803 n_stripes: usize,
1804 backend: Option<(&str, bool)>,
1805 fixed_rate: Option<&str>,
1806 holds: bool,
1807 fullcolor: bool,
1808 full_range: bool,
1809) -> String {
1810 let mut log_msg = format!(
1811 "[{tag}] Stream settings active -> Res: {}x{} | FPS: {:.1} | Stripes: {}",
1812 settings.width, settings.height, settings.target_fps, n_stripes
1813 );
1814
1815 if !settings.codec.is_video() {
1816 log_msg.push_str(&format!(
1817 " | Mode: JPEG | Quality: {}",
1818 settings.jpeg_quality
1819 ));
1820 if let Some(paint_over) = encoders::paint_over_desc(settings, fixed_rate, holds) {
1821 log_msg.push_str(&format!(" | {paint_over}"));
1822 }
1823 } else {
1824 let encoder_type = match backend {
1825 Some((name, _)) => name,
1826 None => encoders::software_library(Codec::H264),
1827 };
1828 log_msg.push_str(&format!(
1829 " | Mode: {} ({})",
1830 settings.codec.display(),
1831 encoder_type
1832 ));
1833
1834 if backend.is_some() || settings.video_fullframe {
1835 log_msg.push_str(" FullFrame");
1836 } else {
1837 log_msg.push_str(" Striped");
1838 }
1839
1840 if settings.video_streaming_mode {
1841 log_msg.push_str(" Streaming");
1842 }
1843
1844 log_msg.push_str(&format!(" | {}", encoders::rate_desc(settings, fixed_rate)));
1845 if let Some(paint_over) = encoders::paint_over_desc(settings, fixed_rate, holds) {
1846 log_msg.push_str(&format!(" | {paint_over}"));
1847 }
1848
1849 log_msg.push_str(&format!(
1850 " | Colorspace: {}",
1851 encoders::colorspace_desc(fullcolor, full_range)
1852 ));
1853 }
1854
1855 log_msg.push_str(&format!(
1856 " | Damage Thresh: {}f | Damage Dur: {}f",
1857 settings.damage_block_threshold, settings.damage_block_duration
1858 ));
1859 log_msg
1860}
1861
1862#[cfg(test)]
1863mod stream_settings_line_tests {
1864 use super::*;
1865
1866 #[test]
1870 fn the_line_states_what_the_session_applies() {
1871 let crf = RustCaptureSettings {
1872 codec: Codec::H264,
1873 video_crf: 25,
1874 video_bitrate_kbps: 8000,
1875 use_paint_over_quality: false,
1876 ..Default::default()
1877 };
1878 let line = |s: &RustCaptureSettings, fixed: Option<&str>| {
1879 stream_settings_line(
1880 "X11",
1881 s,
1882 1,
1883 Some(("NVENC", true)),
1884 fixed,
1885 fixed.is_none(),
1886 false,
1887 false,
1888 )
1889 };
1890 let nvenc = line(&crf, None);
1891 assert!(
1892 nvenc.contains("| CRF: 25 |") && !nvenc.contains("VBV") && !nvenc.contains("8000"),
1893 "{nvenc}"
1894 );
1895 let v4l2 = line(&crf, Some("VBR"));
1896 assert!(
1897 v4l2.contains("| VBR 8000 |") && !v4l2.contains("CRF"),
1898 "{v4l2}"
1899 );
1900 assert!(line(&crf, Some("CBR")).contains("| CBR 8000 |"));
1901 assert!(
1902 line(
1903 &RustCaptureSettings {
1904 video_cbr_mode: true,
1905 ..crf.clone()
1906 },
1907 None
1908 )
1909 .contains("| CBR 8000 |")
1910 );
1911 let jpeg = RustCaptureSettings {
1912 codec: Codec::Jpeg,
1913 jpeg_quality: 75,
1914 paint_over_jpeg_quality: 95,
1915 ..Default::default()
1916 };
1917 assert!(line(&jpeg, None).contains("| PaintOver Q: 95 "));
1918 assert!(
1919 !line(
1920 &RustCaptureSettings {
1921 paint_over_jpeg_quality: 60,
1922 ..jpeg
1923 },
1924 None
1925 )
1926 .contains("PaintOver")
1927 );
1928 }
1929}
1930
1931fn teardown_capture(
1940 cap: &mut wayland::frontend::WlCapture,
1941) -> (
1942 Option<std::thread::JoinHandle<()>>,
1943 Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
1944) {
1945 if let Some(p) = cap.encode_pool.take() {
1946 p.shutdown();
1947 }
1948 if let Some(flag) = cap.deliver_discard.take() {
1949 flag.store(true, Ordering::Relaxed);
1950 }
1951 let encode_join = cap.encode_join.take();
1952 if let Some(tx) = cap.deliver_tx.take() {
1953 drop(tx);
1954 }
1955 let join = cap.deliver_join.take();
1956 cap.pending_hw_delivery = None;
1957 cap.pending_hw_damage = false;
1958 cap.recording_sink = None;
1962 (join, encode_join)
1963}
1964
1965fn reap_dead_host(state: &mut AppState) {
1970 if !state.host.as_ref().is_some_and(|h| !h.alive()) {
1971 return;
1972 }
1973 eprintln!("[HostCapture] host compositor connection lost; captures stop until restarted.");
1974 let ids: Vec<u32> = state
1975 .output_nodes
1976 .iter()
1977 .filter(|n| n.capture.is_some())
1978 .map(|n| n.id)
1979 .collect();
1980 for id in ids {
1981 stop_capture_on_display(state, id);
1982 set_wayland_capture_err(
1986 id,
1987 Some("host compositor connection lost; capture stopped".to_string()),
1988 );
1989 }
1990 state.host = None;
1991 state.host_mode_refusals.clear();
1992 let pending: Vec<_> = state.host_layout_pending.drain().collect();
1995 for (id, p) in pending {
1996 answer_geometry_waiters(state, id, p.geometry_waiters);
1997 }
1998}
1999
2000fn stop_capture_on_display(state: &mut AppState, display_id: u32) {
2003 let Some(idx) = state.node_idx_for_id(display_id) else {
2004 return;
2005 };
2006 if let Some(mut cap) = state.output_nodes[idx].capture.take() {
2007 crate::log::debug!("[Wayland] Capture loop stopped (display {display_id}).");
2008 cap.video_encoder = None;
2009 let (join, encode_join) = teardown_capture(&mut cap);
2010 state.deliver_reaper.extend(join);
2011 state.encode_reaper.extend(encode_join);
2012 }
2013 wayland_alive().lock().unwrap().remove(&display_id);
2014 set_wayland_capture_err(display_id, None);
2015 set_wayland_active_codec(display_id, None);
2016 report::wayland_reports()
2017 .lock()
2018 .unwrap()
2019 .remove(&display_id);
2020 if let Some(p) = state.host_layout_pending.remove(&display_id) {
2021 answer_geometry_waiters(state, display_id, p.geometry_waiters);
2022 }
2023}
2024
2025fn realized_geometry(state: &AppState, display_id: u32) -> (i32, i32, f64) {
2028 state
2029 .node_idx_for_id(display_id)
2030 .map(|idx| {
2031 let node = &state.output_nodes[idx];
2032 match node.capture.as_ref() {
2033 Some(c) => (c.settings.width, c.settings.height, c.settings.scale),
2034 None => node
2035 .output
2036 .current_mode()
2037 .map(|m| {
2038 (
2039 m.size.w,
2040 m.size.h,
2041 node.output.current_scale().fractional_scale(),
2042 )
2043 })
2044 .unwrap_or((0, 0, 0.0)),
2045 }
2046 })
2047 .unwrap_or((0, 0, 0.0))
2048}
2049
2050fn answer_geometry_waiters(
2053 state: &AppState,
2054 display_id: u32,
2055 waiters: Vec<std::sync::mpsc::Sender<(i32, i32, f64)>>,
2056) {
2057 if waiters.is_empty() {
2058 return;
2059 }
2060 let info = realized_geometry(state, display_id);
2061 for w in waiters {
2062 let _ = w.send(info);
2063 }
2064}
2065
2066fn host_layout_resolution(want: (i32, i32), current: Option<(i32, i32)>) -> Option<(i32, i32)> {
2074 current.filter(|&c| c != want)
2075}
2076
2077const HOST_QUIET_PERIODS: f64 = 1.5;
2082
2083const HOST_MODE_REFUSALS_BEFORE_HINT: u32 = 3;
2087
2088fn reconcile_host_layouts(state: &mut AppState) {
2097 if state.host_layout_pending.is_empty() {
2098 return;
2099 }
2100 let ids: Vec<u32> = state.host_layout_pending.keys().copied().collect();
2101 for id in ids {
2102 let Some(pending) = state.host_layout_pending.get(&id) else {
2104 continue;
2105 };
2106 let host_mode = match state.host.as_ref() {
2107 Some(host) => match host.layout_outcome(pending.epoch) {
2108 None => continue,
2111 Some(_) => host_layout_resolution(pending.want, host.current_output_size(id)),
2112 },
2113 None => None,
2114 };
2115 let Some(pending) = state.host_layout_pending.remove(&id) else {
2116 continue;
2117 };
2118 if host_mode.is_none() {
2119 state.host_mode_refusals.remove(&id);
2120 }
2121 if let Some((rw, rh)) = host_mode {
2122 let (w, h) = pending.want;
2123 let restart = state
2124 .node_idx_for_id(id)
2125 .and_then(|idx| state.output_nodes[idx].capture.as_ref())
2126 .map(|cap| (cap.callback.clone(), cap.settings.clone()));
2127 if let Some((cb, mut settings)) = restart {
2128 settings.width = rw;
2129 settings.height = rh;
2130 let followed = if settings.codec.is_video() {
2133 (rw & !1, rh & !1)
2134 } else {
2135 (rw, rh)
2136 };
2137 if followed != (w, h) {
2138 eprintln!(
2139 "[HostCapture] host runs {rw}x{rh} for display {id} ({w}x{h} not applied); capturing at that size."
2140 );
2141 let refusals = {
2142 let n = state.host_mode_refusals.entry(id).or_insert(0);
2143 *n += 1;
2144 *n
2145 };
2146 if refusals == HOST_MODE_REFUSALS_BEFORE_HINT {
2147 eprintln!(
2148 "[HostCapture] display {id}: the host keeps its own mode; a manual resolution of {rw}x{rh} stops the capture restarting on every resize."
2149 );
2150 }
2151 start_capture_on_display(state, id, cb, settings);
2152 let mismatch = if refusals >= HOST_MODE_REFUSALS_BEFORE_HINT {
2153 format!(
2154 "host runs {rw}x{rh} ({w}x{h} not applied; set a manual resolution of {rw}x{rh})"
2155 )
2156 } else {
2157 format!("host runs {rw}x{rh} ({w}x{h} not applied)")
2158 };
2159 let own = wayland_capture_err().lock().unwrap().get(&id).cloned();
2160 set_wayland_capture_err(
2161 id,
2162 Some(match own {
2163 Some(e) => format!("{mismatch}; {e}"),
2164 None => mismatch,
2165 }),
2166 );
2167 }
2168 }
2169 }
2170 answer_geometry_waiters(state, id, pending.geometry_waiters);
2173 }
2174}
2175
2176fn bootstrap_readback_pool(
2186 cap: &mut wayland::frontend::WlCapture,
2187 display_id: u32,
2188 use_gpu: bool,
2189 try_gpu: bool,
2190 prior: Option<FrameEncoder>,
2191 predecessor: Option<std::thread::JoinHandle<Option<FrameEncoder>>>,
2192) {
2193 let Some(deliver_tx) = cap.deliver_tx.clone() else {
2194 return;
2195 };
2196 let settings = cap.settings.clone();
2197 let pool = Arc::new(WlFramePool::new(
2198 WL_POOL_SURFACES,
2199 (settings.width.max(0) as usize) * (settings.height.max(0) as usize) * 4,
2200 ));
2201 cap.pool_last_render = vec![0; WL_POOL_SURFACES];
2202 cap.render_seq = 0;
2203 cap.pool_content_gen = vec![u64::MAX; WL_POOL_SURFACES];
2204 cap.content_gen = 0;
2205 let c = &cap.encode_controls;
2206 c.bitrate_kbps
2207 .store(settings.video_bitrate_kbps, Ordering::Relaxed);
2208 c.vbv_mult_milli.store(
2209 (settings.video_vbv_multiplier * 1000.0).round() as i32,
2210 Ordering::Relaxed,
2211 );
2212 c.fps_bits
2213 .store(settings.target_fps.max(1.0).to_bits(), Ordering::Relaxed);
2214 let cfg = WlEncodeConfig {
2215 settings: settings.clone(),
2216 display_id,
2217 use_gpu,
2218 try_gpu,
2219 prior,
2220 predecessor,
2221 recording_sink: cap.recording_sink.as_ref().map(Arc::downgrade),
2222 deliver_tx,
2223 controls: cap.encode_controls.clone(),
2224 stats: cap.encode_stats.clone(),
2225 report: cap.report.clone(),
2226 };
2227 let pool2 = pool.clone();
2228 cap.encode_join = Some(
2229 thread::Builder::new()
2230 .name(format!("wl-encode-{display_id}"))
2231 .spawn(move || wayland_encode_loop(&pool2, cfg))
2232 .expect("failed to spawn wl-encode thread"),
2233 );
2234 cap.encode_pool = Some(pool);
2235}
2236
2237fn rebuild_zerocopy_encoder(
2241 cap: &wayland::frontend::WlCapture,
2242 state: &mut AppState,
2243) -> Option<FrameEncoder> {
2244 let egl_display = state
2245 .gles_renderer
2246 .as_ref()
2247 .map(|r| r.egl_context().display().get_display_handle().handle)
2248 .unwrap_or(std::ptr::null());
2249 let mut settings = cap.settings.clone();
2250 encoders::select_frame_encoder(
2251 &mut settings,
2252 FrameSource::Dmabuf { egl_display },
2253 None,
2254 "Wayland",
2255 )
2256}
2257
2258pub(crate) const HW_ERROR_RECOVERY_THRESHOLD: u32 = 30;
2262
2263fn start_capture_on_display(
2264 state: &mut AppState,
2265 display_id: u32,
2266 cb: Option<Arc<Py<PyAny>>>,
2267 mut settings: RustCaptureSettings,
2268) {
2269 let _ = state
2272 .cursor_tx
2273 .send(CursorJob::SetSizeCap(settings.cursor_size_cap));
2274
2275 set_wayland_capture_err(display_id, None);
2277
2278 let Some(node_idx) = state.node_idx_for_id(display_id) else {
2279 eprintln!("[Wayland] StartCapture: no output with display id {display_id}.");
2280 set_wayland_capture_err(
2281 display_id,
2282 Some(format!("no output with display id {display_id}")),
2283 );
2284 return;
2285 };
2286 let stream_report = report::StreamReport::new("wayland");
2287 report::wayland_reports()
2288 .lock()
2289 .unwrap()
2290 .insert(display_id, stream_report.clone());
2291 let _report = report::enter(&stream_report);
2292 let mut node = state.output_nodes.remove(node_idx);
2293 let mut geometry_waiters = state
2296 .host_layout_pending
2297 .remove(&display_id)
2298 .map(|p| p.geometry_waiters)
2299 .unwrap_or_default();
2300
2301 if settings.encode_node_index < -1 {
2302 let picked = if state.auto_gpu_selected {
2303 render_node_index(&state.render_node_path)
2304 } else {
2305 first_render_node_index()
2306 };
2307 if let Some(idx) = picked {
2308 settings.encode_node_index = idx;
2309 }
2310 }
2311
2312 if settings.codec.is_video() {
2313 settings.width &= !1;
2314 settings.height &= !1;
2315 }
2316
2317 let mut prior_zero_copy: Option<FrameEncoder> = None;
2320 let mut prior_encode_join: Option<std::thread::JoinHandle<Option<FrameEncoder>>> = None;
2321 let mut prior_deliver_join: Option<std::thread::JoinHandle<()>> = None;
2322 if let Some(mut old) = node.capture.take() {
2323 prior_zero_copy = old.video_encoder.take();
2324 (prior_deliver_join, prior_encode_join) = teardown_capture(&mut old);
2325 }
2326
2327 let recording_sink = crate::recording_sink::RecordingSink::try_bind(
2331 &settings.recording_socket,
2332 settings.target_fps,
2333 );
2334
2335 let host_capture = !settings.wayland_host_display.is_empty();
2341 reap_dead_host(state);
2342 if host_capture && state.host.is_none() {
2343 let gbm_path = if state.use_gpu {
2347 state.gbm_device.as_ref().and_then(|dev| {
2348 use std::os::fd::{AsFd as _, AsRawFd as _};
2349 let fd = dev.as_fd().as_raw_fd();
2350 std::fs::read_link(format!("/proc/self/fd/{fd}")).ok()
2351 })
2352 } else {
2353 None
2354 };
2355 let dma_formats: Vec<(u32, Vec<u64>)> = match (&state.gles_renderer, state.use_gpu) {
2358 (Some(renderer), true) => {
2359 let formats = renderer.egl_context().display().dmabuf_texture_formats();
2360 [Fourcc::Xrgb8888, Fourcc::Argb8888]
2361 .into_iter()
2362 .map(|code| {
2363 (
2364 code as u32,
2365 formats
2366 .iter()
2367 .filter(|f| f.code == code)
2368 .map(|f| u64::from(f.modifier))
2369 .collect(),
2370 )
2371 })
2372 .collect()
2373 }
2374 _ => Vec::new(),
2375 };
2376 match crate::wayland::host::HostSession::connect(
2377 &settings.wayland_host_display,
2378 gbm_path,
2379 state.host_frame_tx.clone(),
2380 dma_formats,
2381 state.cursor_tx.clone(),
2382 ) {
2383 Ok(h) => {
2384 println!(
2385 "[HostCapture] capturing host compositor '{}' ({} outputs).",
2386 settings.wayland_host_display,
2387 h.output_count()
2388 );
2389 state.host = Some(h);
2390 }
2391 Err(e) => {
2392 eprintln!(
2393 "[HostCapture] connect '{}' failed: {e}",
2394 settings.wayland_host_display
2395 );
2396 set_wayland_capture_err(
2397 display_id,
2398 Some(format!(
2399 "host compositor '{}' connect failed ({e}); capturing locally",
2400 settings.wayland_host_display
2401 )),
2402 );
2403 }
2404 }
2405 }
2406 let host_capture = host_capture && state.host.is_some();
2409 if host_capture && let Some(host) = &state.host {
2410 host.set_layout(display_id, node.pos.0, node.pos.1);
2413 }
2414
2415 if node.owner.is_none() {
2420 let target_refresh = (settings.target_fps * 1000.0).round() as i32;
2424 let (current_w, current_h, current_refresh) = match node.output.current_mode() {
2425 Some(m) => (m.size.w, m.size.h, m.refresh),
2426 None => (settings.width, settings.height, target_refresh),
2427 };
2428 let current_scale = node.output.current_scale().fractional_scale();
2429
2430 if current_w != settings.width
2431 || current_h != settings.height
2432 || (current_scale - settings.scale).abs() > 0.001
2433 || current_refresh != target_refresh
2434 {
2435 let mut new_offscreen = None;
2441 let mut gbm_resize_failed = false;
2442 if state.use_gpu
2443 && let Some(gbm) = state.gbm_device.as_mut()
2444 {
2445 match alloc_render_target(
2446 gbm,
2447 settings.width as u32,
2448 settings.height as u32,
2449 GbmFormat::Argb8888,
2450 ) {
2451 Ok(bo) => {
2452 let dmabuf = create_dmabuf_from_bo(&bo);
2453 new_offscreen = Some((bo, dmabuf));
2454 }
2455 Err(e) => {
2456 eprintln!(
2457 "[Wayland] GBM buffer resize to {}x{} failed ({:?}); keeping previous output mode.",
2458 settings.width, settings.height, e
2459 );
2460 gbm_resize_failed = true;
2461 }
2462 }
2463 }
2464 if gbm_resize_failed {
2465 set_wayland_capture_err(
2469 display_id,
2470 Some(format!(
2471 "GPU buffer resize to {}x{} refused; kept {current_w}x{current_h}",
2472 settings.width, settings.height
2473 )),
2474 );
2475 settings.width = current_w;
2476 settings.height = current_h;
2477 settings.scale = current_scale;
2478 settings.target_fps = current_refresh as f64 / 1000.0;
2479 } else {
2480 println!(
2481 "[Wayland] Configuring Output {} ({}): {}x{} @ {:.2} FPS (Scale {:.2})",
2482 display_id,
2483 node.output.name(),
2484 settings.width,
2485 settings.height,
2486 settings.target_fps,
2487 settings.scale
2488 );
2489 let new_mode = OutputMode {
2490 size: (settings.width, settings.height).into(),
2491 refresh: target_refresh,
2492 };
2493 node.output.change_current_state(
2494 Some(new_mode),
2495 Some(Transform::Normal),
2496 Some(OutputScale::Fractional(settings.scale)),
2497 Some(Point::from(node.pos)),
2498 );
2499 node.output.set_preferred(new_mode);
2500 for cs in state
2503 .copy_sessions
2504 .iter()
2505 .filter(|cs| cs.output.upgrade().as_ref() == Some(&node.output))
2506 {
2507 if let Some(c) = wayland::frontend::output_capture_constraints(
2508 &node.output,
2509 state.gles_renderer.as_ref(),
2510 &state.render_node_path,
2511 ) {
2512 cs.session.update_constraints(c);
2513 }
2514 }
2515
2516 let pixel_count =
2517 (settings.width.max(0) as usize) * (settings.height.max(0) as usize);
2518 node.frame_buffer = vec![0u8; pixel_count * 4];
2519 node.target_seeded = false;
2520
2521 if let Some(off) = new_offscreen.take() {
2522 node.offscreen_buffer = Some(off);
2523 }
2524 }
2525 }
2526
2527 let out = node.output.clone();
2528 configure_windows_for_mode(
2529 state,
2530 display_id,
2531 &out,
2532 settings.width,
2533 settings.height,
2534 settings.scale,
2535 );
2536 } else if state.use_gpu {
2537 let have = node
2544 .offscreen_buffer
2545 .as_ref()
2546 .map(|(bo, _)| (bo.width() as i32, bo.height() as i32));
2547 if have != Some((settings.width, settings.height))
2548 && let Some(gbm) = state.gbm_device.as_mut()
2549 {
2550 match alloc_render_target(
2551 gbm,
2552 settings.width as u32,
2553 settings.height as u32,
2554 GbmFormat::Argb8888,
2555 ) {
2556 Ok(bo) => {
2557 let dmabuf = create_dmabuf_from_bo(&bo);
2558 node.offscreen_buffer = Some((bo, dmabuf));
2559 node.damage_tracker = OutputDamageTracker::new(
2560 (settings.width, settings.height),
2561 settings.scale,
2562 Transform::Normal,
2563 );
2564 node.content_tracker = None;
2565 node.view_size = (settings.width, settings.height);
2566 node.view_scale = settings.scale;
2567 node.frame_buffer = vec![
2568 0u8;
2569 (settings.width.max(0) as usize)
2570 * (settings.height.max(0) as usize)
2571 * 4
2572 ];
2573 node.target_seeded = false;
2574 crate::log::debug!(
2575 "[Wayland] View {display_id} render target resized to {}x{}.",
2576 settings.width,
2577 settings.height
2578 );
2579 }
2580 Err(e) => {
2581 eprintln!(
2582 "[Wayland] GBM view buffer resize to {}x{} failed ({:?}); keeping previous target.",
2583 settings.width, settings.height, e
2584 );
2585 set_wayland_capture_err(
2586 display_id,
2587 Some(format!(
2588 "GPU view buffer resize to {}x{} refused",
2589 settings.width, settings.height
2590 )),
2591 );
2592 }
2593 }
2594 }
2595 }
2596
2597 let use_cpu_explicit = settings.use_cpu || settings.encode_node_index == -1;
2598 let gpu_intent = settings.codec.is_video() && !use_cpu_explicit;
2599
2600 let mut different_gpu = false;
2601 if gpu_intent {
2602 let encode_node_idx = settings.encode_node_index.max(0);
2603 if !state.render_node_path.is_empty()
2604 && !state
2605 .render_node_path
2606 .contains(&format!("renderD{}", 128 + encode_node_idx))
2607 {
2608 different_gpu = true;
2609 }
2610 }
2611
2612 let mut video_encoder: Option<FrameEncoder> = None;
2613 if gpu_intent && state.use_gpu && !different_gpu {
2614 let egl_display = state
2615 .gles_renderer
2616 .as_ref()
2617 .map(|r| r.egl_context().display().get_display_handle().handle)
2618 .unwrap_or(std::ptr::null());
2619 video_encoder = encoders::select_frame_encoder(
2620 &mut settings,
2621 FrameSource::Dmabuf { egl_display },
2622 prior_zero_copy.take(),
2623 "Wayland",
2624 );
2625 if video_encoder.is_none() {
2626 set_wayland_capture_err(
2627 display_id,
2628 Some(format!(
2629 "no zero-copy {} encoder on the render node; using readback encode",
2630 settings.codec.display()
2631 )),
2632 );
2633 }
2634 }
2635 drop(prior_zero_copy);
2636
2637 let rendered = if state.use_gpu {
2638 format!("rendered on {}", state.render_node_path)
2639 } else {
2640 "rendered in software (Pixman)".to_string()
2641 };
2642 match video_encoder.as_ref() {
2643 Some(enc) => {
2644 report::capture("dmabuf", true);
2645 println!(
2646 "[Wayland] Zero-copy capture: output {display_id} {}x{} {rendered}, encoded in place on {}.",
2647 settings.width,
2648 settings.height,
2649 enc.backend_name()
2650 );
2651 }
2652 None => {
2653 let why = if different_gpu {
2654 "the encode node is another GPU"
2655 } else if !settings.codec.is_video() {
2656 "JPEG encodes on the CPU"
2657 } else if use_cpu_explicit {
2658 "software encoding selected"
2659 } else if !state.use_gpu {
2660 "no GPU renderer"
2661 } else {
2662 "no zero-copy encoder on the render node"
2663 };
2664 report::capture("readback", false);
2665 report::zero_copy_available(gpu_intent && state.use_gpu && !different_gpu);
2666 report::capture_reason(why);
2667 println!(
2668 "[Wayland] Readback capture: output {display_id} {}x{} {rendered}, read back for the encode thread ({why}).",
2669 settings.width, settings.height
2670 );
2671 }
2672 }
2673
2674 if host_capture && let Some(host) = &state.host {
2683 let epoch = host.start_capture(
2684 display_id,
2685 settings.width,
2686 settings.height,
2687 video_encoder.is_some(),
2688 settings.capture_cursor,
2689 settings.target_fps,
2690 );
2691 state.host_layout_pending.insert(
2692 display_id,
2693 wayland::frontend::PendingHostLayout {
2694 epoch,
2695 want: (settings.width, settings.height),
2696 geometry_waiters: std::mem::take(&mut geometry_waiters),
2697 },
2698 );
2699 }
2700
2701 if recording_sink.is_some() && !settings.codec.is_video() {
2702 eprintln!(
2703 "[recording_sink] WARNING: recording_socket is set but the codec is JPEG. The \
2704 recording socket requires a video stream; set a video codec on the Python \
2705 CaptureSettings to produce a recordable output."
2706 );
2707 }
2708
2709 let watermark_output_scale = node.output.current_scale().fractional_scale();
2712 node.overlay_state
2713 .load_watermark(&settings.watermark_path, watermark_output_scale);
2714 if display_id == 0 {
2715 state.settings = settings.clone();
2716 if state.cursor_callback_set
2717 && let Some(icon) = state.current_cursor_icon.clone()
2718 {
2719 state.send_cursor_image(&icon);
2720 }
2721 }
2722 state.render_cursor_on_framebuffer = settings.capture_cursor;
2723
2724 let mut cap = wayland::frontend::WlCapture {
2725 settings: settings.clone(),
2726 callback: cb.clone(),
2727 video_encoder,
2728 vaapi_state: StripeState::default(),
2729 recording_sink,
2730 deliver_tx: None,
2731 deliver_join: None,
2732 deliver_discard: None,
2733 pending_hw_delivery: None,
2734 pending_hw_damage: false,
2735 encode_pool: None,
2736 encode_join: None,
2737 encode_controls: Arc::new(WlEncodeControls::new()),
2738 encode_stats: Arc::new(WlEncodeStats::new()),
2739 report: stream_report.clone(),
2740 pool_last_render: Vec::new(),
2741 render_seq: 0,
2742 pool_content_gen: Vec::new(),
2743 content_gen: 0,
2744 frame_counter: 0,
2745 pending_force_idr: false,
2746 needs_full_render: true,
2747 pace: FramePace::default(),
2748 hw_error_streak: 0,
2749 hw_rebuilt: false,
2750 };
2751
2752 {
2753 let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
2754 let discard = Arc::new(AtomicBool::new(false));
2755 let thread_discard = discard.clone();
2756 let predecessor = prior_deliver_join;
2757 let join = thread::spawn(move || {
2761 if let Some(handle) = predecessor {
2766 let _ = handle.join();
2767 }
2768 match cb {
2769 Some(cb) => {
2770 crate::boost_thread_priority(-10);
2771 while let Ok(stripes) = rx.recv() {
2772 if thread_discard.load(Ordering::Relaxed)
2773 || PY_SHUTDOWN.load(Ordering::Relaxed)
2774 {
2775 continue;
2776 }
2777 stream_report.tally(&stripes);
2778 Python::attach(|py| {
2779 for s in stripes {
2780 match Py::new(
2781 py,
2782 StripeFrame::new_owned_meta(
2783 s.data,
2784 s.codec.data_type(),
2785 s.stripe_y_start,
2786 s.stripe_height,
2787 s.frame_id,
2788 s.timing,
2789 s.reference,
2790 ),
2791 ) {
2792 Ok(f) => {
2793 if let Err(e) = cb.call1(py, (f,)) {
2794 e.print(py);
2795 }
2796 }
2797 Err(e) => eprintln!("[Wayland] frame alloc error: {e:?}"),
2798 }
2799 }
2800 });
2801 }
2802 }
2803 None => while rx.recv().is_ok() {},
2804 }
2805 });
2806 cap.deliver_tx = Some(tx);
2807 cap.deliver_discard = Some(discard);
2808 cap.deliver_join = Some(join);
2809 }
2810
2811 if cap.video_encoder.is_none() {
2812 bootstrap_readback_pool(
2813 &mut cap,
2814 display_id,
2815 state.use_gpu && !host_capture,
2818 gpu_intent && (!state.use_gpu || different_gpu),
2819 None,
2820 prior_encode_join.take(),
2821 );
2822 } else {
2823 cap.encode_stats.n_stripes.store(1, Ordering::Relaxed);
2824 *cap.encode_stats.desc.lock().unwrap() =
2825 encoder_desc(&settings, cap.video_encoder.as_ref(), true);
2826 set_wayland_active_codec(display_id, Some(settings.codec));
2827 log_stream_settings("Wayland", &settings, 1, cap.video_encoder.as_ref());
2828 }
2829 state.encode_reaper.extend(prior_encode_join);
2832 cap.request_idr();
2836
2837 node.capture = Some(cap);
2838 node.content_hold_until = Some(Instant::now() + WL_CONTENT_HOLD);
2844 state.output_nodes.insert(node_idx, node);
2845 wayland_alive().lock().unwrap().insert(display_id);
2846 answer_geometry_waiters(state, display_id, geometry_waiters);
2847}
2848
2849fn draw_host_watermark(
2858 renderer: &mut GlesRenderer,
2859 overlay: &crate::encoders::overlay::OverlayState,
2860 target: &mut Dmabuf,
2861 size: (i32, i32),
2862) -> Result<SyncPoint, String> {
2863 let elem = overlay
2864 .get_watermark_element(renderer)
2865 .ok_or("watermark element unavailable")?;
2866 let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2867 let mut frame = renderer
2868 .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2869 .map_err(|e| format!("render: {e:?}"))?;
2870 let dst = elem.geometry(1.0.into());
2871 let local = Rectangle::from_size(dst.size);
2872 elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2873 .map_err(|e| format!("draw: {e:?}"))?;
2874 frame.finish().map_err(|e| format!("finish: {e:?}"))
2875}
2876
2877fn compose_host_watermark(
2881 renderer: &mut GlesRenderer,
2882 overlay: &crate::encoders::overlay::OverlayState,
2883 src: &Dmabuf,
2884 target: &mut Dmabuf,
2885 size: (i32, i32),
2886) -> Result<SyncPoint, String> {
2887 let elem = overlay
2888 .get_watermark_element(renderer)
2889 .ok_or("watermark element unavailable")?;
2890 let tex = renderer
2891 .import_dmabuf(src, None)
2892 .map_err(|e| format!("import: {e:?}"))?;
2893 let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2894 let full: Rectangle<i32, Physical> = Rectangle::from_size((size.0, size.1).into());
2895 let mut frame = renderer
2896 .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2897 .map_err(|e| format!("render: {e:?}"))?;
2898 frame
2899 .render_texture_from_to(
2900 &tex,
2901 Rectangle::from_size((size.0 as f64, size.1 as f64).into()),
2902 full,
2903 &[full],
2904 &[full],
2908 Transform::Normal,
2909 1.0,
2910 None,
2911 &[],
2912 )
2913 .map_err(|e| format!("texture: {e:?}"))?;
2914 let dst = elem.geometry(1.0.into());
2915 let local = Rectangle::from_size(dst.size);
2916 elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2917 .map_err(|e| format!("draw: {e:?}"))?;
2918 frame.finish().map_err(|e| format!("finish: {e:?}"))
2919}
2920
2921fn warn_once_host_watermark(e: &str) {
2922 static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2923 if !WARNED.swap(true, Ordering::Relaxed) {
2924 eprintln!("[HostCapture] watermark compositing failed: {e}");
2925 }
2926}
2927
2928fn copy_rows_into_shm(
2931 buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
2932 src: &[u8],
2933 width: i32,
2934 height: i32,
2935) -> Result<(), String> {
2936 use smithay::wayland::shm::with_buffer_contents_mut;
2937 let src_stride = (width.max(0) as usize) * 4;
2938 if src.len() < src_stride * (height.max(0) as usize) {
2939 return Err("source smaller than advertised".into());
2940 }
2941 with_buffer_contents_mut(buffer, |ptr, len, spec| {
2942 if spec.width < width || spec.height < height {
2943 return Err("shm buffer smaller than the output".to_string());
2944 }
2945 let dst_stride = spec.stride as usize;
2946 let offset = spec.offset as usize;
2947 if dst_stride < src_stride || len < offset + dst_stride * (height as usize) {
2948 return Err("shm stride or length mismatch".to_string());
2949 }
2950 for y in 0..height as usize {
2951 unsafe {
2952 std::ptr::copy_nonoverlapping(
2953 src.as_ptr().add(y * src_stride),
2954 ptr.add(offset + y * dst_stride),
2955 src_stride,
2956 );
2957 }
2958 }
2959 Ok(())
2960 })
2961 .map_err(|e| format!("{e:?}"))?
2962}
2963
2964fn service_copy_frames(
2970 state: &mut AppState,
2971 node: &mut wayland::frontend::OutputNode,
2972 width: i32,
2973 height: i32,
2974 damage_rects: &[Rectangle<i32, Physical>],
2975) {
2976 use smithay::backend::renderer::{Blit, BufferType, ExportMem, TextureFilter, buffer_type};
2977 use smithay::utils::Buffer as BufferCoords;
2978
2979 if state.copy_sessions.is_empty() {
2980 return;
2981 }
2982 let time = state.clock.now();
2983 for i in 0..state.copy_sessions.len() {
2984 {
2985 let cs = &state.copy_sessions[i];
2986 if cs.output.upgrade().as_ref() != Some(&node.output)
2987 || cs.pending.is_none()
2988 || (damage_rects.is_empty() && cs.delivered_once)
2989 {
2990 continue;
2991 }
2992 }
2993 let first = !state.copy_sessions[i].delivered_once;
2994 let frame = state.copy_sessions[i].pending.take().unwrap();
2995 let buffer = frame.buffer();
2996 let full = Rectangle::<i32, Physical>::new((0, 0).into(), (width, height).into());
2997 let result: Result<(), String> = match buffer_type(&buffer) {
2998 Some(BufferType::Dma) => (|| {
2999 let renderer = state.gles_renderer.as_mut().ok_or("no GLES renderer")?;
3000 let mut client = smithay::wayland::dmabuf::get_dmabuf(&buffer)
3001 .map_err(|e| e.to_string())?
3002 .clone();
3003 let (_bo, offscreen) = node
3004 .offscreen_buffer
3005 .as_mut()
3006 .ok_or("no composited buffer")?;
3007 let src = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
3008 let mut dst = renderer.bind(&mut client).map_err(|e| format!("{e:?}"))?;
3009 let sync = renderer
3013 .blit(&src, &mut dst, full, full, TextureFilter::Linear)
3014 .map_err(|e| format!("{e:?}"))?;
3015 if !wait_render_fence(&sync, node.id) {
3016 return Err("blit fence not signaled".to_string());
3017 }
3018 Ok(())
3019 })(),
3020 Some(BufferType::Shm) => (|| {
3021 if let Some(renderer) = state.gles_renderer.as_mut() {
3022 let (_bo, offscreen) = node
3023 .offscreen_buffer
3024 .as_mut()
3025 .ok_or("no composited buffer")?;
3026 let fb = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
3027 let mapping = renderer
3028 .copy_framebuffer(
3029 &fb,
3030 Rectangle::new((0, 0).into(), (width, height).into()),
3031 Fourcc::Argb8888,
3032 )
3033 .map_err(|e| format!("{e:?}"))?;
3034 let data = renderer
3035 .map_texture(&mapping)
3036 .map_err(|e| format!("{e:?}"))?;
3037 copy_rows_into_shm(&buffer, data, width, height)
3038 } else {
3039 copy_rows_into_shm(&buffer, &node.frame_buffer, width, height)
3040 }
3041 })(),
3042 _ => Err("unsupported buffer type".into()),
3043 };
3044 match result {
3045 Ok(()) => {
3046 let damage: Option<Vec<Rectangle<i32, BufferCoords>>> = if first {
3047 None
3048 } else {
3049 Some(
3050 damage_rects
3051 .iter()
3052 .map(|r| {
3053 Rectangle::new(
3054 (r.loc.x, r.loc.y).into(),
3055 (r.size.w, r.size.h).into(),
3056 )
3057 })
3058 .collect(),
3059 )
3060 };
3061 frame.success(Transform::Normal, damage, time);
3062 state.copy_sessions[i].delivered_once = true;
3063 }
3064 Err(e) => {
3065 eprintln!("[Wayland] copy-capture frame failed: {e}");
3066 frame.fail(CaptureFailureReason::Unknown);
3067 }
3068 }
3069 }
3070}
3071
3072fn send_cursor_frame(state: &AppState, output: &Output, time: impl Into<Duration>) {
3076 if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
3077 send_frames_surface_tree(surface, output, time, Some(Duration::ZERO), |_, _| {
3078 Some(output.clone())
3079 });
3080 }
3081}
3082
3083fn cursor_surface_hotspot(
3086 surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
3087) -> Point<i32, smithay::utils::Logical> {
3088 with_states(surface, |states| {
3089 states
3090 .data_map
3091 .get::<std::sync::Mutex<smithay::input::pointer::CursorImageAttributes>>()
3092 .and_then(|attrs| attrs.lock().ok().map(|guard| guard.hotspot))
3093 .unwrap_or_default()
3094 })
3095}
3096
3097const RENDER_FENCE_TIMEOUT: Duration = Duration::from_millis(500);
3099
3100const UNDRAWN_TICKS_PER_FRAME: u32 = 60;
3105
3106fn read_back_rgba(
3110 renderer: &mut GlesRenderer,
3111 target: &mut smithay::backend::renderer::gles::GlesTarget<'_>,
3112 width: i32,
3113 height: i32,
3114 dst: &mut [u8],
3115) {
3116 let read = renderer
3117 .render(target, (width, height).into(), Transform::Normal)
3118 .and_then(|mut frame| {
3119 frame.with_context(|gl| unsafe {
3120 gl.ReadPixels(
3121 0,
3122 0,
3123 width,
3124 height,
3125 smithay::backend::renderer::gles::ffi::RGBA,
3126 smithay::backend::renderer::gles::ffi::UNSIGNED_BYTE,
3127 dst.as_mut_ptr() as *mut std::ffi::c_void,
3128 );
3129 })
3130 });
3131 if let Err(e) = read {
3132 eprintln!("[Wayland] Readback failed: {e:?}");
3133 }
3134}
3135
3136fn wait_render_fence(sync: &SyncPoint, display_id: u32) -> bool {
3147 let reached = match sync.get::<EGLFence>() {
3148 Some(fence) => fence
3149 .client_wait(Some(RENDER_FENCE_TIMEOUT), true)
3150 .unwrap_or(false),
3151 None => sync.wait().is_ok(),
3152 };
3153 if !reached {
3154 eprintln!(
3155 "[Wayland] Display {display_id}: render fence not signaled within {:?}; frame dropped.",
3156 RENDER_FENCE_TIMEOUT
3157 );
3158 }
3159 reached
3160}
3161
3162fn render_pass(state: &mut AppState, trigger: TickTrigger) -> bool {
3166 let mut nodes = std::mem::take(&mut state.output_nodes);
3167 let mut any_pool_busy = false;
3168 for node in nodes.iter_mut() {
3169 if render_node_tick(state, node, trigger) {
3170 any_pool_busy = true;
3171 }
3172 }
3173 state.output_nodes = nodes;
3174 any_pool_busy
3175}
3176
3177fn render_display(state: &mut AppState, display_id: u32, trigger: TickTrigger) {
3179 let mut nodes = std::mem::take(&mut state.output_nodes);
3180 if let Some(node) = nodes.iter_mut().find(|n| n.id == display_id) {
3181 render_node_tick(state, node, trigger);
3182 }
3183 state.output_nodes = nodes;
3184}
3185
3186fn render_node_tick(
3187 state: &mut AppState,
3188 node: &mut wayland::frontend::OutputNode,
3189 trigger: TickTrigger,
3190) -> bool {
3191 let take_screenshot = state
3192 .pending_screenshot
3193 .as_ref()
3194 .is_some_and(|(id, _)| *id == node.id);
3195 let copy_frame_wanted = state.copy_frame_pending_for(&node.output);
3196 if node.capture.is_none() && !take_screenshot && !copy_frame_wanted {
3197 return false;
3198 }
3199
3200 if let Some(cap) = node.capture.as_ref()
3202 && !take_screenshot
3203 && !cap.pace.due(trigger, capture_period(cap), Instant::now())
3204 {
3205 return false;
3206 }
3207
3208 let output = node.output.clone();
3209 let origin: Point<i32, smithay::utils::Logical> = node.pos.into();
3210 let output_scale_val = output.current_scale().fractional_scale();
3211 let (width, height) = match node.capture.as_ref() {
3212 Some(c) => (c.settings.width, c.settings.height),
3213 None => output
3214 .current_mode()
3215 .map(|m| (m.size.w, m.size.h))
3216 .unwrap_or((0, 0)),
3217 };
3218 if width <= 0 || height <= 0 {
3219 return false;
3220 }
3221 if node.frame_buffer.len() < (width as usize) * (height as usize) * 4 {
3222 node.frame_buffer = vec![0u8; (width as usize) * (height as usize) * 4];
3223 }
3224 if node.owner.is_some()
3227 && (node.view_size != (width, height) || (node.view_scale - output_scale_val).abs() > 1e-6)
3228 {
3229 node.damage_tracker =
3230 OutputDamageTracker::new((width, height), output_scale_val, Transform::Normal);
3231 node.content_tracker = None;
3232 node.view_size = (width, height);
3233 node.view_scale = output_scale_val;
3234 }
3235 let logical_w = (width as f64 / output_scale_val).round();
3236 let logical_h = (height as f64 / output_scale_val).round();
3237
3238 let hold_frame = match node.content_hold_until {
3244 Some(deadline)
3245 if state.host.is_none()
3246 && Instant::now() < deadline
3247 && !wayland::frontend::output_content_covers(
3248 &state.space,
3249 node.owner.unwrap_or(node.id),
3250 logical_w,
3251 logical_h,
3252 ) =>
3253 {
3254 true
3255 }
3256 Some(_) => {
3257 node.content_hold_until = None;
3258 false
3259 }
3260 None => false,
3261 };
3262
3263 if let Some(cap) = node.capture.as_mut()
3265 && cap
3266 .recording_sink
3267 .as_ref()
3268 .map(|s| s.should_force_idr())
3269 .unwrap_or(false)
3270 {
3271 cap.request_idr();
3272 }
3273 let requested_idr = node
3274 .capture
3275 .as_ref()
3276 .map(|c| c.pending_force_idr)
3277 .unwrap_or(false);
3278 let hw_idr_pending = node
3282 .capture
3283 .as_ref()
3284 .map(|c| c.encode_controls.force_idr.load(Ordering::Relaxed))
3285 .unwrap_or(false);
3286 let want_idr_for_host = requested_idr || hw_idr_pending;
3287
3288 let mut pool_slot: Option<(usize, Vec<u8>)> = None;
3289 if !hold_frame
3290 && let Some(cap) = node.capture.as_ref()
3291 && let Some(ref pool) = cap.encode_pool
3292 {
3293 pool_slot = pool.try_begin();
3294 if pool_slot.is_none() {
3295 return true;
3296 }
3297 }
3298
3299 let loc_enum = node
3300 .capture
3301 .as_ref()
3302 .map(|c| c.settings.watermark_location_enum)
3303 .unwrap_or(state.settings.watermark_location_enum);
3304 node.overlay_state.update_position(width, height, loc_enum);
3305
3306 let host_mode = state
3310 .host
3311 .as_ref()
3312 .map(|h| h.has_output_for(node.id))
3313 .unwrap_or(false);
3314 if !host_mode && let Some(cap) = node.capture.as_mut() {
3315 let period = capture_period(cap);
3316 let paced = FramePace::input_paced(state.input_interval, period);
3317 cap.pace.ticked(trigger, period, Instant::now(), paced);
3318 }
3319
3320 let pointer_local: Option<Point<f64, smithay::utils::Logical>> = state
3323 .seat
3324 .get_pointer()
3325 .map(|p| p.current_location())
3326 .and_then(|pos| {
3327 let rect = Rectangle::<f64, smithay::utils::Logical>::new(
3328 origin.to_f64(),
3329 (logical_w, logical_h).into(),
3330 );
3331 if rect.contains(pos) {
3332 Some(pos - origin.to_f64())
3333 } else {
3334 None
3335 }
3336 });
3337
3338 let composite_ns = wayland::host::now_ns();
3340 let mut render_success = false;
3341 let mut render_sync = None;
3342 let mut damage_rects: Vec<Rectangle<i32, Physical>> = Vec::new();
3343 let needs_full = node
3344 .capture
3345 .as_ref()
3346 .map(|c| c.needs_full_render)
3347 .unwrap_or(!node.target_seeded);
3348
3349 if state.host.is_some() && !host_mode {
3350 if let Some((id, buf)) = pool_slot.take()
3353 && let Some(cap) = node.capture.as_ref()
3354 && let Some(ref pool) = cap.encode_pool
3355 {
3356 pool.cancel(id, buf);
3357 }
3358 return false;
3359 }
3360 let mut host_enc_dmabuf: Option<Dmabuf> = None;
3362 let mut new_stamp: Option<i64> = None;
3363 let mut host_cpu_frame = false;
3366 if host_mode {
3367 const RETAINED_OK: u8 = 0;
3368 const RETAINED_NONE: u8 = 1;
3369 const RETAINED_CPU_FRAME: u8 = 2;
3372 const RETAINED_GPU_FRAME: u8 = 3;
3373 let host_idx = node.id;
3374 let gpu_encoder = node
3375 .capture
3376 .as_ref()
3377 .map(|c| c.video_encoder.is_some())
3378 .unwrap_or(false);
3379 let host = state.host.take().unwrap();
3382 let expect = node
3387 .capture
3388 .as_ref()
3389 .map(|c| (c.settings.width, c.settings.height));
3390 let new_frame = match (host.try_take_frame(host_idx), expect) {
3391 (Some(f), Some((w, h))) if f.width != w || f.height != h => {
3392 host.release_frame(host_idx, f);
3393 None
3394 }
3395 (f, _) => f,
3396 };
3397 let have_new = new_frame.is_some();
3398 new_stamp = new_frame.as_ref().map(|f| f.stamp_ns);
3399 let streaming = node
3405 .capture
3406 .as_ref()
3407 .map(|c| c.settings.video_streaming_mode)
3408 .unwrap_or(false);
3409 let wm_active = node.overlay_state.is_active();
3410 let wm_animated = wm_active && node.overlay_state.is_animated();
3411 let now = Instant::now();
3417 let period = node.capture.as_ref().map(capture_period);
3418 let host_quiet = node.capture.as_ref().is_none_or(|cap| {
3419 cap.pace
3420 .since_last_tick(now)
3421 .is_none_or(|since| since >= capture_period(cap).mul_f64(HOST_QUIET_PERIODS))
3422 });
3423 if !have_new
3424 && !want_idr_for_host
3425 && !take_screenshot
3426 && !wm_animated
3427 && !(streaming && host_quiet)
3428 {
3429 if let Some((id, buf)) = pool_slot.take()
3430 && let Some(cap) = node.capture.as_ref()
3431 && let Some(ref pool) = cap.encode_pool
3432 {
3433 pool.cancel(id, buf);
3434 }
3435 if let Some(cap) = node.capture.as_mut()
3436 && let Some(period) = period
3437 {
3438 let held = if streaming {
3439 cap.pace
3440 .last_tick
3441 .map_or(now, |last| last + period.mul_f64(HOST_QUIET_PERIODS))
3442 } else {
3443 now + period
3444 };
3445 cap.pace.defer(held.max(now + Duration::from_millis(1)));
3446 }
3447 state.host = Some(host);
3448 return false;
3449 }
3450 if let Some(cap) = node.capture.as_mut()
3451 && let Some(period) = period
3452 {
3453 cap.pace.ticked(trigger, period, now, false);
3454 }
3455 if let Some(f) = new_frame {
3456 host.retain_frame(host_idx, f);
3457 }
3458 let mut wm_drawn = false;
3463 let outcome = host.with_retained(host_idx, |r| {
3464 let Some(f) = r else { return RETAINED_NONE };
3465 damage_rects = if have_new {
3466 f.damage.clone()
3467 } else {
3468 Vec::new()
3469 };
3470 if let Some(cpu) = f.cpu.as_ref() {
3471 if gpu_encoder {
3472 return RETAINED_CPU_FRAME;
3473 }
3474 host_cpu_frame = true;
3475 if let Some((_, ref mut buf)) = pool_slot {
3476 cpu.write_bgra(f.width, f.height, buf);
3477 if wm_active {
3478 node.overlay_state.blend_bgra(
3479 buf,
3480 (f.width as usize) * 4,
3481 f.width,
3482 f.height,
3483 );
3484 wm_drawn = true;
3485 }
3486 }
3487 cpu.write_bgra(f.width, f.height, &mut node.frame_buffer);
3488 if wm_active {
3489 node.overlay_state.blend_bgra(
3490 &mut node.frame_buffer,
3491 (f.width as usize) * 4,
3492 f.width,
3493 f.height,
3494 );
3495 }
3496 } else if let Some(dmabuf) = f.dmabuf.as_ref() {
3497 if !gpu_encoder {
3498 return RETAINED_GPU_FRAME;
3499 }
3500 host_enc_dmabuf = Some(dmabuf.clone());
3501 }
3502 RETAINED_OK
3503 });
3504 if have_new && let Some(src) = host_enc_dmabuf.as_ref() {
3506 wayland::host::wait_gpu_done(src);
3507 }
3508 if let Some(src) = host_enc_dmabuf.clone() {
3514 if wm_active && let Some(renderer) = state.gles_renderer.as_mut() {
3515 if wm_animated {
3516 if let Some((_, target)) = node.offscreen_buffer.as_mut() {
3517 match compose_host_watermark(
3518 renderer,
3519 &node.overlay_state,
3520 &src,
3521 target,
3522 (width, height),
3523 ) {
3524 Ok(sync) => {
3525 render_sync = Some(sync);
3526 host_enc_dmabuf = Some(target.clone());
3527 wm_drawn = true;
3528 }
3529 Err(e) => warn_once_host_watermark(&e),
3530 }
3531 }
3532 } else if have_new {
3533 let mut target = src.clone();
3534 match draw_host_watermark(
3535 renderer,
3536 &node.overlay_state,
3537 &mut target,
3538 (width, height),
3539 ) {
3540 Ok(sync) => {
3541 render_sync = Some(sync);
3542 wm_drawn = true;
3543 }
3544 Err(e) => warn_once_host_watermark(&e),
3545 }
3546 }
3547 }
3548 if take_screenshot && let Some(renderer) = state.gles_renderer.as_mut() {
3549 let mut shot = host_enc_dmabuf.clone().unwrap_or(src);
3550 match renderer.bind(&mut shot) {
3551 Ok(fb) => {
3552 let rect = Rectangle::new((0, 0).into(), (width, height).into());
3553 match renderer.copy_framebuffer(&fb, rect, Fourcc::Abgr8888) {
3554 Ok(mapping) => match renderer.map_texture(&mapping) {
3555 Ok(data) => {
3556 let n = data.len().min(node.frame_buffer.len());
3557 node.frame_buffer[..n].copy_from_slice(&data[..n]);
3558 }
3559 Err(e) => eprintln!("[HostCapture] screenshot map: {e:?}"),
3560 },
3561 Err(e) => eprintln!("[HostCapture] screenshot copy: {e:?}"),
3562 }
3563 }
3564 Err(e) => eprintln!("[HostCapture] screenshot bind: {e:?}"),
3565 };
3566 }
3567 }
3568 if wm_drawn && let Some(rect) = node.overlay_state.damage_rect(width, height) {
3569 damage_rects.push(rect);
3570 }
3571 state.host = Some(host);
3572 if outcome != RETAINED_OK {
3573 if let Some((id, buf)) = pool_slot.take()
3574 && let Some(cap) = node.capture.as_ref()
3575 && let Some(ref pool) = cap.encode_pool
3576 {
3577 pool.cancel(id, buf);
3578 }
3579 if outcome == RETAINED_GPU_FRAME {
3583 if let Some(h) = state.host.as_ref() {
3587 h.set_buffer_type(node.id, false);
3588 }
3589 } else if outcome == RETAINED_CPU_FRAME
3590 && let Some(cap) = node.capture.as_mut()
3591 {
3592 eprintln!(
3596 "[HostCapture] host delivers software frames; demoting the zero-copy encoder to readback encode."
3597 );
3598 let _report = report::enter(&cap.report);
3599 report::capture("readback", false);
3600 report::zero_copy_available(false);
3601 report::capture_reason("the host compositor delivers software frames only");
3602 cap.video_encoder = None;
3603 let s = &cap.settings;
3604 let try_gpu = s.codec.is_video() && !(s.use_cpu || s.encode_node_index == -1);
3605 bootstrap_readback_pool(cap, node.id, false, try_gpu, None, None);
3606 cap.request_idr();
3607 if let Some(h) = state.host.as_ref() {
3609 h.set_buffer_type(node.id, false);
3610 }
3611 }
3612 return false;
3613 }
3614 render_success = true;
3615 }
3616
3617 if !host_mode && state.use_gpu {
3618 if let Some(renderer) = state.gles_renderer.as_mut() {
3619 let mut cap = node.capture.as_mut();
3620 if let Some((bo, dmabuf)) = node.offscreen_buffer.as_mut()
3623 && (bo.width() as i32, bo.height() as i32) == (width, height)
3624 {
3625 let render_age = if node.overlay_state.is_animated() || needs_full {
3626 0
3627 } else {
3628 1
3629 };
3630 match renderer.bind(dmabuf) {
3631 Ok(mut frame) => {
3632 let mut elements: Vec<
3633 CompositionElements<
3634 GlesRenderer,
3635 WaylandSurfaceRenderElement<GlesRenderer>,
3636 >,
3637 > = Vec::new();
3638
3639 if state.render_cursor_on_framebuffer
3640 && let Some(pos) = pointer_local
3641 {
3642 let scale = Scale::from(output_scale_val);
3643
3644 if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon
3645 {
3646 let name = wayland::frontend::cursor_icon_to_str(icon);
3647 let time =
3648 Duration::from_millis(state.clock.now().as_millis() as u64);
3649 if let Some(image) = state.cursor_helper.get_image_by_name(
3650 name,
3651 output_scale_val.round() as u32,
3652 time,
3653 ) && let Some(elem) = node.overlay_state.get_cursor_element(
3654 renderer,
3655 image,
3656 pos,
3657 output_scale_val,
3658 ) {
3659 elements.push(CompositionElements::Cursor(elem));
3660 }
3661 } else if let Some(CursorImageStatus::Surface(surface)) =
3662 &state.current_cursor_icon
3663 {
3664 let hot = cursor_surface_hotspot(surface).to_f64();
3665 let phys_pos = (pos - hot).to_physical(scale);
3666 let elem_result = with_states(surface, |states| {
3667 WaylandSurfaceRenderElement::from_surface(
3668 renderer,
3669 surface,
3670 states,
3671 phys_pos,
3672 1.0,
3673 smithay::backend::renderer::element::Kind::Cursor,
3674 )
3675 });
3676 if let Ok(Some(cursor_elem)) = elem_result {
3677 elements.push(CompositionElements::Surface(cursor_elem));
3678 }
3679 } else if state.current_cursor_icon.is_none() {
3680 let time =
3681 Duration::from_millis(state.clock.now().as_millis() as u64);
3682 let image = state
3683 .cursor_helper
3684 .get_image(output_scale_val.round() as u32, time);
3685 if let Some(elem) = node.overlay_state.get_cursor_element(
3686 renderer,
3687 image,
3688 pos,
3689 output_scale_val,
3690 ) {
3691 elements.push(CompositionElements::Cursor(elem));
3692 }
3693 }
3694 }
3695
3696 if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3697 elements.push(CompositionElements::Cursor(elem));
3698 }
3699
3700 {
3701 let layer_map = layer_map_for_output(&output);
3702
3703 push_layer_elements(
3704 renderer,
3705 &mut elements,
3706 &layer_map,
3707 smithay::wayland::shell::wlr_layer::Layer::Overlay,
3708 output_scale_val,
3709 );
3710 push_layer_elements(
3711 renderer,
3712 &mut elements,
3713 &layer_map,
3714 smithay::wayland::shell::wlr_layer::Layer::Top,
3715 output_scale_val,
3716 );
3717 }
3718
3719 for window in state.space.elements_for_output(&output).rev() {
3720 let window_loc =
3721 state.space.element_location(window).unwrap_or_default() - origin;
3722
3723 elements.extend(
3724 window
3725 .render_elements(
3726 renderer,
3727 window_loc.to_physical_precise_round(output_scale_val),
3728 Scale::from(output_scale_val),
3729 1.0,
3730 )
3731 .into_iter()
3732 .map(CompositionElements::Space),
3733 );
3734 }
3735
3736 {
3737 let layer_map = layer_map_for_output(&output);
3738
3739 push_layer_elements(
3740 renderer,
3741 &mut elements,
3742 &layer_map,
3743 smithay::wayland::shell::wlr_layer::Layer::Bottom,
3744 output_scale_val,
3745 );
3746 push_layer_elements(
3747 renderer,
3748 &mut elements,
3749 &layer_map,
3750 smithay::wayland::shell::wlr_layer::Layer::Background,
3751 output_scale_val,
3752 );
3753 }
3754 match node.damage_tracker.render_output(
3755 renderer,
3756 &mut frame,
3757 render_age,
3758 &elements,
3759 [0.1, 0.1, 0.1, 1.0],
3760 ) {
3761 Ok(result) => {
3762 render_success = true;
3763 if let Some(damage) = result.damage {
3764 damage_rects = damage.clone();
3765 node.undrawn_ticks = 0;
3766 } else {
3767 node.undrawn_ticks += 1;
3768 if node.undrawn_ticks >= UNDRAWN_TICKS_PER_FRAME {
3769 node.undrawn_ticks = 0;
3770 let _ = renderer
3771 .render(
3772 &mut frame,
3773 (width, height).into(),
3774 Transform::Normal,
3775 )
3776 .and_then(|f| f.finish());
3777 }
3778 }
3779 if wait_render_fence(&result.sync, node.id) {
3780 if let Some(c) = cap.as_deref_mut() {
3781 c.needs_full_render = false;
3782 }
3783 } else {
3784 render_success = false;
3787 damage_rects.clear();
3788 if let Some(c) = cap.as_deref_mut() {
3789 c.needs_full_render = true;
3790 }
3791 }
3792 }
3793 Err(e) => eprintln!("Render error: {:?}", e),
3794 }
3795 if let Some(c) = cap {
3796 if !damage_rects.is_empty() {
3797 c.content_gen += 1;
3798 }
3799 if let Some((id, ref mut buf)) = pool_slot {
3800 if render_success && c.pool_content_gen[id] != c.content_gen {
3805 read_back_rgba(renderer, &mut frame, width, height, buf);
3806 c.pool_content_gen[id] = c.content_gen;
3807 }
3808 }
3809 }
3810 if pool_slot.is_none() && take_screenshot {
3811 read_back_rgba(
3812 renderer,
3813 &mut frame,
3814 width,
3815 height,
3816 &mut node.frame_buffer,
3817 );
3818 }
3819 }
3820 Err(e) => eprintln!("Failed to bind buffer: {:?}", e),
3821 }
3822 }
3823 }
3824 } else if !host_mode && let Some(renderer) = state.pixman_renderer.as_mut() {
3825 let mut cap = node.capture.as_mut();
3826 let (ptr, buf_age) = match pool_slot {
3827 Some((id, ref mut buf)) => {
3828 let age = cap
3829 .as_ref()
3830 .map(|c| {
3831 if c.pool_last_render[id] == 0 {
3832 0
3833 } else {
3834 (c.render_seq + 1 - c.pool_last_render[id]) as usize
3835 }
3836 })
3837 .unwrap_or(0);
3838 (buf.as_mut_ptr() as *mut u32, age)
3839 }
3840 None => (node.frame_buffer.as_mut_ptr() as *mut u32, 0),
3841 };
3842 let mut image = unsafe {
3843 pixman::Image::from_raw_mut(
3844 pixman::FormatCode::A8R8G8B8,
3845 width as usize,
3846 height as usize,
3847 ptr,
3848 (width as usize) * 4,
3849 false,
3850 )
3851 .expect("Failed to create pixman image")
3852 };
3853 match renderer.bind(&mut image) {
3854 Ok(mut frame) => {
3855 let mut elements: Vec<
3856 CompositionElements<
3857 PixmanRenderer,
3858 WaylandSurfaceRenderElement<PixmanRenderer>,
3859 >,
3860 > = Vec::new();
3861
3862 if state.render_cursor_on_framebuffer
3863 && let Some(pos) = pointer_local
3864 {
3865 let scale = Scale::from(output_scale_val);
3866
3867 if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon {
3868 let name = wayland::frontend::cursor_icon_to_str(icon);
3869 let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3870 if let Some(image) = state.cursor_helper.get_image_by_name(
3871 name,
3872 output_scale_val.round() as u32,
3873 time,
3874 ) && let Some(elem) = node.overlay_state.get_cursor_element(
3875 renderer,
3876 image,
3877 pos,
3878 output_scale_val,
3879 ) {
3880 elements.push(CompositionElements::Cursor(elem));
3881 }
3882 } else if let Some(CursorImageStatus::Surface(surface)) =
3883 &state.current_cursor_icon
3884 {
3885 let hot = cursor_surface_hotspot(surface).to_f64();
3886 let phys_pos = (pos - hot).to_physical(scale);
3887 let elem_result = with_states(surface, |states| {
3888 WaylandSurfaceRenderElement::from_surface(
3889 renderer,
3890 surface,
3891 states,
3892 phys_pos,
3893 1.0,
3894 smithay::backend::renderer::element::Kind::Cursor,
3895 )
3896 });
3897 if let Ok(Some(cursor_elem)) = elem_result {
3898 elements.push(CompositionElements::Surface(cursor_elem));
3899 }
3900 } else if state.current_cursor_icon.is_none() {
3901 let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3902 let image = state
3903 .cursor_helper
3904 .get_image(output_scale_val.round() as u32, time);
3905 if let Some(elem) = node.overlay_state.get_cursor_element(
3906 renderer,
3907 image,
3908 pos,
3909 output_scale_val,
3910 ) {
3911 elements.push(CompositionElements::Cursor(elem));
3912 }
3913 }
3914 }
3915
3916 if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3917 elements.push(CompositionElements::Cursor(elem));
3918 }
3919
3920 {
3921 let layer_map = layer_map_for_output(&output);
3922
3923 push_layer_elements(
3924 renderer,
3925 &mut elements,
3926 &layer_map,
3927 smithay::wayland::shell::wlr_layer::Layer::Overlay,
3928 output_scale_val,
3929 );
3930 push_layer_elements(
3931 renderer,
3932 &mut elements,
3933 &layer_map,
3934 smithay::wayland::shell::wlr_layer::Layer::Top,
3935 output_scale_val,
3936 );
3937 }
3938
3939 for window in state.space.elements_for_output(&output).rev() {
3940 let loc = state.space.element_location(window).unwrap_or_default() - origin;
3941
3942 elements.extend(
3943 window
3944 .render_elements(
3945 renderer,
3946 loc.to_physical_precise_round(output_scale_val),
3947 Scale::from(output_scale_val),
3948 1.0,
3949 )
3950 .into_iter()
3951 .map(CompositionElements::Space),
3952 );
3953 }
3954
3955 {
3956 let layer_map = layer_map_for_output(&output);
3957
3958 push_layer_elements(
3959 renderer,
3960 &mut elements,
3961 &layer_map,
3962 smithay::wayland::shell::wlr_layer::Layer::Bottom,
3963 output_scale_val,
3964 );
3965 push_layer_elements(
3966 renderer,
3967 &mut elements,
3968 &layer_map,
3969 smithay::wayland::shell::wlr_layer::Layer::Background,
3970 output_scale_val,
3971 );
3972 }
3973
3974 let render_age = if node.overlay_state.is_animated() || needs_full {
3975 0
3976 } else {
3977 buf_age
3978 };
3979 let changed = node
3980 .content_tracker
3981 .get_or_insert_with(|| {
3982 OutputDamageTracker::from_mode_source(node.damage_tracker.mode().clone())
3983 })
3984 .damage_output(1, &elements)
3985 .ok()
3986 .map(|(damage, _)| damage.cloned().unwrap_or_default());
3987 match node.damage_tracker.render_output(
3988 renderer,
3989 &mut frame,
3990 render_age,
3991 &elements,
3992 [0.1, 0.1, 0.1, 1.0],
3993 ) {
3994 Ok(result) => {
3995 render_success = true;
3996 if let Some(c) = cap.as_deref_mut() {
3997 c.needs_full_render = false;
3998 }
3999 if let Some(damage) = result.damage {
4000 damage_rects = damage.clone();
4001 }
4002 if let Some(changed) = changed.filter(|_| render_age > 1) {
4003 damage_rects = changed;
4004 }
4005 }
4006 Err(e) => eprintln!("Render error: {:?}", e),
4007 }
4008 if let Some(c) = cap {
4009 c.render_seq += 1;
4010 if render_success && let Some((id, _)) = pool_slot {
4011 c.pool_last_render[id] = c.render_seq;
4012 }
4013 }
4014 }
4015 Err(e) => eprintln!("Failed to bind pixman image: {:?}", e),
4016 }
4017 }
4018
4019 if render_success {
4020 node.target_seeded = true;
4021 }
4022 let screen = node.owner.unwrap_or(node.id);
4029 let own_fps = node
4030 .capture
4031 .as_ref()
4032 .map(|c| c.settings.target_fps)
4033 .unwrap_or(0.0);
4034 let own_id = node.id;
4035 let drives_screen = !state.output_nodes.iter().any(|n| {
4036 let fps = n
4037 .capture
4038 .as_ref()
4039 .map(|c| c.settings.target_fps)
4040 .unwrap_or(0.0);
4041 n.owner.unwrap_or(n.id) == screen
4042 && n.capture.is_some()
4043 && (fps > own_fps || (fps == own_fps && n.id < own_id))
4044 });
4045 if render_success && drives_screen {
4046 let time = state.clock.now();
4047 let mut feedback = OutputPresentationFeedback::new(&output);
4050 for window in state.space.elements_for_output(&output) {
4051 window.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
4052 Some(output.clone())
4053 });
4054 window.take_presentation_feedback(
4055 &mut feedback,
4056 |_, _| Some(output.clone()),
4057 |_, _| wp_presentation_feedback::Kind::empty(),
4058 );
4059 }
4060 for layer in layer_map_for_output(&output).layers() {
4065 layer.send_frame(&output, time, Some(Duration::ZERO), |_, _| {
4066 Some(output.clone())
4067 });
4068 layer.take_presentation_feedback(
4069 &mut feedback,
4070 |_, _| Some(output.clone()),
4071 |_, _| wp_presentation_feedback::Kind::empty(),
4072 );
4073 }
4074 send_cursor_frame(state, &output, time);
4075 let refresh = match node.capture.as_ref() {
4076 Some(c) => Refresh::Fixed(Duration::from_secs_f64(
4077 1.0 / c.settings.target_fps.max(1.0),
4078 )),
4079 None => Refresh::Unknown,
4080 };
4081 node.frame_seq += 1;
4082 feedback.presented(
4083 time,
4084 refresh,
4085 node.frame_seq,
4086 wp_presentation_feedback::Kind::Vsync,
4087 );
4088
4089 if !host_mode {
4092 service_copy_frames(state, node, width, height, &damage_rects);
4093 }
4094
4095 if !hold_frame && let Some(cap) = node.capture.as_mut() {
4096 if cap.encode_join.as_ref().is_some_and(|j| j.is_finished()) {
4101 let prior = cap.encode_join.take().and_then(|j| j.join().ok().flatten());
4102 if let Some(pool) = cap.encode_pool.take() {
4103 pool.shutdown();
4104 }
4105 let s = &cap.settings;
4106 let try_gpu = s.codec.is_video() && !(s.use_cpu || s.encode_node_index == -1);
4107 eprintln!("[Wayland] encode thread died; rebuilding the readback path.");
4108 bootstrap_readback_pool(
4110 cap,
4111 node.id,
4112 state.use_gpu && !host_mode,
4113 try_gpu,
4114 prior,
4115 None,
4116 );
4117 cap.request_idr();
4118 }
4119 if cap.encode_pool.is_some() {
4120 if take_screenshot && let Some((_, ref buf)) = pool_slot {
4121 let n = buf.len().min(node.frame_buffer.len());
4122 node.frame_buffer[..n].copy_from_slice(&buf[..n]);
4123 }
4124 if let Some((id, buf)) = pool_slot.take() {
4125 let frame = WlFrame {
4126 id,
4127 buf,
4128 frame_id: cap.frame_counter,
4129 damage: std::mem::take(&mut damage_rects),
4130 is_animated: node.overlay_state.is_animated(),
4131 captured_ns: new_stamp.unwrap_or(composite_ns),
4132 };
4133 if let Some(pool) = cap.encode_pool.as_ref() {
4134 pool.publish(frame);
4135 }
4136 cap.frame_counter = cap.frame_counter.wrapping_add(1);
4137 }
4138 } else if let Some(ref mut encoder) = cap.video_encoder {
4139 let slot_free = match cap.pending_hw_delivery.take() {
4147 None => true,
4148 Some(pending) => match cap.deliver_tx.as_ref() {
4149 None => true,
4150 Some(tx) => match tx.try_send(pending) {
4151 Ok(()) => true,
4152 Err(std::sync::mpsc::TrySendError::Full(p)) => {
4153 cap.pending_hw_delivery = Some(p);
4154 false
4155 }
4156 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => true,
4157 },
4158 },
4159 };
4160 if !slot_free {
4161 if !damage_rects.is_empty() {
4162 cap.pending_hw_damage = true;
4163 }
4164 } else {
4165 let is_animated = node.overlay_state.is_animated();
4166 let damage =
4167 match crate::pipeline::Damage::of_rects(&damage_rects, width, height) {
4168 crate::pipeline::Damage::None
4169 if std::mem::take(&mut cap.pending_hw_damage) =>
4170 {
4171 crate::pipeline::Damage::Unknown
4172 }
4173 damage => {
4174 cap.pending_hw_damage = false;
4175 damage
4176 }
4177 };
4178 let decision = crate::pipeline::decide_hw_fullframe(
4179 &mut cap.vaapi_state,
4180 &cap.settings,
4181 cap.frame_counter,
4182 damage,
4183 is_animated,
4184 requested_idr,
4185 crate::pipeline::EncoderQuality::of(encoder),
4186 );
4187 let mut send_frame = decision.send;
4188 let force_idr = decision.force_idr;
4189 let target_qp = decision.target_qp;
4190
4191 let mut frame_out = false;
4192 if send_frame
4193 && let Some(sync) = render_sync.take()
4194 && !wait_render_fence(&sync, node.id)
4195 {
4196 send_frame = false;
4197 }
4198 if send_frame {
4199 let enc_dmabuf: Option<Dmabuf> = host_enc_dmabuf
4202 .clone()
4203 .or_else(|| node.offscreen_buffer.as_ref().map(|(_, d)| d.clone()));
4204 let encode_start_ns = wayland::host::now_ns();
4205 decision.prepare(encoder);
4206 let result = match enc_dmabuf {
4207 Some(ref dmabuf) => encoder.encode_dmabuf(
4208 dmabuf,
4209 cap.frame_counter as u64,
4210 target_qp,
4211 force_idr,
4212 ),
4213 None => Err(
4214 "zero-copy encode requires an offscreen buffer (GPU context)"
4215 .to_string(),
4216 ),
4217 };
4218
4219 if let Ok(data) = result {
4220 cap.hw_error_streak = 0;
4221 cap.hw_rebuilt = false;
4222 if !data.is_empty() {
4223 frame_out = true;
4224 if wayland::host::trace()
4225 && let Some(stamp) = new_stamp
4226 {
4227 let age = (wayland::host::now_ns() - stamp) as f64 / 1e6;
4228 eprintln!(
4229 "[HostTrace] output {} frame {} encoded +{age:.2}ms {}B",
4230 node.id,
4231 cap.frame_counter,
4232 data.len()
4233 );
4234 }
4235 cap.encode_stats.frames.fetch_add(1, Ordering::Relaxed);
4236 cap.encode_stats.stripes.fetch_add(1, Ordering::Relaxed);
4237 if let Some(ref tx) = cap.deliver_tx {
4238 let stripes = vec![EncodedStripe {
4239 data: Arc::new(data),
4240 codec: cap.settings.codec,
4241 stripe_y_start: 0,
4242 stripe_height: height,
4243 frame_id: cap.frame_counter as i32,
4244 timing: FrameTiming {
4245 capture_ns: new_stamp.unwrap_or(composite_ns),
4246 encode_start_ns,
4247 encode_end_ns: wayland::host::now_ns(),
4248 },
4249 reference: encoder.last_reference(),
4250 }];
4251 if let Some(ref socket) = cap.recording_sink {
4252 socket.write_frame(&stripes, width, height);
4253 }
4254 crate::recorder::wayland_tap(node.id, &stripes);
4255 match tx.try_send(stripes) {
4258 Ok(()) => {}
4259 Err(std::sync::mpsc::TrySendError::Full(s)) => {
4260 cap.pending_hw_delivery = Some(s);
4261 }
4262 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {}
4263 }
4264 }
4265 }
4266 } else if let Err(e) = result {
4267 eprintln!("[Wayland] HW encode error: {e}");
4268 cap.hw_error_streak = cap.hw_error_streak.saturating_add(1);
4269 if cap.hw_error_streak == HW_ERROR_RECOVERY_THRESHOLD {
4270 let _report = report::enter(&cap.report);
4271 let rebuilt = if cap.hw_rebuilt {
4279 None
4280 } else {
4281 drop(cap.video_encoder.take());
4286 rebuild_zerocopy_encoder(cap, state)
4287 };
4288 match rebuilt {
4289 Some(enc) => {
4290 cap.video_encoder = Some(enc);
4291 cap.pending_force_idr = true;
4292 cap.hw_rebuilt = true;
4293 eprintln!(
4294 "[Wayland] zero-copy HW encoder rebuilt after repeated encode errors."
4295 );
4296 }
4297 None => {
4298 eprintln!(
4299 "[Wayland] zero-copy HW encoder unrecoverable; demoting to readback encode."
4300 );
4301 report::capture("readback", false);
4302 report::capture_reason(
4303 "the zero-copy encoder failed repeatedly and was given up",
4304 );
4305 cap.video_encoder = None;
4306 cap.hw_rebuilt = false;
4307 let s = &cap.settings;
4310 let try_gpu = s.codec.is_video()
4311 && !(s.use_cpu || s.encode_node_index == -1);
4312 bootstrap_readback_pool(
4315 cap,
4316 node.id,
4317 state.use_gpu && !host_mode,
4318 try_gpu,
4319 None,
4320 None,
4321 );
4322 if host_mode && let Some(h) = state.host.as_ref() {
4325 h.set_buffer_type(node.id, false);
4326 }
4327 }
4328 }
4329 cap.hw_error_streak = 0;
4330 }
4331 }
4332 }
4333 cap.pending_force_idr = requested_idr && !frame_out;
4336 cap.frame_counter = cap.frame_counter.wrapping_add(1);
4337 }
4338 }
4339 }
4340 if take_screenshot && let Some((_, resp)) = state.pending_screenshot.take() {
4341 if !node.frame_buffer.is_empty() {
4342 let w = width as u32;
4343 let h = height as u32;
4344 let png = if state.use_gpu && !host_cpu_frame {
4347 crate::computer_use::encode_png_rgba(&node.frame_buffer, w, h)
4348 } else {
4349 let mut rgba = node.frame_buffer.clone();
4350 for px in rgba.as_chunks_mut::<4>().0 {
4351 px.swap(0, 2);
4352 }
4353 crate::computer_use::encode_png_rgba(&rgba, w, h)
4354 };
4355 match png {
4356 Ok(data) => {
4357 let _ = resp.send(Ok(data));
4358 }
4359 Err(e) => {
4360 let _ = resp.send(Err(format!("PNG encode error: {e}")));
4361 eprintln!("[ComputerUse] PNG encode error: {}", e);
4362 }
4363 }
4364 } else {
4365 let _ = resp.send(Err("Screenshot render produced no pixels".to_string()));
4366 }
4367 }
4368 }
4369 if let Some((id, buf)) = pool_slot.take()
4370 && let Some(cap) = node.capture.as_ref()
4371 && let Some(ref pool) = cap.encode_pool
4372 {
4373 pool.cancel(id, buf);
4374 }
4375 false
4376}
4377
4378fn rects_overlap(a: (i32, i32, i32, i32), b: (i32, i32, i32, i32)) -> bool {
4382 let (ax, ay, aw, ah) = (a.0 as i64, a.1 as i64, a.2 as i64, a.3 as i64);
4383 let (bx, by, bw, bh) = (b.0 as i64, b.1 as i64, b.2 as i64, b.3 as i64);
4384 aw > 0
4385 && ah > 0
4386 && bw > 0
4387 && bh > 0
4388 && ax < bx + bw
4389 && bx < ax + aw
4390 && ay < by + bh
4391 && by < ay + ah
4392}
4393
4394type OutputOverlap = (u32, &'static str, (i32, i32, i32, i32));
4397
4398fn find_output_overlap(
4404 nodes: &[wayland::frontend::OutputNode],
4405 skip_id: Option<u32>,
4406 logical: (i32, i32, i32, i32),
4407 physical: (i32, i32, i32, i32),
4408) -> Option<OutputOverlap> {
4409 for n in nodes {
4410 if Some(n.id) == skip_id {
4411 continue;
4412 }
4413 if n.owner.is_some() {
4414 continue;
4415 }
4416 if let Some(geo) = n.logical_geometry() {
4417 let other = (geo.loc.x, geo.loc.y, geo.size.w, geo.size.h);
4418 if rects_overlap(logical, other) {
4419 return Some((n.id, "logical", other));
4420 }
4421 }
4422 if let Some(mode) = n.output.current_mode() {
4423 let other = (n.pos.0, n.pos.1, mode.size.w, mode.size.h);
4424 if rects_overlap(physical, other) {
4425 return Some((n.id, "physical", other));
4426 }
4427 }
4428 }
4429 None
4430}
4431
4432fn create_output_on(
4439 state: &mut AppState,
4440 id: u32,
4441 width: i32,
4442 height: i32,
4443 x: i32,
4444 y: i32,
4445 scale: f64,
4446) -> bool {
4447 if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || scale <= 0.0 {
4448 return false;
4449 }
4450 if let Some(host) = state.host.as_ref() {
4453 let capacity = host.output_count();
4454 if state.output_nodes.len() >= capacity {
4455 eprintln!(
4456 "[Wayland] CreateOutput {id}: rejected, the host compositor has {capacity} output(s) and all are backing displays."
4457 );
4458 return false;
4459 }
4460 }
4461 let logical_size = (
4462 (width as f64 / scale).round() as i32,
4463 (height as f64 / scale).round() as i32,
4464 );
4465 if let Some((oid, flavor, other)) = find_output_overlap(
4466 &state.output_nodes,
4467 None,
4468 (x, y, logical_size.0, logical_size.1),
4469 (x, y, width, height),
4470 ) {
4471 eprintln!(
4472 "[Wayland] CreateOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
4473 if flavor == "logical" {
4474 logical_size.0
4475 } else {
4476 width
4477 },
4478 if flavor == "logical" {
4479 logical_size.1
4480 } else {
4481 height
4482 },
4483 other.2,
4484 other.3,
4485 other.0,
4486 other.1,
4487 );
4488 return false;
4489 }
4490 let output = Output::new(
4491 format!("HEADLESS-{}", id + 1),
4492 PhysicalProperties {
4493 size: (width, height).into(),
4494 subpixel: Subpixel::Unknown,
4495 make: "Pixelflux".into(),
4496 model: "Virtual".into(),
4497 serial_number: format!("{:03}", id + 1),
4498 },
4499 );
4500 let mode = OutputMode {
4501 size: (width, height).into(),
4502 refresh: 60_000,
4503 };
4504 output.change_current_state(
4505 Some(mode),
4506 Some(Transform::Normal),
4507 Some(OutputScale::Fractional(scale)),
4508 Some((x, y).into()),
4509 );
4510 output.set_preferred(mode);
4511 let mut offscreen = None;
4512 if state.use_gpu {
4513 let Some(gbm) = state.gbm_device.as_mut() else {
4514 return false;
4515 };
4516 match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
4517 Ok(bo) => {
4518 let dmabuf = create_dmabuf_from_bo(&bo);
4519 offscreen = Some((bo, dmabuf));
4520 }
4521 Err(e) => {
4522 eprintln!(
4523 "[Wayland] CreateOutput {id}: GBM allocation {width}x{height} failed ({e:?})."
4524 );
4525 return false;
4526 }
4527 }
4528 }
4529 state.space.map_output(&output, (x, y));
4530 let global = output.create_global::<AppState>(&state.dh);
4531 let damage_tracker = OutputDamageTracker::from_output(&output);
4532 if let Some(host) = state.host.as_ref() {
4533 host.set_layout(id, x, y);
4534 }
4535 println!("[Wayland] Output {id} created: {width}x{height} @ ({x}, {y}) scale {scale:.2}.");
4536 state.output_nodes.push(wayland::frontend::OutputNode {
4537 id,
4538 output,
4539 global: Some(global),
4540 owner: None,
4541 view_size: (width, height),
4542 view_scale: 0.0,
4543 pos: (x, y),
4544 damage_tracker,
4545 content_tracker: None,
4546 frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
4547 offscreen_buffer: offscreen,
4548 undrawn_ticks: 0,
4549 overlay_state: OverlayState::default(),
4550 capture: None,
4551 frame_seq: 0,
4552 target_seeded: false,
4553 content_hold_until: None,
4554 });
4555 let mut counts: Vec<(u32, usize)> = Vec::new();
4560 for w in state.space.elements() {
4561 let oid = wayland::frontend::window_output_id(w);
4562 match counts.iter_mut().find(|(o, _)| *o == oid) {
4563 Some((_, c)) => *c += 1,
4564 None => counts.push((oid, 1)),
4565 }
4566 }
4567 let newest = |pred: &dyn Fn(&smithay::desktop::Window) -> bool| {
4568 state
4569 .space
4570 .elements()
4571 .filter(|w| pred(w))
4572 .max_by_key(|w| wayland::frontend::window_meta(w).map(|m| m.id).unwrap_or(0))
4573 .cloned()
4574 };
4575 let parked = |w: &smithay::desktop::Window| {
4576 wayland::frontend::window_meta(w)
4577 .map(|m| m.parked.load(std::sync::atomic::Ordering::Relaxed))
4578 .unwrap_or(false)
4579 };
4580 let adopt = newest(&|w| parked(w) && wayland::frontend::window_output_id(w) == id)
4584 .or_else(|| newest(&parked))
4585 .or_else(|| {
4586 newest(&|w| {
4587 let oid = wayland::frontend::window_output_id(w);
4588 counts.iter().any(|(o, c)| *o == oid && *c >= 2)
4589 })
4590 });
4591 if let Some(window) = adopt {
4592 state.place_window_on_output(&window, id);
4593 crate::log::debug!(
4594 "[Wayland] Output {id}: adopted waiting window {}.",
4595 wayland::frontend::window_meta(&window)
4596 .map(|m| m.id)
4597 .unwrap_or(0)
4598 );
4599 }
4600 true
4601}
4602
4603fn reposition_output_on(state: &mut AppState, id: u32, x: i32, y: i32) -> bool {
4613 let Some(idx) = state.node_idx_for_id(id) else {
4614 return false;
4615 };
4616 let output = state.output_nodes[idx].output.clone();
4617 if state.output_nodes[idx].pos == (x, y) {
4618 return true;
4619 }
4620 if let Some(owner) = state.output_nodes[idx].owner {
4623 let base = state
4624 .node_idx_for_id(owner)
4625 .map(|o| state.output_nodes[o].pos)
4626 .unwrap_or((0, 0));
4627 let mode = output
4628 .current_mode()
4629 .map(|m| (m.size.w, m.size.h))
4630 .unwrap_or((0, 0));
4631 let size = state.output_nodes[idx]
4632 .capture
4633 .as_ref()
4634 .map(|c| (c.settings.width, c.settings.height))
4635 .unwrap_or(state.output_nodes[idx].view_size);
4636 if x < base.0 || y < base.1 || x - base.0 + size.0 > mode.0 || y - base.1 + size.1 > mode.1
4637 {
4638 eprintln!(
4639 "[Wayland] RepositionOutput {id}: rejected, {}x{}+{x}+{y} leaves output \
4640 {owner}'s {}x{}.",
4641 size.0, size.1, mode.0, mode.1
4642 );
4643 return false;
4644 }
4645 state.output_nodes[idx].pos = (x, y);
4646 if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4647 cap.needs_full_render = true;
4648 }
4649 println!("[Wayland] View {id} moved to ({x}, {y}).");
4650 return true;
4651 }
4652 let logical_size = state.output_nodes[idx]
4653 .logical_geometry()
4654 .map(|g| (g.size.w, g.size.h))
4655 .unwrap_or((0, 0));
4656 let physical_size = output
4657 .current_mode()
4658 .map(|m| (m.size.w, m.size.h))
4659 .unwrap_or((0, 0));
4660 if let Some((oid, flavor, other)) = find_output_overlap(
4661 &state.output_nodes,
4662 Some(id),
4663 (x, y, logical_size.0, logical_size.1),
4664 (x, y, physical_size.0, physical_size.1),
4665 ) {
4666 eprintln!(
4667 "[Wayland] RepositionOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
4668 if flavor == "logical" {
4669 logical_size.0
4670 } else {
4671 physical_size.0
4672 },
4673 if flavor == "logical" {
4674 logical_size.1
4675 } else {
4676 physical_size.1
4677 },
4678 other.2,
4679 other.3,
4680 other.0,
4681 other.1,
4682 );
4683 return false;
4684 }
4685 let (old_x, old_y) = state.output_nodes[idx].pos;
4686 state.output_nodes[idx].pos = (x, y);
4687 for view in state
4691 .output_nodes
4692 .iter_mut()
4693 .filter(|n| n.owner == Some(id))
4694 {
4695 view.pos = (view.pos.0 + x - old_x, view.pos.1 + y - old_y);
4696 if let Some(cap) = view.capture.as_mut() {
4697 cap.needs_full_render = true;
4698 }
4699 }
4700 if let Some(host) = state.host.as_ref() {
4701 host.set_layout(id, x, y);
4702 }
4703 output.change_current_state(None, None, None, Some((x, y).into()));
4704 state.space.map_output(&output, (x, y));
4705 let windows: Vec<smithay::desktop::Window> = state
4706 .space
4707 .elements()
4708 .filter(|w| wayland::frontend::window_output_id(w) == id)
4709 .cloned()
4710 .collect();
4711 for window in &windows {
4712 state.space.map_element(window.clone(), (x, y), false);
4713 }
4714 state.refocus_pointer();
4715 if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4716 cap.needs_full_render = true;
4717 }
4718 println!("[Wayland] Output {id} repositioned to ({x}, {y}).");
4719 true
4720}
4721
4722fn destroy_output_on(state: &mut AppState, id: u32) -> bool {
4725 if id == 0 {
4726 return false;
4727 }
4728 let Some(_) = state.node_idx_for_id(id) else {
4729 return false;
4730 };
4731 stop_capture_on_display(state, id);
4732 if let Some(host) = state.host.as_ref() {
4733 host.idle_output(id);
4734 }
4735 wayland_owners().lock().unwrap().remove(&id);
4736 let windows: Vec<smithay::desktop::Window> = state
4738 .space
4739 .elements()
4740 .filter(|w| wayland::frontend::window_output_id(w) == id)
4741 .cloned()
4742 .collect();
4743 for window in &windows {
4744 if state.would_cover_screen(window, 0) {
4748 state.park_window(window, id);
4749 } else {
4750 state.place_window_on_output(window, 0);
4751 }
4752 }
4753 for w in &state.pending_windows {
4754 if let Some(meta) = wayland::frontend::window_meta(w)
4755 && meta.output.load(Ordering::Relaxed) == id
4756 {
4757 meta.output.store(0, Ordering::Relaxed);
4758 }
4759 }
4760 for view in state.view_ids_of(id) {
4764 stop_capture_on_display(state, view);
4765 wayland_owners().lock().unwrap().remove(&view);
4766 if let Some(vidx) = state.node_idx_for_id(view) {
4767 state.output_nodes.remove(vidx);
4768 }
4769 }
4770 let idx = state.node_idx_for_id(id).unwrap();
4771 let node = state.output_nodes.remove(idx);
4772 if let Some(global) = node.global {
4773 state.space.unmap_output(&node.output);
4774 state.dh.remove_global::<AppState>(global);
4775 }
4776 println!(
4777 "[Wayland] Output {id} destroyed; {} window(s) relocated to primary.",
4778 windows.len()
4779 );
4780 true
4781}
4782fn configure_windows_for_mode(
4792 state: &AppState,
4793 display_id: u32,
4794 output: &Output,
4795 width: i32,
4796 height: i32,
4797 scale: f64,
4798) {
4799 let scale = scale.max(0.1);
4800 let logical_width = (width as f64 / scale).round() as i32;
4801 let logical_height = (height as f64 / scale).round() as i32;
4802 for window in state.space.elements() {
4803 if wayland::frontend::window_output_id(window) != display_id {
4804 continue;
4805 }
4806 if wayland::frontend::window_meta(window)
4811 .is_some_and(|meta| meta.parked.load(Ordering::Relaxed))
4812 {
4813 continue;
4814 }
4815 if let Some(surface) = window.wl_surface() {
4816 output.enter(&surface);
4817 with_states(&surface, |states| {
4818 smithay::wayland::compositor::send_surface_state(
4819 &surface,
4820 states,
4821 scale.ceil() as i32,
4822 Transform::Normal,
4823 );
4824 smithay::wayland::fractional_scale::with_fractional_scale(states, |fs| {
4825 fs.set_preferred_scale(scale);
4826 });
4827 });
4828 }
4829 if let Some(toplevel) = window.toplevel() {
4830 toplevel.with_pending_state(|state| {
4831 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
4832 state.states.set(State::Fullscreen);
4833 state.states.set(State::Activated);
4834 state.size = Some((logical_width, logical_height).into());
4835 });
4836 toplevel.send_configure();
4837 }
4838 }
4839}
4840
4841fn resize_output_on(state: &mut AppState, id: u32, width: i32, height: i32, scale: f64) -> bool {
4842 if width <= 0 || height <= 0 || scale <= 0.0 {
4843 return false;
4844 }
4845 let Some(idx) = state.node_idx_for_id(id) else {
4846 return false;
4847 };
4848 if state.output_nodes[idx].owner.is_some() {
4849 eprintln!("[Wayland] ResizeOutput {id}: rejected, {id} is a view of another output.");
4850 return false;
4851 }
4852 let base = state.output_nodes[idx].pos;
4853 for view in state
4856 .output_nodes
4857 .iter()
4858 .filter(|n| n.owner == Some(id) && n.capture.is_some())
4859 {
4860 let (vw, vh) = view
4861 .capture
4862 .as_ref()
4863 .map(|c| (c.settings.width, c.settings.height))
4864 .unwrap_or(view.view_size);
4865 if view.pos.0 - base.0 + vw > width || view.pos.1 - base.1 + vh > height {
4866 eprintln!(
4867 "[Wayland] ResizeOutput {id}: rejected, view {} would fall outside {width}x{height}.",
4868 view.id
4869 );
4870 return false;
4871 }
4872 }
4873 let output = state.output_nodes[idx].output.clone();
4874 let refresh = output.current_mode().map(|m| m.refresh).unwrap_or(60_000);
4875 if output.current_mode().map(|m| (m.size.w, m.size.h)) == Some((width, height))
4876 && (output.current_scale().fractional_scale() - scale).abs() <= 0.001
4877 {
4878 return true;
4879 }
4880 let mut new_offscreen = None;
4884 if state.use_gpu {
4885 let Some(gbm) = state.gbm_device.as_mut() else {
4886 return false;
4887 };
4888 match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
4889 Ok(bo) => {
4890 let dmabuf = create_dmabuf_from_bo(&bo);
4891 new_offscreen = Some((bo, dmabuf));
4892 }
4893 Err(e) => {
4894 eprintln!(
4895 "[Wayland] ResizeOutput {id}: GBM allocation {width}x{height} failed ({e:?})."
4896 );
4897 return false;
4898 }
4899 }
4900 }
4901 let mode = OutputMode {
4902 size: (width, height).into(),
4903 refresh,
4904 };
4905 output.change_current_state(
4906 Some(mode),
4907 Some(Transform::Normal),
4908 Some(OutputScale::Fractional(scale)),
4909 Some(Point::from(base)),
4910 );
4911 output.set_preferred(mode);
4912 state.space.map_output(&output, base);
4913 for cs in state
4916 .copy_sessions
4917 .iter()
4918 .filter(|cs| cs.output.upgrade().as_ref() == Some(&output))
4919 {
4920 if let Some(c) = wayland::frontend::output_capture_constraints(
4921 &output,
4922 state.gles_renderer.as_ref(),
4923 &state.render_node_path,
4924 ) {
4925 cs.session.update_constraints(c);
4926 }
4927 }
4928 let node = &mut state.output_nodes[idx];
4929 node.frame_buffer = vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4];
4930 node.view_size = (width, height);
4931 node.target_seeded = false;
4932 if let Some(off) = new_offscreen.take() {
4933 node.offscreen_buffer = Some(off);
4934 }
4935 for n in state
4936 .output_nodes
4937 .iter_mut()
4938 .filter(|n| n.id == id || n.owner == Some(id))
4939 {
4940 if let Some(cap) = n.capture.as_mut() {
4941 cap.needs_full_render = true;
4942 }
4943 }
4944 configure_windows_for_mode(state, id, &output, width, height, scale);
4945 println!("[Wayland] Output {id} resized to {width}x{height} scale {scale:.2}.");
4946 true
4947}
4948
4949const DEFAULT_VIEW_ID: u32 = 1;
4952
4953fn create_view_on(
4961 state: &mut AppState,
4962 id: u32,
4963 owner: u32,
4964 x: i32,
4965 y: i32,
4966 width: i32,
4967 height: i32,
4968) -> bool {
4969 if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || x < 0 || y < 0 {
4970 return false;
4971 }
4972 let Some(oidx) = state.node_idx_for_id(owner) else {
4973 return false;
4974 };
4975 if state.output_nodes[oidx].owner.is_some() {
4976 eprintln!("[Wayland] CreateView {id}: rejected, output {owner} is itself a view.");
4977 return false;
4978 }
4979 let output = state.output_nodes[oidx].output.clone();
4980 let scale = output.current_scale().fractional_scale();
4981 let Some(mode) = output.current_mode() else {
4982 return false;
4983 };
4984 if x + width > mode.size.w || y + height > mode.size.h {
4985 eprintln!(
4986 "[Wayland] CreateView {id}: rejected, {width}x{height}+{x}+{y} leaves output \
4987 {owner}'s {}x{}.",
4988 mode.size.w, mode.size.h
4989 );
4990 return false;
4991 }
4992 let mut offscreen = None;
4993 if state.use_gpu {
4994 let Some(gbm) = state.gbm_device.as_mut() else {
4995 return false;
4996 };
4997 match alloc_render_target(gbm, width as u32, height as u32, GbmFormat::Argb8888) {
4998 Ok(bo) => {
4999 let dmabuf = create_dmabuf_from_bo(&bo);
5000 offscreen = Some((bo, dmabuf));
5001 }
5002 Err(e) => {
5003 eprintln!(
5004 "[Wayland] CreateView {id}: GBM allocation {width}x{height} failed ({e:?})."
5005 );
5006 return false;
5007 }
5008 }
5009 }
5010 let pos = state.output_nodes[oidx].pos;
5011 let origin = (pos.0 + x, pos.1 + y);
5012 let damage_tracker = OutputDamageTracker::new((width, height), scale, Transform::Normal);
5015 println!("[Wayland] View {id} created over output {owner}: {width}x{height} @ ({x}, {y}).");
5016 state.output_nodes.push(wayland::frontend::OutputNode {
5017 id,
5018 output,
5019 global: None,
5020 owner: Some(owner),
5021 view_size: (width, height),
5022 view_scale: 0.0,
5023 pos: origin,
5024 damage_tracker,
5025 content_tracker: None,
5026 frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
5027 offscreen_buffer: offscreen,
5028 undrawn_ticks: 0,
5029 overlay_state: OverlayState::default(),
5030 capture: None,
5031 frame_seq: 0,
5032 target_seeded: false,
5033 content_hold_until: None,
5034 });
5035 true
5036}
5037
5038struct WaylandThreadConfig {
5041 command_rx: smithay::reexports::calloop::channel::Channel<ThreadCommand>,
5042 wake_rx: smithay::reexports::calloop::channel::Channel<()>,
5043 command_tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
5044 initial_width: i32,
5045 initial_height: i32,
5046 explicit_dri_node: String,
5047 auto_gpu_selected: bool,
5048 cursor_size: i32,
5049 pointer_share: Arc<crate::wayland::frontend::PointerShare>,
5050}
5051
5052fn gpu_render_init(
5058 device_path: &std::path::Path,
5059) -> Result<(RawGbmDevice<File>, GlesRenderer), String> {
5060 let file = File::options()
5061 .read(true)
5062 .write(true)
5063 .open(device_path)
5064 .map_err(|e| format!("Failed to open render device: {}", e))?;
5065 let file_for_alloc = file
5066 .try_clone()
5067 .map_err(|e| format!("Failed to clone file for GBM Allocator: {}", e))?;
5068 let gbm_allocator =
5069 RawGbmDevice::new(file_for_alloc).map_err(|_| "Failed to create Raw GBM Device")?;
5070 let gbm = GbmDevice::new(file).map_err(|_| "Failed to create GBM device")?;
5071 let egl = unsafe { EGLDisplay::new(gbm) }.map_err(|_| "Failed to create EGL display")?;
5072 let context = EGLContext::new(&egl).map_err(|_| "Failed to create EGL context")?;
5073 let renderer =
5074 unsafe { GlesRenderer::new(context) }.map_err(|_| "Failed to init GlesRenderer")?;
5075 Ok((gbm_allocator, renderer))
5076}
5077
5078fn gl_renderer_name(renderer: &mut GlesRenderer) -> String {
5082 renderer
5083 .with_context(|gl| unsafe {
5084 let ptr = gl.GetString(smithay::backend::renderer::gles::ffi::RENDERER);
5085 if ptr.is_null() {
5086 String::new()
5087 } else {
5088 std::ffi::CStr::from_ptr(ptr as *const std::ffi::c_char)
5089 .to_string_lossy()
5090 .into_owned()
5091 }
5092 })
5093 .unwrap_or_default()
5094}
5095
5096fn node_gpu_name(node_index: i32) -> String {
5100 static NAMES: OnceLock<Mutex<std::collections::HashMap<i32, String>>> = OnceLock::new();
5101 let mut names = NAMES.get_or_init(Default::default).lock().unwrap();
5102 names
5103 .entry(node_index)
5104 .or_insert_with(|| {
5105 let path = format!("/dev/dri/renderD{}", 128 + node_index);
5106 gpu_render_init(std::path::Path::new(&path))
5107 .map(|(_gbm, mut renderer)| gl_renderer_name(&mut renderer))
5108 .unwrap_or_default()
5109 })
5110 .clone()
5111}
5112
5113fn gpu_exposed() -> bool {
5119 std::path::Path::new("/dev/nvidiactl").exists()
5120 || std::fs::read_dir("/dev/dri")
5121 .into_iter()
5122 .flatten()
5123 .flatten()
5124 .any(|e| {
5125 let name = e.file_name();
5126 let name = name.to_string_lossy();
5127 name.starts_with("renderD") || name.starts_with("card")
5128 })
5129}
5130
5131fn run_wayland_thread(cfg: WaylandThreadConfig) {
5174 let WaylandThreadConfig {
5175 command_rx,
5176 wake_rx,
5177 command_tx,
5178 initial_width,
5179 initial_height,
5180 explicit_dri_node,
5181 auto_gpu_selected,
5182 cursor_size,
5183 pointer_share,
5184 } = cfg;
5185 let width: i32 = if initial_width > 0 {
5186 initial_width
5187 } else {
5188 1024
5189 };
5190 let height: i32 = if initial_height > 0 {
5191 initial_height
5192 } else {
5193 768
5194 };
5195
5196 let mut event_loop = match EventLoop::<AppState>::try_new() {
5197 Ok(l) => l,
5198 Err(e) => {
5199 eprintln!("[Wayland] compositor thread aborting: event loop init failed: {e}");
5200 return;
5201 }
5202 };
5203 let display: Display<AppState> = match Display::new() {
5204 Ok(d) => d,
5205 Err(e) => {
5206 eprintln!("[Wayland] compositor thread aborting: display init failed: {e}");
5207 return;
5208 }
5209 };
5210 let dh: DisplayHandle = display.handle();
5211 unsafe {
5212 if let Ok(lib) = libloading::Library::new("libwayland-server.so.0") {
5213 if let Ok(set_max) = lib.get::<unsafe extern "C" fn(*mut std::ffi::c_void, usize)>(
5214 b"wl_display_set_default_max_buffer_size\0",
5215 ) {
5216 set_max(
5217 dh.backend_handle().display_ptr() as *mut std::ffi::c_void,
5218 10 * 1024 * 1024,
5219 );
5220 }
5221 std::mem::forget(lib);
5222 }
5223 }
5224
5225 let dri_node = explicit_dri_node;
5226
5227 let mut use_gpu = !dri_node.is_empty();
5228 let render_node_path = dri_node.clone();
5229
5230 let mut gles_renderer = None;
5231 let mut pixman_renderer = None;
5232 let mut offscreen_buffer: Option<(BufferObject<()>, Dmabuf)> = None;
5233 let mut dmabuf_global = None;
5234 let mut gbm_device_raw = None;
5235 let mut dmabuf_state = DmabufState::new();
5236
5237 let mut gpu_success = false;
5238 if use_gpu {
5239 println!("[Wayland] Renderer: GL on {dri_node}.");
5240 let init_res: Result<(), String> = (|| {
5241 let device_path = std::path::Path::new(&dri_node);
5242 let (gbm_allocator, mut renderer) = gpu_render_init(device_path)?;
5243
5244 if let Err(e) = renderer.bind_wl_display(&dh) {
5245 crate::log::debug!(
5246 "[Wayland] EGL did not bind to the Wayland display (optional): {:?}",
5247 e
5248 );
5249 }
5250
5251 let formats = Bind::<Dmabuf>::supported_formats(&renderer)
5252 .ok_or("Failed to query formats")?
5253 .into_iter()
5254 .collect::<Vec<_>>();
5255
5256 let node = DrmNode::from_path(device_path).map_err(|_| "Failed to create DrmNode")?;
5257 let dmabuf_default_feedback =
5258 DmabufFeedbackBuilder::new(node.dev_id(), formats.clone()).build();
5259
5260 dmabuf_global = Some(if let Ok(default_feedback) = dmabuf_default_feedback {
5261 dmabuf_state.create_global_with_default_feedback::<AppState>(&dh, &default_feedback)
5262 } else {
5263 dmabuf_state.create_global::<AppState>(&dh, formats)
5264 });
5265
5266 let bo = alloc_render_target(
5267 &gbm_allocator,
5268 width as u32,
5269 height as u32,
5270 GbmFormat::Argb8888,
5271 )
5272 .map_err(|_| "Failed to allocate GBM buffer")?;
5273
5274 let dmabuf = create_dmabuf_from_bo(&bo);
5275 offscreen_buffer = Some((bo, dmabuf));
5276 gbm_device_raw = Some(gbm_allocator);
5277 gles_renderer = Some(renderer);
5278 Ok(())
5279 })();
5280
5281 match init_res {
5282 Ok(_) => {
5283 gpu_success = true;
5284 report::set_renderer(report::Renderer {
5285 kind: "gl",
5286 node: dri_node.clone(),
5287 gpu: gles_renderer
5288 .as_mut()
5289 .map(gl_renderer_name)
5290 .unwrap_or_default(),
5291 reason: String::new(),
5292 });
5293 }
5294 Err(e) => {
5295 eprintln!(
5296 "[Wayland] GPU renderer failed to initialize ({e}); rendering in software (Pixman)."
5297 );
5298 report::set_renderer(report::Renderer {
5299 kind: "pixman",
5300 node: String::new(),
5301 gpu: String::new(),
5302 reason: format!("the GPU renderer did not initialize on {dri_node}: {e}"),
5303 });
5304 use_gpu = false;
5305 }
5306 }
5307 }
5308
5309 if !gpu_success {
5310 if dri_node.is_empty() {
5311 println!("[Wayland] Renderer: software (Pixman), no render node.");
5312 report::set_renderer(report::Renderer {
5313 kind: "pixman",
5314 node: String::new(),
5315 gpu: String::new(),
5316 reason: "no render node".to_string(),
5317 });
5318 }
5319 pixman_renderer = Some(PixmanRenderer::new().expect("Failed to init PixmanRenderer"));
5320 use_gpu = false;
5321 }
5322
5323 let compositor_state = CompositorState::new_v6::<AppState>(&dh);
5324 let image_capture_source_state = ImageCaptureSourceState::new();
5325 let output_capture_source_state = OutputCaptureSourceState::new::<AppState>(&dh);
5326 let image_copy_capture_state = ImageCopyCaptureState::new::<AppState>(&dh);
5327 let fractional_scale_state = FractionalScaleManagerState::new::<AppState>(&dh);
5328 let shm_state = ShmState::new::<AppState>(&dh, vec![]);
5329 let output_state = OutputManagerState::new_with_xdg_output::<AppState>(&dh);
5330 let mut seat_state = SeatState::new();
5331 let shell_state = XdgShellState::new::<AppState>(&dh);
5332 let space = Space::default();
5333 let layer_shell_state = WlrLayerShellState::new::<AppState>(&dh);
5334 let data_device_state = DataDeviceState::new::<AppState>(&dh);
5335 let data_control_state = DataControlState::new::<AppState, _>(&dh, None, |_| true);
5336 let ext_data_control_state = ExtDataControlState::new::<AppState, _>(&dh, None, |_| true);
5337 let cursor_shape_state = CursorShapeManagerState::new::<AppState>(&dh);
5338 let _vk_global = dh.create_global::<AppState, ZwpVirtualKeyboardManagerV1, _>(1, ());
5339 let pointer_warp_state = PointerWarpManager::new::<AppState>(&dh);
5340 let relative_pointer_state = RelativePointerManagerState::new::<AppState>(&dh);
5341 let pointer_constraints_state = PointerConstraintsState::new::<AppState>(&dh);
5342
5343 let foreign_toplevel_list = ForeignToplevelListState::new::<AppState>(&dh);
5344 let xdg_decoration_state = XdgDecorationState::new::<AppState>(&dh);
5345 let single_pixel_buffer = SinglePixelBufferState::new::<AppState>(&dh);
5346 let viewporter_state = ViewporterState::new::<AppState>(&dh);
5347 let presentation_state = PresentationState::new::<AppState>(&dh, 1);
5348 let xdg_activation_state = XdgActivationState::new::<AppState>(&dh);
5349 let primary_selection_state = PrimarySelectionState::new::<AppState>(&dh);
5350 let popups = PopupManager::default();
5351
5352 let mut seat = seat_state.new_wl_seat(&dh, "seat0");
5353 seat.add_keyboard(XkbConfig::default(), 200, 25)
5354 .expect("Failed to init keyboard");
5355 seat.add_pointer();
5356
5357 let (host_frame_tx, host_frame_rx) = smithay::reexports::calloop::channel::channel::<usize>();
5358
5359 let mut state = AppState {
5360 compositor_state,
5361 fractional_scale_state,
5362 viewporter_state,
5363 presentation_state,
5364 shm_state,
5365 single_pixel_buffer,
5366 dmabuf_state,
5367 dmabuf_global,
5368 ext_data_control_state,
5369 cursor_shape_state,
5370 image_capture_source_state,
5371 output_capture_source_state,
5372 image_copy_capture_state,
5373 copy_sessions: Vec::new(),
5374 output_state,
5375 seat_state,
5376 shell_state,
5377 layer_shell_state,
5378 space,
5379 data_device_state,
5380 data_control_state,
5381 dh: dh.clone(),
5382 seat,
5383 pointer_warp_state,
5384 relative_pointer_state,
5385 pointer_constraints_state,
5386 output_nodes: Vec::new(),
5387 pending_windows: Vec::new(),
5388 foreign_toplevel_list,
5389 xdg_decoration_state,
5390 xdg_activation_state,
5391 primary_selection_state,
5392 popups,
5393 gles_renderer,
5394 pixman_renderer,
5395 gbm_device: gbm_device_raw,
5396 settings: RustCaptureSettings {
5397 width,
5398 height,
5399 ..RustCaptureSettings::default()
5400 },
5401 cursor_callback_set: false,
5402 cursor_tx: wayland::cursor::spawn_cursor_worker(
5403 cursor_size,
5404 RustCaptureSettings::default().cursor_size_cap,
5405 ),
5406 clipboard_callback: None,
5407 pending_clipboard_read: None,
5408 clipboard_generation: Arc::new(std::sync::atomic::AtomicU64::new(0)),
5409 current_selection_mimes: Vec::new(),
5410 last_log_time: Instant::now(),
5411 start_time: Instant::now(),
5412 clock: Clock::new(),
5413 use_gpu,
5414 cursor_helper: Cursor::load(cursor_size),
5415 keymap_policy: wayland::keymap::KeymapPolicy::seat(),
5416 host: None,
5417 host_layout_pending: std::collections::HashMap::new(),
5418 host_frame_tx,
5419 host_mode_refusals: std::collections::HashMap::new(),
5420 current_cursor_icon: None,
5421 cursor_surface_pending: false,
5422 cursor_buffer: None,
5423 render_cursor_on_framebuffer: false,
5424 render_node_path,
5425 auto_gpu_selected,
5426 pending_screenshot: None,
5427 command_rx: None,
5428 last_input_at: None,
5429 pointer_motion_at: None,
5430 pointer_share,
5431 motion_applied: 0,
5432 input_interval: None,
5433 frame_idle_long: false,
5434 last_idle_service_at: None,
5435 deliver_reaper: Vec::new(),
5436 encode_reaper: Vec::new(),
5437 };
5438 {
5441 let initial_keymap = if let Some(kb) = state.seat.get_keyboard() {
5442 kb.with_xkb_state(&mut state, |context| match context.xkb().lock() {
5443 Ok(guard) => {
5444 let keymap = unsafe { guard.keymap() };
5445 keymap.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
5446 }
5447 Err(_) => String::new(),
5448 })
5449 } else {
5450 String::new()
5451 };
5452 state.keymap_policy.rebuild_base(initial_keymap);
5453 }
5454
5455 let output = Output::new(
5456 "HEADLESS-1".into(),
5457 PhysicalProperties {
5458 size: (width, height).into(),
5459 subpixel: Subpixel::Unknown,
5460 make: "Pixelflux".into(),
5461 model: "Virtual".into(),
5462 serial_number: "001".into(),
5463 },
5464 );
5465 output.change_current_state(
5466 Some(OutputMode {
5467 size: (width, height).into(),
5468 refresh: 60_000,
5469 }),
5470 Some(Transform::Normal),
5471 Some(OutputScale::Fractional(1.0)),
5472 Some((0, 0).into()),
5473 );
5474 output.set_preferred(OutputMode {
5475 size: (width, height).into(),
5476 refresh: 60_000,
5477 });
5478 state.space.map_output(&output, (0, 0));
5479 let global = output.create_global::<AppState>(&dh);
5480 let damage_tracker = OutputDamageTracker::from_output(&output);
5481 state.output_nodes.push(wayland::frontend::OutputNode {
5482 id: 0,
5483 output,
5484 global: Some(global),
5485 owner: None,
5486 view_size: (width, height),
5487 view_scale: 0.0,
5488 pos: (0, 0),
5489 damage_tracker,
5490 content_tracker: None,
5491 frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
5492 offscreen_buffer,
5493 undrawn_ticks: 0,
5494 overlay_state: OverlayState::default(),
5495 capture: None,
5496 frame_seq: 0,
5497 target_seeded: false,
5498 content_hold_until: None,
5499 });
5500 create_view_on(&mut state, DEFAULT_VIEW_ID, 0, 0, 0, width, height);
5504
5505 fn note_pointer_motion(state: &mut AppState) {
5513 let now = Instant::now();
5514 if let Some(prev) = state.pointer_motion_at {
5515 let cap = frame_period(state.settings.target_fps) * 4;
5516 let dt = now.saturating_duration_since(prev).min(cap);
5517 state.input_interval = Some(match state.input_interval {
5518 None => dt,
5519 Some(avg) => avg.mul_f64(0.875) + dt.mul_f64(0.125),
5520 });
5521 }
5522 state.pointer_motion_at = Some(now);
5523 }
5524
5525 fn drain_thread_commands(state: &mut AppState) -> bool {
5526 let Some(rx) = state.command_rx.take() else {
5527 return false;
5528 };
5529 let mut had_input = false;
5530 while let Ok(cmd) = rx.try_recv() {
5531 had_input |= matches!(
5532 cmd,
5533 ThreadCommand::KeyboardKey { .. }
5534 | ThreadCommand::KeyboardKeys { .. }
5535 | ThreadCommand::PointerMotion { .. }
5536 | ThreadCommand::PointerRelativeMotion { .. }
5537 | ThreadCommand::PointerButton { .. }
5538 | ThreadCommand::PointerAxis { .. }
5539 | ThreadCommand::PointerScroll { .. }
5540 | ThreadCommand::PointerScrollEnd
5541 );
5542 if matches!(
5543 cmd,
5544 ThreadCommand::PointerMotion { .. } | ThreadCommand::PointerRelativeMotion { .. }
5545 ) {
5546 note_pointer_motion(state);
5547 }
5548 handle_thread_command(state, cmd);
5549 }
5550 if had_input {
5551 state.last_input_at = Some(Instant::now());
5552 }
5553 state.command_rx = Some(rx);
5554 had_input
5555 }
5556
5557 fn send_idle_frame_callbacks(state: &mut AppState) {
5564 let time = state.clock.now();
5565 for node in state.output_nodes.iter().filter(|n| n.owner.is_none()) {
5566 let mut feedback = OutputPresentationFeedback::new(&node.output);
5567 for window in state.space.elements_for_output(&node.output) {
5568 window.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
5569 Some(node.output.clone())
5570 });
5571 window.take_presentation_feedback(
5572 &mut feedback,
5573 |_, _| Some(node.output.clone()),
5574 |_, _| wp_presentation_feedback::Kind::empty(),
5575 );
5576 }
5577 for layer in layer_map_for_output(&node.output).layers() {
5578 layer.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
5579 Some(node.output.clone())
5580 });
5581 layer.take_presentation_feedback(
5582 &mut feedback,
5583 |_, _| Some(node.output.clone()),
5584 |_, _| wp_presentation_feedback::Kind::empty(),
5585 );
5586 }
5587 feedback.discarded();
5588 }
5589 if let Some(output) = state.primary_output() {
5590 send_cursor_frame(state, output, time);
5591 }
5592 if let Some(renderer) = state.gles_renderer.as_mut() {
5593 let _ = renderer.cleanup_texture_cache();
5594 }
5595 }
5596
5597 fn handle_thread_command(state: &mut AppState, cmd: ThreadCommand) {
5598 match cmd {
5599 ThreadCommand::StartCapture {
5600 display_id,
5601 callback,
5602 settings,
5603 } => {
5604 start_capture_on_display(state, display_id, callback.map(Arc::new), settings);
5605 }
5606 ThreadCommand::StopCapture { display_id } => {
5607 stop_capture_on_display(state, display_id);
5612 }
5613 ThreadCommand::CreateOutput {
5614 id,
5615 width,
5616 height,
5617 x,
5618 y,
5619 scale,
5620 reply,
5621 } => {
5622 let _ = reply.send(create_output_on(state, id, width, height, x, y, scale));
5623 }
5624 ThreadCommand::CreateView {
5625 id,
5626 owner,
5627 x,
5628 y,
5629 width,
5630 height,
5631 reply,
5632 } => {
5633 let _ = reply.send(create_view_on(state, id, owner, x, y, width, height));
5634 }
5635 ThreadCommand::ResizeOutput {
5636 id,
5637 width,
5638 height,
5639 scale,
5640 reply,
5641 } => {
5642 let _ = reply.send(resize_output_on(state, id, width, height, scale));
5643 }
5644 ThreadCommand::DestroyOutput { id, reply } => {
5645 let _ = reply.send(destroy_output_on(state, id));
5646 }
5647 ThreadCommand::OutputCapacity { reply } => {
5648 let _ = reply.send(state.host.as_ref().map_or(-1, |h| h.output_count() as i64));
5649 }
5650 ThreadCommand::RepositionOutput { id, x, y, reply } => {
5651 let _ = reply.send(reposition_output_on(state, id, x, y));
5652 }
5653 ThreadCommand::ListOutputs { reply } => {
5654 let list = state
5655 .output_nodes
5656 .iter()
5657 .map(|n| {
5658 let (w, h) = if n.owner.is_some() {
5661 n.capture
5662 .as_ref()
5663 .map(|c| (c.settings.width, c.settings.height))
5664 .unwrap_or(n.view_size)
5665 } else {
5666 n.output
5667 .current_mode()
5668 .map(|m| (m.size.w, m.size.h))
5669 .unwrap_or((0, 0))
5670 };
5671 (
5672 n.id,
5673 n.pos.0,
5674 n.pos.1,
5675 w,
5676 h,
5677 n.output.current_scale().fractional_scale(),
5678 n.capture.is_some(),
5679 )
5680 })
5681 .collect();
5682 let _ = reply.send(list);
5683 }
5684 ThreadCommand::MoveWindowToOutput {
5685 window_id,
5686 output_id,
5687 reply,
5688 } => {
5689 let window = state
5690 .space
5691 .elements()
5692 .find(|w| {
5693 wayland::frontend::window_meta(w)
5694 .map(|m| m.id == window_id)
5695 .unwrap_or(false)
5696 })
5697 .cloned();
5698 let ok = match window {
5699 Some(w) => state.place_window_on_output(&w, output_id),
5700 None => false,
5701 };
5702 let _ = reply.send(ok);
5703 }
5704 ThreadCommand::ListWindows { reply } => {
5705 use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
5706 let mut list = Vec::new();
5707 for window in state.space.elements() {
5708 let Some(meta) = wayland::frontend::window_meta(window) else {
5709 continue;
5710 };
5711 let (title, app_id) = window
5712 .toplevel()
5713 .map(|tl| {
5714 with_states(tl.wl_surface(), |states| {
5715 states
5716 .data_map
5717 .get::<XdgToplevelSurfaceData>()
5718 .map(|d| {
5719 let a = d.lock().unwrap();
5720 (
5721 a.title.clone().unwrap_or_default(),
5722 a.app_id.clone().unwrap_or_default(),
5723 )
5724 })
5725 .unwrap_or_default()
5726 })
5727 })
5728 .unwrap_or_default();
5729 list.push((
5730 meta.id,
5731 title,
5732 app_id,
5733 meta.output.load(Ordering::Relaxed),
5734 meta.parked.load(Ordering::Relaxed),
5735 ));
5736 }
5737 let _ = reply.send(list);
5738 }
5739 ThreadCommand::SetClipboardCallback(cb) => {
5740 state.clipboard_callback = Some(cb);
5741 state.pending_clipboard_read = (!state.current_selection_mimes.is_empty())
5746 .then(|| state.current_selection_mimes.clone());
5747 }
5748 ThreadCommand::SetClipboard { entries } => {
5749 let mut mimes: Vec<String> = Vec::new();
5751 for (mime, _) in &entries {
5752 if !mime.starts_with("text/plain") {
5753 mimes.push(mime.clone());
5754 } else if !mimes.iter().any(|m| m == "TEXT") {
5755 mimes.extend(
5756 [
5757 "text/plain;charset=utf-8",
5758 "UTF8_STRING",
5759 "text/plain",
5760 "STRING",
5761 "TEXT",
5762 ]
5763 .iter()
5764 .map(|s| s.to_string()),
5765 );
5766 }
5767 }
5768 let payload = std::sync::Arc::new(entries);
5769 smithay::wayland::selection::data_device::set_data_device_selection(
5770 &state.dh,
5771 &state.seat,
5772 mimes.clone(),
5773 payload.clone(),
5774 );
5775 smithay::wayland::selection::primary_selection::set_primary_selection(
5778 &state.dh,
5779 &state.seat,
5780 mimes,
5781 payload,
5782 );
5783 state.current_selection_mimes.clear();
5786 }
5787 ThreadCommand::SetCursorCallback(cb) => {
5788 state.cursor_callback_set = cb.is_some();
5789 let _ = state.cursor_tx.send(CursorJob::SetCallback(cb));
5790 if state.cursor_callback_set {
5791 let icon = state
5795 .current_cursor_icon
5796 .clone()
5797 .unwrap_or(CursorImageStatus::Named(Default::default()));
5798 state.send_cursor_image(&icon);
5799 }
5800 }
5801 ThreadCommand::KeyboardKeys { events } => {
5802 for (scancode, key_state_val) in events {
5803 if let Some(host) = state.host.as_ref() {
5804 host.key(scancode, key_state_val > 0);
5805 continue;
5806 }
5807 let key_state = if key_state_val > 0 {
5808 KeyState::Pressed
5809 } else {
5810 KeyState::Released
5811 };
5812 let serial = next_serial();
5813 let time = wayland_time();
5814 if let Some(keyboard) = state.seat.get_keyboard() {
5815 keyboard.input(
5816 state,
5817 Keycode::new(scancode),
5818 key_state,
5819 serial,
5820 time,
5821 |_, _, _| FilterResult::<()>::Forward,
5822 );
5823 }
5824 }
5825 }
5826 ThreadCommand::KeyboardKey {
5827 scancode,
5828 state: key_state_val,
5829 } => {
5830 if let Some(host) = state.host.as_ref() {
5831 host.key(scancode, key_state_val > 0);
5832 return;
5833 }
5834 let key_state = if key_state_val > 0 {
5835 KeyState::Pressed
5836 } else {
5837 KeyState::Released
5838 };
5839 let serial = next_serial();
5840 let time = wayland_time();
5841 if let Some(keyboard) = state.seat.get_keyboard() {
5842 keyboard.input(
5843 state,
5844 Keycode::new(scancode),
5845 key_state,
5846 serial,
5847 time,
5848 |_, _, _| FilterResult::<()>::Forward,
5849 );
5850 }
5851 }
5852 ThreadCommand::SetKeymapString(text) => {
5853 if state.keymap_policy.rebuild_base(text) {
5856 state.apply_keymap_policy();
5857 } else {
5858 eprintln!(
5859 "[Wayland] set_keymap_string: keymap failed to compile; keeping current keymap."
5860 );
5861 }
5862 }
5863 ThreadCommand::SetXkbLayout {
5864 rules,
5865 model,
5866 layout,
5867 variant,
5868 options,
5869 reply,
5870 } => {
5871 match crate::wayland::keymap::compile_rmlvo(
5872 &rules, &model, &layout, &variant, &options,
5873 ) {
5874 Some(text) => {
5875 state.keymap_policy.rebuild_base(text);
5876 state.apply_keymap_policy();
5877 let _ = reply.send(true);
5878 }
5879 None => {
5880 eprintln!(
5881 "[Wayland] set_xkb_layout: RMLVO ({rules:?}, {model:?}, {layout:?}, {variant:?}, {options:?}) failed to compile."
5882 );
5883 let _ = reply.send(false);
5884 }
5885 }
5886 }
5887 ThreadCommand::BindKeysyms { keysyms, reply } => {
5888 let _ = reply.send(state.bind_keysyms(&keysyms));
5889 }
5890 ThreadCommand::SetKeymapOverlay { binds } => {
5891 if state.keymap_policy.has_base() {
5892 state.keymap_policy.set_manual_overlay(&binds);
5893 state.apply_keymap_policy();
5894 } else {
5895 eprintln!("[Wayland] set_keymap_overlay: no base keymap to splice onto.");
5896 }
5897 }
5898 ThreadCommand::GetKeyboardState { reply } => {
5899 let (pressed, mods) = state
5900 .seat
5901 .get_keyboard()
5902 .map(|kb| {
5903 let pressed: Vec<u32> = kb.pressed_keys().iter().map(|c| c.raw()).collect();
5904 let m = kb.modifier_state();
5905 let mask = (m.ctrl as u32)
5906 | (m.shift as u32) << 1
5907 | (m.alt as u32) << 2
5908 | (m.logo as u32) << 3
5909 | (m.caps_lock as u32) << 4
5910 | (m.num_lock as u32) << 5
5911 | (m.iso_level3_shift as u32) << 6
5912 | (m.iso_level5_shift as u32) << 7;
5913 (pressed, mask)
5914 })
5915 .unwrap_or_default();
5916 let _ = reply.send((pressed, mods));
5917 }
5918 ThreadCommand::Barrier { reply } => {
5919 for join in state.encode_reaper.drain(..) {
5927 let _ = join.join();
5928 }
5929 for join in state.deliver_reaper.drain(..) {
5930 let _ = join.join();
5931 }
5932 let _ = reply.send(());
5933 }
5934 ThreadCommand::GetXkbKeymap { reply } => {
5935 let mut keymap_str = String::new();
5936 if let Some(keyboard) = state.seat.get_keyboard() {
5937 keymap_str =
5938 keyboard.with_xkb_state(state, |context| match context.xkb().lock() {
5939 Ok(guard) => {
5940 let keymap = unsafe { guard.keymap() };
5941 keymap.get_as_string(
5942 smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1,
5943 )
5944 }
5945 Err(_) => String::new(),
5946 });
5947 }
5948 let _ = reply.send(keymap_str);
5949 }
5950 ThreadCommand::PointerMotion { x, y, motion } => {
5951 state.motion_applied = state.motion_applied.max(motion);
5952 if let Some(host) = state.host.as_ref() {
5953 host.pointer_motion_abs(x, y);
5954 return;
5955 }
5956 let serial = next_serial();
5957 let time = wayland_time();
5958 let p = state.layout_physical_to_logical(x, y);
5963
5964 if let Some(pointer) = state.seat.get_pointer() {
5965 let under = state.pointer_target_under(p);
5966
5967 state.release_grab_across_screens(&pointer, &under, serial, time);
5968 let entered = pointer.current_focus() != under.as_ref().map(|(t, _)| t.clone());
5969 pointer.motion(
5970 state,
5971 under.clone(),
5972 &MotionEvent {
5973 location: p,
5974 serial,
5975 time,
5976 },
5977 );
5978 if entered && under.is_some() {
5983 pointer.motion(
5984 state,
5985 under.clone(),
5986 &MotionEvent {
5987 location: p,
5988 serial: next_serial(),
5989 time,
5990 },
5991 );
5992 }
5993 pointer.frame(state);
5994 state.activate_constraint_under(&pointer, &under, p);
5995 }
5996 }
5997 ThreadCommand::PointerRelativeMotion { dx, dy, motion } => {
5998 state.motion_applied = state.motion_applied.max(motion);
5999 if let Some(host) = state.host.as_ref() {
6000 host.pointer_motion_rel(dx, dy);
6001 return;
6002 }
6003 let via_fake_input = crate::wayland::ficlient::pointer_motion_rel(dx, dy);
6011 let time = wayland_time();
6012 let serial = next_serial();
6013
6014 if let Some(pointer) = state.seat.get_pointer() {
6015 let current_pos = pointer.current_location();
6016 let event = RelativeMotionEvent {
6017 time,
6018 delta: (dx, dy).into(),
6019 delta_unaccel: (dx, dy).into(),
6020 };
6021 let resting = state
6022 .space
6023 .element_under(current_pos)
6024 .map(|(window, loc)| (FocusTarget::Window(window.clone()), loc.to_f64()));
6025 if state.pointer_locked(&pointer, &resting, current_pos) {
6029 if !via_fake_input {
6030 pointer.relative_motion(state, resting, &event);
6031 }
6032 pointer.frame(state);
6033 return;
6034 }
6035 let new_pos =
6036 state.clamp_logical((current_pos.x + dx, current_pos.y + dy).into());
6037
6038 let under = state
6039 .space
6040 .element_under(new_pos)
6041 .map(|(window, loc)| (FocusTarget::Window(window.clone()), loc.to_f64()));
6042
6043 state.release_grab_across_screens(&pointer, &under, serial, time);
6044 let entered = pointer.current_focus() != under.as_ref().map(|(t, _)| t.clone());
6045 pointer.motion(
6046 state,
6047 under.clone(),
6048 &MotionEvent {
6049 location: new_pos,
6050 serial,
6051 time,
6052 },
6053 );
6054 if entered && under.is_some() {
6057 pointer.motion(
6058 state,
6059 under.clone(),
6060 &MotionEvent {
6061 location: new_pos,
6062 serial: next_serial(),
6063 time,
6064 },
6065 );
6066 }
6067
6068 if !via_fake_input {
6069 pointer.relative_motion(state, under.clone(), &event);
6070 }
6071
6072 pointer.frame(state);
6073 state.activate_constraint_under(&pointer, &under, new_pos);
6074 }
6075 }
6076 ThreadCommand::PointerButton {
6077 btn,
6078 state: btn_state_val,
6079 } => {
6080 if let Some(host) = state.host.as_ref() {
6081 host.pointer_button(btn, btn_state_val > 0);
6082 return;
6083 }
6084 let serial = next_serial();
6085 let time = wayland_time();
6086 let button_state = if btn_state_val > 0 {
6087 smithay::backend::input::ButtonState::Pressed
6088 } else {
6089 smithay::backend::input::ButtonState::Released
6090 };
6091
6092 state.refocus_pointer();
6093 if let Some(pointer) = state.seat.get_pointer() {
6094 if button_state == smithay::backend::input::ButtonState::Pressed {
6095 let pos = pointer.current_location();
6096 let target_window = state.space.element_under(pos).map(|(w, _)| w.clone());
6097
6098 if let Some(window) = target_window {
6099 state.space.raise_element(&window, true);
6100 if let Some(keyboard) = state.seat.get_keyboard() {
6101 keyboard.set_focus(
6102 state,
6103 Some(FocusTarget::Window(window)),
6104 serial,
6105 );
6106 }
6107 }
6108 }
6109 let button = btn;
6110 pointer.button(
6111 state,
6112 &ButtonEvent {
6113 button,
6114 state: button_state,
6115 serial,
6116 time,
6117 },
6118 );
6119 pointer.frame(state);
6120 }
6121 }
6122 ThreadCommand::PointerAxis { x, y } => {
6123 if let Some(host) = state.host.as_ref() {
6124 host.pointer_axis(x, y);
6125 return;
6126 }
6127 let time = wayland_time();
6128 state.refocus_pointer();
6129 if let Some(pointer) = state.seat.get_pointer() {
6130 let mut frame = AxisFrame::new(time).source(AxisSource::Wheel);
6131
6132 if x != 0.0 {
6133 frame = frame
6134 .value(Axis::Horizontal, x)
6135 .v120(Axis::Horizontal, (x * SCROLL_V120_PER_UNIT) as i32);
6136 }
6137
6138 if y != 0.0 {
6139 frame = frame
6140 .value(Axis::Vertical, y)
6141 .v120(Axis::Vertical, (y * SCROLL_V120_PER_UNIT) as i32);
6142 }
6143
6144 if x != 0.0 || y != 0.0 {
6145 pointer.axis(state, frame);
6146 pointer.frame(state);
6147 }
6148 }
6149 }
6150 ThreadCommand::PointerScroll { dx, dy } => {
6151 if let Some(host) = state.host.as_ref() {
6152 host.pointer_scroll(dx, dy);
6153 return;
6154 }
6155 let time = wayland_time();
6156 state.refocus_pointer();
6157 if let Some(pointer) = state.seat.get_pointer()
6158 && (dx != 0.0 || dy != 0.0)
6159 {
6160 let (_, _, scale) =
6161 state.layout_logical_to_physical_scaled(pointer.current_location());
6162 let mut frame = AxisFrame::new(time).source(AxisSource::Finger);
6163 if dx != 0.0 {
6164 frame = frame.value(Axis::Horizontal, dx / scale);
6165 }
6166 if dy != 0.0 {
6167 frame = frame.value(Axis::Vertical, dy / scale);
6168 }
6169 pointer.axis(state, frame);
6170 pointer.frame(state);
6171 }
6172 }
6173 ThreadCommand::PointerScrollEnd => {
6174 if let Some(host) = state.host.as_ref() {
6175 host.pointer_scroll_end();
6176 return;
6177 }
6178 let time = wayland_time();
6179 if let Some(pointer) = state.seat.get_pointer() {
6180 let frame = AxisFrame::new(time)
6181 .source(AxisSource::Finger)
6182 .stop(Axis::Horizontal)
6183 .stop(Axis::Vertical);
6184 pointer.axis(state, frame);
6185 pointer.frame(state);
6186 }
6187 }
6188 ThreadCommand::UpdateCursorConfig {
6189 render_on_framebuffer,
6190 } => {
6191 state.render_cursor_on_framebuffer = render_on_framebuffer;
6192 if let Some(host) = state.host.as_ref() {
6193 host.set_cursor_painting(render_on_framebuffer);
6194 }
6195 }
6196 ThreadCommand::SetCursorSize { size, reply } => {
6197 if size <= 0 {
6198 let _ = reply.send(false);
6199 } else {
6200 state.cursor_helper = Cursor::load(size);
6201 let _ = state.cursor_tx.send(CursorJob::SetSize(size));
6202 for node in state.output_nodes.iter_mut() {
6205 if let Some(cap) = node.capture.as_mut() {
6206 cap.needs_full_render = true;
6207 }
6208 }
6209 let _ = reply.send(true);
6210 }
6211 }
6212 ThreadCommand::RequestIdr { display_id } => {
6213 if let Some(idx) = state.node_idx_for_id(display_id)
6214 && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6215 {
6216 cap.request_idr();
6217 }
6218 }
6219 ThreadCommand::InvalidateReference {
6220 display_id,
6221 frame_id,
6222 } => {
6223 if let Some(idx) = state.node_idx_for_id(display_id)
6224 && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6225 {
6226 cap.invalidate_reference(frame_id);
6227 }
6228 }
6229 ThreadCommand::AcknowledgeReference {
6230 display_id,
6231 frame_id,
6232 held,
6233 } => {
6234 if let Some(idx) = state.node_idx_for_id(display_id)
6235 && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6236 {
6237 cap.acknowledge_reference(frame_id, held);
6238 }
6239 }
6240 ThreadCommand::UpdateRate {
6241 display_id,
6242 bitrate_kbps,
6243 vbv_multiplier,
6244 fps,
6245 } => {
6246 if let Some(idx) = state.node_idx_for_id(display_id)
6247 && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6248 {
6249 if let Some(b) = bitrate_kbps {
6250 cap.settings.video_bitrate_kbps = b;
6251 }
6252 if let Some(v) = vbv_multiplier {
6253 cap.settings.video_vbv_multiplier = v;
6254 }
6255 if let Some(f) = fps
6256 && f > 0.0
6257 {
6258 cap.settings.target_fps = f;
6259 if let Some(host) = state.host.as_ref() {
6260 host.set_fps(display_id, f);
6261 }
6262 }
6263 if let Some(enc) = cap.video_encoder.as_mut()
6264 && let Err(e) = enc.reconfigure_rate(&cap.settings)
6265 {
6266 eprintln!("[Wayland] rate reconfigure failed: {e}");
6270 cap.hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
6271 }
6272 let c = &cap.encode_controls;
6273 c.bitrate_kbps
6274 .store(cap.settings.video_bitrate_kbps, Ordering::Relaxed);
6275 c.vbv_mult_milli.store(
6276 (cap.settings.video_vbv_multiplier * 1000.0).round() as i32,
6277 Ordering::Relaxed,
6278 );
6279 c.fps_bits.store(
6280 cap.settings.target_fps.max(1.0).to_bits(),
6281 Ordering::Relaxed,
6282 );
6283 c.rate_dirty.store(true, Ordering::Release);
6284 if display_id == 0 {
6285 state.settings.video_bitrate_kbps = cap.settings.video_bitrate_kbps;
6286 state.settings.video_vbv_multiplier = cap.settings.video_vbv_multiplier;
6287 state.settings.target_fps = cap.settings.target_fps;
6288 }
6289 }
6290 }
6291 ThreadCommand::UpdateTunables {
6292 display_id,
6293 tunables: t,
6294 } => {
6295 state.render_cursor_on_framebuffer = t.capture_cursor;
6296 if let Some(host) = state.host.as_ref() {
6297 host.set_cursor_painting(t.capture_cursor);
6298 }
6299 let _ = state
6300 .cursor_tx
6301 .send(CursorJob::SetSizeCap(t.cursor_size_cap));
6302 if display_id == 0 {
6303 t.apply_to(&mut state.settings);
6304 }
6305 if let Some(idx) = state.node_idx_for_id(display_id)
6306 && let Some(cap) = state.output_nodes[idx].capture.as_mut()
6307 {
6308 t.apply_to(&mut cap.settings);
6309 *cap.encode_controls.tunables.lock().unwrap() = Some(t);
6310 cap.encode_controls
6311 .tunables_dirty
6312 .store(true, Ordering::Release);
6313 }
6314 }
6315 ThreadCommand::CuScreenshot { display_id, resp } => {
6316 if state.node_idx_for_id(display_id).is_some() {
6317 state.pending_screenshot = Some((display_id, resp));
6318 } else {
6319 let _ = resp.send(Err(format!("Unknown display: {display_id}")));
6320 }
6321 }
6322 ThreadCommand::CuCursorPosition { resp } => {
6323 let pos = state
6324 .seat
6325 .get_pointer()
6326 .map(|p| p.current_location())
6327 .unwrap_or_else(|| (0.0f64, 0.0f64).into());
6328 let _ = resp.send(state.layout_logical_to_physical(pos));
6329 }
6330 ThreadCommand::CuGetInfo { display_id, resp } => {
6331 match state.host_layout_pending.get_mut(&display_id) {
6335 Some(p) => p.geometry_waiters.push(resp),
6336 None => {
6337 let _ = resp.send(realized_geometry(state, display_id));
6338 }
6339 }
6340 }
6341 }
6342 }
6343
6344 state.command_rx = Some(command_rx);
6345 event_loop
6346 .handle()
6347 .insert_source(wake_rx, |_, _, state| {
6348 let had_input = drain_thread_commands(state);
6349 if state.frame_idle_long
6354 && state
6355 .last_input_at
6356 .is_some_and(|t| t.elapsed() < Duration::from_millis(50))
6357 && state
6358 .last_idle_service_at
6359 .is_none_or(|t| t.elapsed() >= Duration::from_millis(16))
6360 {
6361 state.last_idle_service_at = Some(Instant::now());
6362 send_idle_frame_callbacks(state);
6363 }
6364 if had_input
6368 && state.host.is_none()
6369 && state.output_nodes.iter().any(|n| n.capture.is_some())
6370 {
6371 state.space.refresh();
6372 render_pass(state, TickTrigger::Input);
6373 }
6374 })
6375 .unwrap();
6376
6377 event_loop
6378 .handle()
6379 .insert_source(host_frame_rx, |event, _, state| {
6380 let smithay::reexports::calloop::channel::Event::Msg(index) = event else {
6384 return;
6385 };
6386 let Some(id) = state
6387 .host
6388 .as_ref()
6389 .filter(|h| h.has_queued_frame(index))
6390 .and_then(|h| h.display_for_output(index))
6391 else {
6392 return;
6393 };
6394 state.space.refresh();
6395 render_display(state, id, TickTrigger::HostFrame);
6396 })
6397 .unwrap();
6398
6399 let source = match ListeningSocketSource::new_auto() {
6400 Ok(s) => s,
6401 Err(e) => {
6402 eprintln!(
6403 "[Wayland] compositor thread aborting: could not bind a wayland-N socket (XDG_RUNTIME_DIR unset/full?): {e}"
6404 );
6405 return;
6406 }
6407 };
6408 let socket_name = source.socket_name().to_string_lossy().into_owned();
6409 println!("[Wayland] Socket listening on: {:?}", socket_name);
6410 unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) };
6416 publish_socket_name(&socket_name);
6417
6418 event_loop
6419 .handle()
6420 .insert_source(source, |client_stream, _, state| {
6421 if let Err(err) = state
6422 .dh
6423 .insert_client(client_stream, Arc::new(ClientState::default()))
6424 {
6425 eprintln!("Error adding wayland client: {:?}", err);
6426 }
6427 })
6428 .expect("Failed to init wayland socket source");
6429
6430 let timer = Timer::immediate();
6431 event_loop
6432 .handle()
6433 .insert_source(timer, move |_, _, state| {
6434 reap_dead_host(state);
6437 drain_thread_commands(state);
6438 reconcile_host_layouts(state);
6439 let mut i = 0;
6444 while i < state.deliver_reaper.len() {
6445 if state.deliver_reaper[i].is_finished() {
6446 let _ = state.deliver_reaper.swap_remove(i).join();
6447 } else {
6448 i += 1;
6449 }
6450 }
6451 let mut i = 0;
6452 while i < state.encode_reaper.len() {
6453 if state.encode_reaper[i].is_finished() {
6454 let _ = state.encode_reaper.swap_remove(i).join();
6455 } else {
6456 i += 1;
6457 }
6458 }
6459 let loop_start_time = Instant::now();
6460 state.space.refresh();
6461
6462 let now = Instant::now();
6463 let elapsed = now.duration_since(state.last_log_time).as_secs_f64();
6464 if elapsed >= 1.0 {
6465 let mut mem: Option<(usize, u64)> = None;
6468 for node in &state.output_nodes {
6469 let Some(cap) = node.capture.as_ref() else { continue };
6470 let frames = cap.encode_stats.frames.swap(0, Ordering::Relaxed);
6471 let stripes = cap.encode_stats.stripes.swap(0, Ordering::Relaxed);
6472 if crate::log::debug_enabled() {
6473 let actual_fps = frames as f64 / elapsed;
6474 let stripes_per_sec = stripes as f64 / elapsed;
6475 let mode_str = cap.encode_stats.desc.lock().unwrap().clone();
6476 let n_stripes = cap.encode_stats.n_stripes.load(Ordering::Relaxed);
6477 let (current_rss, shm_usage) = *mem
6478 .get_or_insert_with(|| (get_process_rss_bytes(), get_shm_usage_bytes()));
6479
6480 println!("Display: {} Res: {}x{} Mode: {} Stripes: {} EncFPS: {:.2} EncStripes/s: {:.2} Mem: {}MB SHM: {}MB",
6481 node.id, cap.settings.width, cap.settings.height, mode_str, n_stripes, actual_fps, stripes_per_sec, current_rss / 1024 / 1024, shm_usage / 1024 / 1024);
6482 }
6483 }
6484 state.last_log_time = now;
6485 }
6486
6487 let any_capturing = state.output_nodes.iter().any(|n| n.capture.is_some());
6488 let any_copy_frame = state.copy_sessions.iter().any(|cs| cs.pending.is_some());
6489 if !any_capturing && state.pending_screenshot.is_none() && !any_copy_frame {
6490 state.last_idle_service_at = Some(Instant::now());
6501 send_idle_frame_callbacks(state);
6502 let post_input = state
6510 .last_input_at
6511 .is_some_and(|t| t.elapsed() < Duration::from_secs(1));
6512 let idle = if !state.copy_sessions.is_empty() || post_input {
6513 Duration::from_millis(16)
6514 } else {
6515 IDLE_FRAME_INTERVAL
6516 };
6517 state.frame_idle_long = idle == IDLE_FRAME_INTERVAL;
6518 return TimeoutAction::ToDuration(idle);
6519 }
6520 state.frame_idle_long = false;
6521
6522 if render_pass(state, TickTrigger::Timer) {
6523 return TimeoutAction::ToDuration(Duration::from_millis(1));
6524 }
6525 let now = Instant::now();
6529 let next_due = state
6530 .output_nodes
6531 .iter()
6532 .filter_map(|n| n.capture.as_ref())
6533 .map(|cap| cap.pace.next_due(capture_period(cap), now))
6534 .min()
6535 .unwrap_or(loop_start_time + frame_period(state.settings.target_fps));
6536 let wait = next_due.saturating_duration_since(now);
6537 TimeoutAction::ToDuration(if wait.as_millis() < 1 { Duration::from_millis(1) } else { wait })
6538 })
6539 .expect("Failed to init capture timer");
6540
6541 event_loop
6542 .handle()
6543 .insert_source(
6544 Generic::new(display, Interest::READ, Mode::Level),
6545 |_, display, _state| {
6546 if let Err(e) = unsafe { display.get_mut().dispatch_clients(_state) } {
6549 eprintln!("[Wayland] client dispatch error: {e:?}");
6550 }
6551 Ok(PostAction::Continue)
6552 },
6553 )
6554 .unwrap();
6555
6556 crate::computer_use::register_wayland_backend(command_tx.clone());
6557 crate::computer_use::spawn_cu_from_env();
6558 crate::wayland::ficlient::arm(crate::computer_use::app_wayland_socket_path());
6559
6560 let _ = event_loop.run(None, &mut state, |state| {
6561 state.process_pending_clipboard_read();
6562 state.flush_pending_cursor();
6563 if state.host.is_none()
6564 && let Some(pointer) = state.seat.get_pointer()
6565 {
6566 let (x, y, scale) = state.layout_logical_to_physical_scaled(pointer.current_location());
6567 state
6568 .pointer_share
6569 .publish(crate::wayland::frontend::PointerAt {
6570 x,
6571 y,
6572 scale,
6573 motion: state.motion_applied,
6574 });
6575 }
6576 let _ = state.dh.flush_clients();
6577 });
6578}
6579
6580#[pyclass]
6587struct StripeFrame {
6588 data: Arc<Vec<u8>>,
6589 #[pyo3(get, set)]
6590 data_type: i32,
6591 #[pyo3(get, set)]
6592 stripe_y_start: i32,
6593 #[pyo3(get, set)]
6594 stripe_height: i32,
6595 #[pyo3(get, set)]
6596 frame_id: i32,
6597 #[pyo3(get)]
6598 capture_ns: i64,
6599 #[pyo3(get)]
6600 encode_start_ns: i64,
6601 #[pyo3(get)]
6602 encode_end_ns: i64,
6603 #[pyo3(get)]
6606 reference_frame_id: i32,
6607}
6608
6609impl StripeFrame {
6610 fn new_owned_meta(
6614 data: Arc<Vec<u8>>,
6615 data_type: i32,
6616 stripe_y_start: i32,
6617 stripe_height: i32,
6618 frame_id: i32,
6619 timing: FrameTiming,
6620 reference: Reference,
6621 ) -> Self {
6622 Self {
6623 data,
6624 data_type,
6625 stripe_y_start,
6626 stripe_height,
6627 frame_id,
6628 capture_ns: timing.capture_ns,
6629 encode_start_ns: timing.encode_start_ns,
6630 encode_end_ns: timing.encode_end_ns,
6631 reference_frame_id: reference.frame_id(),
6632 }
6633 }
6634}
6635
6636#[pymethods]
6637impl StripeFrame {
6638 #[new]
6641 #[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
6642 fn new(
6643 data: Vec<u8>,
6644 data_type: i32,
6645 stripe_y_start: i32,
6646 stripe_height: i32,
6647 frame_id: i32,
6648 ) -> Self {
6649 Self::new_owned_meta(
6650 Arc::new(data),
6651 data_type,
6652 stripe_y_start,
6653 stripe_height,
6654 frame_id,
6655 FrameTiming::default(),
6656 Reference::Untracked,
6657 )
6658 }
6659
6660 fn __len__(&self) -> usize {
6661 self.data.len()
6662 }
6663
6664 unsafe fn __getbuffer__(
6669 slf: PyRef<'_, Self>,
6670 view: *mut pyo3::ffi::Py_buffer,
6671 flags: std::os::raw::c_int,
6672 ) -> PyResult<()> {
6673 let r = unsafe {
6674 pyo3::ffi::PyBuffer_FillInfo(
6675 view,
6676 slf.as_ptr(),
6677 slf.data.as_ptr() as *mut std::os::raw::c_void,
6678 slf.data.len() as pyo3::ffi::Py_ssize_t,
6679 1,
6680 flags,
6681 )
6682 };
6683 if r != 0 {
6684 return Err(PyErr::fetch(slf.py()));
6685 }
6686 Ok(())
6687 }
6688
6689 unsafe fn __releasebuffer__(&self, _view: *mut pyo3::ffi::Py_buffer) {}
6690}
6691
6692#[pyclass]
6698struct WaylandBackend {
6699 tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
6700 pointer: Arc<crate::wayland::frontend::PointerShare>,
6702 motions: AtomicU64,
6704 wake_tx: smithay::reexports::calloop::channel::Sender<()>,
6708}
6709
6710impl WaylandBackend {
6711 fn send(&self, cmd: ThreadCommand) -> Result<(), String> {
6712 self.tx.send(cmd).map_err(|e| e.to_string())?;
6713 let _ = self.wake_tx.send(());
6714 Ok(())
6715 }
6716}
6717
6718#[pymethods]
6719impl WaylandBackend {
6720 #[new]
6725 #[pyo3(signature = (width, height, dri_node, auto_gpu_selected = false, cursor_size = -1))]
6726 fn new(
6727 width: i32,
6728 height: i32,
6729 dri_node: String,
6730 auto_gpu_selected: bool,
6731 cursor_size: i32,
6732 ) -> Self {
6733 let (tx, rx) = smithay::reexports::calloop::channel::channel();
6734 let (wake_tx, wake_rx) = smithay::reexports::calloop::channel::channel();
6735 let cu_tx = tx.clone();
6736 let pointer = Arc::new(crate::wayland::frontend::PointerShare::default());
6737 let pointer_share = pointer.clone();
6738 thread::spawn(move || {
6739 crate::boost_thread_priority(-15);
6740 run_wayland_thread(WaylandThreadConfig {
6741 command_rx: rx,
6742 wake_rx,
6743 command_tx: cu_tx,
6744 initial_width: width,
6745 initial_height: height,
6746 explicit_dri_node: dri_node,
6747 auto_gpu_selected,
6748 cursor_size,
6749 pointer_share,
6750 });
6751 });
6752 WaylandBackend {
6753 tx,
6754 wake_tx,
6755 pointer,
6756 motions: AtomicU64::new(0),
6757 }
6758 }
6759
6760 fn pointer_location(&self) -> Option<(f64, f64, f64, u64)> {
6769 self.pointer
6770 .read()
6771 .map(|at| (at.x, at.y, at.scale, at.motion))
6772 }
6773
6774 fn start_capture(&self, callback: Py<PyAny>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
6781 let rust_settings = extract_settings(settings)?;
6782 let display_id = read_display_id(settings);
6783
6784 PY_SHUTDOWN.store(false, Ordering::Relaxed);
6785 self.send(ThreadCommand::StartCapture {
6786 display_id,
6787 callback: Some(callback),
6788 settings: rust_settings,
6789 })
6790 .map_err(|e| {
6791 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6792 "Failed to send start command: {}",
6793 e
6794 ))
6795 })?;
6796 Ok(())
6797 }
6798
6799 #[pyo3(signature = (display_id = 0))]
6801 fn stop_capture(&self, display_id: u32) -> PyResult<()> {
6802 self.send(ThreadCommand::StopCapture { display_id })
6803 .map_err(|e| {
6804 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6805 "Failed to send stop command: {}",
6806 e
6807 ))
6808 })?;
6809 Ok(())
6810 }
6811
6812 #[allow(clippy::too_many_arguments)]
6820 #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
6821 fn create_output(
6822 &self,
6823 py: Python<'_>,
6824 id: u32,
6825 width: i32,
6826 height: i32,
6827 x: i32,
6828 y: i32,
6829 scale: f64,
6830 ) -> PyResult<bool> {
6831 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6832 self.send(ThreadCommand::CreateOutput {
6833 id,
6834 width,
6835 height,
6836 x,
6837 y,
6838 scale,
6839 reply: reply_tx,
6840 })
6841 .map_err(|e| {
6842 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6843 "Failed to create output: {}",
6844 e
6845 ))
6846 })?;
6847 Ok(py
6848 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6849 .unwrap_or(false))
6850 }
6851
6852 #[allow(clippy::too_many_arguments)]
6861 #[pyo3(signature = (id, owner, x, y, width, height))]
6862 fn create_view(
6863 &self,
6864 py: Python<'_>,
6865 id: u32,
6866 owner: u32,
6867 x: i32,
6868 y: i32,
6869 width: i32,
6870 height: i32,
6871 ) -> PyResult<bool> {
6872 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6873 self.send(ThreadCommand::CreateView {
6874 id,
6875 owner,
6876 x,
6877 y,
6878 width,
6879 height,
6880 reply: reply_tx,
6881 })
6882 .map_err(|e| {
6883 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6884 "Failed to create view: {}",
6885 e
6886 ))
6887 })?;
6888 Ok(py
6889 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6890 .unwrap_or(false))
6891 }
6892
6893 #[pyo3(signature = (id, width, height, scale = 1.0))]
6897 fn resize_output(
6898 &self,
6899 py: Python<'_>,
6900 id: u32,
6901 width: i32,
6902 height: i32,
6903 scale: f64,
6904 ) -> PyResult<bool> {
6905 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6906 self.send(ThreadCommand::ResizeOutput {
6907 id,
6908 width,
6909 height,
6910 scale,
6911 reply: reply_tx,
6912 })
6913 .map_err(|e| {
6914 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6915 "Failed to resize output: {}",
6916 e
6917 ))
6918 })?;
6919 Ok(py
6920 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6921 .unwrap_or(false))
6922 }
6923
6924 fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
6927 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6928 self.send(ThreadCommand::DestroyOutput {
6929 id,
6930 reply: reply_tx,
6931 })
6932 .map_err(|e| {
6933 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6934 "Failed to destroy output: {}",
6935 e
6936 ))
6937 })?;
6938 Ok(py
6939 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6940 .unwrap_or(false))
6941 }
6942
6943 fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
6949 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
6950 self.send(ThreadCommand::RepositionOutput {
6951 id,
6952 x,
6953 y,
6954 reply: reply_tx,
6955 })
6956 .map_err(|e| {
6957 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6958 "Failed to reposition output: {}",
6959 e
6960 ))
6961 })?;
6962 Ok(py
6963 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6964 .unwrap_or(false))
6965 }
6966
6967 fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
6970 let (reply_tx, reply_rx) = std::sync::mpsc::channel();
6971 self.send(ThreadCommand::ListOutputs { reply: reply_tx })
6972 .map_err(|e| {
6973 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6974 "Failed to list outputs: {}",
6975 e
6976 ))
6977 })?;
6978 Ok(py
6979 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6980 .unwrap_or_default())
6981 }
6982
6983 fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
6988 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<i64>();
6989 self.send(ThreadCommand::OutputCapacity { reply: reply_tx })
6990 .map_err(|e| {
6991 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
6992 "Failed to query output capacity: {}",
6993 e
6994 ))
6995 })?;
6996 Ok(py
6997 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
6998 .unwrap_or(0))
6999 }
7000
7001 fn move_window_to_output(
7004 &self,
7005 py: Python<'_>,
7006 window_id: u32,
7007 output_id: u32,
7008 ) -> PyResult<bool> {
7009 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
7010 self.send(ThreadCommand::MoveWindowToOutput {
7011 window_id,
7012 output_id,
7013 reply: reply_tx,
7014 })
7015 .map_err(|e| {
7016 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7017 "Failed to move window: {}",
7018 e
7019 ))
7020 })?;
7021 Ok(py
7022 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7023 .unwrap_or(false))
7024 }
7025
7026 fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
7030 let (reply_tx, reply_rx) = std::sync::mpsc::channel();
7031 self.send(ThreadCommand::ListWindows { reply: reply_tx })
7032 .map_err(|e| {
7033 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7034 "Failed to list windows: {}",
7035 e
7036 ))
7037 })?;
7038 Ok(py
7039 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7040 .unwrap_or_default())
7041 }
7042
7043 fn set_cursor_callback(&self, callback: Option<Py<PyAny>>) -> PyResult<()> {
7045 self.send(ThreadCommand::SetCursorCallback(callback))
7046 .map_err(|e| {
7047 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7048 "Failed to set cursor callback: {}",
7049 e
7050 ))
7051 })?;
7052 Ok(())
7053 }
7054
7055 fn clear_cursor_callback(&self) -> PyResult<()> {
7056 self.set_cursor_callback(None)
7057 }
7058
7059 fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
7063 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
7064 self.send(ThreadCommand::SetCursorSize {
7065 size,
7066 reply: reply_tx,
7067 })
7068 .map_err(|e| {
7069 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7070 "Failed to set cursor size: {}",
7071 e
7072 ))
7073 })?;
7074 Ok(py
7075 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7076 .unwrap_or(false))
7077 }
7078
7079 fn set_clipboard_callback(&self, callback: Py<PyAny>) -> PyResult<()> {
7084 self.send(ThreadCommand::SetClipboardCallback(callback))
7085 .map_err(|e| {
7086 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7087 "Failed to set clipboard callback: {}",
7088 e
7089 ))
7090 })?;
7091 Ok(())
7092 }
7093
7094 fn set_clipboard(&self, entries: Vec<(String, Vec<u8>)>) -> PyResult<()> {
7099 self.send(ThreadCommand::SetClipboard { entries })
7100 .map_err(|e| {
7101 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7102 "Failed to set clipboard: {}",
7103 e
7104 ))
7105 })?;
7106 Ok(())
7107 }
7108
7109 fn inject_key(&self, scancode: u32, state: u32) -> PyResult<()> {
7110 self.send(ThreadCommand::KeyboardKey { scancode, state })
7111 .map_err(|e| {
7112 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7113 "Failed to inject key: {}",
7114 e
7115 ))
7116 })?;
7117 Ok(())
7118 }
7119
7120 fn inject_keys(&self, events: Vec<(u32, u32)>) -> PyResult<()> {
7123 if events.is_empty() {
7124 return Ok(());
7125 }
7126 self.send(ThreadCommand::KeyboardKeys { events })
7127 .map_err(|e| {
7128 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7129 "Failed to inject keys: {}",
7130 e
7131 ))
7132 })?;
7133 Ok(())
7134 }
7135
7136 fn set_keymap_overlay(&self, binds: Vec<(u32, u32)>) -> PyResult<()> {
7146 self.send(ThreadCommand::SetKeymapOverlay { binds })
7147 .map_err(|e| {
7148 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7149 "Failed to set overlay: {}",
7150 e
7151 ))
7152 })?;
7153 Ok(())
7154 }
7155
7156 fn set_keymap_string(&self, text: String) -> PyResult<()> {
7157 self.send(ThreadCommand::SetKeymapString(text))
7158 .map_err(|e| {
7159 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7160 "Failed to set keymap: {}",
7161 e
7162 ))
7163 })?;
7164 Ok(())
7165 }
7166
7167 fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
7174 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<String>();
7175 self.send(ThreadCommand::GetXkbKeymap { reply: reply_tx })
7176 .map_err(|e| {
7177 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7178 "Failed to request keymap: {}",
7179 e
7180 ))
7181 })?;
7182 let result = py.detach(move || reply_rx.recv_timeout(Duration::from_secs(2)));
7183 match result {
7184 Ok(s) => Ok(s),
7185 Err(_) => Ok(String::new()),
7186 }
7187 }
7188
7189 fn inject_mouse_move(&self, x: f64, y: f64) -> PyResult<u64> {
7192 let motion = self.motions.fetch_add(1, Ordering::Relaxed) + 1;
7193 self.send(ThreadCommand::PointerMotion { x, y, motion })
7194 .map_err(|e| {
7195 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7196 "Failed to inject motion: {}",
7197 e
7198 ))
7199 })?;
7200 Ok(motion)
7201 }
7202
7203 fn inject_relative_mouse_move(&self, dx: f64, dy: f64) -> PyResult<u64> {
7209 let motion = self.motions.fetch_add(1, Ordering::Relaxed) + 1;
7210 self.send(ThreadCommand::PointerRelativeMotion { dx, dy, motion })
7211 .map_err(|e| {
7212 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7213 "Failed to inject relative motion: {}",
7214 e
7215 ))
7216 })?;
7217 Ok(motion)
7218 }
7219
7220 fn inject_mouse_button(&self, btn: u32, state: u32) -> PyResult<()> {
7221 self.send(ThreadCommand::PointerButton { btn, state })
7222 .map_err(|e| {
7223 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7224 "Failed to inject button: {}",
7225 e
7226 ))
7227 })?;
7228 Ok(())
7229 }
7230
7231 fn inject_mouse_scroll(&self, x: f64, y: f64) -> PyResult<()> {
7232 self.send(ThreadCommand::PointerAxis { x, y })
7233 .map_err(|e| {
7234 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7235 "Failed to inject axis: {}",
7236 e
7237 ))
7238 })?;
7239 Ok(())
7240 }
7241
7242 fn inject_finger_scroll(&self, dx: f64, dy: f64) -> PyResult<()> {
7245 self.send(ThreadCommand::PointerScroll { dx, dy })
7246 .map_err(|e| {
7247 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7248 "Failed to inject finger scroll: {}",
7249 e
7250 ))
7251 })?;
7252 Ok(())
7253 }
7254
7255 fn inject_finger_scroll_end(&self) -> PyResult<()> {
7257 self.send(ThreadCommand::PointerScrollEnd).map_err(|e| {
7258 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7259 "Failed to end finger scroll: {}",
7260 e
7261 ))
7262 })?;
7263 Ok(())
7264 }
7265
7266 fn set_cursor_rendering(&self, enabled: bool) -> PyResult<()> {
7267 self.send(ThreadCommand::UpdateCursorConfig {
7268 render_on_framebuffer: enabled,
7269 })
7270 .map_err(|e| {
7271 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7272 "Failed to set cursor config: {}",
7273 e
7274 ))
7275 })?;
7276 Ok(())
7277 }
7278
7279 #[pyo3(signature = (display_id = 0))]
7284 fn request_idr_frame(&self, display_id: u32) -> PyResult<()> {
7285 self.send(ThreadCommand::RequestIdr { display_id })
7286 .map_err(|e| {
7287 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7288 "Failed to request IDR: {}",
7289 e
7290 ))
7291 })?;
7292 Ok(())
7293 }
7294
7295 #[pyo3(signature = (frame_id, display_id = 0))]
7299 fn invalidate_reference(&self, frame_id: u16, display_id: u32) -> PyResult<()> {
7300 self.send(ThreadCommand::InvalidateReference {
7301 display_id,
7302 frame_id,
7303 })
7304 .map_err(|e| {
7305 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7306 "Failed to invalidate reference: {}",
7307 e
7308 ))
7309 })
7310 }
7311
7312 #[pyo3(signature = (frame_id, display_id = 0, held = true))]
7317 fn acknowledge_reference(&self, frame_id: u16, display_id: u32, held: bool) -> PyResult<()> {
7318 self.send(ThreadCommand::AcknowledgeReference {
7319 display_id,
7320 frame_id,
7321 held,
7322 })
7323 .map_err(|e| {
7324 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7325 "Failed to acknowledge reference: {}",
7326 e
7327 ))
7328 })
7329 }
7330
7331 #[pyo3(signature = (bitrate_kbps = None, vbv_multiplier = None, fps = None, display_id = 0))]
7334 fn update_rate(
7335 &self,
7336 bitrate_kbps: Option<i32>,
7337 vbv_multiplier: Option<f64>,
7338 fps: Option<f64>,
7339 display_id: u32,
7340 ) -> PyResult<()> {
7341 self.send(ThreadCommand::UpdateRate {
7342 display_id,
7343 bitrate_kbps,
7344 vbv_multiplier,
7345 fps,
7346 })
7347 .map_err(|e| {
7348 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7349 "Failed to update rate: {}",
7350 e
7351 ))
7352 })?;
7353 Ok(())
7354 }
7355
7356 #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
7360 fn set_xkb_layout(
7361 &self,
7362 py: Python<'_>,
7363 layout: String,
7364 variant: String,
7365 options: String,
7366 model: String,
7367 rules: String,
7368 ) -> PyResult<bool> {
7369 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
7370 self.send(ThreadCommand::SetXkbLayout {
7371 rules,
7372 model,
7373 layout,
7374 variant,
7375 options,
7376 reply: reply_tx,
7377 })
7378 .map_err(|e| {
7379 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7380 "Failed to set layout: {}",
7381 e
7382 ))
7383 })?;
7384 Ok(py
7385 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7386 .unwrap_or(false))
7387 }
7388
7389 fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
7392 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(Vec<u32>, u32)>();
7393 self.send(ThreadCommand::GetKeyboardState { reply: reply_tx })
7394 .map_err(|e| {
7395 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7396 "Failed to read keyboard state: {}",
7397 e
7398 ))
7399 })?;
7400 Ok(py
7401 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
7402 .unwrap_or_default())
7403 }
7404
7405 #[pyo3(signature = (display_id = 0))]
7416 fn get_realized_geometry(
7417 &self,
7418 py: Python<'_>,
7419 display_id: u32,
7420 ) -> PyResult<Option<(i32, i32, f64)>> {
7421 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(i32, i32, f64)>();
7422 self.send(ThreadCommand::CuGetInfo {
7423 display_id,
7424 resp: reply_tx,
7425 })
7426 .map_err(|e| {
7427 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7428 "Failed to read geometry: {}",
7429 e
7430 ))
7431 })?;
7432 Ok(py
7433 .detach(move || reply_rx.recv_timeout(GEOMETRY_BARRIER_TIMEOUT))
7434 .ok())
7435 }
7436
7437 #[pyo3(signature = (display_id = 0))]
7444 fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
7445 wayland_capture_state(display_id)
7446 }
7447}
7448
7449const GEOMETRY_BARRIER_TIMEOUT: Duration = Duration::from_secs(6);
7454
7455fn wayland_capture_state(display_id: u32) -> (String, Option<String>) {
7458 let running = wayland_alive().lock().unwrap().contains(&display_id);
7459 let last_error = wayland_capture_err()
7460 .lock()
7461 .unwrap()
7462 .get(&display_id)
7463 .cloned();
7464 let state = if running {
7465 "running"
7466 } else if last_error.is_some() {
7467 "failed"
7468 } else {
7469 "idle"
7470 };
7471 (state.to_string(), last_error)
7472}
7473
7474impl WaylandBackend {
7475 fn update_tunables(&self, display_id: u32, t: LiveTunables) -> PyResult<()> {
7476 self.send(ThreadCommand::UpdateTunables {
7477 display_id,
7478 tunables: t,
7479 })
7480 .map_err(|e| {
7481 PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
7482 "Failed to update tunables: {}",
7483 e
7484 ))
7485 })?;
7486 Ok(())
7487 }
7488}
7489
7490fn read_display_id(settings: &Bound<'_, PyAny>) -> u32 {
7493 settings
7494 .getattr("display_id")
7495 .ok()
7496 .and_then(|v| v.extract::<u32>().ok())
7497 .unwrap_or(0)
7498}
7499
7500use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
7501use std::sync::{Condvar, Mutex, OnceLock};
7502
7503use crate::encoders::software::{EncodedStripe, FrameTiming};
7504
7505#[pyfunction]
7511#[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
7512fn stripe_frame_from_buffer(
7513 data: Vec<u8>,
7514 data_type: i32,
7515 stripe_y_start: i32,
7516 stripe_height: i32,
7517 frame_id: i32,
7518) -> StripeFrame {
7519 StripeFrame::new_owned_meta(
7520 Arc::new(data),
7521 data_type,
7522 stripe_y_start,
7523 stripe_height,
7524 frame_id,
7525 FrameTiming::default(),
7526 Reference::Untracked,
7527 )
7528}
7529
7530#[pyclass(dict)]
7534struct CaptureSettings {
7535 #[pyo3(get, set)]
7537 display_id: u32,
7538 #[pyo3(get, set)]
7539 capture_width: i32,
7540 #[pyo3(get, set)]
7541 capture_height: i32,
7542 #[pyo3(get, set)]
7543 scale: f64,
7544 #[pyo3(get, set)]
7545 capture_x: i32,
7546 #[pyo3(get, set)]
7547 capture_y: i32,
7548 #[pyo3(get, set)]
7549 target_fps: f64,
7550 #[pyo3(get, set)]
7551 jpeg_quality: i32,
7552 #[pyo3(get, set)]
7553 paint_over_jpeg_quality: i32,
7554 #[pyo3(get, set)]
7555 use_paint_over_quality: bool,
7556 #[pyo3(get, set)]
7557 paint_over_trigger_frames: i32,
7558 #[pyo3(get, set)]
7559 damage_block_threshold: i32,
7560 #[pyo3(get, set)]
7561 damage_block_duration: i32,
7562 #[pyo3(get, set)]
7565 codec: String,
7566 #[pyo3(get, set)]
7567 video_crf: i32,
7568 #[pyo3(get, set)]
7569 video_paintover_crf: i32,
7570 #[pyo3(get, set)]
7571 video_paintover_burst_frames: i32,
7572 #[pyo3(get, set)]
7573 video_fullcolor: bool,
7574 #[pyo3(get, set)]
7578 video_bit_depth: i32,
7579 #[pyo3(get, set)]
7582 video_fullframe: bool,
7583 #[pyo3(get, set)]
7584 video_streaming_mode: bool,
7585 #[pyo3(get, set)]
7586 capture_cursor: bool,
7587 #[pyo3(get, set)]
7588 watermark_path: Py<PyAny>,
7589 #[pyo3(get, set)]
7590 watermark_location_enum: i32,
7591 #[pyo3(get, set)]
7592 encode_node_index: i32,
7593 #[pyo3(get, set)]
7594 use_cpu: bool,
7595 #[pyo3(get, set)]
7596 debug_logging: bool,
7597 #[pyo3(get, set)]
7598 video_cbr_mode: bool,
7599 #[pyo3(get, set)]
7600 video_bitrate_kbps: i32,
7601 #[pyo3(get, set)]
7602 video_vbv_multiplier: f64,
7603 #[pyo3(get, set)]
7604 keyframe_interval_s: f64,
7605 #[pyo3(get, set)]
7606 video_min_qp: i32,
7607 #[pyo3(get, set)]
7608 video_max_qp: i32,
7609 #[pyo3(get, set)]
7610 auto_adjust_screen_capture_size: bool,
7611 #[pyo3(get, set)]
7614 omit_stripe_headers: bool,
7615 #[pyo3(get, set)]
7621 acknowledge_references: bool,
7622 #[pyo3(get, set)]
7625 video_reference_frames: u32,
7626 #[pyo3(get, set)]
7627 encode_node_path: Py<PyAny>,
7628 #[pyo3(get, set)]
7631 render_node_path: Py<PyAny>,
7632 #[pyo3(get, set)]
7635 auto_gpu: Py<PyAny>,
7636 #[pyo3(get, set)]
7638 use_wayland: Py<PyAny>,
7639 #[pyo3(get, set)]
7641 recording_socket: Py<PyAny>,
7642 #[pyo3(get, set)]
7644 wayland_host_display: Py<PyAny>,
7645 #[pyo3(get, set)]
7647 cursor_size: i32,
7648 #[pyo3(get, set)]
7651 cursor_size_cap: i32,
7652}
7653
7654#[pymethods]
7655impl CaptureSettings {
7656 #[new]
7657 fn new(py: Python<'_>) -> Self {
7658 Self {
7659 display_id: 0,
7660 capture_width: 1920,
7661 capture_height: 1080,
7662 scale: 1.0,
7663 capture_x: 0,
7664 capture_y: 0,
7665 target_fps: 60.0,
7666 jpeg_quality: 85,
7667 paint_over_jpeg_quality: 95,
7668 use_paint_over_quality: false,
7669 paint_over_trigger_frames: 10,
7670 damage_block_threshold: 15,
7671 damage_block_duration: 30,
7672 codec: "jpeg".to_string(),
7673 video_crf: 25,
7674 video_paintover_crf: 18,
7675 video_paintover_burst_frames: 5,
7676 video_fullcolor: false,
7677 video_bit_depth: 8,
7678 video_fullframe: false,
7679 video_streaming_mode: false,
7680 capture_cursor: false,
7681 watermark_path: py.None(),
7682 watermark_location_enum: 0,
7683 encode_node_index: -2,
7684 use_cpu: false,
7685 debug_logging: false,
7686 video_cbr_mode: false,
7687 video_bitrate_kbps: 4000,
7688 video_vbv_multiplier: 0.0,
7689 keyframe_interval_s: 0.0,
7690 video_min_qp: 0,
7691 video_max_qp: 0,
7692 auto_adjust_screen_capture_size: false,
7693 omit_stripe_headers: false,
7694 acknowledge_references: false,
7695 video_reference_frames: 0,
7696 encode_node_path: py.None(),
7697 render_node_path: py.None(),
7698 auto_gpu: py.None(),
7699 use_wayland: py.None(),
7700 recording_socket: py.None(),
7701 wayland_host_display: py.None(),
7702 cursor_size: -1,
7703 cursor_size_cap: 128,
7704 }
7705 }
7706}
7707
7708static WAYLAND_BACKEND: OnceLock<Mutex<Option<Py<WaylandBackend>>>> = OnceLock::new();
7710static WAYLAND_SOCKET_NAME: Mutex<Option<String>> = Mutex::new(None);
7715static WAYLAND_SOCKET_CV: Condvar = Condvar::new();
7716
7717fn publish_socket_name(name: &str) {
7718 *WAYLAND_SOCKET_NAME.lock().unwrap() = Some(name.to_string());
7719 WAYLAND_SOCKET_CV.notify_all();
7720}
7721
7722fn wait_socket_name(timeout: Duration) -> Option<String> {
7724 let deadline = Instant::now() + timeout;
7725 let mut g = WAYLAND_SOCKET_NAME.lock().unwrap();
7726 loop {
7727 if let Some(name) = g.as_ref() {
7728 return Some(name.clone());
7729 }
7730 let now = Instant::now();
7731 if now >= deadline {
7732 return None;
7733 }
7734 let (gg, _) = WAYLAND_SOCKET_CV.wait_timeout(g, deadline - now).unwrap();
7735 g = gg;
7736 }
7737}
7738static PENDING_CURSOR_CALLBACK: Mutex<Option<Py<PyAny>>> = Mutex::new(None);
7742static CURSOR_REGISTRATIONS: Mutex<Vec<(u64, Py<PyAny>)>> = Mutex::new(Vec::new());
7746pub(crate) static PY_SHUTDOWN: AtomicBool = AtomicBool::new(false);
7750static WAYLAND_OWNERS: OnceLock<Mutex<std::collections::HashMap<u32, u64>>> = OnceLock::new();
7754
7755fn wayland_owners() -> &'static Mutex<std::collections::HashMap<u32, u64>> {
7756 WAYLAND_OWNERS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
7757}
7758
7759static WAYLAND_ALIVE_DISPLAYS: OnceLock<Mutex<std::collections::HashSet<u32>>> = OnceLock::new();
7763
7764fn wayland_alive() -> &'static Mutex<std::collections::HashSet<u32>> {
7765 WAYLAND_ALIVE_DISPLAYS.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
7766}
7767static WAYLAND_CAPTURE_ERR: OnceLock<Mutex<std::collections::HashMap<u32, String>>> =
7773 OnceLock::new();
7774
7775fn wayland_capture_err() -> &'static Mutex<std::collections::HashMap<u32, String>> {
7776 WAYLAND_CAPTURE_ERR.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
7777}
7778
7779static WAYLAND_ACTIVE_CODEC: OnceLock<Mutex<std::collections::HashMap<u32, u32>>> = OnceLock::new();
7781
7782fn wayland_active_codec() -> &'static Mutex<std::collections::HashMap<u32, u32>> {
7785 WAYLAND_ACTIVE_CODEC.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
7786}
7787
7788fn set_wayland_active_codec(display_id: u32, codec: Option<Codec>) {
7789 let mut map = wayland_active_codec().lock().unwrap();
7790 match codec {
7791 Some(c) => {
7792 map.insert(display_id, c.id());
7793 }
7794 None => {
7795 map.remove(&display_id);
7796 }
7797 }
7798}
7799
7800fn set_wayland_capture_err(display_id: u32, err: Option<String>) {
7801 let mut map = wayland_capture_err().lock().unwrap();
7802 match err {
7803 Some(e) => {
7804 map.insert(display_id, e);
7805 }
7806 None => {
7807 map.remove(&display_id);
7808 }
7809 }
7810}
7811static NEXT_CAPTURE_ID: AtomicU64 = AtomicU64::new(1);
7815static LIVE_X11: OnceLock<Mutex<Vec<Arc<crate::x11::Controls>>>> = OnceLock::new();
7819
7820fn live_x11() -> &'static Mutex<Vec<Arc<crate::x11::Controls>>> {
7821 LIVE_X11.get_or_init(|| Mutex::new(Vec::new()))
7822}
7823
7824pub(crate) fn unpremultiply_rgba(image: &mut image::RgbaImage) {
7831 for p in image.pixels_mut() {
7832 let a = p.0[3] as u32;
7833 if a == 0 {
7834 p.0 = [0, 0, 0, 0];
7835 } else if a < 255 {
7836 for c in &mut p.0[..3] {
7837 *c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
7838 }
7839 }
7840 }
7841}
7842
7843const STOP_JOIN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
7848
7849pub(crate) fn join_within(handle: thread::JoinHandle<()>, what: &str) {
7853 let deadline = std::time::Instant::now() + STOP_JOIN_TIMEOUT;
7854 while !handle.is_finished() {
7855 if std::time::Instant::now() >= deadline {
7856 eprintln!(
7857 "[X11] the {what} thread did not end within {} s; it is left behind",
7858 STOP_JOIN_TIMEOUT.as_secs()
7859 );
7860 return;
7861 }
7862 thread::sleep(std::time::Duration::from_millis(10));
7863 }
7864 let _ = handle.join();
7865}
7866
7867#[cfg(test)]
7868mod join_tests {
7869 use super::*;
7870
7871 #[test]
7873 fn join_within_is_bounded() {
7874 let quick = thread::spawn(|| {});
7875 let started = std::time::Instant::now();
7876 join_within(quick, "quick");
7877 assert!(started.elapsed() < STOP_JOIN_TIMEOUT);
7878 let (tx, rx) = std::sync::mpsc::channel::<()>();
7879 let stuck = thread::spawn(move || {
7880 let _ = rx.recv();
7881 });
7882 let started = std::time::Instant::now();
7883 join_within(stuck, "stuck");
7884 assert!(started.elapsed() >= STOP_JOIN_TIMEOUT);
7885 drop(tx);
7886 }
7887}
7888
7889pub(crate) fn boost_thread_priority(nice: libc::c_int) {
7896 static RTKIT: OnceLock<zbus::Result<zbus::blocking::Connection>> = OnceLock::new();
7897 static REFUSED: std::sync::Once = std::sync::Once::new();
7898 let tid = unsafe { libc::syscall(libc::SYS_gettid) };
7899 if unsafe { libc::setpriority(libc::PRIO_PROCESS, tid as libc::id_t, nice) } == 0 {
7900 return;
7901 }
7902 let granted = RTKIT
7903 .get_or_init(zbus::blocking::Connection::system)
7904 .as_ref()
7905 .map_err(|e| e.to_string())
7906 .and_then(|conn| {
7907 zbus::blocking::Proxy::new(
7908 conn,
7909 "org.freedesktop.RealtimeKit1",
7910 "/org/freedesktop/RealtimeKit1",
7911 "org.freedesktop.RealtimeKit1",
7912 )
7913 .and_then(|rtkit| rtkit.call_method("MakeThreadHighPriority", &(tid as u64, nice)))
7914 .map(|_| ())
7915 .map_err(|e| e.to_string())
7916 });
7917 if let Err(why) = granted {
7918 REFUSED.call_once(|| {
7919 eprintln!("[pixelflux] Thread priority {nice} refused and rtkit does not grant it; capture threads run at normal priority.");
7920 crate::log::debug!("[pixelflux] rtkit: {why}");
7921 });
7922 }
7923}
7924
7925fn wayland_update_rate(
7927 py: Python<'_>,
7928 display_id: u32,
7929 bitrate_kbps: Option<i32>,
7930 vbv_multiplier: Option<f64>,
7931 fps: Option<f64>,
7932) {
7933 if let Some(slot) = WAYLAND_BACKEND.get()
7934 && let Some(be) = slot.lock().unwrap().as_ref()
7935 {
7936 let _ = be
7937 .bind(py)
7938 .borrow()
7939 .update_rate(bitrate_kbps, vbv_multiplier, fps, display_id);
7940 }
7941}
7942
7943fn wayland_update_tunables(py: Python<'_>, display_id: u32, t: LiveTunables) {
7945 if let Some(slot) = WAYLAND_BACKEND.get()
7946 && let Some(be) = slot.lock().unwrap().as_ref()
7947 {
7948 let _ = be.bind(py).borrow().update_tunables(display_id, t);
7949 }
7950}
7951
7952fn ensure_wayland_backend(
7961 py: Python<'_>,
7962 width: i32,
7963 height: i32,
7964 explicit_node: String,
7965 auto_gpu: String,
7966 fallback_node: String,
7967 cursor_size: i32,
7968) -> PyResult<Py<WaylandBackend>> {
7969 let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
7970 let mut g = slot.lock().unwrap();
7971 if g.is_none() {
7972 let mut node = (!explicit_node.is_empty()).then_some(explicit_node);
7973 let mut auto_gpu_selected = false;
7974 if node.is_none()
7975 && let Some(request) = parse_auto_gpu(&auto_gpu)
7976 {
7977 match auto_select_render_node(request.as_deref()) {
7978 Some(picked) => {
7979 println!("[Wayland] AUTO_GPU selected {picked}.");
7980 node = Some(picked);
7981 auto_gpu_selected = true;
7982 }
7983 None => {
7984 if let Some(token) = request {
7985 eprintln!("[pixelflux] AUTO_GPU={token}: no matching GPU found.");
7986 }
7987 }
7988 }
7989 }
7990 let node = node.unwrap_or(fallback_node);
7991 let be = Py::new(
7992 py,
7993 WaylandBackend::new(width, height, node, auto_gpu_selected, cursor_size),
7994 )?;
7995 if let Some(cb) = PENDING_CURSOR_CALLBACK.lock().unwrap().take() {
7996 let _ = be.bind(py).borrow().set_cursor_callback(Some(cb));
7997 }
7998 *g = Some(be);
7999 }
8000 Ok(g.as_ref().unwrap().clone_ref(py))
8001}
8002
8003fn wayland_backend_running(py: Python<'_>) -> Option<Py<WaylandBackend>> {
8007 let slot = WAYLAND_BACKEND.get()?;
8008 let g = slot.lock().unwrap();
8009 g.as_ref().map(|b| b.clone_ref(py))
8010}
8011
8012fn want_wayland(settings: &Bound<'_, PyAny>) -> bool {
8016 if let Some(explicit) = settings
8017 .getattr("use_wayland")
8018 .ok()
8019 .and_then(|v| v.extract::<bool>().ok())
8020 {
8021 return explicit;
8022 }
8023 std::env::var("WAYLAND_DISPLAY")
8024 .map(|v| !v.is_empty())
8025 .unwrap_or(false)
8026}
8027
8028struct ScState {
8032 backend: u8,
8034 cursor_ref: bool,
8042 controls: Option<Arc<crate::x11::Controls>>,
8043 starting: Option<Arc<crate::x11::Controls>>,
8049 handle: Option<thread::JoinHandle<()>>,
8050 cap_thread_id: Option<thread::ThreadId>,
8051 encode_thread_id: Option<thread::ThreadId>,
8054 encode_tid_rx: Option<std::sync::mpsc::Receiver<thread::ThreadId>>,
8062 deliver_handle: Option<thread::JoinHandle<()>>,
8067 deliver_thread_id: Option<thread::ThreadId>,
8068 wl_display: u32,
8070 err: Option<Arc<Mutex<Option<String>>>>,
8074}
8075
8076impl ScState {
8077 fn x11_controls(&self) -> Option<Arc<crate::x11::Controls>> {
8080 match self.backend {
8081 1 => self.controls.clone(),
8082 0 => self.starting.clone(),
8083 _ => None,
8084 }
8085 }
8086}
8087
8088#[pyclass]
8092struct ScreenCapture {
8093 id: u64,
8094 inner: Mutex<ScState>,
8095}
8096
8097impl ScreenCapture {
8098 fn hand_report(&self, py: Python<'_>, report: encoders::reference::ReferenceReport) {
8101 use encoders::reference::ReferenceReport;
8102 let (backend, controls, did) = {
8103 let st = self.inner.lock().unwrap();
8104 (st.backend, st.x11_controls(), st.wl_display)
8105 };
8106 match backend {
8107 0 | 1 => {
8108 if let Some(c) = controls {
8109 c.reference_reports.lock().unwrap().push(report);
8110 }
8111 }
8112 2 => {
8113 if let Some(slot) = WAYLAND_BACKEND.get()
8114 && let Some(be) = slot.lock().unwrap().as_ref()
8115 {
8116 let be = be.bind(py).borrow();
8117 let _ = match report {
8118 ReferenceReport::Lost(frame_id) => be.invalidate_reference(frame_id, did),
8119 ReferenceReport::Held(frame_id) => {
8120 be.acknowledge_reference(frame_id, did, true)
8121 }
8122 ReferenceReport::Sent(frame_id) => {
8123 be.acknowledge_reference(frame_id, did, false)
8124 }
8125 };
8126 }
8127 }
8128 _ => {}
8129 }
8130 }
8131
8132 fn stop_internal(&self, py: Python<'_>) -> PyResult<()> {
8143 let (handle, deliver_handle, same_thread, backend, controls, wl_display, cursor_ref) = {
8144 let mut st = self.inner.lock().unwrap();
8145 if let Some(c) = &st.controls {
8146 c.stop.store(true, Ordering::Relaxed);
8147 }
8148 if let Some(c) = st.starting.take() {
8149 c.stop.store(true, Ordering::Relaxed);
8150 }
8151 let cur = Some(thread::current().id());
8152 if st.encode_thread_id.is_none()
8153 && let Some(rx) = st.encode_tid_rx.as_ref()
8154 && let Ok(id) = rx.try_recv()
8155 {
8156 st.encode_thread_id = Some(id);
8157 }
8158 let same = st.cap_thread_id == cur
8159 || st.encode_thread_id == cur
8160 || st.deliver_thread_id == cur;
8161 let controls = st.controls.take();
8162 let handle = st.handle.take();
8163 let deliver_handle = st.deliver_handle.take();
8164 let backend = st.backend;
8165 let cursor_ref = std::mem::take(&mut st.cursor_ref);
8166 let wl_display = st.wl_display;
8167 st.backend = 0;
8168 st.cap_thread_id = None;
8169 st.encode_thread_id = None;
8170 st.encode_tid_rx = None;
8171 st.deliver_thread_id = None;
8172 st.wl_display = 0;
8173 st.err = None;
8174 (
8175 handle,
8176 deliver_handle,
8177 same,
8178 backend,
8179 controls,
8180 wl_display,
8181 cursor_ref,
8182 )
8183 };
8184 if let Some(c) = &controls {
8185 live_x11().lock().unwrap().retain(|x| !Arc::ptr_eq(x, c));
8186 }
8187 if cursor_ref {
8188 crate::x11::cursor::release(py);
8189 }
8190 if backend == 2 {
8191 let did = wl_display;
8192 let owned = {
8193 let mut owners = wayland_owners().lock().unwrap();
8194 if owners.get(&did) == Some(&self.id) {
8195 owners.remove(&did);
8196 true
8197 } else {
8198 false
8199 }
8200 };
8201 if owned
8202 && let Some(slot) = WAYLAND_BACKEND.get()
8203 && let Some(be) = slot.lock().unwrap().as_ref()
8204 {
8205 let _ = be.bind(py).borrow().stop_capture(did);
8206 }
8207 } else {
8208 if same_thread {
8209 drop(handle);
8211 drop(deliver_handle);
8212 } else {
8213 py.detach(|| {
8214 if let Some(h) = handle {
8215 join_within(h, "capture");
8216 }
8217 if let Some(h) = deliver_handle {
8220 join_within(h, "delivery");
8221 }
8222 });
8223 }
8224 }
8225 Ok(())
8226 }
8227}
8228
8229impl ScreenCapture {
8230 fn report(&self) -> Option<Arc<report::StreamReport>> {
8232 let st = self.inner.lock().unwrap();
8233 match st.backend {
8234 1 => st.controls.as_ref().map(|c| c.report.clone()),
8235 2 => report::wayland_reports()
8236 .lock()
8237 .unwrap()
8238 .get(&st.wl_display)
8239 .cloned(),
8240 _ => None,
8241 }
8242 }
8243}
8244
8245#[pymethods]
8246impl ScreenCapture {
8247 #[new]
8248 fn new() -> Self {
8249 Self {
8250 id: NEXT_CAPTURE_ID.fetch_add(1, Ordering::Relaxed),
8251 inner: Mutex::new(ScState {
8252 backend: 0,
8253 cursor_ref: false,
8254 controls: None,
8255 starting: None,
8256 handle: None,
8257 cap_thread_id: None,
8258 encode_thread_id: None,
8259 encode_tid_rx: None,
8260 deliver_handle: None,
8261 deliver_thread_id: None,
8262 wl_display: 0,
8263 err: None,
8264 }),
8265 }
8266 }
8267
8268 fn start_capture(
8284 &self,
8285 py: Python<'_>,
8286 callback: Py<PyAny>,
8287 settings: &Bound<'_, PyAny>,
8288 ) -> PyResult<()> {
8289 let display_id = read_display_id(settings);
8290 let live_wayland_restart = want_wayland(settings)
8291 && {
8292 let st = self.inner.lock().unwrap();
8293 st.backend == 2 && st.wl_display == display_id
8294 }
8295 && wayland_owners().lock().unwrap().get(&display_id) == Some(&self.id)
8296 && wayland_alive().lock().unwrap().contains(&display_id);
8297 if !live_wayland_restart {
8298 self.stop_internal(py)?;
8299 }
8300 let rs = extract_settings(settings)?;
8301
8302 if want_wayland(settings) {
8303 let read_node = |attr: &str| -> Option<String> {
8304 settings.getattr(attr).ok().and_then(|o| {
8305 o.extract::<String>()
8306 .or_else(|_| {
8307 o.extract::<Vec<u8>>()
8308 .map(|b| String::from_utf8_lossy(&b).into_owned())
8309 })
8310 .ok()
8311 })
8312 };
8313 let cursor_size = settings
8314 .getattr("cursor_size")
8315 .ok()
8316 .and_then(|v| v.extract::<i32>().ok())
8317 .unwrap_or(-1);
8318 let be = ensure_wayland_backend(
8319 py,
8320 rs.width,
8321 rs.height,
8322 read_node("render_node_path").unwrap_or_default(),
8323 read_node("auto_gpu").unwrap_or_default(),
8324 read_node("encode_node_path").unwrap_or_default(),
8325 cursor_size,
8326 )?;
8327 be.bind(py).borrow().start_capture(callback, settings)?;
8328 wayland_owners().lock().unwrap().insert(display_id, self.id);
8329 {
8330 let mut st = self.inner.lock().unwrap();
8331 st.backend = 2;
8332 st.wl_display = display_id;
8333 }
8334 return Ok(());
8335 }
8336
8337 PY_SHUTDOWN.store(false, Ordering::Relaxed);
8341 let mut rs = rs;
8342 if rs.encode_node_index < -1 {
8343 let auto_gpu = settings
8344 .getattr("auto_gpu")
8345 .ok()
8346 .and_then(|o| {
8347 o.extract::<String>()
8348 .or_else(|_| {
8349 o.extract::<Vec<u8>>()
8350 .map(|b| String::from_utf8_lossy(&b).into_owned())
8351 })
8352 .ok()
8353 })
8354 .unwrap_or_default();
8355 if let Some(picked) = auto_render_node(&auto_gpu)
8356 && let Some(idx) = render_node_index(&picked)
8357 {
8358 println!("[X11] AUTO_GPU selected {picked}.");
8359 rs.encode_node_index = idx;
8360 } else if let Some(idx) = first_render_node_index() {
8361 rs.encode_node_index = idx;
8362 }
8363 }
8364
8365 println!(
8366 "[X11] Configuring Output: {}x{} @ {:.2} FPS (Encode Node: {})",
8367 rs.width, rs.height, rs.target_fps, rs.encode_node_index
8368 );
8369
8370 let controls = Arc::new(crate::x11::Controls::new(&rs));
8371 crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
8372 live_x11().lock().unwrap().push(controls.clone());
8373 if let Some(other) = self
8374 .inner
8375 .lock()
8376 .unwrap()
8377 .starting
8378 .replace(controls.clone())
8379 {
8380 other.stop.store(true, Ordering::Relaxed);
8381 }
8382 let c2 = controls.clone();
8383 let c3 = controls.clone();
8384 let cb = callback;
8385 let err_slot: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
8386 let err_slot2 = err_slot.clone();
8387
8388 let (deliver_tx, deliver_rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
8394 let deliver_handle = thread::spawn(move || {
8395 crate::boost_thread_priority(-10);
8396 while let Ok(frame) = deliver_rx.recv() {
8397 if PY_SHUTDOWN.load(Ordering::Relaxed) {
8398 continue;
8399 }
8400 Python::attach(|py| {
8401 for s in frame {
8402 match Py::new(
8403 py,
8404 StripeFrame::new_owned_meta(
8405 s.data,
8406 s.codec.data_type(),
8407 s.stripe_y_start,
8408 s.stripe_height,
8409 s.frame_id,
8410 s.timing,
8411 s.reference,
8412 ),
8413 ) {
8414 Ok(f) => {
8415 if let Err(e) = cb.call1(py, (f,)) {
8416 e.print(py);
8417 }
8418 }
8419 Err(e) => eprintln!("[X11] frame alloc error: {e:?}"),
8420 }
8421 }
8422 });
8423 }
8424 });
8425 let deliver_thread_id = deliver_handle.thread().id();
8426
8427 let report = controls.report.clone();
8428 let on_frame = move |frame: Vec<EncodedStripe>| {
8429 report.tally(&frame);
8430 let _ = deliver_tx.send(frame);
8433 };
8434
8435 let (tid_tx, tid_rx) = std::sync::mpsc::channel();
8436 let (etid_tx, etid_rx) = std::sync::mpsc::channel();
8437 let handle = thread::spawn(move || {
8438 crate::boost_thread_priority(-15);
8439 let _ = tid_tx.send(thread::current().id());
8440 let res = crate::x11::run_capture(rs, c2, etid_tx, on_frame);
8441 c3.stop.store(true, Ordering::Release);
8442 if let Err(e) = res {
8443 let msg = e.to_string();
8444 eprintln!("[X11] capture error: {msg}");
8445 if let Ok(mut g) = err_slot2.lock() {
8446 *g = Some(msg);
8447 }
8448 }
8449 c3.finished.store(true, Ordering::Release);
8452 });
8453 let (tid, etid_res, etid_rx) = py.detach(move || {
8454 let tid = tid_rx.recv().ok();
8455 let etid_res = etid_rx.recv_timeout(std::time::Duration::from_secs(2));
8456 (tid, etid_res, etid_rx)
8457 });
8458 let mut late_etid_rx = None;
8459 let etid = match etid_res {
8460 Ok(id) => Some(id),
8461 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
8462 let _ = handle.join();
8463 live_x11()
8464 .lock()
8465 .unwrap()
8466 .retain(|x| !Arc::ptr_eq(x, &controls));
8467 let mut st = self.inner.lock().unwrap();
8468 if !st
8469 .starting
8470 .as_ref()
8471 .is_some_and(|c| Arc::ptr_eq(c, &controls))
8472 {
8473 return Ok(());
8474 }
8475 st.starting = None;
8476 drop(st);
8477 let msg = err_slot
8478 .lock()
8479 .ok()
8480 .and_then(|g| g.clone())
8481 .unwrap_or_else(|| "X11 capture thread exited during start".to_string());
8482 return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(msg));
8483 }
8484 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
8485 late_etid_rx = Some(etid_rx);
8489 None
8490 }
8491 };
8492 let mut st = self.inner.lock().unwrap();
8493 if !st
8494 .starting
8495 .as_ref()
8496 .is_some_and(|c| Arc::ptr_eq(c, &controls))
8497 {
8498 drop(st);
8499 live_x11()
8500 .lock()
8501 .unwrap()
8502 .retain(|x| !Arc::ptr_eq(x, &controls));
8503 py.detach(|| {
8504 join_within(handle, "capture");
8505 join_within(deliver_handle, "delivery");
8506 });
8507 return Ok(());
8508 }
8509 st.starting = None;
8510 crate::x11::cursor::acquire();
8513 st.backend = 1;
8514 st.cursor_ref = true;
8515 st.controls = Some(controls);
8516 st.handle = Some(handle);
8517 st.cap_thread_id = tid;
8518 st.encode_thread_id = etid;
8519 st.encode_tid_rx = late_etid_rx;
8520 st.deliver_handle = Some(deliver_handle);
8521 st.deliver_thread_id = Some(deliver_thread_id);
8522 st.err = Some(err_slot);
8523 drop(st);
8524 Ok(())
8525 }
8526
8527 fn active_codec(&self, py: Python<'_>) -> PyResult<Option<String>> {
8531 let (backend, controls, wl_display) = {
8532 let st = self.inner.lock().unwrap();
8533 (st.backend, st.controls.clone(), st.wl_display)
8534 };
8535 let id = match backend {
8536 1 => controls.map_or(u32::MAX, |c| c.codec.load(Ordering::Relaxed)),
8537 2 => py
8538 .detach(|| {
8539 wayland_active_codec()
8540 .lock()
8541 .unwrap()
8542 .get(&wl_display)
8543 .copied()
8544 })
8545 .unwrap_or(u32::MAX),
8546 _ => u32::MAX,
8547 };
8548 Ok(Codec::from_id(id).map(|c| c.name().to_string()))
8549 }
8550
8551 fn stream_info(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
8562 let Some(report) = self.report() else {
8563 return Ok(None);
8564 };
8565 let (info, renderer) = py.detach(|| {
8566 let mut info = report.info();
8567 let renderer = (info.backend == "wayland").then(report::renderer);
8568 if info.hardware && info.gpu.is_empty() {
8569 info.gpu = match &renderer {
8570 Some(r) if render_node_index(&r.node) == Some(info.encode_node) => {
8571 r.gpu.clone()
8572 }
8573 _ => node_gpu_name(info.encode_node),
8574 };
8575 }
8576 (info, renderer)
8577 });
8578 let d = pyo3::types::PyDict::new(py);
8579 d.set_item("backend", info.backend)?;
8580 d.set_item("capture", info.capture)?;
8581 d.set_item("zero_copy", info.zero_copy)?;
8582 d.set_item("zero_copy_available", info.zero_copy_available)?;
8583 d.set_item("capture_reason", &info.capture_reason)?;
8584 d.set_item("encoder", &info.encoder)?;
8585 d.set_item("hardware", info.hardware)?;
8586 d.set_item("encoder_reason", &info.encoder_reason)?;
8587 d.set_item("codec", info.codec)?;
8588 d.set_item("fullcolor", info.fullcolor)?;
8589 d.set_item("bit_depth", info.bit_depth)?;
8590 d.set_item("full_range", info.full_range)?;
8591 d.set_item("striped", info.stripes > 1)?;
8592 d.set_item("gpu", &info.gpu)?;
8593 d.set_item("driver", &info.driver)?;
8594 let encode_node = if info.hardware {
8595 format!("/dev/dri/renderD{}", 128 + info.encode_node)
8596 } else {
8597 String::new()
8598 };
8599 d.set_item("encode_node", encode_node)?;
8600 if let Some(r) = renderer {
8601 d.set_item("renderer", r.kind)?;
8602 d.set_item("render_node", &r.node)?;
8603 d.set_item("render_gpu", &r.gpu)?;
8604 d.set_item("renderer_reason", &r.reason)?;
8605 }
8606 Ok(Some(d.into_any().unbind()))
8607 }
8608
8609 fn stream_stats(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
8616 let Some(report) = self.report() else {
8617 return Ok(None);
8618 };
8619 let totals = report.totals();
8620 let peaks = report.take_peaks();
8621 let d = pyo3::types::PyDict::new(py);
8622 d.set_item("frames", totals.frames)?;
8623 d.set_item("bytes", totals.bytes)?;
8624 d.set_item("encode_ns", totals.encode_ns)?;
8625 d.set_item("pipeline_ns", totals.pipeline_ns)?;
8626 d.set_item("pipeline_min_ns", peaks.pipeline_min_ns)?;
8627 d.set_item("pipeline_max_ns", peaks.pipeline_max_ns)?;
8628 d.set_item("frame_max_bytes", peaks.frame_max_bytes)?;
8629 Ok(Some(d.into_any().unbind()))
8630 }
8631
8632 fn stop_capture(&self, py: Python<'_>) -> PyResult<()> {
8633 self.stop_internal(py)
8634 }
8635
8636 fn request_idr_frame(&self, py: Python<'_>) -> PyResult<()> {
8637 let (backend, controls, did) = {
8638 let st = self.inner.lock().unwrap();
8639 (st.backend, st.x11_controls(), st.wl_display)
8640 };
8641 match backend {
8642 0 | 1 => {
8643 if let Some(c) = controls {
8644 c.force_idr.store(true, Ordering::Relaxed);
8645 }
8646 }
8647 2 => {
8648 if let Some(slot) = WAYLAND_BACKEND.get()
8649 && let Some(be) = slot.lock().unwrap().as_ref()
8650 {
8651 let _ = be.bind(py).borrow().request_idr_frame(did);
8652 }
8653 }
8654 _ => {}
8655 }
8656 Ok(())
8657 }
8658
8659 fn invalidate_reference(&self, py: Python<'_>, frame_id: u16) -> PyResult<()> {
8663 self.hand_report(py, encoders::reference::ReferenceReport::Lost(frame_id));
8664 Ok(())
8665 }
8666
8667 #[pyo3(signature = (frame_id, held = true))]
8672 fn acknowledge_reference(&self, py: Python<'_>, frame_id: u16, held: bool) -> PyResult<()> {
8673 use encoders::reference::ReferenceReport;
8674 let report = if held {
8675 ReferenceReport::Held(frame_id)
8676 } else {
8677 ReferenceReport::Sent(frame_id)
8678 };
8679 self.hand_report(py, report);
8680 Ok(())
8681 }
8682
8683 fn update_video_bitrate(&self, py: Python<'_>, kbps: i32) -> PyResult<()> {
8688 let (backend, controls, did) = {
8689 let st = self.inner.lock().unwrap();
8690 (st.backend, st.x11_controls(), st.wl_display)
8691 };
8692 match backend {
8693 0 | 1 => {
8694 if let Some(c) = &controls {
8695 c.bitrate_kbps.store(kbps, Ordering::Relaxed);
8696 c.rate_dirty.store(true, Ordering::Release);
8697 }
8698 }
8699 2 => wayland_update_rate(py, did, Some(kbps), None, None),
8700 _ => {}
8701 }
8702 Ok(())
8703 }
8704
8705 fn update_framerate(&self, py: Python<'_>, fps: f64) -> PyResult<()> {
8706 let (backend, controls, did) = {
8707 let st = self.inner.lock().unwrap();
8708 (st.backend, st.x11_controls(), st.wl_display)
8709 };
8710 match backend {
8711 0 | 1 => {
8712 if let Some(c) = &controls {
8713 c.fps_bits.store(fps.max(1.0).to_bits(), Ordering::Relaxed);
8714 c.rate_dirty.store(true, Ordering::Release);
8715 py.detach(crate::x11::follow_frame_rate);
8716 }
8717 }
8718 2 => wayland_update_rate(py, did, None, None, Some(fps)),
8719 _ => {}
8720 }
8721 Ok(())
8722 }
8723
8724 fn update_vbv_multiplier(&self, py: Python<'_>, multiplier: f64) -> PyResult<()> {
8726 let (backend, controls, did) = {
8727 let st = self.inner.lock().unwrap();
8728 (st.backend, st.x11_controls(), st.wl_display)
8729 };
8730 match backend {
8731 0 | 1 => {
8732 if let Some(c) = &controls {
8733 c.vbv_mult_milli
8734 .store((multiplier * 1000.0).round() as i32, Ordering::Relaxed);
8735 c.rate_dirty.store(true, Ordering::Release);
8736 }
8737 }
8738 2 => wayland_update_rate(py, did, None, Some(multiplier), None),
8739 _ => {}
8740 }
8741 Ok(())
8742 }
8743
8744 fn update_tunables(&self, py: Python<'_>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
8748 let rs = extract_settings(settings)?;
8749 let t = LiveTunables::from_settings(&rs);
8750 let (backend, controls, did) = {
8751 let st = self.inner.lock().unwrap();
8752 (st.backend, st.x11_controls(), st.wl_display)
8753 };
8754 match backend {
8755 0 | 1 => {
8756 if let Some(c) = &controls {
8757 c.capture_cursor.store(t.capture_cursor, Ordering::Relaxed);
8758 *c.tunables.lock().unwrap() = Some(t);
8759 c.tunables_dirty.store(true, Ordering::Release);
8760 crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
8761 }
8762 }
8763 2 => wayland_update_tunables(py, did, t),
8764 _ => {}
8765 }
8766 Ok(())
8767 }
8768
8769 fn update_capture_region(&self, x: i32, y: i32, width: i32, height: i32) -> PyResult<()> {
8775 let controls = {
8776 let st = self.inner.lock().unwrap();
8777 if st.backend == 2 {
8778 return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
8779 "update_capture_region is X11-only; on Wayland restart the capture with new dimensions",
8780 ));
8781 }
8782 st.x11_controls()
8783 };
8784 if let Some(c) = &controls {
8785 *c.region.lock().unwrap() = (x.max(0), y.max(0), width, height);
8786 c.region_dirty.store(true, Ordering::Release);
8787 }
8788 Ok(())
8789 }
8790
8791 #[getter]
8792 fn is_capturing(&self) -> bool {
8793 let st = self.inner.lock().unwrap();
8794 match st.backend {
8795 1 => st
8796 .controls
8797 .as_ref()
8798 .map(|c| !c.stop.load(Ordering::Relaxed))
8799 .unwrap_or(false),
8800 2 => {
8801 wayland_owners().lock().unwrap().get(&st.wl_display) == Some(&self.id)
8802 && wayland_alive().lock().unwrap().contains(&st.wl_display)
8803 }
8804 _ => false,
8805 }
8806 }
8807
8808 fn inject_key(&self, py: Python<'_>, scancode: u32, state: u32) -> PyResult<()> {
8809 wayland_backend_running(py).map_or(Ok(()), |be| {
8810 be.bind(py).borrow().inject_key(scancode, state)
8811 })
8812 }
8813 fn inject_keys(&self, py: Python<'_>, events: Vec<(u32, u32)>) -> PyResult<()> {
8815 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_keys(events))
8816 }
8817 fn set_keymap_string(&self, py: Python<'_>, text: String) -> PyResult<()> {
8818 wayland_backend_running(py)
8819 .map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_string(text))
8820 }
8821 fn set_keymap_overlay(&self, py: Python<'_>, binds: Vec<(u32, u32)>) -> PyResult<()> {
8825 wayland_backend_running(py)
8826 .map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_overlay(binds))
8827 }
8828 fn set_app_wayland_display(&self, display: String) {
8833 crate::computer_use::set_app_wayland_display(if display.is_empty() {
8834 None
8835 } else {
8836 Some(display)
8837 });
8838 }
8839 fn type_text_wayland(&self, py: Python<'_>, display: String, text: String) -> PyResult<()> {
8845 py.detach(move || {
8846 let path = crate::wayland::wlclient::socket_path(&display)
8847 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8848 crate::wayland::vkclient::type_text_to(&path, &text)
8849 })
8850 .map_err(|e: String| {
8851 if e.contains("zwp_virtual_keyboard_manager_v1") {
8852 VirtualKeyboardUnavailable::new_err(e)
8853 } else {
8854 pyo3::exceptions::PyRuntimeError::new_err(e)
8855 }
8856 })
8857 }
8858 fn type_keysyms_wayland(
8862 &self,
8863 py: Python<'_>,
8864 display: String,
8865 keysyms: Vec<u32>,
8866 ) -> PyResult<()> {
8867 py.detach(move || {
8868 let path = crate::wayland::wlclient::socket_path(&display)
8869 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8870 crate::wayland::vkclient::type_keysyms_to(&path, &keysyms)
8871 })
8872 .map_err(|e: String| {
8873 if e.contains("zwp_virtual_keyboard_manager_v1") {
8874 VirtualKeyboardUnavailable::new_err(e)
8875 } else {
8876 pyo3::exceptions::PyRuntimeError::new_err(e)
8877 }
8878 })
8879 }
8880 #[pyo3(signature = (display, scale, index = 0))]
8885 fn set_app_output_scale(
8886 &self,
8887 py: Python<'_>,
8888 display: String,
8889 scale: f64,
8890 index: usize,
8891 ) -> PyResult<bool> {
8892 py.detach(move || {
8893 let path = crate::wayland::wlclient::socket_path(&display)
8894 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8895 crate::wayland::outclient::set_output_scale(&path, index, scale)
8896 })
8897 .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
8898 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8899 }
8900 #[pyo3(signature = (display, width, height, scale, index = 0))]
8909 fn set_app_screen_geometry(
8910 &self,
8911 py: Python<'_>,
8912 display: String,
8913 width: i32,
8914 height: i32,
8915 scale: f64,
8916 index: usize,
8917 ) -> PyResult<bool> {
8918 py.detach(move || {
8919 let path = crate::wayland::wlclient::socket_path(&display)
8920 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8921 crate::wayland::outclient::set_screen_geometry(&path, index, (width, height), scale)
8922 })
8923 .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
8924 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8925 }
8926 fn set_app_screen_layout(
8934 &self,
8935 py: Python<'_>,
8936 display: String,
8937 rects: Vec<(i32, i32, i32, i32)>,
8938 ) -> PyResult<usize> {
8939 py.detach(move || {
8940 let path = crate::wayland::wlclient::socket_path(&display)
8941 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8942 let placed = crate::wayland::outclient::set_screen_layout(&path, rects.clone())?;
8943 if placed == 0 {
8944 crate::wayland::kdeclient::set_screen_layout(&path, rects)
8945 } else {
8946 Ok(placed)
8947 }
8948 })
8949 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8950 }
8951 fn list_app_screens(
8958 &self,
8959 py: Python<'_>,
8960 display: String,
8961 ) -> PyResult<Vec<crate::wayland::AppScreen>> {
8962 py.detach(move || {
8963 let path = crate::wayland::wlclient::socket_path(&display)
8964 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8965 let screens = crate::wayland::outclient::list_screens(&path)?;
8966 if screens.is_empty() {
8967 crate::wayland::kdeclient::list_screens(&path)
8968 } else {
8969 Ok(screens)
8970 }
8971 })
8972 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8973 }
8974 fn app_screen_control_offered(&self, py: Python<'_>, display: String) -> PyResult<bool> {
8980 py.detach(move || {
8981 let path = crate::wayland::wlclient::socket_path(&display)
8982 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
8983 crate::wayland::kdeclient::screen_control_offered(&path)
8984 })
8985 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
8986 }
8987 #[pyo3(signature = (display, name, width = 1920, height = 1080, scale = 1.0))]
8992 fn add_app_screen(
8993 &self,
8994 py: Python<'_>,
8995 display: String,
8996 name: String,
8997 width: i32,
8998 height: i32,
8999 scale: f64,
9000 ) -> PyResult<()> {
9001 py.detach(move || {
9002 let path = crate::wayland::wlclient::socket_path(&display)
9003 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
9004 crate::wayland::kdeclient::add_screen(&path, &name, (width, height), scale)
9005 })
9006 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9007 }
9008 fn remove_app_screen(&self, py: Python<'_>, name: String) -> PyResult<bool> {
9012 Ok(py.detach(move || crate::wayland::kdeclient::remove_screen(&name)))
9013 }
9014 fn hold_spare_app_screens(
9019 &self,
9020 py: Python<'_>,
9021 display: String,
9022 keep: usize,
9023 width: i32,
9024 height: i32,
9025 ) -> PyResult<usize> {
9026 py.detach(move || {
9027 let path = crate::wayland::wlclient::socket_path(&display)
9028 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
9029 crate::wayland::outclient::hold_spare_screens(&path, keep, (width, height))
9030 })
9031 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9032 }
9033 fn clipboard_types_app(&self, py: Python<'_>, display: String) -> PyResult<Vec<String>> {
9035 py.detach(move || crate::wayland::dcclient::list_types(&app_socket_path(&display)?))
9036 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9037 }
9038 fn clipboard_read_app(
9041 &self,
9042 py: Python<'_>,
9043 display: String,
9044 mime: String,
9045 ) -> PyResult<Option<Py<pyo3::types::PyBytes>>> {
9046 let data = py
9047 .detach(move || crate::wayland::dcclient::read(&app_socket_path(&display)?, &mime))
9048 .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
9049 Ok(data.map(|d| pyo3::types::PyBytes::new(py, &d).unbind()))
9050 }
9051 fn clipboard_write_app(
9054 &self,
9055 py: Python<'_>,
9056 display: String,
9057 entries: Vec<(String, Vec<u8>)>,
9058 ) -> PyResult<()> {
9059 py.detach(move || crate::wayland::dcclient::write(&app_socket_path(&display)?, entries))
9060 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9061 }
9062 fn clipboard_clear_app(&self, py: Python<'_>, display: String) -> PyResult<()> {
9064 py.detach(move || crate::wayland::dcclient::clear(&app_socket_path(&display)?))
9065 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9066 }
9067 fn clipboard_watch_app(
9072 &self,
9073 py: Python<'_>,
9074 display: String,
9075 callback: Py<PyAny>,
9076 ) -> PyResult<()> {
9077 py.detach(move || crate::wayland::dcclient::watch(&app_socket_path(&display)?, callback))
9078 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
9079 }
9080 fn clipboard_unwatch_app(&self, py: Python<'_>, display: String) {
9082 let _ = py.detach(move || {
9083 crate::wayland::dcclient::unwatch(&app_socket_path(&display)?);
9084 Ok::<(), String>(())
9085 });
9086 }
9087 fn inject_mouse_move(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<Option<u64>> {
9089 wayland_backend_running(py).map_or(Ok(None), |be| {
9090 be.bind(py).borrow().inject_mouse_move(x, y).map(Some)
9091 })
9092 }
9093 fn inject_relative_mouse_move(
9095 &self,
9096 py: Python<'_>,
9097 dx: f64,
9098 dy: f64,
9099 ) -> PyResult<Option<u64>> {
9100 wayland_backend_running(py).map_or(Ok(None), |be| {
9101 be.bind(py)
9102 .borrow()
9103 .inject_relative_mouse_move(dx, dy)
9104 .map(Some)
9105 })
9106 }
9107 fn inject_mouse_button(&self, py: Python<'_>, btn: u32, state: u32) -> PyResult<()> {
9108 wayland_backend_running(py).map_or(Ok(()), |be| {
9109 be.bind(py).borrow().inject_mouse_button(btn, state)
9110 })
9111 }
9112 fn inject_mouse_scroll(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<()> {
9113 wayland_backend_running(py)
9114 .map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_scroll(x, y))
9115 }
9116 fn inject_finger_scroll(&self, py: Python<'_>, dx: f64, dy: f64) -> PyResult<()> {
9118 wayland_backend_running(py).map_or(Ok(()), |be| {
9119 be.bind(py).borrow().inject_finger_scroll(dx, dy)
9120 })
9121 }
9122 fn inject_finger_scroll_end(&self, py: Python<'_>) -> PyResult<()> {
9124 wayland_backend_running(py)
9125 .map_or(Ok(()), |be| be.bind(py).borrow().inject_finger_scroll_end())
9126 }
9127 fn pointer_location(&self, py: Python<'_>) -> Option<(f64, f64, f64, u64)> {
9131 wayland_backend_running(py).and_then(|be| be.bind(py).borrow().pointer_location())
9132 }
9133 fn set_cursor_rendering(&self, py: Python<'_>, enabled: bool) -> PyResult<()> {
9137 let controls = self.inner.lock().unwrap().x11_controls();
9138 if let Some(c) = &controls {
9139 c.capture_cursor.store(enabled, Ordering::Relaxed);
9140 return Ok(());
9141 }
9142 wayland_backend_running(py).map_or(Ok(()), |be| {
9143 be.bind(py).borrow().set_cursor_rendering(enabled)
9144 })
9145 }
9146 fn set_cursor_callback(&self, py: Python<'_>, callback: Option<Py<PyAny>>) -> PyResult<()> {
9155 let callback = {
9156 let mut regs = CURSOR_REGISTRATIONS.lock().unwrap();
9157 regs.retain(|(id, _)| *id != self.id);
9158 if let Some(cb) = callback {
9159 regs.push((self.id, cb));
9160 }
9161 regs.last().map(|(_, cb)| cb.clone_ref(py))
9162 };
9163 crate::x11::cursor::set_callback(callback.as_ref().map(|c| c.clone_ref(py)));
9164 let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
9165 let g = slot.lock().unwrap();
9166 match g.as_ref() {
9167 Some(be) => be.bind(py).borrow().set_cursor_callback(callback),
9168 None => {
9169 *PENDING_CURSOR_CALLBACK.lock().unwrap() = callback;
9170 Ok(())
9171 }
9172 }
9173 }
9174
9175 fn clear_cursor_callback(&self, py: Python<'_>) -> PyResult<()> {
9179 self.set_cursor_callback(py, None)
9180 }
9181
9182 fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
9183 wayland_backend_running(py).map_or(Ok(String::new()), |be| {
9184 be.bind(py).borrow().get_xkb_keymap_string(py)
9185 })
9186 }
9187 fn set_clipboard_callback(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<()> {
9192 match wayland_backend_running(py) {
9193 Some(be) => be.bind(py).borrow().set_clipboard_callback(callback),
9194 None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
9195 "wayland backend not running",
9196 )),
9197 }
9198 }
9199 fn set_clipboard(&self, py: Python<'_>, entries: Vec<(String, Vec<u8>)>) -> PyResult<()> {
9201 match wayland_backend_running(py) {
9202 Some(be) => be.bind(py).borrow().set_clipboard(entries),
9203 None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
9204 "wayland backend not running",
9205 )),
9206 }
9207 }
9208 #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
9211 fn set_xkb_layout(
9212 &self,
9213 py: Python<'_>,
9214 layout: String,
9215 variant: String,
9216 options: String,
9217 model: String,
9218 rules: String,
9219 ) -> PyResult<bool> {
9220 wayland_backend_running(py).map_or(Ok(false), |be| {
9221 be.bind(py)
9222 .borrow()
9223 .set_xkb_layout(py, layout, variant, options, model, rules)
9224 })
9225 }
9226 fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
9228 wayland_backend_running(py).map_or(Ok((Vec::new(), 0)), |be| {
9229 be.bind(py).borrow().get_keyboard_state(py)
9230 })
9231 }
9232 #[pyo3(signature = (display_id = 0))]
9237 fn get_realized_geometry(
9238 &self,
9239 py: Python<'_>,
9240 display_id: u32,
9241 ) -> PyResult<Option<(i32, i32, f64)>> {
9242 wayland_backend_running(py).map_or(Ok(None), |be| {
9243 be.bind(py).borrow().get_realized_geometry(py, display_id)
9244 })
9245 }
9246 #[pyo3(signature = (display_id = 0))]
9252 fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
9253 let (backend, running, err) = {
9254 let st = self.inner.lock().unwrap();
9255 let running = st
9256 .controls
9257 .as_ref()
9258 .map(|c| !c.stop.load(Ordering::Relaxed))
9259 .unwrap_or(false);
9260 let err = st
9261 .err
9262 .as_ref()
9263 .and_then(|e| e.lock().ok().and_then(|g| g.clone()));
9264 (st.backend, running, err)
9265 };
9266 match backend {
9267 1 => {
9268 let state = if running {
9269 "running"
9270 } else if err.is_some() {
9271 "failed"
9272 } else {
9273 "idle"
9274 };
9275 (state.to_string(), err)
9276 }
9277 2 => wayland_capture_state(display_id),
9278 _ => ("idle".to_string(), None),
9279 }
9280 }
9281 #[allow(clippy::too_many_arguments)]
9285 #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
9286 fn create_output(
9287 &self,
9288 py: Python<'_>,
9289 id: u32,
9290 width: i32,
9291 height: i32,
9292 x: i32,
9293 y: i32,
9294 scale: f64,
9295 ) -> PyResult<bool> {
9296 wayland_backend_running(py).map_or(Ok(false), |be| {
9297 be.bind(py)
9298 .borrow()
9299 .create_output(py, id, width, height, x, y, scale)
9300 })
9301 }
9302 #[allow(clippy::too_many_arguments)]
9305 #[pyo3(signature = (id, owner, x, y, width, height))]
9306 fn create_view(
9307 &self,
9308 py: Python<'_>,
9309 id: u32,
9310 owner: u32,
9311 x: i32,
9312 y: i32,
9313 width: i32,
9314 height: i32,
9315 ) -> PyResult<bool> {
9316 wayland_backend_running(py).map_or(Ok(false), |be| {
9317 be.bind(py)
9318 .borrow()
9319 .create_view(py, id, owner, x, y, width, height)
9320 })
9321 }
9322 #[pyo3(signature = (id, width, height, scale = 1.0))]
9325 fn resize_output(
9326 &self,
9327 py: Python<'_>,
9328 id: u32,
9329 width: i32,
9330 height: i32,
9331 scale: f64,
9332 ) -> PyResult<bool> {
9333 wayland_backend_running(py).map_or(Ok(false), |be| {
9334 be.bind(py)
9335 .borrow()
9336 .resize_output(py, id, width, height, scale)
9337 })
9338 }
9339 fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
9341 wayland_backend_running(py)
9342 .map_or(Ok(false), |be| be.bind(py).borrow().destroy_output(py, id))
9343 }
9344 fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
9347 wayland_backend_running(py).map_or(Ok(false), |be| {
9348 be.bind(py).borrow().reposition_output(py, id, x, y)
9349 })
9350 }
9351 fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
9354 wayland_backend_running(py).map_or(Ok(false), |be| {
9355 be.bind(py).borrow().set_cursor_size(py, size)
9356 })
9357 }
9358 fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
9361 wayland_backend_running(py)
9362 .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_outputs(py))
9363 }
9364 fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
9366 wayland_backend_running(py).map_or(Ok(-1), |be| be.bind(py).borrow().output_capacity(py))
9367 }
9368 fn move_window_to_output(
9370 &self,
9371 py: Python<'_>,
9372 window_id: u32,
9373 output_id: u32,
9374 ) -> PyResult<bool> {
9375 wayland_backend_running(py).map_or(Ok(false), |be| {
9376 be.bind(py)
9377 .borrow()
9378 .move_window_to_output(py, window_id, output_id)
9379 })
9380 }
9381 fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
9384 wayland_backend_running(py)
9385 .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_windows(py))
9386 }
9387}
9388
9389impl Drop for ScreenCapture {
9395 fn drop(&mut self) {
9396 if let Ok(mut st) = self.inner.lock() {
9397 if let Some(c) = &st.controls {
9398 c.stop.store(true, Ordering::Relaxed);
9399 }
9400 if st.backend == 2 && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
9403 let did = st.wl_display;
9404 let owned = {
9405 let mut owners = wayland_owners().lock().unwrap();
9406 if owners.get(&did) == Some(&self.id) {
9407 owners.remove(&did);
9408 true
9409 } else {
9410 false
9411 }
9412 };
9413 if owned
9414 && let Some(slot) = WAYLAND_BACKEND.get()
9415 && let Some(be) = slot.lock().unwrap().as_ref()
9416 {
9417 Python::attach(|py| {
9418 let _ = be.bind(py).borrow().stop_capture(did);
9419 });
9420 }
9421 }
9422 if std::mem::take(&mut st.cursor_ref) && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
9426 Python::attach(crate::x11::cursor::release);
9427 }
9428 }
9429 }
9430}
9431
9432fn recording_status_dict(
9434 py: Python<'_>,
9435 s: &crate::recorder::RecordingStatus,
9436) -> PyResult<Py<PyAny>> {
9437 let d = pyo3::types::PyDict::new(py);
9438 d.set_item("active", s.active)?;
9439 d.set_item("path", &s.path)?;
9440 d.set_item("backend", s.backend)?;
9441 d.set_item("mode", s.mode)?;
9442 d.set_item("frames", s.frames)?;
9443 d.set_item("sync_frames", s.sync_frames)?;
9444 d.set_item("audio_frames", s.audio_frames)?;
9445 d.set_item("dropped", s.dropped)?;
9446 d.set_item("skipped_non_h264", s.skipped_non_h264)?;
9447 d.set_item("bytes", s.bytes)?;
9448 d.set_item("duration_s", s.duration_s)?;
9449 d.set_item("width", s.width)?;
9450 d.set_item("height", s.height)?;
9451 d.set_item("error", s.error.as_deref())?;
9452 Ok(d.into_any().unbind())
9453}
9454
9455#[pyfunction]
9463#[pyo3(signature = (path, settings = None, audio_socket = ""))]
9464fn start_recording(
9465 py: Python<'_>,
9466 path: String,
9467 settings: Option<&Bound<'_, PyAny>>,
9468 audio_socket: &str,
9469) -> PyResult<Py<PyAny>> {
9470 let mut opts = crate::recorder::RecordOptions::from_env(path);
9471 if !audio_socket.is_empty() {
9472 opts.audio_socket = audio_socket.to_string();
9473 }
9474 if let Some(s) = settings {
9475 let rs = extract_settings(s)?;
9476 if rs.codec != Codec::H264 {
9477 return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
9478 "recording requires H.264 capture settings (codec='h264')",
9479 ));
9480 }
9481 opts.display_id = read_display_id(s);
9482 if let Some(explicit) = s
9483 .getattr("use_wayland")
9484 .ok()
9485 .and_then(|v| v.extract::<bool>().ok())
9486 {
9487 opts.backend = Some(if explicit {
9488 crate::recorder::PreferredBackend::Wayland
9489 } else {
9490 crate::recorder::PreferredBackend::X11
9491 });
9492 }
9493 opts.fps = 0.0;
9495 opts.bitrate_kbps = 0;
9496 opts.capture = Some(rs);
9497 }
9498 let status = py
9499 .detach(|| crate::recorder::start(opts))
9500 .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
9501 recording_status_dict(py, &status)
9502}
9503
9504#[pyfunction]
9507fn stop_recording(py: Python<'_>) -> PyResult<Py<PyAny>> {
9508 let status = py
9509 .detach(crate::recorder::stop)
9510 .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
9511 recording_status_dict(py, &status)
9512}
9513
9514#[pyfunction]
9517fn recording_status(py: Python<'_>) -> PyResult<Py<PyAny>> {
9518 match crate::recorder::status() {
9519 Some(s) => recording_status_dict(py, &s),
9520 None => Ok(py.None()),
9521 }
9522}
9523
9524#[pyfunction]
9532#[pyo3(signature = (width = 0, height = 0, render_node = String::new(), auto_gpu = String::new(), cursor_size = -1))]
9533fn ensure_wayland_display(
9534 py: Python<'_>,
9535 width: i32,
9536 height: i32,
9537 render_node: String,
9538 auto_gpu: String,
9539 cursor_size: i32,
9540) -> PyResult<String> {
9541 ensure_wayland_backend(
9542 py,
9543 width,
9544 height,
9545 render_node,
9546 auto_gpu,
9547 String::new(),
9548 cursor_size,
9549 )?;
9550 Ok(py
9551 .detach(|| wait_socket_name(Duration::from_secs(5)))
9552 .unwrap_or_default())
9553}
9554
9555#[pyfunction]
9573#[pyo3(signature = (render_node = String::new(), auto_gpu = String::new()))]
9574fn probe_wayland_gpu(py: Python<'_>, render_node: String, auto_gpu: String) -> PyResult<Py<PyAny>> {
9575 let (node, name, error) = py.detach(|| {
9576 let node = if render_node.is_empty() {
9577 auto_render_node(&auto_gpu)
9578 } else {
9579 Some(render_node)
9580 };
9581 let Some(node) = node else {
9582 return (String::new(), String::new(), "No render node".to_string());
9583 };
9584 let mut name = String::new();
9585 let result =
9586 gpu_render_init(std::path::Path::new(&node)).and_then(|(gbm, mut renderer)| {
9587 let bo = gbm
9588 .create_buffer_object::<()>(
9589 64,
9590 64,
9591 GbmFormat::Argb8888,
9592 BufferObjectFlags::RENDERING,
9593 )
9594 .map_err(|_| "Failed to allocate GBM buffer")?;
9595 bo.fd()
9596 .map_err(|e| format!("Failed to export dmabuf: {e:?}"))?;
9597 name = gl_renderer_name(&mut renderer);
9598 let lowered = name.to_lowercase();
9599 if ["llvmpipe", "softpipe", "swrast", "software rasterizer"]
9600 .iter()
9601 .any(|sw| lowered.contains(sw))
9602 {
9603 return Err(format!("Software rasterizer only ({name})"));
9604 }
9605 Ok(())
9606 });
9607 (node, name, result.err().unwrap_or_default())
9608 });
9609 let d = pyo3::types::PyDict::new(py);
9610 d.set_item("node", &node)?;
9611 d.set_item("accelerated", error.is_empty())?;
9612 d.set_item("gpu", gpu_exposed())?;
9613 d.set_item("renderer", &name)?;
9614 d.set_item("error", &error)?;
9615 Ok(d.into_any().unbind())
9616}
9617
9618#[pyfunction]
9627#[pyo3(signature = (encode_node_index = -2, auto_gpu = ""))]
9628fn hardware_encoders(
9629 py: Python<'_>,
9630 encode_node_index: i32,
9631 auto_gpu: &str,
9632) -> PyResult<Py<PyAny>> {
9633 let d = pyo3::types::PyDict::new(py);
9634 for (codec, backend, ..) in probe_hardware(py, encode_node_index, auto_gpu) {
9635 d.set_item(codec.name(), backend)?;
9636 }
9637 Ok(d.into_any().unbind())
9638}
9639
9640#[pyfunction]
9644#[pyo3(signature = (encode_node_index = -2, auto_gpu = ""))]
9645fn hardware_fullcolor(py: Python<'_>, encode_node_index: i32, auto_gpu: &str) -> Vec<&'static str> {
9646 probe_hardware(py, encode_node_index, auto_gpu)
9647 .into_iter()
9648 .filter(|&(_, _, formats)| formats.fullcolor)
9649 .map(|(codec, ..)| codec.name())
9650 .collect()
9651}
9652
9653#[pyfunction]
9658#[pyo3(signature = (encode_node_index = -2, auto_gpu = ""))]
9659fn hardware_formats(py: Python<'_>, encode_node_index: i32, auto_gpu: &str) -> PyResult<Py<PyAny>> {
9660 let d = pyo3::types::PyDict::new(py);
9661 for (codec, _, formats) in probe_hardware(py, encode_node_index, auto_gpu) {
9662 d.set_item(codec.name(), formats.names())?;
9663 }
9664 Ok(d.into_any().unbind())
9665}
9666
9667fn probe_hardware(
9670 py: Python<'_>,
9671 encode_node_index: i32,
9672 auto_gpu: &str,
9673) -> encoders::HardwareEncoders {
9674 let node = match encode_node_index {
9675 -1 => return Vec::new(),
9676 index if index < -1 => auto_render_node(auto_gpu)
9677 .and_then(|picked| render_node_index(&picked))
9678 .or_else(first_render_node_index)
9679 .unwrap_or(0),
9680 index => index,
9681 };
9682 py.detach(|| encoders::hardware_encoders(node))
9683}
9684
9685#[pyfunction]
9688fn get_wayland_display_name(py: Python<'_>) -> Option<String> {
9689 wayland_backend_running(py)?;
9690 py.detach(|| wait_socket_name(Duration::from_secs(2)))
9691}
9692
9693#[pyfunction]
9702fn _stop_all_captures(py: Python<'_>) {
9703 PY_SHUTDOWN.store(true, Ordering::Relaxed);
9704 py.detach(crate::recorder::finalize_on_exit);
9707 *PENDING_CURSOR_CALLBACK.lock().unwrap() = None;
9708 crate::x11::cursor::shutdown();
9709 crate::wayland::dcclient::unwatch_all();
9710 let x11: Vec<Arc<crate::x11::Controls>> = live_x11().lock().unwrap().iter().cloned().collect();
9716 for c in &x11 {
9717 c.stop.store(true, Ordering::Relaxed);
9718 }
9719 if !x11.is_empty() {
9720 let deadline = Instant::now() + Duration::from_secs(2);
9721 py.detach(|| {
9722 for c in &x11 {
9723 while !c.finished.load(Ordering::Acquire) && Instant::now() < deadline {
9724 std::thread::sleep(Duration::from_millis(5));
9725 }
9726 }
9727 });
9728 }
9729 let be = WAYLAND_BACKEND
9734 .get()
9735 .and_then(|slot| slot.lock().unwrap().as_ref().map(|b| b.clone_ref(py)));
9736 if let Some(be) = be {
9737 let be = be.bind(py).borrow();
9738 let mut displays: Vec<u32> = wayland_alive().lock().unwrap().iter().copied().collect();
9739 if !displays.contains(&0) {
9740 displays.push(0);
9741 }
9742 for did in displays {
9743 let _ = be.stop_capture(did);
9744 }
9745 let (ack_tx, ack_rx) = std::sync::mpsc::channel::<()>();
9749 if be.send(ThreadCommand::Barrier { reply: ack_tx }).is_ok() {
9750 let _ = py.detach(move || ack_rx.recv_timeout(Duration::from_secs(2)));
9751 }
9752 }
9753 wayland_owners().lock().unwrap().clear();
9754 wayland_alive().lock().unwrap().clear();
9755 py.detach(|| std::thread::sleep(Duration::from_millis(50)));
9756}
9757
9758fn app_socket_path(display: &str) -> Result<String, String> {
9762 crate::wayland::wlclient::socket_path(display)
9763 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())
9764}
9765
9766pyo3::create_exception!(
9767 pixelflux,
9768 VirtualKeyboardUnavailable,
9769 pyo3::exceptions::PyRuntimeError,
9770 "The target compositor does not advertise zwp_virtual_keyboard_manager_v1."
9771);
9772
9773#[pyfunction]
9779#[pyo3(signature = (bind, token = None))]
9780fn start_computer_use(bind: String, token: Option<String>) -> PyResult<()> {
9781 crate::computer_use::start_cu_server(&bind, token.as_deref())
9782 .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)
9783}
9784
9785#[pyfunction]
9790#[pyo3(signature = (display = 0))]
9791fn screenshot_png(py: Python<'_>, display: u32) -> PyResult<Py<PyAny>> {
9792 let png = py
9793 .detach(|| crate::computer_use::resolve_backend().and_then(|b| b.screenshot_png(display)))
9794 .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
9795 Ok(pyo3::types::PyBytes::new(py, &png).into_any().unbind())
9796}
9797
9798#[pymodule(gil_used = true)]
9808fn pixelflux(m: &Bound<'_, PyModule>) -> PyResult<()> {
9809 if std::env::var_os("SVT_LOG").is_none() {
9810 unsafe { std::env::set_var("SVT_LOG", "1") };
9811 }
9812 m.add_class::<WaylandBackend>()?;
9813 m.add_class::<StripeFrame>()?;
9814 m.add_class::<CaptureSettings>()?;
9815 m.add_class::<ScreenCapture>()?;
9816 m.add_class::<webcam::VirtualCamera>()?;
9817 m.add_class::<webcam::VirtualCameraSettings>()?;
9818 let software = pyo3::types::PyDict::new(m.py());
9822 for codec in Codec::VIDEO {
9823 if let Some(enc) = encoders::software_encoder(codec) {
9824 software.set_item(codec.name(), enc.library)?;
9825 }
9826 }
9827 m.add("SOFTWARE_ENCODERS", software)?;
9828 let fullcolor: Vec<&str> = Codec::VIDEO
9830 .into_iter()
9831 .filter(|&codec| encoders::software_fullcolor(codec))
9832 .map(|codec| codec.name())
9833 .collect();
9834 m.add("SOFTWARE_FULLCOLOR", fullcolor)?;
9835 let formats = pyo3::types::PyDict::new(m.py());
9838 for codec in Codec::VIDEO {
9839 if encoders::software_encoder(codec).is_some() {
9840 formats.set_item(codec.name(), encoders::software_formats(codec).names())?;
9841 }
9842 }
9843 m.add("SOFTWARE_FORMATS", formats)?;
9844 m.add_function(wrap_pyfunction!(hardware_encoders, m)?)?;
9845 m.add_function(wrap_pyfunction!(hardware_fullcolor, m)?)?;
9846 m.add_function(wrap_pyfunction!(hardware_formats, m)?)?;
9847 m.add_function(wrap_pyfunction!(stripe_frame_from_buffer, m)?)?;
9848 m.add_function(wrap_pyfunction!(ensure_wayland_display, m)?)?;
9849 m.add_function(wrap_pyfunction!(get_wayland_display_name, m)?)?;
9850 m.add_function(wrap_pyfunction!(probe_wayland_gpu, m)?)?;
9851 m.add_function(wrap_pyfunction!(start_recording, m)?)?;
9852 m.add_function(wrap_pyfunction!(stop_recording, m)?)?;
9853 m.add_function(wrap_pyfunction!(recording_status, m)?)?;
9854 m.add_function(wrap_pyfunction!(start_computer_use, m)?)?;
9855 m.add_function(wrap_pyfunction!(screenshot_png, m)?)?;
9856 m.add(
9857 "VirtualKeyboardUnavailable",
9858 m.py().get_type::<VirtualKeyboardUnavailable>(),
9859 )?;
9860 m.add_function(wrap_pyfunction!(_stop_all_captures, m)?)?;
9861 if let Ok(atexit) = m.py().import("atexit") {
9862 let _ = atexit.call_method1("register", (m.getattr("_stop_all_captures")?,));
9863 }
9864 crate::computer_use::spawn_cu_from_env();
9867 crate::recorder::autostart_from_env();
9870
9871 Ok(())
9872}
9873
9874#[cfg(test)]
9875mod annexb_frame_type_tests {
9876 use crate::encoders::h264_frame_type as annexb_frame_type;
9881
9882 const IDR_AU: &[u8] = &[
9883 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0d, 0xac, 0xd9,
9884 0x41, 0x41, 0xfb, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0xc0,
9885 0xf1, 0x42, 0x99, 0x60, 0x00, 0x00, 0x00, 0x01, 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, 0x00,
9886 0x00, 0x01, 0x06, 0x05, 0xff, 0xff, 0xa8, 0xdc, 0x45, 0xe9, 0xbd, 0xe6, 0xd9, 0x48, 0xb7,
9887 0x96, 0x2c, 0xd8, 0x20, 0xd9, 0x23, 0xee, 0xef, 0x78, 0x32, 0x36, 0x34, 0x20, 0x2d, 0x20,
9888 0x63, 0x6f, 0x72, 0x65, 0x20, 0x31, 0x36, 0x34, 0x20, 0x72, 0x33, 0x30, 0x39, 0x35, 0x20,
9889 0x62, 0x61, 0x65, 0x65, 0x34, 0x30, 0x30, 0x20, 0x2d, 0x20, 0x48, 0x2e, 0x32, 0x36, 0x34,
9890 0x2f, 0x4d, 0x50, 0x45, 0x47, 0x2d, 0x34, 0x20, 0x41, 0x56, 0x43, 0x20, 0x63, 0x6f, 0x64,
9891 0x65, 0x63, 0x20, 0x2d, 0x20, 0x43, 0x6f, 0x70, 0x79, 0x6c, 0x65, 0x66, 0x74, 0x20, 0x32,
9892 0x30, 0x30, 0x33, 0x2d, 0x32, 0x30, 0x32, 0x32, 0x20, 0x2d, 0x20, 0x68, 0x74, 0x74, 0x70,
9893 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x6c, 0x61, 0x6e,
9894 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x78, 0x32, 0x36, 0x34, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x20,
9895 0x2d, 0x20, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x20, 0x63, 0x61, 0x62, 0x61,
9896 0x63, 0x3d, 0x31, 0x20, 0x72, 0x65, 0x66, 0x3d, 0x33, 0x20, 0x64, 0x65, 0x62, 0x6c, 0x6f,
9897 0x63, 0x6b, 0x3d, 0x31, 0x3a, 0x30, 0x3a, 0x30, 0x20, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x73,
9898 0x65, 0x3d, 0x30, 0x78, 0x33, 0x3a, 0x30, 0x78, 0x31, 0x31, 0x33, 0x20, 0x6d, 0x65, 0x3d,
9899 0x68, 0x65, 0x78, 0x20, 0x73, 0x75, 0x62, 0x6d, 0x65, 0x3d, 0x37, 0x20, 0x70, 0x73, 0x79,
9900 0x3d, 0x31, 0x20, 0x70, 0x73, 0x79, 0x5f, 0x72, 0x64, 0x3d, 0x31, 0x2e, 0x30, 0x30, 0x3a,
9901 0x30, 0x2e, 0x30, 0x30, 0x20, 0x6d, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x66, 0x3d,
9902 0x31, 0x20, 0x6d, 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x3d, 0x31, 0x36, 0x20, 0x63,
9903 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f, 0x6d, 0x65, 0x3d, 0x31, 0x20, 0x74, 0x72, 0x65, 0x6c,
9904 0x6c, 0x69, 0x73, 0x3d, 0x31, 0x20, 0x38, 0x78, 0x38, 0x64, 0x63, 0x74, 0x3d, 0x31, 0x20,
9905 0x63, 0x71, 0x6d, 0x3d, 0x30, 0x20, 0x64, 0x65, 0x61, 0x64, 0x7a, 0x6f, 0x6e, 0x65, 0x3d,
9906 0x32, 0x31, 0x2c, 0x31, 0x31, 0x20, 0x66, 0x61, 0x73, 0x74, 0x5f, 0x70, 0x73, 0x6b, 0x69,
9907 0x70, 0x3d, 0x31, 0x20, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x61, 0x5f, 0x71, 0x70, 0x5f, 0x6f,
9908 0x66, 0x66, 0x73, 0x65, 0x74, 0x3d, 0x2d, 0x32, 0x20, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64,
9909 0x73, 0x3d, 0x37, 0x20, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x5f, 0x74,
9910 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x31, 0x20, 0x73, 0x6c, 0x69, 0x63, 0x65, 0x64,
9911 0x5f, 0x74, 0x68, 0x72, 0x65, 0x61, 0x64, 0x73, 0x3d, 0x30, 0x20, 0x6e, 0x72, 0x3d, 0x30,
9912 0x20, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x74, 0x65, 0x3d, 0x31, 0x20, 0x69, 0x6e, 0x74,
9913 0x65, 0x72, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x3d, 0x30, 0x20, 0x62, 0x6c, 0x75, 0x72, 0x61,
9914 0x79, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x3d, 0x30, 0x20, 0x63, 0x6f, 0x6e, 0x73,
9915 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x6e, 0x74, 0x72, 0x61, 0x3d, 0x30,
9916 0x20, 0x62, 0x66, 0x72, 0x61, 0x6d, 0x65, 0x73, 0x3d, 0x32, 0x20, 0x62, 0x5f, 0x70, 0x79,
9917 0x72, 0x61, 0x6d, 0x69, 0x64, 0x3d, 0x32, 0x20, 0x62, 0x5f, 0x61, 0x64, 0x61, 0x70, 0x74,
9918 0x3d, 0x31, 0x20, 0x62, 0x5f, 0x62, 0x69, 0x61, 0x73, 0x3d, 0x30, 0x20, 0x64, 0x69, 0x72,
9919 0x65, 0x63, 0x74, 0x3d, 0x31, 0x20, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x62, 0x3d, 0x31,
9920 0x20, 0x6f, 0x70, 0x65, 0x6e, 0x5f, 0x67, 0x6f, 0x70, 0x3d, 0x31, 0x20, 0x77, 0x65, 0x69,
9921 0x67, 0x68, 0x74, 0x70, 0x3d, 0x32, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x3d, 0x31,
9922 0x32, 0x20, 0x6b, 0x65, 0x79, 0x69, 0x6e, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x3d, 0x37, 0x20,
9923 0x73, 0x63, 0x65, 0x6e, 0x65, 0x63, 0x75, 0x74, 0x3d, 0x34, 0x30, 0x20, 0x69, 0x6e, 0x74,
9924 0x72, 0x61, 0x5f, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x3d, 0x30, 0x20, 0x72, 0x63,
9925 0x5f, 0x6c, 0x6f, 0x6f, 0x6b, 0x61, 0x68, 0x65, 0x61, 0x64, 0x3d, 0x31, 0x32, 0x20, 0x72,
9926 0x63, 0x3d, 0x63, 0x72, 0x66, 0x20, 0x6d, 0x62, 0x74, 0x72, 0x65, 0x65, 0x3d, 0x31, 0x20,
9927 0x63, 0x72, 0x66, 0x3d, 0x32, 0x33, 0x2e, 0x30, 0x20, 0x71, 0x63, 0x6f, 0x6d, 0x70, 0x3d,
9928 0x30, 0x2e, 0x36, 0x30, 0x20, 0x71, 0x70, 0x6d, 0x69, 0x6e, 0x3d, 0x30, 0x20, 0x71, 0x70,
9929 0x6d, 0x61, 0x78, 0x3d, 0x36, 0x39, 0x20, 0x71, 0x70, 0x73, 0x74, 0x65, 0x70, 0x3d, 0x34,
9930 0x20, 0x69, 0x70, 0x5f, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x3d, 0x31, 0x2e, 0x34, 0x30, 0x20,
9931 0x61, 0x71, 0x3d, 0x31, 0x3a, 0x31, 0x2e, 0x30, 0x30, 0x00, 0x80, 0x00, 0x00, 0x01, 0x65,
9932 0x88, 0x84, 0x00, 0x47, 0xda, 0xe3, 0x98, 0x5b, 0xd7, 0x57, 0xda, 0x42, 0x3e, 0x83, 0x89,
9933 0x96, 0xcb, 0xc5, 0x1b,
9934 ];
9935 const INTRA_AU: &[u8] = &[
9936 0x00, 0x00, 0x01, 0x09, 0x10, 0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x0d, 0xac, 0xd9,
9937 0x41, 0x41, 0xfb, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00, 0x10, 0x00, 0x00, 0x03, 0x03, 0xc0,
9938 0xf1, 0x42, 0x99, 0x60, 0x00, 0x00, 0x00, 0x01, 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, 0x00,
9939 0x00, 0x01, 0x06, 0x06, 0x01, 0xc4, 0x80, 0x00, 0x00, 0x01, 0x41, 0x88, 0xc3, 0x05, 0xff,
9940 0xd4, 0x57, 0x6d, 0x62, 0x78, 0xad, 0x3e, 0x89, 0xb4, 0xb5, 0x2a, 0xde, 0xcb, 0x0c, 0x64,
9941 ];
9942 const P_AU: &[u8] = &[
9943 0x00, 0x00, 0x01, 0x09, 0x30, 0x00, 0x00, 0x01, 0x41, 0x9a, 0x23, 0x6c, 0x45, 0x7f, 0xb7,
9944 0xe7, 0xf9, 0x23, 0x5b, 0xd9, 0xab, 0xc7, 0x7f, 0xcc, 0x03, 0xc4, 0xaa, 0x6f,
9945 ];
9946 const B_AU: &[u8] = &[
9947 0x00, 0x00, 0x01, 0x09, 0x50, 0x00, 0x00, 0x01, 0x41, 0x9e, 0x41, 0x78, 0x85, 0x3f, 0xfd,
9948 0xb1, 0x75, 0x99, 0x50, 0x2e, 0xe9, 0x5c, 0x1a, 0x0f, 0x32, 0x6a, 0xa7, 0x3d,
9949 ];
9950 const INTRA_VCL: &[u8] = &[
9951 0x00, 0x00, 0x01, 0x41, 0x88, 0xc3, 0x05, 0xff, 0xd4, 0x57, 0x6d, 0x62, 0x78, 0xad, 0x3e,
9952 0x89, 0xb4, 0xb5, 0x2a, 0xde, 0xcb, 0x0c, 0x64, 0x72, 0xe8, 0x3d, 0x54,
9953 ];
9954 const P_VCL: &[u8] = &[
9955 0x00, 0x00, 0x01, 0x41, 0x9a, 0x23, 0x6c, 0x45, 0x7f, 0xb7, 0xe7, 0xf9, 0x23, 0x5b, 0xd9,
9956 0xab, 0xc7, 0x7f, 0xcc, 0x03, 0xc4, 0xaa, 0x6f, 0x9a, 0xd8, 0xcf, 0x4a,
9957 ];
9958 const EPB_INTRA_VCL: &[u8] = &[
9962 0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x03, 0x02, 0x00, 0x00, 0x08, 0x88,
9963 ];
9964
9965 #[test]
9966 fn idr_is_key() {
9967 assert_eq!(annexb_frame_type(IDR_AU), 0x01);
9968 }
9969
9970 #[test]
9971 fn open_gop_intra_is_intra_not_key() {
9972 assert_eq!(annexb_frame_type(INTRA_AU), 0x02);
9973 }
9974
9975 #[test]
9976 fn predicted_frames_are_delta() {
9977 assert_eq!(annexb_frame_type(P_AU), 0x00);
9978 assert_eq!(annexb_frame_type(B_AU), 0x00);
9979 }
9980
9981 #[test]
9982 fn one_predicted_slice_downgrades_a_mixed_unit() {
9983 let mixed = [INTRA_VCL, P_VCL].concat();
9984 assert_eq!(annexb_frame_type(&mixed), 0x00);
9985 }
9986
9987 #[test]
9988 fn unparsable_slice_header_is_delta() {
9989 assert_eq!(annexb_frame_type(&[0x00, 0x00, 0x01, 0x41]), 0x00);
9990 assert_eq!(annexb_frame_type(&[]), 0x00);
9991 }
9992
9993 #[test]
9994 fn emulation_prevention_is_stripped_before_parsing() {
9995 assert_eq!(annexb_frame_type(EPB_INTRA_VCL), 0x02);
9996 }
9997}
9998
9999#[cfg(test)]
10000mod encode_node_tests {
10001 use super::{auto_render_node, render_node_index};
10004
10005 #[test]
10006 fn render_node_path_counts_from_128() {
10007 assert_eq!(render_node_index("/dev/dri/renderD128"), Some(0));
10008 assert_eq!(render_node_index("/dev/dri/renderD130"), Some(2));
10009 assert_eq!(render_node_index("/dev/dri/card0"), None);
10010 }
10011
10012 #[test]
10013 fn auto_gpu_off_picks_nothing() {
10014 assert_eq!(auto_render_node("false"), None);
10015 assert_eq!(auto_render_node(""), None);
10016 }
10017}
10018
10019#[cfg(test)]
10020mod shm_usage_tests {
10021 use super::shm_usage_in;
10024
10025 #[test]
10026 fn only_allocated_blocks_count() {
10027 let dir = std::env::temp_dir().join(format!("pixelflux-shm-{}", std::process::id()));
10028 std::fs::create_dir_all(&dir).unwrap();
10029 let path = dir.to_str().unwrap().to_string();
10030
10031 std::fs::File::create(dir.join("sparse"))
10032 .unwrap()
10033 .set_len(8 * 1024 * 1024 * 1024)
10034 .unwrap();
10035 let sparse = shm_usage_in(&path);
10036
10037 std::fs::write(dir.join("dense"), vec![0u8; 4 * 1024 * 1024]).unwrap();
10038 let dense = shm_usage_in(&path);
10039
10040 std::fs::remove_dir_all(&dir).ok();
10041 assert!(
10042 sparse < 1024 * 1024,
10043 "an 8 GiB sparse file counted {sparse} bytes"
10044 );
10045 assert!(
10046 dense - sparse >= 4 * 1024 * 1024,
10047 "a 4 MiB written file added only {} bytes",
10048 dense - sparse
10049 );
10050 }
10051}
10052
10053#[cfg(test)]
10054mod capture_state_tests {
10055 use super::{set_wayland_capture_err, wayland_alive, wayland_capture_state};
10059
10060 #[test]
10061 fn state_tracks_liveness_and_recorded_error() {
10062 let did = 987_654;
10064 set_wayland_capture_err(did, None);
10065 wayland_alive().lock().unwrap().remove(&did);
10066 assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
10067
10068 set_wayland_capture_err(did, Some("no output".to_string()));
10070 assert_eq!(
10071 wayland_capture_state(did),
10072 ("failed".to_string(), Some("no output".to_string()))
10073 );
10074
10075 wayland_alive().lock().unwrap().insert(did);
10077 set_wayland_capture_err(did, Some("using CPU encode".to_string()));
10078 assert_eq!(
10079 wayland_capture_state(did),
10080 ("running".to_string(), Some("using CPU encode".to_string()))
10081 );
10082
10083 wayland_alive().lock().unwrap().remove(&did);
10085 set_wayland_capture_err(did, None);
10086 assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
10087 }
10088}
10089
10090#[cfg(test)]
10091mod host_layout_tests {
10092 use super::host_layout_resolution;
10096
10097 #[test]
10098 fn the_capture_follows_the_mode_the_host_runs() {
10099 let want = (1920, 1080);
10100 assert_eq!(host_layout_resolution(want, Some(want)), None);
10102 assert_eq!(host_layout_resolution(want, None), None);
10104 assert_eq!(
10109 host_layout_resolution(want, Some((2560, 1440))),
10110 Some((2560, 1440))
10111 );
10112 assert_eq!(
10113 host_layout_resolution(want, Some((1280, 720))),
10114 Some((1280, 720))
10115 );
10116 }
10117}
10118
10119#[cfg(test)]
10120mod output_overlap_tests {
10121 use super::rects_overlap;
10125
10126 #[test]
10127 fn disjoint_rects_do_not_overlap() {
10128 assert!(!rects_overlap((0, 0, 100, 100), (200, 0, 100, 100)));
10129 assert!(!rects_overlap((0, 0, 100, 100), (0, 200, 100, 100)));
10130 }
10131
10132 #[test]
10133 fn touching_edges_do_not_overlap() {
10134 assert!(!rects_overlap((0, 0, 100, 100), (100, 0, 100, 100)));
10136 assert!(!rects_overlap((0, 0, 100, 100), (0, 100, 100, 100)));
10137 assert!(!rects_overlap((0, 0, 100, 100), (100, 100, 50, 50)));
10139 }
10140
10141 #[test]
10142 fn one_pixel_intrusion_overlaps() {
10143 assert!(rects_overlap((0, 0, 100, 100), (99, 0, 100, 100)));
10144 assert!(rects_overlap((0, 0, 100, 100), (0, 99, 100, 100)));
10145 }
10146
10147 #[test]
10148 fn containment_and_identity_overlap() {
10149 assert!(rects_overlap((0, 0, 100, 100), (25, 25, 10, 10)));
10150 assert!(rects_overlap((25, 25, 10, 10), (0, 0, 100, 100)));
10151 assert!(rects_overlap((5, 5, 50, 50), (5, 5, 50, 50)));
10152 }
10153
10154 #[test]
10155 fn empty_or_negative_rects_never_overlap() {
10156 assert!(!rects_overlap((10, 10, 0, 50), (0, 0, 100, 100)));
10157 assert!(!rects_overlap((10, 10, 50, 0), (0, 0, 100, 100)));
10158 assert!(!rects_overlap((10, 10, -5, 5), (0, 0, 100, 100)));
10159 assert!(!rects_overlap((0, 0, 100, 100), (10, 10, 0, 0)));
10160 }
10161
10162 #[test]
10163 fn negative_origins_overlap_correctly() {
10164 assert!(rects_overlap((-50, -50, 100, 100), (0, 0, 100, 100)));
10165 assert!(!rects_overlap((-100, -100, 100, 100), (0, 0, 100, 100)));
10166 }
10167
10168 #[test]
10169 fn extreme_coordinates_do_not_wrap() {
10170 assert!(!rects_overlap(
10171 (i32::MAX - 10, 0, 10, 10),
10172 (i32::MIN, 0, 10, 10)
10173 ));
10174 assert!(rects_overlap(
10175 (i32::MAX - 10, 0, 10, 10),
10176 (i32::MAX - 5, 0, 10, 10)
10177 ));
10178 }
10179}
10180
10181#[cfg(test)]
10182mod wl_frame_pool_tests {
10183 use super::*;
10188
10189 fn frame(id: usize, buf: Vec<u8>, n: u16) -> WlFrame {
10190 WlFrame {
10191 id,
10192 buf,
10193 frame_id: n,
10194 damage: Vec::new(),
10195 is_animated: false,
10196 captured_ns: 0,
10197 }
10198 }
10199
10200 #[test]
10201 fn begin_gated_on_slot_and_free_list() {
10202 let p = WlFramePool::new(2, 16);
10203 let (a, abuf) = p.try_begin().expect("first buffer");
10204 let (b, bbuf) = p.try_begin().expect("second buffer");
10205 assert_ne!(a, b);
10206 assert!(p.try_begin().is_none(), "free list exhausted");
10207 p.publish(frame(a, abuf, 0));
10208 p.cancel(b, bbuf);
10209 assert!(p.try_begin().is_none(), "slot occupied blocks begin");
10210 let f = p.take().expect("published frame");
10211 assert_eq!(f.frame_id, 0);
10212 p.recycle(f.id, f.buf);
10213 assert!(p.try_begin().is_some(), "drained slot re-enables begin");
10214 }
10215
10216 #[test]
10217 fn frames_flow_in_order_and_buffers_recycle() {
10218 let p = Arc::new(WlFramePool::new(2, 4));
10219 let p2 = p.clone();
10220 let consumer = thread::spawn(move || {
10221 let mut seen = Vec::new();
10222 while let Some(f) = p2.take() {
10223 seen.push(f.frame_id);
10224 p2.recycle(f.id, f.buf);
10225 }
10226 seen
10227 });
10228 let mut published = 0u16;
10229 while published < 50 {
10230 if let Some((id, buf)) = p.try_begin() {
10231 p.publish(frame(id, buf, published));
10232 published += 1;
10233 } else {
10234 thread::sleep(Duration::from_micros(50));
10235 }
10236 }
10237 thread::sleep(Duration::from_millis(50));
10238 p.shutdown();
10239 let seen = consumer.join().unwrap();
10240 assert_eq!(seen, (0..50).collect::<Vec<u16>>(), "every frame, in order");
10241 }
10242
10243 #[test]
10244 fn shutdown_unblocks_take() {
10245 let p = Arc::new(WlFramePool::new(1, 4));
10246 let p2 = p.clone();
10247 let t = thread::spawn(move || p2.take());
10248 thread::sleep(Duration::from_millis(30));
10249 p.shutdown();
10250 assert!(t.join().unwrap().is_none(), "take returns None on shutdown");
10251 }
10252
10253 #[test]
10254 fn cancel_returns_buffer_for_reuse() {
10255 let p = WlFramePool::new(1, 8);
10256 let (id, buf) = p.try_begin().expect("buffer");
10257 assert!(p.try_begin().is_none());
10258 p.cancel(id, buf);
10259 assert!(p.try_begin().is_some(), "canceled reservation reusable");
10260 }
10261}
10262
10263#[cfg(test)]
10264mod auto_gpu_token_tests {
10265 use super::{CardIdentity, card_matches_token};
10270
10271 fn pci(driver: &str, vendor: u32) -> CardIdentity {
10272 CardIdentity {
10273 driver: driver.into(),
10274 pci_vendor: Some(vendor),
10275 compatibles: vec![],
10276 }
10277 }
10278
10279 fn dt(driver: &str, compatibles: &[&str]) -> CardIdentity {
10280 CardIdentity {
10281 driver: driver.into(),
10282 pci_vendor: None,
10283 compatibles: compatibles.iter().map(|c| c.to_string()).collect(),
10284 }
10285 }
10286
10287 #[test]
10288 fn driver_names_match_without_any_table() {
10289 assert!(card_matches_token("amdgpu", &pci("amdgpu", 0x1002)));
10290 assert!(card_matches_token(
10291 "panfrost",
10292 &dt("panfrost", &["rockchip,rk3399-mali"])
10293 ));
10294 assert!(card_matches_token("nouveau", &pci("nouveau", 0x10de)));
10295 assert!(!card_matches_token("i915", &pci("amdgpu", 0x1002)));
10296 }
10297
10298 #[test]
10299 fn vendor_names_and_raw_ids_match_pci_identity() {
10300 let nv = pci("nouveau", 0x10de);
10301 assert!(card_matches_token("nvidia", &nv));
10302 assert!(card_matches_token("0x10de", &nv));
10303 assert!(card_matches_token("10de", &nv));
10304 assert!(!card_matches_token("amd", &nv));
10305 assert!(card_matches_token("ati", &pci("radeon", 0x1002)));
10306 }
10307
10308 #[test]
10309 fn devicetree_prefixes_match_literally_and_via_aliases() {
10310 let mali = dt("panfrost", &["rockchip,rk3399-mali", "arm,mali-t860"]);
10311 assert!(card_matches_token("rockchip", &mali));
10312 assert!(card_matches_token("arm", &mali));
10313 assert!(card_matches_token("mali", &mali));
10314 let adreno = dt("msm", &["qcom,adreno-630", "qcom,adreno"]);
10315 assert!(card_matches_token("qcom", &adreno));
10316 assert!(card_matches_token("qualcomm", &adreno));
10317 assert!(card_matches_token("adreno", &adreno));
10318 assert!(!card_matches_token("brcm", &adreno));
10319 assert!(card_matches_token(
10320 "videocore",
10321 &dt("v3d", &["brcm,bcm2711-v3d"])
10322 ));
10323 }
10324
10325 #[test]
10326 fn missing_identity_fields_never_false_match() {
10327 let bare = CardIdentity {
10328 driver: String::new(),
10329 pci_vendor: None,
10330 compatibles: vec![],
10331 };
10332 for t in ["nvidia", "amdgpu", "0x10de", "qcom"] {
10333 assert!(!card_matches_token(t, &bare));
10334 }
10335 }
10336}