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

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

813 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813
//! Backend produkcyjny: **tty-udev** (DRM/KMS + EGL/GBM + libinput + libseat).
//!
//! PO CO: to ścieżka "prawdziwego" środowiska graficznego — kompozytor przejmuje
//! konsolę (TTY), sam ustawia tryby wyświetlania przez DRM/KMS i czyta wejście
//! bezpośrednio z libinput, z uprawnieniami zarządzanymi przez libseat (logind
//! albo seatd). W przeciwieństwie do winit nie działa "w oknie".
//!
//! Moduł jest włączany feature'em `udev` (domyślnie wyłączonym), bo wymaga
//! bibliotek systemowych: libdrm, libgbm, libinput, libudev, libseat.
//!
//! ⚠️ STATUS: kod napisany według sprawdzonych wzorców z `smithay-0.7.0/anvil`
//! (`run_udev`, `device_added`, `connector_connected`, pętla VBlank), ale NIE był
//! kompilowany ani uruchamiany na sprzęcie. Pierwsze uruchomienie wymaga TTY
//! (nie sesji graficznej) i zwykle uprawnień: użyj logind/libseat, albo uruchom
//! z innego TTY jako root. Miejsca szczególnie warte weryfikacji oznaczono
//! komentarzami `// WERYFIKUJ:`.

use std::{
    collections::HashMap,
    path::{Path, PathBuf},
    sync::atomic::Ordering,
    time::Duration,
};

use rustix::fs::OFlags;
use tracing::{error, info, warn};

use smithay::{
    backend::{
        allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
        drm::{
            compositor::FrameFlags,
            exporter::gbm::GbmFramebufferExporter,
            output::{DrmOutput, DrmOutputManager, DrmOutputRenderElements},
            DrmDevice, DrmDeviceFd, DrmEvent, DrmNode, NodeType,
        },
        egl::{context::ContextPriority, EGLDevice, EGLDisplay},
        input::{InputBackend, InputEvent},
        libinput::{LibinputInputBackend, LibinputSessionInterface},
        renderer::{
            gles::GlesRenderer,
            multigpu::{gbm::GbmGlesBackend, GpuManager, MultiRenderer},
            ImportDma, ImportMemWl,
        },
        session::{libseat::LibSeatSession, Event as SessionEvent, Session},
        udev::{primary_gpu, UdevBackend, UdevEvent},
    },
    output::{Mode as WlMode, Output, PhysicalProperties, Subpixel},
    reexports::{
        calloop::{EventLoop, RegistrationToken},
        drm::control::{connector, crtc, ModeTypeFlags},
        input::Libinput,
        wayland_server::{backend::GlobalId, Display},
    },
    utils::{Clock, DeviceFd, Monotonic, Point, Transform},
};
// `DrmScanner` przeniesiono z Smithay do osobnego crate'a `smithay-drm-extras`.
use smithay_drm_extras::drm_scanner::{DrmScanEvent, DrmScanner};

use crate::{render, state::MyCompositor};

/// Renderer łączący API renderujący z API alokującym (multi-GPU).
#[allow(dead_code)]
type UdevRenderer<'a> = MultiRenderer<
    'a,
    'a,
    GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
    GbmGlesBackend<GlesRenderer, DrmDeviceFd>,
>;

/// Konkretny typ kompozytora DRM/GBM (skrót dokumentacyjny).
#[allow(dead_code)]
type GbmDrmCompositor = smithay::backend::drm::compositor::DrmCompositor<
    GbmAllocator<DrmDeviceFd>,
    GbmFramebufferExporter<DrmDeviceFd>,
    (),
    DrmDeviceFd,
>;

/// Identyfikuje wyjście po (urządzenie DRM, CRTC) — wkładane do `user_data` Output.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UdevOutputId {
    pub device_id: DrmNode,
    pub crtc: crtc::Handle,
}

