🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
//! `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<Output>,
/// Region w układzie lokalnym wyjścia (None = całe wyjście).
pub region: Option<Rectangle<i32, Logical>>,
/// 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<Output>,
pub region: Option<Rectangle<i32, Logical>>,
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::<MyCompositor, ZwlrScreencopyManagerV1, ()>(3, ());
tracing::debug!("global zwlr_screencopy_manager_v1 zarejestrowany");
}
// --- Manager: obsługa `capture_output` / `capture_output_region` --------------
impl GlobalDispatch<ZwlrScreencopyManagerV1, (), MyCompositor> for MyCompositor {
fn bind(
_state: &mut MyCompositor,
_handle: &DisplayHandle,
_client: &Client,
resource: New<ZwlrScreencopyManagerV1>,
_data: &(),
data_init: &mut DataInit<'_, MyCompositor>,
) {
data_init.init(resource, ());
}
}
impl Dispatch<ZwlrScreencopyManagerV1, (), MyCompositor> 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<ZwlrScreencopyFrameV1, FrameUserData, MyCompositor> 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<PaganWindow>,
pointer_location: Point<f64, Logical>,
queue: &mut Vec<PendingCapture>,
) {
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<PaganWindow>,
pointer_location: Point<f64, Logical>,
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(())
}