pagan-compositor
Kompozytor Wayland PaganDE: floating window manager z hybrydowym CSD, zbudowany na Smithay 0.7.
- Renderer: OpenGL (
GlesRenderer, zbudowany naglow). - Backend deweloperski: winit (
--winit). - Menedżer okien: pływający — kaskadowe rozmieszczanie, przeciąganie, zmiana rozmiaru, Z-order.
- Dekoracje: client-side (wymuszamy
zxdg_toplevel_decoration_v1w trybieclient), kompozytor dokłada tylko cień.
✅ Status weryfikacji: kompiluje się (
cargo build,cargo build --features udev) i jest uruchamiany na backendziewinit. Realny test wizualny potwierdził: pasek tytułu 40 px z menu i przyciskami na wspólnej osi Y, rogi 12 px, cień GPU, kursor z motywu XCursor (grim -c). Backendtty-udevkompiluje się, ale nie był uruchamiany na sprzęcie z DRM — patrz// WERYFIKUJ:.
Wymagania
- Rust (rustup, stabilny toolchain) —
curl https://sh.rustup.rs -sSf | sh- Potrzebne są
cargoirustc. Wiele pakietów dostarcza oba naraz (np.rustw Arch/Manjaro albo pakietrustcw PaganOS/LFS — sprawdźfootprint: obecnośćusr/bin/cargo). Zainstalujcargoosobno tylko wtedy, gdy Twoje repo ma je rozdzielone. - Wersja: Smithay 0.7 wymaga Rust ≥ 1.80.1;
cairo-rs 0.22(toolkit) wymaga świeżego stabilnego toolchainu (edition 2024). Sprawdźrustc --version. Jeśli masz starszy — użyjrustup default stablealbo zaktualizuj pakiet.
- Potrzebne są
- Podstawowe biblioteki systemowe (dla winit + EGL/GL):
- Debian/Ubuntu:
libegl1-mesa-dev libgl1-mesa-dev libwayland-dev libxkbcommon-dev - Fedora:
mesa-libEGL-devel mesa-libGL-devel wayland-devel libxkbcommon-devel
- Debian/Ubuntu:
pkg-config
Smithay jest używany z vendored kopii ../smithay-0.7.0 (offline,
powtarzalnie). Jeśli wolisz crates.io, zmień w Cargo.toml na
smithay = { version = "0.7", default-features = false, features = [...] }.
Budowanie i uruchomienie
cd pagan-compositor
cargo build
cargo run -- --winit
Po starcie na stdout pojawi się linia:
WAYLAND_DISPLAY=wayland-1
Kompozytor działa jako zwykłe okno na Twoim pulpicie. W drugim terminalu
uruchom klienta (np. demo toolkitu) ustawiając ten socket. Zobacz
../pagan-toolkit/README.md.
Logi: RUST_LOG=pagan_compositor=debug cargo run -- --winit.
Architektura
src/
├── main.rs # wejście, logowanie, wybór backendu
├── winit.rs # backend dev: okno + pętla główna + render
├── udev.rs # backend produkcyjny: DRM/KMS + libinput + libseat
├── state.rs # MyCompositor: stan + handlery protokołów, floating WM
├── output.rs # monitory: układ, hotplug, konfiguracja, ratowanie okien
├── window.rs # PaganWindow: okno w Space + cień (per-okno, Z-order-poprawny)
├── focus.rs # KeyboardFocusTarget / PointerFocusTarget
├── grabs.rs # move / resize (standardowe xdg_toplevel.move/resize)
├── input.rs # mysz, klawiatura, Z-order, surface_under, skróty
├── snap.rs # strefy doklejania okien (krawędzie/rogi) + testy
├── cursor.rs # kursor z motywu XCursor (cache ikon, animacja klatek)
├── theme.rs # motyw: kolory tła/cienia/kursora + natychmiastowe przeładowanie
├── xwayland.rs # XWayland: X11Wm, schowek X11, obsługa okien X11
├── screencopy.rs# wlr-screencopy napisany od zera
├── render.rs # elementy renderowania, kursor, DnD, OutputDamageTracker
└── shaders/ # GLSL: rounded_corners.frag, shadow.frag (Zadanie 3)
Protokoły
Zaimplementowane globalne: wl_compositor/wl_subcompositor, wl_shm,
wl_output (+xdg_output), wl_seat (+pointer/keyboard/touch), xdg_wm_base,
xdg_decoration_v1 (ClientSide), wl_data_device (schowek + DnD),
primary_selection, xdg_activation, fractional_scale, pointer_constraints,
text_input_manager, wlr-layer-shell (panele/tła/nakładki), linux-dmabuf
(bufory GPU), wp_viewporter, wp_relative_pointer, wp_pointer_gestures,
wlr-screencopy (zrzuty ekranu — napisany od zera, patrz niżej),
XWayland (xwayland_shell + X11Wm; gdy brak binarki Xwayland, kompozytor
loguje ostrzeżenie i działa dalej).
Screencopy (zrzuty ekranu) — napisane od zera
Smithay 0.7 nie ma screencopy, więc implementujemy serwer sami w src/screencopy.rs
(definicje protokołu z wayland-protocols-wlr, semantyka nasza):
zwlr_screencopy_manager_v1(v4):capture_outputicapture_output_region,zwlr_screencopy_frame_v1:copy/copy_with_damage, kursor opcjonalnie,- przechwycenie: render wyjścia do tekstury offscreen (
Offscreen+Bind), odczyt pikseli (ExportMem::copy_framebuffer), kopiowanie do bufora SHM klienta,ready()z timestampem (albofailed()). - Kolejność: żądania
copylądują wscreencopy_queue, a realizuje je pętla renderowania (winit/udev), bo tylko tam jest renderer.
Test (na kompozytorze, na którym działa grim):
sudo pacman -S --needed grim slurp # narzędzia
WAYLAND_DISPLAY=wayland-1 grim /tmp/pagan.png
WAYLAND_DISPLAY=wayland-1 grim -g "$(slurp)" /tmp/region.png
Ograniczenia: obsługiwane są bufory SHM (to, czego używa grim). Bufory
dmabuf nie są jeszcze przyjmowane (nie wysyłamy zdarzenia linux_dmabuf,
więc klient wybierze SHM; dmabuf dostanie failed()). Przechwycenie wyjścia
innego niż o pozycji (0,0) wymaga jeszcze korekty przesunięcia — patrz // API-NOTE.
Konfiguracja (zmienne środowiskowe)
| Zmienna | Znaczenie |
|---|---|
PAGAN_SCALE=1.5 |
skala (HiDPI) wszystkich wyjść |
PAGAN_TRANSFORM=90\|180\|270\|flipped\|normal |
obrót wyjścia |
PAGAN_OUTPUTS="DP-1:0,0;HDMI-A-1:1920,0" |
ręczny układ monitorów (pozycje logiczne) |
PAGAN_CONFIG=/ścieżka/outputs.conf |
wskazanie pliku konfiguracji monitorów |
PAGAN_THEME=/ścieżka/theme.conf |
wskazanie pliku motywu (kolorów) |
PAGAN_DRM_DEVICE=/dev/dri/card1 |
wybór GPU dla backendu udev |
PAGAN_SNAP_THRESHOLD=12 |
próg doklejania okna do krawędzi (px) |
PAGAN_SNAP_BOTTOM=1 |
włącz doklejanie także do dolnej krawędzi |
PAGAN_LANG |
(toolkit) język UI |
Domyślnie (bez konfiguracji) wyjścia układane są poziomo od (0,0), bez przerw.
PAGAN_SCALE/PAGAN_TRANSFORM nadpisują ustawienia backendu tylko gdy podane —
winit wymaga np. Flipped180 do poprawnego rysowania.
Plik konfiguracji monitorów
Oprócz zmiennych środowiskowych układ można zapisać trwale w pliku
$XDG_CONFIG_HOME/pagan/outputs.conf (domyślnie ~/.config/pagan/outputs.conf).
Jeden monitor = jeden wiersz NAZWA X Y [SKALA] [OBROT]:
# nazwa x y skala obrot
DP-1 0 0 1.0 normal
HDMI-A-1 2560 0 1.5 90
Pola SKALA i OBROT są opcjonalne; # zaczyna komentarz. Nazwy monitorów
Nazwy monitorów zobaczysz w logu startowym (układ wyjść zaktualizowany ... DP-1=...).
Kolejność nadpisywania: plik → PAGAN_OUTPUTS/PAGAN_SCALE/PAGAN_TRANSFORM
(zmienne wygrywają, więc łatwo je przetestować bez ruszania pliku).
Motyw kompozytora (kolory) — theme.conf
Kompozytor sam maluje tylko kilka rzeczy (tło pulpitu, cień pod oknami, awaryjny
kursor), więc motyw steruje właśnie nimi. Plik: ~/.config/pagan/theme.conf
(albo $PAGAN_THEME). Przy pierwszym uruchomieniu powstaje z komentowanym
szablonem, więc nie trzeba zgadywać nazw kluczy.
background = #0e0f14 # tło pulpitu
shadow_color = #000000 # kolor cienia (RGB)
shadow_opacity = 0.42 # maksymalna nieprzezroczystość (0..1)
shadow_blur = 24 # rozmycie w px (większe = bardziej rozlane)
shadow_offset_y = 6 # przesunięcie cienia w dół
shadow_radius = 12 # promień rogów sylwetki cienia
cursor_fill = #f2f2f7 # awaryjny kursor (gdy brak motywu XCursor)
cursor_border = #14141a
Kolory zapisujesz jako #RRGGBB lub #RRGGBBAA; komentarz to cały wiersz
zaczynający się od #.
Przeładowanie jest natychmiastowe: kompozytor obserwuje plik przez inotify
(katalog, nie sam plik — edytory często zapisują przez rename) i po każdej
zmianie wczytuje motyw od nowa. Nie trzeba restartu ani żadnego klawisza.
Dodatkowo Ctrl+Alt+R wymusza przeładowanie ręcznie (przydatne, gdy edytor
nadpisuje plik w nietypowy sposób albo gdy oglądasz zmiany „na żądanie”).
Zmiana motywu jest widoczna od następnej klatki: kolor tła idzie do czyszczenia
bufora, a parametry cienia są uniformami shadera. Zrzuty ekranu (wlr-screencopy)
też od razu używają nowych kolorów.
Instalacja zależności (Manjaro/Arch)
# Rust (jeśli brak):
sudo pacman -S --needed rustup && rustup default stable
# Backend dev (winit) + toolkit:
sudo pacman -S --needed mesa wayland libxkbcommon cairo pkgconf
# Backend tty-udev (produkcja):
sudo pacman -S --needed libdrm libgbm libinput libseat libxkbcommon \
mesa vulkan-icd-loader
# Klienci X11 (XWayland) — opcjonalnie, ale bez tego aplikacje X11 nie działają:
sudo pacman -S --needed xorg-xwayland
# Narzędzia do TESTU screencopy (NIE są częścią Rusta — nie instalują cargo):
sudo pacman -S --needed grim slurp
Uwaga:
cargo/rustcpochodzą z pakietu rust (albo rustup) i są potrzebne do budowania projektów.grim/slurpto niezależne narzędzia-klienty Waylanda, używane tylko do przetestowania zrzutów ekranu.
Na Manjaro XFCE sesja jest X11 — kompozytor odpalasz albo jako okno (--winit),
albo po przełączeniu na inny TTY (--tty-udev, przez logind/libseat lub root).
Skróty globalne kompozytora
Przechwytywane zanim trafią do klienta (patrz input.rs::compositor_shortcut):
| Skrót | Działanie |
|---|---|
Ctrl+Alt+Backspace |
zakończ kompozytor |
Ctrl+Alt+Left / Right |
fokus na poprzednie / następne okno |
Ctrl+Alt+Delete |
zamknij okno z fokusem (xdg_toplevel.close) |
Ctrl+Alt+M |
zminimalizuj okno z fokusem |
Ctrl+Alt+U |
przywróć ostatnio zminimalizowane okno |
Ctrl+Alt+R |
przeładuj motyw z pliku |
Hybrydowe CSD — jak to działa
- Klient (toolkit) rysuje całe okno: zaokrąglone tło, pasek tytułu, menu i przyciski. Kompozytor nie zna ich położenia.
- Klik w pasek tytułu → klient wysyła
xdg_toplevel.move(seat, serial, x, y). Ciągnięcie krawędzi →xdg_toplevel.resize(seat, serial, edges, w, h). - Kompozytor ustanawia grab (
grabs.rs) i przesuwa/rozciąga okno. xdg_decoration_manager_v1w trybieclientsprawia, że klienci nie oczekują dekoracji od nas (a jeśli proszą oserver, respektujemy to).
Zaleta: menu i przyciski są zawsze idealnie wyrównane (rysuje je ten sam kod), a kompozytor nie wymaga żadnego niestandardowego protokołu.
Dlaczego Space<PaganWindow>, a nie Space<Window>
W specyfikacji była propozycja pub space: Space<Window>. Użyliśmy cienkiego
wrappera PaganWindow(pub Window), bo cień musi być rysowany pod konkretnym
oknem, ale nad oknami niżej. Smithay pozwala dołączyć własne elementy tylko do
typu implementującego AsRenderElements. Wrapper deleguje cały kontrakt
SpaceElement/WaylandFocus do Window, więc różnica jest minimalna, a cień
ma poprawny Z-order. Jeśli cienie nie są potrzebne, wystarczy podmienić typ.
Cień (własny shader GLSL — GPU)
Cień jest liczony na GPU własnym pixel shaderem shaders/shadow.frag
(kompilowany przez GlesRenderer::compile_custom_pixel_shader, rysowany jako
PixelShaderElement):
- sylwetka to zaokrąglony prostokąt liczony odległością ze znakiem (SDF) — cień pasuje do 12-pikselowych rogów rysowanych przez toolkit,
- miękkość to
exp(-d/blur)(tanie przybliżenie rozmycia Gaussa), - cień jest przesunięty w dół i rysowany pod oknem (w tej samej grupie elementów co okno → poprawny Z-order).
Konsekwencja: typy renderowania są specjalizowane do GlesRenderer
(PixelShaderElement działa tylko z konkretnym rendererem). Używamy GlesRenderer
w obu backendach, więc to nie ogranicza.
shaders/rounded_corners.frag to gotowa maska rogów (pixel shader) na przyszły
tryb SSD / okna bez własnego CSD. W obecnej konfiguracji (CSD) nie jest nakładana,
bo klient rysuje rogi sam, a pixel shader nie potrafi "wymazać" już narysowanych
pikseli (szczegóły w nagłówku pliku).
Kluczowe decyzje
xdg_decoration= ClientSide jako domyślne. To fundament hybrydowego CSD.- Move/resize przez standardowe żądania klienta, nie przez własny protokół.
- Kaskada 20 px (
CASCADE_STEP) z zawijaniem do początku przy krawędzi ekranu. - Fokus = podniesienie w Z-order: kliknięcie okna aktywuje je i wysuwa.
- Renderer
glow(GlesRenderer) — najprostsza ścieżka dev; Vulkan (wgpu) to przyszły feature. - Bez animacji w tym kroku (zgodnie z ustaleniem). Miejsce na nie to
input.rs/window.rs; springs dojdą jako osobny moduł.
Monitory (liczba, rozmiary, hotplug)
Kompozytor obsługuje wiele wyjść tak, jak wypada:
- Liczba i rozmiar — backend
udevtworzy jedno wyjście per podłączony konektor/CRTC, w trybie preferowanym monitora (WlMode::from(drm_mode)), z fizycznym rozmiarem panelu (mm) wPhysicalProperties. Backend winit ma z definicji jedno wyjście; jego rozmiar śledziWinitEvent::Resized. - Układ —
output.rs::relayout_outputsustawia wyjścia poziomo od (0,0), bez przerw (po odłączeniu środkowego monitora reszta się dosuwa). - Hotplug —
DrmScanEvent::Connected/Disconnecteddodaje/usuwa wyjścia oraz przełącza sesję; po każdej zmianie wołany jest re-layout. - Ratowanie okien —
ensure_windows_on_screenprzenosi okna, które po zmianie układu wypadły poza wszystkie wyjścia (np. stały na odłączonym ekranie). - Nowe okna — lądują na monitorze pod kursorem (kaskada w obrębie tego wyjścia), co jest standardem w pływających menedżerach okien.
- Kursor — przy ruchu względnym (libinput) jest przyciągany do najbliższego wyjścia, więc nie „zapuszcza się” w martwe pole przy układzie w kształcie L.
Stan wyjść logujemy po każdej zmianie:
układ wyjść zaktualizowany count=2 outputs=DP-1=2560x1440+0+0, HDMI-A-1=1920x1080+2560+0.
Czego jeszcze nie ma: przeliczania układu przy zmianie skali w locie.
Skalowanie (HiDPI) i obrót są już wspierane — globalnie (PAGAN_SCALE,
PAGAN_TRANSFORM) oraz per monitor w outputs.conf.
Backend tty-udev (produkcja)
Zaimplementowany w src/udev.rs, włączany feature'em udev:
# Zależności systemowe (Debian/Ubuntu):
# libdrm-dev libgbm-dev libinput-dev libudev-dev libseat-dev
cargo build --features udev
sudo ./target/debug/pagan-compositor --tty-udev # albo przez logind/libseat bez sudo
Zakres: sesja libseat (pauza/wznowienie VT), libinput (ruch względny, przyciski,
scroll, klawiatura), wybór GPU (PAGAN_DRM_DEVICE opcjonalnie), per-urządzenie
DrmOutputManager + GbmAllocator + GbmFramebufferExporter, tworzenie wyjść
per konektor/CRTC, pętla VBlank i hotplug. Logika okien/protokołów jest w 100%
współdzielona z backendem winit (MyCompositor::init).
✅ Ten kod kompiluje się (cargo build --features udev, zweryfikowane), ale
nie był uruchamiany na sprzęcie z DRM (brak dostępu do TTY/GPU w środowisku
budowania). Miejsca wskazane // WERYFIKUJ: warto sprawdzić przy pierwszym
uruchomieniu na prawdziwym TTY.
Znane ograniczenia / możliwe korekty przy pierwszym uruchomieniu na TTY
- Backend
udevjest niezweryfikowany na sprzęcie (patrz sekcja wyżej i// WERYFIKUJ:). - Obsługa subsurface'ów w
commitjest uproszczona (bierzemy root drzewa). - XWayland zaimplementowany; wymaga obecności binarki
Xwayland(sudo pag install xorg-xwaylandw PaganOS,sudo pacman -S xorg-xwaylandw Arch/Manjaro). Bez niej kompozytor działa, ale bez aplikacji X11. wlr-screencopynapisaliśmy od zera (src/screencopy.rs) — działa zgrimna backendzie winit (kursor:grim -c).- Minimalizacja chowa okno z
Spacei przywraca przezCtrl+Alt+U. Docelowo trafi na pasek zadań /wlr-foreign-toplevel. - Ikona DnD jest już rysowana pod kursorem (
render.rs::output_elements). - Kursor pochodzi z motywu XCursor (
XCURSOR_THEME/XCURSOR_SIZE); gdy motywu brak, rysowany jest czytelny prostokąt zapasowy. pointer_gestures/relative_pointersą zarejestrowane; pełne przekazywanie zdarzeń gestów zależy od backendu (udev przekazuje ruch względny).