🔒 Repository is read-only – file editing is disabled.

PaganDE/pagan-compositor/src/screencopy.rs main

370 linii Raw ← Powrót
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(())
}