← luna

Commit b741c44

5
files
+612
added
-55
deleted
@@ -52,16 +52,21 @@ gnieździe nasłuchu i deskryptorze backendu. `libLuna` jest zwykłą zależnoś
52 52 z fallbackiem na pojedyncze wyjście nested, gdy DRM niedostępne.
53 53 - **Schowek** (`wl_data_device_manager`): kopiuj/wklej między klientami — `wl_data_source`,
54 54 `data_offer`/`offer`/`selection` rozsyłane do pozostałych urządzeń, `wl_data_offer.receive`
55 - przekazywane do źródła (`.send`).
55 + przekazywane do źródła (`.send`). **Drag & drop**: `start_drag`, `enter`/`motion`/`leave`/
56 + `drop` do celu (z ofertą), `dnd_drop_performed`/`dnd_finished`/`cancelled` do źródła.
56 57 - **Klawiatura**: układ i wariant z otoczenia (`LUNA_KEYBOARD_LAYOUT`, `LUNA_KEYBOARD_VARIANT`,
57 58 sanityzowane), `wl_keyboard.repeat_info`.
59 +- **Realne wyjście DRM/KMS** (`SELENE_BACKEND=drm`, opcjonalne): przejęcie mastera, bufor
60 + „dumb”, framebuffer (`ADDFB2`), `SETCRTC` i **page-flip** na wykrytych monitorach; render
61 + sceny wprost do buforów (podwójne buforowanie). Gdy master trzyma inna sesja — bezpieczny
62 + fallback do nested (nie odbiera ekranu).
58 63 - Wielojęzyczność (`libluna-i18n`) i optymalizacja (`[profile.release]` LTO, cache `shm`).
59 64
60 65 ## Jeszcze nie / Not yet
61 66
62 -- **Backend DRM/KMS**: samo **wykrywanie** monitorów działa; skanowanie (bufory, page-flip,
63 - master) i GLES oraz hotplug (`zwlr_output_manager`) — do zrobienia.
64 -- **Drag & drop** i **selekcja pierwotna** (poza kopiuj/wklej); IME / `text-input`.
67 +- **Wejście w trybie DRM** (evdev/`libinput`) — skanowanie działa, ale bez rodzimego wejścia
68 + okna są tylko wyświetlane; GLES/akceleracja i hotplug (`zwlr_output_manager`) — do zrobienia.
69 +- **Selekcja pierwotna** (`primary selection`); IME / `text-input`.
65 70 - `wl_output` dynamiczne (zmiana trybu), fokus klawiatury po kliknięciu poza oknem.
66 71 - Render tylko przy zmianie modelu (częściowe odświeżanie/damage).
67 72
@@ -74,6 +79,17 @@ XDG_RUNTIME_DIR="$PWD/.selene-runtime" cargo run -p selene
74 79 # → "Selene compositor started (wayland-0)" + gniazdo wayland-0 w katalogu runtime
75 80 ```
76 81
82 +Tryb nested (domyślny) uruchamia okno na nadrzędnym kompozytorze. **Realne wyjście DRM**
83 +(gdy masz wolnego mastera, np. z osobnej konsoli):
84 +
85 +```sh
86 +SELENE_BACKEND=drm XDG_RUNTIME_DIR="$PWD/.selene-runtime" cargo run -p selene
87 +# diagnostyka bez zmiany trybu: SELENE_DRM_PROBE=1 cargo run -p selene
88 +```
89 +
90 +Uwaga: tryb DRM przejmuje ekran (page-flip); gdy master trzyma Twoja sesja, Selene
91 +zgłasza to i **nie** rusza wyświetlaczy.
92 +
77 93 Klienta podłączysz ustawiając `WAYLAND_DISPLAY=wayland-0` i `XDG_RUNTIME_DIR` na ten sam
78 94 katalog (renderowanie pojawi się, gdy wejdzie backend wyświetlania).
79 95
@@ -88,9 +104,9 @@ de/selene/src/
88 104 shell.rs # xdg_wm_base / xdg_surface / xdg_toplevel / xdg_popup / xdg_positioner
89 105 decoration.rs # zxdg_decoration_manager_v1 (SSD: odpowiedź server_side)
90 106 layer.rs # zwlr_layer_shell_v1 (panele/docki: kotwice, warstwy, wybór monitora)
91 - data.rs # wl_data_device_manager (schowek: selekcja, data_offer/source)
107 + data.rs # wl_data_device_manager (schowek + drag & drop)
92 108 monitors.rs # wl_output (model wielu monitorów, układ, trafienia)
93 - drm.rs # wykrywanie monitorów DRM/KMS (ioctl) z fallbackiem nested
109 + drm.rs # wykrywanie + scanout DRM/KMS (ioctl, dumb buffer, page-flip), fallback nested
94 110 seat.rs # wl_seat: wskaźnik + klawiatura + trasowanie wejścia i operacje okien
95 111 input.rs # magistrala wejścia (kolejka + wybudzanie)
96 112 output.rs # wyjście nested (libluna-platform + libluna-render)
@@ -9,20 +9,37 @@
9 9 //! by Selene wiedział, ile i jakich monitorów ma użyć.
10 10
11 11 use std::mem::MaybeUninit;
12 -use std::os::fd::{AsFd, BorrowedFd};
12 +use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
13 13 use std::path::PathBuf;
14 14
15 15 use rustix::fs::{Mode, OFlags};
16 +use rustix::mm::{mmap, munmap, MapFlags, ProtFlags};
16 17
17 18 use crate::monitors::{layout_horizontal, OutputInfo};
18 19
19 20 // — Kody ioctl KMS (`_IOWR`) —
20 21 const DRM_IOCTL_MODE_GETRESOURCES: u32 = 0xC040_64A0;
21 22 const DRM_IOCTL_MODE_GETCONNECTOR: u32 = 0xC050_64A7;
23 +const DRM_IOCTL_MODE_GETENCODER: u32 = 0xC014_64A6;
24 +const DRM_IOCTL_MODE_SETCRTC: u32 = 0xC068_64A2;
25 +const DRM_IOCTL_MODE_CREATE_DUMB: u32 = 0xC020_64B2;
26 +const DRM_IOCTL_MODE_MAP_DUMB: u32 = 0xC010_64B3;
27 +const DRM_IOCTL_MODE_ADDFB2: u32 = 0xC068_64B8;
28 +const DRM_IOCTL_MODE_PAGE_FLIP: u32 = 0xC018_64B0;
29 +
30 +// — ioctle bez argumentu (`_IO`) —
31 +const DRM_IOCTL_SET_MASTER: u32 = 0x0000_641E;
32 +const DRM_IOCTL_DROP_MASTER: u32 = 0x0000_641F;
22 33
23 34 const DRM_MODE_CONNECTED: u32 = 1;
24 35 /// `DRM_MODE_TYPE_PREFERRED` (bit 3) — natywny tryb monitora.
25 36 const DRM_MODE_TYPE_PREFERRED: u32 = 1 << 3;
37 +/// `DRM_MODE_PAGE_FLIP_EVENT` — proś o zdarzenie po zakończeniu flipu.
38 +const DRM_MODE_PAGE_FLIP_EVENT: u32 = 1;
39 +/// Cztery znaki `XRGB8888` (kolejność bajtów B,G,R,X).
40 +const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258;
41 +/// `DRM_EVENT_FLIP_COMPLETE`.
42 +const DRM_EVENT_FLIP_COMPLETE: u32 = 2;
26 43
27 44 /// `struct drm_mode_card_res`.
28 45 #[repr(C)]
@@ -85,6 +102,80 @@ struct ModeInfo {
85 102 name: [u8; 32],
86 103 }
87 104
105 +/// `struct drm_mode_get_encoder`.
106 +#[repr(C)]
107 +#[derive(Clone, Copy, Default)]
108 +struct ModeGetEncoder {
109 + encoder_id: u32,
110 + encoder_type: u32,
111 + crtc_id: u32,
112 + possible_crtcs: u32,
113 + possible_clones: u32,
114 +}
115 +
116 +/// `struct drm_mode_crtc` (używany też przez `GETCRTC`/`SETCRTC`).
117 +#[repr(C)]
118 +#[derive(Clone, Copy, Default)]
119 +struct ModeCrtc {
120 + set_connectors_ptr: u64,
121 + count_connectors: u32,
122 + crtc_id: u32,
123 + fb_id: u32,
124 + x: u32,
125 + y: u32,
126 + gamma_size: u32,
127 + mode_valid: u32,
128 + mode: ModeInfo,
129 +}
130 +
131 +/// `struct drm_mode_create_dumb`.
132 +#[repr(C)]
133 +#[derive(Clone, Copy, Default)]
134 +struct ModeCreateDumb {
135 + height: u32,
136 + width: u32,
137 + bpp: u32,
138 + flags: u32,
139 + handle: u32,
140 + pitch: u32,
141 + size: u64,
142 +}
143 +
144 +/// `struct drm_mode_map_dumb`.
145 +#[repr(C)]
146 +#[derive(Clone, Copy, Default)]
147 +struct ModeMapDumb {
148 + handle: u32,
149 + pad: u32,
150 + offset: u64,
151 +}
152 +
153 +/// `struct drm_mode_fb_cmd2`.
154 +#[repr(C)]
155 +#[derive(Clone, Copy, Default)]
156 +struct ModeFbCmd2 {
157 + fb_id: u32,
158 + width: u32,
159 + height: u32,
160 + pixel_format: u32,
161 + flags: u32,
162 + handles: [u32; 4],
163 + pitches: [u32; 4],
164 + offsets: [u32; 4],
165 + modifier: [u64; 4],
166 +}
167 +
168 +/// `struct drm_mode_crtc_page_flip`.
169 +#[repr(C)]
170 +#[derive(Clone, Copy, Default)]
171 +struct ModePageFlip {
172 + crtc_id: u32,
173 + fb_id: u32,
174 + flags: u32,
175 + reserved: u32,
176 + user_data: u64,
177 +}
178 +
88 179 /// Opakowanie na ioctl typu „read-write” z jednym struktem wejścia/wyjścia.
89 180 struct Rw<T, const OP: u32>(MaybeUninit<T>);
90 181
@@ -117,11 +208,52 @@ fn ioctl_rw<T: Copy, const OP: u32, F: AsFd>(fd: F, value: T) -> rustix::io::Res
117 208 unsafe { rustix::ioctl::ioctl(fd, Rw::<T, OP>::new(value)) }
118 209 }
119 210
120 -/// Wykrywa monitory. `None` oznacza, że DRM/KMS nie jest dostępne (użyj wyjścia nested).
121 -pub fn discover() -> Option<Vec<OutputInfo>> {
211 +/// ioctl bez argumentu (np. `SET_MASTER`/`DROP_MASTER`).
212 +struct NoArg<const OP: u32>;
213 +
214 +unsafe impl<const OP: u32> rustix::ioctl::Ioctl for NoArg<OP> {
215 + type Output = ();
216 + const OPCODE: rustix::ioctl::Opcode =
217 + rustix::ioctl::Opcode::old(OP as rustix::ioctl::RawOpcode);
218 + const IS_MUTATING: bool = false;
219 +
220 + fn as_ptr(&mut self) -> *mut core::ffi::c_void {
221 + core::ptr::null_mut()
222 + }
223 +
224 + unsafe fn output_from_ptr(
225 + _: rustix::ioctl::IoctlOutput,
226 + _: *mut core::ffi::c_void,
227 + ) -> rustix::io::Result<()> {
228 + Ok(())
229 + }
230 +}
231 +
232 +fn ioctl_none<const OP: u32, F: AsFd>(fd: F) -> rustix::io::Result<()> {
233 + unsafe { rustix::ioctl::ioctl(fd, NoArg::<OP>) }
234 +}
235 +
236 +/// Diagnostyka: sprawdza, czy można przejąć mastera DRM **bez zmiany trybu**.
237 +/// Zwraca ścieżkę karty, jeśli master jest wolny (i od razu go zwalnia).
238 +pub fn check_master() -> Option<PathBuf> {
239 + for card in card_paths() {
240 + let Ok(fd) = rustix::fs::open(&card, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty()) else {
241 + continue;
242 + };
243 + if ioctl_none::<DRM_IOCTL_SET_MASTER, _>(&fd).is_ok() {
244 + let _ = ioctl_none::<DRM_IOCTL_DROP_MASTER, _>(&fd);
245 + return Some(card);
246 + }
247 + }
248 + None
249 +}
250 +
251 +/// Lista ścieżek kart DRM (`/dev/dri/card*`), posortowana.
252 +fn card_paths() -> Vec<PathBuf> {
122 253 let mut cards: Vec<PathBuf> = std::fs::read_dir("/dev/dri")
123 - .ok()?
124 - .filter_map(|e| e.ok())
254 + .into_iter()
255 + .flatten()
256 + .flatten()
125 257 .map(|e| e.path())
126 258 .filter(|p| {
127 259 p.file_name()
@@ -130,9 +262,13 @@ pub fn discover() -> Option<Vec<OutputInfo>> {
130 262 })
131 263 .collect();
132 264 cards.sort();
265 + cards
266 +}
133 267
134 - for card in &cards {
135 - let Ok(fd) = rustix::fs::open(card, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty()) else {
268 +/// Wykrywa monitory. `None` oznacza, że DRM/KMS nie jest dostępne (użyj wyjścia nested).
269 +pub fn discover() -> Option<Vec<OutputInfo>> {
270 + for card in card_paths() {
271 + let Ok(fd) = rustix::fs::open(&card, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty()) else {
136 272 continue;
137 273 };
138 274 if let Some(mut outs) = probe_card(fd.as_fd()) {
@@ -240,6 +376,327 @@ fn connector_name(kind: u32, index: u32) -> String {
240 376 format!("{base}-{index}")
241 377 }
242 378
379 +// ————————————————————————————————————————————————————————————————————————
380 +// Scanout: realne wyjście na monitory przez KMS (dumb buffer + page-flip)
381 +// ————————————————————————————————————————————————————————————————————————
382 +
383 +/// Zasoby karty: identyfikatory konektorów, CRTC i enkoderów.
384 +struct Resources {
385 + connectors: Vec<u32>,
386 + crtcs: Vec<u32>,
387 + #[allow(dead_code)]
388 + encoders: Vec<u32>,
389 +}
390 +
391 +fn fetch_resources(fd: BorrowedFd) -> Option<Resources> {
392 + let res =
393 + ioctl_rw::<ModeCardRes, DRM_IOCTL_MODE_GETRESOURCES, _>(fd, ModeCardRes::default()).ok()?;
394 + let (cn, cc, ce) = (
395 + res.count_connectors as usize,
396 + res.count_crtcs as usize,
397 + res.count_encoders as usize,
398 + );
399 + let (mut connectors, mut crtcs, mut encoders) =
400 + (vec![0u32; cn], vec![0u32; cc], vec![0u32; ce]);
401 + let req = ModeCardRes {
402 + count_connectors: cn as u32,
403 + connector_id_ptr: connectors.as_mut_ptr() as u64,
404 + count_crtcs: cc as u32,
405 + crtc_id_ptr: crtcs.as_mut_ptr() as u64,
406 + count_encoders: ce as u32,
407 + encoder_id_ptr: encoders.as_mut_ptr() as u64,
408 + ..ModeCardRes::default()
409 + };
410 + ioctl_rw::<ModeCardRes, DRM_IOCTL_MODE_GETRESOURCES, _>(fd, req).ok()?;
411 + Some(Resources {
412 + connectors,
413 + crtcs,
414 + encoders,
415 + })
416 +}
417 +
418 +/// Bufor „dumb” (bez GPU): sterta pamięci zmapowana do renderowania i skanowania.
419 +struct DumbBuffer {
420 + fb_id: u32,
421 + pitch: usize,
422 + size: usize,
423 + ptr: *mut u8,
424 +}
425 +
426 +// Bezpieczne: bufor należy wyłącznie do wątku wyjścia i nie jest współdzielony.
427 +unsafe impl Send for DumbBuffer {}
428 +
429 +impl Drop for DumbBuffer {
430 + fn drop(&mut self) {
431 + if !self.ptr.is_null() {
432 + unsafe {
433 + let _ = munmap(self.ptr.cast(), self.size);
434 + }
435 + }
436 + }
437 +}
438 +
439 +/// Jedno wyjście DRM: konektor + CRTC + podwójny bufor.
440 +struct DrmOutput {
441 + name: String,
442 + crtc_id: u32,
443 + width: u32,
444 + height: u32,
445 + buffers: [DumbBuffer; 2],
446 + front: usize,
447 +}
448 +
449 +/// Backend wyjścia DRM/KMS (jedna karta, wiele monitorów).
450 +pub struct DrmBackend {
451 + fd: OwnedFd,
452 + outputs: Vec<DrmOutput>,
453 +}
454 +
455 +impl Drop for DrmBackend {
456 + fn drop(&mut self) {
457 + // Zwolnij bycie masterem, gdy kończymy.
458 + let _ = ioctl_none::<DRM_IOCTL_DROP_MASTER, _>(&self.fd);
459 + }
460 +}
461 +
462 +impl DrmBackend {
463 + /// Otwiera kartę, przejmuje mastera i ustawia tryb na wszystkich podłączonych monitorach.
464 + /// Zwraca `None`, gdy DRM jest niedostępne lub ktoś inny trzyma mastera (np. sesja użytkownika).
465 + pub fn open() -> Option<Self> {
466 + for card in card_paths() {
467 + let Ok(fd) = rustix::fs::open(&card, OFlags::RDWR | OFlags::CLOEXEC, Mode::empty())
468 + else {
469 + continue;
470 + };
471 + if ioctl_none::<DRM_IOCTL_SET_MASTER, _>(&fd).is_err() {
472 + continue; // inny master (uruchomiona sesja) — nie odbieramy ekranu
473 + }
474 + let Some(res) = fetch_resources(fd.as_fd()) else {
475 + let _ = ioctl_none::<DRM_IOCTL_DROP_MASTER, _>(&fd);
476 + continue;
477 + };
478 + let mut outputs = Vec::new();
479 + for &conn in &res.connectors {
480 + if let Some(out) = setup_output(fd.as_fd(), conn, &res) {
481 + outputs.push(out);
482 + }
483 + }
484 + if outputs.is_empty() {
485 + let _ = ioctl_none::<DRM_IOCTL_DROP_MASTER, _>(&fd);
486 + continue;
487 + }
488 + return Some(Self { fd, outputs });
489 + }
490 + None
491 + }
492 +
493 + /// `(nazwa, szerokość, wysokość)` każdego wyjścia.
494 + pub fn outputs(&self) -> Vec<(String, u32, u32)> {
495 + self.outputs
496 + .iter()
497 + .map(|o| (o.name.clone(), o.width, o.height))
498 + .collect()
499 + }
500 +
501 + /// Wypycha klatkę (RGBA, wymiary = rozmiar wyjścia) i kolejkuje page-flip.
502 + /// Po wywołaniu należy poczekać na [`DrmBackend::wait_flip`].
503 + pub fn present(&mut self, idx: usize, rgba: &[u8]) -> bool {
504 + let fd = &self.fd;
505 + let Some(out) = self.outputs.get_mut(idx) else {
506 + return false;
507 + };
508 + let back = (out.front + 1) & 1;
509 + swizzle_into(
510 + out.buffers[back].ptr,
511 + out.buffers[back].pitch,
512 + rgba,
513 + out.width as usize,
514 + out.height as usize,
515 + );
516 + let req = ModePageFlip {
517 + crtc_id: out.crtc_id,
518 + fb_id: out.buffers[back].fb_id,
519 + flags: DRM_MODE_PAGE_FLIP_EVENT,
520 + reserved: 0,
521 + user_data: idx as u64,
522 + };
523 + if ioctl_rw::<ModePageFlip, DRM_IOCTL_MODE_PAGE_FLIP, _>(fd, req).is_err() {
524 + return false;
525 + }
526 + out.front = back;
527 + true
528 + }
529 +
530 + /// Czeka na zakończenie page-flipu (zdarzenie z jądra).
531 + pub fn wait_flip(&self) {
532 + let mut buf = [0u8; 32];
533 + loop {
534 + match rustix::io::read(&self.fd, &mut buf) {
535 + Ok(n) if n >= 8 => {
536 + let ty = u32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
537 + if ty == DRM_EVENT_FLIP_COMPLETE {
538 + return;
539 + }
540 + }
541 + _ => return,
542 + }
543 + }
544 + }
545 +}
546 +
547 +/// Konfiguruje jedno wyjście: tryb, bufor dumb, framebuffer i `SETCRTC`.
548 +fn setup_output(fd: BorrowedFd, conn_id: u32, res: &Resources) -> Option<DrmOutput> {
549 + let c = ioctl_rw::<ModeGetConnector, DRM_IOCTL_MODE_GETCONNECTOR, _>(
550 + fd,
551 + ModeGetConnector {
552 + connector_id: conn_id,
553 + ..ModeGetConnector::default()
554 + },
555 + )
556 + .ok()?;
557 + if c.connection != DRM_MODE_CONNECTED {
558 + return None;
559 + }
560 + let count = c.count_modes as usize;
561 + if count == 0 {
562 + return None;
563 + }
564 + let mut modes = vec![ModeInfo::default(); count];
565 + let c = ioctl_rw::<ModeGetConnector, DRM_IOCTL_MODE_GETCONNECTOR, _>(
566 + fd,
567 + ModeGetConnector {
568 + connector_id: conn_id,
569 + count_modes: count as u32,
570 + modes_ptr: modes.as_mut_ptr() as u64,
571 + ..ModeGetConnector::default()
572 + },
573 + )
574 + .ok()?;
575 + let mode = *modes
576 + .iter()
577 + .find(|m| m.type_ & DRM_MODE_TYPE_PREFERRED != 0)
578 + .or_else(|| modes.first())?;
579 +
580 + // Enkoder → CRTC (lub pierwszy możliwy).
581 + let enc = ioctl_rw::<ModeGetEncoder, DRM_IOCTL_MODE_GETENCODER, _>(
582 + fd,
583 + ModeGetEncoder {
584 + encoder_id: c.encoder_id,
585 + ..ModeGetEncoder::default()
586 + },
587 + )
588 + .ok()?;
589 + let crtc_id = if enc.crtc_id != 0 {
590 + enc.crtc_id
591 + } else {
592 + let idx = enc.possible_crtcs.trailing_zeros() as usize;
593 + *res.crtcs.get(idx).or_else(|| res.crtcs.first())?
594 + };
595 +
596 + let width = mode.hdisplay as u32;
597 + let height = mode.vdisplay as u32;
598 + let b0 = create_dumb(fd, width, height)?;
599 + let b1 = create_dumb(fd, width, height)?;
600 + if !set_crtc(fd, crtc_id, b0.fb_id, conn_id, &mode) {
601 + return None;
602 + }
603 + Some(DrmOutput {
604 + name: connector_name(c.connector_type, c.connector_type_id),
605 + crtc_id,
606 + width,
607 + height,
608 + buffers: [b0, b1],
609 + front: 0,
610 + })
611 +}
612 +
613 +/// Tworzy bufor dumb, mapuje go i rejestruje jako framebuffer.
614 +fn create_dumb(fd: BorrowedFd, width: u32, height: u32) -> Option<DumbBuffer> {
615 + let dumb = ioctl_rw::<ModeCreateDumb, DRM_IOCTL_MODE_CREATE_DUMB, _>(
616 + fd,
617 + ModeCreateDumb {
618 + width,
619 + height,
620 + bpp: 32,
621 + ..ModeCreateDumb::default()
622 + },
623 + )
624 + .ok()?;
625 + let map = ioctl_rw::<ModeMapDumb, DRM_IOCTL_MODE_MAP_DUMB, _>(
626 + fd,
627 + ModeMapDumb {
628 + handle: dumb.handle,
629 + ..ModeMapDumb::default()
630 + },
631 + )
632 + .ok()?;
633 + let ptr = unsafe {
634 + mmap(
635 + core::ptr::null_mut(),
636 + dumb.size as usize,
637 + ProtFlags::READ | ProtFlags::WRITE,
638 + MapFlags::SHARED,
639 + fd,
640 + map.offset,
641 + )
642 + }
643 + .ok()? as *mut u8;
644 + let fb = ioctl_rw::<ModeFbCmd2, DRM_IOCTL_MODE_ADDFB2, _>(
645 + fd,
646 + ModeFbCmd2 {
647 + width,
648 + height,
649 + pixel_format: DRM_FORMAT_XRGB8888,
650 + handles: [dumb.handle, 0, 0, 0],
651 + pitches: [dumb.pitch, 0, 0, 0],
652 + ..ModeFbCmd2::default()
653 + },
654 + )
655 + .ok()?;
656 + Some(DumbBuffer {
657 + fb_id: fb.fb_id,
658 + pitch: dumb.pitch as usize,
659 + size: dumb.size as usize,
660 + ptr,
661 + })
662 +}
663 +
664 +/// Ustawia tryb na CRTC (`SETCRTC`).
665 +fn set_crtc(fd: BorrowedFd, crtc_id: u32, fb_id: u32, connector_id: u32, mode: &ModeInfo) -> bool {
666 + let mut connectors = [connector_id];
667 + let req = ModeCrtc {
668 + set_connectors_ptr: connectors.as_mut_ptr() as u64,
669 + count_connectors: 1,
670 + crtc_id,
671 + fb_id,
672 + mode_valid: 1,
673 + mode: *mode,
674 + ..ModeCrtc::default()
675 + };
676 + ioctl_rw::<ModeCrtc, DRM_IOCTL_MODE_SETCRTC, _>(fd, req).is_ok()
677 +}
678 +
679 +/// Przepisuje klatkę RGBA do bufora w formacie `XRGB8888` (kolejność B,G,R,X).
680 +fn swizzle_into(dst: *mut u8, pitch: usize, rgba: &[u8], width: usize, height: usize) {
681 + if dst.is_null() || rgba.len() < width * height * 4 {
682 + return;
683 + }
684 + unsafe {
685 + for y in 0..height {
686 + let src = &rgba[y * width * 4..(y + 1) * width * 4];
687 + let row = dst.add(y * pitch);
688 + for x in 0..width {
689 + let s = &src[x * 4..x * 4 + 4];
690 + let d = row.add(x * 4);
691 + *d = s[2];
692 + *d.add(1) = s[1];
693 + *d.add(2) = s[0];
694 + *d.add(3) = 0xff;
695 + }
696 + }
697 + }
698 +}
699 +
243 700 #[cfg(test)]
244 701 mod tests {
245 702 use super::*;
@@ -258,5 +715,17 @@ mod tests {
258 715 assert_eq!(core::mem::size_of::<ModeCardRes>(), 64);
259 716 assert_eq!(core::mem::size_of::<ModeGetConnector>(), 80);
260 717 assert_eq!(core::mem::size_of::<ModeInfo>(), 68);
718 + assert_eq!(core::mem::size_of::<ModeGetEncoder>(), 20);
719 + assert_eq!(core::mem::size_of::<ModeCrtc>(), 104);
720 + assert_eq!(core::mem::size_of::<ModeCreateDumb>(), 32);
721 + assert_eq!(core::mem::size_of::<ModeMapDumb>(), 16);
722 + assert_eq!(core::mem::size_of::<ModeFbCmd2>(), 104);
723 + assert_eq!(core::mem::size_of::<ModePageFlip>(), 24);
724 + }
725 +
726 + #[test]
727 + fn xrgb8888_fourcc_is_little_endian_bgrx() {
728 + // 'X','R','2','4' — kontroluje kolejność bajtów przy przepisywaniu klatki.
729 + assert_eq!(DRM_FORMAT_XRGB8888.to_le_bytes(), *b"XR24");
261 730 }
262 731 }
@@ -39,6 +39,15 @@ fn main() -> Result<(), Box<dyn Error>> {
39 39 let mut display: Display<State> = Display::new()?;
40 40 let mut handle = display.handle();
41 41
42 + // Diagnostyka DRM: `SELENE_DRM_PROBE=1` sprawdza dostępność mastera bez zmiany trybu.
43 + if std::env::var("SELENE_DRM_PROBE").as_deref() == Ok("1") {
44 + match drm::check_master() {
45 + Some(card) => println!("DRM master available: {}", card.display()),
46 + None => println!("DRM master unavailable (held by another session)"),
47 + }
48 + return Ok(());
49 + }
50 +
42 51 let mut state = State::default();
43 52
44 53 // Wykrywanie monitorów: DRM/KMS, a gdy niedostępne — pojedyncze wyjście nested.
@@ -85,15 +94,19 @@ fn main() -> Result<(), Box<dyn Error>> {
85 94 // Magistrala wejścia (wątek wyjścia → wątek protokołu) z parą gniazd do wybudzania.
86 95 let (events, mut wake_read, wake_write) = input::channel();
87 96
88 - // Wyjście nested: okno na nadrzędnym kompozytorze, budowane w osobnym wątku.
89 - output::spawn(
90 - Arc::clone(&state.windows),
91 - Arc::clone(&state.outputs),
92 - Arc::clone(&events),
93 - wake_write,
94 - 1280,
95 - 800,
96 - );
97 + // Wyjście: realne DRM/KMS (gdy `SELENE_BACKEND=drm`) lub nested na nadrzędnym kompozytorze.
98 + if std::env::var("SELENE_BACKEND").as_deref() == Ok("drm") {
99 + output::spawn_drm(Arc::clone(&state.windows));
100 + } else {
101 + output::spawn(
102 + Arc::clone(&state.windows),
103 + Arc::clone(&state.outputs),
104 + Arc::clone(&events),
105 + wake_write,
106 + 1280,
107 + 800,
108 + );
109 + }
97 110
98 111 let listener_fd = socket.as_raw_fd();
99 112
@@ -1,15 +1,18 @@
1 -//! Wyjście nested: Selene otwiera okno na nadrzędnym kompozytorze i rysuje scenę okien.
1 +//! Wyjście: nested (okno na nadrzędnym kompozytorze) oraz **realne DRM/KMS** (scanout).
2 2 //!
3 -//! Wątek wyjścia pełni dwie role: (1) rysuje skomponowaną scenę przez `libluna-render`
4 -//! i (2) odbiera wejście od nadrzędnego kompozytora, przekazując je do magistrali
5 -//! (`crate::input`), skąd wątek protokołu trasuje je do klientów.
3 +//! Wątek nested pełni dwie role: rysuje scenę przez `libluna-render` i odbiera wejście
4 +//! od nadrzędnego kompozytora, przekazując je do magistrali (`crate::input`).
5 +//! Wątek DRM renderuje scenę wprost do buforów „dumb” i skanuje je na monitorach
6 +//! (page-flip). Wejście w trybie DRM (evdev) jest kolejnym krokiem.
6 7
7 8 use std::os::unix::net::UnixStream;
9 +use std::time::Duration;
8 10
9 -use libluna_core::{Color, Point, Rect, ThemeSettings};
11 +use libluna_core::{Color, Point, Rect, Size, ThemeSettings};
10 12 use libluna_platform::{run, AppConfig, Frame};
11 -use libluna_render::TextRun;
13 +use libluna_render::{Scene, SoftwareRenderer, TextRun};
12 14
15 +use crate::drm::DrmBackend;
13 16 use crate::input::{self, Events};
14 17 use crate::monitors::Outputs;
15 18 use crate::state::{Window, WindowModel};
@@ -41,17 +44,72 @@ pub fn spawn(
41 44 // Przekaż wejście do magistrali.
42 45 input::push(&events, &mut wake, frame.events);
43 46 // Narysuj scenę.
44 - draw(frame, &model);
47 + let size = frame.size;
48 + build_scene(frame.scene, size, 0.0, 0.0, &model);
45 49 // Utrzymuj klatki — model okien zmienia się poza tą pętlą.
46 50 frame.request_frame();
47 51 });
48 52 });
49 53 }
50 54
51 -fn draw(frame: &mut Frame, model: &WindowModel) {
52 - frame.scene.clear();
53 - frame.scene.fill_rect(
54 - Rect::from_size(frame.size),
55 +/// Uruchamia wątek wyjścia DRM/KMS — renderuje scenę do buforów i skanuje na monitorach.
56 +///
57 +/// Wymaga bycia DRM masterem; jeśli inną sesję trzyma master, kończy bez efektu
58 +/// (wywołujący powinien wtedy użyć wyjścia nested).
59 +pub fn spawn_drm(model: WindowModel) {
60 + std::thread::spawn(move || {
61 + let Some(mut backend) = DrmBackend::open() else {
62 + eprintln!(
63 + "{}",
64 + libluna_i18n::tr("DRM backend unavailable (no master)")
65 + );
66 + return;
67 + };
68 + let infos = backend.outputs();
69 + let sizes: Vec<(u32, u32)> = infos.iter().map(|(_, w, h)| (*w, *h)).collect();
70 + let origins: Vec<(f64, f64)> = {
71 + // Wyjścia ułożone poziomo (spójne z modelem monitorów).
72 + let mut x = 0.0;
73 + sizes
74 + .iter()
75 + .map(|(w, _)| {
76 + let o = (x, 0.0);
77 + x += *w as f64;
78 + o
79 + })
80 + .collect()
81 + };
82 + for (name, w, h) in &infos {
83 + println!("{}: {} {}x{}", libluna_i18n::tr("DRM output"), name, w, h);
84 + }
85 + let mut renderers: Vec<SoftwareRenderer> = sizes
86 + .iter()
87 + .map(|(w, h)| SoftwareRenderer::new(*w, *h))
88 + .collect();
89 + let bg = Color::from_rgba8(0x1e, 0x20, 0x28, 0xff);
90 +
91 + loop {
92 + for i in 0..sizes.len() {
93 + let (w, h) = sizes[i];
94 + let (ox, oy) = origins[i];
95 + let mut scene = Scene::new();
96 + build_scene(&mut scene, Size::new(w as f64, h as f64), ox, oy, &model);
97 + renderers[i].draw(&scene, 1.0, bg);
98 + backend.present(i, renderers[i].data());
99 + backend.wait_flip();
100 + }
101 + // Bez wejścia w trybie DRM odświeżamy w stałym rytmie (~60 Hz).
102 + std::thread::sleep(Duration::from_millis(16));
103 + }
104 + });
105 +}
106 +
107 +/// Buduje scenę okien w rozmiarze wyjścia, przesuniętą o `(ox, oy)` (origin monitora).
108 +fn build_scene(scene: &mut Scene, size: Size, ox: f64, oy: f64, model: &WindowModel) {
109 + let sh = |r: Rect| r.offset(-ox, -oy);
110 + scene.clear();
111 + scene.fill_rect(
112 + Rect::from_size(size),
55 113 Color::from_rgba8(0x1e, 0x20, 0x28, 0xff),
56 114 );
57 115
@@ -60,50 +118,47 @@ fn draw(frame: &mut Frame, model: &WindowModel) {
60 118 let mut order: Vec<&Window> = windows.iter().collect();
61 119 order.sort_by_key(|w| w.z);
62 120 for w in order {
63 - if !w.mapped {
121 + if !w.mapped || w.minimized {
64 122 continue;
65 123 }
66 124 // Tło treści (na wypadek przezroczystości bufora klienta).
67 - frame
68 - .scene
69 - .fill_rect(w.content(), Color::from_rgba8(0x00, 0x00, 0x00, 0xff));
125 + scene.fill_rect(sh(w.content()), Color::from_rgba8(0x00, 0x00, 0x00, 0xff));
70 126 // Treść okna (bufor klienta).
71 - frame.scene.image(w.content(), w.data.clone());
127 + scene.image(sh(w.content()), w.data.clone());
72 128 // Dekoracja tylko dla topleveli (popupy i panele jej nie mają).
73 129 if !w.popup && !w.layer {
74 - frame
75 - .scene
76 - .fill_rect(w.titlebar(), Color::from_rgba8(0x2c, 0x2f, 0x38, 0xff));
77 - frame.scene.text(
78 - TextRun::new(Point::new(w.x + 10.0, w.y + 20.0), &w.title, 13.0)
130 + scene.fill_rect(sh(w.titlebar()), Color::from_rgba8(0x2c, 0x2f, 0x38, 0xff));
131 + scene.text(
132 + TextRun::new(Point::new(w.x - ox + 10.0, w.y - oy + 20.0), &w.title, 13.0)
79 133 .with_color(Color::from_rgba8(0xee, 0xee, 0xee, 0xff)),
80 134 );
81 135 let glyph = Color::from_rgba8(0xd0, 0xd0, 0xd0, 0xff);
136 + let shift = |p: Point| Point::new(p.x - ox, p.y - oy);
82 137 // Zamknięcie — krzyżyk.
83 - let c = w.close_rect().center();
84 - frame.scene.stroke_line(
138 + let c = shift(w.close_rect().center());
139 + scene.stroke_line(
85 140 Point::new(c.x - 4.0, c.y - 4.0),
86 141 Point::new(c.x + 4.0, c.y + 4.0),
87 142 1.6,
88 143 glyph,
89 144 );
90 - frame.scene.stroke_line(
145 + scene.stroke_line(
91 146 Point::new(c.x + 4.0, c.y - 4.0),
92 147 Point::new(c.x - 4.0, c.y + 4.0),
93 148 1.6,
94 149 glyph,
95 150 );
96 151 // Maksymalizacja — kwadrat.
97 - let m = w.maximize_rect().center();
98 - frame.scene.stroke_rounded_rect(
152 + let m = shift(w.maximize_rect().center());
153 + scene.stroke_rounded_rect(
99 154 Rect::from_xywh(m.x - 4.0, m.y - 4.0, 8.0, 8.0),
100 155 0.0,
101 156 1.4,
102 157 glyph,
103 158 );
104 159 // Minimalizacja — kreska.
105 - let n = w.minimize_rect().center();
106 - frame.scene.stroke_line(
160 + let n = shift(w.minimize_rect().center());
161 + scene.stroke_line(
107 162 Point::new(n.x - 4.0, n.y + 3.0),
108 163 Point::new(n.x + 4.0, n.y + 3.0),
109 164 1.6,
@@ -112,8 +167,8 @@ fn draw(frame: &mut Frame, model: &WindowModel) {
112 167 }
113 168 // Obramowanie całego okna (tylko zwykłe okna).
114 169 if !w.layer {
115 - frame.scene.stroke_rounded_rect(
116 - w.outer(),
170 + scene.stroke_rounded_rect(
171 + sh(w.outer()),
117 172 6.0,
118 173 1.0,
119 174 Color::from_rgba8(0x50, 0x54, 0x60, 0xff),
@@ -122,7 +177,7 @@ fn draw(frame: &mut Frame, model: &WindowModel) {
122 177 }
123 178 }
124 179
125 - frame.scene.text(
180 + scene.text(
126 181 TextRun::new(Point::new(16.0, 28.0), "Selene — kompozytor Luna", 14.0)
127 182 .with_color(Color::from_rgba8(0xdd, 0xdd, 0xdd, 0xff)),
128 183 );
@@ -152,10 +152,14 @@ a na końcu odtworzył realne aplikacje środowiska Luna z makiet.
152 152 ✅ M-K6: **schowek** (`wl_data_device_manager`: `data_source`/`data_offer`/`selection`,
153 153 kopiuj/wklej między klientami) oraz **układy klawiatury** z otoczenia
154 154 (`LUNA_KEYBOARD_LAYOUT`/`LUNA_KEYBOARD_VARIANT`) i `wl_keyboard.repeat_info`.
155 + ✅ M-K7: **drag & drop** (`start_drag`, `enter`/`motion`/`leave`/`drop`, `dnd_finished`)
156 + oraz **realne wyjście DRM/KMS** (`SELENE_BACKEND=drm`, opcjonalne): master, bufor dumb,
157 + `ADDFB2`, `SETCRTC`, podwójne buforowanie i **page-flip**; bezpieczny fallback do nested,
158 + gdy master trzyma inna sesja.
155 159 ✅ Optymalizacje: cache `mmap` puli `wl_shm` i zero-copy w rendererze (`PixmapRef::from_bytes`).
156 160 Uruchomienie: `cargo run -p selene` (szczegóły: `de/README.md`).
157 -- Dalej: realny backend DRM/KMS + GLES (skanowanie, page-flip, bufory), drag & drop
158 - i selekcja pierwotna, IME (`text-input`), hotplug monitorów (`zwlr_output_manager`).
161 +- Dalej: **wejście w trybie DRM** (evdev/`libinput`), GLES/akceleracja, hotplug monitorów
162 + (`zwlr_output_manager`), selekcja pierwotna, IME (`text-input`).
159 163 - **Luna** (`de/luna`) — powłoka środowiska (panel, launcher, OSD) na `libluna-widgets`.
160 164
161 165 ## Później (poza MVP)