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)