/// Stan backendu tty-udev. Trzymany w `MyCompositor::udev`.
pub struct UdevState {
    /// Uchwyt sesji (libseat) — wspólny dla DRM i libinput.
    pub session: LibSeatSession,
    /// Nazwa seatu (np. "seat0") — do `udev_assign_seat`.
    pub seat_name: String,
    /// Preferowane GPU (z niego bierzemy renderer domyślny).
    pub primary_gpu: DrmNode,
    /// Menedżer rendererów per GPU.
    pub gpus: GpuManager<GbmGlesBackend<GlesRenderer, DrmDeviceFd>>,
    /// Urządzenia DRM (klucz: node), każde z własnym menedżerem wyjść.
    pub backends: HashMap<DrmNode, UdevDevice>,
    /// Kursor rysowany przez kompozytor (wspólny dla wszystkich wyjść).
    pub pointer_element: render::PointerElement,
    /// Zegar monotoniczny (m.in. do znaczników czasu klatek).
    pub clock: Clock<Monotonic>,
}

/// Dane jednego urządzenia DRM.
pub struct UdevDevice {
    pub drm_output_manager: DrmOutputManager<
        GbmAllocator<DrmDeviceFd>,
        GbmFramebufferExporter<DrmDeviceFd>,
        (),
        DrmDeviceFd,
    >,
    pub drm_scanner: DrmScanner,
    pub render_node: Option<DrmNode>,
    pub surfaces: HashMap<crtc::Handle, UdevSurfaceData>,
    /// Uchwyt rejestracji notifiera DRM w pętli — trzymany, żeby żył.
    pub registration_token: RegistrationToken,
}

/// Dane pojedynczego wyjścia (CRTC) na urządzeniu DRM.
pub struct UdevSurfaceData {
    pub output: Output,
    pub drm_output:
        DrmOutput<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>,
    pub global: Option<GlobalId>,
}

