gioser

tawasuyu/brahman

Rust 320TOML 122JSON 14Markdown 114164 Sutichaykuna

⎇ 5ede927d3432c0c895e863b39ca7eb5f73b78213 crates/apps/mirada-compositor/src/drm_backend.rs Rust · 879 siq'ikuna · 35.2 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(mirada): sesión de escritorio — autostart y conmutación de VT 5ede927d3 · 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
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}