⎇ 758f61f52a35514669064a1aefd44f22f07a9e9c
crates/apps/mirada-compositor/src/drm_backend.rs
History
Who touched what
raw
feat(carmen): modo greeter — mirada-compositor como DM
1//! `drm_backend` — el Cuerpo del compositor sobre **DRM/KMS**, sin
2//! sesión gráfica anfitriona: corre directo sobre una TTY, como tu
3//! escritorio de verdad.
4//!
5//! Construido por fases para verificarlo en hardware paso a paso:
6//!
7//! - **Fase 1 — bring-up**: sesión (`libseat`), GPU, dispositivo DRM,
8//! enumerar salidas.
9//! - **Fase 2a — pipeline de render**: GBM, EGL y `GlesRenderer`, con un
10//! `DrmCompositor` para la salida conectada.
11//! - **Fase 2b — bucle Wayland** (esto): un bucle `calloop` que atiende
12//! a los clientes Wayland, el teclado (`libinput`) y el VBlank, y
13//! compone las ventanas de verdad. Aquí `mirada-compositor --drm` ya
14//! es un escritorio funcionando.
15//!
16//! Todo con logs para diagnosticar sin el hardware delante.
17
18use std::error::Error;
19use std::sync::Arc;
20use std::time::{Duration, Instant};
21
22use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
23use smithay::backend::allocator::Fourcc;
24use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags};
25use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
26use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent};
27use smithay::backend::egl::{EGLContext, EGLDisplay};
28use smithay::backend::input::{
29 AbsolutePositionEvent, Axis, AxisSource, ButtonState, InputEvent, KeyState, KeyboardKeyEvent,
30 PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
31};
32use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
33use smithay::backend::renderer::element::solid::SolidColorRenderElement;
34use smithay::backend::renderer::element::surface::{
35 render_elements_from_surface_tree, WaylandSurfaceRenderElement,
36};
37use smithay::backend::renderer::element::{render_elements, Id, Kind};
38use smithay::backend::renderer::gles::GlesRenderer;
39use smithay::backend::renderer::utils::CommitCounter;
40use smithay::backend::renderer::{ImportAll, ImportDma};
41use smithay::backend::session::libseat::LibSeatSession;
42use smithay::backend::session::{Event as SessionEvent, Session};
43use smithay::backend::udev;
44use smithay::input::keyboard::FilterResult;
45use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
46use smithay::output::OutputModeSource;
47use smithay::reexports::calloop::channel::{channel as ticket_channel, Event as TicketEvent};
48use smithay::reexports::calloop::generic::Generic;
49use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
50use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction};
51use smithay::reexports::drm::control::connector::State as ConnectorState;
52use smithay::reexports::drm::control::{Device as ControlDevice, ModeTypeFlags};
53use smithay::reexports::input::Libinput;
54use smithay::reexports::rustix::fs::OFlags;
55use smithay::reexports::wayland_server::{Display, ListeningSocket};
56use smithay::utils::{
57 DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
58};
59
60use brahman_auth::SessionTicket;
61use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
62
63use crate::{
64 combo_string, send_frames_surface_tree, App, BodyMode, Brain, ClientState, DragGrab, DragMode,
65 Setup,
66};
67
68/// El `DrmCompositor` concreto para la salida (un solo GPU).
69type Compositor =
70 DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>;
71
72render_elements! {
73 /// Lo que el backend DRM compone en un cuadro: superficies de cliente
74 /// y rectángulos de color sólido (el cursor y los marcos de ventana).
75 Frame<R> where R: ImportAll;
76 Window = WaylandSurfaceRenderElement<R>,
77 Solid = SolidColorRenderElement,
78}
79
80/// Color de fondo del escritorio cuando no hay nada que lo tape.
81const CLEAR_COLOR: [f32; 4] = [0.05, 0.05, 0.08, 1.0];
82
83/// Lado del cursor de software, en píxeles.
84const CURSOR_SIZE: i32 = 12;
85
86/// Color del cursor — un cuadrado casi blanco, opaco.
87const CURSOR_COLOR: [f32; 4] = [0.95, 0.95, 0.97, 1.0];
88
89/// Lado mínimo de una ventana al redimensionarla con el ratón.
90const MIN_WINDOW: i32 = 120;
91
92/// Grosor del marco de una ventana, en píxeles.
93const BORDER_WIDTH: i32 = 2;
94
95/// Color del marco de la ventana enfocada — un azul que resalta.
96const BORDER_FOCUS: [f32; 4] = [0.36, 0.56, 0.92, 1.0];
97
98/// Color del marco de las ventanas sin foco — gris discreto.
99const BORDER_NORMAL: [f32; 4] = [0.22, 0.22, 0.27, 1.0];
100
101/// Los 4 rectángulos `(x, y, w, h)` del marco de una ventana cuyo
102/// contenido ocupa `(sx, sy, sw, sh)`. El marco va *hacia adentro* (pisa
103/// el borde de la superficie), así nunca se solapa con el de la ventana
104/// vecina: arriba, abajo, izquierda, derecha.
105fn border_rects(sx: i32, sy: i32, sw: i32, sh: i32) -> [(i32, i32, i32, i32); 4] {
106 let bw = BORDER_WIDTH;
107 let side_h = (sh - 2 * bw).max(0);
108 [
109 (sx, sy, sw, bw),
110 (sx, sy + sh - bw, sw, bw),
111 (sx, sy + bw, bw, side_h),
112 (sx + sw - bw, sy + bw, bw, side_h),
113 ]
114}
115
116/// Códigos de botón de `<linux/input-event-codes.h>`.
117const BTN_LEFT: u32 = 0x110;
118const BTN_RIGHT: u32 = 0x111;
119
120/// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`.
121struct DrmState {
122 app: App,
123 display: Display<App>,
124 /// El dispositivo DRM — se conserva para pausarlo y reactivarlo al
125 /// conmutar de VT.
126 drm: DrmDevice,
127 compositor: Compositor,
128 renderer: GlesRenderer,
129 /// Contexto `libinput` — se suspende y reanuda al conmutar de VT.
130 libinput: Libinput,
131 /// `false` mientras la sesión está cedida a otra VT — no se compone.
132 active: bool,
133 /// `true` entre que se encola un page-flip y llega su VBlank.
134 pending_flip: bool,
135 keymap_path: Option<std::path::PathBuf>,
136 keymap_watch: Option<mirada_brain::KeymapWatch>,
137 ctl: Option<crate::CtlServer>,
138 /// Inicio del compositor — base de tiempos para los frame-callbacks.
139 start: Instant,
140 /// Nº de ventanas en el último `tick` — para registrar los cambios.
141 last_windows: usize,
142 /// Identidad estable del cursor de software — el seguimiento de daño
143 /// la usa para no recomponer todo cuando el cursor sólo se mueve.
144 cursor_id: Id,
145 /// Ventana sobre la que estaba el puntero — para el foco-sigue-ratón.
146 last_pointer_window: Option<u64>,
147 /// Tamaño de la salida, en píxeles — los topes del puntero.
148 output_size: (f64, f64),
149}
150
151impl DrmState {
152 /// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
153 fn render(&mut self) {
154 if !self.active {
155 return; // la sesión está en otra VT — no tocamos la GPU
156 }
157 if self.pending_flip {
158 return; // aún esperamos el VBlank del cuadro anterior
159 }
160 let output_h = self.app.output_size.1;
161
162 // Paso 1 · refresca los búferes del marco de cada ventana — su
163 // tamaño (sigue al contenido) y su color (según el foco). Cada
164 // `SolidColorBuffer` sube su contador de daño sólo si algo cambió.
165 for w in &mut self.app.windows {
166 if !w.visible || w.is_shell {
167 continue; // el shell no lleva marco
168 }
169 let (x, y) = crate::render_loc(w, output_h);
170 let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
171 let color = if w.focused { BORDER_FOCUS } else { BORDER_NORMAL };
172 let rects = border_rects(x, y, sw, sh);
173 for (buf, (_, _, bw, bh)) in w.borders.iter_mut().zip(rects) {
174 buf.update((bw, bh), color);
175 }
176 }
177
178 // Paso 2 · arma los elementos — lista front-to-back (índice 0 =
179 // encima): el cursor, y por cada ventana su marco sobre su
180 // superficie. Las flotantes van antes que las teseladas.
181 let elements: Vec<Frame<GlesRenderer>> = {
182 let mut out: Vec<Frame<GlesRenderer>> = Vec::new();
183
184 // El cursor — la superficie que pidió el cliente (la «I» del
185 // texto, una mano…), o el cuadrado por defecto si pidió un
186 // cursor con nombre y no hay tema. `Hidden` no pinta nada.
187 let (cx, cy) = self.app.pointer_loc;
188 match &self.app.cursor_status {
189 CursorImageStatus::Hidden => {}
190 CursorImageStatus::Surface(surface) if surface.alive() => {
191 let (hx, hy) = crate::cursor_hotspot(surface);
192 let loc = (cx.round() as i32 - hx, cy.round() as i32 - hy);
193 for el in render_elements_from_surface_tree(
194 &mut self.renderer,
195 surface,
196 loc,
197 1.0,
198 1.0,
199 Kind::Cursor,
200 ) {
201 out.push(Frame::Window(el));
202 }
203 }
204 _ => {
205 let cursor_rect = Rectangle::new(
206 Point::<i32, Physical>::from((cx.round() as i32, cy.round() as i32)),
207 Size::<i32, Physical>::from((CURSOR_SIZE, CURSOR_SIZE)),
208 );
209 out.push(Frame::Solid(SolidColorRenderElement::new(
210 self.cursor_id.clone(),
211 cursor_rect,
212 CommitCounter::default(),
213 CURSOR_COLOR,
214 Kind::Cursor,
215 )));
216 }
217 }
218
219 // El shell va sobre todo; luego las flotantes; luego las
220 // teseladas. `sort_by_key` es estable: respeta el orden de
221 // apertura dentro de cada grupo.
222 let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
223 shown.sort_by_key(|w| (!w.is_shell, !w.floating));
224 for w in &shown {
225 let (x, y) = crate::render_loc(w, output_h);
226 let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
227 // El marco, encima de la propia superficie de la ventana
228 // — el shell no lleva.
229 if !w.is_shell {
230 let rects = border_rects(x, y, sw, sh);
231 for (buf, (bx, by, _, _)) in w.borders.iter().zip(rects) {
232 out.push(Frame::Solid(SolidColorRenderElement::from_buffer(
233 buf,
234 (bx, by),
235 1.0,
236 1.0,
237 Kind::Unspecified,
238 )));
239 }
240 }
241 for el in render_elements_from_surface_tree(
242 &mut self.renderer,
243 &w.surface,
244 (x, y),
245 1.0,
246 1.0,
247 Kind::Unspecified,
248 ) {
249 out.push(Frame::Window(el));
250 }
251 }
252 out
253 };
254 match self.compositor.render_frame::<_, _>(
255 &mut self.renderer,
256 &elements,
257 CLEAR_COLOR,
258 FrameFlags::DEFAULT,
259 ) {
260 Ok(result) => {
261 if !result.is_empty {
262 match self.compositor.queue_frame(()) {
263 Ok(()) => self.pending_flip = true,
264 Err(e) => eprintln!("mirada-compositor · queue_frame: {e}"),
265 }
266 }
267 }
268 Err(e) => eprintln!("mirada-compositor · render_frame: {e}"),
269 }
270 // Avisa a cada cliente de que puede dibujar el siguiente cuadro.
271 let time = self.start.elapsed().as_millis() as u32;
272 for w in &self.app.windows {
273 send_frames_surface_tree(&w.surface, time);
274 }
275 // También a la superficie del cursor, por si es un cursor animado.
276 if let CursorImageStatus::Surface(surface) = &self.app.cursor_status {
277 if surface.alive() {
278 send_frames_surface_tree(surface, time);
279 }
280 }
281 }
282
283 /// La sesión se cede a otra VT (`Ctrl+Alt+Fn`): suelta la GPU y deja
284 /// de leer el ratón y el teclado, para no chocar con quien ahora
285 /// manda en la pantalla.
286 fn pause_session(&mut self) {
287 self.active = false;
288 self.drm.pause();
289 self.libinput.suspend();
290 println!("mirada-compositor · sesión cedida a otra VT.");
291 }
292
293 /// La sesión vuelve a esta VT: recupera la GPU y la entrada, reinicia
294 /// el estado del compositor y repinta.
295 fn resume_session(&mut self) {
296 if self.libinput.resume().is_err() {
297 eprintln!("mirada-compositor · libinput.resume falló.");
298 }
299 if let Err(e) = self.drm.activate(false) {
300 eprintln!("mirada-compositor · drm.activate falló: {e}");
301 }
302 if let Err(e) = self.compositor.reset_state() {
303 eprintln!("mirada-compositor · compositor.reset_state falló: {e}");
304 }
305 self.active = true;
306 self.pending_flip = false;
307 self.render();
308 println!("mirada-compositor · sesión recuperada.");
309 }
310
311 /// Tarea periódica: Cerebro enlazado, recarga del keymap, API de
312 /// control, composición y vaciado hacia los clientes.
313 fn tick(&mut self) {
314 self.app.brain_poll();
315
316 let n = self.app.windows.len();
317 if n != self.last_windows {
318 eprintln!("mirada-compositor · ventanas en pantalla: {n}");
319 self.last_windows = n;
320 }
321
322 if self.keymap_watch.as_ref().is_some_and(|w| w.changed()) {
323 if let Some(path) = &self.keymap_path {
324 match Keymap::load(path) {
325 Ok(km) => {
326 let cmd = if let Brain::Embedded(d) = &mut self.app.brain {
327 Some(d.set_keymap(km))
328 } else {
329 None
330 };
331 if let Some(cmd) = cmd {
332 self.app.apply_commands(vec![cmd]);
333 }
334 println!("mirada-compositor · keymap recargado.");
335 }
336 Err(e) => eprintln!("mirada-compositor · keymap inválido: {e}"),
337 }
338 }
339 }
340
341 if let Some(ctl) = &self.ctl {
342 while let Some(mut conn) = ctl.poll() {
343 let reply = match conn.read_request() {
344 Ok(Some(req)) => self.app.serve_ctl(req),
345 Ok(None) => continue,
346 Err(e) => CtlReply::Error(format!("{e}")),
347 };
348 let _ = conn.reply(&reply);
349 }
350 }
351
352 self.render();
353 let _ = self.display.flush_clients();
354 }
355
356 /// Procesa un evento de `libinput`: teclado y puntero.
357 fn handle_input(&mut self, event: InputEvent<LibinputInputBackend>) {
358 let time = self.start.elapsed().as_millis() as u32;
359 match event {
360 // --- Teclado: intercepta los atajos del Cerebro --------------
361 InputEvent::Keyboard { event } => {
362 let Some(keyboard) = self.app.keyboard.clone() else {
363 return;
364 };
365 let code = event.key_code();
366 let key_state = event.state();
367 let pressed = key_state == KeyState::Pressed;
368 keyboard.input::<(), _>(
369 &mut self.app,
370 code,
371 key_state,
372 SERIAL_COUNTER.next_serial(),
373 time,
374 |st, mods, handle| {
375 if !pressed {
376 return FilterResult::Forward;
377 }
378 if let Some(combo) = combo_string(mods, handle.modified_sym()) {
379 if st.grabs.contains(&combo) {
380 st.pending_keybind = Some(combo);
381 return FilterResult::Intercept(());
382 }
383 }
384 FilterResult::Forward
385 },
386 );
387 if let Some(combo) = self.app.pending_keybind.take() {
388 let ev = self.app.body.keybind(combo);
389 self.app.brain_feed(ev);
390 }
391 }
392
393 // --- Puntero: movimiento relativo (ratón, touchpad) ----------
394 InputEvent::PointerMotion { event } => {
395 let (mut x, mut y) = self.app.pointer_loc;
396 x = (x + event.delta_x()).clamp(0.0, self.output_size.0);
397 y = (y + event.delta_y()).clamp(0.0, self.output_size.1);
398 self.app.pointer_loc = (x, y);
399 if !self.drag_update() {
400 self.pointer_motion(time);
401 }
402 }
403
404 // --- Puntero: movimiento absoluto (táctil, tableta) ----------
405 InputEvent::PointerMotionAbsolute { event } => {
406 let space = Size::<i32, Logical>::from((
407 self.output_size.0 as i32,
408 self.output_size.1 as i32,
409 ));
410 let pos = event.position_transformed(space);
411 self.app.pointer_loc = (
412 pos.x.clamp(0.0, self.output_size.0),
413 pos.y.clamp(0.0, self.output_size.1),
414 );
415 if !self.drag_update() {
416 self.pointer_motion(time);
417 }
418 }
419
420 // --- Puntero: botones ----------------------------------------
421 InputEvent::PointerButton { event } => {
422 let pressed = event.state() == ButtonState::Pressed;
423 let button = event.button_code();
424
425 // ¿Empieza un arrastre? `Super`+botón sobre una ventana:
426 // izquierdo mueve, derecho redimensiona.
427 if pressed && self.app.drag.is_none() {
428 let super_held = self
429 .app
430 .keyboard
431 .as_ref()
432 .is_some_and(|kb| kb.modifier_state().logo);
433 let mode = match button {
434 BTN_LEFT if super_held => Some(DragMode::Move),
435 BTN_RIGHT if super_held => Some(DragMode::Resize),
436 _ => None,
437 };
438 if let Some(mode) = mode {
439 let (x, y) = self.app.pointer_loc;
440 if let Some(i) = self.window_at(x, y) {
441 let w = &self.app.windows[i];
442 let grab = DragGrab {
443 id: w.id,
444 mode,
445 start_pointer: (x, y),
446 start_rect: (w.loc.0, w.loc.1, w.size.0, w.size.1),
447 };
448 self.app.drag = Some(grab);
449 return; // el arrastre captura el botón
450 }
451 }
452 }
453
454 // Durante un arrastre los botones no llegan al cliente;
455 // soltar cualquiera lo termina.
456 if self.app.drag.is_some() {
457 if !pressed {
458 self.app.drag = None;
459 }
460 return;
461 }
462
463 // Botón normal: a la ventana bajo el puntero.
464 let Some(pointer) = self.app.pointer.clone() else {
465 return;
466 };
467 pointer.button(
468 &mut self.app,
469 &ButtonEvent {
470 serial: SERIAL_COUNTER.next_serial(),
471 time,
472 button,
473 state: event.state(),
474 },
475 );
476 pointer.frame(&mut self.app);
477 }
478
479 // --- Puntero: rueda / desplazamiento -------------------------
480 InputEvent::PointerAxis { event } => {
481 let Some(pointer) = self.app.pointer.clone() else {
482 return;
483 };
484 let source = event.source();
485 let mut frame = AxisFrame::new(time).source(source);
486 for axis in [Axis::Horizontal, Axis::Vertical] {
487 match event.amount(axis) {
488 Some(v) if v != 0.0 => frame = frame.value(axis, v),
489 Some(_) if source == AxisSource::Finger => {
490 frame = frame.stop(axis);
491 }
492 _ => {}
493 }
494 if let Some(d) = event.amount_v120(axis) {
495 frame = frame.v120(axis, d as i32);
496 }
497 }
498 pointer.axis(&mut self.app, frame);
499 pointer.frame(&mut self.app);
500 }
501
502 _ => {} // otros dispositivos: aún no
503 }
504 }
505
506 /// Reenvía el puntero a la ventana que tiene debajo y, si esa ventana
507 /// cambió, aplica el foco-sigue-ratón avisando al Cerebro.
508 fn pointer_motion(&mut self, time: u32) {
509 let Some(pointer) = self.app.pointer.clone() else {
510 return;
511 };
512 let (x, y) = self.app.pointer_loc;
513 let hit = self.window_at(x, y);
514 let focus = hit.map(|i| {
515 let w = &self.app.windows[i];
516 let (lx, ly) = crate::render_loc(w, self.app.output_size.1);
517 (
518 w.surface.clone(),
519 Point::<f64, Logical>::from((lx as f64, ly as f64)),
520 )
521 });
522 pointer.motion(
523 &mut self.app,
524 focus,
525 &MotionEvent {
526 location: Point::from((x, y)),
527 serial: SERIAL_COUNTER.next_serial(),
528 time,
529 },
530 );
531 pointer.frame(&mut self.app);
532
533 // Sobre el escritorio pelado no manda ningún cliente: el cursor
534 // vuelve al de por defecto (si no, se queda con la «I» del texto
535 // de la última ventana).
536 if hit.is_none() {
537 self.app.cursor_status = CursorImageStatus::default_named();
538 }
539
540 // Foco-sigue-ratón: al pasar a otra ventana, que la enfoque quien
541 // corresponda — el Cerebro para las teseladas, carmen mismo para
542 // el shell (que no vive en el Cerebro).
543 let hovered = hit.map(|i| self.app.windows[i].id);
544 if hovered != self.last_pointer_window {
545 self.last_pointer_window = hovered;
546 match hit {
547 Some(i) if self.app.windows[i].is_shell => {
548 let surf = self.app.windows[i].surface.clone();
549 if let Some(kb) = self.app.keyboard.clone() {
550 kb.set_focus(&mut self.app, Some(surf), SERIAL_COUNTER.next_serial());
551 }
552 }
553 Some(i) => {
554 let id = self.app.windows[i].id;
555 let ev = self.app.body.pointer_enter(id);
556 self.app.brain_feed(ev);
557 }
558 None => {}
559 }
560 }
561 }
562
563 /// Si hay un arrastre en curso, recalcula el rectángulo de la ventana
564 /// y se lo manda al Cerebro (que la hace flotar ahí). Devuelve `true`
565 /// si consumió el movimiento — entonces el puntero no llega al cliente.
566 fn drag_update(&mut self) -> bool {
567 let Some(drag) = self.app.drag.as_ref() else {
568 return false;
569 };
570 let mode = drag.mode;
571 let (spx, spy) = drag.start_pointer;
572 let (sx, sy, sw, sh) = drag.start_rect;
573 let id = drag.id;
574
575 let (px, py) = self.app.pointer_loc;
576 let dx = (px - spx) as i32;
577 let dy = (py - spy) as i32;
578 let rect = match mode {
579 DragMode::Move => Rect::new(sx + dx, sy + dy, sw, sh),
580 DragMode::Resize => Rect::new(
581 sx,
582 sy,
583 (sw + dx).max(MIN_WINDOW),
584 (sh + dy).max(MIN_WINDOW),
585 ),
586 };
587 self.app.brain_feed(BodyEvent::WindowFloatTo { id, rect });
588 true
589 }
590
591 /// El índice de la ventana visible bajo el punto `(x, y)`, si la hay
592 /// — en orden front-to-back (el shell gana a las flotantes, y éstas a
593 /// las teseladas).
594 fn window_at(&self, x: f64, y: f64) -> Option<usize> {
595 let mut idx: Vec<usize> = (0..self.app.windows.len())
596 .filter(|&i| self.app.windows[i].visible)
597 .collect();
598 idx.sort_by_key(|&i| {
599 let w = &self.app.windows[i];
600 (!w.is_shell, !w.floating)
601 });
602 let output_h = self.app.output_size.1;
603 idx.into_iter().find(|&i| {
604 let w = &self.app.windows[i];
605 let (lx, ly) = crate::render_loc(w, output_h);
606 let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
607 x >= lx as f64 && y >= ly as f64 && x < (lx + sw) as f64 && y < (ly + sh) as f64
608 })
609 }
610}
611
612/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. Con `greeter`,
613/// el compositor nace en modo DM: ver [`BodyMode`].
614pub fn run(greeter: bool) -> Result<(), Box<dyn Error>> {
615 println!("mirada-compositor · backend DRM.");
616 println!("──────────────────────────────────────────────────");
617
618 // 1 · Sesión.
619 println!("[1/8] abriendo la sesión (libseat) …");
620 let (mut session, session_notifier) = LibSeatSession::new().map_err(|e| {
621 format!(
622 "no pude abrir la sesión libseat: {e}\n \
623 ¿estás en una TTY de verdad (Ctrl+Alt+F3), con `seatd` o `logind`?"
624 )
625 })?;
626 let seat_name = session.seat();
627 println!(" sesión abierta · seat «{seat_name}»");
628
629 // 2 · GPU primaria.
630 println!("[2/8] buscando la GPU primaria …");
631 let gpu = udev::primary_gpu(&seat_name)
632 .map_err(|e| format!("error consultando udev: {e}"))?
633 .ok_or("no encontré ninguna GPU — ¿existe algún /dev/dri/card*?")?;
634 println!(" GPU primaria: {}", gpu.display());
635
636 // 3 · Dispositivo DRM.
637 println!("[3/8] abriendo el dispositivo DRM …");
638 let fd = session
639 .open(&gpu, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NONBLOCK)
640 .map_err(|e| format!("no pude abrir {}: {e}", gpu.display()))?;
641 let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd));
642 let (mut drm, drm_notifier) =
643 DrmDevice::new(drm_fd.clone(), true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
644 println!(" dispositivo DRM listo.");
645
646 // 4 · Elegir la salida conectada: conector + CRTC + modo.
647 println!("[4/8] eligiendo salida …");
648 let resources = drm
649 .resource_handles()
650 .map_err(|e| format!("no pude leer los recursos DRM: {e}"))?;
651 let mut chosen = None;
652 for &conn_handle in resources.connectors() {
653 let conn = match drm.get_connector(conn_handle, false) {
654 Ok(c) => c,
655 Err(_) => continue,
656 };
657 if conn.state() != ConnectorState::Connected {
658 continue;
659 }
660 let name = format!("{:?}-{}", conn.interface(), conn.interface_id());
661 // Registra todos los modos del panel — diagnóstico.
662 for m in conn.modes() {
663 let (mw, mh) = m.size();
664 let pref = if m.mode_type().contains(ModeTypeFlags::PREFERRED) {
665 " [PREFERRED]"
666 } else {
667 ""
668 };
669 eprintln!(" modo de «{name}»: {mw}×{mh} @ {} Hz{pref}", m.vrefresh());
670 }
671 // Elige el modo de mayor área (a igualdad, mayor refresco) — el
672 // nativo del panel. La marca PREFERRED no es fiable: a veces
673 // señala un modo menor.
674 let mode = conn
675 .modes()
676 .iter()
677 .max_by_key(|m| {
678 let (mw, mh) = m.size();
679 (mw as u32 * mh as u32, m.vrefresh())
680 })
681 .copied();
682 let Some(mode) = mode else {
683 continue;
684 };
685 let crtc = conn
686 .encoders()
687 .iter()
688 .filter_map(|enc| drm.get_encoder(*enc).ok())
689 .find_map(|enc| resources.filter_crtcs(enc.possible_crtcs()).into_iter().next());
690 if let Some(crtc) = crtc {
691 let (w, h) = mode.size();
692 println!(" salida «{name}» · {w}×{h} · CRTC {crtc:?}");
693 chosen = Some((conn_handle, crtc, mode, name));
694 break;
695 }
696 }
697 let (conn_handle, crtc, mode, out_name) =
698 chosen.ok_or("ninguna salida conectada con CRTC disponible")?;
699 let (mode_w, mode_h) = mode.size();
700
701 // 5 · GBM + EGL + GlesRenderer.
702 println!("[5/8] inicializando GBM + EGL + GlesRenderer …");
703 let gbm = GbmDevice::new(drm_fd.clone()).map_err(|e| format!("GbmDevice::new falló: {e}"))?;
704 let egl_display =
705 unsafe { EGLDisplay::new(gbm.clone()) }.map_err(|e| format!("EGLDisplay::new falló: {e}"))?;
706 let egl_context =
707 EGLContext::new(&egl_display).map_err(|e| format!("EGLContext::new falló: {e}"))?;
708 let renderer =
709 unsafe { GlesRenderer::new(egl_context) }.map_err(|e| format!("GlesRenderer falló: {e}"))?;
710 println!(" renderer GLES listo.");
711
712 // 6 · Superficie DRM + DrmCompositor de la salida.
713 println!("[6/8] creando la superficie DRM y el compositor …");
714 let surface = drm
715 .create_surface(crtc, mode, &[conn_handle])
716 .map_err(|e| format!("create_surface falló: {e}"))?;
717 let allocator =
718 GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
719 let exporter = GbmFramebufferExporter::new(gbm.clone(), None);
720 let renderer_formats = renderer.dmabuf_formats();
721 let mode_source = OutputModeSource::Static {
722 size: Size::from((mode_w as i32, mode_h as i32)),
723 scale: Scale::from(1.0),
724 transform: Transform::Normal,
725 };
726 let compositor: Compositor = DrmCompositor::new(
727 mode_source,
728 surface,
729 None,
730 allocator,
731 exporter,
732 [Fourcc::Argb8888, Fourcc::Xrgb8888],
733 renderer_formats,
734 drm.cursor_size(),
735 Some(gbm.clone()),
736 )
737 .map_err(|e| format!("DrmCompositor::new falló: {e}"))?;
738 println!(" compositor de «{out_name}» listo.");
739
740 // 7 · El estado Wayland (Cerebro, teclado, keymap, control).
741 println!("[7/8] armando el estado Wayland …");
742 let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } =
743 crate::build_app(greeter)?;
744 // Con el renderer ya creado, anuncia dmabuf — sin esto las apps que
745 // pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
746 crate::announce_dmabuf(&mut app, &display.handle(), &renderer);
747 // La salida del Cerebro = el modo del monitor.
748 let ev = app.body.add_output(0, mode_w as i32, mode_h as i32);
749 app.brain_feed(ev);
750 app.output_size = (mode_w as i32, mode_h as i32);
751 // El puntero arranca en el centro de la pantalla.
752 app.pointer_loc = (mode_w as f64 / 2.0, mode_h as f64 / 2.0);
753 // Anuncia el monitor en el protocolo Wayland — los clientes lo exigen.
754 let _wl_output = crate::announce_output(
755 &display.handle(),
756 &out_name,
757 mode_w as i32,
758 mode_h as i32,
759 mode.vrefresh() as i32 * 1000,
760 );
761
762 // El socket Wayland por el que se conectan los clientes.
763 let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
764 let socket_name = listener
765 .socket_name()
766 .and_then(|s| s.to_str())
767 .unwrap_or("wayland-?")
768 .to_string();
769 std::env::set_var("WAYLAND_DISPLAY", &socket_name);
770 println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
771
772 // Modo DM: lanza el greeter y recibe su tiquet por un canal de
773 // `calloop`. Modo normal: autoarranque + `MIRADA_STARTUP`.
774 let greeter_rx = if app.mode == BodyMode::Greeter {
775 let (tx, rx) = ticket_channel::<SessionTicket>();
776 crate::spawn_greeter(move |ticket| {
777 let _ = tx.send(ticket);
778 })?;
779 Some(rx)
780 } else {
781 // Autoarranque: los programas de `~/.config/mirada/autostart`.
782 crate::spawn_autostart(None);
783 // App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza
784 // como hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin
785 // saltar de VT.
786 if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
787 crate::spawn_command(&cmd, None);
788 }
789 None
790 };
791
792 // 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
793 println!("[8/8] montando el bucle de eventos …");
794 let mut event_loop: EventLoop<DrmState> =
795 EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
796 let handle = event_loop.handle();
797
798 // Sesión: pausa/activación al conmutar de VT.
799 handle
800 .insert_source(session_notifier, |event, _, state: &mut DrmState| match event {
801 SessionEvent::PauseSession => state.pause_session(),
802 SessionEvent::ActivateSession => state.resume_session(),
803 })
804 .map_err(|e| format!("insert session: {e}"))?;
805
806 // VBlank: el page-flip terminó.
807 handle
808 .insert_source(drm_notifier, |event, _meta, state| match event {
809 DrmEvent::VBlank(_crtc) => {
810 if let Err(e) = state.compositor.frame_submitted() {
811 eprintln!("mirada-compositor · frame_submitted: {e}");
812 }
813 state.pending_flip = false;
814 }
815 DrmEvent::Error(e) => eprintln!("mirada-compositor · DRM: {e}"),
816 })
817 .map_err(|e| format!("insert drm: {e}"))?;
818
819 // Teclado y ratón vía libinput. Guardamos un clon del contexto (es
820 // un manejador con contador de referencias) para suspenderlo y
821 // reanudarlo al conmutar de VT.
822 let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
823 libinput
824 .udev_assign_seat(&seat_name)
825 .map_err(|()| "libinput: no pude asignar el seat")?;
826 let libinput_handle = libinput.clone();
827 handle
828 .insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
829 state.handle_input(event);
830 })
831 .map_err(|e| format!("insert libinput: {e}"))?;
832
833 // Clientes Wayland nuevos.
834 handle
835 .insert_source(
836 Generic::new(listener, Interest::READ, CalloopMode::Level),
837 |_readiness, listener, state| {
838 while let Some(stream) = listener.accept()? {
839 eprintln!("mirada-compositor · cliente Wayland conectado.");
840 let _ = state
841 .display
842 .handle()
843 .insert_client(stream, Arc::new(ClientState::default()));
844 }
845 Ok(PostAction::Continue)
846 },
847 )
848 .map_err(|e| format!("insert socket: {e}"))?;
849
850 // Peticiones de los clientes ya conectados.
851 let poll_fd = display.backend().poll_fd().try_clone_to_owned()?;
852 handle
853 .insert_source(
854 Generic::new(poll_fd, Interest::READ, CalloopMode::Level),
855 |_readiness, _fd, state| {
856 let DrmState { display, app, .. } = state;
857 if let Err(e) = display.dispatch_clients(app) {
858 eprintln!("mirada-compositor · dispatch: {e}");
859 }
860 let _ = display.flush_clients();
861 Ok(PostAction::Continue)
862 },
863 )
864 .map_err(|e| format!("insert display: {e}"))?;
865
866 // Timer de composición + tareas — ~60 Hz.
867 handle
868 .insert_source(Timer::immediate(), |_instant, _meta, state| {
869 state.tick();
870 TimeoutAction::ToDuration(Duration::from_millis(16))
871 })
872 .map_err(|e| format!("insert timer: {e}"))?;
873
874 // Tiquet del greeter (modo DM): al llegar, el traspaso a la sesión.
875 // El hilo lector del greeter despierta el bucle por este canal.
876 if let Some(rx) = greeter_rx {
877 handle
878 .insert_source(rx, |event, _, state: &mut DrmState| {
879 if let TicketEvent::Msg(ticket) = event {
880 state.app.complete_greeter_handoff(ticket);
881 }
882 })
883 .map_err(|e| format!("insert greeter: {e}"))?;
884 }
885
886 // Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
887 // compositor solo (0 o sin definir = sin tope). El teclado ya
888 // funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
889 let timeout_secs: u64 = std::env::var("MIRADA_DRM_TIMEOUT")
890 .ok()
891 .and_then(|v| v.parse().ok())
892 .unwrap_or(0);
893
894 println!("──────────────────────────────────────────────────");
895 println!("mirada-compositor · escritorio en marcha sobre «{out_name}».");
896 println!(" Lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
897 println!(" Salir: Super+Shift+e · o Ctrl+C en esta TTY.");
898 if timeout_secs > 0 {
899 println!(" Se cerrará solo a los {timeout_secs}s (MIRADA_DRM_TIMEOUT=0 lo quita).");
900 }
901
902 let mut state = DrmState {
903 app,
904 display,
905 drm,
906 compositor,
907 renderer,
908 libinput: libinput_handle,
909 active: true,
910 pending_flip: false,
911 keymap_path,
912 keymap_watch,
913 ctl,
914 start: Instant::now(),
915 last_windows: 0,
916 cursor_id: Id::new(),
917 last_pointer_window: None,
918 output_size: (mode_w as f64, mode_h as f64),
919 };
920
921 let signal = event_loop.get_signal();
922 event_loop
923 .run(None, &mut state, |state| {
924 let timed_out =
925 timeout_secs > 0 && state.start.elapsed() > Duration::from_secs(timeout_secs);
926 if !state.app.running || timed_out {
927 if timed_out {
928 println!("mirada-compositor · tope de tiempo — cerrando.");
929 }
930 signal.stop();
931 }
932 })
933 .map_err(|e| format!("el bucle de eventos falló: {e}"))?;
934
935 println!("mirada-compositor · adiós.");
936 Ok(())
937}