/// Punkt wejścia backendu tty-udev.
pub fn run_udev() -> anyhow::Result<()> {
    let mut event_loop = EventLoop::<MyCompositor>::try_new()?;
    let display = Display::<MyCompositor>::new()?;

    // 1) Sesja przez libseat — daje nam uprawnienia do DRM, VT i urządzeń wejścia.
    let (session, notifier) = LibSeatSession::new()
        .map_err(|err| anyhow::anyhow!("nie udało się otworzyć sesji libseat: {err}"))?;
    let seat_name = session.seat();

    // 2) Wybór GPU. Preferujemy jawny override przez PAGAN_DRM_DEVICE.
    let primary_gpu = if let Ok(var) = std::env::var("PAGAN_DRM_DEVICE") {
        DrmNode::from_path(var).map_err(|err| anyhow::anyhow!("PAGAN_DRM_DEVICE: {err}"))?
    } else {
        primary_gpu(&seat_name)?
            .and_then(|path| {
                DrmNode::from_path(path)
                    .ok()?
                    .node_with_type(NodeType::Render)?
                    .ok()
            })
            .ok_or_else(|| {
                anyhow::anyhow!("nie znaleziono żadnego GPU (searched seat {seat_name})")
            })?
    };
    info!(%primary_gpu, "PaganDE: wybrano GPU");

    // 3) Menedżer rendererów (openGL przez glow, kontekst o wysokim priorytecie).
    let mut gpus = GpuManager::new(GbmGlesBackend::with_context_priority(ContextPriority::High))
        .map_err(|err| anyhow::anyhow!("GpuManager: {err:?}"))?;

    // Formaty SHM/dmabuf bierzemy z renderera ZANIM `gpus` trafi do stanu.
    let (shm_formats, dmabuf_formats) = {
        let renderer = gpus
            .single_renderer(&primary_gpu)
            .map_err(|err| anyhow::anyhow!("renderer: {err:?}"))?;
        (renderer.shm_formats(), renderer.dmabuf_formats())
    };

    // 4) Stan protokołów kompozytora (bez wyjścia — dodamy je przy device_added).
    let mut state = MyCompositor::init(display, event_loop.handle(), true);
    state.shm_state.update_formats(shm_formats);
    state.enable_dmabuf(dmabuf_formats);

    // XWayland (aplikacje X11).
    state.start_xwayland();
    state.attach_udev(UdevState {
        session: session.clone(),
        seat_name: seat_name.clone(),
        primary_gpu,
        gpus,
        backends: HashMap::new(),
        pointer_element: render::PointerElement::default(),
        clock: Clock::new(),
    });

    // 5) libinput — wejście. Podłączony do sesji, więc automatycznie respektuje
    //    pauzę/aktywację VT.
    let mut libinput_context =
        Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
    libinput_context
        .udev_assign_seat(&seat_name)
        .map_err(|_| anyhow::anyhow!("libinput: nie udało się przypisać seatu {seat_name}"))?;
    let libinput_backend = LibinputInputBackend::new(libinput_context.clone());

    event_loop
        .handle()
        .insert_source(
            libinput_backend,
            move |event, _, data: &mut MyCompositor| {
                data.process_input_event(event);
            },
        )
        .map_err(|err| anyhow::anyhow!("rejestracja libinput: {err}"))?;

    // 6) Pauza/wznowienie sesji (przełączanie VT, suspend).
    {
        let mut libinput_context = libinput_context.clone();
        event_loop
            .handle()
            .insert_source(
                notifier,
                move |event, &mut (), data: &mut MyCompositor| match event {
                    SessionEvent::PauseSession => {
                        libinput_context.suspend();
                        info!("sesja wstrzymana — pauzuję wyjścia DRM");
                        if let Some(udev) = data.udev.as_mut() {
                            for device in udev.backends.values_mut() {
                                device.drm_output_manager.pause();
                            }
                        }
                    }
                    SessionEvent::ActivateSession => {
                        info!("sesja wznowiona — aktywuję wyjścia DRM");
                        if let Err(err) = libinput_context.resume() {
                            error!(?err, "libinput: nie udało się wznowić");
                        }
                        // WERYFIKUJ: `activate(false)` zachowuje stan connectorów; gdy
                        // sterownik tego nie lubi, zmień na `activate(true)`.
                        let nodes: Vec<DrmNode> = data
                            .udev
                            .as_ref()
                            .map(|udev| udev.backends.keys().copied().collect())
                            .unwrap_or_default();
                        for node in nodes {
                            if let Some(udev) = data.udev.as_mut() {
                                if let Some(device) = udev.backends.get_mut(&node) {
                                    if let Err(err) = device.drm_output_manager.activate(false) {
                                        error!(?err, %node, "nie udało się aktywować DRM");
                                    }
                                }
                            }
                            data.handle
                                .insert_idle(move |state| state.udev_render(node));
                        }
                    }
                },
            )
            .map_err(|err| anyhow::anyhow!("rejestracja notifiera sesji: {err}"))?;
    }

    // 7) Udev — wykrywanie urządzeń DRM i hotplug.
    let udev_backend =
        UdevBackend::new(&seat_name).map_err(|err| anyhow::anyhow!("UdevBackend: {err}"))?;

    // Najpierw dodajemy urządzenia już obecne (żeby zdążyły zainicjalizować GPU).
    let existing: Vec<(u64, PathBuf)> = udev_backend
        .device_list()
        .map(|(device_id, path)| (device_id as u64, path.to_path_buf()))
        .collect();
    for (device_id, path) in existing {
        if let Ok(node) = DrmNode::from_dev_id(device_id as _) {
            if let Err(err) = state.udev_device_added(node, &path) {
                warn!(%node, ?err, "pomijam urządzenie DRM");
            }
        }
    }

    event_loop
        .handle()
        .insert_source(
            udev_backend,
            move |event, _, data: &mut MyCompositor| match event {
                UdevEvent::Added { device_id, path } => {
                    if let Ok(node) = DrmNode::from_dev_id(device_id) {
                        if let Err(err) = data.udev_device_added(node, &path) {
                            warn!(%node, ?err, "nie udało się dodać urządzenia DRM");
                        }
                    }
                }
                UdevEvent::Changed { device_id } => {
                    if let Ok(node) = DrmNode::from_dev_id(device_id) {
                        data.udev_device_changed(node);
                    }
                }
                UdevEvent::Removed { device_id } => {
                    if let Ok(node) = DrmNode::from_dev_id(device_id) {
                        data.udev_device_removed(node);
                    }
                }
            },
        )
        .map_err(|err| anyhow::anyhow!("rejestracja UdevBackend: {err}"))?;

    info!("PaganDE (tty-udev) wystartował. Zakończ Ctrl+C lub przełącz VT.");

    // 8) Pętla główna. Rendering jest sterowany zdarzeniami (VBlank → render).
    while state.running.load(Ordering::SeqCst) {
        if let Err(err) = event_loop.dispatch(Some(Duration::from_millis(16)), &mut state) {
            error!(?err, "błąd pętli zdarzeń");
            state.running.store(false, Ordering::SeqCst);
            continue;
        }
        state.space.refresh();
        // Lista okien dla paneli (tytuły, stan aktywny/zminimalizowany).
        state.sync_foreign_toplevels();
        state.popups.cleanup();
        let _ = state.display_handle.flush_clients();
    }

    Ok(())
}

