← luna

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;