gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198629 Sutichaykuna

⎇ 2bd6aaad0249e0a87d5a5c55ea1a250b5f258eac crates/apps/mirada-compositor/src/drm_backend.rs Rust · 907 siq'ikuna · 36.7 KB Ñawpa willay Pi imata llamkarqan ch'uya

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