/// Błąd dodawania urządzenia DRM.
#[derive(Debug)]
pub enum DeviceAddError {
    DeviceOpen(String),
    DrmDevice(smithay::backend::drm::DrmError),
    GbmDevice(std::io::Error),
    AddNode(String),
}

impl std::fmt::Display for DeviceAddError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DeviceAddError::DeviceOpen(err) => write!(f, "otwarcie urządzenia: {err}"),
            DeviceAddError::DrmDevice(err) => write!(f, "DrmDevice: {err:?}"),
            DeviceAddError::GbmDevice(err) => write!(f, "GbmDevice: {err}"),
            DeviceAddError::AddNode(err) => write!(f, "dodanie node'a GPU: {err}"),
        }
    }
}

impl std::error::Error for DeviceAddError {}

impl MyCompositor {
    /// Inicjalizuje urządzenie DRM: otwiera je, tworzy GBM, alokator, exporter,
    /// menedżer wyjść oraz rejestruje notifier VBlank.
    pub fn udev_device_added(&mut self, node: DrmNode, path: &Path) -> Result<(), DeviceAddError> {
        let (mut session, primary_gpu) = match self.udev.as_ref() {
            Some(udev) => (udev.session.clone(), udev.primary_gpu),
            None => return Err(DeviceAddError::AddNode("brak stanu udev".into())),
        };

        // Otwarcie urządzenia przez sesję (nadaje uprawnienia DRM master).
        let fd = session
            .open(
                path,
                OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
            )
            .map_err(|err| DeviceAddError::DeviceOpen(format!("{err:?}")))?;
        let fd = DrmDeviceFd::new(DeviceFd::from(fd));

        let (drm, notifier) =
            DrmDevice::new(fd.clone(), true).map_err(DeviceAddError::DrmDevice)?;
        let gbm = GbmDevice::new(fd).map_err(DeviceAddError::GbmDevice)?;

        // Notifier VBlank: po każdym wyświetleniu klatki planujemy kolejny render.
        let registration_token = self
            .handle
            .insert_source(
                notifier,
                move |event, _metadata, data: &mut MyCompositor| match event {
                    DrmEvent::VBlank(crtc) => data.udev_frame_finish(node, crtc),
                    DrmEvent::Error(err) => error!(?err, "błąd DRM"),
                },
            )
            .map_err(|err| DeviceAddError::AddNode(err.to_string()))?;

        // Zbudowanie kontekstu EGL dla tego urządzenia i rejestracja renderera.
        // WERYFIKUJ: `EGLDisplay::new` jest `unsafe` (surowe uchwyty EGL).
        let display = unsafe {
            EGLDisplay::new(gbm.clone()).map_err(|err| DeviceAddError::AddNode(err.to_string()))?
        };
        let egl_device = EGLDevice::device_for_display(&display)
            .map_err(|err| DeviceAddError::AddNode(err.to_string()))?;
        let render_node = egl_device
            .try_get_render_node()
            .ok()
            .flatten()
            .unwrap_or(node);

        {
            let udev = self
                .udev
                .as_mut()
                .ok_or(DeviceAddError::AddNode("brak stanu udev".into()))?;
            udev.gpus
                .as_mut()
                .add_node(render_node, gbm.clone())
                .map_err(|err| DeviceAddError::AddNode(err.to_string()))?;
        }

        let allocator = GbmAllocator::new(
            gbm.clone(),
            GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT,
        );
        let framebuffer_exporter = GbmFramebufferExporter::new(gbm.clone(), Some(render_node));

        // Zbiór formatów renderowania wspieranych przez EGL (do alokacji buforów).
        let render_formats = {
            let udev = self
                .udev
                .as_mut()
                .ok_or(DeviceAddError::AddNode("brak stanu udev".into()))?;
            let mut renderer = udev
                .gpus
                .single_renderer(&render_node)
                .map_err(|err| DeviceAddError::AddNode(format!("{err:?}")))?;
            renderer
                .as_mut()
                .egl_context()
                .dmabuf_render_formats()
                .iter()
                .copied()
                .collect::<Vec<_>>()
        };

        let drm_output_manager = DrmOutputManager::new(
            drm,
            allocator,
            framebuffer_exporter,
            Some(gbm),
            [
                smithay::backend::allocator::Fourcc::Argb8888,
                smithay::backend::allocator::Fourcc::Xrgb8888,
            ],
            render_formats,
        );

        let udev = self
            .udev
            .as_mut()
            .ok_or(DeviceAddError::AddNode("brak stanu udev".into()))?;
        udev.backends.insert(
            node,
            UdevDevice {
                drm_output_manager,
                drm_scanner: DrmScanner::new(),
                render_node: Some(render_node),
                surfaces: HashMap::new(),
                registration_token,
            },
        );
        let _ = primary_gpu;

        // Skanujemy już podłączone monitory.
        self.udev_device_changed(node);
        Ok(())
    }

