Commit 49cbca4
2
files
+263
added
-65
deleted
@@ -26,6 +26,8 @@ const DRM_IOCTL_MODE_CREATE_DUMB: u32 = 0xC020_64B2;
26
26
const DRM_IOCTL_MODE_MAP_DUMB: u32 = 0xC010_64B3;
27
27
const DRM_IOCTL_MODE_ADDFB2: u32 = 0xC068_64B8;
28
28
const DRM_IOCTL_MODE_PAGE_FLIP: u32 = 0xC018_64B0;
29
+/// `drm_mode_cursor` (`sizeof` = 28 = 0x1C).
30
+const DRM_IOCTL_MODE_CURSOR: u32 = 0xC01C_64A3;
29
31
30
32
// — ioctle bez argumentu (`_IO`) —
31
33
const DRM_IOCTL_SET_MASTER: u32 = 0x0000_641E;
@@ -38,6 +40,12 @@ const DRM_MODE_TYPE_PREFERRED: u32 = 1 << 3;
38
40
const DRM_MODE_PAGE_FLIP_EVENT: u32 = 1;
39
41
/// Cztery znaki `XRGB8888` (kolejność bajtów B,G,R,X).
40
42
const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258;
43
+/// Cztery znaki `ARGB8888` (kolejność bajtów B,G,R,A) — format kursora.
44
+const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241;
45
+/// `DRM_MODE_CURSOR_BO` — ustaw obraz kursora (albo wyłącz, gdy `handle` = 0).
46
+const DRM_MODE_CURSOR_BO: u32 = 0x01;
47
+/// `DRM_MODE_CURSOR_MOVE` — przesuń kursor bez zmiany obrazu.
48
+const DRM_MODE_CURSOR_MOVE: u32 = 0x02;
41
49
/// `DRM_EVENT_FLIP_COMPLETE`.
42
50
const DRM_EVENT_FLIP_COMPLETE: u32 = 2;
43
51
@@ -176,6 +184,19 @@ struct ModePageFlip {
176
184
user_data: u64,
177
185
}
178
186
187
+/// `struct drm_mode_cursor` — ustawienie/przesunięcie kursora sprzętowego.
188
+#[repr(C)]
189
+#[derive(Clone, Copy, Default)]
190
+struct ModeCursor {
191
+ flags: u32,
192
+ crtc_id: u32,
193
+ x: i32,
194
+ y: i32,
195
+ width: u32,
196
+ height: u32,
197
+ handle: u32,
198
+}
199
+
179
200
/// Opakowanie na ioctl typu „read-write” z jednym struktem wejścia/wyjścia.
180
201
struct Rw<T, const OP: u32>(MaybeUninit<T>);
181
202
@@ -426,6 +447,8 @@ fn fetch_resources(fd: BorrowedFd) -> Option<Resources> {
426
447
/// Bufor „dumb” (bez GPU): sterta pamięci zmapowana do renderowania i skanowania.
427
448
struct DumbBuffer {
428
449
fb_id: u32,
450
+ /// Uchwyt GEM (potrzebny m.in. do kursora sprzętowego).
451
+ handle: u32,
429
452
pitch: usize,
430
453
size: usize,
431
454
ptr: *mut u8,
@@ -453,6 +476,8 @@ struct DrmOutput {
453
476
hz: u32,
454
477
buffers: [DumbBuffer; 2],
455
478
front: usize,
479
+ /// Bufor obrazu kursora sprzętowego (tworzony leniwie).
480
+ cursor: Option<DumbBuffer>,
456
481
}
457
482
458
483
/// Powód, dla którego nie udało się otworzyć wyjścia DRM (do diagnostyki).
@@ -566,6 +591,73 @@ impl DrmBackend {
566
591
}
567
592
}
568
593
}
594
+
595
+ /// Ustawia obraz kursora sprzętowego na wyjściu `idx` w pozycji `(x, y)`.
596
+ ///
597
+ /// `false`, gdy sterownik nie obsługuje kursora — wywołujący wraca do kursora
598
+ /// programowego (rysowanego w scenie).
599
+ pub fn set_cursor_image(
600
+ &mut self,
601
+ idx: usize,
602
+ rgba: &[u8],
603
+ w: u32,
604
+ h: u32,
605
+ x: i32,
606
+ y: i32,
607
+ ) -> bool {
608
+ let fd = self.fd.as_fd();
609
+ let Some(out) = self.outputs.get_mut(idx) else {
610
+ return false;
611
+ };
612
+ if out.cursor.is_none() {
613
+ match create_dumb_fmt(fd, w, h, DRM_FORMAT_ARGB8888, false) {
614
+ Some(b) => out.cursor = Some(b),
615
+ None => return false,
616
+ }
617
+ }
618
+ let Some(cur) = out.cursor.as_mut() else {
619
+ return false;
620
+ };
621
+ swizzle_argb_into(cur.ptr, cur.pitch, rgba, w as usize, h as usize);
622
+ let req = ModeCursor {
623
+ flags: DRM_MODE_CURSOR_BO,
624
+ crtc_id: out.crtc_id,
625
+ x,
626
+ y,
627
+ width: w,
628
+ height: h,
629
+ handle: cur.handle,
630
+ };
631
+ ioctl_rw::<ModeCursor, DRM_IOCTL_MODE_CURSOR, _>(&self.fd, req).is_ok()
632
+ }
633
+
634
+ /// Przesuwa kursor sprzętowy na wyjściu `idx` (bez zmiany obrazu).
635
+ pub fn move_cursor(&mut self, idx: usize, x: i32, y: i32) -> bool {
636
+ let Some(crtc_id) = self.outputs.get(idx).map(|o| o.crtc_id) else {
637
+ return false;
638
+ };
639
+ let req = ModeCursor {
640
+ flags: DRM_MODE_CURSOR_MOVE,
641
+ crtc_id,
642
+ x,
643
+ y,
644
+ ..ModeCursor::default()
645
+ };
646
+ ioctl_rw::<ModeCursor, DRM_IOCTL_MODE_CURSOR, _>(&self.fd, req).is_ok()
647
+ }
648
+
649
+ /// Wyłącza kursor sprzętowy na wyjściu `idx`.
650
+ pub fn hide_cursor(&mut self, idx: usize) -> bool {
651
+ let Some(crtc_id) = self.outputs.get(idx).map(|o| o.crtc_id) else {
652
+ return false;
653
+ };
654
+ let req = ModeCursor {
655
+ flags: DRM_MODE_CURSOR_BO,
656
+ crtc_id,
657
+ ..ModeCursor::default()
658
+ };
659
+ ioctl_rw::<ModeCursor, DRM_IOCTL_MODE_CURSOR, _>(&self.fd, req).is_ok()
660
+ }
569
661
}
570
662
571
663
/// Konfiguruje jedno wyjście: tryb, bufor dumb, framebuffer i `SETCRTC`.
@@ -629,11 +721,24 @@ fn setup_output(fd: BorrowedFd, conn_id: u32, res: &Resources) -> Option<DrmOutp
629
721
hz: mode.vrefresh,
630
722
buffers: [b0, b1],
631
723
front: 0,
724
+ cursor: None,
632
725
})
633
726
}
634
727
635
-/// Tworzy bufor dumb, mapuje go i rejestruje jako framebuffer.
728
+/// Tworzy bufor dumb, mapuje go i rejestruje jako framebuffer (scanout).
636
729
fn create_dumb(fd: BorrowedFd, width: u32, height: u32) -> Option<DumbBuffer> {
730
+ create_dumb_fmt(fd, width, height, DRM_FORMAT_XRGB8888, true)
731
+}
732
+
733
+/// Tworzy bufor dumb w zadanym formacie; `register_fb` dodaje też framebuffer
734
+/// (niepotrzebne dla kursora, gdzie liczy się tylko uchwyt GEM).
735
+fn create_dumb_fmt(
736
+ fd: BorrowedFd,
737
+ width: u32,
738
+ height: u32,
739
+ format: u32,
740
+ register_fb: bool,
741
+) -> Option<DumbBuffer> {
637
742
let dumb = ioctl_rw::<ModeCreateDumb, DRM_IOCTL_MODE_CREATE_DUMB, _>(
638
743
fd,
639
744
ModeCreateDumb {
@@ -663,20 +768,26 @@ fn create_dumb(fd: BorrowedFd, width: u32, height: u32) -> Option<DumbBuffer> {
663
768
)
664
769
}
665
770
.ok()? as *mut u8;
666
- let fb = ioctl_rw::<ModeFbCmd2, DRM_IOCTL_MODE_ADDFB2, _>(
667
- fd,
668
- ModeFbCmd2 {
669
- width,
670
- height,
671
- pixel_format: DRM_FORMAT_XRGB8888,
672
- handles: [dumb.handle, 0, 0, 0],
673
- pitches: [dumb.pitch, 0, 0, 0],
674
- ..ModeFbCmd2::default()
675
- },
676
- )
677
- .ok()?;
771
+ let fb_id = if register_fb {
772
+ ioctl_rw::<ModeFbCmd2, DRM_IOCTL_MODE_ADDFB2, _>(
773
+ fd,
774
+ ModeFbCmd2 {
775
+ width,
776
+ height,
777
+ pixel_format: format,
778
+ handles: [dumb.handle, 0, 0, 0],
779
+ pitches: [dumb.pitch, 0, 0, 0],
780
+ ..ModeFbCmd2::default()
781
+ },
782
+ )
783
+ .ok()?
784
+ .fb_id
785
+ } else {
786
+ 0
787
+ };
678
788
Some(DumbBuffer {
679
- fb_id: fb.fb_id,
789
+ fb_id,
790
+ handle: dumb.handle,
680
791
pitch: dumb.pitch as usize,
681
792
size: dumb.size as usize,
682
793
ptr,
@@ -698,6 +809,27 @@ fn set_crtc(fd: BorrowedFd, crtc_id: u32, fb_id: u32, connector_id: u32, mode: &
698
809
ioctl_rw::<ModeCrtc, DRM_IOCTL_MODE_SETCRTC, _>(fd, req).is_ok()
699
810
}
700
811
812
+/// Przepisuje RGBA do bufora kursora w formacie `ARGB8888` (kolejność B,G,R,A).
813
+fn swizzle_argb_into(dst: *mut u8, pitch: usize, rgba: &[u8], width: usize, height: usize) {
814
+ if dst.is_null() || rgba.len() < width * height * 4 {
815
+ return;
816
+ }
817
+ unsafe {
818
+ for y in 0..height {
819
+ let src = &rgba[y * width * 4..(y + 1) * width * 4];
820
+ let row = dst.add(y * pitch);
821
+ for x in 0..width {
822
+ let s = &src[x * 4..x * 4 + 4];
823
+ let d = row.add(x * 4);
824
+ *d = s[2];
825
+ *d.add(1) = s[1];
826
+ *d.add(2) = s[0];
827
+ *d.add(3) = s[3];
828
+ }
829
+ }
830
+ }
831
+}
832
+
701
833
/// Przepisuje klatkę RGBA do bufora w formacie `XRGB8888` (kolejność B,G,R,X).
702
834
fn swizzle_into(dst: *mut u8, pitch: usize, rgba: &[u8], width: usize, height: usize) {
703
835
if dst.is_null() || rgba.len() < width * height * 4 {
@@ -743,12 +875,15 @@ mod tests {
743
875
assert_eq!(core::mem::size_of::<ModeMapDumb>(), 16);
744
876
assert_eq!(core::mem::size_of::<ModeFbCmd2>(), 104);
745
877
assert_eq!(core::mem::size_of::<ModePageFlip>(), 24);
878
+ assert_eq!(core::mem::size_of::<ModeCursor>(), 28);
746
879
}
747
880
748
881
#[test]
749
882
fn xrgb8888_fourcc_is_little_endian_bgrx() {
750
883
// 'X','R','2','4' — kontroluje kolejność bajtów przy przepisywaniu klatki.
751
884
assert_eq!(DRM_FORMAT_XRGB8888.to_le_bytes(), *b"XR24");
885
+ // 'A','R','2','4' — format kursora (z kanałem alfa).
886
+ assert_eq!(DRM_FORMAT_ARGB8888.to_le_bytes(), *b"AR24");
752
887
}
753
888
754
889
fn mode(w: u16, h: u16, hz: u32, preferred: bool) -> ModeInfo {
@@ -140,6 +140,24 @@ pub fn spawn_drm(model: WindowModel, cursor: CursorModel) {
140
140
.collect();
141
141
let bg = Color::from_rgba8(0x1e, 0x20, 0x28, 0xff);
142
142
143
+ // Sprzętowy kursor: renderujemy raz obraz strzałki (ARGB) i odtąd poruszamy
144
+ // tylko płaszczyzną kursora — ruch myszy nie przemalowuje sceny.
145
+ let (cursor_rgba, cw, ch) = cursor_image();
146
+ let mut hw_cursor = true;
147
+ for i in 0..sizes.len() {
148
+ // Próba ustawienia obrazu (potem chowamy); gdy sterownik nie umie — kursor programowy.
149
+ if !backend.set_cursor_image(i, &cursor_rgba, cw, ch, 0, 0) || !backend.hide_cursor(i) {
150
+ hw_cursor = false;
151
+ }
152
+ }
153
+ if !hw_cursor {
154
+ eprintln!(
155
+ "{}",
156
+ libluna_i18n::tr("DRM hardware cursor unavailable; using software cursor")
157
+ );
158
+ }
159
+ let mut cursor_shown = vec![false; sizes.len()];
160
+
143
161
// Diagnostyka (SELENE_STATS=1): fps i rozbicie czasu na render/present/wait_flip.
144
162
let stats = std::env::var("SELENE_STATS").is_ok();
145
163
let mut flip_error_logged = false;
@@ -147,74 +165,119 @@ pub fn spawn_drm(model: WindowModel, cursor: CursorModel) {
147
165
let (mut ms_render, mut ms_present, mut ms_wait) = (0.0f64, 0.0f64, 0.0f64);
148
166
let mut report = std::time::Instant::now();
149
167
let mut last_sig: Option<u64> = None;
168
+ let mut last_cur: Option<Point> = None;
150
169
loop {
151
- // Przerysowujemy tylko, gdy coś się zmieniło (okna albo kursor). Inaczej
152
- // śpimy — mniej CPU i brak zbędnych page-flipów, przy płynnym kursorze.
153
170
let cur = *cursor.lock_or_recover();
154
- let sig = scene_signature(&model, cur);
155
- if last_sig == Some(sig) {
171
+
172
+ // Sygnatura sceny nie zależy od kursora, gdy rysuje go sprzęt — dzięki temu
173
+ // samo przesunięcie myszy nie powoduje przemalowania sceny.
174
+ let sig = scene_signature(&model, if hw_cursor { None } else { cur });
175
+ let scene_changed = last_sig != Some(sig);
176
+ let cur_changed = last_cur != cur;
177
+ if !scene_changed && !cur_changed {
156
178
std::thread::sleep(Duration::from_millis(4));
157
179
continue;
158
180
}
159
- last_sig = Some(sig);
181
+ last_cur = cur;
160
182
161
- for i in 0..sizes.len() {
162
- let (w, h) = sizes[i];
163
- let (ox, oy) = origins[i];
183
+ // Kursor sprzętowy: ustaw/przesuń płaszczyznę kursora (bez dotykania sceny).
184
+ if hw_cursor && cur_changed {
185
+ for i in 0..sizes.len() {
186
+ let (w, h) = sizes[i];
187
+ let (ox, oy) = origins[i];
188
+ let local = cur.map(|c| ((c.x - ox) as i32, (c.y - oy) as i32));
189
+ let inside = cur
190
+ .map(|c| {
191
+ let (lx, ly) = (c.x - ox, c.y - oy);
192
+ lx >= 0.0 && ly >= 0.0 && lx < w as f64 && ly < h as f64
193
+ })
194
+ .unwrap_or(false);
195
+ if inside {
196
+ let (x, y) = local.unwrap();
197
+ if cursor_shown[i] {
198
+ backend.move_cursor(i, x, y);
199
+ } else {
200
+ backend.set_cursor_image(i, &cursor_rgba, cw, ch, x, y);
201
+ cursor_shown[i] = true;
202
+ }
203
+ } else if cursor_shown[i] {
204
+ backend.hide_cursor(i);
205
+ cursor_shown[i] = false;
206
+ }
207
+ }
208
+ }
164
209
165
- let t = std::time::Instant::now();
166
- let mut scene = Scene::new();
167
- build_scene(
168
- &mut scene,
169
- Size::new(w as f64, h as f64),
170
- ox,
171
- oy,
172
- &model,
173
- cur,
174
- );
175
- renderers[i].draw(&scene, 1.0, bg);
176
- ms_render += t.elapsed().as_secs_f64() * 1000.0;
210
+ if scene_changed {
211
+ last_sig = Some(sig);
212
+ for i in 0..sizes.len() {
213
+ let (w, h) = sizes[i];
214
+ let (ox, oy) = origins[i];
177
215
178
- let t = std::time::Instant::now();
179
- let flipped = backend.present(i, renderers[i].data());
180
- if !flipped && !flip_error_logged {
181
- eprintln!(
182
- "{}",
183
- libluna_i18n::tr("DRM page-flip failed (screen may stay blank)")
216
+ let t = std::time::Instant::now();
217
+ let mut scene = Scene::new();
218
+ build_scene(
219
+ &mut scene,
220
+ Size::new(w as f64, h as f64),
221
+ ox,
222
+ oy,
223
+ &model,
224
+ if hw_cursor { None } else { cur },
184
225
);
185
- flip_error_logged = true;
186
- }
187
- ms_present += t.elapsed().as_secs_f64() * 1000.0;
226
+ renderers[i].draw(&scene, 1.0, bg);
227
+ ms_render += t.elapsed().as_secs_f64() * 1000.0;
188
228
189
- // Czekamy na flip tylko, gdy został zakolejkowany (inaczej `read`
190
- // mógłby zablokować się na zawsze).
191
- if flipped {
192
229
let t = std::time::Instant::now();
193
- backend.wait_flip();
194
- ms_wait += t.elapsed().as_secs_f64() * 1000.0;
230
+ let flipped = backend.present(i, renderers[i].data());
231
+ if !flipped && !flip_error_logged {
232
+ eprintln!(
233
+ "{}",
234
+ libluna_i18n::tr("DRM page-flip failed (screen may stay blank)")
235
+ );
236
+ flip_error_logged = true;
237
+ }
238
+ ms_present += t.elapsed().as_secs_f64() * 1000.0;
239
+
240
+ // Czekamy na flip tylko, gdy został zakolejkowany (inaczej `read`
241
+ // mógłby zablokować się na zawsze).
242
+ if flipped {
243
+ let t = std::time::Instant::now();
244
+ backend.wait_flip();
245
+ ms_wait += t.elapsed().as_secs_f64() * 1000.0;
246
+ }
247
+ }
248
+ frames += 1;
249
+ if stats && report.elapsed().as_secs_f64() >= 2.0 {
250
+ let secs = report.elapsed().as_secs_f64();
251
+ let per_frame = (frames * sizes.len() as u64).max(1) as f64;
252
+ eprintln!(
253
+ "selene/drm: {:.1} klatek/s | render {:.2} ms, present {:.2} ms, wait_flip {:.2} ms",
254
+ frames as f64 / secs,
255
+ ms_render / per_frame,
256
+ ms_present / per_frame,
257
+ ms_wait / per_frame
258
+ );
259
+ frames = 0;
260
+ ms_render = 0.0;
261
+ ms_present = 0.0;
262
+ ms_wait = 0.0;
263
+ report = std::time::Instant::now();
195
264
}
196
- }
197
- frames += 1;
198
- if stats && report.elapsed().as_secs_f64() >= 2.0 {
199
- let secs = report.elapsed().as_secs_f64();
200
- let per_frame = (frames * sizes.len() as u64).max(1) as f64;
201
- eprintln!(
202
- "selene/drm: {:.1} fps | render {:.2} ms, present {:.2} ms, wait_flip {:.2} ms",
203
- frames as f64 / secs,
204
- ms_render / per_frame,
205
- ms_present / per_frame,
206
- ms_wait / per_frame
207
- );
208
- frames = 0;
209
- ms_render = 0.0;
210
- ms_present = 0.0;
211
- ms_wait = 0.0;
212
- report = std::time::Instant::now();
213
265
}
214
266
}
215
267
});
216
268
}
217
269
270
+/// Obraz kursora (strzałka) w RGBA8 — do płaszczyzny kursora DRM.
271
+fn cursor_image() -> (Vec<u8>, u32, u32) {
272
+ let (w, h) = (64u32, 64u32);
273
+ let mut renderer = SoftwareRenderer::new(w, h);
274
+ let mut scene = Scene::new();
275
+ scene.clear();
276
+ draw_cursor(&mut scene, Point::new(2.0, 2.0));
277
+ renderer.draw(&scene, 1.0, Color::rgba(0.0, 0.0, 0.0, 0.0));
278
+ (renderer.data().to_vec(), w, h)
279
+}
280
+
218
281
/// Sygnatura sceny (okna + kursor) — do wykrywania, że nic się nie zmieniło.
219
282
fn scene_signature(model: &WindowModel, cursor: Option<Point>) -> u64 {
220
283
let mut h: u64 = 0xcbf2_9ce4_8422_2325;