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

PaganDE/pagan-toolkit/src/shm.rs main

154 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
//! Alokacja pamięci współdzielonej i wystawienie jej jako `wl_shm`.
//!
//! Kluczowa zasada: **deskryptor pliku i mapowanie muszą żyć razem** z obiektami
//! `wl_shm_pool`/`wl_buffer`. Kompozytor czyta bowiem z pamięci, do której fd
//! wskazuje — jeśli plik zostałby zamknięty, bufory przestałyby być poprawne.
//! Dlatego [`ShmBuffer`] trzyma `File`, `MmapMut`, `WlShmPool` i `WlBuffer` w jednej
//! strukturze, której czas życia pilnuje kompilator.

use std::fmt;
use std::fs::File;
use std::os::fd::AsFd;

use memmap2::MmapMut;
use wayland_client::protocol::wl_buffer::WlBuffer;
use wayland_client::protocol::wl_shm::{Format, WlShm};
use wayland_client::protocol::wl_shm_pool::WlShmPool;
use wayland_client::{Dispatch, QueueHandle};

/// Błędy alokacji bufora SHM.
#[derive(Debug)]
pub enum ShmError {
    /// Błąd operacji wejścia/wyjścia na pliku tymczasowym.
    Io(std::io::Error),
    /// Żądany rozmiar jest niepoprawny (zero lub ujemny).
    ZeroSize,
    /// Rozmiar przekracza zakres `i32` wymagany przez protokół.
    TooLarge(usize),
}

impl fmt::Display for ShmError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ShmError::Io(err) => write!(f, "błąd I/O pamięci współdzielonej: {err}"),
            ShmError::ZeroSize => write!(f, "rozmiar bufora SHM musi być dodatni"),
            ShmError::TooLarge(size) => {
                write!(
                    f,
                    "rozmiar bufora SHM ({size} B) przekracza zakres protokołu"
                )
            }
        }
    }
}

impl std::error::Error for ShmError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ShmError::Io(err) => Some(err),
            _ => None,
        }
    }
}

impl From<std::io::Error> for ShmError {
    fn from(value: std::io::Error) -> Self {
        ShmError::Io(value)
    }
}

/// Bufor pamięci współdzielonej gotowy do wystawienia jako `wl_buffer`.
pub struct ShmBuffer {
    /// Plik tymczasowy będący „nośnikiem" pamięci. MUSI żyć tak długo jak bufory.
    pub file: File,
    /// Mapowanie pliku do przestrzeni adresowej procesu — tu renderuje Cairo.
    pub mmap: MmapMut,
    /// Szerokość bufora w pikselach.
    pub width: i32,
    /// Wysokość bufora w pikselach.
    pub height: i32,
    /// Liczba bajtów na wiersz.
    pub stride: i32,
    /// Łączny rozmiar bufora w bajtach.
    pub size: usize,
    /// Format piksela zgłoszony kompozytorowi.
    pub format: Format,
    /// Obiekt `wl_shm_pool` (trzymany, aby móc go później zniszczyć/zresize'ować).
    pub pool: WlShmPool,
    /// Obiekt `wl_buffer`, który podłączamy do powierzchni.
    pub buffer: WlBuffer,
}

impl ShmBuffer {
    /// Tworzy plik w pamięci, mapuje go i wystawia jako `wl_buffer` w formacie ARGB32.
    ///
    /// API-NOTE: używamy `tempfile::tempfile()`, bo jest przenośne i nie wymaga
    /// dodatkowej zależności. Dla pełnej zgodności ze specyfikacją `wl_shm` warto
    /// rozważyć `memfd_create` + pieczęć `F_SEAL_SHRINK` (chroni pamięć przed
    /// przycięciem przez kompozytor). Wersja z `tempfile` jest poprawna funkcjonalnie.
    pub fn new<State>(
        shm: &WlShm,
        qh: &QueueHandle<State>,
        width: i32,
        height: i32,
    ) -> Result<Self, ShmError>
    where
        State: Dispatch<WlShmPool, ()> + Dispatch<WlBuffer, ()> + 'static,
    {
        if width <= 0 || height <= 0 {
            return Err(ShmError::ZeroSize);
        }

        let format = Format::Argb8888;
        // ARGB8888 to zawsze 4 bajty na piksel.
        let stride = width.checked_mul(4).ok_or(ShmError::TooLarge(usize::MAX))?;
        let size = (stride as usize)
            .checked_mul(height as usize)
            .ok_or(ShmError::TooLarge(usize::MAX))?;
        if size > i32::MAX as usize {
            return Err(ShmError::TooLarge(size));
        }

        // 1. Nośnik pamięci. `tempfile()` tworzy anonimowy plik w tmpfs.
        let file = tempfile::tempfile()?;
        file.set_len(size as u64)?;

        // 2. Mapowanie do zapisu.
        // SAFETY: plik ma ustawioną długość `size`; mapujemy go prywatnie na wyłączność
        // tego procesu (kompozytor dostaje ten sam plik przez fd, mapuje go u siebie).
        let mmap = unsafe { MmapMut::map_mut(&file)? };

        // 3. Wystawienie puli i bufora dla kompozytora.
        let pool = shm.create_pool(file.as_fd(), size as i32, qh, ());
        let buffer = pool.create_buffer(0, width, height, stride, format, qh, ());

        Ok(Self {
            file,
            mmap,
            width,
            height,
            stride,
            size,
            format,
            pool,
            buffer,
        })
    }

    /// Czy bieżący bufor pasuje do zadanego rozmiaru (pozwala uniknąć realokacji).
    pub fn matches(&self, width: i32, height: i32) -> bool {
        self.width == width && self.height == height
    }
}

impl fmt::Debug for ShmBuffer {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("ShmBuffer")
            .field("width", &self.width)
            .field("height", &self.height)
            .field("stride", &self.stride)
            .field("size", &self.size)
            .field("format", &self.format)
            .finish_non_exhaustive()
    }
}