    /// Reakcja na zmianę urządzenia: skanuje konektory i dodaje/usuwa wyjścia.
    pub fn udev_device_changed(&mut self, node: DrmNode) {
        let scan_result = {
            let Some(udev) = self.udev.as_mut() else {
                return;
            };
            let Some(device) = udev.backends.get_mut(&node) else {
                return;
            };
            match device
                .drm_scanner
                .scan_connectors(device.drm_output_manager.device())
            {
                Ok(result) => result,
                Err(err) => {
                    warn!(?err, %node, "skanowanie konektorów nie powiodło się");
                    return;
                }
            }
        };

        for event in scan_result {
            match event {
                DrmScanEvent::Connected {
                    connector,
                    crtc: Some(crtc),
                } => {
                    self.udev_connector_connected(node, connector, crtc);
                }
                DrmScanEvent::Disconnected {
                    connector: _,
                    crtc: Some(crtc),
                } => {
                    self.udev_connector_disconnected(node, crtc);
                }
                _ => {}
            }
        }
    }

    /// Tworzy wyjście Waylanda i kompozytor DRM dla podłączonego konektora.
    pub fn udev_connector_connected(
        &mut self,
        node: DrmNode,
        connector: connector::Info,
        crtc: crtc::Handle,
    ) {
        let output_name = format!(
            "{}-{}",
            connector.interface().as_str(),
            connector.interface_id()
        );

        // Wybierz tryb preferowany (albo pierwszy dostępny).
        let mode_id = connector
            .modes()
            .iter()
            .position(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED))
            .unwrap_or(0);
        let Some(drm_mode) = connector.modes().get(mode_id).copied() else {
            warn!(%output_name, "konektor bez trybów");
            return;
        };
        let wl_mode = WlMode::from(drm_mode);

        // Układ: nowe wyjście na prawo od już istniejących.
        let x = self.space.outputs().fold(0, |acc, output| {
            acc + self
                .space
                .output_geometry(output)
                .map(|g| g.size.w)
                .unwrap_or(0)
        });
        let position = Point::from((x, 0));

        let output = Output::new(
            output_name.clone(),
            PhysicalProperties {
                // Fizyczny rozmiar panelu (mm) — informacyjny dla klientów.
                size: connector
                    .size()
                    .map(|(w, h)| (w as i32, h as i32))
                    .unwrap_or((0, 0))
                    .into(),
                subpixel: Subpixel::Unknown,
                make: "PaganDE".into(),
                model: "DRM".into(),
            },
        );
        let global = output.create_global::<MyCompositor>(&self.display_handle);
        output.set_preferred(wl_mode);
        output.change_current_state(Some(wl_mode), Some(Transform::Normal), None, Some(position));
        self.space.map_output(&output, position);
        output.user_data().insert_if_missing(|| UdevOutputId {
            device_id: node,
            crtc,
        });

        // Ustawiamy wszystkie wyjścia poziomo (bez przerw) i ratujemy okna,
        // jeśli nowy monitor zmienił układ.
        self.relayout_outputs();

        // Renderer dla tego GPU.
        let Some(udev) = self.udev.as_mut() else {
            return;
        };
        let render_node = udev
            .backends
            .get(&node)
            .and_then(|device| device.render_node)
            .unwrap_or(udev.primary_gpu);
        let mut renderer = match udev.gpus.single_renderer(&render_node) {
            Ok(renderer) => renderer,
            Err(err) => {
                warn!(?err, "brak renderera dla wyjścia");
                return;
            }
        };

