gioser

tawasuyu/brahman

Rust 483TOML 191Markdown 50JSON 176777 Sutichaykuna

⎇ 5230d42b112a5fc846d3108275b38188bd28e1db crates/apps/mirada-compositor/src/drm_backend.rs Rust · 496 siq'ikuna · 19.7 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(mirada-compositor): centrar la ventana en su celda si el cliente no la llena 5230d42b1 · 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::{InputEvent, KeyState, KeyboardKeyEvent};
29use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
30use smithay::backend::renderer::element::surface::{
31    render_elements_from_surface_tree, WaylandSurfaceRenderElement,
32};
33use smithay::backend::renderer::element::Kind;
34use smithay::backend::renderer::gles::GlesRenderer;
35use smithay::backend::renderer::ImportDma;
36use smithay::backend::session::libseat::LibSeatSession;
37use smithay::backend::session::{Event as SessionEvent, Session};
38use smithay::backend::udev;
39use smithay::input::keyboard::FilterResult;
40use smithay::output::OutputModeSource;
41use smithay::reexports::calloop::generic::Generic;
42use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
43use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction};
44use smithay::reexports::drm::control::connector::State as ConnectorState;
45use smithay::reexports::drm::control::{Device as ControlDevice, ModeTypeFlags};
46use smithay::reexports::input::Libinput;
47use smithay::reexports::rustix::fs::OFlags;
48use smithay::reexports::wayland_server::{Display, ListeningSocket};
49use smithay::utils::{DeviceFd, Scale, Size, Transform, SERIAL_COUNTER};
50
51use mirada_brain::{CtlReply, Keymap};
52
53use crate::{combo_string, send_frames_surface_tree, App, Brain, ClientState, Setup};
54
55/// El `DrmCompositor` concreto para la salida (un solo GPU).
56type Compositor =
57    DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>;
58
59/// Color de fondo del escritorio cuando no hay nada que lo tape.
60const CLEAR_COLOR: [f32; 4] = [0.05, 0.05, 0.08, 1.0];
61
62/// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`.
63struct DrmState {
64    app: App,
65    display: Display<App>,
66    compositor: Compositor,
67    renderer: GlesRenderer,
68    /// `true` entre que se encola un page-flip y llega su VBlank.
69    pending_flip: bool,
70    keymap_path: Option<std::path::PathBuf>,
71    keymap_watch: Option<mirada_brain::KeymapWatch>,
72    ctl: Option<crate::CtlServer>,
73    /// Inicio del compositor — base de tiempos para los frame-callbacks.
74    start: Instant,
75    /// Nº de ventanas en el último `tick` — para registrar los cambios.
76    last_windows: usize,
77}
78
79impl DrmState {
80    /// Compone las ventanas y, si hubo cambios, encola el cuadro.
81    fn render(&mut self) {
82        if self.pending_flip {
83            return; // aún esperamos el VBlank del cuadro anterior
84        }
85        // Elementos a pintar: las flotantes primero (lista front-to-back).
86        let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = {
87            let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
88            shown.sort_by_key(|w| !w.floating);
89            shown
90                .iter()
91                .flat_map(|w| {
92                    render_elements_from_surface_tree(
93                        &mut self.renderer,
94                        &w.surface,
95                        crate::render_loc(w),
96                        1.0,
97                        1.0,
98                        Kind::Unspecified,
99                    )
100                })
101                .collect()
102        };
103        match self.compositor.render_frame::<_, _>(
104            &mut self.renderer,
105            &elements,
106            CLEAR_COLOR,
107            FrameFlags::DEFAULT,
108        ) {
109            Ok(result) => {
110                if !result.is_empty {
111                    match self.compositor.queue_frame(()) {
112                        Ok(()) => self.pending_flip = true,
113                        Err(e) => eprintln!("mirada-compositor · queue_frame: {e}"),
114                    }
115                }
116            }
117            Err(e) => eprintln!("mirada-compositor · render_frame: {e}"),
118        }
119        // Avisa a cada cliente de que puede dibujar el siguiente cuadro.
120        let time = self.start.elapsed().as_millis() as u32;
121        for w in &self.app.windows {
122            send_frames_surface_tree(&w.surface, time);
123        }
124    }
125
126    /// Tarea periódica: Cerebro enlazado, recarga del keymap, API de
127    /// control, composición y vaciado hacia los clientes.
128    fn tick(&mut self) {
129        self.app.brain_poll();
130
131        let n = self.app.windows.len();
132        if n != self.last_windows {
133            eprintln!("mirada-compositor · ventanas en pantalla: {n}");
134            self.last_windows = n;
135        }
136
137        if self.keymap_watch.as_ref().is_some_and(|w| w.changed()) {
138            if let Some(path) = &self.keymap_path {
139                match Keymap::load(path) {
140                    Ok(km) => {
141                        let cmd = if let Brain::Embedded(d) = &mut self.app.brain {
142                            Some(d.set_keymap(km))
143                        } else {
144                            None
145                        };
146                        if let Some(cmd) = cmd {
147                            self.app.apply_commands(vec![cmd]);
148                        }
149                        println!("mirada-compositor · keymap recargado.");
150                    }
151                    Err(e) => eprintln!("mirada-compositor · keymap inválido: {e}"),
152                }
153            }
154        }
155
156        if let Some(ctl) = &self.ctl {
157            while let Some(mut conn) = ctl.poll() {
158                let reply = match conn.read_request() {
159                    Ok(Some(req)) => self.app.serve_ctl(req),
160                    Ok(None) => continue,
161                    Err(e) => CtlReply::Error(format!("{e}")),
162                };
163                let _ = conn.reply(&reply);
164            }
165        }
166
167        self.render();
168        let _ = self.display.flush_clients();
169    }
170
171    /// Procesa un evento de `libinput` — por ahora, sólo el teclado.
172    fn handle_input(&mut self, event: InputEvent<LibinputInputBackend>) {
173        let InputEvent::Keyboard { event } = event else {
174            return; // dispositivos, puntero, táctil: aún no
175        };
176        let Some(keyboard) = self.app.keyboard.clone() else {
177            return;
178        };
179        let code = event.key_code();
180        let key_state = event.state();
181        let pressed = key_state == KeyState::Pressed;
182        let time = self.start.elapsed().as_millis() as u32;
183        keyboard.input::<(), _>(
184            &mut self.app,
185            code,
186            key_state,
187            SERIAL_COUNTER.next_serial(),
188            time,
189            |st, mods, handle| {
190                if !pressed {
191                    return FilterResult::Forward;
192                }
193                if let Some(combo) = combo_string(mods, handle.modified_sym()) {
194                    if st.grabs.contains(&combo) {
195                        st.pending_keybind = Some(combo);
196                        return FilterResult::Intercept(());
197                    }
198                }
199                FilterResult::Forward
200            },
201        );
202        if let Some(combo) = self.app.pending_keybind.take() {
203            let ev = self.app.body.keybind(combo);
204            self.app.brain_feed(ev);
205        }
206    }
207}
208
209/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b.
210pub fn run() -> Result<(), Box<dyn Error>> {
211    println!("mirada-compositor · backend DRM.");
212    println!("──────────────────────────────────────────────────");
213
214    // 1 · Sesión.
215    println!("[1/8] abriendo la sesión (libseat) …");
216    let (mut session, session_notifier) = LibSeatSession::new().map_err(|e| {
217        format!(
218            "no pude abrir la sesión libseat: {e}\n       \
219             ¿estás en una TTY de verdad (Ctrl+Alt+F3), con `seatd` o `logind`?"
220        )
221    })?;
222    let seat_name = session.seat();
223    println!("      sesión abierta · seat «{seat_name}»");
224
225    // 2 · GPU primaria.
226    println!("[2/8] buscando la GPU primaria …");
227    let gpu = udev::primary_gpu(&seat_name)
228        .map_err(|e| format!("error consultando udev: {e}"))?
229        .ok_or("no encontré ninguna GPU — ¿existe algún /dev/dri/card*?")?;
230    println!("      GPU primaria: {}", gpu.display());
231
232    // 3 · Dispositivo DRM.
233    println!("[3/8] abriendo el dispositivo DRM …");
234    let fd = session
235        .open(&gpu, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NONBLOCK)
236        .map_err(|e| format!("no pude abrir {}: {e}", gpu.display()))?;
237    let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd));
238    let (mut drm, drm_notifier) =
239        DrmDevice::new(drm_fd.clone(), true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
240    println!("      dispositivo DRM listo.");
241
242    // 4 · Elegir la salida conectada: conector + CRTC + modo.
243    println!("[4/8] eligiendo salida …");
244    let resources = drm
245        .resource_handles()
246        .map_err(|e| format!("no pude leer los recursos DRM: {e}"))?;
247    let mut chosen = None;
248    for &conn_handle in resources.connectors() {
249        let conn = match drm.get_connector(conn_handle, false) {
250            Ok(c) => c,
251            Err(_) => continue,
252        };
253        if conn.state() != ConnectorState::Connected {
254            continue;
255        }
256        let name = format!("{:?}-{}", conn.interface(), conn.interface_id());
257        // Registra todos los modos del panel — diagnóstico.
258        for m in conn.modes() {
259            let (mw, mh) = m.size();
260            let pref = if m.mode_type().contains(ModeTypeFlags::PREFERRED) {
261                " [PREFERRED]"
262            } else {
263                ""
264            };
265            eprintln!("      modo de «{name}»: {mw}×{mh} @ {} Hz{pref}", m.vrefresh());
266        }
267        // Elige el modo de mayor área (a igualdad, mayor refresco) — el
268        // nativo del panel. La marca PREFERRED no es fiable: a veces
269        // señala un modo menor.
270        let mode = conn
271            .modes()
272            .iter()
273            .max_by_key(|m| {
274                let (mw, mh) = m.size();
275                (mw as u32 * mh as u32, m.vrefresh())
276            })
277            .copied();
278        let Some(mode) = mode else {
279            continue;
280        };
281        let crtc = conn
282            .encoders()
283            .iter()
284            .filter_map(|enc| drm.get_encoder(*enc).ok())
285            .find_map(|enc| resources.filter_crtcs(enc.possible_crtcs()).into_iter().next());
286        if let Some(crtc) = crtc {
287            let (w, h) = mode.size();
288            println!("      salida «{name}» · {w}×{h} · CRTC {crtc:?}");
289            chosen = Some((conn_handle, crtc, mode, name));
290            break;
291        }
292    }
293    let (conn_handle, crtc, mode, out_name) =
294        chosen.ok_or("ninguna salida conectada con CRTC disponible")?;
295    let (mode_w, mode_h) = mode.size();
296
297    // 5 · GBM + EGL + GlesRenderer.
298    println!("[5/8] inicializando GBM + EGL + GlesRenderer …");
299    let gbm = GbmDevice::new(drm_fd.clone()).map_err(|e| format!("GbmDevice::new falló: {e}"))?;
300    let egl_display =
301        unsafe { EGLDisplay::new(gbm.clone()) }.map_err(|e| format!("EGLDisplay::new falló: {e}"))?;
302    let egl_context =
303        EGLContext::new(&egl_display).map_err(|e| format!("EGLContext::new falló: {e}"))?;
304    let renderer =
305        unsafe { GlesRenderer::new(egl_context) }.map_err(|e| format!("GlesRenderer falló: {e}"))?;
306    println!("      renderer GLES listo.");
307
308    // 6 · Superficie DRM + DrmCompositor de la salida.
309    println!("[6/8] creando la superficie DRM y el compositor …");
310    let surface = drm
311        .create_surface(crtc, mode, &[conn_handle])
312        .map_err(|e| format!("create_surface falló: {e}"))?;
313    let allocator =
314        GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
315    let exporter = GbmFramebufferExporter::new(gbm.clone(), None);
316    let renderer_formats = renderer.dmabuf_formats();
317    let mode_source = OutputModeSource::Static {
318        size: Size::from((mode_w as i32, mode_h as i32)),
319        scale: Scale::from(1.0),
320        transform: Transform::Normal,
321    };
322    let compositor: Compositor = DrmCompositor::new(
323        mode_source,
324        surface,
325        None,
326        allocator,
327        exporter,
328        [Fourcc::Argb8888, Fourcc::Xrgb8888],
329        renderer_formats,
330        drm.cursor_size(),
331        Some(gbm.clone()),
332    )
333    .map_err(|e| format!("DrmCompositor::new falló: {e}"))?;
334    println!("      compositor de «{out_name}» listo.");
335
336    // 7 · El estado Wayland (Cerebro, teclado, keymap, control).
337    println!("[7/8] armando el estado Wayland …");
338    let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } = crate::build_app()?;
339    // La salida del Cerebro = el modo del monitor.
340    let ev = app.body.add_output(0, mode_w as i32, mode_h as i32);
341    app.brain_feed(ev);
342    // Anuncia el monitor en el protocolo Wayland — los clientes lo exigen.
343    let _wl_output = crate::announce_output(
344        &display.handle(),
345        &out_name,
346        mode_w as i32,
347        mode_h as i32,
348        mode.vrefresh() as i32 * 1000,
349    );
350
351    // El socket Wayland por el que se conectan los clientes.
352    let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
353    let socket_name = listener
354        .socket_name()
355        .and_then(|s| s.to_str())
356        .unwrap_or("wayland-?")
357        .to_string();
358    std::env::set_var("WAYLAND_DISPLAY", &socket_name);
359    println!("      escuchando en WAYLAND_DISPLAY={socket_name}");
360
361    // App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza como
362    // hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
363    if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
364        if !cmd.trim().is_empty() {
365            match std::process::Command::new("sh").arg("-c").arg(&cmd).spawn() {
366                Ok(child) => println!("      app de arranque lanzada (pid {}): {cmd}", child.id()),
367                Err(e) => eprintln!("      no pude lanzar «{cmd}»: {e}"),
368            }
369        }
370    }
371
372    // 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
373    println!("[8/8] montando el bucle de eventos …");
374    let mut event_loop: EventLoop<DrmState> =
375        EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
376    let handle = event_loop.handle();
377
378    // Sesión: pausa/activación al cambiar de VT.
379    handle
380        .insert_source(session_notifier, |event, _, _state| match event {
381            SessionEvent::PauseSession => println!("mirada-compositor · sesión en pausa."),
382            SessionEvent::ActivateSession => println!("mirada-compositor · sesión activa."),
383        })
384        .map_err(|e| format!("insert session: {e}"))?;
385
386    // VBlank: el page-flip terminó.
387    handle
388        .insert_source(drm_notifier, |event, _meta, state| match event {
389            DrmEvent::VBlank(_crtc) => {
390                if let Err(e) = state.compositor.frame_submitted() {
391                    eprintln!("mirada-compositor · frame_submitted: {e}");
392                }
393                state.pending_flip = false;
394            }
395            DrmEvent::Error(e) => eprintln!("mirada-compositor · DRM: {e}"),
396        })
397        .map_err(|e| format!("insert drm: {e}"))?;
398
399    // Teclado y ratón vía libinput.
400    let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
401    libinput
402        .udev_assign_seat(&seat_name)
403        .map_err(|()| "libinput: no pude asignar el seat")?;
404    handle
405        .insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
406            state.handle_input(event);
407        })
408        .map_err(|e| format!("insert libinput: {e}"))?;
409
410    // Clientes Wayland nuevos.
411    handle
412        .insert_source(
413            Generic::new(listener, Interest::READ, CalloopMode::Level),
414            |_readiness, listener, state| {
415                while let Some(stream) = listener.accept()? {
416                    eprintln!("mirada-compositor · cliente Wayland conectado.");
417                    let _ = state
418                        .display
419                        .handle()
420                        .insert_client(stream, Arc::new(ClientState::default()));
421                }
422                Ok(PostAction::Continue)
423            },
424        )
425        .map_err(|e| format!("insert socket: {e}"))?;
426
427    // Peticiones de los clientes ya conectados.
428    let poll_fd = display.backend().poll_fd().try_clone_to_owned()?;
429    handle
430        .insert_source(
431            Generic::new(poll_fd, Interest::READ, CalloopMode::Level),
432            |_readiness, _fd, state| {
433                let DrmState { display, app, .. } = state;
434                if let Err(e) = display.dispatch_clients(app) {
435                    eprintln!("mirada-compositor · dispatch: {e}");
436                }
437                let _ = display.flush_clients();
438                Ok(PostAction::Continue)
439            },
440        )
441        .map_err(|e| format!("insert display: {e}"))?;
442
443    // Timer de composición + tareas — ~60 Hz.
444    handle
445        .insert_source(Timer::immediate(), |_instant, _meta, state| {
446            state.tick();
447            TimeoutAction::ToDuration(Duration::from_millis(16))
448        })
449        .map_err(|e| format!("insert timer: {e}"))?;
450
451    // Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
452    // compositor solo (0 o sin definir = sin tope). El teclado ya
453    // funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
454    let timeout_secs: u64 = std::env::var("MIRADA_DRM_TIMEOUT")
455        .ok()
456        .and_then(|v| v.parse().ok())
457        .unwrap_or(0);
458
459    println!("──────────────────────────────────────────────────");
460    println!("mirada-compositor · escritorio en marcha sobre «{out_name}».");
461    println!("   Lanza un cliente:  WAYLAND_DISPLAY={socket_name} foot");
462    println!("   Salir: Super+Shift+e  ·  o Ctrl+C en esta TTY.");
463    if timeout_secs > 0 {
464        println!("   Se cerrará solo a los {timeout_secs}s (MIRADA_DRM_TIMEOUT=0 lo quita).");
465    }
466
467    let mut state = DrmState {
468        app,
469        display,
470        compositor,
471        renderer,
472        pending_flip: false,
473        keymap_path,
474        keymap_watch,
475        ctl,
476        start: Instant::now(),
477        last_windows: 0,
478    };
479
480    let signal = event_loop.get_signal();
481    event_loop
482        .run(None, &mut state, |state| {
483            let timed_out =
484                timeout_secs > 0 && state.start.elapsed() > Duration::from_secs(timeout_secs);
485            if !state.app.running || timed_out {
486                if timed_out {
487                    println!("mirada-compositor · tope de tiempo — cerrando.");
488                }
489                signal.stop();
490            }
491        })
492        .map_err(|e| format!("el bucle de eventos falló: {e}"))?;
493
494    println!("mirada-compositor · adiós.");
495    Ok(())
496}