1#![allow(dead_code)]
54
55use std::fs::File;
56use std::sync::Arc;
57use std::thread;
58use std::time::{Duration, Instant};
59
60use gbm::{BufferObject, BufferObjectFlags, Device as RawGbmDevice, Format as GbmFormat};
61use pyo3::prelude::*;
62use pyo3::types::{PyAny, PyModule};
63use yuv::{
64 BufferStoreMut, YuvBiPlanarImageMut, YuvConversionMode, YuvRange, YuvStandardMatrix,
65};
66
67use smithay::wayland::single_pixel_buffer::SinglePixelBufferState;
68use smithay::wayland::viewporter::ViewporterState;
69use smithay::wayland::presentation::{PresentationState, Refresh};
70use smithay::wayland::image_capture_source::{ImageCaptureSourceState, OutputCaptureSourceState};
71use smithay::wayland::image_copy_capture::{CaptureFailureReason, ImageCopyCaptureState};
72use smithay::desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback};
73use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
74use smithay::wayland::selection::wlr_data_control::DataControlState;
75use smithay::wayland::selection::ext_data_control::DataControlState as ExtDataControlState;
76use smithay::wayland::cursor_shape::CursorShapeManagerState;
77use smithay::{
78 backend::{
79 allocator::{
80 dmabuf::{Dmabuf, DmabufFlags},
81 gbm::GbmDevice,
82 Fourcc, Modifier,
83 },
84 drm::DrmNode,
85 egl::{EGLContext, EGLDisplay},
86 input::{Axis, AxisSource, KeyState, Keycode},
87 renderer::{
88 damage::OutputDamageTracker,
89 element::{
90 memory::MemoryRenderBufferRenderElement,
91 surface::WaylandSurfaceRenderElement,
92 AsRenderElements, Element, RenderElement, Wrap,
93 },
94 gles::GlesRenderer,
95 pixman::PixmanRenderer,
96 sync::SyncPoint,
97 Bind, ExportMem, Frame as _, ImportAll, ImportDma, ImportEgl, ImportMem,
98 Renderer as _,
99 },
100 },
101 desktop::{space::SpaceRenderElements, Space},
102 input::{
103 keyboard::{FilterResult, XkbConfig},
104 pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent, RelativeMotionEvent},
105 SeatState,
106 },
107 output::{Mode as OutputMode, Output, PhysicalProperties, Scale as OutputScale, Subpixel},
108 reexports::{
109 calloop::{
110 generic::Generic, timer::{TimeoutAction, Timer},
111 EventLoop, Interest, Mode, PostAction,
112 },
113 pixman,
114 wayland_server::{Display, DisplayHandle},
115 },
116 utils::{Clock, Physical, Point, Rectangle, Scale, Transform},
117 wayland::{
118 compositor::{with_states, CompositorState},
119 dmabuf::{DmabufFeedbackBuilder, DmabufState},
120 fractional_scale::FractionalScaleManagerState,
121 output::OutputManagerState,
122 selection::data_device::DataDeviceState,
123 seat::WaylandFocus,
124 shell::xdg::XdgShellState,
125 shm::ShmState,
126 socket::ListeningSocketSource,
127 pointer_warp::PointerWarpManager,
128 relative_pointer::RelativePointerManagerState,
129 pointer_constraints::PointerConstraintsState,
130 foreign_toplevel_list::ForeignToplevelListState,
131 shell::xdg::decoration::XdgDecorationState,
132 },
133 desktop::{layer_map_for_output, PopupManager},
134 wayland::shell::wlr_layer::WlrLayerShellState,
135 wayland::xdg_activation::XdgActivationState,
136 wayland::selection::primary_selection::PrimarySelectionState,
137};
138
139pub mod encoders {
140 pub mod nvenc;
142 #[cfg(any(feature = "openh264", test))]
145 pub mod oh264;
146 pub mod overlay;
148 pub mod software;
151 pub mod vaapi;
153
154 #[cfg(not(any(feature = "gpl", feature = "openh264")))]
155 compile_error!(
156 "pixelflux needs a software H.264 encoder: enable the `gpl` feature (libx264, the default) or `openh264`."
157 );
158
159 #[cfg(feature = "gpl")]
164 pub const SOFTWARE_H264_ENCODER: &str = "x264";
165 #[cfg(not(feature = "gpl"))]
166 pub const SOFTWARE_H264_ENCODER: &str = "openh264";
167
168 pub const SOFTWARE_H264_FULLCOLOR: bool = cfg!(feature = "gpl");
172
173 pub(crate) const QP_HYSTERESIS_LIMIT: u32 = 60;
181
182 pub(crate) fn min_h264_level(width: u32, height: u32, fps: u32) -> u32 {
192 let mbs = (width as u64).div_ceil(16) * (height as u64).div_ceil(16);
193 let mbps = mbs * fps.max(1) as u64;
194 const LEVELS: [(u32, u64, u64); 8] = [
195 (41, 8192, 245760),
196 (42, 8704, 522240),
197 (50, 22080, 589824),
198 (51, 36864, 983040),
199 (52, 36864, 2073600),
200 (60, 139264, 4177920),
201 (61, 139264, 8355840),
202 (62, 139264, 16711680),
203 ];
204 for &(level, max_fs, max_mbps) in &LEVELS {
205 if mbs <= max_fs && mbps <= max_mbps {
206 return level;
207 }
208 }
209 62
210 }
211
212 pub fn vbv_bits(bitrate_bps: u32, fps: f64, keyframe_interval_s: f64, multiplier: f64) -> u32 {
231 let frame_bits = bitrate_bps as f64 / fps.max(1.0);
232 let mult = if multiplier > 0.0 {
233 multiplier
234 } else if keyframe_interval_s > 0.0 {
235 3.0
236 } else {
237 1.5
238 };
239 (frame_bits * mult).round().max(1.0).min(u32::MAX as f64) as u32
240 }
241
242 pub fn colorspace_desc(fullcolor: bool, software: bool) -> &'static str {
247 match (fullcolor, software) {
248 (true, true) => "I444 (Full Range)",
249 (true, false) => "I444 (Limited Range)",
250 _ => "I420 (Limited Range)",
251 }
252 }
253}
254
255pub mod wayland;
257pub mod recording_sink;
259pub mod recorder;
261pub mod computer_use;
263pub mod pipeline;
265pub mod x11;
267pub mod nvgpufilter;
269
270pub mod webcam;
271
272pub use encoders::nvenc;
273pub use encoders::software::StripeState;
274pub use encoders::vaapi;
275
276fn get_process_rss_bytes() -> usize {
277 if let Ok(contents) = std::fs::read_to_string("/proc/self/statm")
278 && let Some(rss_pages) = contents.split_whitespace().nth(1)
279 && let Ok(pages) = rss_pages.parse::<usize>() {
280 return pages * 4096;
281 }
282 0
283}
284
285fn shm_usage_in(dir: &str) -> u64 {
288 use std::os::unix::fs::MetadataExt;
289 let mut total_size = 0;
290 if let Ok(entries) = std::fs::read_dir(dir) {
291 for entry in entries.flatten() {
292 if let Ok(metadata) = entry.metadata() {
293 total_size += metadata.blocks() * 512;
294 }
295 }
296 }
297 total_size
298}
299
300fn get_shm_usage_bytes() -> u64 {
301 shm_usage_in("/dev/shm")
302}
303
304use encoders::nvenc::NvencEncoder;
305use encoders::overlay::OverlayState;
306use encoders::software::MAX_STRIPE_CAPACITY;
307use encoders::vaapi::VaapiEncoder;
308
309use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1;
310
311use wayland::cursor::{Cursor, CursorJob};
312use wayland::frontend::{AppState, ClientState, FocusTarget, GpuEncoder, next_serial, wayland_time, wayland_utime};
313
314smithay::backend::renderer::element::render_elements! {
315 pub CompositionElements<R, E> where R: ImportAll + ImportMem;
316 Space=SpaceRenderElements<R, E>,
317 Window=Wrap<E>,
318 Cursor=MemoryRenderBufferRenderElement<R>,
319 Surface=WaylandSurfaceRenderElement<R>,
320}
321
322pub(crate) fn create_dmabuf_from_bo(bo: &BufferObject<()>) -> Dmabuf {
330 let fd = bo.fd().expect("Failed to get FD from GBM BO");
331 let modifier = bo.modifier();
332 let stride = bo.stride();
333 let width = bo.width();
334 let height = bo.height();
335
336 let drm_modifier = Modifier::from(Into::<u64>::into(modifier));
337
338 let mut builder = Dmabuf::builder(
339 (width as i32, height as i32),
340 Fourcc::Argb8888,
341 drm_modifier,
342 DmabufFlags::empty(),
343 );
344
345 builder.add_plane(fd, 0, 0, stride);
346 builder.build().expect("Failed to build Dmabuf from GBM BO")
347}
348
349#[derive(Clone, Debug, PartialEq)]
357pub struct RustCaptureSettings {
358 pub width: i32,
359 pub height: i32,
360 pub scale: f64,
361 pub capture_x: i32,
362 pub capture_y: i32,
363 pub target_fps: f64,
364 pub jpeg_quality: i32,
365 pub paint_over_jpeg_quality: i32,
366 pub use_paint_over_quality: bool,
367 pub paint_over_trigger_frames: u32,
368 pub damage_block_threshold: u32,
369 pub damage_block_duration: u32,
370 pub output_mode: i32,
371 pub video_crf: i32,
372 pub video_paintover_crf: i32,
373 pub video_paintover_burst_frames: i32,
374 pub video_fullcolor: bool,
375 pub video_fullframe: bool,
376 pub video_streaming_mode: bool,
377 pub capture_cursor: bool,
378 pub cursor_size_cap: i32,
381 pub watermark_path: String,
382 pub watermark_location_enum: i32,
383 pub encode_node_index: i32,
384 pub use_cpu: bool,
385 pub debug_logging: bool,
386 pub auto_adjust_screen_capture_size: bool,
387 pub recording_socket: String,
388 pub wayland_host_display: String,
391 pub omit_stripe_headers: bool,
394 pub video_cbr_mode: bool,
395 pub video_bitrate_kbps: i32,
396 pub video_vbv_multiplier: f64,
400 pub keyframe_interval_s: f64,
403 pub video_min_qp: i32,
408 pub video_max_qp: i32,
409}
410
411#[derive(Clone, Copy, Debug)]
417pub struct LiveTunables {
418 pub jpeg_quality: i32,
419 pub paint_over_jpeg_quality: i32,
420 pub use_paint_over_quality: bool,
421 pub paint_over_trigger_frames: u32,
422 pub video_crf: i32,
423 pub video_paintover_crf: i32,
424 pub video_paintover_burst_frames: i32,
425 pub video_streaming_mode: bool,
426 pub keyframe_interval_s: f64,
427 pub capture_cursor: bool,
428 pub cursor_size_cap: i32,
429}
430
431impl LiveTunables {
432 pub fn from_settings(s: &RustCaptureSettings) -> Self {
434 Self {
435 jpeg_quality: s.jpeg_quality,
436 paint_over_jpeg_quality: s.paint_over_jpeg_quality,
437 use_paint_over_quality: s.use_paint_over_quality,
438 paint_over_trigger_frames: s.paint_over_trigger_frames,
439 video_crf: s.video_crf,
440 video_paintover_crf: s.video_paintover_crf,
441 video_paintover_burst_frames: s.video_paintover_burst_frames,
442 video_streaming_mode: s.video_streaming_mode,
443 keyframe_interval_s: s.keyframe_interval_s,
444 capture_cursor: s.capture_cursor,
445 cursor_size_cap: s.cursor_size_cap,
446 }
447 }
448
449 pub fn apply_to(&self, s: &mut RustCaptureSettings) {
451 s.jpeg_quality = self.jpeg_quality;
452 s.paint_over_jpeg_quality = self.paint_over_jpeg_quality;
453 s.use_paint_over_quality = self.use_paint_over_quality;
454 s.paint_over_trigger_frames = self.paint_over_trigger_frames;
455 s.video_crf = self.video_crf;
456 s.video_paintover_crf = self.video_paintover_crf;
457 s.video_paintover_burst_frames = self.video_paintover_burst_frames;
458 s.video_streaming_mode = self.video_streaming_mode;
459 s.keyframe_interval_s = self.keyframe_interval_s;
460 s.cursor_size_cap = self.cursor_size_cap;
461 s.capture_cursor = self.capture_cursor;
462 }
463}
464
465impl Default for RustCaptureSettings {
466 fn default() -> Self {
467 Self {
468 width: 1024,
469 height: 768,
470 scale: 1.0,
471 capture_x: 0,
472 capture_y: 0,
473 target_fps: 60.0,
474 jpeg_quality: 75,
475 paint_over_jpeg_quality: 95,
476 use_paint_over_quality: true,
477 paint_over_trigger_frames: 15,
478 damage_block_threshold: 10,
479 damage_block_duration: 30,
480 output_mode: 0,
481 video_crf: 25,
482 video_paintover_crf: 18,
483 video_paintover_burst_frames: 5,
484 video_fullcolor: false,
485 video_fullframe: false,
486 video_streaming_mode: false,
487 capture_cursor: false,
488 cursor_size_cap: 32,
489 watermark_path: String::new(),
490 watermark_location_enum: 0,
491 encode_node_index: -2,
492 use_cpu: false,
493 debug_logging: false,
494 auto_adjust_screen_capture_size: false,
495 recording_socket: String::new(),
496 wayland_host_display: String::new(),
497 omit_stripe_headers: false,
498 video_cbr_mode: false,
499 video_bitrate_kbps: 4000,
500 video_vbv_multiplier: 0.0,
501 keyframe_interval_s: 0.0,
502 video_min_qp: 0,
503 video_max_qp: 0,
504 }
505 }
506}
507
508pub(crate) fn extract_settings(settings: &Bound<'_, PyAny>) -> PyResult<RustCaptureSettings> {
526 let watermark_path_obj = settings.getattr("watermark_path")?;
527 let watermark_path = if let Ok(s) = watermark_path_obj.extract::<String>() {
528 s
529 } else if let Ok(b) = watermark_path_obj.extract::<Vec<u8>>() {
530 String::from_utf8_lossy(&b).into_owned()
531 } else {
532 String::new()
533 };
534
535 let scale = settings
536 .getattr("scale")
537 .ok()
538 .and_then(|x| x.extract().ok())
539 .unwrap_or(1.0);
540
541 let sanitize_dim = |v: i32| -> i32 {
546 if v <= 0 { 0 } else { v.min(MAX_CAPTURE_DIM) }
547 };
548 let sanitize_fps = |v: f64| -> f64 {
549 if v.is_finite() && v > 0.0 { v.min(MAX_FPS) } else { DEFAULT_FPS }
550 };
551 let sanitize_scale = |v: f64| -> f64 {
552 if v.is_finite() && v > 0.0 { v.min(MAX_SCALE) } else { 1.0 }
553 };
554
555 Ok(RustCaptureSettings {
556 width: sanitize_dim(settings.getattr("capture_width")?.extract()?),
557 height: sanitize_dim(settings.getattr("capture_height")?.extract()?),
558 scale: sanitize_scale(scale),
559 capture_x: settings.getattr("capture_x")?.extract()?,
560 capture_y: settings.getattr("capture_y")?.extract()?,
561 target_fps: sanitize_fps(settings.getattr("target_fps")?.extract()?),
562 jpeg_quality: settings.getattr("jpeg_quality")?.extract::<i32>()?.clamp(1, 100),
563 paint_over_jpeg_quality: settings.getattr("paint_over_jpeg_quality")?.extract::<i32>()?.clamp(1, 100),
564 use_paint_over_quality: settings.getattr("use_paint_over_quality")?.extract()?,
565 paint_over_trigger_frames: settings.getattr("paint_over_trigger_frames")?.extract()?,
566 damage_block_threshold: settings.getattr("damage_block_threshold")?.extract()?,
567 damage_block_duration: settings.getattr("damage_block_duration")?.extract()?,
568 output_mode: settings.getattr("output_mode")?.extract()?,
569 video_crf: settings.getattr("video_crf")?.extract()?,
570 video_paintover_crf: settings.getattr("video_paintover_crf")?.extract()?,
571 video_paintover_burst_frames: settings.getattr("video_paintover_burst_frames")?.extract()?,
572 video_fullcolor: settings.getattr("video_fullcolor")?.extract()?,
573 video_fullframe: settings.getattr("video_fullframe")?.extract()?,
574 video_streaming_mode: settings.getattr("video_streaming_mode")?.extract()?,
575 capture_cursor: settings.getattr("capture_cursor")?.extract()?,
576 cursor_size_cap: settings
577 .getattr("cursor_size_cap")
578 .ok()
579 .and_then(|v| v.extract::<i32>().ok())
580 .unwrap_or(32),
581 watermark_path,
582 watermark_location_enum: settings.getattr("watermark_location_enum")?.extract()?,
583 encode_node_index: settings.getattr("encode_node_index")?.extract()?,
584 use_cpu: settings.getattr("use_cpu")?.extract()?,
585 debug_logging: settings.getattr("debug_logging")?.extract()?,
586 auto_adjust_screen_capture_size: settings
587 .getattr("auto_adjust_screen_capture_size")
588 .ok()
589 .and_then(|v| v.extract::<bool>().ok())
590 .unwrap_or(false),
591 recording_socket: settings
592 .getattr("recording_socket")
593 .ok()
594 .and_then(|v| v.extract::<String>().ok())
595 .unwrap_or_default(),
596 wayland_host_display: settings
597 .getattr("wayland_host_display")
598 .ok()
599 .and_then(|v| v.extract::<String>().ok())
600 .unwrap_or_default(),
601 omit_stripe_headers: settings
602 .getattr("omit_stripe_headers")
603 .ok()
604 .and_then(|v| v.extract::<bool>().ok())
605 .unwrap_or(false),
606 video_cbr_mode: settings.getattr("video_cbr_mode")?.extract()?,
607 video_bitrate_kbps: settings.getattr("video_bitrate_kbps")?.extract()?,
608 video_vbv_multiplier: settings
609 .getattr("video_vbv_multiplier")
610 .ok()
611 .and_then(|v| v.extract::<f64>().ok())
612 .unwrap_or(0.0),
613 keyframe_interval_s: settings
614 .getattr("keyframe_interval_s")
615 .ok()
616 .and_then(|v| v.extract::<f64>().ok())
617 .unwrap_or(0.0),
618 video_min_qp: settings
619 .getattr("video_min_qp")
620 .ok()
621 .and_then(|v| v.extract::<i32>().ok())
622 .unwrap_or(0),
623 video_max_qp: settings
624 .getattr("video_max_qp")
625 .ok()
626 .and_then(|v| v.extract::<i32>().ok())
627 .unwrap_or(0),
628 })
629}
630
631pub type OutputDesc = (u32, i32, i32, i32, i32, f64, bool);
634
635pub type WindowDesc = (u32, String, String, u32, bool);
638
639pub enum ThreadCommand {
646 StartCapture { display_id: u32, callback: Option<Py<PyAny>>, settings: RustCaptureSettings },
650 StopCapture { display_id: u32 },
652 CreateOutput {
656 id: u32,
657 width: i32,
658 height: i32,
659 x: i32,
660 y: i32,
661 scale: f64,
662 reply: std::sync::mpsc::Sender<bool>,
663 },
664 DestroyOutput { id: u32, reply: std::sync::mpsc::Sender<bool> },
667 RepositionOutput { id: u32, x: i32, y: i32, reply: std::sync::mpsc::Sender<bool> },
672 ListOutputs { reply: std::sync::mpsc::Sender<Vec<OutputDesc>> },
674 OutputCapacity { reply: std::sync::mpsc::Sender<i64> },
678 MoveWindowToOutput { window_id: u32, output_id: u32, reply: std::sync::mpsc::Sender<bool> },
680 ListWindows { reply: std::sync::mpsc::Sender<Vec<WindowDesc>> },
682 SetCursorCallback(Py<PyAny>),
683 SetClipboardCallback(Py<PyAny>),
684 SetClipboard { mime: String, data: Vec<u8> },
687 KeyboardKey { scancode: u32, state: u32 },
688 KeyboardKeys { events: Vec<(u32, u32)> },
692 SetKeymapString(String),
696 SetXkbLayout {
700 rules: String,
701 model: String,
702 layout: String,
703 variant: String,
704 options: String,
705 reply: std::sync::mpsc::Sender<bool>,
706 },
707 BindKeysyms {
712 keysyms: Vec<u32>,
713 reply: std::sync::mpsc::Sender<Vec<(u32, u32)>>,
714 },
715 SetKeymapOverlay { binds: Vec<(u32, u32)> },
721 GetKeyboardState {
724 reply: std::sync::mpsc::Sender<(Vec<u32>, u32)>,
725 },
726 GetXkbKeymap { reply: std::sync::mpsc::Sender<String> },
729 Barrier { reply: std::sync::mpsc::Sender<()> },
734 PointerMotion { x: f64, y: f64 },
735 PointerRelativeMotion { dx: f64, dy: f64 },
736 PointerButton { btn: u32, state: u32 },
740 PointerAxis { x: f64, y: f64 },
741 UpdateCursorConfig { render_on_framebuffer: bool },
742 SetCursorSize { size: i32, reply: std::sync::mpsc::Sender<bool> },
746 RequestIdr { display_id: u32 },
749 UpdateRate {
752 display_id: u32,
753 bitrate_kbps: Option<i32>,
754 vbv_multiplier: Option<f64>,
755 fps: Option<f64>,
756 },
757 UpdateTunables { display_id: u32, tunables: LiveTunables },
760 CuScreenshot { display_id: u32, resp: std::sync::mpsc::Sender<Result<Vec<u8>, String>> },
763 CuCursorPosition { resp: std::sync::mpsc::Sender<(f64, f64)> },
764 CuGetInfo { display_id: u32, resp: std::sync::mpsc::Sender<(i32, i32, f64)> },
765}
766
767pub(crate) fn get_gpu_driver(card_index: i32) -> String {
781 let path = format!("/sys/class/drm/renderD{}/device/driver", 128 + card_index);
782 match std::fs::read_link(&path) {
783 Ok(link_path) => link_path.to_string_lossy().to_lowercase(),
784 Err(_) => String::new(),
785 }
786}
787
788pub(crate) fn driver_selects_nvenc(encode_driver: &str) -> bool {
793 encode_driver.is_empty() || encode_driver.contains("nvidia")
794}
795
796struct CardIdentity {
801 driver: String,
802 pci_vendor: Option<u32>,
803 compatibles: Vec<String>,
804}
805
806fn read_card_identity(device: &std::path::Path) -> CardIdentity {
813 let mut id = CardIdentity { driver: String::new(), pci_vendor: None, compatibles: Vec::new() };
814 if let Ok(uevent) = std::fs::read_to_string(device.join("uevent")) {
815 for line in uevent.lines() {
816 if let Some(v) = line.strip_prefix("DRIVER=") {
817 id.driver = v.trim().to_lowercase();
818 } else if let Some(v) = line.strip_prefix("PCI_ID=") {
819 id.pci_vendor = v.split(':').next().and_then(|h| u32::from_str_radix(h, 16).ok());
820 } else if line.starts_with("OF_COMPATIBLE_") && !line.starts_with("OF_COMPATIBLE_N")
821 && let Some(v) = line.split_once('=').map(|x| x.1) {
822 id.compatibles.push(v.trim().to_lowercase());
823 }
824 }
825 }
826 if id.pci_vendor.is_none() {
827 id.pci_vendor = std::fs::read_to_string(device.join("vendor"))
828 .ok()
829 .and_then(|v| u32::from_str_radix(v.trim().trim_start_matches("0x"), 16).ok());
830 }
831 if id.compatibles.is_empty()
832 && let Ok(modalias) = std::fs::read_to_string(device.join("modalias")) {
833 let modalias = modalias.trim();
834 if let Some(rest) = modalias.strip_prefix("of:") {
835 id.compatibles
836 .extend(rest.split('C').skip(1).map(|c| c.to_lowercase()));
837 }
838 }
839 if id.driver.is_empty() {
840 id.driver = std::fs::read_link(device.join("driver"))
841 .map(|p| p.file_name().map(|n| n.to_string_lossy().to_lowercase()).unwrap_or_default())
842 .unwrap_or_default();
843 }
844 id
845}
846
847const VENDOR_PCI_IDS: &[(&str, &[u32])] = &[
851 ("nvidia", &[0x10de, 0x12d2]),
852 ("amd", &[0x1002, 0x1022]),
853 ("ati", &[0x1002]),
854 ("intel", &[0x8086, 0x8087]),
855 ("arm", &[0x13b5]),
856 ("qualcomm", &[0x5143, 0x17cb]),
857 ("broadcom", &[0x14e4]),
858 ("apple", &[0x106b]),
859 ("mediatek", &[0x14c3]),
860 ("samsung", &[0x144d]),
861 ("vmware", &[0x15ad]),
862 ("microsoft", &[0x1414]),
863 ("virtio", &[0x1af4]),
864];
865
866const OF_PREFIX_ALIASES: &[(&str, &str)] = &[
869 ("mali", "arm"),
870 ("qualcomm", "qcom"),
871 ("adreno", "qcom"),
872 ("broadcom", "brcm"),
873 ("videocore", "brcm"),
874 ("imagination", "img"),
875 ("powervr", "img"),
876];
877
878fn card_matches_token(token: &str, id: &CardIdentity) -> bool {
884 if !id.driver.is_empty() && token == id.driver {
885 return true;
886 }
887 if let Some(vid) = id.pci_vendor {
888 if u32::from_str_radix(token.trim_start_matches("0x"), 16) == Ok(vid) {
889 return true;
890 }
891 if let Some((_, ids)) = VENDOR_PCI_IDS.iter().find(|(n, _)| *n == token)
892 && ids.contains(&vid) {
893 return true;
894 }
895 }
896 if !id.compatibles.is_empty() {
897 let prefix = OF_PREFIX_ALIASES
898 .iter()
899 .find(|(n, _)| *n == token)
900 .map(|(_, p)| *p)
901 .unwrap_or(token);
902 let want = format!("{prefix},");
903 if id.compatibles.iter().any(|c| c.starts_with(&want)) {
904 return true;
905 }
906 }
907 false
908}
909
910fn parse_auto_gpu(value: &str) -> Option<Option<String>> {
916 let value = value.to_lowercase();
917 match value.as_str() {
918 "" | "false" | "0" | "off" | "no" => None,
919 "true" | "1" | "on" | "yes" => Some(None),
920 token => Some(Some(token.to_string())),
921 }
922}
923
924fn auto_select_render_node(token: Option<&str>) -> Option<String> {
941 let mut cards: Vec<(u32, std::path::PathBuf)> = std::fs::read_dir("/sys/class/drm")
942 .into_iter()
943 .flatten()
944 .flatten()
945 .filter_map(|e| {
946 let num = e.file_name().into_string().ok()?.strip_prefix("card")?.parse::<u32>().ok()?;
947 Some((num, e.path()))
948 })
949 .collect();
950 cards.sort_by_key(|(n, _)| *n);
951 for (_, path) in &cards {
952 if let Some(t) = token
953 && !card_matches_token(t, &read_card_identity(&path.join("device"))) {
954 continue;
955 }
956 if let Ok(drm_entries) = std::fs::read_dir(path.join("device/drm")) {
957 for de in drm_entries.flatten() {
958 let name = de.file_name().into_string().unwrap_or_default();
959 if name.starts_with("renderD") {
960 let dev = format!("/dev/dri/{}", name);
961 if std::path::Path::new(&dev).exists() {
962 return Some(dev);
963 }
964 }
965 }
966 }
967 }
968 if token.is_some() {
969 return None;
970 }
971 let mut nodes: Vec<String> = std::fs::read_dir("/dev/dri")
972 .ok()?
973 .flatten()
974 .filter_map(|e| e.file_name().into_string().ok())
975 .filter(|n| n.starts_with("renderD"))
976 .collect();
977 nodes.sort();
978 nodes.first().map(|n| format!("/dev/dri/{}", n))
979}
980
981
982pub struct WlFrame {
986 id: usize,
988 buf: Vec<u8>,
989 frame_id: u16,
990 damage: Vec<Rectangle<i32, Physical>>,
991 is_animated: bool,
992}
993
994struct WlPoolInner {
996 free: Vec<(usize, Vec<u8>)>,
997 slot: Option<WlFrame>,
998}
999
1000pub struct WlFramePool {
1008 inner: Mutex<WlPoolInner>,
1009 cv: Condvar,
1010 stop: AtomicBool,
1011}
1012
1013impl WlFramePool {
1014 fn new(n: usize, buf_len: usize) -> Self {
1018 Self {
1019 inner: Mutex::new(WlPoolInner {
1020 free: (0..n).map(|i| (i, vec![0u8; buf_len])).collect(),
1021 slot: None,
1022 }),
1023 cv: Condvar::new(),
1024 stop: AtomicBool::new(false),
1025 }
1026 }
1027
1028 fn try_begin(&self) -> Option<(usize, Vec<u8>)> {
1033 let mut g = self.inner.lock().unwrap();
1034 if g.slot.is_some() {
1035 return None;
1036 }
1037 g.free.pop()
1038 }
1039
1040 fn publish(&self, frame: WlFrame) {
1042 let mut g = self.inner.lock().unwrap();
1043 debug_assert!(g.slot.is_none());
1044 g.slot = Some(frame);
1045 drop(g);
1046 self.cv.notify_all();
1047 }
1048
1049 fn cancel(&self, id: usize, buf: Vec<u8>) {
1051 self.inner.lock().unwrap().free.push((id, buf));
1052 }
1053
1054 fn take(&self) -> Option<WlFrame> {
1057 let mut g = self.inner.lock().unwrap();
1058 loop {
1059 if let Some(f) = g.slot.take() {
1060 return Some(f);
1061 }
1062 if self.stop.load(Ordering::Acquire) {
1063 return None;
1064 }
1065 let (gg, _) = self.cv.wait_timeout(g, WL_POOL_WAKE_QUANTUM).unwrap();
1066 g = gg;
1067 }
1068 }
1069
1070 fn recycle(&self, id: usize, buf: Vec<u8>) {
1073 self.inner.lock().unwrap().free.push((id, buf));
1074 }
1075
1076 fn shutdown(&self) {
1079 let g = self.inner.lock().unwrap();
1080 self.stop.store(true, Ordering::Release);
1081 drop(g);
1082 self.cv.notify_all();
1083 }
1084}
1085
1086pub struct WlEncodeControls {
1092 rate_dirty: AtomicBool,
1093 bitrate_kbps: AtomicI32,
1094 vbv_mult_milli: AtomicI32,
1095 fps_milli: AtomicU64,
1096 force_idr: AtomicBool,
1097 tunables_dirty: AtomicBool,
1100 tunables: Mutex<Option<LiveTunables>>,
1101}
1102
1103impl WlEncodeControls {
1104 fn new() -> Self {
1105 Self {
1106 rate_dirty: AtomicBool::new(false),
1107 bitrate_kbps: AtomicI32::new(0),
1108 vbv_mult_milli: AtomicI32::new(0),
1109 fps_milli: AtomicU64::new(0),
1110 force_idr: AtomicBool::new(false),
1111 tunables_dirty: AtomicBool::new(false),
1112 tunables: Mutex::new(None),
1113 }
1114 }
1115}
1116
1117const WL_POOL_SURFACES: usize = 2;
1120const WL_POOL_WAKE_QUANTUM: Duration = Duration::from_millis(20);
1122const WL_CONTENT_HOLD: Duration = Duration::from_millis(500);
1129
1130const MAX_CAPTURE_DIM: i32 = 16384;
1134const MAX_FPS: f64 = 1000.0;
1135const IDLE_FRAME_INTERVAL: Duration = Duration::from_millis(250);
1139const DEFAULT_FPS: f64 = 60.0;
1140const MAX_SCALE: f64 = 8.0;
1141pub(crate) const SCROLL_V120_PER_UNIT: f64 = 12.0;
1145
1146pub struct WlEncodeStats {
1150 frames: AtomicU32,
1151 stripes: AtomicU32,
1152 n_stripes: AtomicU32,
1153 desc: Mutex<String>,
1154}
1155
1156impl WlEncodeStats {
1157 fn new() -> Self {
1158 Self {
1159 frames: AtomicU32::new(0),
1160 stripes: AtomicU32::new(0),
1161 n_stripes: AtomicU32::new(1),
1162 desc: Mutex::new(String::new()),
1163 }
1164 }
1165}
1166
1167struct WlEncodeConfig {
1171 settings: RustCaptureSettings,
1172 display_id: u32,
1174 use_gpu: bool,
1176 try_gpu: bool,
1178 prior: Option<GpuEncoder>,
1181 predecessor: Option<std::thread::JoinHandle<Option<GpuEncoder>>>,
1186 recording_sink: Option<std::sync::Weak<crate::recording_sink::RecordingSink>>,
1189 deliver_tx: std::sync::mpsc::SyncSender<Vec<EncodedStripe>>,
1190 controls: Arc<WlEncodeControls>,
1191 stats: Arc<WlEncodeStats>,
1192}
1193
1194fn build_readback_encoders(
1206 settings: &RustCaptureSettings,
1207 try_gpu: bool,
1208 prior: Option<GpuEncoder>,
1209) -> Option<GpuEncoder> {
1210 if !try_gpu {
1211 return None;
1212 }
1213 let encode_driver = get_gpu_driver(settings.encode_node_index.max(0));
1214 println!(
1215 "[Wayland] Encode Node Index: {} | Driver: {}",
1216 settings.encode_node_index.max(0), encode_driver
1217 );
1218 if driver_selects_nvenc(&encode_driver) {
1219 if let Some(GpuEncoder::Nvenc(mut enc)) = prior {
1220 match enc.reconfigure_resolution(settings) {
1221 Ok(()) => {
1222 println!("[Wayland] NVENC session reconfigured in place.");
1223 return Some(GpuEncoder::Nvenc(enc));
1224 }
1225 Err(e) => eprintln!(
1226 "[Wayland] NVENC in-place reconfigure unavailable ({e}); rebuilding."
1227 ),
1228 }
1229 }
1230 println!("[Wayland] Nvidia Encoder detected. Initializing NVENC...");
1231 match NvencEncoder::new(settings, std::ptr::null()) {
1232 Ok(e) => {
1233 println!("[Wayland] NVENC Encoder initialized successfully.");
1234 return Some(GpuEncoder::Nvenc(e));
1235 }
1236 Err(e) => eprintln!(
1237 "[Wayland] Failed to init NVENC: {}. Falling back to CPU ({}).",
1238 e,
1239 encoders::SOFTWARE_H264_ENCODER
1240 ),
1241 }
1242 } else {
1243 println!("[Wayland] Initializing Unified VAAPI Encoder...");
1244 match VaapiEncoder::new(settings) {
1245 Ok(e) => {
1246 println!(
1247 "[Wayland] VAAPI Encoder initialized successfully ({}).",
1248 if e.is_fullcolor() { "4:4:4" } else { "4:2:0" }
1249 );
1250 return Some(GpuEncoder::Vaapi(e));
1251 }
1252 Err(e) => eprintln!(
1253 "[Wayland] Failed to init VAAPI: {}. Falling back to CPU ({}).",
1254 e,
1255 encoders::SOFTWARE_H264_ENCODER
1256 ),
1257 }
1258 }
1259 None
1260}
1261
1262fn hw_plane_buffer(width: i32, height: i32, fullcolor: bool, planar: bool) -> Vec<u8> {
1266 if !planar {
1267 return Vec::new();
1268 }
1269 let n = (width * height) as usize;
1270 vec![0u8; if fullcolor { n * 3 } else { n * 3 / 2 }]
1271}
1272
1273fn wayland_encode_loop(pool: &WlFramePool, cfg: WlEncodeConfig) -> Option<GpuEncoder> {
1303 crate::boost_thread_priority(-10);
1304 let mut settings = cfg.settings;
1305 let inherited = cfg.predecessor.and_then(|h| h.join().ok().flatten());
1306 let mut video_encoder =
1307 build_readback_encoders(&settings, cfg.try_gpu, cfg.prior.or(inherited));
1308 if cfg.try_gpu && video_encoder.is_none() {
1309 println!(
1310 "[Wayland] Decision: No GPU Encoder available -> Using CPU Software Encoding ({}).",
1311 encoders::SOFTWARE_H264_ENCODER
1312 );
1313 }
1314 let n_stripes = wayland_stripe_count(&settings, video_encoder.is_some());
1315 cfg.stats.n_stripes.store(n_stripes as u32, Ordering::Relaxed);
1316 *cfg.stats.desc.lock().unwrap() = encoder_desc(&settings, video_encoder.as_ref(), false);
1317 log_stream_settings(&settings, n_stripes, video_encoder.as_ref());
1318
1319 let width = settings.width;
1320 let height = settings.height;
1321 let mut stripes: Vec<StripeState> = Vec::with_capacity(MAX_STRIPE_CAPACITY);
1322 let mut stripes_carrying: f32 = 1.0;
1324 let mut hw_state = StripeState::default();
1325 let mut hw_fullcolor = encoder_fullcolor(video_encoder.as_ref(), &settings);
1329 let mut nv12_buffer: Vec<u8> = hw_plane_buffer(
1330 width,
1331 height,
1332 hw_fullcolor,
1333 matches!(video_encoder, Some(GpuEncoder::Vaapi(_))),
1334 );
1335 let mut hw_error_streak: u32 = 0;
1337 let mut hw_rebuilt = false;
1338
1339 while let Some(mut f) = pool.take() {
1340 if cfg.controls.tunables_dirty.swap(false, Ordering::Acquire)
1341 && let Some(t) = cfg.controls.tunables.lock().unwrap().take() {
1342 t.apply_to(&mut settings);
1343 }
1344 if cfg.controls.rate_dirty.swap(false, Ordering::Acquire) {
1345 settings.video_bitrate_kbps = cfg.controls.bitrate_kbps.load(Ordering::Relaxed);
1346 settings.video_vbv_multiplier =
1347 cfg.controls.vbv_mult_milli.load(Ordering::Relaxed) as f64 / 1000.0;
1348 let fps = (cfg.controls.fps_milli.load(Ordering::Relaxed) as f64) / 1000.0;
1349 if fps > 0.0 {
1350 settings.target_fps = fps;
1351 }
1352 match video_encoder.as_mut() {
1353 Some(GpuEncoder::Nvenc(enc)) => enc.reconfigure_rate(&settings),
1354 Some(GpuEncoder::Vaapi(enc)) => {
1355 if let Err(e) = enc.reconfigure_rate(&settings) {
1356 eprintln!("[wl-encode] VAAPI rate reconfigure failed: {e}");
1360 hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
1361 }
1362 }
1363 None => {}
1364 }
1365 }
1366
1367 let recording_sink = cfg.recording_sink.as_ref().and_then(|w| w.upgrade());
1371 let requested_idr = cfg.controls.force_idr.swap(false, Ordering::Relaxed)
1372 || recording_sink.as_ref().is_some_and(|s| s.should_force_idr());
1373
1374 let mut out: Vec<EncodedStripe> = Vec::new();
1375 if let Some(ref mut encoder) = video_encoder {
1376 let decision = crate::pipeline::decide_hw_fullframe(
1377 &mut hw_state,
1378 &settings,
1379 f.frame_id,
1380 !f.damage.is_empty(),
1381 f.is_animated,
1382 requested_idr,
1383 );
1384 if decision.send {
1385 let w = width as u32;
1386 let h = height as u32;
1387 let force_idr = decision.force_idr;
1388 let outcome = match encoder {
1389 GpuEncoder::Nvenc(enc) => enc.encode_cpu_packed(
1394 &f.buf,
1395 (w * 4) as usize,
1396 cfg.use_gpu,
1397 f.frame_id as u64,
1398 decision.target_qp,
1399 force_idr,
1400 ),
1401 GpuEncoder::Vaapi(enc) => {
1402 let matrix = YuvStandardMatrix::Bt709;
1411 let range = YuvRange::Limited;
1412 let y_size = (w * h) as usize;
1413 let csc = if hw_fullcolor {
1414 let (y_plane, rest) = nv12_buffer.split_at_mut(y_size);
1415 let (u_plane, v_plane) = rest.split_at_mut(y_size);
1416 let mut planar_image = yuv::YuvPlanarImageMut {
1417 y_plane: BufferStoreMut::Borrowed(y_plane),
1418 y_stride: w,
1419 u_plane: BufferStoreMut::Borrowed(u_plane),
1420 u_stride: w,
1421 v_plane: BufferStoreMut::Borrowed(v_plane),
1422 v_stride: w,
1423 width: w,
1424 height: h,
1425 };
1426 if cfg.use_gpu {
1427 yuv::rgba_to_yuv444(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1428 } else {
1429 yuv::bgra_to_yuv444(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1430 }
1431 } else {
1432 let (y_plane, uv_plane) = nv12_buffer.split_at_mut(y_size);
1433 let mut planar_image = YuvBiPlanarImageMut {
1434 y_plane: BufferStoreMut::Borrowed(y_plane),
1435 y_stride: w,
1436 uv_plane: BufferStoreMut::Borrowed(uv_plane),
1437 uv_stride: w,
1438 width: w,
1439 height: h,
1440 };
1441 if cfg.use_gpu {
1442 yuv::rgba_to_yuv_nv12(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1443 } else {
1444 yuv::bgra_to_yuv_nv12(&mut planar_image, &f.buf, w * 4, range, matrix, YuvConversionMode::Fast)
1445 }
1446 };
1447 match csc {
1448 Err(e) => Err(format!(
1449 "{} CSC failed: {e:?}",
1450 if hw_fullcolor { "YUV444" } else { "NV12" }
1451 )),
1452 Ok(()) => enc.encode_raw(
1453 &nv12_buffer,
1454 f.frame_id as u64,
1455 decision.target_qp,
1456 force_idr,
1457 ),
1458 }
1459 }
1460 };
1461 match outcome {
1462 Ok(data) => {
1463 hw_error_streak = 0;
1464 hw_rebuilt = false;
1465 if !data.is_empty() {
1466 out.push(EncodedStripe {
1467 data: Arc::new(data),
1468 data_type: 2,
1469 stripe_y_start: 0,
1470 stripe_height: height,
1471 frame_id: f.frame_id as i32,
1472 });
1473 }
1474 }
1475 Err(e) => {
1476 if hw_error_streak % HW_ERROR_RECOVERY_THRESHOLD == 0 {
1479 eprintln!("[wl-encode] HW encode error: {e}");
1480 }
1481 hw_error_streak = hw_error_streak.saturating_add(1);
1482 if hw_error_streak >= HW_ERROR_RECOVERY_THRESHOLD {
1483 hw_error_streak = 0;
1489 let try_gpu = !hw_rebuilt;
1490 if try_gpu {
1491 eprintln!("[wl-encode] rebuilding readback HW encoder after repeated encode errors.");
1492 } else {
1493 eprintln!(
1494 "[wl-encode] readback HW encoder unrecoverable; demoting to software encoding ({}).",
1495 encoders::SOFTWARE_H264_ENCODER
1496 );
1497 }
1498 drop(video_encoder.take());
1502 video_encoder = build_readback_encoders(&settings, try_gpu, None);
1503 hw_rebuilt = try_gpu && video_encoder.is_some();
1504 hw_fullcolor = encoder_fullcolor(video_encoder.as_ref(), &settings);
1505 nv12_buffer = hw_plane_buffer(
1506 width,
1507 height,
1508 hw_fullcolor,
1509 matches!(video_encoder, Some(GpuEncoder::Vaapi(_))),
1510 );
1511 cfg.controls.force_idr.store(true, Ordering::Relaxed);
1512 let n = wayland_stripe_count(&settings, video_encoder.is_some());
1513 cfg.stats.n_stripes.store(n as u32, Ordering::Relaxed);
1514 *cfg.stats.desc.lock().unwrap() =
1515 encoder_desc(&settings, video_encoder.as_ref(), false);
1516 log_stream_settings(&settings, n, video_encoder.as_ref());
1517 }
1518 }
1519 }
1520 }
1521 } else {
1522 let mut damage = std::mem::take(&mut f.damage);
1523 if f.is_animated {
1524 damage.push(Rectangle::new((0, 0).into(), (width, height).into()));
1525 }
1526 let force_idr_all = requested_idr
1527 || (settings.output_mode == 1
1528 && crate::pipeline::periodic_idr_due(&settings, f.frame_id));
1529 out = encoders::software::encode_cpu(
1530 &mut stripes,
1531 &mut stripes_carrying,
1532 &f.buf,
1533 width,
1534 height,
1535 &damage,
1536 &settings,
1537 f.frame_id,
1538 cfg.use_gpu,
1539 false,
1540 force_idr_all,
1541 );
1542 }
1543
1544 let WlFrame { id, buf, .. } = f;
1545 pool.recycle(id, buf);
1546 if requested_idr && out.is_empty() {
1549 cfg.controls.force_idr.store(true, Ordering::Relaxed);
1550 }
1551 if !out.is_empty() {
1552 cfg.stats.frames.fetch_add(1, Ordering::Relaxed);
1553 cfg.stats.stripes.fetch_add(out.len() as u32, Ordering::Relaxed);
1554 if let Some(ref socket) = recording_sink {
1555 socket.write_frame(&out, settings.height);
1556 }
1557 crate::recorder::wayland_tap(cfg.display_id, &out);
1558 let _ = cfg.deliver_tx.send(out);
1559 }
1560 }
1561 if settings.debug_logging {
1562 println!(
1563 "[Wayland] Encode thread exiting (hw={}, stripes={}).",
1564 video_encoder.is_some(),
1565 stripes.len()
1566 );
1567 }
1568 video_encoder
1569}
1570
1571fn encoder_fullcolor(video_encoder: Option<&GpuEncoder>, settings: &RustCaptureSettings) -> bool {
1577 match video_encoder {
1578 Some(GpuEncoder::Vaapi(enc)) => enc.is_fullcolor(),
1579 Some(GpuEncoder::Nvenc(_)) => settings.video_fullcolor,
1580 None => settings.video_fullcolor && encoders::SOFTWARE_H264_FULLCOLOR,
1581 }
1582}
1583
1584fn encoder_desc(
1587 settings: &RustCaptureSettings,
1588 video_encoder: Option<&GpuEncoder>,
1589 zero_copy: bool,
1590) -> String {
1591 if settings.output_mode == 0 {
1592 return format!("JPEG Q:{}", settings.jpeg_quality);
1593 }
1594 let copy_mode = if zero_copy { "ZeroCopy" } else { "Readback" };
1595 let backend = match video_encoder {
1596 Some(GpuEncoder::Nvenc(_)) => format!("NVENC ({})", copy_mode),
1597 Some(GpuEncoder::Vaapi(_)) => format!("VAAPI ({})", copy_mode),
1598 None => format!("CPU {}", encoders::SOFTWARE_H264_ENCODER),
1599 };
1600 let is_444 = encoder_fullcolor(video_encoder, settings);
1601 let cs_str = if is_444 { "CS_IN:I444" } else { "CS_IN:I420" };
1602 let range_str = if is_444 && video_encoder.is_none() { "FR" } else { "LR" };
1605 let frame_str = if video_encoder.is_some() || settings.video_fullframe {
1606 "FF"
1607 } else {
1608 "Striped"
1609 };
1610 format!("H264 ({}) {} {} {} CRF:{}", backend, cs_str, range_str, frame_str, settings.video_crf)
1611}
1612
1613fn wayland_stripe_count(settings: &RustCaptureSettings, fullframe_encoder: bool) -> usize {
1619 crate::encoders::software::stripe_count(
1620 settings.height,
1621 settings.output_mode,
1622 fullframe_encoder || settings.video_fullframe,
1623 )
1624}
1625
1626fn log_stream_settings(
1629 settings: &RustCaptureSettings,
1630 n_stripes: usize,
1631 video_encoder: Option<&GpuEncoder>,
1632) {
1633 let mut log_msg = format!(
1634 "Stream settings active -> Res: {}x{} | FPS: {:.1} | Stripes: {}",
1635 settings.width, settings.height, settings.target_fps, n_stripes
1636 );
1637
1638 if settings.output_mode == 0 {
1639 log_msg.push_str(&format!(" | Mode: JPEG | Quality: {}", settings.jpeg_quality));
1640 if settings.use_paint_over_quality {
1641 log_msg.push_str(&format!(
1642 " | PaintOver Q: {} (Trigger: {}f)",
1643 settings.paint_over_jpeg_quality, settings.paint_over_trigger_frames
1644 ));
1645 }
1646 } else {
1647 let encoder_type = match video_encoder {
1648 Some(GpuEncoder::Nvenc(_)) => "NVENC",
1649 Some(GpuEncoder::Vaapi(_)) => "VAAPI",
1650 None => encoders::SOFTWARE_H264_ENCODER,
1651 };
1652 log_msg.push_str(&format!(" | Mode: H264 ({})", encoder_type));
1653
1654 if video_encoder.is_some() || settings.video_fullframe {
1655 log_msg.push_str(" FullFrame");
1656 } else {
1657 log_msg.push_str(" Striped");
1658 }
1659
1660 if settings.video_streaming_mode {
1661 log_msg.push_str(" Streaming");
1662 }
1663
1664 if settings.video_cbr_mode {
1665 log_msg.push_str(&format!(" | CBR {}", settings.video_bitrate_kbps));
1666 } else {
1667 log_msg.push_str(&format!(" | CRF: {}", settings.video_crf));
1668 if settings.video_bitrate_kbps > 0 {
1669 log_msg.push_str(&format!(" | VBV: {} kbps", settings.video_bitrate_kbps));
1670 }
1671 }
1672
1673 if settings.use_paint_over_quality {
1674 log_msg.push_str(&format!(
1675 " | PaintOver CRF: {} (Burst: {}f)",
1676 settings.video_paintover_crf, settings.video_paintover_burst_frames
1677 ));
1678 }
1679
1680 let is_actually_444 = encoder_fullcolor(video_encoder, settings);
1681 log_msg.push_str(&format!(
1682 " | Colorspace: {}",
1683 encoders::colorspace_desc(is_actually_444, video_encoder.is_none())
1684 ));
1685 }
1686
1687 log_msg.push_str(&format!(
1688 " | Damage Thresh: {}f | Damage Dur: {}f",
1689 settings.damage_block_threshold, settings.damage_block_duration
1690 ));
1691
1692 println!("{}", log_msg);
1693}
1694
1695fn teardown_capture(
1704 cap: &mut wayland::frontend::WlCapture,
1705) -> (
1706 Option<std::thread::JoinHandle<()>>,
1707 Option<std::thread::JoinHandle<Option<GpuEncoder>>>,
1708) {
1709 if let Some(p) = cap.encode_pool.take() {
1710 p.shutdown();
1711 }
1712 if let Some(flag) = cap.deliver_discard.take() {
1713 flag.store(true, Ordering::Relaxed);
1714 }
1715 let encode_join = cap.encode_join.take();
1716 if let Some(tx) = cap.deliver_tx.take() {
1717 drop(tx);
1718 }
1719 let join = cap.deliver_join.take();
1720 cap.pending_hw_delivery = None;
1721 cap.pending_hw_damage = false;
1722 cap.recording_sink = None;
1726 (join, encode_join)
1727}
1728
1729fn reap_dead_host(state: &mut AppState) {
1734 if !state.host.as_ref().is_some_and(|h| !h.alive()) {
1735 return;
1736 }
1737 eprintln!("[HostCapture] host compositor connection lost; captures stop until restarted.");
1738 let ids: Vec<u32> = state
1739 .output_nodes
1740 .iter()
1741 .filter(|n| n.capture.is_some())
1742 .map(|n| n.id)
1743 .collect();
1744 for id in ids {
1745 stop_capture_on_display(state, id);
1746 set_wayland_capture_err(
1750 id,
1751 Some("host compositor connection lost; capture stopped".to_string()),
1752 );
1753 }
1754 state.host = None;
1755 let pending: Vec<_> = state.host_layout_pending.drain().collect();
1758 for (id, p) in pending {
1759 answer_geometry_waiters(state, id, p.geometry_waiters);
1760 }
1761}
1762
1763fn stop_capture_on_display(state: &mut AppState, display_id: u32) {
1766 let Some(idx) = state.node_idx_for_id(display_id) else { return };
1767 if let Some(mut cap) = state.output_nodes[idx].capture.take() {
1768 println!("[Wayland] Capture loop stopped (display {display_id}).");
1769 cap.video_encoder = None;
1770 let (join, encode_join) = teardown_capture(&mut cap);
1771 state.deliver_reaper.extend(join);
1772 state.encode_reaper.extend(encode_join);
1773 }
1774 wayland_alive().lock().unwrap().remove(&display_id);
1775 set_wayland_capture_err(display_id, None);
1776 if let Some(p) = state.host_layout_pending.remove(&display_id) {
1777 answer_geometry_waiters(state, display_id, p.geometry_waiters);
1778 }
1779}
1780
1781fn realized_geometry(state: &AppState, display_id: u32) -> (i32, i32, f64) {
1784 state
1785 .node_idx_for_id(display_id)
1786 .map(|idx| {
1787 let node = &state.output_nodes[idx];
1788 match node.capture.as_ref() {
1789 Some(c) => (c.settings.width, c.settings.height, c.settings.scale),
1790 None => node
1791 .output
1792 .current_mode()
1793 .map(|m| {
1794 (
1795 m.size.w,
1796 m.size.h,
1797 node.output.current_scale().fractional_scale(),
1798 )
1799 })
1800 .unwrap_or((0, 0, 0.0)),
1801 }
1802 })
1803 .unwrap_or((0, 0, 0.0))
1804}
1805
1806fn answer_geometry_waiters(
1809 state: &AppState,
1810 display_id: u32,
1811 waiters: Vec<std::sync::mpsc::Sender<(i32, i32, f64)>>,
1812) {
1813 if waiters.is_empty() {
1814 return;
1815 }
1816 let info = realized_geometry(state, display_id);
1817 for w in waiters {
1818 let _ = w.send(info);
1819 }
1820}
1821
1822fn host_layout_resolution(
1828 realized: bool,
1829 want: (i32, i32),
1830 current: Option<(i32, i32)>,
1831) -> Option<(i32, i32)> {
1832 if realized {
1833 return None;
1834 }
1835 current.filter(|&c| c != want)
1836}
1837
1838fn reconcile_host_layouts(state: &mut AppState) {
1846 if state.host_layout_pending.is_empty() {
1847 return;
1848 }
1849 let ids: Vec<u32> = state.host_layout_pending.keys().copied().collect();
1850 for id in ids {
1851 let Some(pending) = state.host_layout_pending.get(&id) else { continue };
1853 let verdict = match state.host.as_ref() {
1854 Some(host) => match host.layout_outcome(pending.epoch) {
1855 None => continue,
1856 Some(realized) => {
1857 host_layout_resolution(realized, pending.want, host.current_output_size(id))
1858 }
1859 },
1860 None => None,
1861 };
1862 let Some(pending) = state.host_layout_pending.remove(&id) else { continue };
1863 if let Some((rw, rh)) = verdict {
1864 let (w, h) = pending.want;
1865 let restart = state
1866 .node_idx_for_id(id)
1867 .and_then(|idx| state.output_nodes[idx].capture.as_ref())
1868 .map(|cap| (cap.callback.clone(), cap.settings.clone()));
1869 if let Some((cb, mut settings)) = restart {
1870 settings.width = rw;
1871 settings.height = rh;
1872 let followed = if settings.output_mode == 1 { (rw & !1, rh & !1) } else { (rw, rh) };
1875 if followed != (w, h) {
1876 eprintln!(
1877 "[HostCapture] host kept {rw}x{rh} for display {id} ({w}x{h} declined); capturing at that size."
1878 );
1879 start_capture_on_display(state, id, cb, settings);
1880 let refusal = format!("host kept {rw}x{rh} ({w}x{h} declined)");
1881 let own = wayland_capture_err().lock().unwrap().get(&id).cloned();
1882 set_wayland_capture_err(
1883 id,
1884 Some(match own {
1885 Some(e) => format!("{refusal}; {e}"),
1886 None => refusal,
1887 }),
1888 );
1889 }
1890 }
1891 }
1892 answer_geometry_waiters(state, id, pending.geometry_waiters);
1895 }
1896}
1897
1898fn bootstrap_readback_pool(
1908 cap: &mut wayland::frontend::WlCapture,
1909 display_id: u32,
1910 use_gpu: bool,
1911 try_gpu: bool,
1912 prior: Option<GpuEncoder>,
1913 predecessor: Option<std::thread::JoinHandle<Option<GpuEncoder>>>,
1914) {
1915 let Some(deliver_tx) = cap.deliver_tx.clone() else {
1916 return;
1917 };
1918 let settings = cap.settings.clone();
1919 let pool = Arc::new(WlFramePool::new(
1920 WL_POOL_SURFACES,
1921 (settings.width.max(0) as usize) * (settings.height.max(0) as usize) * 4,
1922 ));
1923 cap.pool_last_render = vec![0; WL_POOL_SURFACES];
1924 cap.render_seq = 0;
1925 cap.pool_content_gen = vec![u64::MAX; WL_POOL_SURFACES];
1926 cap.content_gen = 0;
1927 let c = &cap.encode_controls;
1928 c.bitrate_kbps.store(settings.video_bitrate_kbps, Ordering::Relaxed);
1929 c.vbv_mult_milli.store(
1930 (settings.video_vbv_multiplier * 1000.0).round() as i32,
1931 Ordering::Relaxed,
1932 );
1933 c.fps_milli.store(
1934 (settings.target_fps.max(1.0) * 1000.0) as u64,
1935 Ordering::Relaxed,
1936 );
1937 let cfg = WlEncodeConfig {
1938 settings: settings.clone(),
1939 display_id,
1940 use_gpu,
1941 try_gpu,
1942 prior,
1943 predecessor,
1944 recording_sink: cap.recording_sink.as_ref().map(Arc::downgrade),
1945 deliver_tx,
1946 controls: cap.encode_controls.clone(),
1947 stats: cap.encode_stats.clone(),
1948 };
1949 let pool2 = pool.clone();
1950 cap.encode_join = Some(
1951 thread::Builder::new()
1952 .name(format!("wl-encode-{display_id}"))
1953 .spawn(move || wayland_encode_loop(&pool2, cfg))
1954 .expect("failed to spawn wl-encode thread"),
1955 );
1956 cap.encode_pool = Some(pool);
1957}
1958
1959fn rebuild_zerocopy_encoder(
1963 cap: &wayland::frontend::WlCapture,
1964 state: &mut AppState,
1965) -> Option<GpuEncoder> {
1966 let settings = &cap.settings;
1967 let encode_driver = get_gpu_driver(settings.encode_node_index.max(0));
1968 if driver_selects_nvenc(&encode_driver) {
1969 let egl_display = state
1970 .gles_renderer
1971 .as_ref()
1972 .map(|r| r.egl_context().display().get_display_handle().handle)
1973 .unwrap_or(std::ptr::null());
1974 NvencEncoder::new(settings, egl_display)
1975 .ok()
1976 .map(GpuEncoder::Nvenc)
1977 } else {
1978 VaapiEncoder::new(settings).ok().map(GpuEncoder::Vaapi)
1979 }
1980}
1981
1982pub(crate) const HW_ERROR_RECOVERY_THRESHOLD: u32 = 30;
1986
1987fn start_capture_on_display(
1988 state: &mut AppState,
1989 display_id: u32,
1990 cb: Option<Arc<Py<PyAny>>>,
1991 mut settings: RustCaptureSettings,
1992) {
1993 use smithay::wayland::fractional_scale::with_fractional_scale;
1994
1995 let _ = state.cursor_tx.send(CursorJob::SetSizeCap(settings.cursor_size_cap));
1998
1999 set_wayland_capture_err(display_id, None);
2001
2002 let Some(node_idx) = state.node_idx_for_id(display_id) else {
2003 eprintln!("[Wayland] StartCapture: no output with display id {display_id}.");
2004 set_wayland_capture_err(
2005 display_id,
2006 Some(format!("no output with display id {display_id}")),
2007 );
2008 return;
2009 };
2010 let mut node = state.output_nodes.remove(node_idx);
2011 let mut geometry_waiters = state
2014 .host_layout_pending
2015 .remove(&display_id)
2016 .map(|p| p.geometry_waiters)
2017 .unwrap_or_default();
2018
2019 if state.auto_gpu_selected && settings.encode_node_index < -1
2020 && let Some(idx_str) = state.render_node_path.strip_prefix("/dev/dri/renderD")
2021 && let Ok(idx) = idx_str.parse::<i32>() {
2022 settings.encode_node_index = idx - 128;
2023 }
2024
2025 if settings.output_mode == 1 {
2026 settings.width &= !1;
2027 settings.height &= !1;
2028 }
2029
2030 let mut prior_zero_copy: Option<GpuEncoder> = None;
2033 let mut prior_encode_join: Option<std::thread::JoinHandle<Option<GpuEncoder>>> = None;
2034 let mut prior_deliver_join: Option<std::thread::JoinHandle<()>> = None;
2035 if let Some(mut old) = node.capture.take() {
2036 prior_zero_copy = old.video_encoder.take();
2037 (prior_deliver_join, prior_encode_join) = teardown_capture(&mut old);
2038 }
2039
2040 let recording_sink = crate::recording_sink::RecordingSink::try_bind(&settings.recording_socket);
2044
2045 let host_capture = !settings.wayland_host_display.is_empty();
2051 reap_dead_host(state);
2052 if host_capture && state.host.is_none() {
2053 let gbm_path = if state.use_gpu {
2057 state.gbm_device.as_ref().and_then(|dev| {
2058 use std::os::fd::{AsFd as _, AsRawFd as _};
2059 let fd = dev.as_fd().as_raw_fd();
2060 std::fs::read_link(format!("/proc/self/fd/{fd}")).ok()
2061 })
2062 } else {
2063 None
2064 };
2065 match crate::wayland::host::HostSession::connect(&settings.wayland_host_display, gbm_path) {
2066 Ok(h) => {
2067 println!(
2068 "[HostCapture] capturing host compositor '{}' ({} outputs).",
2069 settings.wayland_host_display,
2070 h.output_count()
2071 );
2072 state.host = Some(h);
2073 }
2074 Err(e) => {
2075 eprintln!(
2076 "[HostCapture] connect '{}' failed: {e}",
2077 settings.wayland_host_display
2078 );
2079 set_wayland_capture_err(
2080 display_id,
2081 Some(format!(
2082 "host compositor '{}' connect failed ({e}); capturing locally",
2083 settings.wayland_host_display
2084 )),
2085 );
2086 }
2087 }
2088 }
2089 let host_capture = host_capture && state.host.is_some();
2092 if host_capture && let Some(host) = &state.host {
2093 host.set_layout(display_id, node.pos.0, node.pos.1);
2096 }
2097
2098 {
2099 let target_refresh = (settings.target_fps * 1000.0).round() as i32;
2103 let (current_w, current_h, current_refresh) = match node.output.current_mode() {
2104 Some(m) => (m.size.w, m.size.h, m.refresh),
2105 None => (settings.width, settings.height, target_refresh),
2106 };
2107 let current_scale = node.output.current_scale().fractional_scale();
2108
2109 if current_w != settings.width
2110 || current_h != settings.height
2111 || (current_scale - settings.scale).abs() > 0.001
2112 || current_refresh != target_refresh
2113 {
2114 let mut new_offscreen = None;
2120 let mut gbm_resize_failed = false;
2121 if state.use_gpu
2122 && let Some(gbm) = state.gbm_device.as_mut() {
2123 match gbm.create_buffer_object(
2124 settings.width as u32,
2125 settings.height as u32,
2126 GbmFormat::Argb8888,
2127 BufferObjectFlags::RENDERING,
2128 ) {
2129 Ok(bo) => {
2130 let dmabuf = create_dmabuf_from_bo(&bo);
2131 new_offscreen = Some((bo, dmabuf));
2132 }
2133 Err(e) => {
2134 eprintln!(
2135 "[Wayland] GBM buffer resize to {}x{} failed ({:?}); keeping previous output mode.",
2136 settings.width, settings.height, e
2137 );
2138 gbm_resize_failed = true;
2139 }
2140 }
2141 }
2142 if gbm_resize_failed {
2143 set_wayland_capture_err(
2147 display_id,
2148 Some(format!(
2149 "GPU buffer resize to {}x{} refused; kept {current_w}x{current_h}",
2150 settings.width, settings.height
2151 )),
2152 );
2153 settings.width = current_w;
2154 settings.height = current_h;
2155 settings.scale = current_scale;
2156 settings.target_fps = current_refresh as f64 / 1000.0;
2157 } else {
2158 println!(
2159 "[Wayland] Configuring Output {} ({}): {}x{} @ {:.2} FPS (Scale {:.2})",
2160 display_id, node.output.name(),
2161 settings.width, settings.height, settings.target_fps, settings.scale
2162 );
2163 let new_mode = OutputMode {
2164 size: (settings.width, settings.height).into(),
2165 refresh: target_refresh,
2166 };
2167 node.output.change_current_state(
2168 Some(new_mode),
2169 Some(Transform::Normal),
2170 Some(OutputScale::Fractional(settings.scale)),
2171 Some(Point::from(node.pos)),
2172 );
2173 node.output.set_preferred(new_mode);
2174 for cs in state
2177 .copy_sessions
2178 .iter()
2179 .filter(|cs| cs.output.upgrade().as_ref() == Some(&node.output))
2180 {
2181 if let Some(c) = wayland::frontend::output_capture_constraints(
2182 &node.output,
2183 state.gles_renderer.as_ref(),
2184 &state.render_node_path,
2185 ) {
2186 cs.session.update_constraints(c);
2187 }
2188 }
2189
2190 let pixel_count =
2191 (settings.width.max(0) as usize) * (settings.height.max(0) as usize);
2192 node.frame_buffer = vec![0u8; pixel_count * 4];
2193 node.target_seeded = false;
2194
2195 if let Some(off) = new_offscreen.take() {
2196 node.offscreen_buffer = Some(off);
2197 }
2198 }
2199 }
2200
2201 let scale = settings.scale.max(0.1);
2202 let logical_width = (settings.width as f64 / scale).round() as i32;
2203 let logical_height = (settings.height as f64 / scale).round() as i32;
2204
2205 for window in state.space.elements() {
2206 if wayland::frontend::window_output_id(window) != display_id {
2207 continue;
2208 }
2209 if wayland::frontend::window_meta(window)
2214 .is_some_and(|meta| meta.parked.load(Ordering::Relaxed))
2215 {
2216 continue;
2217 }
2218 if let Some(surface) = window.wl_surface() {
2219 node.output.enter(&surface);
2220 with_states(&surface, |states| {
2221 smithay::wayland::compositor::send_surface_state(
2222 &surface, states, scale.ceil() as i32, Transform::Normal,
2223 );
2224 with_fractional_scale(states, |fs| {
2225 fs.set_preferred_scale(scale);
2226 });
2227 });
2228 }
2229 if let Some(toplevel) = window.toplevel() {
2230 toplevel.with_pending_state(|state| {
2231 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State;
2232 state.states.set(State::Fullscreen);
2233 state.states.set(State::Activated);
2234 state.size = Some((logical_width, logical_height).into());
2235 });
2236 toplevel.send_configure();
2237 }
2238 }
2239 }
2240
2241 let use_cpu_explicit = settings.use_cpu || settings.encode_node_index == -1;
2242 let gpu_intent = settings.output_mode == 1 && !use_cpu_explicit;
2243 if use_cpu_explicit {
2244 println!("[Wayland] CPU encoding selected (use_cpu=true or encode_node_index=-1).");
2245 }
2246
2247 let mut different_gpu = false;
2248 if gpu_intent {
2249 let encode_node_idx = settings.encode_node_index.max(0);
2250 if !state.render_node_path.is_empty()
2251 && !state.render_node_path.contains(&format!("renderD{}", 128 + encode_node_idx))
2252 {
2253 different_gpu = true;
2254 }
2255 }
2256
2257 let mut video_encoder: Option<GpuEncoder> = None;
2258 if gpu_intent && state.use_gpu && !different_gpu {
2259 let encode_driver = get_gpu_driver(settings.encode_node_index.max(0));
2260 println!(
2261 "[Wayland] Encode Node Index: {} | Driver: {}",
2262 settings.encode_node_index.max(0), encode_driver
2263 );
2264
2265 if driver_selects_nvenc(&encode_driver) {
2266 let reused = match prior_zero_copy.as_mut() {
2267 Some(GpuEncoder::Nvenc(enc)) => match enc.reconfigure_resolution(&settings) {
2268 Ok(()) => {
2269 println!("[Wayland] NVENC session reconfigured in place.");
2270 true
2271 }
2272 Err(e) => {
2273 eprintln!("[Wayland] NVENC in-place reconfigure unavailable ({e}); rebuilding.");
2274 false
2275 }
2276 },
2277 _ => false,
2278 };
2279 if reused {
2280 video_encoder = prior_zero_copy.take();
2281 } else {
2282 prior_zero_copy = None;
2283 println!("[Wayland] Nvidia Encoder detected. Initializing NVENC...");
2284 let egl_display = if let Some(renderer) = state.gles_renderer.as_ref() {
2285 renderer.egl_context().display().get_display_handle().handle
2286 } else {
2287 std::ptr::null()
2288 };
2289
2290 match NvencEncoder::new(&settings, egl_display) {
2291 Ok(encoder) => {
2292 video_encoder = Some(GpuEncoder::Nvenc(encoder));
2293 println!("[Wayland] NVENC Encoder initialized successfully.");
2294 }
2295 Err(e) => {
2296 eprintln!(
2297 "[Wayland] Failed to init NVENC: {}. Falling back to CPU.",
2298 e
2299 );
2300 set_wayland_capture_err(
2301 display_id,
2302 Some(format!("NVENC init failed ({e}); using CPU encode")),
2303 );
2304 }
2305 }
2306 }
2307 } else {
2308 prior_zero_copy = None;
2309 println!("[Wayland] Initializing Unified VAAPI Encoder...");
2310 match VaapiEncoder::new(&settings) {
2311 Ok(encoder) => {
2312 println!(
2313 "[Wayland] VAAPI Encoder initialized successfully ({}).",
2314 if encoder.is_fullcolor() { "4:4:4" } else { "4:2:0" }
2315 );
2316 video_encoder = Some(GpuEncoder::Vaapi(encoder));
2317 }
2318 Err(e) => {
2319 eprintln!(
2320 "[Wayland] Failed to init VAAPI: {}. Falling back to CPU.",
2321 e
2322 );
2323 set_wayland_capture_err(
2324 display_id,
2325 Some(format!("VAAPI init failed ({e}); using CPU encode")),
2326 );
2327 }
2328 }
2329 }
2330 }
2331 drop(prior_zero_copy);
2332
2333 if different_gpu {
2334 println!("[Wayland] Decision: Rendering and Encoding GPUs differ -> Forcing Readback (CPU path for pixels).");
2335 }
2336 if video_encoder.is_none() {
2337 println!("[Wayland] Decision: Readback path (encode thread) active.");
2338 } else if !different_gpu {
2339 println!("[Wayland] Decision: Zero-Copy path active.");
2340 }
2341
2342 if host_capture && let Some(host) = &state.host {
2351 let epoch = host.start_capture(
2352 display_id,
2353 settings.width,
2354 settings.height,
2355 video_encoder.is_some(),
2356 settings.capture_cursor,
2357 );
2358 state.host_layout_pending.insert(
2359 display_id,
2360 wayland::frontend::PendingHostLayout {
2361 epoch,
2362 want: (settings.width, settings.height),
2363 geometry_waiters: std::mem::take(&mut geometry_waiters),
2364 },
2365 );
2366 }
2367
2368 if recording_sink.is_some() && settings.output_mode == 0 {
2369 eprintln!(
2370 "[recording_sink] WARNING: recording_socket is set but output_mode is JPEG (0). \
2371 The recording socket requires a single H.264 stream. Please set output_mode=1 \
2372 on the Python CaptureSettings to produce a recordable output."
2373 );
2374 }
2375
2376 let watermark_output_scale = node.output.current_scale().fractional_scale();
2379 node.overlay_state
2380 .load_watermark(&settings.watermark_path, watermark_output_scale);
2381 if display_id == 0 {
2382 state.settings = settings.clone();
2383 if state.cursor_callback_set
2384 && let Some(icon) = state.current_cursor_icon.clone() {
2385 state.send_cursor_image(&icon);
2386 }
2387 }
2388 state.render_cursor_on_framebuffer = settings.capture_cursor;
2389
2390 let mut cap = wayland::frontend::WlCapture {
2391 settings: settings.clone(),
2392 callback: cb.clone(),
2393 video_encoder,
2394 vaapi_state: StripeState::default(),
2395 recording_sink,
2396 deliver_tx: None,
2397 deliver_join: None,
2398 deliver_discard: None,
2399 pending_hw_delivery: None,
2400 pending_hw_damage: false,
2401 encode_pool: None,
2402 encode_join: None,
2403 encode_controls: Arc::new(WlEncodeControls::new()),
2404 encode_stats: Arc::new(WlEncodeStats::new()),
2405 pool_last_render: Vec::new(),
2406 render_seq: 0,
2407 pool_content_gen: Vec::new(),
2408 content_gen: 0,
2409 frame_counter: 0,
2410 pending_force_idr: false,
2411 needs_full_render: true,
2412 last_tick: None,
2413 hw_error_streak: 0,
2414 hw_rebuilt: false,
2415 };
2416
2417 {
2418 let (tx, rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
2419 let discard = Arc::new(AtomicBool::new(false));
2420 let thread_discard = discard.clone();
2421 let predecessor = prior_deliver_join;
2422 let join = thread::spawn(move || {
2426 if let Some(handle) = predecessor {
2431 let _ = handle.join();
2432 }
2433 match cb {
2434 Some(cb) => {
2435 crate::boost_thread_priority(-10);
2436 while let Ok(stripes) = rx.recv() {
2437 if thread_discard.load(Ordering::Relaxed)
2438 || PY_SHUTDOWN.load(Ordering::Relaxed) { continue; }
2439 Python::attach(|py| {
2440 for s in stripes {
2441 match Py::new(py, StripeFrame::new_owned_meta(
2442 s.data, s.data_type, s.stripe_y_start,
2443 s.stripe_height, s.frame_id,
2444 )) {
2445 Ok(f) => { if let Err(e) = cb.call1(py, (f,)) { e.print(py); } }
2446 Err(e) => eprintln!("[wayland] frame alloc error: {e:?}"),
2447 }
2448 }
2449 });
2450 }
2451 }
2452 None => while rx.recv().is_ok() {},
2453 }
2454 });
2455 cap.deliver_tx = Some(tx);
2456 cap.deliver_discard = Some(discard);
2457 cap.deliver_join = Some(join);
2458 }
2459
2460 if cap.video_encoder.is_none() {
2461 bootstrap_readback_pool(
2462 &mut cap,
2463 display_id,
2464 state.use_gpu && !host_capture,
2467 gpu_intent && (!state.use_gpu || different_gpu),
2468 None,
2469 prior_encode_join.take(),
2470 );
2471 } else {
2472 cap.encode_stats.n_stripes.store(1, Ordering::Relaxed);
2473 *cap.encode_stats.desc.lock().unwrap() =
2474 encoder_desc(&settings, cap.video_encoder.as_ref(), true);
2475 log_stream_settings(&settings, 1, cap.video_encoder.as_ref());
2476 }
2477 state.encode_reaper.extend(prior_encode_join);
2480 cap.request_idr();
2484
2485 node.capture = Some(cap);
2486 node.content_hold_until = Some(Instant::now() + WL_CONTENT_HOLD);
2492 state.output_nodes.insert(node_idx, node);
2493 wayland_alive().lock().unwrap().insert(display_id);
2494 answer_geometry_waiters(state, display_id, geometry_waiters);
2495}
2496
2497fn draw_host_watermark(
2506 renderer: &mut GlesRenderer,
2507 overlay: &crate::encoders::overlay::OverlayState,
2508 target: &mut Dmabuf,
2509 size: (i32, i32),
2510) -> Result<SyncPoint, String> {
2511 let elem = overlay
2512 .get_watermark_element(renderer)
2513 .ok_or("watermark element unavailable")?;
2514 let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2515 let mut frame = renderer
2516 .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2517 .map_err(|e| format!("render: {e:?}"))?;
2518 let dst = elem.geometry(1.0.into());
2519 let local = Rectangle::from_size(dst.size);
2520 elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2521 .map_err(|e| format!("draw: {e:?}"))?;
2522 frame.finish().map_err(|e| format!("finish: {e:?}"))
2523}
2524
2525fn compose_host_watermark(
2529 renderer: &mut GlesRenderer,
2530 overlay: &crate::encoders::overlay::OverlayState,
2531 src: &Dmabuf,
2532 target: &mut Dmabuf,
2533 size: (i32, i32),
2534) -> Result<SyncPoint, String> {
2535 let elem = overlay
2536 .get_watermark_element(renderer)
2537 .ok_or("watermark element unavailable")?;
2538 let tex = renderer
2539 .import_dmabuf(src, None)
2540 .map_err(|e| format!("import: {e:?}"))?;
2541 let mut fb = renderer.bind(target).map_err(|e| format!("bind: {e:?}"))?;
2542 let full: Rectangle<i32, Physical> = Rectangle::from_size((size.0, size.1).into());
2543 let mut frame = renderer
2544 .render(&mut fb, (size.0, size.1).into(), Transform::Normal)
2545 .map_err(|e| format!("render: {e:?}"))?;
2546 frame
2547 .render_texture_from_to(
2548 &tex,
2549 Rectangle::from_size((size.0 as f64, size.1 as f64).into()),
2550 full,
2551 &[full],
2552 &[full],
2556 Transform::Normal,
2557 1.0,
2558 None,
2559 &[],
2560 )
2561 .map_err(|e| format!("texture: {e:?}"))?;
2562 let dst = elem.geometry(1.0.into());
2563 let local = Rectangle::from_size(dst.size);
2564 elem.draw(&mut frame, elem.src(), dst, &[local], &[], None)
2565 .map_err(|e| format!("draw: {e:?}"))?;
2566 frame.finish().map_err(|e| format!("finish: {e:?}"))
2567}
2568
2569fn warn_once_host_watermark(e: &str) {
2570 static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
2571 if !WARNED.swap(true, Ordering::Relaxed) {
2572 eprintln!("[HostCapture] watermark compositing failed: {e}");
2573 }
2574}
2575
2576fn copy_rows_into_shm(
2579 buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer,
2580 src: &[u8],
2581 width: i32,
2582 height: i32,
2583) -> Result<(), String> {
2584 use smithay::wayland::shm::with_buffer_contents_mut;
2585 let src_stride = (width.max(0) as usize) * 4;
2586 if src.len() < src_stride * (height.max(0) as usize) {
2587 return Err("source smaller than advertised".into());
2588 }
2589 with_buffer_contents_mut(buffer, |ptr, len, spec| {
2590 if spec.width < width || spec.height < height {
2591 return Err("shm buffer smaller than the output".to_string());
2592 }
2593 let dst_stride = spec.stride as usize;
2594 let offset = spec.offset as usize;
2595 if dst_stride < src_stride || len < offset + dst_stride * (height as usize) {
2596 return Err("shm stride or length mismatch".to_string());
2597 }
2598 for y in 0..height as usize {
2599 unsafe {
2600 std::ptr::copy_nonoverlapping(
2601 src.as_ptr().add(y * src_stride),
2602 ptr.add(offset + y * dst_stride),
2603 src_stride,
2604 );
2605 }
2606 }
2607 Ok(())
2608 })
2609 .map_err(|e| format!("{e:?}"))?
2610}
2611
2612fn service_copy_frames(
2618 state: &mut AppState,
2619 node: &mut wayland::frontend::OutputNode,
2620 width: i32,
2621 height: i32,
2622 damage_rects: &[Rectangle<i32, Physical>],
2623) {
2624 use smithay::backend::renderer::{buffer_type, Blit, BufferType, ExportMem, TextureFilter};
2625 use smithay::utils::Buffer as BufferCoords;
2626
2627 if state.copy_sessions.is_empty() {
2628 return;
2629 }
2630 let time = state.clock.now();
2631 for i in 0..state.copy_sessions.len() {
2632 {
2633 let cs = &state.copy_sessions[i];
2634 if cs.output.upgrade().as_ref() != Some(&node.output)
2635 || cs.pending.is_none()
2636 || (damage_rects.is_empty() && cs.delivered_once)
2637 {
2638 continue;
2639 }
2640 }
2641 let first = !state.copy_sessions[i].delivered_once;
2642 let frame = state.copy_sessions[i].pending.take().unwrap();
2643 let buffer = frame.buffer();
2644 let full = Rectangle::<i32, Physical>::new((0, 0).into(), (width, height).into());
2645 let result: Result<(), String> = match buffer_type(&buffer) {
2646 Some(BufferType::Dma) => (|| {
2647 let renderer = state.gles_renderer.as_mut().ok_or("no GLES renderer")?;
2648 let mut client = smithay::wayland::dmabuf::get_dmabuf(&buffer)
2649 .map_err(|e| e.to_string())?
2650 .clone();
2651 let (_bo, offscreen) = node
2652 .offscreen_buffer
2653 .as_mut()
2654 .ok_or("no composited buffer")?;
2655 let src = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
2656 let mut dst = renderer.bind(&mut client).map_err(|e| format!("{e:?}"))?;
2657 renderer
2661 .blit(&src, &mut dst, full, full, TextureFilter::Linear)
2662 .map_err(|e| format!("{e:?}"))?
2663 .wait()
2664 .map_err(|_| "blit fence interrupted".to_string())?;
2665 Ok(())
2666 })(),
2667 Some(BufferType::Shm) => (|| {
2668 if let Some(renderer) = state.gles_renderer.as_mut() {
2669 let (_bo, offscreen) = node
2670 .offscreen_buffer
2671 .as_mut()
2672 .ok_or("no composited buffer")?;
2673 let fb = renderer.bind(offscreen).map_err(|e| format!("{e:?}"))?;
2674 let mapping = renderer
2675 .copy_framebuffer(
2676 &fb,
2677 Rectangle::new((0, 0).into(), (width, height).into()),
2678 Fourcc::Argb8888,
2679 )
2680 .map_err(|e| format!("{e:?}"))?;
2681 let data = renderer.map_texture(&mapping).map_err(|e| format!("{e:?}"))?;
2682 copy_rows_into_shm(&buffer, data, width, height)
2683 } else {
2684 copy_rows_into_shm(&buffer, &node.frame_buffer, width, height)
2685 }
2686 })(),
2687 _ => Err("unsupported buffer type".into()),
2688 };
2689 match result {
2690 Ok(()) => {
2691 let damage: Option<Vec<Rectangle<i32, BufferCoords>>> = if first {
2692 None
2693 } else {
2694 Some(
2695 damage_rects
2696 .iter()
2697 .map(|r| {
2698 Rectangle::new(
2699 (r.loc.x, r.loc.y).into(),
2700 (r.size.w, r.size.h).into(),
2701 )
2702 })
2703 .collect(),
2704 )
2705 };
2706 frame.success(Transform::Normal, damage, time);
2707 state.copy_sessions[i].delivered_once = true;
2708 }
2709 Err(e) => {
2710 eprintln!("[Wayland] copy-capture frame failed: {e}");
2711 frame.fail(CaptureFailureReason::Unknown);
2712 }
2713 }
2714 }
2715}
2716
2717fn send_cursor_frame(state: &AppState, output: &Output, time: impl Into<Duration>) {
2721 if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
2722 send_frames_surface_tree(surface, output, time, Some(Duration::ZERO), |_, _| Some(output.clone()));
2723 }
2724}
2725
2726fn cursor_surface_hotspot(
2729 surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
2730) -> Point<i32, smithay::utils::Logical> {
2731 with_states(surface, |states| {
2732 states
2733 .data_map
2734 .get::<std::sync::Mutex<smithay::input::pointer::CursorImageAttributes>>()
2735 .and_then(|attrs| attrs.lock().ok().map(|guard| guard.hotspot))
2736 .unwrap_or_default()
2737 })
2738}
2739
2740fn render_node_tick(
2741 state: &mut AppState,
2742 node: &mut wayland::frontend::OutputNode,
2743) -> bool {
2744 let take_screenshot = state
2745 .pending_screenshot
2746 .as_ref()
2747 .is_some_and(|(id, _)| *id == node.id);
2748 let copy_frame_wanted = state.copy_frame_pending_for(&node.output);
2749 if node.capture.is_none() && !take_screenshot && !copy_frame_wanted {
2750 return false;
2751 }
2752
2753 if let Some(cap) = node.capture.as_ref()
2756 && !take_screenshot {
2757 let fps = cap.settings.target_fps.max(1.0);
2758 if let Some(last) = cap.last_tick
2759 && last.elapsed().as_secs_f64() < (1.0 / fps) * 0.9 {
2760 return false;
2761 }
2762 }
2763
2764 let output = node.output.clone();
2765 let origin: Point<i32, smithay::utils::Logical> = node.pos.into();
2766 let output_scale_val = output.current_scale().fractional_scale();
2767 let (width, height) = match node.capture.as_ref() {
2768 Some(c) => (c.settings.width, c.settings.height),
2769 None => output
2770 .current_mode()
2771 .map(|m| (m.size.w, m.size.h))
2772 .unwrap_or((0, 0)),
2773 };
2774 if width <= 0 || height <= 0 {
2775 return false;
2776 }
2777 if node.frame_buffer.len() < (width as usize) * (height as usize) * 4 {
2778 node.frame_buffer = vec![0u8; (width as usize) * (height as usize) * 4];
2779 }
2780 let logical_w = (width as f64 / output_scale_val).round();
2781 let logical_h = (height as f64 / output_scale_val).round();
2782
2783 let hold_frame = match node.content_hold_until {
2789 Some(deadline)
2790 if state.host.is_none()
2791 && Instant::now() < deadline
2792 && !wayland::frontend::output_content_covers(
2793 &state.space,
2794 node.id,
2795 logical_w,
2796 logical_h,
2797 ) =>
2798 {
2799 true
2800 }
2801 Some(_) => {
2802 node.content_hold_until = None;
2803 false
2804 }
2805 None => false,
2806 };
2807
2808 if let Some(cap) = node.capture.as_mut()
2810 && cap
2811 .recording_sink
2812 .as_ref()
2813 .map(|s| s.should_force_idr())
2814 .unwrap_or(false)
2815 {
2816 cap.request_idr();
2817 }
2818 let requested_idr = node.capture.as_ref().map(|c| c.pending_force_idr).unwrap_or(false);
2819 let hw_idr_pending = node
2823 .capture
2824 .as_ref()
2825 .map(|c| c.encode_controls.force_idr.load(Ordering::Relaxed))
2826 .unwrap_or(false);
2827 let want_idr_for_host = requested_idr || hw_idr_pending;
2828
2829 let mut pool_slot: Option<(usize, Vec<u8>)> = None;
2830 if !hold_frame
2831 && let Some(cap) = node.capture.as_ref()
2832 && let Some(ref pool) = cap.encode_pool {
2833 pool_slot = pool.try_begin();
2834 if pool_slot.is_none() {
2835 return true;
2836 }
2837 }
2838
2839 let loc_enum = node
2840 .capture
2841 .as_ref()
2842 .map(|c| c.settings.watermark_location_enum)
2843 .unwrap_or(state.settings.watermark_location_enum);
2844 node.overlay_state.update_position(width, height, loc_enum);
2845
2846 if let Some(cap) = node.capture.as_mut() {
2847 cap.last_tick = Some(Instant::now());
2848 }
2849
2850 let pointer_local: Option<Point<f64, smithay::utils::Logical>> = state
2853 .seat
2854 .get_pointer()
2855 .map(|p| p.current_location())
2856 .and_then(|pos| {
2857 let rect = Rectangle::<f64, smithay::utils::Logical>::new(
2858 origin.to_f64(),
2859 (logical_w, logical_h).into(),
2860 );
2861 if rect.contains(pos) {
2862 Some(pos - origin.to_f64())
2863 } else {
2864 None
2865 }
2866 });
2867
2868 let mut render_success = false;
2869 let mut render_sync = None;
2870 let mut damage_rects: Vec<Rectangle<i32, Physical>> = Vec::new();
2871 let needs_full = node.capture.as_ref().map(|c| c.needs_full_render).unwrap_or(!node.target_seeded);
2872
2873 let host_mode = state.host.as_ref().map(|h| h.has_output_for(node.id)).unwrap_or(false);
2876 if state.host.is_some() && !host_mode {
2877 if let Some((id, buf)) = pool_slot.take()
2880 && let Some(cap) = node.capture.as_ref()
2881 && let Some(ref pool) = cap.encode_pool {
2882 pool.cancel(id, buf);
2883 }
2884 return false;
2885 }
2886 let mut host_enc_dmabuf: Option<Dmabuf> = None;
2888 let mut host_cpu_frame = false;
2891 if host_mode {
2892 const RETAINED_OK: u8 = 0;
2893 const RETAINED_NONE: u8 = 1;
2894 const RETAINED_CPU_FRAME: u8 = 2;
2897 const RETAINED_GPU_FRAME: u8 = 3;
2898 let host_idx = node.id;
2899 let gpu_encoder = node
2900 .capture
2901 .as_ref()
2902 .map(|c| c.video_encoder.is_some())
2903 .unwrap_or(false);
2904 let host = state.host.take().unwrap();
2907 let expect = node
2912 .capture
2913 .as_ref()
2914 .map(|c| (c.settings.width, c.settings.height));
2915 let new_frame = match (host.try_take_frame(host_idx), expect) {
2916 (Some(f), Some((w, h))) if f.width != w || f.height != h => {
2917 host.release_frame(host_idx, f);
2918 None
2919 }
2920 (f, _) => f,
2921 };
2922 let have_new = new_frame.is_some();
2923 let streaming = node
2929 .capture
2930 .as_ref()
2931 .map(|c| c.settings.video_streaming_mode)
2932 .unwrap_or(false);
2933 let wm_active = node.overlay_state.is_active();
2934 let wm_animated = wm_active && node.overlay_state.is_animated();
2935 if !have_new && !want_idr_for_host && !streaming && !take_screenshot && !wm_animated {
2936 if let Some((id, buf)) = pool_slot.take()
2937 && let Some(cap) = node.capture.as_ref()
2938 && let Some(ref pool) = cap.encode_pool {
2939 pool.cancel(id, buf);
2940 }
2941 state.host = Some(host);
2942 return false;
2943 }
2944 if let Some(f) = new_frame {
2945 host.retain_frame(host_idx, f);
2946 }
2947 let mut wm_drawn = false;
2952 let outcome = host.with_retained(host_idx, |r| {
2953 let Some(f) = r else { return RETAINED_NONE };
2954 damage_rects = if have_new { f.damage.clone() } else { Vec::new() };
2955 if let Some(cpu) = f.cpu.as_ref() {
2956 if gpu_encoder {
2957 return RETAINED_CPU_FRAME;
2958 }
2959 host_cpu_frame = true;
2960 if let Some((_, ref mut buf)) = pool_slot {
2961 cpu.write_bgra(f.width, f.height, buf);
2962 if wm_active {
2963 node.overlay_state.blend_bgra(buf, (f.width as usize) * 4, f.width, f.height);
2964 wm_drawn = true;
2965 }
2966 }
2967 cpu.write_bgra(f.width, f.height, &mut node.frame_buffer);
2968 if wm_active {
2969 node.overlay_state
2970 .blend_bgra(&mut node.frame_buffer, (f.width as usize) * 4, f.width, f.height);
2971 }
2972 } else if let Some(dmabuf) = f.dmabuf.as_ref() {
2973 if !gpu_encoder {
2974 return RETAINED_GPU_FRAME;
2975 }
2976 host_enc_dmabuf = Some(dmabuf.clone());
2977 }
2978 RETAINED_OK
2979 });
2980 if let Some(src) = host_enc_dmabuf.clone() {
2986 if wm_active
2987 && let Some(renderer) = state.gles_renderer.as_mut() {
2988 if wm_animated {
2989 if let Some((_, target)) = node.offscreen_buffer.as_mut() {
2990 match compose_host_watermark(
2991 renderer,
2992 &node.overlay_state,
2993 &src,
2994 target,
2995 (width, height),
2996 ) {
2997 Ok(sync) => {
2998 render_sync = Some(sync);
2999 host_enc_dmabuf = Some(target.clone());
3000 wm_drawn = true;
3001 }
3002 Err(e) => warn_once_host_watermark(&e),
3003 }
3004 }
3005 } else if have_new {
3006 let mut target = src.clone();
3007 match draw_host_watermark(
3008 renderer,
3009 &node.overlay_state,
3010 &mut target,
3011 (width, height),
3012 ) {
3013 Ok(sync) => {
3014 render_sync = Some(sync);
3015 wm_drawn = true;
3016 }
3017 Err(e) => warn_once_host_watermark(&e),
3018 }
3019 }
3020 }
3021 if take_screenshot
3022 && let Some(renderer) = state.gles_renderer.as_mut() {
3023 let mut shot = host_enc_dmabuf.clone().unwrap_or(src);
3024 match renderer.bind(&mut shot) {
3025 Ok(fb) => {
3026 let rect = Rectangle::new((0, 0).into(), (width, height).into());
3027 match renderer.copy_framebuffer(&fb, rect, Fourcc::Abgr8888) {
3028 Ok(mapping) => match renderer.map_texture(&mapping) {
3029 Ok(data) => {
3030 let n = data.len().min(node.frame_buffer.len());
3031 node.frame_buffer[..n].copy_from_slice(&data[..n]);
3032 }
3033 Err(e) => eprintln!("[HostCapture] screenshot map: {e:?}"),
3034 },
3035 Err(e) => eprintln!("[HostCapture] screenshot copy: {e:?}"),
3036 }
3037 }
3038 Err(e) => eprintln!("[HostCapture] screenshot bind: {e:?}"),
3039 };
3040 }
3041 }
3042 if wm_drawn
3043 && let Some(rect) = node.overlay_state.damage_rect(width, height) {
3044 damage_rects.push(rect);
3045 }
3046 state.host = Some(host);
3047 if outcome != RETAINED_OK {
3048 if let Some((id, buf)) = pool_slot.take()
3049 && let Some(cap) = node.capture.as_ref()
3050 && let Some(ref pool) = cap.encode_pool {
3051 pool.cancel(id, buf);
3052 }
3053 if outcome == RETAINED_GPU_FRAME {
3057 if let Some(h) = state.host.as_ref() {
3061 h.set_buffer_type(node.id, false);
3062 }
3063 } else if outcome == RETAINED_CPU_FRAME
3064 && let Some(cap) = node.capture.as_mut() {
3065 eprintln!(
3069 "[HostCapture] host delivers software frames; demoting the zero-copy encoder to readback encode."
3070 );
3071 cap.video_encoder = None;
3072 let s = &cap.settings;
3073 let try_gpu = s.output_mode == 1
3074 && !(s.use_cpu || s.encode_node_index == -1);
3075 bootstrap_readback_pool(cap, node.id, false, try_gpu, None, None);
3076 cap.request_idr();
3077 if let Some(h) = state.host.as_ref() {
3079 h.set_buffer_type(node.id, false);
3080 }
3081 }
3082 return false;
3083 }
3084 render_success = true;
3085 }
3086
3087 if !host_mode && state.use_gpu {
3088 if let Some(renderer) = state.gles_renderer.as_mut() {
3089 let mut cap = node.capture.as_mut();
3090 if let Some((_bo, dmabuf)) = node.offscreen_buffer.as_mut() {
3091 let render_age = if node.overlay_state.is_animated() || needs_full { 0 } else { 1 };
3092 match renderer.bind(dmabuf) {
3093 Ok(mut frame) => {
3094 let mut elements: Vec<CompositionElements<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>> = Vec::new();
3095
3096 if state.render_cursor_on_framebuffer
3097 && let Some(pos) = pointer_local {
3098 let scale = Scale::from(output_scale_val);
3099
3100 if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon {
3101 let name = wayland::frontend::cursor_icon_to_str(icon);
3102 let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3103 if let Some(image) = state.cursor_helper.get_image_by_name(name, output_scale_val.round() as u32, time)
3104 && let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3105 elements.push(CompositionElements::Cursor(elem));
3106 }
3107 } else if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
3108 let hot = cursor_surface_hotspot(surface).to_f64();
3109 let phys_pos = (pos - hot).to_physical(scale);
3110 let elem_result = with_states(surface, |states| {
3111 WaylandSurfaceRenderElement::from_surface(renderer, surface, states, phys_pos, 1.0, smithay::backend::renderer::element::Kind::Cursor)
3112 });
3113 if let Ok(Some(cursor_elem)) = elem_result {
3114 elements.push(CompositionElements::Surface(cursor_elem));
3115 }
3116 } else if state.current_cursor_icon.is_none() {
3117 let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3118 let image = state.cursor_helper.get_image(output_scale_val.round() as u32, time);
3119 if let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3120 elements.push(CompositionElements::Cursor(elem));
3121 }
3122 }
3123 }
3124
3125 if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3126 elements.push(CompositionElements::Cursor(elem));
3127 }
3128
3129 {
3130 let layer_map = layer_map_for_output(&output);
3131
3132 let draw_layer = |renderer: &mut GlesRenderer, elements: &mut Vec<CompositionElements<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3133 for surface in layer_map.layers().rev() {
3134 let current_layer = surface.layer();
3135 if current_layer == target_layer
3136 && let Some(geo) = layer_map.layer_geometry(surface) {
3137 let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3138 WaylandSurfaceRenderElement::from_surface(
3139 renderer, surface.wl_surface(), states,
3140 geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3141 smithay::backend::renderer::element::Kind::Unspecified
3142 )
3143 });
3144 if let Ok(Some(e)) = elem {
3145 elements.push(CompositionElements::Surface(e));
3146 }
3147 }
3148 }
3149 };
3150
3151 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Overlay);
3152 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Top);
3153 }
3154
3155 for window in state.space.elements_for_output(&output).collect::<Vec<_>>().into_iter().rev() {
3156 let window_loc = state.space.element_location(window).unwrap_or_default() - origin;
3157
3158 if let Some(surface) = window.wl_surface() {
3159 let popups = PopupManager::popups_for_surface(&surface);
3160 for (popup, location) in popups {
3161 let popup_surface = popup.wl_surface();
3162 let popup_pos = window_loc + location;
3163 let elem = smithay::wayland::compositor::with_states(popup_surface, |states| {
3164 WaylandSurfaceRenderElement::from_surface(
3165 renderer,
3166 popup_surface,
3167 states,
3168 popup_pos.to_physical_precise_round(output_scale_val),
3169 1.0,
3170 smithay::backend::renderer::element::Kind::Unspecified
3171 )
3172 });
3173 if let Ok(Some(e)) = elem {
3174 elements.push(CompositionElements::Surface(e));
3175 }
3176 }
3177 }
3178
3179 elements.extend(window.render_elements(renderer, window_loc.to_physical_precise_round(output_scale_val), Scale::from(output_scale_val), 1.0).into_iter().map(CompositionElements::Space));
3180 }
3181
3182 {
3183 let layer_map = layer_map_for_output(&output);
3184
3185 let draw_layer = |renderer: &mut GlesRenderer, elements: &mut Vec<CompositionElements<GlesRenderer, WaylandSurfaceRenderElement<GlesRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3186 for surface in layer_map.layers().rev() {
3187 let current_layer = surface.layer();
3188 if current_layer == target_layer
3189 && let Some(geo) = layer_map.layer_geometry(surface) {
3190 let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3191 WaylandSurfaceRenderElement::from_surface(
3192 renderer, surface.wl_surface(), states,
3193 geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3194 smithay::backend::renderer::element::Kind::Unspecified
3195 )
3196 });
3197 if let Ok(Some(e)) = elem {
3198 elements.push(CompositionElements::Surface(e));
3199 }
3200 }
3201 }
3202 };
3203
3204 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Bottom);
3205 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Background);
3206 }
3207 match node.damage_tracker.render_output(renderer, &mut frame, render_age, &elements, [0.1, 0.1, 0.1, 1.0]) {
3208 Ok(result) => {
3209 render_success = true;
3210 if let Some(damage) = result.damage {
3211 damage_rects = damage.clone();
3212 }
3213 render_sync = Some(result.sync);
3214 if let Some(c) = cap.as_deref_mut() {
3215 c.needs_full_render = false;
3216 }
3217 },
3218 Err(e) => eprintln!("Render error: {:?}", e)
3219 }
3220 if let Some(c) = cap {
3221 if !damage_rects.is_empty() {
3222 c.content_gen += 1;
3223 }
3224 if let Some((id, ref mut buf)) = pool_slot {
3225 if render_success && c.pool_content_gen[id] != c.content_gen {
3230 let _ = renderer.with_context(|gl| unsafe {
3231 gl.ReadPixels(
3232 0,
3233 0,
3234 width,
3235 height,
3236 smithay::backend::renderer::gles::ffi::RGBA,
3237 smithay::backend::renderer::gles::ffi::UNSIGNED_BYTE,
3238 buf.as_mut_ptr() as *mut std::ffi::c_void,
3239 );
3240 });
3241 c.pool_content_gen[id] = c.content_gen;
3242 }
3243 }
3244 }
3245 if pool_slot.is_none() && take_screenshot {
3246 let _ = renderer.with_context(|gl| unsafe {
3247 gl.ReadPixels(
3248 0,
3249 0,
3250 width,
3251 height,
3252 smithay::backend::renderer::gles::ffi::RGBA,
3253 smithay::backend::renderer::gles::ffi::UNSIGNED_BYTE,
3254 node.frame_buffer.as_mut_ptr() as *mut std::ffi::c_void,
3255 );
3256 });
3257 }
3258 },
3259 Err(e) => eprintln!("Failed to bind buffer: {:?}", e)
3260 }
3261 }
3262 }
3263 } else if !host_mode
3264 && let Some(renderer) = state.pixman_renderer.as_mut() {
3265 let mut cap = node.capture.as_mut();
3266 let (ptr, buf_age) = match pool_slot {
3267 Some((id, ref mut buf)) => {
3268 let age = cap
3269 .as_ref()
3270 .map(|c| {
3271 if c.pool_last_render[id] == 0 {
3272 0
3273 } else {
3274 (c.render_seq + 1 - c.pool_last_render[id]) as usize
3275 }
3276 })
3277 .unwrap_or(0);
3278 (buf.as_mut_ptr() as *mut u32, age)
3279 }
3280 None => (node.frame_buffer.as_mut_ptr() as *mut u32, 0),
3281 };
3282 let mut image = unsafe {
3283 pixman::Image::from_raw_mut(pixman::FormatCode::A8R8G8B8, width as usize, height as usize, ptr, (width as usize) * 4, false).expect("Failed to create pixman image")
3284 };
3285 match renderer.bind(&mut image) {
3286 Ok(mut frame) => {
3287 let mut elements: Vec<CompositionElements<PixmanRenderer, WaylandSurfaceRenderElement<PixmanRenderer>>> = Vec::new();
3288
3289 if state.render_cursor_on_framebuffer
3290 && let Some(pos) = pointer_local {
3291 let scale = Scale::from(output_scale_val);
3292
3293 if let Some(CursorImageStatus::Named(icon)) = &state.current_cursor_icon {
3294 let name = wayland::frontend::cursor_icon_to_str(icon);
3295 let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3296 if let Some(image) = state.cursor_helper.get_image_by_name(name, output_scale_val.round() as u32, time)
3297 && let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3298 elements.push(CompositionElements::Cursor(elem));
3299 }
3300 } else if let Some(CursorImageStatus::Surface(surface)) = &state.current_cursor_icon {
3301 let hot = cursor_surface_hotspot(surface).to_f64();
3302 let phys_pos = (pos - hot).to_physical(scale);
3303 let elem_result = with_states(surface, |states| {
3304 WaylandSurfaceRenderElement::from_surface(renderer, surface, states, phys_pos, 1.0, smithay::backend::renderer::element::Kind::Cursor)
3305 });
3306 if let Ok(Some(cursor_elem)) = elem_result {
3307 elements.push(CompositionElements::Surface(cursor_elem));
3308 }
3309 } else if state.current_cursor_icon.is_none() {
3310 let time = Duration::from_millis(state.clock.now().as_millis() as u64);
3311 let image = state.cursor_helper.get_image(output_scale_val.round() as u32, time);
3312 if let Some(elem) = node.overlay_state.get_cursor_element(renderer, image, pos, output_scale_val) {
3313 elements.push(CompositionElements::Cursor(elem));
3314 }
3315 }
3316 }
3317
3318 if let Some(elem) = node.overlay_state.get_watermark_element(renderer) {
3319 elements.push(CompositionElements::Cursor(elem));
3320 }
3321
3322 {
3323 let layer_map = layer_map_for_output(&output);
3324
3325 let draw_layer = |renderer: &mut PixmanRenderer, elements: &mut Vec<CompositionElements<PixmanRenderer, WaylandSurfaceRenderElement<PixmanRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3326 for surface in layer_map.layers().rev() {
3327 let current_layer = surface.layer();
3328 if current_layer == target_layer
3329 && let Some(geo) = layer_map.layer_geometry(surface) {
3330 let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3331 WaylandSurfaceRenderElement::from_surface(
3332 renderer, surface.wl_surface(), states,
3333 geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3334 smithay::backend::renderer::element::Kind::Unspecified
3335 )
3336 });
3337 if let Ok(Some(e)) = elem {
3338 elements.push(CompositionElements::Surface(e));
3339 }
3340 }
3341 }
3342 };
3343
3344 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Overlay);
3345 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Top);
3346 }
3347
3348 for window in state.space.elements_for_output(&output).collect::<Vec<_>>().into_iter().rev() {
3349 let loc = state.space.element_location(window).unwrap_or_default() - origin;
3350
3351 if let Some(surface) = window.wl_surface() {
3352 let popups = PopupManager::popups_for_surface(&surface);
3353 for (popup, location) in popups {
3354 let popup_surface = popup.wl_surface(); {
3355 let popup_pos = loc + location;
3356 let elem = smithay::wayland::compositor::with_states(popup_surface, |states| {
3357 WaylandSurfaceRenderElement::from_surface(
3358 renderer,
3359 popup_surface,
3360 states,
3361 popup_pos.to_physical_precise_round(output_scale_val),
3362 1.0,
3363 smithay::backend::renderer::element::Kind::Unspecified
3364 )
3365 });
3366 if let Ok(Some(e)) = elem {
3367 elements.push(CompositionElements::Surface(e));
3368 }
3369 }
3370 }
3371 }
3372
3373 elements.extend(window.render_elements(renderer, loc.to_physical_precise_round(output_scale_val), Scale::from(output_scale_val), 1.0).into_iter().map(CompositionElements::Space));
3374 }
3375
3376 {
3377 let layer_map = layer_map_for_output(&output);
3378
3379 let draw_layer = |renderer: &mut PixmanRenderer, elements: &mut Vec<CompositionElements<PixmanRenderer, WaylandSurfaceRenderElement<PixmanRenderer>>>, target_layer: smithay::wayland::shell::wlr_layer::Layer| {
3380 for surface in layer_map.layers().rev() {
3381 let current_layer = surface.layer();
3382 if current_layer == target_layer
3383 && let Some(geo) = layer_map.layer_geometry(surface) {
3384 let elem = smithay::wayland::compositor::with_states(surface.wl_surface(), |states| {
3385 WaylandSurfaceRenderElement::from_surface(
3386 renderer, surface.wl_surface(), states,
3387 geo.loc.to_physical_precise_round(output_scale_val), 1.0,
3388 smithay::backend::renderer::element::Kind::Unspecified
3389 )
3390 });
3391 if let Ok(Some(e)) = elem {
3392 elements.push(CompositionElements::Surface(e));
3393 }
3394 }
3395 }
3396 };
3397
3398 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Bottom);
3399 draw_layer(renderer, &mut elements, smithay::wayland::shell::wlr_layer::Layer::Background);
3400 }
3401
3402 let render_age = if node.overlay_state.is_animated() || needs_full { 0 } else { buf_age };
3403 match node.damage_tracker.render_output(renderer, &mut frame, render_age, &elements, [0.1, 0.1, 0.1, 1.0]) {
3404 Ok(result) => {
3405 render_success = true;
3406 if let Some(c) = cap.as_deref_mut() {
3407 c.needs_full_render = false;
3408 }
3409 if let Some(damage) = result.damage { damage_rects = damage.clone(); }
3410 },
3411 Err(e) => eprintln!("Render error: {:?}", e)
3412 }
3413 if let Some(c) = cap {
3414 c.render_seq += 1;
3415 if render_success
3416 && let Some((id, _)) = pool_slot {
3417 c.pool_last_render[id] = c.render_seq;
3418 }
3419 }
3420 },
3421 Err(e) => eprintln!("Failed to bind pixman image: {:?}", e)
3422 }
3423 }
3424
3425 if render_success {
3426 node.target_seeded = true;
3427 let time = state.clock.now();
3428 let mut feedback = OutputPresentationFeedback::new(&output);
3431 for window in state.space.elements_for_output(&output).cloned().collect::<Vec<_>>() {
3432 window.send_frame(&output, time, Some(Duration::ZERO), |_, _| Some(output.clone()));
3433 window.take_presentation_feedback(
3434 &mut feedback,
3435 |_, _| Some(output.clone()),
3436 |_, _| wp_presentation_feedback::Kind::empty(),
3437 );
3438 }
3439 for layer in layer_map_for_output(&output).layers() {
3444 layer.send_frame(&output, time, Some(Duration::ZERO), |_, _| Some(output.clone()));
3445 layer.take_presentation_feedback(
3446 &mut feedback,
3447 |_, _| Some(output.clone()),
3448 |_, _| wp_presentation_feedback::Kind::empty(),
3449 );
3450 }
3451 send_cursor_frame(state, &output, time);
3452 let refresh = match node.capture.as_ref() {
3453 Some(c) => Refresh::Fixed(Duration::from_secs_f64(1.0 / c.settings.target_fps.max(1.0))),
3454 None => Refresh::Unknown,
3455 };
3456 node.frame_seq += 1;
3457 feedback.presented(time, refresh, node.frame_seq, wp_presentation_feedback::Kind::Vsync);
3458
3459 if !host_mode {
3462 service_copy_frames(state, node, width, height, &damage_rects);
3463 }
3464
3465 if !hold_frame && let Some(cap) = node.capture.as_mut() {
3466 if cap.encode_join.as_ref().is_some_and(|j| j.is_finished()) {
3471 let prior = cap
3472 .encode_join
3473 .take()
3474 .and_then(|j| j.join().ok().flatten());
3475 if let Some(pool) = cap.encode_pool.take() {
3476 pool.shutdown();
3477 }
3478 let s = &cap.settings;
3479 let try_gpu = s.output_mode == 1
3480 && !(s.use_cpu || s.encode_node_index == -1);
3481 eprintln!("[Wayland] encode thread died; rebuilding the readback path.");
3482 bootstrap_readback_pool(cap, node.id, state.use_gpu && !host_mode, try_gpu, prior, None);
3484 cap.request_idr();
3485 }
3486 if cap.encode_pool.is_some() {
3487 if take_screenshot
3488 && let Some((_, ref buf)) = pool_slot {
3489 let n = buf.len().min(node.frame_buffer.len());
3490 node.frame_buffer[..n].copy_from_slice(&buf[..n]);
3491 }
3492 if let Some((id, buf)) = pool_slot.take() {
3493 let is_animated = node.overlay_state.is_animated();
3494 cap.encode_pool.as_ref().unwrap().publish(WlFrame {
3495 id,
3496 buf,
3497 frame_id: cap.frame_counter,
3498 damage: std::mem::take(&mut damage_rects),
3499 is_animated,
3500 });
3501 cap.frame_counter = cap.frame_counter.wrapping_add(1);
3502 }
3503 } else if let Some(ref mut encoder) = cap.video_encoder {
3504 let slot_free = match cap.pending_hw_delivery.take() {
3512 None => true,
3513 Some(pending) => match cap.deliver_tx.as_ref() {
3514 None => true,
3515 Some(tx) => match tx.try_send(pending) {
3516 Ok(()) => true,
3517 Err(std::sync::mpsc::TrySendError::Full(p)) => {
3518 cap.pending_hw_delivery = Some(p);
3519 false
3520 }
3521 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => true,
3522 },
3523 },
3524 };
3525 if !slot_free {
3526 if !damage_rects.is_empty() {
3527 cap.pending_hw_damage = true;
3528 }
3529 } else {
3530 let is_animated = node.overlay_state.is_animated();
3531 let had_damage = !damage_rects.is_empty()
3532 || std::mem::take(&mut cap.pending_hw_damage);
3533 let decision = crate::pipeline::decide_hw_fullframe(
3534 &mut cap.vaapi_state,
3535 &cap.settings,
3536 cap.frame_counter,
3537 had_damage,
3538 is_animated,
3539 requested_idr,
3540 );
3541 let send_frame = decision.send;
3542 let force_idr = decision.force_idr;
3543 let target_qp = decision.target_qp;
3544
3545 let mut frame_out = false;
3546 if send_frame {
3547 if let Some(sync) = render_sync.take() {
3548 let _ = sync.wait();
3549 }
3550 let enc_dmabuf: Option<Dmabuf> = host_enc_dmabuf
3553 .clone()
3554 .or_else(|| node.offscreen_buffer.as_ref().map(|(_, d)| d.clone()));
3555 let result = match encoder {
3556 GpuEncoder::Nvenc(enc) => {
3557 if let Some(ref dmabuf) = enc_dmabuf {
3558 enc.encode(dmabuf, cap.frame_counter as u64, target_qp, force_idr)
3559 } else {
3560 Err("NVENC ZeroCopy requires offscreen buffer (GPU context)".to_string())
3561 }
3562 },
3563 GpuEncoder::Vaapi(enc) => {
3564 if let Some(ref dmabuf) = enc_dmabuf {
3565 enc.encode_dmabuf(dmabuf, cap.frame_counter as u64, target_qp, force_idr)
3566 } else {
3567 Err("Vaapi ZeroCopy requires offscreen buffer (GPU context)".to_string())
3568 }
3569 }
3570 };
3571
3572 if let Ok(data) = result {
3573 cap.hw_error_streak = 0;
3574 cap.hw_rebuilt = false;
3575 if !data.is_empty() {
3576 frame_out = true;
3577 cap.encode_stats.frames.fetch_add(1, Ordering::Relaxed);
3578 cap.encode_stats.stripes.fetch_add(1, Ordering::Relaxed);
3579 if let Some(ref tx) = cap.deliver_tx {
3580 let stripes = vec![EncodedStripe {
3581 data: Arc::new(data), data_type: 2, stripe_y_start: 0,
3582 stripe_height: height, frame_id: cap.frame_counter as i32,
3583 }];
3584 if let Some(ref socket) = cap.recording_sink {
3585 socket.write_frame(&stripes, height);
3586 }
3587 crate::recorder::wayland_tap(node.id, &stripes);
3588 match tx.try_send(stripes) {
3591 Ok(()) => {}
3592 Err(std::sync::mpsc::TrySendError::Full(s)) => {
3593 cap.pending_hw_delivery = Some(s);
3594 }
3595 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => {}
3596 }
3597 }
3598 }
3599 } else if let Err(e) = result {
3600 eprintln!("HW Encode Error: {}", e);
3601 cap.hw_error_streak = cap.hw_error_streak.saturating_add(1);
3602 if cap.hw_error_streak == HW_ERROR_RECOVERY_THRESHOLD {
3603 let rebuilt = if cap.hw_rebuilt {
3611 None
3612 } else {
3613 drop(cap.video_encoder.take());
3618 rebuild_zerocopy_encoder(cap, state)
3619 };
3620 match rebuilt {
3621 Some(enc) => {
3622 cap.video_encoder = Some(enc);
3623 cap.pending_force_idr = true;
3624 cap.hw_rebuilt = true;
3625 eprintln!("[Wayland] zero-copy HW encoder rebuilt after repeated encode errors.");
3626 }
3627 None => {
3628 eprintln!("[Wayland] zero-copy HW encoder unrecoverable; demoting to readback encode.");
3629 cap.video_encoder = None;
3630 cap.hw_rebuilt = false;
3631 let s = &cap.settings;
3634 let try_gpu = s.output_mode == 1
3635 && !(s.use_cpu || s.encode_node_index == -1);
3636 bootstrap_readback_pool(
3639 cap, node.id, state.use_gpu && !host_mode, try_gpu, None,
3640 None,
3641 );
3642 if host_mode
3645 && let Some(h) = state.host.as_ref() {
3646 h.set_buffer_type(node.id, false);
3647 }
3648 }
3649 }
3650 cap.hw_error_streak = 0;
3651 }
3652 }
3653 }
3654 cap.pending_force_idr = requested_idr && !frame_out;
3657 cap.frame_counter = cap.frame_counter.wrapping_add(1);
3658 }
3659 }
3660 }
3661 if take_screenshot
3662 && let Some((_, resp)) = state.pending_screenshot.take() {
3663 if !node.frame_buffer.is_empty() {
3664 let w = width as u32;
3665 let h = height as u32;
3666 let png = if state.use_gpu && !host_cpu_frame {
3669 crate::computer_use::encode_png_rgba(&node.frame_buffer, w, h)
3670 } else {
3671 let mut rgba = node.frame_buffer.clone();
3672 for px in rgba.chunks_exact_mut(4) {
3673 px.swap(0, 2);
3674 }
3675 crate::computer_use::encode_png_rgba(&rgba, w, h)
3676 };
3677 match png {
3678 Ok(data) => { let _ = resp.send(Ok(data)); }
3679 Err(e) => {
3680 let _ = resp.send(Err(format!("PNG encode error: {e}")));
3681 eprintln!("[ComputerUse] PNG encode error: {}", e);
3682 }
3683 }
3684 } else {
3685 let _ = resp.send(Err("Screenshot render produced no pixels".to_string()));
3686 }
3687 }
3688 }
3689 if let Some((id, buf)) = pool_slot.take()
3690 && let Some(cap) = node.capture.as_ref()
3691 && let Some(ref pool) = cap.encode_pool {
3692 pool.cancel(id, buf);
3693 }
3694 false
3695}
3696
3697fn rects_overlap(a: (i32, i32, i32, i32), b: (i32, i32, i32, i32)) -> bool {
3701 let (ax, ay, aw, ah) = (a.0 as i64, a.1 as i64, a.2 as i64, a.3 as i64);
3702 let (bx, by, bw, bh) = (b.0 as i64, b.1 as i64, b.2 as i64, b.3 as i64);
3703 aw > 0 && ah > 0 && bw > 0 && bh > 0
3704 && ax < bx + bw && bx < ax + aw
3705 && ay < by + bh && by < ay + ah
3706}
3707
3708type OutputOverlap = (u32, &'static str, (i32, i32, i32, i32));
3711
3712fn find_output_overlap(
3718 nodes: &[wayland::frontend::OutputNode],
3719 skip_id: Option<u32>,
3720 logical: (i32, i32, i32, i32),
3721 physical: (i32, i32, i32, i32),
3722) -> Option<OutputOverlap> {
3723 for n in nodes {
3724 if Some(n.id) == skip_id {
3725 continue;
3726 }
3727 if let Some(geo) = n.logical_geometry() {
3728 let other = (geo.loc.x, geo.loc.y, geo.size.w, geo.size.h);
3729 if rects_overlap(logical, other) {
3730 return Some((n.id, "logical", other));
3731 }
3732 }
3733 if let Some(mode) = n.output.current_mode() {
3734 let other = (n.pos.0, n.pos.1, mode.size.w, mode.size.h);
3735 if rects_overlap(physical, other) {
3736 return Some((n.id, "physical", other));
3737 }
3738 }
3739 }
3740 None
3741}
3742
3743fn create_output_on(
3750 state: &mut AppState,
3751 id: u32,
3752 width: i32,
3753 height: i32,
3754 x: i32,
3755 y: i32,
3756 scale: f64,
3757) -> bool {
3758 if state.node_idx_for_id(id).is_some() || width <= 0 || height <= 0 || scale <= 0.0 {
3759 return false;
3760 }
3761 if let Some(host) = state.host.as_ref() {
3764 let capacity = host.output_count();
3765 if state.output_nodes.len() >= capacity {
3766 eprintln!(
3767 "[Wayland] CreateOutput {id}: rejected, the host compositor has {capacity} output(s) and all are backing displays."
3768 );
3769 return false;
3770 }
3771 }
3772 let logical_size = (
3773 (width as f64 / scale).round() as i32,
3774 (height as f64 / scale).round() as i32,
3775 );
3776 if let Some((oid, flavor, other)) = find_output_overlap(
3777 &state.output_nodes,
3778 None,
3779 (x, y, logical_size.0, logical_size.1),
3780 (x, y, width, height),
3781 ) {
3782 eprintln!(
3783 "[Wayland] CreateOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
3784 if flavor == "logical" { logical_size.0 } else { width },
3785 if flavor == "logical" { logical_size.1 } else { height },
3786 other.2, other.3, other.0, other.1,
3787 );
3788 return false;
3789 }
3790 let output = Output::new(
3791 format!("HEADLESS-{}", id + 1),
3792 PhysicalProperties {
3793 size: (width, height).into(),
3794 subpixel: Subpixel::Unknown,
3795 make: "Pixelflux".into(),
3796 model: "Virtual".into(),
3797 serial_number: format!("{:03}", id + 1),
3798 },
3799 );
3800 let mode = OutputMode { size: (width, height).into(), refresh: 60_000 };
3801 output.change_current_state(
3802 Some(mode),
3803 Some(Transform::Normal),
3804 Some(OutputScale::Fractional(scale)),
3805 Some((x, y).into()),
3806 );
3807 output.set_preferred(mode);
3808 let mut offscreen = None;
3809 if state.use_gpu {
3810 let Some(gbm) = state.gbm_device.as_mut() else { return false };
3811 match gbm.create_buffer_object(
3812 width as u32,
3813 height as u32,
3814 GbmFormat::Argb8888,
3815 BufferObjectFlags::RENDERING,
3816 ) {
3817 Ok(bo) => {
3818 let dmabuf = create_dmabuf_from_bo(&bo);
3819 offscreen = Some((bo, dmabuf));
3820 }
3821 Err(e) => {
3822 eprintln!("[Wayland] CreateOutput {id}: GBM allocation {width}x{height} failed ({e:?}).");
3823 return false;
3824 }
3825 }
3826 }
3827 state.space.map_output(&output, (x, y));
3828 let global = output.create_global::<AppState>(&state.dh);
3829 let damage_tracker = OutputDamageTracker::from_output(&output);
3830 if let Some(host) = state.host.as_ref() {
3831 host.set_layout(id, x, y);
3832 }
3833 println!("[Wayland] Output {id} created: {width}x{height} @ ({x}, {y}) scale {scale:.2}.");
3834 state.output_nodes.push(wayland::frontend::OutputNode {
3835 id,
3836 output,
3837 global,
3838 pos: (x, y),
3839 damage_tracker,
3840 frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
3841 offscreen_buffer: offscreen,
3842 overlay_state: OverlayState::default(),
3843 capture: None,
3844 frame_seq: 0,
3845 target_seeded: false,
3846 content_hold_until: None,
3847 });
3848 let mut counts: Vec<(u32, usize)> = Vec::new();
3853 for w in state.space.elements() {
3854 let oid = wayland::frontend::window_output_id(w);
3855 match counts.iter_mut().find(|(o, _)| *o == oid) {
3856 Some((_, c)) => *c += 1,
3857 None => counts.push((oid, 1)),
3858 }
3859 }
3860 let newest = |pred: &dyn Fn(&smithay::desktop::Window) -> bool| {
3861 state
3862 .space
3863 .elements()
3864 .filter(|w| pred(w))
3865 .max_by_key(|w| wayland::frontend::window_meta(w).map(|m| m.id).unwrap_or(0))
3866 .cloned()
3867 };
3868 let adopt = newest(&|w| {
3869 wayland::frontend::window_meta(w)
3870 .map(|m| m.parked.load(std::sync::atomic::Ordering::Relaxed))
3871 .unwrap_or(false)
3872 })
3873 .or_else(|| {
3874 newest(&|w| {
3875 let oid = wayland::frontend::window_output_id(w);
3876 counts.iter().any(|(o, c)| *o == oid && *c >= 2)
3877 })
3878 });
3879 if let Some(window) = adopt {
3880 state.place_window_on_output(&window, id);
3881 println!(
3882 "[Wayland] Output {id}: adopted waiting window {}.",
3883 wayland::frontend::window_meta(&window).map(|m| m.id).unwrap_or(0)
3884 );
3885 }
3886 true
3887}
3888
3889fn reposition_output_on(state: &mut AppState, id: u32, x: i32, y: i32) -> bool {
3899 let Some(idx) = state.node_idx_for_id(id) else { return false };
3900 let output = state.output_nodes[idx].output.clone();
3901 if state.output_nodes[idx].pos == (x, y) {
3902 return true;
3903 }
3904 let logical_size = state.output_nodes[idx]
3905 .logical_geometry()
3906 .map(|g| (g.size.w, g.size.h))
3907 .unwrap_or((0, 0));
3908 let physical_size = output.current_mode().map(|m| (m.size.w, m.size.h)).unwrap_or((0, 0));
3909 if let Some((oid, flavor, other)) = find_output_overlap(
3910 &state.output_nodes,
3911 Some(id),
3912 (x, y, logical_size.0, logical_size.1),
3913 (x, y, physical_size.0, physical_size.1),
3914 ) {
3915 eprintln!(
3916 "[Wayland] RepositionOutput {id}: rejected, {flavor} rect {}x{}+{x}+{y} overlaps output {oid} at {}x{}+{}+{}.",
3917 if flavor == "logical" { logical_size.0 } else { physical_size.0 },
3918 if flavor == "logical" { logical_size.1 } else { physical_size.1 },
3919 other.2, other.3, other.0, other.1,
3920 );
3921 return false;
3922 }
3923 state.output_nodes[idx].pos = (x, y);
3924 if let Some(host) = state.host.as_ref() {
3925 host.set_layout(id, x, y);
3926 }
3927 output.change_current_state(None, None, None, Some((x, y).into()));
3928 state.space.map_output(&output, (x, y));
3929 let windows: Vec<smithay::desktop::Window> = state
3930 .space
3931 .elements()
3932 .filter(|w| wayland::frontend::window_output_id(w) == id)
3933 .cloned()
3934 .collect();
3935 for window in &windows {
3936 state.space.map_element(window.clone(), (x, y), false);
3937 }
3938 if let Some(cap) = state.output_nodes[idx].capture.as_mut() {
3939 cap.needs_full_render = true;
3940 }
3941 println!("[Wayland] Output {id} repositioned to ({x}, {y}).");
3942 true
3943}
3944
3945fn destroy_output_on(state: &mut AppState, id: u32) -> bool {
3948 if id == 0 {
3949 return false;
3950 }
3951 let Some(_) = state.node_idx_for_id(id) else { return false };
3952 stop_capture_on_display(state, id);
3953 if let Some(host) = state.host.as_ref() {
3954 host.idle_output(id);
3955 }
3956 wayland_owners().lock().unwrap().remove(&id);
3957 let windows: Vec<smithay::desktop::Window> = state
3959 .space
3960 .elements()
3961 .filter(|w| wayland::frontend::window_output_id(w) == id)
3962 .cloned()
3963 .collect();
3964 for window in &windows {
3965 if state.would_cover_screen(window, 0) {
3968 state.park_window(window, 0);
3969 } else {
3970 state.place_window_on_output(window, 0);
3971 }
3972 }
3973 for w in &state.pending_windows {
3974 if let Some(meta) = wayland::frontend::window_meta(w)
3975 && meta.output.load(Ordering::Relaxed) == id {
3976 meta.output.store(0, Ordering::Relaxed);
3977 }
3978 }
3979 let idx = state.node_idx_for_id(id).unwrap();
3980 let node = state.output_nodes.remove(idx);
3981 state.space.unmap_output(&node.output);
3982 state.dh.remove_global::<AppState>(node.global);
3983 println!(
3984 "[Wayland] Output {id} destroyed; {} window(s) relocated to primary.",
3985 windows.len()
3986 );
3987 true
3988}
3989struct WaylandThreadConfig {
3992 command_rx: smithay::reexports::calloop::channel::Channel<ThreadCommand>,
3993 wake_rx: smithay::reexports::calloop::channel::Channel<()>,
3994 command_tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
3995 initial_width: i32,
3996 initial_height: i32,
3997 explicit_dri_node: String,
3998 auto_gpu_selected: bool,
3999 cursor_size: i32,
4000}
4001
4002fn gpu_render_init(
4008 device_path: &std::path::Path,
4009) -> Result<(RawGbmDevice<File>, GlesRenderer), String> {
4010 let file = File::options().read(true).write(true).open(device_path)
4011 .map_err(|e| format!("Failed to open render device: {}", e))?;
4012 let file_for_alloc = file.try_clone()
4013 .map_err(|e| format!("Failed to clone file for GBM Allocator: {}", e))?;
4014 let gbm_allocator = RawGbmDevice::new(file_for_alloc)
4015 .map_err(|_| "Failed to create Raw GBM Device")?;
4016 let gbm = GbmDevice::new(file)
4017 .map_err(|_| "Failed to create GBM device")?;
4018 let egl = unsafe { EGLDisplay::new(gbm) }
4019 .map_err(|_| "Failed to create EGL display")?;
4020 let context = EGLContext::new(&egl)
4021 .map_err(|_| "Failed to create EGL context")?;
4022 let renderer = unsafe { GlesRenderer::new(context) }
4023 .map_err(|_| "Failed to init GlesRenderer")?;
4024 Ok((gbm_allocator, renderer))
4025}
4026
4027fn gl_renderer_name(renderer: &mut GlesRenderer) -> String {
4031 renderer
4032 .with_context(|gl| unsafe {
4033 let ptr = gl.GetString(smithay::backend::renderer::gles::ffi::RENDERER);
4034 if ptr.is_null() {
4035 String::new()
4036 } else {
4037 std::ffi::CStr::from_ptr(ptr as *const std::ffi::c_char)
4038 .to_string_lossy()
4039 .into_owned()
4040 }
4041 })
4042 .unwrap_or_default()
4043}
4044
4045fn gpu_exposed() -> bool {
4051 std::path::Path::new("/dev/nvidiactl").exists()
4052 || std::fs::read_dir("/dev/dri")
4053 .into_iter()
4054 .flatten()
4055 .flatten()
4056 .any(|e| {
4057 let name = e.file_name();
4058 let name = name.to_string_lossy();
4059 name.starts_with("renderD") || name.starts_with("card")
4060 })
4061}
4062
4063fn run_wayland_thread(cfg: WaylandThreadConfig) {
4106 let WaylandThreadConfig {
4107 command_rx,
4108 wake_rx,
4109 command_tx,
4110 initial_width,
4111 initial_height,
4112 explicit_dri_node,
4113 auto_gpu_selected,
4114 cursor_size,
4115 } = cfg;
4116 let width: i32 = if initial_width > 0 { initial_width } else { 1024 };
4117 let height: i32 = if initial_height > 0 { initial_height } else { 768 };
4118
4119 let mut event_loop = match EventLoop::<AppState>::try_new() {
4120 Ok(l) => l,
4121 Err(e) => {
4122 eprintln!("[Wayland] compositor thread aborting: event loop init failed: {e}");
4123 return;
4124 }
4125 };
4126 let display: Display<AppState> = match Display::new() {
4127 Ok(d) => d,
4128 Err(e) => {
4129 eprintln!("[Wayland] compositor thread aborting: display init failed: {e}");
4130 return;
4131 }
4132 };
4133 let dh: DisplayHandle = display.handle();
4134 unsafe {
4135 if let Ok(lib) = libloading::Library::new("libwayland-server.so.0") {
4136 if let Ok(set_max) = lib.get::<unsafe extern "C" fn(*mut std::ffi::c_void, usize)>(
4137 b"wl_display_set_default_max_buffer_size\0",
4138 ) {
4139 set_max(
4140 dh.backend_handle().display_ptr() as *mut std::ffi::c_void,
4141 10 * 1024 * 1024,
4142 );
4143 }
4144 std::mem::forget(lib);
4145 }
4146 }
4147
4148 let dri_node = explicit_dri_node;
4149
4150 let mut use_gpu = !dri_node.is_empty();
4151 let render_node_path = dri_node.clone();
4152
4153 let mut gles_renderer = None;
4154 let mut pixman_renderer = None;
4155 let mut offscreen_buffer: Option<(BufferObject<()>, Dmabuf)> = None;
4156 let mut dmabuf_global = None;
4157 let mut gbm_device_raw = None;
4158 let mut dmabuf_state = DmabufState::new();
4159
4160 let mut gpu_success = false;
4161 if use_gpu {
4162 println!("[Wayland] Initializing GL Renderer using device: {}", dri_node);
4163 let init_res: Result<(), String> = (|| {
4164 let device_path = std::path::Path::new(&dri_node);
4165 let (gbm_allocator, mut renderer) = gpu_render_init(device_path)?;
4166
4167 if let Err(e) = renderer.bind_wl_display(&dh) {
4168 println!("[Wayland] Warning: Failed to bind EGL to Wayland Display (Optional): {:?}", e);
4169 }
4170
4171 let formats = Bind::<Dmabuf>::supported_formats(&renderer)
4172 .ok_or("Failed to query formats")?
4173 .into_iter()
4174 .collect::<Vec<_>>();
4175
4176 let node = DrmNode::from_path(device_path)
4177 .map_err(|_| "Failed to create DrmNode")?;
4178 let dmabuf_default_feedback = DmabufFeedbackBuilder::new(node.dev_id(), formats.clone()).build();
4179
4180 dmabuf_global = Some(if let Ok(default_feedback) = dmabuf_default_feedback {
4181 dmabuf_state.create_global_with_default_feedback::<AppState>(&dh, &default_feedback)
4182 } else {
4183 dmabuf_state.create_global::<AppState>(&dh, formats)
4184 });
4185
4186 let bo = gbm_allocator.create_buffer_object(
4187 width as u32, height as u32, GbmFormat::Argb8888, BufferObjectFlags::RENDERING
4188 ).map_err(|_| "Failed to allocate GBM buffer")?;
4189
4190 let dmabuf = create_dmabuf_from_bo(&bo);
4191 offscreen_buffer = Some((bo, dmabuf));
4192 gbm_device_raw = Some(gbm_allocator);
4193 gles_renderer = Some(renderer);
4194 Ok(())
4195 })();
4196
4197 match init_res {
4198 Ok(_) => gpu_success = true,
4199 Err(e) => {
4200 println!("[Wayland] GPU Initialization failed: {}. Falling back to Software Renderer (Pixman).", e);
4201 use_gpu = false;
4202 }
4203 }
4204 }
4205
4206 if !gpu_success {
4207 if dri_node.is_empty() {
4208 println!("[Wayland] No render node. Initializing Software Renderer (Pixman).");
4209 }
4210 pixman_renderer = Some(PixmanRenderer::new().expect("Failed to init PixmanRenderer"));
4211 use_gpu = false;
4212 }
4213
4214 let compositor_state = CompositorState::new_v6::<AppState>(&dh);
4215 let image_capture_source_state = ImageCaptureSourceState::new();
4216 let output_capture_source_state = OutputCaptureSourceState::new::<AppState>(&dh);
4217 let image_copy_capture_state = ImageCopyCaptureState::new::<AppState>(&dh);
4218 let fractional_scale_state = FractionalScaleManagerState::new::<AppState>(&dh);
4219 let shm_state = ShmState::new::<AppState>(&dh, vec![]);
4220 let output_state = OutputManagerState::new_with_xdg_output::<AppState>(&dh);
4221 let mut seat_state = SeatState::new();
4222 let shell_state = XdgShellState::new::<AppState>(&dh);
4223 let space = Space::default();
4224 let layer_shell_state = WlrLayerShellState::new::<AppState>(&dh);
4225 let data_device_state = DataDeviceState::new::<AppState>(&dh);
4226 let data_control_state = DataControlState::new::<AppState, _>(&dh, None, |_| true);
4227 let ext_data_control_state = ExtDataControlState::new::<AppState, _>(&dh, None, |_| true);
4228 let cursor_shape_state = CursorShapeManagerState::new::<AppState>(&dh);
4229 let _vk_global = dh.create_global::<AppState, ZwpVirtualKeyboardManagerV1, _>(1, ());
4230 let pointer_warp_state = PointerWarpManager::new::<AppState>(&dh);
4231 let relative_pointer_state = RelativePointerManagerState::new::<AppState>(&dh);
4232 let pointer_constraints_state = PointerConstraintsState::new::<AppState>(&dh);
4233
4234 let foreign_toplevel_list = ForeignToplevelListState::new::<AppState>(&dh);
4235 let xdg_decoration_state = XdgDecorationState::new::<AppState>(&dh);
4236 let single_pixel_buffer = SinglePixelBufferState::new::<AppState>(&dh);
4237 let viewporter_state = ViewporterState::new::<AppState>(&dh);
4238 let presentation_state = PresentationState::new::<AppState>(&dh, 1);
4239 let xdg_activation_state = XdgActivationState::new::<AppState>(&dh);
4240 let primary_selection_state = PrimarySelectionState::new::<AppState>(&dh);
4241 let popups = PopupManager::default();
4242
4243 let mut seat = seat_state.new_wl_seat(&dh, "seat0");
4244 seat.add_keyboard(XkbConfig::default(), 200, 25)
4245 .expect("Failed to init keyboard");
4246 seat.add_pointer();
4247
4248 let mut state = AppState {
4249 compositor_state,
4250 fractional_scale_state,
4251 viewporter_state,
4252 presentation_state,
4253 shm_state,
4254 single_pixel_buffer,
4255 dmabuf_state,
4256 dmabuf_global,
4257 ext_data_control_state,
4258 cursor_shape_state,
4259 image_capture_source_state,
4260 output_capture_source_state,
4261 image_copy_capture_state,
4262 copy_sessions: Vec::new(),
4263 output_state,
4264 seat_state,
4265 shell_state,
4266 layer_shell_state,
4267 space,
4268 data_device_state,
4269 data_control_state,
4270 dh: dh.clone(),
4271 seat,
4272 pointer_warp_state,
4273 relative_pointer_state,
4274 pointer_constraints_state,
4275 output_nodes: Vec::new(),
4276 pending_windows: Vec::new(),
4277 foreign_toplevel_list,
4278 xdg_decoration_state,
4279 xdg_activation_state,
4280 primary_selection_state,
4281 popups,
4282 gles_renderer,
4283 pixman_renderer,
4284 gbm_device: gbm_device_raw,
4285 settings: RustCaptureSettings {
4286 width,
4287 height,
4288 ..RustCaptureSettings::default()
4289 },
4290 cursor_callback_set: false,
4291 cursor_tx: wayland::cursor::spawn_cursor_worker(
4292 cursor_size,
4293 RustCaptureSettings::default().cursor_size_cap,
4294 ),
4295 clipboard_callback: None,
4296 pending_clipboard_read: None,
4297 current_selection_mime: None,
4298 last_log_time: Instant::now(),
4299 start_time: Instant::now(),
4300 clock: Clock::new(),
4301 use_gpu,
4302 cursor_helper: Cursor::load(cursor_size),
4303 keymap_policy: wayland::keymap::KeymapPolicy::empty(),
4304 host: None,
4305 host_layout_pending: std::collections::HashMap::new(),
4306 current_cursor_icon: None,
4307 cursor_surface_pending: false,
4308 cursor_buffer: None,
4309 render_cursor_on_framebuffer: false,
4310 render_node_path,
4311 auto_gpu_selected,
4312 pending_screenshot: None,
4313 command_rx: None,
4314 last_input_at: None,
4315 frame_idle_long: false,
4316 last_idle_service_at: None,
4317 deliver_reaper: Vec::new(),
4318 encode_reaper: Vec::new(),
4319 };
4320 {
4323 let initial_keymap = if let Some(kb) = state.seat.get_keyboard() {
4324 kb.with_xkb_state(&mut state, |context| match context.xkb().lock() {
4325 Ok(guard) => {
4326 let keymap = unsafe { guard.keymap() };
4327 keymap.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
4328 }
4329 Err(_) => String::new(),
4330 })
4331 } else {
4332 String::new()
4333 };
4334 state.keymap_policy.rebuild_base(initial_keymap);
4335 }
4336
4337 let output = Output::new(
4338 "HEADLESS-1".into(),
4339 PhysicalProperties {
4340 size: (width, height).into(),
4341 subpixel: Subpixel::Unknown,
4342 make: "Pixelflux".into(),
4343 model: "Virtual".into(),
4344 serial_number: "001".into(),
4345 },
4346 );
4347 output.change_current_state(
4348 Some(OutputMode {
4349 size: (width, height).into(),
4350 refresh: 60_000,
4351 }),
4352 Some(Transform::Normal),
4353 Some(OutputScale::Fractional(1.0)),
4354 Some((0, 0).into()),
4355 );
4356 output.set_preferred(OutputMode {
4357 size: (width, height).into(),
4358 refresh: 60_000,
4359 });
4360 state.space.map_output(&output, (0, 0));
4361 let global = output.create_global::<AppState>(&dh);
4362 let damage_tracker = OutputDamageTracker::from_output(&output);
4363 state.output_nodes.push(wayland::frontend::OutputNode {
4364 id: 0,
4365 output,
4366 global,
4367 pos: (0, 0),
4368 damage_tracker,
4369 frame_buffer: vec![0u8; (width.max(0) as usize) * (height.max(0) as usize) * 4],
4370 offscreen_buffer,
4371 overlay_state: OverlayState::default(),
4372 capture: None,
4373 frame_seq: 0,
4374 target_seeded: false,
4375 content_hold_until: None,
4376 });
4377
4378 fn drain_thread_commands(state: &mut AppState) {
4382 let Some(rx) = state.command_rx.take() else { return };
4383 let mut had_input = false;
4384 while let Ok(cmd) = rx.try_recv() {
4385 had_input |= matches!(
4386 cmd,
4387 ThreadCommand::KeyboardKey { .. }
4388 | ThreadCommand::KeyboardKeys { .. }
4389 | ThreadCommand::PointerMotion { .. }
4390 | ThreadCommand::PointerRelativeMotion { .. }
4391 | ThreadCommand::PointerButton { .. }
4392 | ThreadCommand::PointerAxis { .. }
4393 );
4394 handle_thread_command(state, cmd);
4395 }
4396 if had_input {
4397 state.last_input_at = Some(Instant::now());
4398 }
4399 state.command_rx = Some(rx);
4400 }
4401
4402 fn send_idle_frame_callbacks(state: &mut AppState) {
4409 let time = state.clock.now();
4410 for node in &state.output_nodes {
4411 let mut feedback = OutputPresentationFeedback::new(&node.output);
4412 for window in state
4413 .space
4414 .elements_for_output(&node.output)
4415 .cloned()
4416 .collect::<Vec<_>>()
4417 {
4418 window.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
4419 Some(node.output.clone())
4420 });
4421 window.take_presentation_feedback(
4422 &mut feedback,
4423 |_, _| Some(node.output.clone()),
4424 |_, _| wp_presentation_feedback::Kind::empty(),
4425 );
4426 }
4427 for layer in layer_map_for_output(&node.output).layers() {
4428 layer.send_frame(&node.output, time, Some(Duration::ZERO), |_, _| {
4429 Some(node.output.clone())
4430 });
4431 layer.take_presentation_feedback(
4432 &mut feedback,
4433 |_, _| Some(node.output.clone()),
4434 |_, _| wp_presentation_feedback::Kind::empty(),
4435 );
4436 }
4437 feedback.discarded();
4438 }
4439 if let Some(output) = state.primary_output() {
4440 send_cursor_frame(state, output, time);
4441 }
4442 if let Some(renderer) = state.gles_renderer.as_mut() {
4443 let _ = renderer.cleanup_texture_cache();
4444 }
4445 }
4446
4447 fn handle_thread_command(state: &mut AppState, cmd: ThreadCommand) {
4448 match cmd {
4449 ThreadCommand::StartCapture { display_id, callback, settings } => {
4450 start_capture_on_display(state, display_id, callback.map(Arc::new), settings);
4451 }
4452 ThreadCommand::StopCapture { display_id } => {
4453 stop_capture_on_display(state, display_id);
4458 }
4459 ThreadCommand::CreateOutput { id, width, height, x, y, scale, reply } => {
4460 let _ = reply.send(create_output_on(state, id, width, height, x, y, scale));
4461 }
4462 ThreadCommand::DestroyOutput { id, reply } => {
4463 let _ = reply.send(destroy_output_on(state, id));
4464 }
4465 ThreadCommand::OutputCapacity { reply } => {
4466 let _ = reply
4467 .send(state.host.as_ref().map_or(-1, |h| h.output_count() as i64));
4468 }
4469 ThreadCommand::RepositionOutput { id, x, y, reply } => {
4470 let _ = reply.send(reposition_output_on(state, id, x, y));
4471 }
4472 ThreadCommand::ListOutputs { reply } => {
4473 let list = state
4474 .output_nodes
4475 .iter()
4476 .map(|n| {
4477 let (w, h) = n
4478 .output
4479 .current_mode()
4480 .map(|m| (m.size.w, m.size.h))
4481 .unwrap_or((0, 0));
4482 (
4483 n.id,
4484 n.pos.0,
4485 n.pos.1,
4486 w,
4487 h,
4488 n.output.current_scale().fractional_scale(),
4489 n.capture.is_some(),
4490 )
4491 })
4492 .collect();
4493 let _ = reply.send(list);
4494 }
4495 ThreadCommand::MoveWindowToOutput { window_id, output_id, reply } => {
4496 let window = state
4497 .space
4498 .elements()
4499 .find(|w| {
4500 wayland::frontend::window_meta(w)
4501 .map(|m| m.id == window_id)
4502 .unwrap_or(false)
4503 })
4504 .cloned();
4505 let ok = match window {
4506 Some(w) => state.place_window_on_output(&w, output_id),
4507 None => false,
4508 };
4509 let _ = reply.send(ok);
4510 }
4511 ThreadCommand::ListWindows { reply } => {
4512 use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
4513 let mut list = Vec::new();
4514 for window in state.space.elements() {
4515 let Some(meta) = wayland::frontend::window_meta(window) else { continue };
4516 let (title, app_id) = window
4517 .toplevel()
4518 .map(|tl| {
4519 with_states(tl.wl_surface(), |states| {
4520 states
4521 .data_map
4522 .get::<XdgToplevelSurfaceData>()
4523 .map(|d| {
4524 let a = d.lock().unwrap();
4525 (
4526 a.title.clone().unwrap_or_default(),
4527 a.app_id.clone().unwrap_or_default(),
4528 )
4529 })
4530 .unwrap_or_default()
4531 })
4532 })
4533 .unwrap_or_default();
4534 list.push((
4535 meta.id,
4536 title,
4537 app_id,
4538 meta.output.load(Ordering::Relaxed),
4539 meta.parked.load(Ordering::Relaxed),
4540 ));
4541 }
4542 let _ = reply.send(list);
4543 }
4544 ThreadCommand::SetClipboardCallback(cb) => {
4545 state.clipboard_callback = Some(cb);
4546 if let Some(mime) = state.current_selection_mime.clone() {
4550 state.pending_clipboard_read = Some(mime);
4551 }
4552 }
4553 ThreadCommand::SetClipboard { mime, data } => {
4554 let mimes: Vec<String> = if mime.starts_with("text/plain") {
4555 ["text/plain;charset=utf-8", "UTF8_STRING", "text/plain",
4556 "STRING", "TEXT"].iter().map(|s| s.to_string()).collect()
4557 } else {
4558 vec![mime.clone()]
4559 };
4560 let payload = std::sync::Arc::new((mime, data));
4561 smithay::wayland::selection::data_device::set_data_device_selection(
4562 &state.dh,
4563 &state.seat.clone(),
4564 mimes.clone(),
4565 payload.clone(),
4566 );
4567 smithay::wayland::selection::primary_selection::set_primary_selection(
4570 &state.dh,
4571 &state.seat.clone(),
4572 mimes,
4573 payload,
4574 );
4575 state.current_selection_mime = None;
4578 }
4579 ThreadCommand::SetCursorCallback(cb) => {
4580 let _ = state.cursor_tx.send(CursorJob::SetCallback(cb));
4581 state.cursor_callback_set = true;
4582 if let Some(icon) = state.current_cursor_icon.clone() {
4583 state.send_cursor_image(&icon);
4584 } else {
4585 state.send_cursor_image(&CursorImageStatus::Named(Default::default()));
4590 }
4591 }
4592 ThreadCommand::KeyboardKeys { events } => {
4593 for (scancode, key_state_val) in events {
4594 if let Some(host) = state.host.as_ref() {
4595 host.key(scancode, key_state_val > 0);
4596 continue;
4597 }
4598 let key_state = if key_state_val > 0 {
4599 KeyState::Pressed
4600 } else {
4601 KeyState::Released
4602 };
4603 let serial = next_serial();
4604 let time = wayland_time();
4605 if let Some(keyboard) = state.seat.get_keyboard() {
4606 keyboard.input(
4607 state,
4608 Keycode::new(scancode),
4609 key_state,
4610 serial,
4611 time,
4612 |_, _, _| FilterResult::<()>::Forward,
4613 );
4614 }
4615 }
4616 }
4617 ThreadCommand::KeyboardKey { scancode, state: key_state_val } => {
4618 if let Some(host) = state.host.as_ref() {
4619 host.key(scancode, key_state_val > 0);
4620 return;
4621 }
4622 let key_state = if key_state_val > 0 { KeyState::Pressed } else { KeyState::Released };
4623 let serial = next_serial();
4624 let time = wayland_time();
4625 if let Some(keyboard) = state.seat.get_keyboard() {
4626 keyboard.input(state, Keycode::new(scancode), key_state, serial, time, |_, _, _| {
4627 FilterResult::<()>::Forward
4628 });
4629 }
4630 }
4631 ThreadCommand::SetKeymapString(text) => {
4632 if state.keymap_policy.rebuild_base(text) {
4635 state.apply_keymap_policy();
4636 } else {
4637 eprintln!("[Wayland] set_keymap_string: keymap failed to compile; keeping current keymap.");
4638 }
4639 }
4640 ThreadCommand::SetXkbLayout { rules, model, layout, variant, options, reply } => {
4641 match crate::wayland::keymap::compile_rmlvo(&rules, &model, &layout, &variant, &options) {
4642 Some(text) => {
4643 state.keymap_policy.rebuild_base(text);
4644 state.apply_keymap_policy();
4645 let _ = reply.send(true);
4646 }
4647 None => {
4648 eprintln!("[Wayland] set_xkb_layout: RMLVO ({rules:?}, {model:?}, {layout:?}, {variant:?}, {options:?}) failed to compile.");
4649 let _ = reply.send(false);
4650 }
4651 }
4652 }
4653 ThreadCommand::BindKeysyms { keysyms, reply } => {
4654 let _ = reply.send(state.bind_keysyms(&keysyms));
4655 }
4656 ThreadCommand::SetKeymapOverlay { binds } => {
4657 if state.keymap_policy.has_base() {
4658 state.keymap_policy.set_manual_overlay(&binds);
4659 state.apply_keymap_policy();
4660 } else {
4661 eprintln!(
4662 "[Wayland] set_keymap_overlay: no base keymap to splice onto."
4663 );
4664 }
4665 }
4666 ThreadCommand::GetKeyboardState { reply } => {
4667 let (pressed, mods) = state
4668 .seat
4669 .get_keyboard()
4670 .map(|kb| {
4671 let pressed: Vec<u32> =
4672 kb.pressed_keys().iter().map(|c| c.raw()).collect();
4673 let m = kb.modifier_state();
4674 let mask = (m.ctrl as u32)
4675 | (m.shift as u32) << 1
4676 | (m.alt as u32) << 2
4677 | (m.logo as u32) << 3
4678 | (m.caps_lock as u32) << 4
4679 | (m.num_lock as u32) << 5
4680 | (m.iso_level3_shift as u32) << 6
4681 | (m.iso_level5_shift as u32) << 7;
4682 (pressed, mask)
4683 })
4684 .unwrap_or_default();
4685 let _ = reply.send((pressed, mods));
4686 }
4687 ThreadCommand::Barrier { reply } => {
4688 for join in state.encode_reaper.drain(..) {
4696 let _ = join.join();
4697 }
4698 for join in state.deliver_reaper.drain(..) {
4699 let _ = join.join();
4700 }
4701 let _ = reply.send(());
4702 }
4703 ThreadCommand::GetXkbKeymap { reply } => {
4704 let mut keymap_str = String::new();
4705 if let Some(keyboard) = state.seat.get_keyboard() {
4706 keymap_str = keyboard.with_xkb_state(state, |context| {
4707 match context.xkb().lock() {
4708 Ok(guard) => {
4709 let keymap = unsafe { guard.keymap() };
4710 keymap.get_as_string(
4711 smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1,
4712 )
4713 }
4714 Err(_) => String::new(),
4715 }
4716 });
4717 }
4718 let _ = reply.send(keymap_str);
4719 }
4720 ThreadCommand::PointerMotion { x, y } => {
4721 if let Some(host) = state.host.as_ref() {
4722 host.pointer_motion_abs(x, y);
4723 return;
4724 }
4725 let serial = next_serial();
4726 let time = wayland_time();
4727 let p = state.layout_physical_to_logical(x, y);
4732
4733 if let Some(pointer) = state.seat.get_pointer() {
4734 let layer_hit = |state: &AppState, layers: &[smithay::wayland::shell::wlr_layer::Layer]| {
4738 let idx = state.node_idx_under(p)?;
4739 let node = &state.output_nodes[idx];
4740 let origin = Point::<i32, smithay::utils::Logical>::from(node.pos);
4741 let local = (p - origin.to_f64()).to_i32_round();
4742 let layer_map = layer_map_for_output(&node.output);
4743 for layer in layer_map.layers().rev() {
4744 if layers.contains(&layer.layer())
4745 && let Some(bbox) = layer_map.layer_geometry(layer)
4746 && bbox.contains(local) {
4747 return Some((
4748 FocusTarget::LayerSurface(layer.clone()),
4749 (bbox.loc + origin).to_f64(),
4750 ));
4751 }
4752 }
4753 None
4754 };
4755
4756 let mut under = layer_hit(state, &[
4757 smithay::wayland::shell::wlr_layer::Layer::Overlay,
4758 smithay::wayland::shell::wlr_layer::Layer::Top,
4759 ]);
4760
4761 if under.is_none() {
4762 under = state.space.element_under(p).map(|(window, loc)| {
4763 (FocusTarget::Window(window.clone()), loc.to_f64())
4764 });
4765 }
4766
4767 if under.is_none() {
4768 under = layer_hit(state, &[
4769 smithay::wayland::shell::wlr_layer::Layer::Bottom,
4770 smithay::wayland::shell::wlr_layer::Layer::Background,
4771 ]);
4772 }
4773
4774 pointer.motion(state, under, &MotionEvent { location: p, serial, time });
4775 pointer.frame(state);
4776 }
4777 }
4778 ThreadCommand::PointerRelativeMotion { dx, dy } => {
4779 if let Some(host) = state.host.as_ref() {
4780 host.pointer_motion_rel(dx, dy);
4781 return;
4782 }
4783 let utime = wayland_utime();
4784 let time = wayland_time();
4785 let serial = next_serial();
4786
4787 if let Some(pointer) = state.seat.get_pointer() {
4788 let current_pos = pointer.current_location();
4789 let new_pos = state.clamp_logical(
4790 (current_pos.x + dx, current_pos.y + dy).into(),
4791 );
4792
4793 let under = state.space.element_under(new_pos).map(|(window, loc)| {
4794 (FocusTarget::Window(window.clone()), loc.to_f64())
4795 });
4796
4797 pointer.motion(
4798 state,
4799 under.clone(),
4800 &MotionEvent {
4801 location: new_pos,
4802 serial,
4803 time
4804 }
4805 );
4806
4807 let event = RelativeMotionEvent {
4808 utime,
4809 delta: (dx, dy).into(),
4810 delta_unaccel: (dx, dy).into(),
4811 };
4812 pointer.relative_motion(state, under, &event);
4813
4814 pointer.frame(state);
4815 }
4816 }
4817 ThreadCommand::PointerButton { btn, state: btn_state_val } => {
4818 if let Some(host) = state.host.as_ref() {
4819 host.pointer_button(btn, btn_state_val > 0);
4820 return;
4821 }
4822 let serial = next_serial();
4823 let time = wayland_time();
4824 let button_state = if btn_state_val > 0 { smithay::backend::input::ButtonState::Pressed } else { smithay::backend::input::ButtonState::Released };
4825
4826 if let Some(pointer) = state.seat.get_pointer() {
4827 if button_state == smithay::backend::input::ButtonState::Pressed {
4828 let pos = pointer.current_location();
4829 let target_window = state.space.element_under(pos).map(|(w, _)| w.clone());
4830
4831 if let Some(window) = target_window {
4832 state.space.raise_element(&window, true);
4833 if let Some(keyboard) = state.seat.get_keyboard() {
4834 keyboard.set_focus(state, Some(FocusTarget::Window(window)), serial);
4835 }
4836 }
4837 }
4838 let button = btn;
4839 pointer.button(state, &ButtonEvent { button, state: button_state, serial, time });
4840 pointer.frame(state);
4841 }
4842 }
4843 ThreadCommand::PointerAxis { x, y } => {
4844 if let Some(host) = state.host.as_ref() {
4845 host.pointer_axis(x, y);
4846 return;
4847 }
4848 let time = wayland_time();
4849
4850 if let Some(pointer) = state.seat.get_pointer() {
4851 let mut frame = AxisFrame::new(time).source(AxisSource::Wheel);
4852
4853 if x != 0.0 {
4854 frame = frame
4855 .value(Axis::Horizontal, x)
4856 .v120(Axis::Horizontal, (x * SCROLL_V120_PER_UNIT) as i32);
4857 }
4858
4859 if y != 0.0 {
4860 frame = frame
4861 .value(Axis::Vertical, y)
4862 .v120(Axis::Vertical, (y * SCROLL_V120_PER_UNIT) as i32);
4863 }
4864
4865 if x != 0.0 || y != 0.0 {
4866 pointer.axis(state, frame);
4867 pointer.frame(state);
4868 }
4869 }
4870 }
4871 ThreadCommand::UpdateCursorConfig { render_on_framebuffer } => {
4872 state.render_cursor_on_framebuffer = render_on_framebuffer;
4873 if let Some(host) = state.host.as_ref() {
4874 host.set_cursor_painting(render_on_framebuffer);
4875 }
4876 }
4877 ThreadCommand::SetCursorSize { size, reply } => {
4878 if size <= 0 {
4879 let _ = reply.send(false);
4880 } else {
4881 state.cursor_helper = Cursor::load(size);
4882 let _ = state.cursor_tx.send(CursorJob::SetSize(size));
4883 for node in state.output_nodes.iter_mut() {
4886 if let Some(cap) = node.capture.as_mut() {
4887 cap.needs_full_render = true;
4888 }
4889 }
4890 let _ = reply.send(true);
4891 }
4892 }
4893 ThreadCommand::RequestIdr { display_id } => {
4894 if let Some(idx) = state.node_idx_for_id(display_id)
4895 && let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4896 cap.request_idr();
4897 }
4898 }
4899 ThreadCommand::UpdateRate { display_id, bitrate_kbps, vbv_multiplier, fps } => {
4900 if let Some(idx) = state.node_idx_for_id(display_id)
4901 && let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4902 if let Some(b) = bitrate_kbps { cap.settings.video_bitrate_kbps = b; }
4903 if let Some(v) = vbv_multiplier { cap.settings.video_vbv_multiplier = v; }
4904 if let Some(f) = fps && f > 0.0 { cap.settings.target_fps = f; }
4905 if let Some(GpuEncoder::Nvenc(enc)) = cap.video_encoder.as_mut() {
4906 enc.reconfigure_rate(&cap.settings);
4907 }
4908 if let Some(GpuEncoder::Vaapi(enc)) = cap.video_encoder.as_mut()
4909 && let Err(e) = enc.reconfigure_rate(&cap.settings) {
4910 eprintln!("[Wayland] VAAPI rate reconfigure failed: {e}");
4914 cap.hw_error_streak = HW_ERROR_RECOVERY_THRESHOLD - 1;
4915 }
4916 let c = &cap.encode_controls;
4917 c.bitrate_kbps.store(cap.settings.video_bitrate_kbps, Ordering::Relaxed);
4918 c.vbv_mult_milli.store(
4919 (cap.settings.video_vbv_multiplier * 1000.0).round() as i32,
4920 Ordering::Relaxed,
4921 );
4922 c.fps_milli.store(
4923 (cap.settings.target_fps.max(1.0) * 1000.0) as u64,
4924 Ordering::Relaxed,
4925 );
4926 c.rate_dirty.store(true, Ordering::Release);
4927 if display_id == 0 {
4928 state.settings.video_bitrate_kbps = cap.settings.video_bitrate_kbps;
4929 state.settings.video_vbv_multiplier = cap.settings.video_vbv_multiplier;
4930 state.settings.target_fps = cap.settings.target_fps;
4931 }
4932 }
4933 }
4934 ThreadCommand::UpdateTunables { display_id, tunables: t } => {
4935 state.render_cursor_on_framebuffer = t.capture_cursor;
4936 if let Some(host) = state.host.as_ref() {
4937 host.set_cursor_painting(t.capture_cursor);
4938 }
4939 let _ = state.cursor_tx.send(CursorJob::SetSizeCap(t.cursor_size_cap));
4940 if display_id == 0 {
4941 t.apply_to(&mut state.settings);
4942 }
4943 if let Some(idx) = state.node_idx_for_id(display_id)
4944 && let Some(cap) = state.output_nodes[idx].capture.as_mut() {
4945 t.apply_to(&mut cap.settings);
4946 *cap.encode_controls.tunables.lock().unwrap() = Some(t);
4947 cap.encode_controls.tunables_dirty.store(true, Ordering::Release);
4948 }
4949 }
4950 ThreadCommand::CuScreenshot { display_id, resp } => {
4951 if state.node_idx_for_id(display_id).is_some() {
4952 state.pending_screenshot = Some((display_id, resp));
4953 } else {
4954 let _ = resp.send(Err(format!("Unknown display: {display_id}")));
4955 }
4956 }
4957 ThreadCommand::CuCursorPosition { resp } => {
4958 let pos = state.seat.get_pointer()
4959 .map(|p| p.current_location())
4960 .unwrap_or_else(|| (0.0f64, 0.0f64).into());
4961 let _ = resp.send(state.layout_logical_to_physical(pos));
4962 }
4963 ThreadCommand::CuGetInfo { display_id, resp } => {
4964 match state.host_layout_pending.get_mut(&display_id) {
4968 Some(p) => p.geometry_waiters.push(resp),
4969 None => {
4970 let _ = resp.send(realized_geometry(state, display_id));
4971 }
4972 }
4973 }
4974 }
4975 }
4976
4977 state.command_rx = Some(command_rx);
4978 event_loop
4979 .handle()
4980 .insert_source(wake_rx, |_, _, state| {
4981 drain_thread_commands(state);
4982 if state.frame_idle_long
4987 && state
4988 .last_input_at
4989 .is_some_and(|t| t.elapsed() < Duration::from_millis(50))
4990 && state
4991 .last_idle_service_at
4992 .is_none_or(|t| t.elapsed() >= Duration::from_millis(16))
4993 {
4994 state.last_idle_service_at = Some(Instant::now());
4995 send_idle_frame_callbacks(state);
4996 }
4997 })
4998 .unwrap();
4999
5000 let source = match ListeningSocketSource::new_auto() {
5001 Ok(s) => s,
5002 Err(e) => {
5003 eprintln!("[Wayland] compositor thread aborting: could not bind a wayland-N socket (XDG_RUNTIME_DIR unset/full?): {e}");
5004 return;
5005 }
5006 };
5007 let socket_name = source.socket_name().to_string_lossy().into_owned();
5008 println!("[Wayland] Socket listening on: {:?}", socket_name);
5009 unsafe { std::env::set_var("WAYLAND_DISPLAY", &socket_name) };
5015 publish_socket_name(&socket_name);
5016
5017 event_loop
5018 .handle()
5019 .insert_source(source, |client_stream, _, state| {
5020 if let Err(err) = state
5021 .dh
5022 .insert_client(client_stream, Arc::new(ClientState::default()))
5023 {
5024 eprintln!("Error adding wayland client: {:?}", err);
5025 }
5026 })
5027 .expect("Failed to init wayland socket source");
5028
5029 let timer = Timer::immediate();
5030 event_loop
5031 .handle()
5032 .insert_source(timer, move |_, _, state| {
5033 reap_dead_host(state);
5036 drain_thread_commands(state);
5037 reconcile_host_layouts(state);
5038 let mut i = 0;
5043 while i < state.deliver_reaper.len() {
5044 if state.deliver_reaper[i].is_finished() {
5045 let _ = state.deliver_reaper.swap_remove(i).join();
5046 } else {
5047 i += 1;
5048 }
5049 }
5050 let mut i = 0;
5051 while i < state.encode_reaper.len() {
5052 if state.encode_reaper[i].is_finished() {
5053 let _ = state.encode_reaper.swap_remove(i).join();
5054 } else {
5055 i += 1;
5056 }
5057 }
5058 let loop_start_time = Instant::now();
5059 state.space.refresh();
5060
5061 let now = Instant::now();
5062 let elapsed = now.duration_since(state.last_log_time).as_secs_f64();
5063 if elapsed >= 1.0 {
5064 let mut mem: Option<(usize, u64)> = None;
5067 for node in &state.output_nodes {
5068 let Some(cap) = node.capture.as_ref() else { continue };
5069 let frames = cap.encode_stats.frames.swap(0, Ordering::Relaxed);
5070 let stripes = cap.encode_stats.stripes.swap(0, Ordering::Relaxed);
5071 if cap.settings.debug_logging {
5072 let actual_fps = frames as f64 / elapsed;
5073 let stripes_per_sec = stripes as f64 / elapsed;
5074 let mode_str = cap.encode_stats.desc.lock().unwrap().clone();
5075 let n_stripes = cap.encode_stats.n_stripes.load(Ordering::Relaxed);
5076 let (current_rss, shm_usage) = *mem
5077 .get_or_insert_with(|| (get_process_rss_bytes(), get_shm_usage_bytes()));
5078
5079 println!("Display: {} Res: {}x{} Mode: {} Stripes: {} EncFPS: {:.2} EncStripes/s: {:.2} Mem: {}MB SHM: {}MB",
5080 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);
5081 }
5082 }
5083 state.last_log_time = now;
5084 }
5085
5086 let any_capturing = state.output_nodes.iter().any(|n| n.capture.is_some());
5087 let any_copy_frame = state.copy_sessions.iter().any(|cs| cs.pending.is_some());
5088 if !any_capturing && state.pending_screenshot.is_none() && !any_copy_frame {
5089 state.last_idle_service_at = Some(Instant::now());
5100 send_idle_frame_callbacks(state);
5101 let post_input = state
5109 .last_input_at
5110 .is_some_and(|t| t.elapsed() < Duration::from_secs(1));
5111 let idle = if !state.copy_sessions.is_empty() || post_input {
5112 Duration::from_millis(16)
5113 } else {
5114 IDLE_FRAME_INTERVAL
5115 };
5116 state.frame_idle_long = idle == IDLE_FRAME_INTERVAL;
5117 return TimeoutAction::ToDuration(idle);
5118 }
5119 state.frame_idle_long = false;
5120
5121 let mut nodes = std::mem::take(&mut state.output_nodes);
5125 let mut any_pool_busy = false;
5126 for node in nodes.iter_mut() {
5127 if render_node_tick(state, node) {
5128 any_pool_busy = true;
5129 }
5130 }
5131 state.output_nodes = nodes;
5132
5133 if any_pool_busy {
5134 return TimeoutAction::ToDuration(Duration::from_millis(1));
5135 }
5136 let work_elapsed = loop_start_time.elapsed();
5137 let max_fps = state
5138 .output_nodes
5139 .iter()
5140 .filter_map(|n| n.capture.as_ref().map(|c| c.settings.target_fps))
5141 .fold(0.0f64, f64::max);
5142 let raw_fps = if max_fps > 0.0 { max_fps } else { state.settings.target_fps };
5143 let fps = if raw_fps.is_finite() && raw_fps > 0.0 { raw_fps.min(MAX_FPS) } else { DEFAULT_FPS };
5146 let target_frame_duration = Duration::from_secs_f64(1.0 / fps);
5147 let wait_duration = target_frame_duration.saturating_sub(work_elapsed);
5148 let final_wait = if wait_duration.as_millis() < 1 { Duration::from_millis(1) } else { wait_duration };
5149 TimeoutAction::ToDuration(final_wait)
5150 })
5151 .expect("Failed to init capture timer");
5152
5153 event_loop
5154 .handle()
5155 .insert_source(Generic::new(display, Interest::READ, Mode::Level), |_, display, _state| {
5156 if let Err(e) = unsafe { display.get_mut().dispatch_clients(_state) } {
5159 eprintln!("[Wayland] client dispatch error: {e:?}");
5160 }
5161 Ok(PostAction::Continue)
5162 })
5163 .unwrap();
5164
5165 crate::computer_use::register_wayland_backend(command_tx.clone());
5166 crate::computer_use::spawn_cu_from_env();
5167
5168 let _ = event_loop.run(None, &mut state, |state| {
5169 state.process_pending_clipboard_read();
5170 state.flush_pending_cursor();
5171 let _ = state.dh.flush_clients();
5172 });
5173}
5174
5175#[pyclass]
5180struct StripeFrame {
5181 data: Arc<Vec<u8>>,
5182 #[pyo3(get, set)]
5183 data_type: i32,
5184 #[pyo3(get, set)]
5185 stripe_y_start: i32,
5186 #[pyo3(get, set)]
5187 stripe_height: i32,
5188 #[pyo3(get, set)]
5189 frame_id: i32,
5190}
5191
5192impl StripeFrame {
5193 fn new_owned_meta(data: Arc<Vec<u8>>, data_type: i32, stripe_y_start: i32, stripe_height: i32, frame_id: i32) -> Self {
5197 Self { data, data_type, stripe_y_start, stripe_height, frame_id }
5198 }
5199}
5200
5201#[pymethods]
5202impl StripeFrame {
5203 #[new]
5206 #[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
5207 fn new(data: Vec<u8>, data_type: i32, stripe_y_start: i32, stripe_height: i32, frame_id: i32) -> Self {
5208 Self { data: Arc::new(data), data_type, stripe_y_start, stripe_height, frame_id }
5209 }
5210
5211 fn __len__(&self) -> usize {
5212 self.data.len()
5213 }
5214
5215 unsafe fn __getbuffer__(
5220 slf: PyRef<'_, Self>,
5221 view: *mut pyo3::ffi::Py_buffer,
5222 flags: std::os::raw::c_int,
5223 ) -> PyResult<()> {
5224 let r = unsafe {
5225 pyo3::ffi::PyBuffer_FillInfo(
5226 view,
5227 slf.as_ptr(),
5228 slf.data.as_ptr() as *mut std::os::raw::c_void,
5229 slf.data.len() as pyo3::ffi::Py_ssize_t,
5230 1,
5231 flags,
5232 )
5233 };
5234 if r != 0 {
5235 return Err(PyErr::fetch(slf.py()));
5236 }
5237 Ok(())
5238 }
5239
5240 unsafe fn __releasebuffer__(&self, _view: *mut pyo3::ffi::Py_buffer) {}
5241}
5242
5243#[pyclass]
5249struct WaylandBackend {
5250 tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
5251 wake_tx: smithay::reexports::calloop::channel::Sender<()>,
5255}
5256
5257impl WaylandBackend {
5258 fn send(&self, cmd: ThreadCommand) -> Result<(), String> {
5259 self.tx.send(cmd).map_err(|e| e.to_string())?;
5260 let _ = self.wake_tx.send(());
5261 Ok(())
5262 }
5263}
5264
5265#[pymethods]
5266impl WaylandBackend {
5267 #[new]
5272 #[pyo3(signature = (width, height, dri_node, auto_gpu_selected = false, cursor_size = -1))]
5273 fn new(
5274 width: i32,
5275 height: i32,
5276 dri_node: String,
5277 auto_gpu_selected: bool,
5278 cursor_size: i32,
5279 ) -> Self {
5280 let (tx, rx) = smithay::reexports::calloop::channel::channel();
5281 let (wake_tx, wake_rx) = smithay::reexports::calloop::channel::channel();
5282 let cu_tx = tx.clone();
5283 thread::spawn(move || {
5284 crate::boost_thread_priority(-15);
5285 run_wayland_thread(WaylandThreadConfig {
5286 command_rx: rx,
5287 wake_rx,
5288 command_tx: cu_tx,
5289 initial_width: width,
5290 initial_height: height,
5291 explicit_dri_node: dri_node,
5292 auto_gpu_selected,
5293 cursor_size,
5294 });
5295 });
5296 WaylandBackend { tx, wake_tx }
5297 }
5298
5299 fn start_capture(&self, callback: Py<PyAny>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
5306 let rust_settings = extract_settings(settings)?;
5307 let display_id = read_display_id(settings);
5308
5309 PY_SHUTDOWN.store(false, Ordering::Relaxed);
5310 self.send(ThreadCommand::StartCapture { display_id, callback: Some(callback), settings: rust_settings })
5311 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to send start command: {}", e)))?;
5312 Ok(())
5313 }
5314
5315 #[pyo3(signature = (display_id = 0))]
5317 fn stop_capture(&self, display_id: u32) -> PyResult<()> {
5318 self.send(ThreadCommand::StopCapture { display_id })
5319 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to send stop command: {}", e)))?;
5320 Ok(())
5321 }
5322
5323 #[allow(clippy::too_many_arguments)]
5331 #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
5332 fn create_output(
5333 &self,
5334 py: Python<'_>,
5335 id: u32,
5336 width: i32,
5337 height: i32,
5338 x: i32,
5339 y: i32,
5340 scale: f64,
5341 ) -> PyResult<bool> {
5342 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5343 self.send(ThreadCommand::CreateOutput { id, width, height, x, y, scale, reply: reply_tx })
5344 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to create output: {}", e)))?;
5345 Ok(py
5346 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5347 .unwrap_or(false))
5348 }
5349
5350 fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
5353 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5354 self.send(ThreadCommand::DestroyOutput { id, reply: reply_tx })
5355 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to destroy output: {}", e)))?;
5356 Ok(py
5357 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5358 .unwrap_or(false))
5359 }
5360
5361 fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
5367 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5368 self.send(ThreadCommand::RepositionOutput { id, x, y, reply: reply_tx })
5369 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to reposition output: {}", e)))?;
5370 Ok(py
5371 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5372 .unwrap_or(false))
5373 }
5374
5375 fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
5378 let (reply_tx, reply_rx) = std::sync::mpsc::channel();
5379 self.send(ThreadCommand::ListOutputs { reply: reply_tx })
5380 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to list outputs: {}", e)))?;
5381 Ok(py
5382 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5383 .unwrap_or_default())
5384 }
5385
5386 fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
5391 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<i64>();
5392 self.send(ThreadCommand::OutputCapacity { reply: reply_tx })
5393 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to query output capacity: {}", e)))?;
5394 Ok(py
5395 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5396 .unwrap_or(0))
5397 }
5398
5399 fn move_window_to_output(&self, py: Python<'_>, window_id: u32, output_id: u32) -> PyResult<bool> {
5402 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5403 self.send(ThreadCommand::MoveWindowToOutput { window_id, output_id, reply: reply_tx })
5404 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to move window: {}", e)))?;
5405 Ok(py
5406 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5407 .unwrap_or(false))
5408 }
5409
5410 fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
5414 let (reply_tx, reply_rx) = std::sync::mpsc::channel();
5415 self.send(ThreadCommand::ListWindows { reply: reply_tx })
5416 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to list windows: {}", e)))?;
5417 Ok(py
5418 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5419 .unwrap_or_default())
5420 }
5421
5422 fn set_cursor_callback(&self, callback: Py<PyAny>) -> PyResult<()> {
5423 self.send(ThreadCommand::SetCursorCallback(callback))
5424 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set cursor callback: {}", e)))?;
5425 Ok(())
5426 }
5427
5428 fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
5432 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5433 self.send(ThreadCommand::SetCursorSize { size, reply: reply_tx })
5434 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set cursor size: {}", e)))?;
5435 Ok(py
5436 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5437 .unwrap_or(false))
5438 }
5439
5440 fn set_clipboard_callback(&self, callback: Py<PyAny>) -> PyResult<()> {
5442 self.send(ThreadCommand::SetClipboardCallback(callback))
5443 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set clipboard callback: {}", e)))?;
5444 Ok(())
5445 }
5446
5447 fn set_clipboard(&self, mime: String, data: Vec<u8>) -> PyResult<()> {
5449 self.send(ThreadCommand::SetClipboard { mime, data })
5450 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set clipboard: {}", e)))?;
5451 Ok(())
5452 }
5453
5454 fn inject_key(&self, scancode: u32, state: u32) -> PyResult<()> {
5455 self.send(ThreadCommand::KeyboardKey { scancode, state })
5456 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject key: {}", e)))?;
5457 Ok(())
5458 }
5459
5460 fn inject_keys(&self, events: Vec<(u32, u32)>) -> PyResult<()> {
5463 if events.is_empty() {
5464 return Ok(());
5465 }
5466 self.send(ThreadCommand::KeyboardKeys { events })
5467 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject keys: {}", e)))?;
5468 Ok(())
5469 }
5470
5471 fn set_keymap_overlay(&self, binds: Vec<(u32, u32)>) -> PyResult<()> {
5481 self.send(ThreadCommand::SetKeymapOverlay { binds })
5482 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set overlay: {}", e)))?;
5483 Ok(())
5484 }
5485
5486 fn set_keymap_string(&self, text: String) -> PyResult<()> {
5487 self.send(ThreadCommand::SetKeymapString(text))
5488 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set keymap: {}", e)))?;
5489 Ok(())
5490 }
5491
5492 fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
5499 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<String>();
5500 self.send(ThreadCommand::GetXkbKeymap { reply: reply_tx })
5501 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to request keymap: {}", e)))?;
5502 let result = py.detach(move || reply_rx.recv_timeout(Duration::from_secs(2)));
5503 match result {
5504 Ok(s) => Ok(s),
5505 Err(_) => Ok(String::new()),
5506 }
5507 }
5508
5509 fn inject_mouse_move(&self, x: f64, y: f64) -> PyResult<()> {
5510 self.send(ThreadCommand::PointerMotion { x, y })
5511 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject motion: {}", e)))?;
5512 Ok(())
5513 }
5514
5515 fn inject_relative_mouse_move(&self, dx: f64, dy: f64) -> PyResult<()> {
5516 self.send(ThreadCommand::PointerRelativeMotion { dx, dy })
5517 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject relative motion: {}", e)))?;
5518 Ok(())
5519 }
5520
5521 fn inject_mouse_button(&self, btn: u32, state: u32) -> PyResult<()> {
5522 self.send(ThreadCommand::PointerButton { btn, state })
5523 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject button: {}", e)))?;
5524 Ok(())
5525 }
5526
5527 fn inject_mouse_scroll(&self, x: f64, y: f64) -> PyResult<()> {
5528 self.send(ThreadCommand::PointerAxis { x, y })
5529 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to inject axis: {}", e)))?;
5530 Ok(())
5531 }
5532
5533 fn set_cursor_rendering(&self, enabled: bool) -> PyResult<()> {
5534 self.send(ThreadCommand::UpdateCursorConfig { render_on_framebuffer: enabled })
5535 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set cursor config: {}", e)))?;
5536 Ok(())
5537 }
5538
5539 #[pyo3(signature = (display_id = 0))]
5544 fn request_idr_frame(&self, display_id: u32) -> PyResult<()> {
5545 self.send(ThreadCommand::RequestIdr { display_id })
5546 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to request IDR: {}", e)))?;
5547 Ok(())
5548 }
5549
5550 #[pyo3(signature = (bitrate_kbps = None, vbv_multiplier = None, fps = None, display_id = 0))]
5553 fn update_rate(&self, bitrate_kbps: Option<i32>, vbv_multiplier: Option<f64>, fps: Option<f64>, display_id: u32) -> PyResult<()> {
5554 self.send(ThreadCommand::UpdateRate { display_id, bitrate_kbps, vbv_multiplier, fps })
5555 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to update rate: {}", e)))?;
5556 Ok(())
5557 }
5558
5559 #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
5563 fn set_xkb_layout(
5564 &self,
5565 py: Python<'_>,
5566 layout: String,
5567 variant: String,
5568 options: String,
5569 model: String,
5570 rules: String,
5571 ) -> PyResult<bool> {
5572 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<bool>();
5573 self.send(ThreadCommand::SetXkbLayout { rules, model, layout, variant, options, reply: reply_tx })
5574 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to set layout: {}", e)))?;
5575 Ok(py
5576 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5577 .unwrap_or(false))
5578 }
5579
5580 fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
5583 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(Vec<u32>, u32)>();
5584 self.send(ThreadCommand::GetKeyboardState { reply: reply_tx })
5585 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to read keyboard state: {}", e)))?;
5586 Ok(py
5587 .detach(move || reply_rx.recv_timeout(Duration::from_secs(2)))
5588 .unwrap_or_default())
5589 }
5590
5591 #[pyo3(signature = (display_id = 0))]
5602 fn get_realized_geometry(
5603 &self,
5604 py: Python<'_>,
5605 display_id: u32,
5606 ) -> PyResult<Option<(i32, i32, f64)>> {
5607 let (reply_tx, reply_rx) = std::sync::mpsc::channel::<(i32, i32, f64)>();
5608 self.send(ThreadCommand::CuGetInfo { display_id, resp: reply_tx })
5609 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to read geometry: {}", e)))?;
5610 Ok(py
5611 .detach(move || reply_rx.recv_timeout(GEOMETRY_BARRIER_TIMEOUT))
5612 .ok())
5613 }
5614
5615 #[pyo3(signature = (display_id = 0))]
5622 fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
5623 wayland_capture_state(display_id)
5624 }
5625}
5626
5627const GEOMETRY_BARRIER_TIMEOUT: Duration = Duration::from_secs(6);
5632
5633fn wayland_capture_state(display_id: u32) -> (String, Option<String>) {
5636 let running = wayland_alive().lock().unwrap().contains(&display_id);
5637 let last_error = wayland_capture_err().lock().unwrap().get(&display_id).cloned();
5638 let state = if running {
5639 "running"
5640 } else if last_error.is_some() {
5641 "failed"
5642 } else {
5643 "idle"
5644 };
5645 (state.to_string(), last_error)
5646}
5647
5648impl WaylandBackend {
5649 fn update_tunables(&self, display_id: u32, t: LiveTunables) -> PyResult<()> {
5650 self.send(ThreadCommand::UpdateTunables { display_id, tunables: t })
5651 .map_err(|e| PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!("Failed to update tunables: {}", e)))?;
5652 Ok(())
5653 }
5654}
5655
5656fn read_display_id(settings: &Bound<'_, PyAny>) -> u32 {
5659 settings
5660 .getattr("display_id")
5661 .ok()
5662 .and_then(|v| v.extract::<u32>().ok())
5663 .unwrap_or(0)
5664}
5665
5666use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering};
5667use std::sync::{Condvar, Mutex, OnceLock};
5668
5669use crate::encoders::software::EncodedStripe;
5670
5671#[pyfunction]
5677#[pyo3(signature = (data, data_type = 0, stripe_y_start = 0, stripe_height = 0, frame_id = 0))]
5678fn stripe_frame_from_buffer(
5679 data: Vec<u8>,
5680 data_type: i32,
5681 stripe_y_start: i32,
5682 stripe_height: i32,
5683 frame_id: i32,
5684) -> StripeFrame {
5685 StripeFrame::new_owned_meta(Arc::new(data), data_type, stripe_y_start, stripe_height, frame_id)
5686}
5687
5688#[pyclass(dict)]
5692struct CaptureSettings {
5693 #[pyo3(get, set)] display_id: u32,
5695 #[pyo3(get, set)] capture_width: i32,
5696 #[pyo3(get, set)] capture_height: i32,
5697 #[pyo3(get, set)] scale: f64,
5698 #[pyo3(get, set)] capture_x: i32,
5699 #[pyo3(get, set)] capture_y: i32,
5700 #[pyo3(get, set)] target_fps: f64,
5701 #[pyo3(get, set)] jpeg_quality: i32,
5702 #[pyo3(get, set)] paint_over_jpeg_quality: i32,
5703 #[pyo3(get, set)] use_paint_over_quality: bool,
5704 #[pyo3(get, set)] paint_over_trigger_frames: i32,
5705 #[pyo3(get, set)] damage_block_threshold: i32,
5706 #[pyo3(get, set)] damage_block_duration: i32,
5707 #[pyo3(get, set)] output_mode: i32,
5708 #[pyo3(get, set)] video_crf: i32,
5709 #[pyo3(get, set)] video_paintover_crf: i32,
5710 #[pyo3(get, set)] video_paintover_burst_frames: i32,
5711 #[pyo3(get, set)] video_fullcolor: bool,
5712 #[pyo3(get, set)] video_fullframe: bool,
5713 #[pyo3(get, set)] video_streaming_mode: bool,
5714 #[pyo3(get, set)] capture_cursor: bool,
5715 #[pyo3(get, set)] watermark_path: Py<PyAny>,
5716 #[pyo3(get, set)] watermark_location_enum: i32,
5717 #[pyo3(get, set)] encode_node_index: i32,
5718 #[pyo3(get, set)] use_cpu: bool,
5719 #[pyo3(get, set)] debug_logging: bool,
5720 #[pyo3(get, set)] video_cbr_mode: bool,
5721 #[pyo3(get, set)] video_bitrate_kbps: i32,
5722 #[pyo3(get, set)] video_vbv_multiplier: f64,
5723 #[pyo3(get, set)] keyframe_interval_s: f64,
5724 #[pyo3(get, set)] video_min_qp: i32,
5725 #[pyo3(get, set)] video_max_qp: i32,
5726 #[pyo3(get, set)] auto_adjust_screen_capture_size: bool,
5727 #[pyo3(get, set)] omit_stripe_headers: bool,
5728 #[pyo3(get, set)] encode_node_path: Py<PyAny>,
5729 #[pyo3(get, set)] render_node_path: Py<PyAny>,
5732 #[pyo3(get, set)] auto_gpu: Py<PyAny>,
5735 #[pyo3(get, set)] use_wayland: Py<PyAny>,
5737 #[pyo3(get, set)] recording_socket: Py<PyAny>,
5739 #[pyo3(get, set)] wayland_host_display: Py<PyAny>,
5741 #[pyo3(get, set)] cursor_size: i32,
5743 #[pyo3(get, set)] cursor_size_cap: i32,
5746}
5747
5748#[pymethods]
5749impl CaptureSettings {
5750 #[new]
5751 fn new(py: Python<'_>) -> Self {
5752 Self {
5753 display_id: 0,
5754 capture_width: 1920, capture_height: 1080, scale: 1.0, capture_x: 0, capture_y: 0,
5755 target_fps: 60.0, jpeg_quality: 85, paint_over_jpeg_quality: 95,
5756 use_paint_over_quality: false, paint_over_trigger_frames: 10,
5757 damage_block_threshold: 15, damage_block_duration: 30, output_mode: 0,
5758 video_crf: 25, video_paintover_crf: 18, video_paintover_burst_frames: 5,
5759 video_fullcolor: false, video_fullframe: false, video_streaming_mode: false,
5760 capture_cursor: false, watermark_path: py.None(), watermark_location_enum: 0,
5761 encode_node_index: -2, use_cpu: false, debug_logging: false,
5762 video_cbr_mode: false, video_bitrate_kbps: 4000, video_vbv_multiplier: 0.0,
5763 keyframe_interval_s: 0.0,
5764 video_min_qp: 0, video_max_qp: 0,
5765 auto_adjust_screen_capture_size: false, omit_stripe_headers: false,
5766 encode_node_path: py.None(),
5767 render_node_path: py.None(), auto_gpu: py.None(), use_wayland: py.None(),
5768 recording_socket: py.None(), wayland_host_display: py.None(),
5769 cursor_size: -1, cursor_size_cap: 32,
5770 }
5771 }
5772}
5773
5774static WAYLAND_BACKEND: OnceLock<Mutex<Option<Py<WaylandBackend>>>> = OnceLock::new();
5776static WAYLAND_SOCKET_NAME: Mutex<Option<String>> = Mutex::new(None);
5781static WAYLAND_SOCKET_CV: Condvar = Condvar::new();
5782
5783fn publish_socket_name(name: &str) {
5784 *WAYLAND_SOCKET_NAME.lock().unwrap() = Some(name.to_string());
5785 WAYLAND_SOCKET_CV.notify_all();
5786}
5787
5788fn wait_socket_name(timeout: Duration) -> Option<String> {
5790 let deadline = Instant::now() + timeout;
5791 let mut g = WAYLAND_SOCKET_NAME.lock().unwrap();
5792 loop {
5793 if let Some(name) = g.as_ref() {
5794 return Some(name.clone());
5795 }
5796 let now = Instant::now();
5797 if now >= deadline {
5798 return None;
5799 }
5800 let (gg, _) = WAYLAND_SOCKET_CV.wait_timeout(g, deadline - now).unwrap();
5801 g = gg;
5802 }
5803}
5804static PENDING_CURSOR_CALLBACK: Mutex<Option<Py<PyAny>>> = Mutex::new(None);
5808pub(crate) static PY_SHUTDOWN: AtomicBool = AtomicBool::new(false);
5812static WAYLAND_OWNERS: OnceLock<Mutex<std::collections::HashMap<u32, u64>>> = OnceLock::new();
5816
5817fn wayland_owners() -> &'static Mutex<std::collections::HashMap<u32, u64>> {
5818 WAYLAND_OWNERS.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
5819}
5820
5821static WAYLAND_ALIVE_DISPLAYS: OnceLock<Mutex<std::collections::HashSet<u32>>> = OnceLock::new();
5825
5826fn wayland_alive() -> &'static Mutex<std::collections::HashSet<u32>> {
5827 WAYLAND_ALIVE_DISPLAYS.get_or_init(|| Mutex::new(std::collections::HashSet::new()))
5828}
5829static WAYLAND_CAPTURE_ERR: OnceLock<Mutex<std::collections::HashMap<u32, String>>> =
5835 OnceLock::new();
5836
5837fn wayland_capture_err() -> &'static Mutex<std::collections::HashMap<u32, String>> {
5838 WAYLAND_CAPTURE_ERR.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
5839}
5840
5841fn set_wayland_capture_err(display_id: u32, err: Option<String>) {
5843 let mut map = wayland_capture_err().lock().unwrap();
5844 match err {
5845 Some(e) => {
5846 map.insert(display_id, e);
5847 }
5848 None => {
5849 map.remove(&display_id);
5850 }
5851 }
5852}
5853static NEXT_CAPTURE_ID: AtomicU64 = AtomicU64::new(1);
5857static LIVE_X11: OnceLock<Mutex<Vec<Arc<crate::x11::Controls>>>> = OnceLock::new();
5861
5862fn live_x11() -> &'static Mutex<Vec<Arc<crate::x11::Controls>>> {
5863 LIVE_X11.get_or_init(|| Mutex::new(Vec::new()))
5864}
5865
5866pub(crate) fn unpremultiply_rgba(image: &mut image::RgbaImage) {
5873 for p in image.pixels_mut() {
5874 let a = p.0[3] as u32;
5875 if a == 0 {
5876 p.0 = [0, 0, 0, 0];
5877 } else if a < 255 {
5878 for c in &mut p.0[..3] {
5879 *c = ((*c as u32 * 255 + a / 2) / a).min(255) as u8;
5880 }
5881 }
5882 }
5883}
5884
5885pub(crate) fn boost_thread_priority(nice: libc::c_int) {
5889 unsafe {
5890 let tid = libc::syscall(libc::SYS_gettid) as libc::id_t;
5891 let _ = libc::setpriority(libc::PRIO_PROCESS, tid, nice);
5892 }
5893}
5894
5895fn wayland_update_rate(
5897 py: Python<'_>,
5898 display_id: u32,
5899 bitrate_kbps: Option<i32>,
5900 vbv_multiplier: Option<f64>,
5901 fps: Option<f64>,
5902) {
5903 if let Some(slot) = WAYLAND_BACKEND.get()
5904 && let Some(be) = slot.lock().unwrap().as_ref() {
5905 let _ = be.bind(py).borrow().update_rate(bitrate_kbps, vbv_multiplier, fps, display_id);
5906 }
5907}
5908
5909fn wayland_update_tunables(py: Python<'_>, display_id: u32, t: LiveTunables) {
5911 if let Some(slot) = WAYLAND_BACKEND.get()
5912 && let Some(be) = slot.lock().unwrap().as_ref() {
5913 let _ = be.bind(py).borrow().update_tunables(display_id, t);
5914 }
5915}
5916
5917fn ensure_wayland_backend(
5926 py: Python<'_>,
5927 width: i32,
5928 height: i32,
5929 explicit_node: String,
5930 auto_gpu: String,
5931 fallback_node: String,
5932 cursor_size: i32,
5933) -> PyResult<Py<WaylandBackend>> {
5934 let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
5935 let mut g = slot.lock().unwrap();
5936 if g.is_none() {
5937 let mut node = (!explicit_node.is_empty()).then_some(explicit_node);
5938 let mut auto_gpu_selected = false;
5939 if node.is_none()
5940 && let Some(request) = parse_auto_gpu(&auto_gpu) {
5941 match auto_select_render_node(request.as_deref()) {
5942 Some(picked) => {
5943 println!("[Wayland] AUTO_GPU enabled. Selected: {}", picked);
5944 node = Some(picked);
5945 auto_gpu_selected = true;
5946 }
5947 None => {
5948 if let Some(token) = request {
5949 eprintln!("[pixelflux] AUTO_GPU={token}: no matching GPU found.");
5950 }
5951 }
5952 }
5953 }
5954 let node = node.unwrap_or(fallback_node);
5955 let be = Py::new(
5956 py,
5957 WaylandBackend::new(width, height, node, auto_gpu_selected, cursor_size),
5958 )?;
5959 if let Some(cb) = PENDING_CURSOR_CALLBACK.lock().unwrap().take() {
5960 let _ = be.bind(py).borrow().set_cursor_callback(cb);
5961 }
5962 *g = Some(be);
5963 }
5964 Ok(g.as_ref().unwrap().clone_ref(py))
5965}
5966
5967fn wayland_backend_running(py: Python<'_>) -> Option<Py<WaylandBackend>> {
5971 let slot = WAYLAND_BACKEND.get()?;
5972 let g = slot.lock().unwrap();
5973 g.as_ref().map(|b| b.clone_ref(py))
5974}
5975
5976fn want_wayland(settings: &Bound<'_, PyAny>) -> bool {
5980 if let Some(explicit) = settings
5981 .getattr("use_wayland")
5982 .ok()
5983 .and_then(|v| v.extract::<bool>().ok())
5984 {
5985 return explicit;
5986 }
5987 std::env::var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false)
5988}
5989
5990struct ScState {
5994 backend: u8,
5996 cursor_ref: bool,
6004 controls: Option<Arc<crate::x11::Controls>>,
6005 handle: Option<thread::JoinHandle<()>>,
6006 cap_thread_id: Option<thread::ThreadId>,
6007 encode_thread_id: Option<thread::ThreadId>,
6010 encode_tid_rx: Option<std::sync::mpsc::Receiver<thread::ThreadId>>,
6018 deliver_handle: Option<thread::JoinHandle<()>>,
6023 deliver_thread_id: Option<thread::ThreadId>,
6024 wl_display: u32,
6026 err: Option<Arc<Mutex<Option<String>>>>,
6030}
6031
6032#[pyclass]
6036struct ScreenCapture {
6037 id: u64,
6038 inner: Mutex<ScState>,
6039}
6040
6041impl ScreenCapture {
6042 fn stop_internal(&self, py: Python<'_>) -> PyResult<()> {
6052 let (handle, deliver_handle, same_thread, backend, controls, wl_display, cursor_ref) = {
6053 let mut st = self.inner.lock().unwrap();
6054 if let Some(c) = &st.controls {
6055 c.stop.store(true, Ordering::Relaxed);
6056 }
6057 let cur = Some(thread::current().id());
6058 if st.encode_thread_id.is_none()
6059 && let Some(rx) = st.encode_tid_rx.as_ref()
6060 && let Ok(id) = rx.try_recv() {
6061 st.encode_thread_id = Some(id);
6062 }
6063 let same = st.cap_thread_id == cur
6064 || st.encode_thread_id == cur
6065 || st.deliver_thread_id == cur;
6066 let controls = st.controls.take();
6067 let handle = st.handle.take();
6068 let deliver_handle = st.deliver_handle.take();
6069 let backend = st.backend;
6070 let cursor_ref = std::mem::take(&mut st.cursor_ref);
6071 let wl_display = st.wl_display;
6072 st.backend = 0;
6073 st.cap_thread_id = None;
6074 st.encode_thread_id = None;
6075 st.encode_tid_rx = None;
6076 st.deliver_thread_id = None;
6077 st.wl_display = 0;
6078 st.err = None;
6079 (handle, deliver_handle, same, backend, controls, wl_display, cursor_ref)
6080 };
6081 if let Some(c) = &controls {
6082 live_x11().lock().unwrap().retain(|x| !Arc::ptr_eq(x, c));
6083 }
6084 if cursor_ref {
6085 crate::x11::cursor::release(py);
6086 }
6087 if backend == 2 {
6088 let did = wl_display;
6089 let owned = {
6090 let mut owners = wayland_owners().lock().unwrap();
6091 if owners.get(&did) == Some(&self.id) {
6092 owners.remove(&did);
6093 true
6094 } else {
6095 false
6096 }
6097 };
6098 if owned
6099 && let Some(slot) = WAYLAND_BACKEND.get()
6100 && let Some(be) = slot.lock().unwrap().as_ref() {
6101 let _ = be.bind(py).borrow().stop_capture(did);
6102 }
6103 } else {
6104 if same_thread {
6105 drop(handle);
6107 drop(deliver_handle);
6108 } else {
6109 py.detach(|| {
6110 if let Some(h) = handle {
6111 let _ = h.join();
6112 }
6113 if let Some(h) = deliver_handle {
6117 let _ = h.join();
6118 }
6119 });
6120 }
6121 }
6122 Ok(())
6123 }
6124}
6125
6126#[pymethods]
6127impl ScreenCapture {
6128 #[new]
6129 fn new() -> Self {
6130 Self {
6131 id: NEXT_CAPTURE_ID.fetch_add(1, Ordering::Relaxed),
6132 inner: Mutex::new(ScState {
6133 backend: 0,
6134 cursor_ref: false,
6135 controls: None,
6136 handle: None,
6137 cap_thread_id: None,
6138 encode_thread_id: None,
6139 encode_tid_rx: None,
6140 deliver_handle: None,
6141 deliver_thread_id: None,
6142 wl_display: 0,
6143 err: None,
6144 }),
6145 }
6146 }
6147
6148 fn start_capture(
6160 &self,
6161 py: Python<'_>,
6162 callback: Py<PyAny>,
6163 settings: &Bound<'_, PyAny>,
6164 ) -> PyResult<()> {
6165 let display_id = read_display_id(settings);
6166 let live_wayland_restart = want_wayland(settings)
6167 && {
6168 let st = self.inner.lock().unwrap();
6169 st.backend == 2 && st.wl_display == display_id
6170 }
6171 && wayland_owners().lock().unwrap().get(&display_id) == Some(&self.id)
6172 && wayland_alive().lock().unwrap().contains(&display_id);
6173 if !live_wayland_restart {
6174 self.stop_internal(py)?;
6175 }
6176 let rs = extract_settings(settings)?;
6177
6178 if want_wayland(settings) {
6179 let read_node = |attr: &str| -> Option<String> {
6180 settings.getattr(attr).ok().and_then(|o| {
6181 o.extract::<String>()
6182 .or_else(|_| {
6183 o.extract::<Vec<u8>>()
6184 .map(|b| String::from_utf8_lossy(&b).into_owned())
6185 })
6186 .ok()
6187 })
6188 };
6189 let cursor_size = settings
6190 .getattr("cursor_size")
6191 .ok()
6192 .and_then(|v| v.extract::<i32>().ok())
6193 .unwrap_or(-1);
6194 let be = ensure_wayland_backend(
6195 py,
6196 rs.width,
6197 rs.height,
6198 read_node("render_node_path").unwrap_or_default(),
6199 read_node("auto_gpu").unwrap_or_default(),
6200 read_node("encode_node_path").unwrap_or_default(),
6201 cursor_size,
6202 )?;
6203 be.bind(py).borrow().start_capture(callback, settings)?;
6204 wayland_owners().lock().unwrap().insert(display_id, self.id);
6205 {
6206 let mut st = self.inner.lock().unwrap();
6207 st.backend = 2;
6208 st.wl_display = display_id;
6209 }
6210 return Ok(());
6211 }
6212
6213 PY_SHUTDOWN.store(false, Ordering::Relaxed);
6217 let mut rs = rs;
6218 if rs.encode_node_index < -1 {
6219 let auto_gpu = settings
6220 .getattr("auto_gpu")
6221 .ok()
6222 .and_then(|o| {
6223 o.extract::<String>()
6224 .or_else(|_| {
6225 o.extract::<Vec<u8>>()
6226 .map(|b| String::from_utf8_lossy(&b).into_owned())
6227 })
6228 .ok()
6229 })
6230 .unwrap_or_default();
6231 if let Some(request) = parse_auto_gpu(&auto_gpu)
6232 && let Some(picked) = auto_select_render_node(request.as_deref())
6233 && let Some(idx) = picked
6234 .strip_prefix("/dev/dri/renderD")
6235 .and_then(|s| s.parse::<i32>().ok())
6236 {
6237 println!("[x11] AUTO_GPU enabled. Selected: {picked}");
6238 rs.encode_node_index = idx - 128;
6239 }
6240 }
6241
6242 println!(
6243 "[x11] Configuring Output: {}x{} @ {:.2} FPS (Encode Node: {})",
6244 rs.width, rs.height, rs.target_fps, rs.encode_node_index
6245 );
6246
6247 let controls = Arc::new(crate::x11::Controls::new(&rs));
6248 let cursor_cap = rs.cursor_size_cap;
6249 live_x11().lock().unwrap().push(controls.clone());
6250 let c2 = controls.clone();
6251 let c3 = controls.clone();
6252 let cb = callback;
6253 let err_slot: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
6254 let err_slot2 = err_slot.clone();
6255
6256 let (deliver_tx, deliver_rx) = std::sync::mpsc::sync_channel::<Vec<EncodedStripe>>(1);
6262 let deliver_handle = thread::spawn(move || {
6263 crate::boost_thread_priority(-10);
6264 while let Ok(frame) = deliver_rx.recv() {
6265 if PY_SHUTDOWN.load(Ordering::Relaxed) {
6266 continue;
6267 }
6268 Python::attach(|py| {
6269 for s in frame {
6270 match Py::new(
6271 py,
6272 StripeFrame::new_owned_meta(
6273 s.data,
6274 s.data_type,
6275 s.stripe_y_start,
6276 s.stripe_height,
6277 s.frame_id,
6278 ),
6279 ) {
6280 Ok(f) => {
6281 if let Err(e) = cb.call1(py, (f,)) {
6282 e.print(py);
6283 }
6284 }
6285 Err(e) => eprintln!("[x11] frame alloc error: {e:?}"),
6286 }
6287 }
6288 });
6289 }
6290 });
6291 let deliver_thread_id = deliver_handle.thread().id();
6292
6293 let on_frame = move |frame: Vec<EncodedStripe>| {
6294 let _ = deliver_tx.send(frame);
6297 };
6298
6299 let (tid_tx, tid_rx) = std::sync::mpsc::channel();
6300 let (etid_tx, etid_rx) = std::sync::mpsc::channel();
6301 let handle = thread::spawn(move || {
6302 crate::boost_thread_priority(-15);
6303 let _ = tid_tx.send(thread::current().id());
6304 let res = crate::x11::run_capture(rs, c2, etid_tx, on_frame);
6305 c3.stop.store(true, Ordering::Release);
6306 if let Err(e) = res {
6307 let msg = e.to_string();
6308 eprintln!("[x11] capture error: {msg}");
6309 if let Ok(mut g) = err_slot2.lock() {
6310 *g = Some(msg);
6311 }
6312 }
6313 c3.finished.store(true, Ordering::Release);
6316 });
6317 let (tid, etid_res, etid_rx) = py.detach(move || {
6318 let tid = tid_rx.recv().ok();
6319 let etid_res = etid_rx.recv_timeout(std::time::Duration::from_secs(2));
6320 (tid, etid_res, etid_rx)
6321 });
6322 let mut late_etid_rx = None;
6323 let etid = match etid_res {
6324 Ok(id) => Some(id),
6325 Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
6326 let _ = handle.join();
6327 live_x11().lock().unwrap().retain(|x| !Arc::ptr_eq(x, &controls));
6328 let msg = err_slot
6329 .lock()
6330 .ok()
6331 .and_then(|g| g.clone())
6332 .unwrap_or_else(|| "X11 capture thread exited during start".to_string());
6333 return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(msg));
6334 }
6335 Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
6336 late_etid_rx = Some(etid_rx);
6340 None
6341 }
6342 };
6343 crate::x11::cursor::acquire(cursor_cap);
6346 let mut st = self.inner.lock().unwrap();
6347 st.backend = 1;
6348 st.cursor_ref = true;
6349 st.controls = Some(controls);
6350 st.handle = Some(handle);
6351 st.cap_thread_id = tid;
6352 st.encode_thread_id = etid;
6353 st.encode_tid_rx = late_etid_rx;
6354 st.deliver_handle = Some(deliver_handle);
6355 st.deliver_thread_id = Some(deliver_thread_id);
6356 st.err = Some(err_slot);
6357 drop(st);
6358 Ok(())
6359 }
6360
6361 fn stop_capture(&self, py: Python<'_>) -> PyResult<()> {
6362 self.stop_internal(py)
6363 }
6364
6365 fn request_idr_frame(&self, py: Python<'_>) -> PyResult<()> {
6366 let (backend, controls, did) = {
6367 let st = self.inner.lock().unwrap();
6368 (st.backend, st.controls.clone(), st.wl_display)
6369 };
6370 match backend {
6371 1 => {
6372 if let Some(c) = controls {
6373 c.force_idr.store(true, Ordering::Relaxed);
6374 }
6375 }
6376 2 => {
6377 if let Some(slot) = WAYLAND_BACKEND.get()
6378 && let Some(be) = slot.lock().unwrap().as_ref() {
6379 let _ = be.bind(py).borrow().request_idr_frame(did);
6380 }
6381 }
6382 _ => {}
6383 }
6384 Ok(())
6385 }
6386
6387 fn update_video_bitrate(&self, py: Python<'_>, kbps: i32) -> PyResult<()> {
6392 let (backend, controls, did) = {
6393 let st = self.inner.lock().unwrap();
6394 (st.backend, st.controls.clone(), st.wl_display)
6395 };
6396 match backend {
6397 1 => {
6398 if let Some(c) = &controls {
6399 c.bitrate_kbps.store(kbps, Ordering::Relaxed);
6400 c.rate_dirty.store(true, Ordering::Release);
6401 }
6402 }
6403 2 => wayland_update_rate(py, did, Some(kbps), None, None),
6404 _ => {}
6405 }
6406 Ok(())
6407 }
6408
6409 fn update_framerate(&self, py: Python<'_>, fps: f64) -> PyResult<()> {
6410 let (backend, controls, did) = {
6411 let st = self.inner.lock().unwrap();
6412 (st.backend, st.controls.clone(), st.wl_display)
6413 };
6414 match backend {
6415 1 => {
6416 if let Some(c) = &controls {
6417 c.fps_milli.store((fps.max(1.0) * 1000.0) as u64, Ordering::Relaxed);
6418 c.rate_dirty.store(true, Ordering::Release);
6419 }
6420 }
6421 2 => wayland_update_rate(py, did, None, None, Some(fps)),
6422 _ => {}
6423 }
6424 Ok(())
6425 }
6426
6427 fn update_vbv_multiplier(&self, py: Python<'_>, multiplier: f64) -> PyResult<()> {
6429 let (backend, controls, did) = {
6430 let st = self.inner.lock().unwrap();
6431 (st.backend, st.controls.clone(), st.wl_display)
6432 };
6433 match backend {
6434 1 => {
6435 if let Some(c) = &controls {
6436 c.vbv_mult_milli
6437 .store((multiplier * 1000.0).round() as i32, Ordering::Relaxed);
6438 c.rate_dirty.store(true, Ordering::Release);
6439 }
6440 }
6441 2 => wayland_update_rate(py, did, None, Some(multiplier), None),
6442 _ => {}
6443 }
6444 Ok(())
6445 }
6446
6447 fn update_tunables(&self, py: Python<'_>, settings: &Bound<'_, PyAny>) -> PyResult<()> {
6451 let rs = extract_settings(settings)?;
6452 let t = LiveTunables::from_settings(&rs);
6453 let (backend, controls, did) = {
6454 let st = self.inner.lock().unwrap();
6455 (st.backend, st.controls.clone(), st.wl_display)
6456 };
6457 match backend {
6458 1 => {
6459 if let Some(c) = &controls {
6460 c.capture_cursor.store(t.capture_cursor, Ordering::Relaxed);
6461 *c.tunables.lock().unwrap() = Some(t);
6462 c.tunables_dirty.store(true, Ordering::Release);
6463 }
6464 crate::x11::cursor::set_size_cap(rs.cursor_size_cap);
6465 }
6466 2 => wayland_update_tunables(py, did, t),
6467 _ => {}
6468 }
6469 Ok(())
6470 }
6471
6472 fn update_capture_region(&self, x: i32, y: i32, width: i32, height: i32) -> PyResult<()> {
6478 let controls = {
6479 let st = self.inner.lock().unwrap();
6480 if st.backend == 2 {
6481 return Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
6482 "update_capture_region is X11-only; on Wayland restart the capture with new dimensions",
6483 ));
6484 }
6485 st.controls.clone()
6486 };
6487 if let Some(c) = &controls {
6488 *c.region.lock().unwrap() = (x.max(0), y.max(0), width, height);
6489 c.region_dirty.store(true, Ordering::Release);
6490 }
6491 Ok(())
6492 }
6493
6494 #[getter]
6495 fn is_capturing(&self) -> bool {
6496 let st = self.inner.lock().unwrap();
6497 match st.backend {
6498 1 => st
6499 .controls
6500 .as_ref()
6501 .map(|c| !c.stop.load(Ordering::Relaxed))
6502 .unwrap_or(false),
6503 2 => wayland_owners().lock().unwrap().get(&st.wl_display) == Some(&self.id)
6504 && wayland_alive().lock().unwrap().contains(&st.wl_display),
6505 _ => false,
6506 }
6507 }
6508
6509 fn inject_key(&self, py: Python<'_>, scancode: u32, state: u32) -> PyResult<()> {
6510 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_key(scancode, state))
6511 }
6512 fn inject_keys(&self, py: Python<'_>, events: Vec<(u32, u32)>) -> PyResult<()> {
6514 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_keys(events))
6515 }
6516 fn set_keymap_string(&self, py: Python<'_>, text: String) -> PyResult<()> {
6517 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_string(text))
6518 }
6519 fn set_keymap_overlay(&self, py: Python<'_>, binds: Vec<(u32, u32)>) -> PyResult<()> {
6523 wayland_backend_running(py)
6524 .map_or(Ok(()), |be| be.bind(py).borrow().set_keymap_overlay(binds))
6525 }
6526 fn set_app_wayland_display(&self, display: String) {
6530 crate::computer_use::set_app_wayland_display(
6531 if display.is_empty() { None } else { Some(display) },
6532 );
6533 }
6534 fn type_text_wayland(&self, py: Python<'_>, display: String, text: String) -> PyResult<()> {
6540 py.detach(move || {
6541 let path = crate::wayland::wlclient::socket_path(&display)
6542 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6543 crate::wayland::vkclient::type_text_to(&path, &text)
6544 })
6545 .map_err(|e: String| {
6546 if e.contains("zwp_virtual_keyboard_manager_v1") {
6547 VirtualKeyboardUnavailable::new_err(e)
6548 } else {
6549 pyo3::exceptions::PyRuntimeError::new_err(e)
6550 }
6551 })
6552 }
6553 fn type_keysyms_wayland(
6557 &self,
6558 py: Python<'_>,
6559 display: String,
6560 keysyms: Vec<u32>,
6561 ) -> PyResult<()> {
6562 py.detach(move || {
6563 let path = crate::wayland::wlclient::socket_path(&display)
6564 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6565 crate::wayland::vkclient::type_keysyms_to(&path, &keysyms)
6566 })
6567 .map_err(|e: String| {
6568 if e.contains("zwp_virtual_keyboard_manager_v1") {
6569 VirtualKeyboardUnavailable::new_err(e)
6570 } else {
6571 pyo3::exceptions::PyRuntimeError::new_err(e)
6572 }
6573 })
6574 }
6575 fn set_app_output_scale(
6580 &self,
6581 py: Python<'_>,
6582 display: String,
6583 index: usize,
6584 scale: f64,
6585 ) -> PyResult<bool> {
6586 py.detach(move || {
6587 let path = crate::wayland::wlclient::socket_path(&display)
6588 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6589 crate::wayland::outclient::set_output_scale(&path, index, scale)
6590 })
6591 .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
6592 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6593 }
6594 fn set_app_screen_geometry(
6600 &self,
6601 py: Python<'_>,
6602 display: String,
6603 index: usize,
6604 width: i32,
6605 height: i32,
6606 scale: f64,
6607 ) -> PyResult<bool> {
6608 py.detach(move || {
6609 let path = crate::wayland::wlclient::socket_path(&display)
6610 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6611 crate::wayland::outclient::set_screen_geometry(&path, index, (width, height), scale)
6612 })
6613 .map(|outcome| matches!(outcome, crate::wayland::outclient::ScaleOutcome::Applied))
6614 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6615 }
6616 fn hold_spare_app_screens(
6621 &self,
6622 py: Python<'_>,
6623 display: String,
6624 keep: usize,
6625 width: i32,
6626 height: i32,
6627 ) -> PyResult<usize> {
6628 py.detach(move || {
6629 let path = crate::wayland::wlclient::socket_path(&display)
6630 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())?;
6631 crate::wayland::outclient::hold_spare_screens(&path, keep, (width, height))
6632 })
6633 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6634 }
6635 fn clipboard_types_app(&self, py: Python<'_>, display: String) -> PyResult<Vec<String>> {
6637 py.detach(move || {
6638 crate::wayland::dcclient::list_types(&app_socket_path(&display)?)
6639 })
6640 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6641 }
6642 fn clipboard_read_app(
6645 &self,
6646 py: Python<'_>,
6647 display: String,
6648 mime: String,
6649 ) -> PyResult<Option<Py<pyo3::types::PyBytes>>> {
6650 let data = py
6651 .detach(move || crate::wayland::dcclient::read(&app_socket_path(&display)?, &mime))
6652 .map_err(pyo3::exceptions::PyRuntimeError::new_err)?;
6653 Ok(data.map(|d| pyo3::types::PyBytes::new(py, &d).unbind()))
6654 }
6655 fn clipboard_write_app(
6658 &self,
6659 py: Python<'_>,
6660 display: String,
6661 entries: Vec<(String, Vec<u8>)>,
6662 ) -> PyResult<()> {
6663 py.detach(move || crate::wayland::dcclient::write(&app_socket_path(&display)?, entries))
6664 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6665 }
6666 fn clipboard_clear_app(&self, py: Python<'_>, display: String) -> PyResult<()> {
6668 py.detach(move || crate::wayland::dcclient::clear(&app_socket_path(&display)?))
6669 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6670 }
6671 fn clipboard_watch_app(
6675 &self,
6676 py: Python<'_>,
6677 display: String,
6678 callback: Py<PyAny>,
6679 ) -> PyResult<()> {
6680 py.detach(move || crate::wayland::dcclient::watch(&app_socket_path(&display)?, callback))
6681 .map_err(pyo3::exceptions::PyRuntimeError::new_err)
6682 }
6683 fn clipboard_unwatch_app(&self, py: Python<'_>, display: String) {
6685 let _ = py.detach(move || {
6686 crate::wayland::dcclient::unwatch(&app_socket_path(&display)?);
6687 Ok::<(), String>(())
6688 });
6689 }
6690 fn inject_mouse_move(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<()> {
6691 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_move(x, y))
6692 }
6693 fn inject_relative_mouse_move(&self, py: Python<'_>, dx: f64, dy: f64) -> PyResult<()> {
6694 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_relative_mouse_move(dx, dy))
6695 }
6696 fn inject_mouse_button(&self, py: Python<'_>, btn: u32, state: u32) -> PyResult<()> {
6697 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_button(btn, state))
6698 }
6699 fn inject_mouse_scroll(&self, py: Python<'_>, x: f64, y: f64) -> PyResult<()> {
6700 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().inject_mouse_scroll(x, y))
6701 }
6702 fn set_cursor_rendering(&self, py: Python<'_>, enabled: bool) -> PyResult<()> {
6706 let (backend, controls) = {
6707 let st = self.inner.lock().unwrap();
6708 (st.backend, st.controls.clone())
6709 };
6710 if backend == 1 {
6711 if let Some(c) = &controls {
6712 c.capture_cursor.store(enabled, Ordering::Relaxed);
6713 }
6714 return Ok(());
6715 }
6716 wayland_backend_running(py).map_or(Ok(()), |be| be.bind(py).borrow().set_cursor_rendering(enabled))
6717 }
6718 fn set_cursor_callback(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<()> {
6725 crate::x11::cursor::set_callback(callback.clone_ref(py));
6726 let slot = WAYLAND_BACKEND.get_or_init(|| Mutex::new(None));
6727 let g = slot.lock().unwrap();
6728 match g.as_ref() {
6729 Some(be) => be.bind(py).borrow().set_cursor_callback(callback),
6730 None => {
6731 *PENDING_CURSOR_CALLBACK.lock().unwrap() = Some(callback);
6732 Ok(())
6733 }
6734 }
6735 }
6736 fn get_xkb_keymap_string(&self, py: Python<'_>) -> PyResult<String> {
6737 wayland_backend_running(py)
6738 .map_or(Ok(String::new()), |be| be.bind(py).borrow().get_xkb_keymap_string(py))
6739 }
6740 fn set_clipboard_callback(&self, py: Python<'_>, callback: Py<PyAny>) -> PyResult<()> {
6742 match wayland_backend_running(py) {
6743 Some(be) => be.bind(py).borrow().set_clipboard_callback(callback),
6744 None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
6745 "wayland backend not running",
6746 )),
6747 }
6748 }
6749 fn set_clipboard(&self, py: Python<'_>, mime: String, data: Vec<u8>) -> PyResult<()> {
6751 match wayland_backend_running(py) {
6752 Some(be) => be.bind(py).borrow().set_clipboard(mime, data),
6753 None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
6754 "wayland backend not running",
6755 )),
6756 }
6757 }
6758 #[pyo3(signature = (layout, variant = String::new(), options = String::new(), model = String::new(), rules = String::new()))]
6761 fn set_xkb_layout(
6762 &self,
6763 py: Python<'_>,
6764 layout: String,
6765 variant: String,
6766 options: String,
6767 model: String,
6768 rules: String,
6769 ) -> PyResult<bool> {
6770 wayland_backend_running(py).map_or(Ok(false), |be| {
6771 be.bind(py).borrow().set_xkb_layout(py, layout, variant, options, model, rules)
6772 })
6773 }
6774 fn get_keyboard_state(&self, py: Python<'_>) -> PyResult<(Vec<u32>, u32)> {
6776 wayland_backend_running(py)
6777 .map_or(Ok((Vec::new(), 0)), |be| be.bind(py).borrow().get_keyboard_state(py))
6778 }
6779 #[pyo3(signature = (display_id = 0))]
6784 fn get_realized_geometry(
6785 &self,
6786 py: Python<'_>,
6787 display_id: u32,
6788 ) -> PyResult<Option<(i32, i32, f64)>> {
6789 wayland_backend_running(py)
6790 .map_or(Ok(None), |be| be.bind(py).borrow().get_realized_geometry(py, display_id))
6791 }
6792 #[pyo3(signature = (display_id = 0))]
6798 fn capture_state(&self, display_id: u32) -> (String, Option<String>) {
6799 let (backend, running, err) = {
6800 let st = self.inner.lock().unwrap();
6801 let running = st
6802 .controls
6803 .as_ref()
6804 .map(|c| !c.stop.load(Ordering::Relaxed))
6805 .unwrap_or(false);
6806 let err = st.err.as_ref().and_then(|e| e.lock().ok().and_then(|g| g.clone()));
6807 (st.backend, running, err)
6808 };
6809 match backend {
6810 1 => {
6811 let state = if running {
6812 "running"
6813 } else if err.is_some() {
6814 "failed"
6815 } else {
6816 "idle"
6817 };
6818 (state.to_string(), err)
6819 }
6820 2 => wayland_capture_state(display_id),
6821 _ => ("idle".to_string(), None),
6822 }
6823 }
6824 #[allow(clippy::too_many_arguments)]
6828 #[pyo3(signature = (id, width, height, x = 0, y = 0, scale = 1.0))]
6829 fn create_output(
6830 &self,
6831 py: Python<'_>,
6832 id: u32,
6833 width: i32,
6834 height: i32,
6835 x: i32,
6836 y: i32,
6837 scale: f64,
6838 ) -> PyResult<bool> {
6839 wayland_backend_running(py).map_or(Ok(false), |be| {
6840 be.bind(py).borrow().create_output(py, id, width, height, x, y, scale)
6841 })
6842 }
6843 fn destroy_output(&self, py: Python<'_>, id: u32) -> PyResult<bool> {
6845 wayland_backend_running(py)
6846 .map_or(Ok(false), |be| be.bind(py).borrow().destroy_output(py, id))
6847 }
6848 fn reposition_output(&self, py: Python<'_>, id: u32, x: i32, y: i32) -> PyResult<bool> {
6851 wayland_backend_running(py)
6852 .map_or(Ok(false), |be| be.bind(py).borrow().reposition_output(py, id, x, y))
6853 }
6854 fn set_cursor_size(&self, py: Python<'_>, size: i32) -> PyResult<bool> {
6857 wayland_backend_running(py)
6858 .map_or(Ok(false), |be| be.bind(py).borrow().set_cursor_size(py, size))
6859 }
6860 fn list_outputs(&self, py: Python<'_>) -> PyResult<Vec<OutputDesc>> {
6863 wayland_backend_running(py)
6864 .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_outputs(py))
6865 }
6866 fn output_capacity(&self, py: Python<'_>) -> PyResult<i64> {
6868 wayland_backend_running(py)
6869 .map_or(Ok(-1), |be| be.bind(py).borrow().output_capacity(py))
6870 }
6871 fn move_window_to_output(&self, py: Python<'_>, window_id: u32, output_id: u32) -> PyResult<bool> {
6873 wayland_backend_running(py).map_or(Ok(false), |be| {
6874 be.bind(py).borrow().move_window_to_output(py, window_id, output_id)
6875 })
6876 }
6877 fn list_windows(&self, py: Python<'_>) -> PyResult<Vec<WindowDesc>> {
6880 wayland_backend_running(py)
6881 .map_or(Ok(Vec::new()), |be| be.bind(py).borrow().list_windows(py))
6882 }
6883}
6884
6885impl Drop for ScreenCapture {
6891 fn drop(&mut self) {
6892 if let Ok(mut st) = self.inner.lock() {
6893 if let Some(c) = &st.controls {
6894 c.stop.store(true, Ordering::Relaxed);
6895 }
6896 if st.backend == 2 && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
6899 let did = st.wl_display;
6900 let owned = {
6901 let mut owners = wayland_owners().lock().unwrap();
6902 if owners.get(&did) == Some(&self.id) {
6903 owners.remove(&did);
6904 true
6905 } else {
6906 false
6907 }
6908 };
6909 if owned
6910 && let Some(slot) = WAYLAND_BACKEND.get()
6911 && let Some(be) = slot.lock().unwrap().as_ref() {
6912 Python::attach(|py| {
6913 let _ = be.bind(py).borrow().stop_capture(did);
6914 });
6915 }
6916 }
6917 if std::mem::take(&mut st.cursor_ref) && !crate::PY_SHUTDOWN.load(Ordering::Relaxed) {
6921 Python::attach(crate::x11::cursor::release);
6922 }
6923 }
6924 }
6925}
6926
6927fn recording_status_dict(py: Python<'_>, s: &crate::recorder::RecordingStatus) -> PyResult<Py<PyAny>> {
6929 let d = pyo3::types::PyDict::new(py);
6930 d.set_item("active", s.active)?;
6931 d.set_item("path", &s.path)?;
6932 d.set_item("backend", s.backend)?;
6933 d.set_item("mode", s.mode)?;
6934 d.set_item("frames", s.frames)?;
6935 d.set_item("sync_frames", s.sync_frames)?;
6936 d.set_item("dropped", s.dropped)?;
6937 d.set_item("skipped_non_h264", s.skipped_non_h264)?;
6938 d.set_item("bytes", s.bytes)?;
6939 d.set_item("duration_s", s.duration_s)?;
6940 d.set_item("width", s.width)?;
6941 d.set_item("height", s.height)?;
6942 d.set_item("error", s.error.as_deref())?;
6943 Ok(d.into_any().unbind())
6944}
6945
6946#[pyfunction]
6953#[pyo3(signature = (path, settings = None))]
6954fn start_recording(
6955 py: Python<'_>,
6956 path: String,
6957 settings: Option<&Bound<'_, PyAny>>,
6958) -> PyResult<Py<PyAny>> {
6959 let mut opts = crate::recorder::RecordOptions::from_env(path);
6960 if let Some(s) = settings {
6961 let rs = extract_settings(s)?;
6962 if rs.output_mode != 1 {
6963 return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
6964 "recording requires H.264 capture settings (output_mode=1); JPEG cannot be recorded",
6965 ));
6966 }
6967 opts.display_id = read_display_id(s);
6968 if let Some(explicit) = s
6969 .getattr("use_wayland")
6970 .ok()
6971 .and_then(|v| v.extract::<bool>().ok())
6972 {
6973 opts.backend = Some(if explicit {
6974 crate::recorder::PreferredBackend::Wayland
6975 } else {
6976 crate::recorder::PreferredBackend::X11
6977 });
6978 }
6979 opts.fps = 0.0;
6981 opts.bitrate_kbps = 0;
6982 opts.capture = Some(rs);
6983 }
6984 let status = py
6985 .detach(|| crate::recorder::start(opts))
6986 .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
6987 recording_status_dict(py, &status)
6988}
6989
6990#[pyfunction]
6993fn stop_recording(py: Python<'_>) -> PyResult<Py<PyAny>> {
6994 let status = py
6995 .detach(crate::recorder::stop)
6996 .map_err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>)?;
6997 recording_status_dict(py, &status)
6998}
6999
7000#[pyfunction]
7003fn recording_status(py: Python<'_>) -> PyResult<Py<PyAny>> {
7004 match crate::recorder::status() {
7005 Some(s) => recording_status_dict(py, &s),
7006 None => Ok(py.None()),
7007 }
7008}
7009
7010#[pyfunction]
7018#[pyo3(signature = (width = 0, height = 0, render_node = String::new(), auto_gpu = String::new(), cursor_size = -1))]
7019fn ensure_wayland_display(
7020 py: Python<'_>,
7021 width: i32,
7022 height: i32,
7023 render_node: String,
7024 auto_gpu: String,
7025 cursor_size: i32,
7026) -> PyResult<String> {
7027 ensure_wayland_backend(py, width, height, render_node, auto_gpu, String::new(), cursor_size)?;
7028 Ok(py
7029 .detach(|| wait_socket_name(Duration::from_secs(5)))
7030 .unwrap_or_default())
7031}
7032
7033#[pyfunction]
7051#[pyo3(signature = (render_node = String::new(), auto_gpu = String::new()))]
7052fn probe_wayland_gpu(
7053 py: Python<'_>,
7054 render_node: String,
7055 auto_gpu: String,
7056) -> PyResult<Py<PyAny>> {
7057 let (node, name, error) = py.detach(|| {
7058 let node = if render_node.is_empty() {
7059 parse_auto_gpu(&auto_gpu).and_then(|token| auto_select_render_node(token.as_deref()))
7060 } else {
7061 Some(render_node)
7062 };
7063 let Some(node) = node else {
7064 return (String::new(), String::new(), "No render node".to_string());
7065 };
7066 let mut name = String::new();
7067 let result = gpu_render_init(std::path::Path::new(&node)).and_then(|(gbm, mut renderer)| {
7068 let bo = gbm
7069 .create_buffer_object::<()>(64, 64, GbmFormat::Argb8888, BufferObjectFlags::RENDERING)
7070 .map_err(|_| "Failed to allocate GBM buffer")?;
7071 bo.fd().map_err(|e| format!("Failed to export dmabuf: {e:?}"))?;
7072 name = gl_renderer_name(&mut renderer);
7073 let lowered = name.to_lowercase();
7074 if ["llvmpipe", "softpipe", "swrast", "software rasterizer"]
7075 .iter()
7076 .any(|sw| lowered.contains(sw))
7077 {
7078 return Err(format!("Software rasterizer only ({name})"));
7079 }
7080 Ok(())
7081 });
7082 (node, name, result.err().unwrap_or_default())
7083 });
7084 let d = pyo3::types::PyDict::new(py);
7085 d.set_item("node", &node)?;
7086 d.set_item("accelerated", error.is_empty())?;
7087 d.set_item("gpu", gpu_exposed())?;
7088 d.set_item("renderer", &name)?;
7089 d.set_item("error", &error)?;
7090 Ok(d.into_any().unbind())
7091}
7092
7093#[pyfunction]
7096fn get_wayland_display_name(py: Python<'_>) -> Option<String> {
7097 wayland_backend_running(py)?;
7098 py.detach(|| wait_socket_name(Duration::from_secs(2)))
7099}
7100
7101#[pyfunction]
7110fn _stop_all_captures(py: Python<'_>) {
7111 PY_SHUTDOWN.store(true, Ordering::Relaxed);
7112 py.detach(crate::recorder::finalize_on_exit);
7115 *PENDING_CURSOR_CALLBACK.lock().unwrap() = None;
7116 crate::x11::cursor::shutdown();
7117 crate::wayland::dcclient::unwatch_all();
7118 let x11: Vec<Arc<crate::x11::Controls>> = live_x11().lock().unwrap().iter().cloned().collect();
7124 for c in &x11 {
7125 c.stop.store(true, Ordering::Relaxed);
7126 }
7127 if !x11.is_empty() {
7128 let deadline = Instant::now() + Duration::from_secs(2);
7129 py.detach(|| {
7130 for c in &x11 {
7131 while !c.finished.load(Ordering::Acquire) && Instant::now() < deadline {
7132 std::thread::sleep(Duration::from_millis(5));
7133 }
7134 }
7135 });
7136 }
7137 let be = WAYLAND_BACKEND
7142 .get()
7143 .and_then(|slot| slot.lock().unwrap().as_ref().map(|b| b.clone_ref(py)));
7144 if let Some(be) = be {
7145 let be = be.bind(py).borrow();
7146 let mut displays: Vec<u32> = wayland_alive().lock().unwrap().iter().copied().collect();
7147 if !displays.contains(&0) {
7148 displays.push(0);
7149 }
7150 for did in displays {
7151 let _ = be.stop_capture(did);
7152 }
7153 let (ack_tx, ack_rx) = std::sync::mpsc::channel::<()>();
7157 if be.send(ThreadCommand::Barrier { reply: ack_tx }).is_ok() {
7158 let _ = py.detach(move || ack_rx.recv_timeout(Duration::from_secs(2)));
7159 }
7160 }
7161 wayland_owners().lock().unwrap().clear();
7162 wayland_alive().lock().unwrap().clear();
7163 py.detach(|| std::thread::sleep(Duration::from_millis(50)));
7164}
7165
7166fn app_socket_path(display: &str) -> Result<String, String> {
7170 crate::wayland::wlclient::socket_path(display)
7171 .ok_or_else(|| "XDG_RUNTIME_DIR is unset".to_string())
7172}
7173
7174pyo3::create_exception!(
7175 pixelflux,
7176 VirtualKeyboardUnavailable,
7177 pyo3::exceptions::PyRuntimeError,
7178 "The target compositor does not advertise zwp_virtual_keyboard_manager_v1."
7179);
7180
7181#[pyfunction]
7185fn start_computer_use(bind: String) {
7186 crate::computer_use::start_cu_server(&bind);
7187}
7188
7189#[pymodule(gil_used = true)]
7196fn pixelflux(m: &Bound<'_, PyModule>) -> PyResult<()> {
7197 m.add_class::<WaylandBackend>()?;
7198 m.add_class::<StripeFrame>()?;
7199 m.add_class::<CaptureSettings>()?;
7200 m.add_class::<ScreenCapture>()?;
7201 m.add_class::<webcam::VirtualCamera>()?;
7202 m.add_class::<webcam::VirtualCameraSettings>()?;
7203 m.add("X11_CURSOR_CALLBACK", true)?;
7205 m.add("SOFTWARE_H264_ENCODER", encoders::SOFTWARE_H264_ENCODER)?;
7208 m.add_function(wrap_pyfunction!(stripe_frame_from_buffer, m)?)?;
7209 m.add_function(wrap_pyfunction!(ensure_wayland_display, m)?)?;
7210 m.add_function(wrap_pyfunction!(get_wayland_display_name, m)?)?;
7211 m.add_function(wrap_pyfunction!(probe_wayland_gpu, m)?)?;
7212 m.add_function(wrap_pyfunction!(start_recording, m)?)?;
7213 m.add_function(wrap_pyfunction!(stop_recording, m)?)?;
7214 m.add_function(wrap_pyfunction!(recording_status, m)?)?;
7215 m.add_function(wrap_pyfunction!(start_computer_use, m)?)?;
7216 m.add(
7217 "VirtualKeyboardUnavailable",
7218 m.py().get_type::<VirtualKeyboardUnavailable>(),
7219 )?;
7220 m.add_function(wrap_pyfunction!(_stop_all_captures, m)?)?;
7221 if let Ok(atexit) = m.py().import("atexit") {
7222 let _ = atexit.call_method1("register", (m.getattr("_stop_all_captures")?,));
7223 }
7224 crate::computer_use::spawn_cu_from_env();
7227 crate::recorder::autostart_from_env();
7230
7231
7232 Ok(())
7233}
7234
7235#[cfg(test)]
7236mod shm_usage_tests {
7237 use super::shm_usage_in;
7240
7241 #[test]
7242 fn only_allocated_blocks_count() {
7243 let dir = std::env::temp_dir().join(format!("pixelflux-shm-{}", std::process::id()));
7244 std::fs::create_dir_all(&dir).unwrap();
7245 let path = dir.to_str().unwrap().to_string();
7246
7247 std::fs::File::create(dir.join("sparse"))
7248 .unwrap()
7249 .set_len(8 * 1024 * 1024 * 1024)
7250 .unwrap();
7251 let sparse = shm_usage_in(&path);
7252
7253 std::fs::write(dir.join("dense"), vec![0u8; 4 * 1024 * 1024]).unwrap();
7254 let dense = shm_usage_in(&path);
7255
7256 std::fs::remove_dir_all(&dir).ok();
7257 assert!(sparse < 1024 * 1024, "an 8 GiB sparse file counted {sparse} bytes");
7258 assert!(
7259 dense - sparse >= 4 * 1024 * 1024,
7260 "a 4 MiB written file added only {} bytes",
7261 dense - sparse
7262 );
7263 }
7264}
7265
7266#[cfg(test)]
7267mod capture_state_tests {
7268 use super::{set_wayland_capture_err, wayland_alive, wayland_capture_state};
7272
7273 #[test]
7274 fn state_tracks_liveness_and_recorded_error() {
7275 let did = 987_654;
7277 set_wayland_capture_err(did, None);
7278 wayland_alive().lock().unwrap().remove(&did);
7279 assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
7280
7281 set_wayland_capture_err(did, Some("no output".to_string()));
7283 assert_eq!(
7284 wayland_capture_state(did),
7285 ("failed".to_string(), Some("no output".to_string()))
7286 );
7287
7288 wayland_alive().lock().unwrap().insert(did);
7290 set_wayland_capture_err(did, Some("using CPU encode".to_string()));
7291 assert_eq!(
7292 wayland_capture_state(did),
7293 ("running".to_string(), Some("using CPU encode".to_string()))
7294 );
7295
7296 wayland_alive().lock().unwrap().remove(&did);
7298 set_wayland_capture_err(did, None);
7299 assert_eq!(wayland_capture_state(did), ("idle".to_string(), None));
7300 }
7301}
7302
7303#[cfg(test)]
7304mod host_layout_tests {
7305 use super::host_layout_resolution;
7309
7310 #[test]
7311 fn verdict_resolves_to_a_follow_size_only_when_the_host_kept_another_mode() {
7312 let want = (1920, 1080);
7313 assert_eq!(host_layout_resolution(true, want, Some((1280, 720))), None);
7315 assert_eq!(host_layout_resolution(true, want, None), None);
7316 assert_eq!(host_layout_resolution(false, want, Some(want)), None);
7319 assert_eq!(host_layout_resolution(false, want, Some((2560, 1440))), Some((2560, 1440)));
7321 assert_eq!(host_layout_resolution(false, want, None), None);
7323 }
7324}
7325
7326#[cfg(test)]
7327mod output_overlap_tests {
7328 use super::rects_overlap;
7332
7333 #[test]
7334 fn disjoint_rects_do_not_overlap() {
7335 assert!(!rects_overlap((0, 0, 100, 100), (200, 0, 100, 100)));
7336 assert!(!rects_overlap((0, 0, 100, 100), (0, 200, 100, 100)));
7337 }
7338
7339 #[test]
7340 fn touching_edges_do_not_overlap() {
7341 assert!(!rects_overlap((0, 0, 100, 100), (100, 0, 100, 100)));
7343 assert!(!rects_overlap((0, 0, 100, 100), (0, 100, 100, 100)));
7344 assert!(!rects_overlap((0, 0, 100, 100), (100, 100, 50, 50)));
7346 }
7347
7348 #[test]
7349 fn one_pixel_intrusion_overlaps() {
7350 assert!(rects_overlap((0, 0, 100, 100), (99, 0, 100, 100)));
7351 assert!(rects_overlap((0, 0, 100, 100), (0, 99, 100, 100)));
7352 }
7353
7354 #[test]
7355 fn containment_and_identity_overlap() {
7356 assert!(rects_overlap((0, 0, 100, 100), (25, 25, 10, 10)));
7357 assert!(rects_overlap((25, 25, 10, 10), (0, 0, 100, 100)));
7358 assert!(rects_overlap((5, 5, 50, 50), (5, 5, 50, 50)));
7359 }
7360
7361 #[test]
7362 fn empty_or_negative_rects_never_overlap() {
7363 assert!(!rects_overlap((10, 10, 0, 50), (0, 0, 100, 100)));
7364 assert!(!rects_overlap((10, 10, 50, 0), (0, 0, 100, 100)));
7365 assert!(!rects_overlap((10, 10, -5, 5), (0, 0, 100, 100)));
7366 assert!(!rects_overlap((0, 0, 100, 100), (10, 10, 0, 0)));
7367 }
7368
7369 #[test]
7370 fn negative_origins_overlap_correctly() {
7371 assert!(rects_overlap((-50, -50, 100, 100), (0, 0, 100, 100)));
7372 assert!(!rects_overlap((-100, -100, 100, 100), (0, 0, 100, 100)));
7373 }
7374
7375 #[test]
7376 fn extreme_coordinates_do_not_wrap() {
7377 assert!(!rects_overlap((i32::MAX - 10, 0, 10, 10), (i32::MIN, 0, 10, 10)));
7378 assert!(rects_overlap((i32::MAX - 10, 0, 10, 10), (i32::MAX - 5, 0, 10, 10)));
7379 }
7380}
7381
7382#[cfg(test)]
7383mod wl_frame_pool_tests {
7384 use super::*;
7389
7390 fn frame(id: usize, buf: Vec<u8>, n: u16) -> WlFrame {
7391 WlFrame {
7392 id,
7393 buf,
7394 frame_id: n,
7395 damage: Vec::new(),
7396 is_animated: false,
7397 }
7398 }
7399
7400 #[test]
7401 fn begin_gated_on_slot_and_free_list() {
7402 let p = WlFramePool::new(2, 16);
7403 let (a, abuf) = p.try_begin().expect("first buffer");
7404 let (b, bbuf) = p.try_begin().expect("second buffer");
7405 assert_ne!(a, b);
7406 assert!(p.try_begin().is_none(), "free list exhausted");
7407 p.publish(frame(a, abuf, 0));
7408 p.cancel(b, bbuf);
7409 assert!(p.try_begin().is_none(), "slot occupied blocks begin");
7410 let f = p.take().expect("published frame");
7411 assert_eq!(f.frame_id, 0);
7412 p.recycle(f.id, f.buf);
7413 assert!(p.try_begin().is_some(), "drained slot re-enables begin");
7414 }
7415
7416 #[test]
7417 fn frames_flow_in_order_and_buffers_recycle() {
7418 let p = Arc::new(WlFramePool::new(2, 4));
7419 let p2 = p.clone();
7420 let consumer = thread::spawn(move || {
7421 let mut seen = Vec::new();
7422 while let Some(f) = p2.take() {
7423 seen.push(f.frame_id);
7424 p2.recycle(f.id, f.buf);
7425 }
7426 seen
7427 });
7428 let mut published = 0u16;
7429 while published < 50 {
7430 if let Some((id, buf)) = p.try_begin() {
7431 p.publish(frame(id, buf, published));
7432 published += 1;
7433 } else {
7434 thread::sleep(Duration::from_micros(50));
7435 }
7436 }
7437 thread::sleep(Duration::from_millis(50));
7438 p.shutdown();
7439 let seen = consumer.join().unwrap();
7440 assert_eq!(seen, (0..50).collect::<Vec<u16>>(), "every frame, in order");
7441 }
7442
7443 #[test]
7444 fn shutdown_unblocks_take() {
7445 let p = Arc::new(WlFramePool::new(1, 4));
7446 let p2 = p.clone();
7447 let t = thread::spawn(move || p2.take());
7448 thread::sleep(Duration::from_millis(30));
7449 p.shutdown();
7450 assert!(t.join().unwrap().is_none(), "take returns None on shutdown");
7451 }
7452
7453 #[test]
7454 fn cancel_returns_buffer_for_reuse() {
7455 let p = WlFramePool::new(1, 8);
7456 let (id, buf) = p.try_begin().expect("buffer");
7457 assert!(p.try_begin().is_none());
7458 p.cancel(id, buf);
7459 assert!(p.try_begin().is_some(), "cancelled reservation reusable");
7460 }
7461}
7462
7463#[cfg(test)]
7464mod auto_gpu_token_tests {
7465 use super::{card_matches_token, CardIdentity};
7470
7471 fn pci(driver: &str, vendor: u32) -> CardIdentity {
7472 CardIdentity { driver: driver.into(), pci_vendor: Some(vendor), compatibles: vec![] }
7473 }
7474
7475 fn dt(driver: &str, compatibles: &[&str]) -> CardIdentity {
7476 CardIdentity {
7477 driver: driver.into(),
7478 pci_vendor: None,
7479 compatibles: compatibles.iter().map(|c| c.to_string()).collect(),
7480 }
7481 }
7482
7483 #[test]
7484 fn driver_names_match_without_any_table() {
7485 assert!(card_matches_token("amdgpu", &pci("amdgpu", 0x1002)));
7486 assert!(card_matches_token("panfrost", &dt("panfrost", &["rockchip,rk3399-mali"])));
7487 assert!(card_matches_token("nouveau", &pci("nouveau", 0x10de)));
7488 assert!(!card_matches_token("i915", &pci("amdgpu", 0x1002)));
7489 }
7490
7491 #[test]
7492 fn vendor_names_and_raw_ids_match_pci_identity() {
7493 let nv = pci("nouveau", 0x10de);
7494 assert!(card_matches_token("nvidia", &nv));
7495 assert!(card_matches_token("0x10de", &nv));
7496 assert!(card_matches_token("10de", &nv));
7497 assert!(!card_matches_token("amd", &nv));
7498 assert!(card_matches_token("ati", &pci("radeon", 0x1002)));
7499 }
7500
7501 #[test]
7502 fn devicetree_prefixes_match_literally_and_via_aliases() {
7503 let mali = dt("panfrost", &["rockchip,rk3399-mali", "arm,mali-t860"]);
7504 assert!(card_matches_token("rockchip", &mali));
7505 assert!(card_matches_token("arm", &mali));
7506 assert!(card_matches_token("mali", &mali));
7507 let adreno = dt("msm", &["qcom,adreno-630", "qcom,adreno"]);
7508 assert!(card_matches_token("qcom", &adreno));
7509 assert!(card_matches_token("qualcomm", &adreno));
7510 assert!(card_matches_token("adreno", &adreno));
7511 assert!(!card_matches_token("brcm", &adreno));
7512 assert!(card_matches_token("videocore", &dt("v3d", &["brcm,bcm2711-v3d"])));
7513 }
7514
7515 #[test]
7516 fn missing_identity_fields_never_false_match() {
7517 let bare = CardIdentity { driver: String::new(), pci_vendor: None, compatibles: vec![] };
7518 for t in ["nvidia", "amdgpu", "0x10de", "qcom"] {
7519 assert!(!card_matches_token(t, &bare));
7520 }
7521 }
7522}