⎇ fb2f3a2d015088a9caa5041588fe3ae86a52f971
crates/apps/mirada-compositor/src/drm_backend.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(mirada-compositor): backend DRM — app de arranque + traza de ventanas
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;
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 w.loc,
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 /// Va instrumentado: registra los dispositivos que encuentra y cada
173 /// tecla, para diagnosticar la entrada sin el hardware delante.
174 fn handle_input(&mut self, event: InputEvent<LibinputInputBackend>) {
175 match event {
176 InputEvent::DeviceAdded { device } => {
177 eprintln!("input · dispositivo detectado: «{}»", device.name());
178 }
179 InputEvent::Keyboard { event } => {
180 let Some(keyboard) = self.app.keyboard.clone() else {
181 eprintln!("input · ¡sin teclado en el seat!");
182 return;
183 };
184 let code = event.key_code();
185 let key_state = event.state();
186 let pressed = key_state == KeyState::Pressed;
187 let time = self.start.elapsed().as_millis() as u32;
188 keyboard.input::<(), _>(
189 &mut self.app,
190 code,
191 key_state,
192 SERIAL_COUNTER.next_serial(),
193 time,
194 |st, mods, handle| {
195 if !pressed {
196 return FilterResult::Forward;
197 }
198 let combo = combo_string(mods, handle.modified_sym());
199 let grabbed = combo.as_ref().is_some_and(|c| st.grabs.contains(c));
200 eprintln!("input · tecla {code:?} → combo {combo:?} · atajo={grabbed}");
201 if grabbed {
202 st.pending_keybind = combo;
203 return FilterResult::Intercept(());
204 }
205 FilterResult::Forward
206 },
207 );
208 if let Some(combo) = self.app.pending_keybind.take() {
209 let ev = self.app.body.keybind(combo);
210 self.app.brain_feed(ev);
211 }
212 }
213 _ => {} // puntero/táctil: pendiente
214 }
215 }
216}
217
218/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b.
219pub fn run() -> Result<(), Box<dyn Error>> {
220 println!("mirada-compositor · backend DRM.");
221 println!("──────────────────────────────────────────────────");
222
223 // 1 · Sesión.
224 println!("[1/8] abriendo la sesión (libseat) …");
225 let (mut session, session_notifier) = LibSeatSession::new().map_err(|e| {
226 format!(
227 "no pude abrir la sesión libseat: {e}\n \
228 ¿estás en una TTY de verdad (Ctrl+Alt+F3), con `seatd` o `logind`?"
229 )
230 })?;
231 let seat_name = session.seat();
232 println!(" sesión abierta · seat «{seat_name}»");
233
234 // 2 · GPU primaria.
235 println!("[2/8] buscando la GPU primaria …");
236 let gpu = udev::primary_gpu(&seat_name)
237 .map_err(|e| format!("error consultando udev: {e}"))?
238 .ok_or("no encontré ninguna GPU — ¿existe algún /dev/dri/card*?")?;
239 println!(" GPU primaria: {}", gpu.display());
240
241 // 3 · Dispositivo DRM.
242 println!("[3/8] abriendo el dispositivo DRM …");
243 let fd = session
244 .open(&gpu, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NONBLOCK)
245 .map_err(|e| format!("no pude abrir {}: {e}", gpu.display()))?;
246 let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd));
247 let (mut drm, drm_notifier) =
248 DrmDevice::new(drm_fd.clone(), true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
249 println!(" dispositivo DRM listo.");
250
251 // 4 · Elegir la salida conectada: conector + CRTC + modo.
252 println!("[4/8] eligiendo salida …");
253 let resources = drm
254 .resource_handles()
255 .map_err(|e| format!("no pude leer los recursos DRM: {e}"))?;
256 let mut chosen = None;
257 for &conn_handle in resources.connectors() {
258 let conn = match drm.get_connector(conn_handle, false) {
259 Ok(c) => c,
260 Err(_) => continue,
261 };
262 if conn.state() != ConnectorState::Connected {
263 continue;
264 }
265 let name = format!("{:?}-{}", conn.interface(), conn.interface_id());
266 let Some(&mode) = conn.modes().first() else {
267 continue;
268 };
269 let crtc = conn
270 .encoders()
271 .iter()
272 .filter_map(|enc| drm.get_encoder(*enc).ok())
273 .find_map(|enc| resources.filter_crtcs(enc.possible_crtcs()).into_iter().next());
274 if let Some(crtc) = crtc {
275 let (w, h) = mode.size();
276 println!(" salida «{name}» · {w}×{h} · CRTC {crtc:?}");
277 chosen = Some((conn_handle, crtc, mode, name));
278 break;
279 }
280 }
281 let (conn_handle, crtc, mode, out_name) =
282 chosen.ok_or("ninguna salida conectada con CRTC disponible")?;
283 let (mode_w, mode_h) = mode.size();
284
285 // 5 · GBM + EGL + GlesRenderer.
286 println!("[5/8] inicializando GBM + EGL + GlesRenderer …");
287 let gbm = GbmDevice::new(drm_fd.clone()).map_err(|e| format!("GbmDevice::new falló: {e}"))?;
288 let egl_display =
289 unsafe { EGLDisplay::new(gbm.clone()) }.map_err(|e| format!("EGLDisplay::new falló: {e}"))?;
290 let egl_context =
291 EGLContext::new(&egl_display).map_err(|e| format!("EGLContext::new falló: {e}"))?;
292 let renderer =
293 unsafe { GlesRenderer::new(egl_context) }.map_err(|e| format!("GlesRenderer falló: {e}"))?;
294 println!(" renderer GLES listo.");
295
296 // 6 · Superficie DRM + DrmCompositor de la salida.
297 println!("[6/8] creando la superficie DRM y el compositor …");
298 let surface = drm
299 .create_surface(crtc, mode, &[conn_handle])
300 .map_err(|e| format!("create_surface falló: {e}"))?;
301 let allocator =
302 GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
303 let exporter = GbmFramebufferExporter::new(gbm.clone(), None);
304 let renderer_formats = renderer.dmabuf_formats();
305 let mode_source = OutputModeSource::Static {
306 size: Size::from((mode_w as i32, mode_h as i32)),
307 scale: Scale::from(1.0),
308 transform: Transform::Normal,
309 };
310 let compositor: Compositor = DrmCompositor::new(
311 mode_source,
312 surface,
313 None,
314 allocator,
315 exporter,
316 [Fourcc::Argb8888, Fourcc::Xrgb8888],
317 renderer_formats,
318 drm.cursor_size(),
319 Some(gbm.clone()),
320 )
321 .map_err(|e| format!("DrmCompositor::new falló: {e}"))?;
322 println!(" compositor de «{out_name}» listo.");
323
324 // 7 · El estado Wayland (Cerebro, teclado, keymap, control).
325 println!("[7/8] armando el estado Wayland …");
326 let Setup { mut display, mut app, keymap_path, keymap_watch, ctl } = crate::build_app()?;
327 // La salida del Cerebro = el modo del monitor.
328 let ev = app.body.add_output(0, mode_w as i32, mode_h as i32);
329 app.brain_feed(ev);
330
331 // El socket Wayland por el que se conectan los clientes.
332 let listener = ListeningSocket::bind_auto("wayland", 1..32)?;
333 let socket_name = listener
334 .socket_name()
335 .and_then(|s| s.to_str())
336 .unwrap_or("wayland-?")
337 .to_string();
338 std::env::set_var("WAYLAND_DISPLAY", &socket_name);
339 println!(" escuchando en WAYLAND_DISPLAY={socket_name}");
340
341 // App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza como
342 // hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin saltar de VT.
343 if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
344 if !cmd.trim().is_empty() {
345 match std::process::Command::new("sh").arg("-c").arg(&cmd).spawn() {
346 Ok(child) => println!(" app de arranque lanzada (pid {}): {cmd}", child.id()),
347 Err(e) => eprintln!(" no pude lanzar «{cmd}»: {e}"),
348 }
349 }
350 }
351
352 // 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
353 println!("[8/8] montando el bucle de eventos …");
354 let mut event_loop: EventLoop<DrmState> =
355 EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
356 let handle = event_loop.handle();
357
358 // Sesión: pausa/activación al cambiar de VT.
359 handle
360 .insert_source(session_notifier, |event, _, _state| match event {
361 SessionEvent::PauseSession => println!("mirada-compositor · sesión en pausa."),
362 SessionEvent::ActivateSession => println!("mirada-compositor · sesión activa."),
363 })
364 .map_err(|e| format!("insert session: {e}"))?;
365
366 // VBlank: el page-flip terminó.
367 handle
368 .insert_source(drm_notifier, |event, _meta, state| match event {
369 DrmEvent::VBlank(_crtc) => {
370 if let Err(e) = state.compositor.frame_submitted() {
371 eprintln!("mirada-compositor · frame_submitted: {e}");
372 }
373 state.pending_flip = false;
374 }
375 DrmEvent::Error(e) => eprintln!("mirada-compositor · DRM: {e}"),
376 })
377 .map_err(|e| format!("insert drm: {e}"))?;
378
379 // Teclado y ratón vía libinput.
380 let mut libinput = Libinput::new_with_udev(LibinputSessionInterface::from(session.clone()));
381 libinput
382 .udev_assign_seat(&seat_name)
383 .map_err(|()| "libinput: no pude asignar el seat")?;
384 handle
385 .insert_source(LibinputInputBackend::new(libinput), |event, _meta, state| {
386 state.handle_input(event);
387 })
388 .map_err(|e| format!("insert libinput: {e}"))?;
389
390 // Clientes Wayland nuevos.
391 handle
392 .insert_source(
393 Generic::new(listener, Interest::READ, CalloopMode::Level),
394 |_readiness, listener, state| {
395 while let Some(stream) = listener.accept()? {
396 eprintln!("mirada-compositor · cliente Wayland conectado.");
397 let _ = state
398 .display
399 .handle()
400 .insert_client(stream, Arc::new(ClientState::default()));
401 }
402 Ok(PostAction::Continue)
403 },
404 )
405 .map_err(|e| format!("insert socket: {e}"))?;
406
407 // Peticiones de los clientes ya conectados.
408 let poll_fd = display.backend().poll_fd().try_clone_to_owned()?;
409 handle
410 .insert_source(
411 Generic::new(poll_fd, Interest::READ, CalloopMode::Level),
412 |_readiness, _fd, state| {
413 let DrmState { display, app, .. } = state;
414 if let Err(e) = display.dispatch_clients(app) {
415 eprintln!("mirada-compositor · dispatch: {e}");
416 }
417 let _ = display.flush_clients();
418 Ok(PostAction::Continue)
419 },
420 )
421 .map_err(|e| format!("insert display: {e}"))?;
422
423 // Timer de composición + tareas — ~60 Hz.
424 handle
425 .insert_source(Timer::immediate(), |_instant, _meta, state| {
426 state.tick();
427 TimeoutAction::ToDuration(Duration::from_millis(16))
428 })
429 .map_err(|e| format!("insert timer: {e}"))?;
430
431 // Salida garantizada mientras depuramos: se cierra solo a los N s
432 // (env `MIRADA_DRM_TIMEOUT`, 0 lo desactiva) — así nunca te quedas
433 // atrapado si el teclado aún no responde.
434 let timeout_secs: u64 = std::env::var("MIRADA_DRM_TIMEOUT")
435 .ok()
436 .and_then(|v| v.parse().ok())
437 .unwrap_or(60);
438
439 println!("──────────────────────────────────────────────────");
440 println!("mirada-compositor · escritorio en marcha sobre «{out_name}».");
441 println!(" Lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
442 println!(" Salir: Super+Shift+e · o Ctrl+C en esta TTY.");
443 if timeout_secs > 0 {
444 println!(" Se cerrará solo a los {timeout_secs}s (MIRADA_DRM_TIMEOUT=0 lo quita).");
445 }
446
447 let mut state = DrmState {
448 app,
449 display,
450 compositor,
451 renderer,
452 pending_flip: false,
453 keymap_path,
454 keymap_watch,
455 ctl,
456 start: Instant::now(),
457 last_windows: 0,
458 };
459
460 let signal = event_loop.get_signal();
461 event_loop
462 .run(None, &mut state, |state| {
463 let timed_out =
464 timeout_secs > 0 && state.start.elapsed() > Duration::from_secs(timeout_secs);
465 if !state.app.running || timed_out {
466 if timed_out {
467 println!("mirada-compositor · tope de tiempo — cerrando.");
468 }
469 signal.stop();
470 }
471 })
472 .map_err(|e| format!("el bucle de eventos falló: {e}"))?;
473
474 println!("mirada-compositor · adiós.");
475 Ok(())
476}