1#[cfg(feature = "computer-use-server")]
25pub mod server;
26
27use std::collections::HashMap;
28use std::io::Cursor;
29use std::sync::atomic::{AtomicBool, Ordering};
30use std::sync::mpsc;
31use std::sync::{Arc, Mutex, OnceLock};
32use std::thread;
33use std::time::{Duration, Instant};
34
35use smithay::input::keyboard::xkb;
36
37use base64::Engine;
38use base64::engine::general_purpose::STANDARD as BASE64;
39use image::{ImageBuffer, ImageFormat, Rgba};
40use serde::Deserialize;
41
42use crate::ThreadCommand;
43use crate::wayland::keymap::keysym_for_char;
44
45fn clamp<T: PartialOrd>(v: T, lo: T, hi: T) -> T {
46 if v < lo {
47 lo
48 } else if v > hi {
49 hi
50 } else {
51 v
52 }
53}
54
55pub fn encode_png_rgba(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
61 let img = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, data)
62 .ok_or("Failed to create image buffer")?;
63 let mut png = Vec::new();
64 img.write_to(&mut Cursor::new(&mut png), ImageFormat::Png)
65 .map_err(|e| format!("PNG encode error: {}", e))?;
66 Ok(png)
67}
68
69pub(crate) const MAX_SCREENSHOT_REQUESTS: usize = 4;
70const MAX_SCREENSHOT_REQUESTS_PER_DISPLAY: usize = 2;
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum ScreenshotPixelFormat {
74 Rgba,
75 Bgra,
76 Rgbx,
77 Bgrx,
78}
79
80pub(crate) struct ScreenshotBuffers {
84 kept: Mutex<Vec<Vec<u8>>>,
85}
86
87const SCREENSHOT_BUFFERS_KEPT: usize = MAX_SCREENSHOT_REQUESTS_PER_DISPLAY;
88
89static SCREENSHOT_BUFFERS: ScreenshotBuffers = ScreenshotBuffers::new();
90
91impl ScreenshotBuffers {
92 const fn new() -> Self {
93 Self {
94 kept: Mutex::new(Vec::new()),
95 }
96 }
97
98 fn take(&'static self, len: usize) -> ScreenshotBuffer {
99 let mut pixels = self.kept.lock().unwrap().pop().unwrap_or_default();
100 if pixels.len() > len {
102 pixels.truncate(len);
103 } else {
104 pixels.resize(len, 0);
105 }
106 ScreenshotBuffer { pixels, home: self }
107 }
108
109 fn keep(&self, pixels: Vec<u8>) {
110 let mut kept = self.kept.lock().unwrap();
111 if kept.len() < SCREENSHOT_BUFFERS_KEPT && pixels.capacity() > 0 {
112 kept.push(pixels);
113 }
114 }
115}
116
117pub struct ScreenshotBuffer {
119 pixels: Vec<u8>,
120 home: &'static ScreenshotBuffers,
121}
122
123impl ScreenshotBuffer {
124 pub(crate) fn with_len(len: usize) -> Self {
126 SCREENSHOT_BUFFERS.take(len)
127 }
128
129 pub(crate) fn copy_of(pixels: &[u8]) -> Self {
131 let mut buffer = Self::with_len(pixels.len());
132 buffer.copy_from_slice(pixels);
133 buffer
134 }
135}
136
137impl From<Vec<u8>> for ScreenshotBuffer {
138 fn from(pixels: Vec<u8>) -> Self {
139 Self {
140 pixels,
141 home: &SCREENSHOT_BUFFERS,
142 }
143 }
144}
145
146impl std::ops::Deref for ScreenshotBuffer {
147 type Target = [u8];
148 fn deref(&self) -> &[u8] {
149 &self.pixels
150 }
151}
152
153impl std::ops::DerefMut for ScreenshotBuffer {
154 fn deref_mut(&mut self) -> &mut [u8] {
155 &mut self.pixels
156 }
157}
158
159impl Drop for ScreenshotBuffer {
160 fn drop(&mut self) {
161 self.home.keep(std::mem::take(&mut self.pixels));
162 }
163}
164
165pub(crate) struct ReadbackSlot {
170 pub(crate) id: u32,
171 pub(crate) size: usize,
172 ptr: usize,
173 mapped: Mutex<bool>,
174}
175
176impl ReadbackSlot {
177 pub(crate) unsafe fn new(id: u32, ptr: *const u8, size: usize) -> Self {
180 Self {
181 id,
182 size,
183 ptr: ptr as usize,
184 mapped: Mutex::new(true),
185 }
186 }
187
188 pub(crate) fn in_use(self: &Arc<Self>) -> bool {
191 Arc::strong_count(self) > 1
192 }
193
194 pub(crate) fn unmap(&self) {
196 *self.mapped.lock().unwrap() = false;
197 }
198
199 pub(crate) fn copy_out(&self, len: usize) -> Result<ScreenshotBuffer, String> {
200 let mapped = self.mapped.lock().unwrap();
201 if !*mapped {
202 return Err("Screenshot compositor stopped".to_string());
203 }
204 if len > self.size {
205 return Err("Screenshot readback produced incomplete pixels".to_string());
206 }
207 let pixels = unsafe { std::slice::from_raw_parts(self.ptr as *const u8, len) };
209 Ok(ScreenshotBuffer::copy_of(pixels))
210 }
211}
212
213pub struct GpuReadback {
215 pub(crate) slot: Arc<ReadbackSlot>,
216 pub(crate) sync: smithay::backend::renderer::sync::SyncPoint,
217 pub(crate) len: usize,
218}
219
220impl GpuReadback {
221 fn resolve(self, deadline: Instant) -> Result<ScreenshotBuffer, String> {
223 while !self.sync.is_reached() {
224 if Instant::now() >= deadline {
225 return Err("Screenshot timed out".to_string());
226 }
227 thread::sleep(Duration::from_micros(200));
228 }
229 self.slot.copy_out(self.len)
230 }
231}
232
233pub enum ScreenshotPixels {
235 Ready(ScreenshotBuffer),
236 Gpu(GpuReadback),
237}
238
239impl From<Vec<u8>> for ScreenshotPixels {
240 fn from(pixels: Vec<u8>) -> Self {
241 Self::Ready(pixels.into())
242 }
243}
244
245pub struct ScreenshotFrame {
248 pub pixels: ScreenshotPixels,
249 pub width: u32,
250 pub height: u32,
251 pub format: ScreenshotPixelFormat,
252}
253
254impl ScreenshotFrame {
255 fn resolve(self, deadline: Instant) -> Result<Self, String> {
258 let pixels = match self.pixels {
259 ScreenshotPixels::Ready(pixels) => pixels,
260 ScreenshotPixels::Gpu(readback) => readback.resolve(deadline)?,
261 };
262 Ok(Self {
263 pixels: ScreenshotPixels::Ready(pixels),
264 ..self
265 })
266 }
267
268 fn encode_png(self) -> Result<Vec<u8>, String> {
269 let ScreenshotPixels::Ready(mut pixels) = self.pixels else {
270 return Err("Screenshot pixels are still being read back".to_string());
271 };
272 let expected = (self.width as usize)
273 .checked_mul(self.height as usize)
274 .and_then(|n| n.checked_mul(4));
275 if self.width == 0 || self.height == 0 || expected != Some(pixels.len()) {
276 return Err("Screenshot render produced an invalid framebuffer".to_string());
277 }
278 let swap = matches!(
279 self.format,
280 ScreenshotPixelFormat::Bgra | ScreenshotPixelFormat::Bgrx
281 );
282 let opaque = matches!(
283 self.format,
284 ScreenshotPixelFormat::Rgbx | ScreenshotPixelFormat::Bgrx
285 );
286 if swap || opaque {
287 for px in pixels.as_chunks_mut::<4>().0 {
288 if swap {
289 px.swap(0, 2);
290 }
291 if opaque {
292 px[3] = 255;
293 }
294 }
295 }
296 encode_png_rgba(&pixels, self.width, self.height)
297 }
298}
299
300#[derive(Default)]
304struct ScreenshotAdmission {
305 counts: Mutex<HashMap<u32, usize>>,
306}
307
308impl ScreenshotAdmission {
309 fn acquire(&self, display: u32) -> Result<ScreenshotPermit<'_>, String> {
310 let mut counts = self.counts.lock().unwrap();
311 if counts.values().sum::<usize>() >= MAX_SCREENSHOT_REQUESTS
312 || counts.get(&display).copied().unwrap_or(0) >= MAX_SCREENSHOT_REQUESTS_PER_DISPLAY
313 {
314 return Err("Screenshot busy".to_string());
315 }
316 *counts.entry(display).or_default() += 1;
317 Ok(ScreenshotPermit {
318 admission: self,
319 display,
320 canceled: Arc::new(AtomicBool::new(false)),
321 })
322 }
323}
324
325struct ScreenshotPermit<'a> {
326 admission: &'a ScreenshotAdmission,
327 display: u32,
328 canceled: Arc<AtomicBool>,
329}
330
331impl Drop for ScreenshotPermit<'_> {
332 fn drop(&mut self) {
333 self.canceled.store(true, Ordering::Release);
334 let mut counts = self.admission.counts.lock().unwrap();
335 if let Some(count) = counts.get_mut(&self.display) {
336 *count -= 1;
337 if *count == 0 {
338 counts.remove(&self.display);
339 }
340 }
341 }
342}
343
344static SCREENSHOT_ADMISSION: OnceLock<ScreenshotAdmission> = OnceLock::new();
345
346fn scancode_for_keyname(name: &str) -> Option<u32> {
347 Some(match name.to_lowercase().as_str() {
348 "return" | "enter" => 36,
349 "tab" => 23,
350 "escape" | "esc" => 9,
351 "backspace" => 22,
352 "delete" => 119,
353 "insert" => 118,
354 "home" => 110,
355 "end" => 115,
356 "pageup" => 112,
357 "pagedown" => 117,
358 "up" => 111,
359 "down" => 116,
360 "left" => 113,
361 "right" => 114,
362 "space" => 65,
363 "capslock" => 66,
364 "numlock" => 77,
365 "scrolllock" => 78,
366 "printscreen" | "sysrq" | "print" => 107,
367 "pause" | "break" => 127,
368 "f1" => 67,
369 "f2" => 68,
370 "f3" => 69,
371 "f4" => 70,
372 "f5" => 71,
373 "f6" => 72,
374 "f7" => 73,
375 "f8" => 74,
376 "f9" => 75,
377 "f10" => 76,
378 "f11" => 95,
379 "f12" => 96,
380 "f13" => 191,
381 "f14" => 192,
382 "f15" => 193,
383 "f16" => 194,
384 "f17" => 195,
385 "f18" => 196,
386 "f19" => 197,
387 "f20" => 198,
388 "f21" => 199,
389 "f22" => 200,
390 "f23" => 201,
391 "f24" => 202,
392 "ctrl" | "lctrl" | "leftctrl" => 37,
393 "rctrl" | "rightctrl" => 105,
394 "shift" | "lshift" | "leftshift" => 50,
395 "rshift" | "rightshift" => 62,
396 "alt" | "lalt" | "leftalt" => 64,
397 "ralt" | "rightalt" => 108,
398 "super" | "meta" | "lsuper" | "leftmeta" | "leftsuper" | "windows" | "leftwindows" => 133,
399 "rsuper" | "rightmeta" | "rightsuper" | "rightwindows" => 134,
400 "menu" | "compose" => 135,
401 "kp_0" | "kp0" => 90,
402 "kp_1" | "kp1" => 87,
403 "kp_2" | "kp2" => 88,
404 "kp_3" | "kp3" => 89,
405 "kp_4" | "kp4" => 83,
406 "kp_5" | "kp5" => 84,
407 "kp_6" | "kp6" => 85,
408 "kp_7" | "kp7" => 79,
409 "kp_8" | "kp8" => 80,
410 "kp_9" | "kp9" => 81,
411 "kp_decimal" | "kp_dot" => 91,
412 "kp_divide" | "kp_slash" => 106,
413 "kp_multiply" | "kp_asterisk" => 63,
414 "kp_subtract" | "kp_minus" => 82,
415 "kp_add" | "kp_plus" => 86,
416 "kp_enter" => 104,
417 _ => return None,
418 })
419}
420
421fn keysym_for_key_spec(name: &str) -> u32 {
426 let mut chars = name.chars();
427 if let (Some(c), None) = (chars.next(), chars.next()) {
428 return keysym_for_char(c);
429 }
430 let sym = xkb::keysym_from_name(name, xkb::KEYSYM_NO_FLAGS).raw();
431 if sym != 0 {
432 return sym;
433 }
434 xkb::keysym_from_name(name, xkb::KEYSYM_CASE_INSENSITIVE).raw()
435}
436
437fn is_modifier(scancode: u32) -> bool {
438 matches!(scancode, 37 | 105 | 50 | 62 | 64 | 108 | 133 | 134)
439}
440
441#[derive(Clone, Copy)]
444pub enum CuButton {
445 Left,
446 Right,
447 Middle,
448}
449
450pub trait CuBackend {
454 fn name(&self) -> &'static str;
455 fn fb_size(&self) -> Result<(i32, i32), String>;
456 fn display_fb_size(&self, display: u32) -> Result<(i32, i32), String> {
459 if display == 0 {
460 self.fb_size()
461 } else {
462 Err(format!("Unknown display: {display}"))
463 }
464 }
465 fn key(&self, scancode: u32, pressed: bool);
466 fn mouse_move(&self, x: f64, y: f64);
467 fn button(&self, btn: CuButton, pressed: bool);
468 fn scroll(&self, dx: f64, dy: f64);
469 fn screenshot_png(&self, display: u32) -> Result<Vec<u8>, String>;
471 fn cursor_pos(&self) -> Result<(f64, f64), String>;
472 fn with_transient_keysyms(&self, keysyms: &[u32], seq: &mut dyn FnMut(&HashMap<u32, u32>)) {
480 let _ = keysyms;
481 seq(&HashMap::new());
482 }
483 fn resolve_keysyms(&self, keysyms: &[u32]) -> Vec<(u32, u32)>;
489 fn altgr_keycode(&self) -> u32 {
494 KC_ALTGR
495 }
496}
497
498const BTN_LEFT: u32 = 0x110;
499const BTN_RIGHT: u32 = 0x111;
500const BTN_MIDDLE: u32 = 0x112;
501
502const REPLY_TIMEOUT: Duration = Duration::from_secs(5);
506
507#[derive(Clone)]
510pub struct CuWaylandBackend {
511 tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
512 wake_tx: smithay::reexports::calloop::channel::Sender<()>,
513}
514
515impl CuBackend for CuWaylandBackend {
516 fn name(&self) -> &'static str {
517 "wayland"
518 }
519
520 fn fb_size(&self) -> Result<(i32, i32), String> {
521 self.display_fb_size(0)
522 }
523
524 fn display_fb_size(&self, display: u32) -> Result<(i32, i32), String> {
525 let (resp_tx, resp_rx) = mpsc::channel();
526 self.tx
527 .send(ThreadCommand::CuGetInfo {
528 display_id: display,
529 resp: resp_tx,
530 })
531 .map_err(|_| "Failed to request compositor info".to_string())?;
532 let (w, h, _) = resp_rx
533 .recv_timeout(REPLY_TIMEOUT)
534 .map_err(|_| "Compositor info request failed".to_string())?;
535 if w <= 0 || h <= 0 {
538 return Err(format!("Unknown display: {display}"));
539 }
540 Ok((w, h))
541 }
542
543 fn key(&self, scancode: u32, pressed: bool) {
544 let _ = self.tx.send(ThreadCommand::KeyboardKey {
545 scancode,
546 state: if pressed { 1 } else { 0 },
547 });
548 }
549
550 fn mouse_move(&self, x: f64, y: f64) {
551 let _ = self
552 .tx
553 .send(ThreadCommand::PointerMotion { x, y, motion: 0 });
554 }
555
556 fn button(&self, btn: CuButton, pressed: bool) {
557 let btn = match btn {
558 CuButton::Left => BTN_LEFT,
559 CuButton::Right => BTN_RIGHT,
560 CuButton::Middle => BTN_MIDDLE,
561 };
562 let _ = self.tx.send(ThreadCommand::PointerButton {
563 btn,
564 state: if pressed { 1 } else { 0 },
565 });
566 }
567
568 fn scroll(&self, dx: f64, dy: f64) {
569 let _ = self.tx.send(ThreadCommand::PointerAxis { x: dx, y: dy });
570 }
571
572 fn screenshot_png(&self, display: u32) -> Result<Vec<u8>, String> {
573 let permit = SCREENSHOT_ADMISSION
574 .get_or_init(ScreenshotAdmission::default)
575 .acquire(display)?;
576 let deadline = Instant::now() + REPLY_TIMEOUT;
577 let (resp_tx, resp_rx) = mpsc::channel();
578 self.tx
579 .send(ThreadCommand::CuScreenshot {
580 display_id: display,
581 resp: resp_tx,
582 canceled: Arc::clone(&permit.canceled),
583 deadline,
584 })
585 .map_err(|_| "Failed to request screenshot".to_string())?;
586 let _ = self.wake_tx.send(());
587 let frame = resp_rx
588 .recv_timeout(deadline.saturating_duration_since(Instant::now()))
589 .map_err(|error| match error {
590 mpsc::RecvTimeoutError::Timeout => "Screenshot timed out".to_string(),
591 mpsc::RecvTimeoutError::Disconnected => {
592 "Screenshot compositor disconnected".to_string()
593 }
594 })??;
595 let frame = frame.resolve(deadline)?;
596 if Instant::now() >= deadline {
597 return Err("Screenshot timed out".to_string());
598 }
599 frame.encode_png()
600 }
601
602 fn cursor_pos(&self) -> Result<(f64, f64), String> {
603 let (resp_tx, resp_rx) = mpsc::channel();
604 self.tx
605 .send(ThreadCommand::CuCursorPosition { resp: resp_tx })
606 .map_err(|_| "Failed to request cursor position".to_string())?;
607 resp_rx
608 .recv_timeout(REPLY_TIMEOUT)
609 .map_err(|_| "Cursor position failed".to_string())
610 }
611
612 fn resolve_keysyms(&self, keysyms: &[u32]) -> Vec<(u32, u32)> {
613 let (resp_tx, resp_rx) = mpsc::channel();
614 if self
615 .tx
616 .send(ThreadCommand::BindKeysyms {
617 keysyms: keysyms.to_vec(),
618 reply: resp_tx,
619 })
620 .is_err()
621 {
622 return vec![(0, 0); keysyms.len()];
623 }
624 resp_rx
625 .recv_timeout(REPLY_TIMEOUT)
626 .unwrap_or_else(|_| vec![(0, 0); keysyms.len()])
627 }
628}
629
630const TYPE_RUN_KEYSYMS: usize = 8;
635
636fn distinct_runs(syms: &[u32], max: usize) -> Vec<&[u32]> {
638 let mut runs = Vec::new();
639 let (mut start, mut seen) = (0, std::collections::HashSet::new());
640 for (i, sym) in syms.iter().enumerate() {
641 if !seen.contains(sym) && seen.len() == max {
642 runs.push(&syms[start..i]);
643 start = i;
644 seen.clear();
645 }
646 seen.insert(*sym);
647 }
648 if start < syms.len() {
649 runs.push(&syms[start..]);
650 }
651 runs
652}
653
654struct KeyResolver<'a> {
659 backend: &'a dyn CuBackend,
660 cache: HashMap<u32, (u32, u32)>,
661}
662
663impl<'a> KeyResolver<'a> {
664 fn new(backend: &'a dyn CuBackend) -> Self {
665 Self {
666 backend,
667 cache: HashMap::new(),
668 }
669 }
670
671 fn prefetch(&mut self, keysyms: &[u32]) {
673 let missing: Vec<u32> = {
674 let mut seen = std::collections::HashSet::new();
675 keysyms
676 .iter()
677 .copied()
678 .filter(|&s| s != 0 && !self.cache.contains_key(&s) && seen.insert(s))
679 .collect()
680 };
681 if missing.is_empty() {
682 return;
683 }
684 let out = self.backend.resolve_keysyms(&missing);
685 for (sym, hit) in missing.into_iter().zip(out) {
686 self.cache.insert(sym, hit);
687 }
688 }
689
690 fn resolve(&mut self, keysym: u32) -> Option<(u32, u32)> {
693 if keysym == 0 {
694 return None;
695 }
696 self.prefetch(&[keysym]);
697 let hit = self.cache.get(&keysym).copied().unwrap_or((0, 0));
698 (hit.0 != 0).then_some(hit)
699 }
700}
701
702const KC_SHIFT: u32 = 50;
705const KC_ALTGR: u32 = 108;
706
707fn level_modifiers(level: u32, held_mask: u32, altgr: u32) -> Vec<u32> {
711 let mut out = Vec::new();
712 if level & 1 != 0 && held_mask & 1 == 0 {
713 out.push(KC_SHIFT);
714 }
715 if level & 2 != 0 && held_mask & 2 == 0 && altgr != 0 {
716 out.push(altgr);
717 }
718 out
719}
720
721#[derive(Deserialize)]
722struct CuActionRequest {
723 action: String,
724 coordinate: Option<[f64; 2]>,
725 start_coordinate: Option<[f64; 2]>,
726 text: Option<String>,
727 key: Option<String>,
728 scroll_direction: Option<String>,
729 scroll_amount: Option<i32>,
730 duration: Option<f64>,
731 region: Option<[f64; 4]>,
732 display: Option<u32>,
735}
736
737fn ok_json() -> String {
738 "{\"result\":\"ok\"}".to_string()
739}
740
741fn error_json(error: &str) -> String {
743 serde_json::json!({ "error": error }).to_string()
744}
745
746fn handle_action(req: CuActionRequest, backend: &dyn CuBackend) -> String {
747 let result = handle_action_inner(req, backend);
748 match result {
749 Ok(response) => response,
750 Err(e) => error_json(&e),
751 }
752}
753
754fn handle_action_inner(req: CuActionRequest, b: &dyn CuBackend) -> Result<String, String> {
765 let sleep_ms = |ms: u64| thread::sleep(Duration::from_millis(ms));
766
767 let (fb_w, fb_h) = b.fb_size()?;
768
769 let handle_coord = |coord: [f64; 2]| -> (f64, f64) {
770 (
771 clamp(coord[0], 0.0, f64::from(fb_w - 1)),
772 clamp(coord[1], 0.0, f64::from(fb_h - 1)),
773 )
774 };
775
776 let handle_modifier = |mod_name: &str| -> Option<u32> {
777 let sc = scancode_for_keyname(mod_name)?;
778 if is_modifier(sc) { Some(sc) } else { None }
779 };
780
781 match req.action.as_str() {
782 "screenshot" => {
783 let png = b.screenshot_png(req.display.unwrap_or(0))?;
784 let b64 = BASE64.encode(&png);
785 Ok(format!("{{\"data\":\"{}\"}}", b64))
786 }
787
788 "mouse_move" => {
789 let coord = req.coordinate.ok_or("Missing coordinate")?;
790 let (fx, fy) = handle_coord(coord);
791 b.mouse_move(fx, fy);
792 Ok(ok_json())
793 }
794
795 "left_click" | "right_click" | "middle_click" => {
796 let btn = match req.action.as_str() {
797 "left_click" => CuButton::Left,
798 "right_click" => CuButton::Right,
799 _ => CuButton::Middle,
800 };
801 if let Some(coord) = req.coordinate {
802 let (fx, fy) = handle_coord(coord);
803 b.mouse_move(fx, fy);
804 sleep_ms(30);
805 }
806 if let Some(ref mod_name) = req.text
807 && let Some(sc) = handle_modifier(mod_name)
808 {
809 b.key(sc, true);
810 sleep_ms(20);
811 }
812 b.button(btn, true);
813 sleep_ms(20);
814 b.button(btn, false);
815 if let Some(ref mod_name) = req.text
816 && let Some(sc) = handle_modifier(mod_name)
817 {
818 sleep_ms(10);
819 b.key(sc, false);
820 }
821 Ok(ok_json())
822 }
823
824 "double_click" | "triple_click" => {
825 let n = if req.action == "double_click" { 2 } else { 3 };
826 if let Some(coord) = req.coordinate {
827 let (fx, fy) = handle_coord(coord);
828 b.mouse_move(fx, fy);
829 sleep_ms(30);
830 }
831 if let Some(ref mod_name) = req.text
832 && let Some(sc) = handle_modifier(mod_name)
833 {
834 b.key(sc, true);
835 sleep_ms(20);
836 }
837 for _ in 0..n {
838 b.button(CuButton::Left, true);
839 sleep_ms(10);
840 b.button(CuButton::Left, false);
841 sleep_ms(10);
842 }
843 if let Some(ref mod_name) = req.text
844 && let Some(sc) = handle_modifier(mod_name)
845 {
846 sleep_ms(10);
847 b.key(sc, false);
848 }
849 Ok(ok_json())
850 }
851
852 "left_click_drag" => {
853 let start = req.start_coordinate.ok_or("Missing start_coordinate")?;
854 let end = req.coordinate.ok_or("Missing coordinate")?;
855 let (sx, sy) = handle_coord(start);
856 let (ex, ey) = handle_coord(end);
857 b.mouse_move(sx, sy);
858 sleep_ms(30);
859 b.button(CuButton::Left, true);
860 sleep_ms(30);
861 b.mouse_move(ex, ey);
862 sleep_ms(30);
863 b.button(CuButton::Left, false);
864 Ok(ok_json())
865 }
866
867 "left_mouse_down" => {
868 b.button(CuButton::Left, true);
869 Ok(ok_json())
870 }
871
872 "left_mouse_up" => {
873 b.button(CuButton::Left, false);
874 Ok(ok_json())
875 }
876
877 "type" => {
878 let text = req.text.as_deref().ok_or("Missing text")?;
879 if b.name() == "wayland"
884 && let Some(sock) = app_wayland_socket_path()
885 {
886 static FAILED_SOCK: Mutex<Option<String>> = Mutex::new(None);
890 match crate::wayland::vkclient::type_text_to(&sock, text) {
891 Ok(()) => {
892 *FAILED_SOCK.lock().unwrap() = None;
893 return Ok(ok_json());
894 }
895 Err(e) => {
896 let mut last = FAILED_SOCK.lock().unwrap();
897 if last.as_deref() != Some(sock.as_str()) {
898 eprintln!(
899 "[ComputerUse] app-compositor type via {sock} failed ({e}); using local seat until it is reachable"
900 );
901 *last = Some(sock);
902 }
903 }
904 }
905 }
906 let all: Vec<u32> = text.chars().map(keysym_for_char).collect();
907 for syms in distinct_runs(&all, TYPE_RUN_KEYSYMS) {
908 let mut resolver = KeyResolver::new(b);
909 resolver.prefetch(syms);
910 let mut unresolved: Vec<u32> = Vec::new();
913 for &sym in syms {
914 if sym != 0 && resolver.resolve(sym).is_none() && !unresolved.contains(&sym) {
915 unresolved.push(sym);
916 }
917 }
918 b.with_transient_keysyms(&unresolved, &mut |bound| {
919 for (i, &sym) in syms.iter().enumerate() {
920 if i > 0 && i % 50 == 0 {
921 sleep_ms(20);
922 }
923 if let Some((sc, level)) = resolver.resolve(sym) {
924 let level_mods = level_modifiers(level, 0, b.altgr_keycode());
925 for &m in &level_mods {
926 b.key(m, true);
927 sleep_ms(5);
928 }
929 b.key(sc, true);
930 sleep_ms(10);
931 b.key(sc, false);
932 for &m in level_mods.iter().rev() {
933 b.key(m, false);
934 }
935 sleep_ms(8);
936 } else if let Some(&kc) = bound.get(&sym) {
937 b.key(kc, true);
939 sleep_ms(10);
940 b.key(kc, false);
941 sleep_ms(8);
942 }
943 }
944 });
945 }
946 Ok(ok_json())
947 }
948
949 "key" => {
950 let text = req.text.as_deref().ok_or("Missing text")?;
951 let mut resolver = KeyResolver::new(b);
952 let mut mods: Vec<u32> = Vec::new();
953 let mut main_key: Option<(u32, u32)> = None;
954 let mut unresolved_sym: Option<u32> = None;
957 for part in text.split('+') {
958 let trimmed = part.trim();
959 if let Some(sc) = scancode_for_keyname(trimmed) {
960 if is_modifier(sc) {
961 mods.push(sc);
962 } else {
963 main_key = Some((sc, 0));
964 unresolved_sym = None;
965 }
966 } else {
967 let sym = keysym_for_key_spec(trimmed);
968 if let Some(hit) = resolver.resolve(sym) {
969 main_key = Some(hit);
970 unresolved_sym = None;
971 } else if sym != 0 {
972 main_key = None;
973 unresolved_sym = Some(sym);
974 }
975 }
976 }
977 let unresolved: Vec<u32> = unresolved_sym.into_iter().collect();
978 b.with_transient_keysyms(&unresolved, &mut |bound| {
979 let main_key = main_key.or_else(|| {
980 unresolved_sym
982 .and_then(|s| bound.get(&s))
983 .map(|&kc| (kc, 0))
984 });
985 for &sc in &mods {
986 b.key(sc, true);
987 sleep_ms(10);
988 }
989 if let Some((sc, level)) = main_key {
990 let altgr = b.altgr_keycode();
994 let held = mods.iter().fold(0u32, |m, &sc| match sc {
995 50 | 62 => m | 1,
996 sc if sc == KC_ALTGR || sc == altgr => m | 2,
997 _ => m,
998 });
999 let level_mods = level_modifiers(level, held, altgr);
1000 for &m in &level_mods {
1001 b.key(m, true);
1002 sleep_ms(10);
1003 }
1004 b.key(sc, true);
1005 sleep_ms(30);
1006 b.key(sc, false);
1007 for &m in level_mods.iter().rev() {
1008 sleep_ms(10);
1009 b.key(m, false);
1010 }
1011 } else if let Some(&last_mod) = mods.last() {
1012 b.key(last_mod, true);
1013 sleep_ms(30);
1014 b.key(last_mod, false);
1015 }
1016 for &sc in mods.iter().rev() {
1017 sleep_ms(10);
1018 b.key(sc, false);
1019 }
1020 });
1021 Ok(ok_json())
1022 }
1023
1024 "hold_key" => {
1025 let text = req.text.as_deref().ok_or("Missing text")?;
1026 let duration = req.duration.ok_or("Missing duration")?;
1027 let duration = duration.min(100.0);
1028 let trimmed = text.trim();
1029 let mut resolver = KeyResolver::new(b);
1030 let hit = match scancode_for_keyname(trimmed) {
1031 Some(sc) => Some((sc, 0)),
1032 None => resolver.resolve(keysym_for_key_spec(trimmed)),
1033 };
1034 if let Some((sc, level)) = hit {
1035 let level_mods = level_modifiers(level, 0, b.altgr_keycode());
1036 for &m in &level_mods {
1037 b.key(m, true);
1038 sleep_ms(10);
1039 }
1040 b.key(sc, true);
1041 sleep_ms((duration * 1000.0) as u64);
1042 b.key(sc, false);
1043 for &m in level_mods.iter().rev() {
1044 sleep_ms(10);
1045 b.key(m, false);
1046 }
1047 }
1048 Ok(ok_json())
1049 }
1050
1051 "scroll" => {
1052 let dir = req
1053 .scroll_direction
1054 .as_deref()
1055 .ok_or("Missing scroll_direction")?;
1056 let amount = f64::from(req.scroll_amount.unwrap_or(1).max(0));
1057 if let Some(coord) = req.coordinate {
1058 let (fx, fy) = handle_coord(coord);
1059 b.mouse_move(fx, fy);
1060 sleep_ms(30);
1061 }
1062 if let Some(ref mod_name) = req.text
1063 && let Some(sc) = handle_modifier(mod_name)
1064 {
1065 b.key(sc, true);
1066 sleep_ms(20);
1067 }
1068 let (dx, dy) = match dir {
1069 "up" => (0.0, -amount),
1070 "down" => (0.0, amount),
1071 "left" => (-amount, 0.0),
1072 "right" => (amount, 0.0),
1073 _ => return Err(format!("Invalid scroll_direction: {}", dir)),
1074 };
1075 b.scroll(dx, dy);
1076 sleep_ms(30);
1077 if let Some(ref mod_name) = req.text
1078 && let Some(sc) = handle_modifier(mod_name)
1079 {
1080 sleep_ms(10);
1081 b.key(sc, false);
1082 }
1083 Ok(ok_json())
1084 }
1085
1086 "cursor_position" => {
1087 let (x, y) = b.cursor_pos()?;
1088 Ok(format!(
1089 "{{\"text\":\"X={},Y={}\"}}",
1090 x.round() as i32,
1091 y.round() as i32
1092 ))
1093 }
1094
1095 "wait" => {
1096 let duration = req.duration.unwrap_or(1.0).min(100.0);
1097 sleep_ms((duration * 1000.0) as u64);
1098 Ok(ok_json())
1099 }
1100
1101 "zoom" => {
1102 let region = req.region.ok_or("Missing region")?;
1103 let display = req.display.unwrap_or(0);
1104 let (dw, dh) = b.display_fb_size(display)?;
1107 let (x0, y0, x1, y1) = (region[0], region[1], region[2], region[3]);
1108 let left = clamp(x0.round() as u32, 0, dw as u32 - 1);
1109 let top = clamp(y0.round() as u32, 0, dh as u32 - 1);
1110 let right = clamp(x1.round() as u32, left + 1, dw as u32);
1111 let bottom = clamp(y1.round() as u32, top + 1, dh as u32);
1112 let crop_w = right - left;
1113 let crop_h = bottom - top;
1114 let png = b.screenshot_png(display)?;
1115 if crop_w > 0
1116 && crop_h > 0
1117 && let Ok(img) = image::load_from_memory(&png)
1118 {
1119 let cropped = img.crop_imm(left, top, crop_w, crop_h);
1120 let mut out = Vec::new();
1121 cropped
1122 .write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
1123 .map_err(|e| format!("Crop/encode error: {}", e))?;
1124 let b64 = BASE64.encode(&out);
1125 return Ok(format!("{{\"data\":\"{}\"}}", b64));
1126 }
1127 let b64 = BASE64.encode(&png);
1128 Ok(format!("{{\"data\":\"{}\"}}", b64))
1129 }
1130
1131 _ => Err(format!("Unknown action: {}", req.action)),
1132 }
1133}
1134
1135static WAYLAND_BACKEND: Mutex<Option<CuWaylandBackend>> = Mutex::new(None);
1136
1137#[derive(Deserialize, Default)]
1140struct RecordStartRequest {
1141 path: Option<String>,
1144 display: Option<u32>,
1146 fps: Option<f64>,
1148 bitrate_kbps: Option<i32>,
1150 audio_socket: Option<String>,
1152}
1153
1154fn named_record_path(dir: Option<&str>, name: &str) -> Result<String, String> {
1157 let dir = dir.ok_or("a recording named by the caller needs PIXELFLUX_RECORD_DIR")?;
1158 let mut parts = std::path::Path::new(name).components();
1159 match (parts.next(), parts.next()) {
1160 (Some(std::path::Component::Normal(file)), None) => Ok(std::path::Path::new(dir)
1161 .join(file)
1162 .to_string_lossy()
1163 .into_owned()),
1164 _ => Err(format!("'{name}' is not a file name")),
1165 }
1166}
1167
1168pub fn record_start(body: &str) -> String {
1172 let req: RecordStartRequest = if body.trim().is_empty() {
1173 RecordStartRequest::default()
1174 } else {
1175 match serde_json::from_str(body) {
1176 Ok(r) => r,
1177 Err(e) => return error_json(&format!("Invalid JSON: {e}")),
1178 }
1179 };
1180 let record_dir = std::env::var("PIXELFLUX_RECORD_DIR")
1181 .ok()
1182 .filter(|d| !d.is_empty());
1183 let path = match req.path {
1184 Some(name) => match named_record_path(record_dir.as_deref(), &name) {
1185 Ok(path) => path,
1186 Err(e) => return error_json(&e),
1187 },
1188 None => std::env::var("PIXELFLUX_RECORD")
1189 .ok()
1190 .filter(|p| !p.is_empty())
1191 .unwrap_or_else(|| {
1192 let ts = std::time::SystemTime::now()
1193 .duration_since(std::time::UNIX_EPOCH)
1194 .map(|d| d.as_secs())
1195 .unwrap_or(0);
1196 let runtime = std::env::var("XDG_RUNTIME_DIR")
1198 .ok()
1199 .filter(|d| !d.is_empty());
1200 let dir = record_dir
1201 .as_deref()
1202 .or(runtime.as_deref())
1203 .unwrap_or("/tmp");
1204 format!("{dir}/pixelflux-record-{ts}.mp4")
1205 }),
1206 };
1207 let mut opts = crate::recorder::RecordOptions::from_env(path);
1208 if let Some(d) = req.display {
1209 opts.display_id = d;
1210 }
1211 if let Some(f) = req.fps {
1212 opts.fps = f;
1213 }
1214 if let Some(b) = req.bitrate_kbps {
1215 opts.bitrate_kbps = b;
1216 }
1217 if let Some(a) = req.audio_socket {
1218 opts.audio_socket = a;
1219 }
1220 match crate::recorder::start(opts) {
1221 Ok(s) => crate::recorder::status_to_json(&s).to_string(),
1222 Err(e) => error_json(&e),
1223 }
1224}
1225
1226pub fn record_stop() -> String {
1229 match crate::recorder::stop() {
1230 Ok(s) => {
1231 let mut v = crate::recorder::status_to_json(&s);
1232 v["stopped"] = serde_json::Value::Bool(true);
1233 v.to_string()
1234 }
1235 Err(e) => error_json(&e),
1236 }
1237}
1238
1239pub fn record_status() -> String {
1242 match crate::recorder::status() {
1243 Some(s) => crate::recorder::status_to_json(&s).to_string(),
1244 None => serde_json::json!({ "active": false }).to_string(),
1245 }
1246}
1247
1248static CU_APP_WAYLAND_DISPLAY: Mutex<Option<String>> = Mutex::new(None);
1253
1254pub fn set_app_wayland_display(display: Option<String>) {
1257 *CU_APP_WAYLAND_DISPLAY.lock().unwrap() = display.filter(|s| !s.is_empty());
1258 crate::wayland::ficlient::arm(app_wayland_socket_path());
1259}
1260
1261pub(crate) fn app_wayland_socket_path() -> Option<String> {
1266 let name = CU_APP_WAYLAND_DISPLAY
1267 .lock()
1268 .unwrap()
1269 .clone()
1270 .or_else(|| std::env::var("PIXELFLUX_APP_WAYLAND_DISPLAY").ok())
1271 .filter(|s| !s.is_empty())?;
1272 if crate::wait_socket_name(Duration::from_millis(0)).as_deref() == Some(name.as_str()) {
1273 return None;
1274 }
1275 crate::wayland::wlclient::socket_path(&name)
1276}
1277
1278pub fn register_wayland_backend(
1281 tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
1282 wake_tx: smithay::reexports::calloop::channel::Sender<()>,
1283) {
1284 *WAYLAND_BACKEND.lock().unwrap() = Some(CuWaylandBackend { tx, wake_tx });
1285}
1286
1287pub(crate) fn wayland_command_sender()
1290-> Option<smithay::reexports::calloop::channel::Sender<ThreadCommand>> {
1291 WAYLAND_BACKEND
1292 .lock()
1293 .unwrap()
1294 .as_ref()
1295 .map(|be| be.tx.clone())
1296}
1297
1298pub(crate) fn resolve_backend() -> Result<Box<dyn CuBackend>, String> {
1302 if let Some(backend) = WAYLAND_BACKEND.lock().unwrap().clone() {
1303 return Ok(Box::new(backend));
1304 }
1305 crate::x11::computer_use::CuX11Backend::connect().map(|be| Box::new(be) as Box<dyn CuBackend>)
1306}
1307
1308#[derive(Debug)]
1311pub struct InvalidRequest(pub String);
1312
1313static LAST_BACKEND: Mutex<&'static str> = Mutex::new("");
1315
1316pub fn action(body: &str) -> Result<String, InvalidRequest> {
1322 let parsed: CuActionRequest = serde_json::from_str(body)
1323 .map_err(|e| InvalidRequest(error_json(&format!("Invalid JSON: {e}"))))?;
1324 Ok(match resolve_backend() {
1325 Ok(backend) => {
1326 let name = backend.name();
1327 if std::mem::replace(&mut *LAST_BACKEND.lock().unwrap(), name) != name {
1328 println!("[ComputerUse] Using {name} backend");
1329 }
1330 handle_action(parsed, backend.as_ref())
1331 }
1332 Err(e) => error_json(&e),
1333 })
1334}
1335
1336pub fn act(body: &str) -> String {
1339 action(body).unwrap_or_else(|invalid| invalid.0)
1340}
1341
1342#[cfg(test)]
1343mod api_tests {
1344 use super::{InvalidRequest, act, action, record_start, record_status};
1345
1346 #[test]
1349 fn a_body_that_is_no_action_is_an_invalid_request() {
1350 for body in ["", "{", "[]", r#"{"action":7}"#, r#"{"coordinate":[1,2]}"#] {
1351 let InvalidRequest(reply) = action(body).expect_err(body);
1352 let reply: serde_json::Value = serde_json::from_str(&reply).expect(body);
1353 let error = reply["error"].as_str().expect(body);
1354 assert!(error.starts_with("Invalid JSON: "), "{error}");
1355 assert_eq!(act(body), reply.to_string());
1356 }
1357 }
1358
1359 #[test]
1362 fn the_recorder_answers_with_no_recording() {
1363 assert_eq!(record_status(), r#"{"active":false}"#);
1364 let reply: serde_json::Value = serde_json::from_str(&record_start("{")).unwrap();
1365 assert!(
1366 reply["error"]
1367 .as_str()
1368 .is_some_and(|e| e.starts_with("Invalid JSON: ")),
1369 "{reply}"
1370 );
1371 }
1372}
1373
1374#[cfg(test)]
1375mod run_tests {
1376 use super::distinct_runs;
1377
1378 #[test]
1381 fn a_text_is_typed_in_runs_of_few_distinct_characters() {
1382 let syms = [1, 2, 1, 3, 4, 4, 5, 1, 6];
1383 assert_eq!(
1384 distinct_runs(&syms, 3),
1385 vec![&[1, 2, 1, 3][..], &[4, 4, 5, 1][..], &[6][..]]
1386 );
1387 assert_eq!(distinct_runs(&syms, 9), vec![&syms[..]]);
1388 assert!(distinct_runs(&[], 3).is_empty());
1389 }
1390}
1391
1392#[cfg(test)]
1393mod record_path_tests {
1394 use super::named_record_path;
1395
1396 #[test]
1397 fn a_named_recording_stays_in_the_operators_directory() {
1398 assert_eq!(
1399 named_record_path(Some("/rec"), "take.mp4").unwrap(),
1400 "/rec/take.mp4"
1401 );
1402 for name in ["/etc/passwd", "../x.mp4", "a/b.mp4", "..", ".", ""] {
1403 assert!(named_record_path(Some("/rec"), name).is_err(), "{name:?}");
1404 }
1405 assert!(
1406 named_record_path(None, "take.mp4").is_err(),
1407 "no directory, no named file"
1408 );
1409 }
1410}
1411
1412#[cfg(test)]
1413mod tests {
1414 use super::{
1415 GpuReadback, ReadbackSlot, ScreenshotAdmission, ScreenshotBuffers, ScreenshotFrame,
1416 ScreenshotPixelFormat, ScreenshotPixels, encode_png_rgba,
1417 };
1418 use std::io::Cursor;
1419 use std::sync::Arc;
1420 use std::sync::atomic::Ordering;
1421 use std::time::{Duration, Instant};
1422
1423 #[test]
1424 fn screenshot_png_preserves_channels_alpha_and_encoding() {
1425 let mut rgba: Vec<u8> = (0..4 * 17 * 11).map(|n| (n * 37) as u8).collect();
1426 rgba[3] = 0;
1427 rgba[7] = 128;
1428 rgba[11] = 255;
1429 let mut previous_png = Vec::new();
1430 image::ImageBuffer::<image::Rgba<u8>, _>::from_raw(17, 11, rgba.clone())
1431 .unwrap()
1432 .write_to(&mut Cursor::new(&mut previous_png), image::ImageFormat::Png)
1433 .unwrap();
1434 assert_eq!(encode_png_rgba(&rgba, 17, 11).unwrap(), previous_png);
1435 for format in [ScreenshotPixelFormat::Rgba, ScreenshotPixelFormat::Bgra] {
1436 let mut pixels = rgba.clone();
1437 if matches!(format, ScreenshotPixelFormat::Bgra) {
1438 for px in pixels.as_chunks_mut::<4>().0 {
1439 px.swap(0, 2);
1440 }
1441 }
1442 let png = ScreenshotFrame {
1443 pixels: pixels.into(),
1444 width: 17,
1445 height: 11,
1446 format,
1447 }
1448 .encode_png()
1449 .unwrap();
1450 assert_eq!(png, previous_png);
1451 let decoded = image::load_from_memory(&png).unwrap().into_rgba8();
1452 assert_eq!(decoded.dimensions(), (17, 11));
1453 assert_eq!(decoded.into_raw(), rgba);
1454 }
1455 }
1456
1457 #[test]
1458 fn screenshot_rejects_incomplete_oversized_and_overflowing_frames() {
1459 for (width, height, len) in [
1460 (0, 1, 0),
1461 (1, 0, 0),
1462 (2, 2, 15),
1463 (2, 2, 17),
1464 (u32::MAX, u32::MAX, 0),
1465 ] {
1466 assert!(
1467 ScreenshotFrame {
1468 pixels: vec![0; len].into(),
1469 width,
1470 height,
1471 format: ScreenshotPixelFormat::Rgba,
1472 }
1473 .encode_png()
1474 .is_err()
1475 );
1476 }
1477 }
1478
1479 #[test]
1480 fn screenshot_opaque_formats_ignore_unused_alpha_bytes() {
1481 for format in [ScreenshotPixelFormat::Rgbx, ScreenshotPixelFormat::Bgrx] {
1482 let mut pixels = vec![11, 22, 33, 0, 44, 55, 66, 128, 77, 88, 99, 255];
1483 if format == ScreenshotPixelFormat::Bgrx {
1484 for px in pixels.as_chunks_mut::<4>().0 {
1485 px.swap(0, 2);
1486 }
1487 }
1488 let png = ScreenshotFrame {
1489 pixels: pixels.into(),
1490 width: 3,
1491 height: 1,
1492 format,
1493 }
1494 .encode_png()
1495 .unwrap();
1496 let decoded = image::load_from_memory(&png).unwrap().into_rgba8();
1497 assert_eq!(
1498 decoded.into_raw(),
1499 [11, 22, 33, 255, 44, 55, 66, 255, 77, 88, 99, 255]
1500 );
1501 }
1502 }
1503
1504 #[derive(Debug, Default)]
1506 struct TestFence(std::sync::atomic::AtomicBool);
1507
1508 impl smithay::backend::renderer::sync::Fence for TestFence {
1509 fn is_signaled(&self) -> bool {
1510 self.0.load(Ordering::Acquire)
1511 }
1512 fn wait(&self) -> Result<(), smithay::backend::renderer::sync::Interrupted> {
1513 Ok(())
1514 }
1515 fn is_exportable(&self) -> bool {
1516 false
1517 }
1518 fn export(&self) -> Option<std::os::fd::OwnedFd> {
1519 None
1520 }
1521 }
1522
1523 #[test]
1524 fn screenshot_readback_slot_copies_while_mapped_and_refuses_after() {
1525 let mapping: Vec<u8> = (0..16).collect();
1526 let slot = Arc::new(unsafe { ReadbackSlot::new(7, mapping.as_ptr(), mapping.len()) });
1528 assert!(!slot.in_use());
1529 let held = Arc::clone(&slot);
1530 assert!(slot.in_use());
1531 assert_eq!(&*held.copy_out(8).unwrap(), &mapping[..8]);
1532 assert!(held.copy_out(17).is_err());
1533 slot.unmap();
1534 assert_eq!(
1535 held.copy_out(8).err().unwrap(),
1536 "Screenshot compositor stopped"
1537 );
1538 drop(held);
1539 assert!(!slot.in_use());
1540 }
1541
1542 #[test]
1543 fn screenshot_gpu_readback_waits_for_its_fence_until_the_deadline() {
1544 let mapping = [9u8; 12];
1545 let slot = Arc::new(unsafe { ReadbackSlot::new(1, mapping.as_ptr(), mapping.len()) });
1547 let fence = Arc::new(TestFence::default());
1548 let pending = |fence: &Arc<TestFence>| GpuReadback {
1549 slot: Arc::clone(&slot),
1550 sync: smithay::backend::renderer::sync::SyncPoint::from(TestFenceRef(Arc::clone(
1551 fence,
1552 ))),
1553 len: 12,
1554 };
1555 let late = pending(&fence).resolve(Instant::now() + Duration::from_millis(5));
1556 assert_eq!(late.err().unwrap(), "Screenshot timed out");
1557 assert!(!slot.in_use(), "a timed-out readback releases its slot");
1558 fence.0.store(true, Ordering::Release);
1559 let ready = pending(&fence)
1560 .resolve(Instant::now() + Duration::from_secs(1))
1561 .unwrap();
1562 assert_eq!(&*ready, &mapping[..]);
1563 let frame = ScreenshotFrame {
1564 pixels: ScreenshotPixels::Gpu(pending(&fence)),
1565 width: 3,
1566 height: 1,
1567 format: ScreenshotPixelFormat::Rgba,
1568 };
1569 assert!(
1570 ScreenshotFrame {
1571 pixels: ScreenshotPixels::Gpu(pending(&fence)),
1572 ..frame.resolve(Instant::now()).unwrap()
1573 }
1574 .encode_png()
1575 .is_err(),
1576 "pixels still on their way are never encoded"
1577 );
1578 }
1579
1580 #[derive(Debug)]
1582 struct TestFenceRef(Arc<TestFence>);
1583
1584 impl smithay::backend::renderer::sync::Fence for TestFenceRef {
1585 fn is_signaled(&self) -> bool {
1586 self.0.is_signaled()
1587 }
1588 fn wait(&self) -> Result<(), smithay::backend::renderer::sync::Interrupted> {
1589 Ok(())
1590 }
1591 fn is_exportable(&self) -> bool {
1592 false
1593 }
1594 fn export(&self) -> Option<std::os::fd::OwnedFd> {
1595 None
1596 }
1597 }
1598
1599 #[test]
1600 fn screenshot_buffers_are_kept_for_the_next_screenshot_and_bounded() {
1601 static BUFFERS: ScreenshotBuffers = ScreenshotBuffers::new();
1602 let first = BUFFERS.take(64);
1603 assert_eq!(first.len(), 64);
1604 let address = first.as_ptr();
1605 drop(first);
1606 let again = BUFFERS.take(16);
1608 assert_eq!((again.len(), again.as_ptr()), (16, address));
1609 drop(again);
1610 let grown = BUFFERS.take(4096);
1611 assert_eq!(grown.len(), 4096);
1612 let held: Vec<_> = (0..4).map(|_| BUFFERS.take(8)).collect();
1613 drop((grown, held));
1614 assert_eq!(BUFFERS.kept.lock().unwrap().len(), 2);
1615 }
1616
1617 #[test]
1618 fn screenshot_capacity_spans_readback_and_encoding_until_release() {
1619 let admission = ScreenshotAdmission::default();
1620 let first = admission.acquire(0).unwrap();
1621 let canceled = Arc::clone(&first.canceled);
1622 let second = admission.acquire(0).unwrap();
1623 assert!(admission.acquire(0).is_err());
1624 let third = admission.acquire(1).unwrap();
1625 let fourth = admission.acquire(2).unwrap();
1626 assert!(admission.acquire(3).is_err());
1627 assert!(!canceled.load(Ordering::Acquire));
1628 drop(first);
1629 assert!(canceled.load(Ordering::Acquire));
1630 let replacement = admission.acquire(3).unwrap();
1631 assert!(admission.acquire(0).is_err());
1632 drop((second, third, fourth, replacement));
1633 assert!(admission.counts.lock().unwrap().is_empty());
1634 assert!(admission.acquire(0).is_ok());
1635 }
1636}