diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs index 254bfc6..78490ae 100644 --- a/crates/apps/mirada-compositor/src/drm_backend.rs +++ b/crates/apps/mirada-compositor/src/drm_backend.rs @@ -1,20 +1,22 @@ //! `drm_backend` — el Cuerpo del compositor sobre **DRM/KMS**, sin -//! sesión gráfica anfitriona: corre directo sobre una TTY. +//! sesión gráfica anfitriona: corre directo sobre una TTY, como tu +//! escritorio de verdad. //! -//! Por fases, para verificarlo en hardware real paso a paso: +//! Construido por fases para verificarlo en hardware paso a paso: //! //! - **Fase 1 — bring-up**: sesión (`libseat`), GPU, dispositivo DRM, //! enumerar salidas. -//! - **Fase 2a — pipeline de render** (esto): GBM, EGL y `GlesRenderer`, -//! con un `DrmCompositor` para la salida conectada y un test que pinta -//! la pantalla de colores unos segundos. Confirma que EGL, GBM, el -//! *modeset* y el *page-flip* funcionan. -//! - **Fase 2b** (siguiente): el bucle Wayland completo — clientes, -//! `libinput`, composición real de ventanas. +//! - **Fase 2a — pipeline de render**: GBM, EGL y `GlesRenderer`, con un +//! `DrmCompositor` para la salida conectada. +//! - **Fase 2b — bucle Wayland** (esto): un bucle `calloop` que atiende +//! a los clientes Wayland, el teclado (`libinput`) y el VBlank, y +//! compone las ventanas de verdad. Aquí `mirada-compositor --drm` ya +//! es un escritorio funcionando. //! //! Todo con logs para diagnosticar sin el hardware delante. use std::error::Error; +use std::sync::Arc; use std::time::{Duration, Instant}; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; @@ -23,73 +25,187 @@ use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags}; use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter; use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent}; use smithay::backend::egl::{EGLContext, EGLDisplay}; -use smithay::backend::renderer::element::surface::WaylandSurfaceRenderElement; +use smithay::backend::input::{InputEvent, KeyState, KeyboardKeyEvent}; +use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; +use smithay::backend::renderer::element::surface::{ + render_elements_from_surface_tree, WaylandSurfaceRenderElement, +}; +use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::ImportDma; use smithay::backend::session::libseat::LibSeatSession; -use smithay::backend::session::Session; +use smithay::backend::session::{Event as SessionEvent, Session}; use smithay::backend::udev; +use smithay::input::keyboard::FilterResult; use smithay::output::OutputModeSource; -use smithay::reexports::calloop::EventLoop; +use smithay::reexports::calloop::generic::Generic; +use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; +use smithay::reexports::calloop::{EventLoop, Interest, Mode as CalloopMode, PostAction}; use smithay::reexports::drm::control::connector::State as ConnectorState; use smithay::reexports::drm::control::Device as ControlDevice; +use smithay::reexports::input::Libinput; use smithay::reexports::rustix::fs::OFlags; -use smithay::utils::{DeviceFd, Scale, Size, Transform}; +use smithay::reexports::wayland_server::{Display, ListeningSocket}; +use smithay::utils::{DeviceFd, Scale, Size, Transform, SERIAL_COUNTER}; -/// El `DrmCompositor` concreto para una salida (un solo GPU, `()` de -/// datos de usuario por cuadro). +use mirada_brain::{CtlReply, Keymap}; + +use crate::{combo_string, send_frames_surface_tree, App, Brain, ClientState, Setup}; + +/// El `DrmCompositor` concreto para la salida (un solo GPU). type Compositor = DrmCompositor, GbmFramebufferExporter, (), DrmDeviceFd>; -/// El estado del test de la fase 2a: lo comparten los callbacks de `calloop`. -struct TestState { +/// Color de fondo del escritorio cuando no hay nada que lo tape. +const CLEAR_COLOR: [f32; 4] = [0.05, 0.05, 0.08, 1.0]; + +/// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`. +struct DrmState { + app: App, + display: Display, compositor: Compositor, renderer: GlesRenderer, - /// Cuántos cuadros se han pintado. - frames: u32, - /// Inicio del test, para un tope por tiempo (anti-cuelgue). + /// `true` entre que se encola un page-flip y llega su VBlank. + pending_flip: bool, + keymap_path: Option, + keymap_watch: Option, + ctl: Option, + /// Inicio del compositor — base de tiempos para los frame-callbacks. start: Instant, } -impl TestState { - /// Pinta un cuadro: limpia la pantalla a un color que va cambiando - /// (para que siempre haya daño y el *page-flip* no se salte) y lo - /// encola para el siguiente VBlank. +impl DrmState { + /// Compone las ventanas y, si hubo cambios, encola el cuadro. fn render(&mut self) { - // Un ciclo lento por rojo → verde → azul. - let phase = (self.frames / 60) % 3; - let t = (self.frames % 60) as f32 / 60.0; - let color = match phase { - 0 => [t, 0.0, 1.0 - t, 1.0], - 1 => [1.0 - t, t, 0.0, 1.0], - _ => [0.0, 1.0 - t, t, 1.0], + if self.pending_flip { + return; // aún esperamos el VBlank del cuadro anterior + } + // Elementos a pintar: las flotantes primero (lista front-to-back). + let elements: Vec> = { + let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect(); + shown.sort_by_key(|w| !w.floating); + shown + .iter() + .flat_map(|w| { + render_elements_from_surface_tree( + &mut self.renderer, + &w.surface, + w.loc, + 1.0, + 1.0, + Kind::Unspecified, + ) + }) + .collect() }; - let elements: Vec> = Vec::new(); - match self - .compositor - .render_frame::<_, _>(&mut self.renderer, &elements, color, FrameFlags::DEFAULT) - { + match self.compositor.render_frame::<_, _>( + &mut self.renderer, + &elements, + CLEAR_COLOR, + FrameFlags::DEFAULT, + ) { Ok(result) => { if !result.is_empty { - if let Err(e) = self.compositor.queue_frame(()) { - eprintln!(" error al encolar el cuadro: {e}"); + match self.compositor.queue_frame(()) { + Ok(()) => self.pending_flip = true, + Err(e) => eprintln!("mirada-compositor · queue_frame: {e}"), } } - self.frames += 1; } - Err(e) => eprintln!(" error pintando el cuadro: {e}"), + Err(e) => eprintln!("mirada-compositor · render_frame: {e}"), + } + // Avisa a cada cliente de que puede dibujar el siguiente cuadro. + let time = self.start.elapsed().as_millis() as u32; + for w in &self.app.windows { + send_frames_surface_tree(&w.surface, time); + } + } + + /// Tarea periódica: Cerebro enlazado, recarga del keymap, API de + /// control, composición y vaciado hacia los clientes. + fn tick(&mut self) { + self.app.brain_poll(); + + if self.keymap_watch.as_ref().is_some_and(|w| w.changed()) { + if let Some(path) = &self.keymap_path { + match Keymap::load(path) { + Ok(km) => { + let cmd = if let Brain::Embedded(d) = &mut self.app.brain { + Some(d.set_keymap(km)) + } else { + None + }; + if let Some(cmd) = cmd { + self.app.apply_commands(vec![cmd]); + } + println!("mirada-compositor · keymap recargado."); + } + Err(e) => eprintln!("mirada-compositor · keymap inválido: {e}"), + } + } + } + + if let Some(ctl) = &self.ctl { + while let Some(mut conn) = ctl.poll() { + let reply = match conn.read_request() { + Ok(Some(req)) => self.app.serve_ctl(req), + Ok(None) => continue, + Err(e) => CtlReply::Error(format!("{e}")), + }; + let _ = conn.reply(&reply); + } + } + + self.render(); + let _ = self.display.flush_clients(); + } + + /// Procesa un evento de `libinput` — por ahora, sólo el teclado. + fn handle_input(&mut self, event: InputEvent) { + let InputEvent::Keyboard { event } = event else { + return; // puntero/táctil: pendiente + }; + let Some(keyboard) = self.app.keyboard.clone() else { + return; + }; + let code = event.key_code(); + let key_state = event.state(); + let pressed = key_state == KeyState::Pressed; + let time = self.start.elapsed().as_millis() as u32; + keyboard.input::<(), _>( + &mut self.app, + code, + key_state, + SERIAL_COUNTER.next_serial(), + time, + |st, mods, handle| { + if !pressed { + return FilterResult::Forward; + } + if let Some(combo) = combo_string(mods, handle.modified_sym()) { + if st.grabs.contains(&combo) { + st.pending_keybind = Some(combo); + return FilterResult::Intercept(()); + } + } + FilterResult::Forward + }, + ); + if let Some(combo) = self.app.pending_keybind.take() { + let ev = self.app.body.keybind(combo); + self.app.brain_feed(ev); } } } -/// Arranca el Cuerpo sobre DRM/KMS — fases 1 y 2a. +/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. pub fn run() -> Result<(), Box> { - println!("mirada-compositor · backend DRM — fases 1 (bring-up) y 2a (render)."); + println!("mirada-compositor · backend DRM."); println!("──────────────────────────────────────────────────"); // 1 · Sesión. - println!("[1/7] abriendo la sesión (libseat) …"); - let (mut session, _notifier) = LibSeatSession::new().map_err(|e| { + println!("[1/8] abriendo la sesión (libseat) …"); + let (mut session, session_notifier) = LibSeatSession::new().map_err(|e| { format!( "no pude abrir la sesión libseat: {e}\n \ ¿estás en una TTY de verdad (Ctrl+Alt+F3), con `seatd` o `logind`?" @@ -99,14 +215,14 @@ pub fn run() -> Result<(), Box> { println!(" sesión abierta · seat «{seat_name}»"); // 2 · GPU primaria. - println!("[2/7] buscando la GPU primaria …"); + println!("[2/8] buscando la GPU primaria …"); let gpu = udev::primary_gpu(&seat_name) .map_err(|e| format!("error consultando udev: {e}"))? .ok_or("no encontré ninguna GPU — ¿existe algún /dev/dri/card*?")?; println!(" GPU primaria: {}", gpu.display()); // 3 · Dispositivo DRM. - println!("[3/7] abriendo el dispositivo DRM …"); + println!("[3/8] abriendo el dispositivo DRM …"); let fd = session .open(&gpu, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NONBLOCK) .map_err(|e| format!("no pude abrir {}: {e}", gpu.display()))?; @@ -116,11 +232,10 @@ pub fn run() -> Result<(), Box> { println!(" dispositivo DRM listo."); // 4 · Elegir la salida conectada: conector + CRTC + modo. - println!("[4/7] eligiendo salida (conector + CRTC + modo) …"); + println!("[4/8] eligiendo salida …"); let resources = drm .resource_handles() .map_err(|e| format!("no pude leer los recursos DRM: {e}"))?; - let mut chosen = None; for &conn_handle in resources.connectors() { let conn = match drm.get_connector(conn_handle, false) { @@ -132,30 +247,26 @@ pub fn run() -> Result<(), Box> { } let name = format!("{:?}-{}", conn.interface(), conn.interface_id()); let Some(&mode) = conn.modes().first() else { - println!(" «{name}» sin modos — la salto"); continue; }; - // Un CRTC capaz de gobernar este conector, vía sus encoders. let crtc = conn .encoders() .iter() .filter_map(|enc| drm.get_encoder(*enc).ok()) .find_map(|enc| resources.filter_crtcs(enc.possible_crtcs()).into_iter().next()); - match crtc { - Some(crtc) => { - let (w, h) = mode.size(); - println!(" salida «{name}» · {w}×{h} · CRTC {crtc:?}"); - chosen = Some((conn_handle, crtc, mode, name)); - break; - } - None => println!(" «{name}» sin CRTC libre — la salto"), + if let Some(crtc) = crtc { + let (w, h) = mode.size(); + println!(" salida «{name}» · {w}×{h} · CRTC {crtc:?}"); + chosen = Some((conn_handle, crtc, mode, name)); + break; } } let (conn_handle, crtc, mode, out_name) = chosen.ok_or("ninguna salida conectada con CRTC disponible")?; + let (mode_w, mode_h) = mode.size(); // 5 · GBM + EGL + GlesRenderer. - println!("[5/7] inicializando GBM + EGL + GlesRenderer …"); + println!("[5/8] inicializando GBM + EGL + GlesRenderer …"); let gbm = GbmDevice::new(drm_fd.clone()).map_err(|e| format!("GbmDevice::new falló: {e}"))?; let egl_display = unsafe { EGLDisplay::new(gbm.clone()) }.map_err(|e| format!("EGLDisplay::new falló: {e}"))?; @@ -165,17 +276,17 @@ pub fn run() -> Result<(), Box> { unsafe { GlesRenderer::new(egl_context) }.map_err(|e| format!("GlesRenderer falló: {e}"))?; println!(" renderer GLES listo."); - // 6 · La superficie DRM y el DrmCompositor de esta salida. - println!("[6/7] creando la superficie DRM y el compositor …"); + // 6 · Superficie DRM + DrmCompositor de la salida. + println!("[6/8] creando la superficie DRM y el compositor …"); let surface = drm .create_surface(crtc, mode, &[conn_handle]) .map_err(|e| format!("create_surface falló: {e}"))?; - let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT); + let allocator = + GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT); let exporter = GbmFramebufferExporter::new(gbm.clone(), None); let renderer_formats = renderer.dmabuf_formats(); - let (mw, mh) = mode.size(); let mode_source = OutputModeSource::Static { - size: Size::from((mw as i32, mh as i32)), + size: Size::from((mode_w as i32, mode_h as i32)), scale: Scale::from(1.0), transform: Transform::Normal, }; @@ -193,46 +304,127 @@ pub fn run() -> Result<(), Box> { .map_err(|e| format!("DrmCompositor::new falló: {e}"))?; println!(" compositor de «{out_name}» listo."); - // 7 · Bucle de prueba: pinta colores ~6 s, sincronizado al VBlank. - println!("[7/7] test de pintado — la pantalla debería cambiar de color …"); - let mut event_loop: EventLoop = + // 7 · El estado Wayland (Cerebro, teclado, keymap, control). + println!("[7/8] armando el estado Wayland …"); + let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } = crate::build_app()?; + // La salida del Cerebro = el modo del monitor. + let ev = app.body.add_output(0, mode_w as i32, mode_h as i32); + app.brain_feed(ev); + + // El socket Wayland por el que se conectan los clientes. + let listener = ListeningSocket::bind_auto("wayland", 1..32)?; + let socket_name = listener + .socket_name() + .and_then(|s| s.to_str()) + .unwrap_or("wayland-?") + .to_string(); + std::env::set_var("WAYLAND_DISPLAY", &socket_name); + println!(" escuchando en WAYLAND_DISPLAY={socket_name}"); + + // 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer. + println!("[8/8] montando el bucle de eventos …"); + let mut event_loop: EventLoop = EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?; - event_loop - .handle() + let handle = event_loop.handle(); + + // Sesión: pausa/activación al cambiar de VT. + handle + .insert_source(session_notifier, |event, _, _state| match event { + SessionEvent::PauseSession => println!("mirada-compositor · sesión en pausa."), + SessionEvent::ActivateSession => println!("mirada-compositor · sesión activa."), + }) + .map_err(|e| format!("insert session: {e}"))?; + + // VBlank: el page-flip terminó. + handle .insert_source(drm_notifier, |event, _meta, state| match event { DrmEvent::VBlank(_crtc) => { if let Err(e) = state.compositor.frame_submitted() { - eprintln!(" frame_submitted error: {e}"); + eprintln!("mirada-compositor · frame_submitted: {e}"); } - state.render(); + state.pending_flip = false; } - DrmEvent::Error(e) => eprintln!(" DRM error: {e}"), + DrmEvent::Error(e) => eprintln!("mirada-compositor · DRM: {e}"), }) - .map_err(|e| format!("no pude registrar el DRM en calloop: {e}"))?; + .map_err(|e| format!("insert drm: {e}"))?; - let mut state = TestState { + // Teclado y ratón vía libinput. + let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone())); + libinput + .udev_assign_seat(&seat_name) + .map_err(|()| "libinput: no pude asignar el seat")?; + handle + .insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| { + state.handle_input(event); + }) + .map_err(|e| format!("insert libinput: {e}"))?; + + // Clientes Wayland nuevos. + handle + .insert_source( + Generic::new(listener, Interest::READ, CalloopMode::Level), + |_readiness, listener, state| { + while let Some(stream) = listener.accept()? { + let _ = state + .display + .handle() + .insert_client(stream, Arc::new(ClientState::default())); + } + Ok(PostAction::Continue) + }, + ) + .map_err(|e| format!("insert socket: {e}"))?; + + // Peticiones de los clientes ya conectados. + let poll_fd = display.backend().poll_fd().try_clone_to_owned()?; + handle + .insert_source( + Generic::new(poll_fd, Interest::READ, CalloopMode::Level), + |_readiness, _fd, state| { + let DrmState { display, app, .. } = state; + if let Err(e) = display.dispatch_clients(app) { + eprintln!("mirada-compositor · dispatch: {e}"); + } + let _ = display.flush_clients(); + Ok(PostAction::Continue) + }, + ) + .map_err(|e| format!("insert display: {e}"))?; + + // Timer de composición + tareas — ~60 Hz. + handle + .insert_source(Timer::immediate(), |_instant, _meta, state| { + state.tick(); + TimeoutAction::ToDuration(Duration::from_millis(16)) + }) + .map_err(|e| format!("insert timer: {e}"))?; + + println!("──────────────────────────────────────────────────"); + println!("mirada-compositor · escritorio en marcha sobre «{out_name}»."); + println!(" Lanza un cliente: WAYLAND_DISPLAY={socket_name} foot"); + println!(" Salir: Super+Shift+e."); + + let mut state = DrmState { + app, + display, compositor, renderer, - frames: 0, + pending_flip: false, + keymap_path, + keymap_watch, + ctl, start: Instant::now(), }; - // Primer cuadro: arranca la cadena render → VBlank → render. - state.render(); let signal = event_loop.get_signal(); event_loop - .run(Some(Duration::from_millis(16)), &mut state, |state| { - // Tope: ~6 s de cuadros, o 10 s de reloj (anti-cuelgue si no - // llegaran los VBlank). - if state.frames >= 360 || state.start.elapsed() > Duration::from_secs(10) { + .run(None, &mut state, |state| { + if !state.app.running { signal.stop(); } }) .map_err(|e| format!("el bucle de eventos falló: {e}"))?; - println!("──────────────────────────────────────────────────"); - println!("mirada-compositor · fase 2a completada — {} cuadros pintados.", state.frames); - println!(" Si viste la pantalla cambiar de color, EGL/GBM/modeset/page-flip"); - println!(" funcionan. Copia estos logs y seguimos con la fase 2b (clientes)."); + println!("mirada-compositor · adiós."); Ok(()) } diff --git a/crates/apps/mirada-compositor/src/main.rs b/crates/apps/mirada-compositor/src/main.rs index ac7c338..7e64689 100644 --- a/crates/apps/mirada-compositor/src/main.rs +++ b/crates/apps/mirada-compositor/src/main.rs @@ -513,9 +513,21 @@ fn load_user_rules() -> Rules { } } -/// El backend `winit`: corre anidado dentro de una sesión gráfica. -fn run_winit() -> Result<(), Box> { - let mut display: Display = Display::new()?; +/// Lo que comparten los dos backends gráficos: el `Display` de Wayland, +/// el `App` ya armado y la maquinaria de keymap y control. +struct Setup { + display: Display, + app: App, + keymap_path: Option, + keymap_watch: Option, + ctl: Option, +} + +/// Arma el estado del compositor — todo lo independiente del backend +/// gráfico (Wayland, Cerebro, teclado, keymap, control). Cada backend +/// (winit o DRM) registra luego su propia salida y monta su bucle. +fn build_app() -> Result> { + let display: Display = Display::new()?; let dh = display.handle(); let mut seat_state = SeatState::new(); @@ -545,7 +557,7 @@ fn run_winit() -> Result<(), Box> { } }; - let mut state = App { + let mut app = App { compositor_state: CompositorState::new::(&dh), xdg_shell_state: XdgShellState::new::(&dh), shm_state: ShmState::new::(&dh, Vec::new()), @@ -562,18 +574,18 @@ fn run_winit() -> Result<(), Box> { running: true, }; - let keyboard = state.seat.add_keyboard(Default::default(), 200, 25)?; - state.keyboard = Some(keyboard.clone()); + let keyboard = app.seat.add_keyboard(Default::default(), 200, 25)?; + app.keyboard = Some(keyboard); // En modo embebido, el propio Desktop dicta los atajos a interceptar. - if let Brain::Embedded(desktop) = &state.brain { + if let Brain::Embedded(desktop) = &app.brain { let grab = desktop.grab_keys(); - state.apply_commands(vec![grab]); + app.apply_commands(vec![grab]); } // Vigilancia del keymap para recargarlo en caliente — sólo tiene // sentido con el Cerebro embebido. - let keymap_watch = match (&state.brain, &keymap_path) { + let keymap_watch = match (&app.brain, &keymap_path) { (Brain::Embedded(_), Some(p)) => Keymap::watch(p).ok(), _ => None, }; @@ -583,7 +595,7 @@ fn run_winit() -> Result<(), Box> { // API de control (mirada-ctl) — sólo con el Cerebro embebido; si es // externo, el socket de control lo abre él. - let ctl = match &state.brain { + let ctl = match &app.brain { Brain::Embedded(_) => { let path = mirada_brain::ctl::default_socket_path(); match CtlServer::bind(&path) { @@ -600,6 +612,20 @@ fn run_winit() -> Result<(), Box> { Brain::Linked(_) => None, }; + Ok(Setup { display, app, keymap_path, keymap_watch, ctl }) +} + +/// El backend `winit`: corre anidado dentro de una sesión gráfica. +fn run_winit() -> Result<(), Box> { + let Setup { + mut display, + app: mut state, + keymap_path, + keymap_watch, + ctl, + } = build_app()?; + let keyboard = state.keyboard.clone().expect("teclado inicializado"); + // El backend gráfico va primero. winit abre la ventana del compositor // dentro de tu sesión gráfica anfitriona, y para encontrarla lee // `WAYLAND_DISPLAY` / `DISPLAY` del entorno. Si publicáramos antes