tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definitions

⎇ 758f61f52a35514669064a1aefd44f22f07a9e9c crates/apps/mirada-compositor/src/drm_backend.rs Rust · 937 lines · 37.8 KB History Who touched what raw

feat(carmen): modo greeter — mirada-compositor como DM 758f61f52 · sergio ·

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}