//! `wlr-screencopy` — implementacja serwera od zera. //! //! PO CO: Smithay 0.7 nie ma screencopy (ani `ext-image-copy-capture`). Bez tego //! nie działają narzędzia typu `grim` (zrzuty ekranu) czy `wf-recorder`. Piszemy //! więc logikę serwera sami; definicje protokołu pochodzą z `wayland-protocols-wlr` //! (wygenerowany „kabel"), a cała semantyka — przechwycenie i skopiowanie treści //! wyjścia do bufora klienta — jest tutaj. //! //! Jak to działa (protokół `zwlr_screencopy_manager_v1`): //! 1. klient woła `capture_output`/`capture_output_region` → tworzymy obiekt ramki, //! 2. wysyłamy `buffer(...)` (format/rozmiar/stride bufora, jakiego oczekujemy) //! oraz `buffer_done()`, //! 3. klient tworzy `wl_buffer` i woła `copy(buffer)` (albo `copy_with_damage`), //! 4. renderujemy treść wyjścia do tekstury offscreen, odczytujemy piksele do CPU //! i kopiujemy je do pamięci bufora klienta, po czym wysyłamy `ready(...)`, //! a przy błędzie `failed()`. //! //! WAŻNE OGRANICZENIE KOLEJNOŚCI: przechwycenie wymaga renderera, a renderer żyje //! w pętli renderowania (winit) / w stanie backendu udev — nie w obsłudze protokołu. //! Dlatego żądania `copy` trafiają do kolejki (`screencopy_queue`) i są realizowane //! w pętli renderowania (`process`). Dzięki temu protokół nie blokuje dispatchu. //! //! Obsługujemy bufory **SHM** (to, czego używa `grim`). Dla buforów dmabuf nie //! wysyłamy zdarzenia `linux_dmabuf`, więc klient wybierze SHM; gdyby jednak //! przysłał dmabuf, dostanie `failed()`. use std::time::{SystemTime, UNIX_EPOCH}; use tracing::trace; use smithay::{ backend::{ allocator::Fourcc, renderer::{ damage::OutputDamageTracker, gles::{GlesRenderer, GlesTexture}, Bind, ExportMem, Offscreen, TextureMapping, }, }, output::Output, reexports::{ wayland_protocols_wlr::screencopy::v1::server::{ zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1}, zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1}, }, wayland_server::{ protocol::{wl_buffer::WlBuffer, wl_shm::Format as ShmFormat}, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, }, }, utils::{Logical, Point, Rectangle, Size, Transform}, wayland::shm::with_buffer_contents_mut, }; use crate::{render, state::MyCompositor, window::PaganWindow}; /// User-data obiektu ramki: co i jak przechwycić. #[derive(Debug, Clone)] pub struct FrameUserData { /// Wyjście do przechwycenia (`None`, gdy zniknęło). pub output: Option, /// Region w układzie lokalnym wyjścia (None = całe wyjście). pub region: Option>, /// Czy dorysować kursor. pub overlay_cursor: bool, } /// Żądanie `copy` zakolejkowane do wykonania w pętli renderowania. pub struct PendingCapture { pub frame: ZwlrScreencopyFrameV1, pub output: Option, pub region: Option>, pub overlay_cursor: bool, pub buffer: WlBuffer, } /// Rejestruje global `zwlr_screencopy_manager_v1`. /// /// PO CO wersja 3: to maksimum obsługiwane przez `wayland-protocols-wlr 0.3` /// (definicje protokołu). Zawiera `buffer_done` (v2) oraz zdarzenie /// `linux_dmabuf` (v3) — tego drugiego nie wysyłamy, bo obsługujemy SHM, /// ale klient sam wybierze SHM, gdy nie zobaczy `linux_dmabuf`. pub fn init(display: &DisplayHandle) { display.create_global::(3, ()); tracing::debug!("global zwlr_screencopy_manager_v1 zarejestrowany"); } // --- Manager: obsługa `capture_output` / `capture_output_region` -------------- impl GlobalDispatch for MyCompositor { fn bind( _state: &mut MyCompositor, _handle: &DisplayHandle, _client: &Client, resource: New, _data: &(), data_init: &mut DataInit<'_, MyCompositor>, ) { data_init.init(resource, ()); } } impl Dispatch for MyCompositor { fn request( state: &mut MyCompositor, _client: &Client, _manager: &ZwlrScreencopyManagerV1, request: zwlr_screencopy_manager_v1::Request, _data: &(), _handle: &DisplayHandle, data_init: &mut DataInit<'_, MyCompositor>, ) { match request { zwlr_screencopy_manager_v1::Request::CaptureOutput { overlay_cursor: cursor_overlay, output, frame, } => { let output = Output::from_resource(&output); let user_data = FrameUserData { output: output.clone(), region: None, overlay_cursor: cursor_overlay != 0, }; let resource = data_init.init(frame, user_data); // Informujemy klienta, jakiego bufora oczekujemy (SHM XRGB8888). if let Some((width, height)) = output_size(state, output.as_ref()) { send_buffer_event(&resource, width, height); } else { resource.failed(); } } zwlr_screencopy_manager_v1::Request::CaptureOutputRegion { overlay_cursor: cursor_overlay, output, x, y, width, height, frame, } => { let output = Output::from_resource(&output); let region = Rectangle::new(Point::from((x, y)), Size::from((width, height))); let user_data = FrameUserData { output: output.clone(), region: Some(region), overlay_cursor: cursor_overlay != 0, }; let resource = data_init.init(frame, user_data); // Bufor klienta zawsze ma rozmiar regionu. if width > 0 && height > 0 { send_buffer_event(&resource, width, height); } else { resource.failed(); } } zwlr_screencopy_manager_v1::Request::Destroy => {} _ => {} } } } /// Rozmiar wyjścia w pikselach fizycznych (z bieżącego trybu). fn output_size(_state: &MyCompositor, output: Option<&Output>) -> Option<(i32, i32)> { let mode = output?.current_mode()?; Some((mode.size.w, mode.size.h)) } /// Wysyła `buffer(...)` + `buffer_done()` — klient tworzy pasujący `wl_buffer`. fn send_buffer_event(frame: &ZwlrScreencopyFrameV1, width: i32, height: i32) { let stride = width * 4; // API-NOTE: pierwszy argument to `wl_shm::Format`, nie surowy `u32`. frame.buffer( ShmFormat::Xrgb8888, width as u32, height as u32, stride as u32, ); // `buffer_done` istnieje dopiero od wersji 3. Klient może zbindować niższą // wersję (np. grim binduje v2) — wysłanie wtedy zdarzenia nr 6 zerwałoby // połączenie. Bezpieczeństwo wersjonowania jest obowiązkiem serwera. if frame.version() >= 3 { frame.buffer_done(); } } // --- Ramka: obsługa `copy` / `copy_with_damage` ------------------------------ impl Dispatch for MyCompositor { fn request( state: &mut MyCompositor, _client: &Client, frame: &ZwlrScreencopyFrameV1, request: zwlr_screencopy_frame_v1::Request, data: &FrameUserData, _handle: &DisplayHandle, _data_init: &mut DataInit<'_, MyCompositor>, ) { match request { zwlr_screencopy_frame_v1::Request::Copy { buffer } | zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => { // Nie renderujemy tutaj — nie mamy dostępu do renderera. Kolejkujemy. state.screencopy_queue.push(PendingCapture { frame: frame.clone(), output: data.output.clone(), region: data.region, overlay_cursor: data.overlay_cursor, buffer, }); } zwlr_screencopy_frame_v1::Request::Destroy => {} _ => {} } } } // --- Wykonanie przechwycenia (wołane z pętli renderowania) ------------------- /// Przetwarza zakolejkowane przechwycenia, używając podanego renderera. /// /// Funkcja wolna (nie metoda), żeby dało się ją wywołać z rozłącznymi pożyczkami: /// `renderer` (z backendu), `space` (stan okien) i `queue` (kolejka) należą do /// różnych pól i nie kolidują ze sobą. pub fn process( renderer: &mut GlesRenderer, space: &smithay::desktop::Space, pointer_location: Point, queue: &mut Vec, ) { if queue.is_empty() { return; } let pending = std::mem::take(queue); for request in pending { match capture(renderer, space, pointer_location, &request) { Ok(()) => { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default(); let secs = now.as_secs(); request.frame.ready( (secs >> 32) as u32, (secs & 0xffff_ffff) as u32, now.subsec_nanos(), ); } Err(err) => { trace!(?err, "przechwycenie nie powiodło się"); request.frame.failed(); } } } } /// Przechwytuje treść wyjścia i kopiuje ją do bufora klienta. fn capture( renderer: &mut GlesRenderer, space: &smithay::desktop::Space, pointer_location: Point, request: &PendingCapture, ) -> Result<(), &'static str> { let output = request.output.as_ref().ok_or("wyjście zniknęło")?; let mode = output.current_mode().ok_or("wyjście bez trybu")?; let size = mode.size; if size.w <= 0 || size.h <= 0 { return Err("niepoprawny rozmiar wyjścia"); } // Tekstura offscreen, do której renderujemy treść wyjścia. let mut texture: GlesTexture = renderer .create_buffer(Fourcc::Argb8888, Size::from((size.w, size.h))) .map_err(|_| "nie udało się utworzyć bufora offscreen")?; // Framebuffer żyje krótko — render i odczyt muszą zmieścić się w tym bloku. let target = renderer .bind(&mut texture) .map_err(|_| "nie udało się zbindować bufora offscreen")?; // Elementy do narysowania: opcjonalnie kursor na wierzchu. let mut overlay_pointer = if request.overlay_cursor { let mut pointer = render::PointerElement::default(); pointer.update(render::pointer_physical(pointer_location), true); Some(pointer) } else { None }; // Ikony DnD i ducha snapu nie rysujemy w zrzutach — to stan przejściowy interakcji. // Ikony DnD i ducha snapu nie rysujemy w zrzutach — to stan przejściowy interakcji. let (elements, clear_color) = render::output_elements( space, output, overlay_pointer.as_mut(), None, None, pointer_location, renderer, ); // `size` pochodzi z trybu wyjścia, więc jest już w pikselach fizycznych // (skala 1, brak obrotu) — nie ma czego przeliczać. let physical_size = size; let mut tracker = OutputDamageTracker::new(physical_size, 1.0, Transform::Normal); // `target` jest `mut`, choć nie mutujemy go wprost — render_output potrzebuje &mut. let mut target = target; tracker .render_output(renderer, &mut target, 0, &elements, clear_color) .map_err(|_| "błąd renderowania offscreen")?; // Region od klienta jest w układzie logicznym, a `copy_framebuffer` wymaga // współrzędnych BUFORA. Skala 1 i brak transformacji oznaczają, że wartości // są identyczne — zgadzać musi się tylko typ. let region = request .region .map(|region| region.to_buffer(1, Transform::Normal, &physical_size.to_logical(1))) .unwrap_or_else(|| { // Fallback: cały ekran. `to_buffer` istnieje tylko dla współrzędnych // logicznych, więc najpierw przeliczamy fizyczne na logiczne (skala 1). Rectangle::from_size(physical_size.to_logical(1).to_buffer(1, Transform::Normal)) }); let mapping = renderer .copy_framebuffer(&target, region, Fourcc::Xrgb8888) .map_err(|_| "nie udało się odczytać pikseli")?; // GL `glReadPixels` czyta od dołu, więc mapowanie zwykle mówi `flipped = true`. let flipped = mapping.flipped(); let source: &[u8] = renderer .map_texture(&mapping) .map_err(|_| "nie udało się zmapować odczytanych pikseli")?; // Kopiujemy wiersz po wierszu, szanując stride bufora klienta. let source_stride = (region.size.w.max(0) as usize) * 4; let rows = region.size.h.max(0) as usize; let copied = with_buffer_contents_mut(&request.buffer, |ptr, len, data| { let destination_stride = data.stride.max(0) as usize; for row in 0..rows { // Jeśli mapowanie jest odwrócone (GL czyta od dołu), bierzemy wiersze // źródła w odwrotnej kolejności — inaczej zrzut wyszedłby do góry nogami. let source_row = if flipped { rows - 1 - row } else { row }; let source_offset = source_row * source_stride; let destination_offset = row * destination_stride; if source_offset + source_stride > source.len() || destination_offset + source_stride > len { break; } // Bezpieczeństwo: zakresy sprawdzone powyżej; `ptr` pochodzi z mapowania SHM. unsafe { std::ptr::copy_nonoverlapping( source[source_offset..source_offset + source_stride].as_ptr(), ptr.add(destination_offset), source_stride, ); } } true }) .map_err(|_| "brak dostępu do bufora klienta (nie-SHM?)")?; if !copied { return Err("nie udało się skopiować pikseli"); } Ok(()) }