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