        let Some(device) = udev.backends.get_mut(&node) else {
            return;
        };
        let planes = device.drm_output_manager.device().planes(&crtc).ok();

        let drm_output = match device
            .drm_output_manager
            .initialize_output::<GlesRenderer, render::OutputRenderElements>(
                crtc,
                drm_mode,
                &[connector.handle()],
                &output,
                planes,
                renderer.as_mut(),
                &DrmOutputRenderElements::default(),
            ) {
            Ok(drm_output) => drm_output,
            Err(err) => {
                warn!(?err, %output_name, "nie udało się zainicjalizować wyjścia DRM");
                return;
            }
        };

        device.surfaces.insert(
            crtc,
            UdevSurfaceData {
                output: output.clone(),
                drm_output,
                global: Some(global),
            },
        );

        info!(%output_name, ?crtc, "skonfigurowano wyjście");

        // Pierwsza klatka.
        self.handle
            .insert_idle(move |state| state.udev_render(node));
    }

    /// Usuwa wyjście dla odłączonego konektora.
    pub fn udev_connector_disconnected(&mut self, node: DrmNode, crtc: crtc::Handle) {
        // Zdejmujemy pożyczkę na `self.udev` w bloku, żeby potem móc wywołać
        // `relayout_outputs` (metoda przyjmująca całe `&mut self`).
        let removed = {
            let Some(udev) = self.udev.as_mut() else {
                return;
            };
            let Some(device) = udev.backends.get_mut(&node) else {
                return;
            };
            device.surfaces.remove(&crtc)
        };
        let Some(surface) = removed else { return };

        if let Some(global) = surface.global.as_ref() {
            self.display_handle
                .remove_global::<MyCompositor>(global.clone());
        }
        self.space.unmap_output(&surface.output);
        info!(?crtc, "usunięto wyjście");

        // Re-layout bez przerwy i przeniesienie okien z usuniętego ekranu.
        self.relayout_outputs();
    }

    /// Usuwa całe urządzenie DRM (odłączone/utracone).
    pub fn udev_device_removed(&mut self, node: DrmNode) {
        let removed = {
            let Some(udev) = self.udev.as_mut() else {
                return;
            };
            udev.backends.remove(&node)
        };
        let Some(device) = removed else { return };

        for surface in device.surfaces.values() {
            if let Some(global) = surface.global.as_ref() {
                self.display_handle
                    .remove_global::<MyCompositor>(global.clone());
            }
            self.space.unmap_output(&surface.output);
        }
        info!(%node, "usunięto urządzenie DRM");

        self.relayout_outputs();
    }

    /// Renderuje i kolejkuje klatkę na wszystkich wyjściach danego urządzenia.
    pub fn udev_render(&mut self, node: DrmNode) {
        let crtcs: Vec<crtc::Handle> = self
            .udev
            .as_ref()
            .and_then(|udev| udev.backends.get(&node))
            .map(|device| device.surfaces.keys().copied().collect())
            .unwrap_or_default();

        for crtc in crtcs {
            self.udev_render_surface(node, crtc);
        }
    }

    /// Renderuje jedną klatkę wyjścia i — jeśli coś się zmieniło — kolejkuje ją.
    pub fn udev_render_surface(&mut self, node: DrmNode, crtc: crtc::Handle) {
        let Some(output) = self.udev_output(node, crtc) else {
            return;
        };

        let render_node = {
            let Some(udev) = self.udev.as_ref() else {
                return;
            };
            udev.backends
                .get(&node)
                .and_then(|device| device.render_node)
                .unwrap_or(udev.primary_gpu)
        };

        let pointer_location = self.pointer_location;
        let cursor_status = self.cursor_status.clone();

        let Some(udev) = self.udev.as_mut() else {
            return;
        };

        // Kursor WYJMUJEMY na czas renderu: `render_elements` potrzebuje `&mut`,
        // a jednocześnie pożyczamy `udev.gpus` i `udev.backends` — wyjęcie elementu
        // rozplątuje te pożyczki bez walki z borrow checkerem.
        let mut pointer_element = std::mem::take(&mut udev.pointer_element);
        pointer_element.update(render::pointer_physical(pointer_location), true);
        pointer_element.set_status(cursor_status);

        let mut renderer = match udev.gpus.single_renderer(&render_node) {
            Ok(renderer) => renderer,
            Err(err) => {
                warn!(?err, "brak renderera podczas renderowania");
                udev.pointer_element = pointer_element;
                return;
            }
        };

        let Some(device) = udev.backends.get_mut(&node) else {
            udev.pointer_element = pointer_element;
            return;
        };
        let Some(surface) = device.surfaces.get_mut(&crtc) else {
            udev.pointer_element = pointer_element;
            return;
        };

        // Nasz renderer jest zbudowany na GlesRenderer (pixel shadery paska SSD), a
        // `MultiRenderer` udostępnia wewnętrzny renderer przez `AsMut`.
        let gles: &mut GlesRenderer = renderer.as_mut();
        let (elements, clear_color) = render::output_elements(
            &self.space,
            &output,
            Some(&mut pointer_element),
            self.dnd_icon.as_ref(),
            self.snap_preview,
            pointer_location,
            gles,
        );

        match surface
            .drm_output
            .render_frame(gles, &elements, clear_color, FrameFlags::DEFAULT)
        {
            Ok(result) => {
                if !result.is_empty {
                    if let Err(err) = surface.drm_output.queue_frame(()) {
                        warn!(?err, "nie udało się zakolejkować klatki");
                    }
                }
            }
            Err(err) => warn!(?err, "błąd renderowania wyjścia"),
        }

        // Oddajemy kursor z powrotem do stanu backendu.
        udev.pointer_element = pointer_element;
    }

    /// Po VBlank: kończymy klatkę i planujemy kolejny render.
    pub fn udev_frame_finish(&mut self, node: DrmNode, crtc: crtc::Handle) {
        let Some(udev) = self.udev.as_mut() else {
            return;
        };
        if let Some(device) = udev.backends.get_mut(&node) {
            if let Some(surface) = device.surfaces.get_mut(&crtc) {
                // Przetwarza zakończoną klatkę (zwalnia bufory, domyka kolejki).
                let _ = surface.drm_output.frame_submitted();
            }
        }

        // Zakolejkowane przechwycenia ekranu (wlr-screencopy) — mamy tu renderer.
        if !self.screencopy_queue.is_empty() {
            let render_node = self
                .udev
                .as_ref()
                .map(|udev| udev.primary_gpu)
                .unwrap_or(node);
            if let Some(udev) = self.udev.as_mut() {
                if let Ok(mut renderer) = udev.gpus.single_renderer(&render_node) {
                    crate::screencopy::process(
                        renderer.as_mut(),
                        &self.space,
                        self.pointer_location,
                        &mut self.screencopy_queue,
                    );
                }
            }
        }

        // Zaplanuj kolejną klatkę. Prosty, ciągły rendering na VBlank wystarcza
        // na start; optymalizacja to renderowanie tylko przy uszkodzeniach.
        self.handle
            .insert_idle(move |state| state.udev_render(node));
    }

    /// Znajduje `Output` odpowiadające parze (urządzenie, CRTC).
    fn udev_output(&self, node: DrmNode, crtc: crtc::Handle) -> Option<Output> {
        self.space
            .outputs()
            .find(|output| {
                output.user_data().get::<UdevOutputId>()
                    == Some(&UdevOutputId {
                        device_id: node,
                        crtc,
                    })
            })
            .cloned()
    }

    /// Przetwarza zdarzenie wejścia z libinput (backend udev).
    ///
    /// PO CO osobno od `process_input_event_windowed`: libinput podaje ruch
    /// WZGLĘDNY (delty), a winit absolutny. Obsługa przycisków/scrolla/klawiatury
    /// jest wspólna (`input.rs`).
    pub fn process_input_event<B: InputBackend>(&mut self, event: InputEvent<B>) {
        match event {
            InputEvent::Keyboard { event } => self.handle_keyboard::<B>(event),
            InputEvent::PointerMotion { event } => self.on_pointer_move::<B>(event),
            InputEvent::PointerButton { event } => self.on_pointer_button::<B>(event),
            InputEvent::PointerAxis { event } => self.on_pointer_axis::<B>(event),
            _ => {}
        }
    }
}