Files
brahman/crates/apps/mirada-compositor/src/drm_backend.rs
T
sergio 758f61f52a feat(carmen): modo greeter — mirada-compositor como DM
`mirada-compositor --greeter` arranca como gestor de login: lanza
mirada-greeter como proceso hijo, lee su stdout y, al recibir el
SessionTicket, muta de BodyMode::Greeter a BodyMode::Session sin
reiniciar el servidor Wayland — la «mutación atómica» del DM.

- BodyMode { Greeter, Session }: eje ortogonal a Brain (Embedded/Linked).
- modo greeter: sin atajos registrados, rechaza Spawn, sin autoarranque.
- traspaso (complete_greeter_handoff): registra los atajos y arranca la
  sesión — el comando del tiquet, o el autoarranque del usuario.
- privilegios: el compositor corre como root; spawn_command baja a
  setuid/setgid + grupos suplementarios del usuario autenticado.
- bandera ortogonal al backend (--greeter [--drm|--winit]); el tiquet
  llega por un canal calloop en DRM y por mpsc en winit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 00:06:59 +00:00

938 lines
38 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `drm_backend` — el Cuerpo del compositor sobre **DRM/KMS**, sin
//! sesión gráfica anfitriona: corre directo sobre una TTY, como tu
//! escritorio de verdad.
//!
//! 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**: 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};
use smithay::backend::allocator::Fourcc;
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::input::{
AbsolutePositionEvent, Axis, AxisSource, ButtonState, InputEvent, KeyState, KeyboardKeyEvent,
PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
};
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
use smithay::backend::renderer::element::surface::{
render_elements_from_surface_tree, WaylandSurfaceRenderElement,
};
use smithay::backend::renderer::element::{render_elements, Id, Kind};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::utils::CommitCounter;
use smithay::backend::renderer::{ImportAll, ImportDma};
use smithay::backend::session::libseat::LibSeatSession;
use smithay::backend::session::{Event as SessionEvent, Session};
use smithay::backend::udev;
use smithay::input::keyboard::FilterResult;
use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
use smithay::output::OutputModeSource;
use smithay::reexports::calloop::channel::{channel as ticket_channel, Event as TicketEvent};
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, ModeTypeFlags};
use smithay::reexports::input::Libinput;
use smithay::reexports::rustix::fs::OFlags;
use smithay::reexports::wayland_server::{Display, ListeningSocket};
use smithay::utils::{
DeviceFd, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
};
use brahman_auth::SessionTicket;
use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
use crate::{
combo_string, send_frames_surface_tree, App, BodyMode, Brain, ClientState, DragGrab, DragMode,
Setup,
};
/// El `DrmCompositor` concreto para la salida (un solo GPU).
type Compositor =
DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>;
render_elements! {
/// Lo que el backend DRM compone en un cuadro: superficies de cliente
/// y rectángulos de color sólido (el cursor y los marcos de ventana).
Frame<R> where R: ImportAll;
Window = WaylandSurfaceRenderElement<R>,
Solid = SolidColorRenderElement,
}
/// 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];
/// Lado del cursor de software, en píxeles.
const CURSOR_SIZE: i32 = 12;
/// Color del cursor — un cuadrado casi blanco, opaco.
const CURSOR_COLOR: [f32; 4] = [0.95, 0.95, 0.97, 1.0];
/// Lado mínimo de una ventana al redimensionarla con el ratón.
const MIN_WINDOW: i32 = 120;
/// Grosor del marco de una ventana, en píxeles.
const BORDER_WIDTH: i32 = 2;
/// Color del marco de la ventana enfocada — un azul que resalta.
const BORDER_FOCUS: [f32; 4] = [0.36, 0.56, 0.92, 1.0];
/// Color del marco de las ventanas sin foco — gris discreto.
const BORDER_NORMAL: [f32; 4] = [0.22, 0.22, 0.27, 1.0];
/// Los 4 rectángulos `(x, y, w, h)` del marco de una ventana cuyo
/// contenido ocupa `(sx, sy, sw, sh)`. El marco va *hacia adentro* (pisa
/// el borde de la superficie), así nunca se solapa con el de la ventana
/// vecina: arriba, abajo, izquierda, derecha.
fn border_rects(sx: i32, sy: i32, sw: i32, sh: i32) -> [(i32, i32, i32, i32); 4] {
let bw = BORDER_WIDTH;
let side_h = (sh - 2 * bw).max(0);
[
(sx, sy, sw, bw),
(sx, sy + sh - bw, sw, bw),
(sx, sy + bw, bw, side_h),
(sx + sw - bw, sy + bw, bw, side_h),
]
}
/// Códigos de botón de `<linux/input-event-codes.h>`.
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
/// El estado del bucle DRM — lo comparten todos los callbacks de `calloop`.
struct DrmState {
app: App,
display: Display<App>,
/// El dispositivo DRM — se conserva para pausarlo y reactivarlo al
/// conmutar de VT.
drm: DrmDevice,
compositor: Compositor,
renderer: GlesRenderer,
/// Contexto `libinput` — se suspende y reanuda al conmutar de VT.
libinput: Libinput,
/// `false` mientras la sesión está cedida a otra VT — no se compone.
active: bool,
/// `true` entre que se encola un page-flip y llega su VBlank.
pending_flip: bool,
keymap_path: Option<std::path::PathBuf>,
keymap_watch: Option<mirada_brain::KeymapWatch>,
ctl: Option<crate::CtlServer>,
/// Inicio del compositor — base de tiempos para los frame-callbacks.
start: Instant,
/// Nº de ventanas en el último `tick` — para registrar los cambios.
last_windows: usize,
/// Identidad estable del cursor de software — el seguimiento de daño
/// la usa para no recomponer todo cuando el cursor sólo se mueve.
cursor_id: Id,
/// Ventana sobre la que estaba el puntero — para el foco-sigue-ratón.
last_pointer_window: Option<u64>,
/// Tamaño de la salida, en píxeles — los topes del puntero.
output_size: (f64, f64),
}
impl DrmState {
/// Compone el cursor y las ventanas y, si hubo cambios, encola el cuadro.
fn render(&mut self) {
if !self.active {
return; // la sesión está en otra VT — no tocamos la GPU
}
if self.pending_flip {
return; // aún esperamos el VBlank del cuadro anterior
}
let output_h = self.app.output_size.1;
// Paso 1 · refresca los búferes del marco de cada ventana — su
// tamaño (sigue al contenido) y su color (según el foco). Cada
// `SolidColorBuffer` sube su contador de daño sólo si algo cambió.
for w in &mut self.app.windows {
if !w.visible || w.is_shell {
continue; // el shell no lleva marco
}
let (x, y) = crate::render_loc(w, output_h);
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
let color = if w.focused { BORDER_FOCUS } else { BORDER_NORMAL };
let rects = border_rects(x, y, sw, sh);
for (buf, (_, _, bw, bh)) in w.borders.iter_mut().zip(rects) {
buf.update((bw, bh), color);
}
}
// Paso 2 · arma los elementos — lista front-to-back (índice 0 =
// encima): el cursor, y por cada ventana su marco sobre su
// superficie. Las flotantes van antes que las teseladas.
let elements: Vec<Frame<GlesRenderer>> = {
let mut out: Vec<Frame<GlesRenderer>> = Vec::new();
// El cursor — la superficie que pidió el cliente (la «I» del
// texto, una mano…), o el cuadrado por defecto si pidió un
// cursor con nombre y no hay tema. `Hidden` no pinta nada.
let (cx, cy) = self.app.pointer_loc;
match &self.app.cursor_status {
CursorImageStatus::Hidden => {}
CursorImageStatus::Surface(surface) if surface.alive() => {
let (hx, hy) = crate::cursor_hotspot(surface);
let loc = (cx.round() as i32 - hx, cy.round() as i32 - hy);
for el in render_elements_from_surface_tree(
&mut self.renderer,
surface,
loc,
1.0,
1.0,
Kind::Cursor,
) {
out.push(Frame::Window(el));
}
}
_ => {
let cursor_rect = Rectangle::new(
Point::<i32, Physical>::from((cx.round() as i32, cy.round() as i32)),
Size::<i32, Physical>::from((CURSOR_SIZE, CURSOR_SIZE)),
);
out.push(Frame::Solid(SolidColorRenderElement::new(
self.cursor_id.clone(),
cursor_rect,
CommitCounter::default(),
CURSOR_COLOR,
Kind::Cursor,
)));
}
}
// El shell va sobre todo; luego las flotantes; luego las
// teseladas. `sort_by_key` es estable: respeta el orden de
// apertura dentro de cada grupo.
let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
shown.sort_by_key(|w| (!w.is_shell, !w.floating));
for w in &shown {
let (x, y) = crate::render_loc(w, output_h);
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
// El marco, encima de la propia superficie de la ventana
// — el shell no lleva.
if !w.is_shell {
let rects = border_rects(x, y, sw, sh);
for (buf, (bx, by, _, _)) in w.borders.iter().zip(rects) {
out.push(Frame::Solid(SolidColorRenderElement::from_buffer(
buf,
(bx, by),
1.0,
1.0,
Kind::Unspecified,
)));
}
}
for el in render_elements_from_surface_tree(
&mut self.renderer,
&w.surface,
(x, y),
1.0,
1.0,
Kind::Unspecified,
) {
out.push(Frame::Window(el));
}
}
out
};
match self.compositor.render_frame::<_, _>(
&mut self.renderer,
&elements,
CLEAR_COLOR,
FrameFlags::DEFAULT,
) {
Ok(result) => {
if !result.is_empty {
match self.compositor.queue_frame(()) {
Ok(()) => self.pending_flip = true,
Err(e) => eprintln!("mirada-compositor · queue_frame: {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);
}
// También a la superficie del cursor, por si es un cursor animado.
if let CursorImageStatus::Surface(surface) = &self.app.cursor_status {
if surface.alive() {
send_frames_surface_tree(surface, time);
}
}
}
/// La sesión se cede a otra VT (`Ctrl+Alt+Fn`): suelta la GPU y deja
/// de leer el ratón y el teclado, para no chocar con quien ahora
/// manda en la pantalla.
fn pause_session(&mut self) {
self.active = false;
self.drm.pause();
self.libinput.suspend();
println!("mirada-compositor · sesión cedida a otra VT.");
}
/// La sesión vuelve a esta VT: recupera la GPU y la entrada, reinicia
/// el estado del compositor y repinta.
fn resume_session(&mut self) {
if self.libinput.resume().is_err() {
eprintln!("mirada-compositor · libinput.resume falló.");
}
if let Err(e) = self.drm.activate(false) {
eprintln!("mirada-compositor · drm.activate falló: {e}");
}
if let Err(e) = self.compositor.reset_state() {
eprintln!("mirada-compositor · compositor.reset_state falló: {e}");
}
self.active = true;
self.pending_flip = false;
self.render();
println!("mirada-compositor · sesión recuperada.");
}
/// 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();
let n = self.app.windows.len();
if n != self.last_windows {
eprintln!("mirada-compositor · ventanas en pantalla: {n}");
self.last_windows = n;
}
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`: teclado y puntero.
fn handle_input(&mut self, event: InputEvent<LibinputInputBackend>) {
let time = self.start.elapsed().as_millis() as u32;
match event {
// --- Teclado: intercepta los atajos del Cerebro --------------
InputEvent::Keyboard { event } => {
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;
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);
}
}
// --- Puntero: movimiento relativo (ratón, touchpad) ----------
InputEvent::PointerMotion { event } => {
let (mut x, mut y) = self.app.pointer_loc;
x = (x + event.delta_x()).clamp(0.0, self.output_size.0);
y = (y + event.delta_y()).clamp(0.0, self.output_size.1);
self.app.pointer_loc = (x, y);
if !self.drag_update() {
self.pointer_motion(time);
}
}
// --- Puntero: movimiento absoluto (táctil, tableta) ----------
InputEvent::PointerMotionAbsolute { event } => {
let space = Size::<i32, Logical>::from((
self.output_size.0 as i32,
self.output_size.1 as i32,
));
let pos = event.position_transformed(space);
self.app.pointer_loc = (
pos.x.clamp(0.0, self.output_size.0),
pos.y.clamp(0.0, self.output_size.1),
);
if !self.drag_update() {
self.pointer_motion(time);
}
}
// --- Puntero: botones ----------------------------------------
InputEvent::PointerButton { event } => {
let pressed = event.state() == ButtonState::Pressed;
let button = event.button_code();
// ¿Empieza un arrastre? `Super`+botón sobre una ventana:
// izquierdo mueve, derecho redimensiona.
if pressed && self.app.drag.is_none() {
let super_held = self
.app
.keyboard
.as_ref()
.is_some_and(|kb| kb.modifier_state().logo);
let mode = match button {
BTN_LEFT if super_held => Some(DragMode::Move),
BTN_RIGHT if super_held => Some(DragMode::Resize),
_ => None,
};
if let Some(mode) = mode {
let (x, y) = self.app.pointer_loc;
if let Some(i) = self.window_at(x, y) {
let w = &self.app.windows[i];
let grab = DragGrab {
id: w.id,
mode,
start_pointer: (x, y),
start_rect: (w.loc.0, w.loc.1, w.size.0, w.size.1),
};
self.app.drag = Some(grab);
return; // el arrastre captura el botón
}
}
}
// Durante un arrastre los botones no llegan al cliente;
// soltar cualquiera lo termina.
if self.app.drag.is_some() {
if !pressed {
self.app.drag = None;
}
return;
}
// Botón normal: a la ventana bajo el puntero.
let Some(pointer) = self.app.pointer.clone() else {
return;
};
pointer.button(
&mut self.app,
&ButtonEvent {
serial: SERIAL_COUNTER.next_serial(),
time,
button,
state: event.state(),
},
);
pointer.frame(&mut self.app);
}
// --- Puntero: rueda / desplazamiento -------------------------
InputEvent::PointerAxis { event } => {
let Some(pointer) = self.app.pointer.clone() else {
return;
};
let source = event.source();
let mut frame = AxisFrame::new(time).source(source);
for axis in [Axis::Horizontal, Axis::Vertical] {
match event.amount(axis) {
Some(v) if v != 0.0 => frame = frame.value(axis, v),
Some(_) if source == AxisSource::Finger => {
frame = frame.stop(axis);
}
_ => {}
}
if let Some(d) = event.amount_v120(axis) {
frame = frame.v120(axis, d as i32);
}
}
pointer.axis(&mut self.app, frame);
pointer.frame(&mut self.app);
}
_ => {} // otros dispositivos: aún no
}
}
/// Reenvía el puntero a la ventana que tiene debajo y, si esa ventana
/// cambió, aplica el foco-sigue-ratón avisando al Cerebro.
fn pointer_motion(&mut self, time: u32) {
let Some(pointer) = self.app.pointer.clone() else {
return;
};
let (x, y) = self.app.pointer_loc;
let hit = self.window_at(x, y);
let focus = hit.map(|i| {
let w = &self.app.windows[i];
let (lx, ly) = crate::render_loc(w, self.app.output_size.1);
(
w.surface.clone(),
Point::<f64, Logical>::from((lx as f64, ly as f64)),
)
});
pointer.motion(
&mut self.app,
focus,
&MotionEvent {
location: Point::from((x, y)),
serial: SERIAL_COUNTER.next_serial(),
time,
},
);
pointer.frame(&mut self.app);
// Sobre el escritorio pelado no manda ningún cliente: el cursor
// vuelve al de por defecto (si no, se queda con la «I» del texto
// de la última ventana).
if hit.is_none() {
self.app.cursor_status = CursorImageStatus::default_named();
}
// Foco-sigue-ratón: al pasar a otra ventana, que la enfoque quien
// corresponda — el Cerebro para las teseladas, carmen mismo para
// el shell (que no vive en el Cerebro).
let hovered = hit.map(|i| self.app.windows[i].id);
if hovered != self.last_pointer_window {
self.last_pointer_window = hovered;
match hit {
Some(i) if self.app.windows[i].is_shell => {
let surf = self.app.windows[i].surface.clone();
if let Some(kb) = self.app.keyboard.clone() {
kb.set_focus(&mut self.app, Some(surf), SERIAL_COUNTER.next_serial());
}
}
Some(i) => {
let id = self.app.windows[i].id;
let ev = self.app.body.pointer_enter(id);
self.app.brain_feed(ev);
}
None => {}
}
}
}
/// Si hay un arrastre en curso, recalcula el rectángulo de la ventana
/// y se lo manda al Cerebro (que la hace flotar ahí). Devuelve `true`
/// si consumió el movimiento — entonces el puntero no llega al cliente.
fn drag_update(&mut self) -> bool {
let Some(drag) = self.app.drag.as_ref() else {
return false;
};
let mode = drag.mode;
let (spx, spy) = drag.start_pointer;
let (sx, sy, sw, sh) = drag.start_rect;
let id = drag.id;
let (px, py) = self.app.pointer_loc;
let dx = (px - spx) as i32;
let dy = (py - spy) as i32;
let rect = match mode {
DragMode::Move => Rect::new(sx + dx, sy + dy, sw, sh),
DragMode::Resize => Rect::new(
sx,
sy,
(sw + dx).max(MIN_WINDOW),
(sh + dy).max(MIN_WINDOW),
),
};
self.app.brain_feed(BodyEvent::WindowFloatTo { id, rect });
true
}
/// El índice de la ventana visible bajo el punto `(x, y)`, si la hay
/// — en orden front-to-back (el shell gana a las flotantes, y éstas a
/// las teseladas).
fn window_at(&self, x: f64, y: f64) -> Option<usize> {
let mut idx: Vec<usize> = (0..self.app.windows.len())
.filter(|&i| self.app.windows[i].visible)
.collect();
idx.sort_by_key(|&i| {
let w = &self.app.windows[i];
(!w.is_shell, !w.floating)
});
let output_h = self.app.output_size.1;
idx.into_iter().find(|&i| {
let w = &self.app.windows[i];
let (lx, ly) = crate::render_loc(w, output_h);
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
x >= lx as f64 && y >= ly as f64 && x < (lx + sw) as f64 && y < (ly + sh) as f64
})
}
}
/// Arranca el Cuerpo sobre DRM/KMS — fases 1, 2a y 2b. Con `greeter`,
/// el compositor nace en modo DM: ver [`BodyMode`].
pub fn run(greeter: bool) -> Result<(), Box<dyn Error>> {
println!("mirada-compositor · backend DRM.");
println!("──────────────────────────────────────────────────");
// 1 · Sesión.
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`?"
)
})?;
let seat_name = session.seat();
println!(" sesión abierta · seat «{seat_name}»");
// 2 · 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/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()))?;
let drm_fd = DrmDeviceFd::new(DeviceFd::from(fd));
let (mut drm, drm_notifier) =
DrmDevice::new(drm_fd.clone(), true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
println!(" dispositivo DRM listo.");
// 4 · Elegir la salida conectada: 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) {
Ok(c) => c,
Err(_) => continue,
};
if conn.state() != ConnectorState::Connected {
continue;
}
let name = format!("{:?}-{}", conn.interface(), conn.interface_id());
// Registra todos los modos del panel — diagnóstico.
for m in conn.modes() {
let (mw, mh) = m.size();
let pref = if m.mode_type().contains(ModeTypeFlags::PREFERRED) {
" [PREFERRED]"
} else {
""
};
eprintln!(" modo de «{name}»: {mw}×{mh} @ {} Hz{pref}", m.vrefresh());
}
// Elige el modo de mayor área (a igualdad, mayor refresco) — el
// nativo del panel. La marca PREFERRED no es fiable: a veces
// señala un modo menor.
let mode = conn
.modes()
.iter()
.max_by_key(|m| {
let (mw, mh) = m.size();
(mw as u32 * mh as u32, m.vrefresh())
})
.copied();
let Some(mode) = mode else {
continue;
};
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());
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/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}"))?;
let egl_context =
EGLContext::new(&egl_display).map_err(|e| format!("EGLContext::new falló: {e}"))?;
let renderer =
unsafe { GlesRenderer::new(egl_context) }.map_err(|e| format!("GlesRenderer falló: {e}"))?;
println!(" renderer GLES listo.");
// 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 exporter = GbmFramebufferExporter::new(gbm.clone(), None);
let renderer_formats = renderer.dmabuf_formats();
let mode_source = OutputModeSource::Static {
size: Size::from((mode_w as i32, mode_h as i32)),
scale: Scale::from(1.0),
transform: Transform::Normal,
};
let compositor: Compositor = DrmCompositor::new(
mode_source,
surface,
None,
allocator,
exporter,
[Fourcc::Argb8888, Fourcc::Xrgb8888],
renderer_formats,
drm.cursor_size(),
Some(gbm.clone()),
)
.map_err(|e| format!("DrmCompositor::new falló: {e}"))?;
println!(" compositor de «{out_name}» listo.");
// 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(greeter)?;
// Con el renderer ya creado, anuncia dmabuf — sin esto las apps que
// pintan por GPU (GPUI, navegadores acelerados) no pueden conectarse.
crate::announce_dmabuf(&mut app, &display.handle(), &renderer);
// 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);
app.output_size = (mode_w as i32, mode_h as i32);
// El puntero arranca en el centro de la pantalla.
app.pointer_loc = (mode_w as f64 / 2.0, mode_h as f64 / 2.0);
// Anuncia el monitor en el protocolo Wayland — los clientes lo exigen.
let _wl_output = crate::announce_output(
&display.handle(),
&out_name,
mode_w as i32,
mode_h as i32,
mode.vrefresh() as i32 * 1000,
);
// 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}");
// Modo DM: lanza el greeter y recibe su tiquet por un canal de
// `calloop`. Modo normal: autoarranque + `MIRADA_STARTUP`.
let greeter_rx = if app.mode == BodyMode::Greeter {
let (tx, rx) = ticket_channel::<SessionTicket>();
crate::spawn_greeter(move |ticket| {
let _ = tx.send(ticket);
})?;
Some(rx)
} else {
// Autoarranque: los programas de `~/.config/mirada/autostart`.
crate::spawn_autostart(None);
// App de arranque: si `MIRADA_STARTUP` trae un comando, se lanza
// como hijo (hereda `WAYLAND_DISPLAY`) — cómodo para probar sin
// saltar de VT.
if let Ok(cmd) = std::env::var("MIRADA_STARTUP") {
crate::spawn_command(&cmd, None);
}
None
};
// 8 · El bucle `calloop`: VBlank, teclado, clientes y un timer.
println!("[8/8] montando el bucle de eventos …");
let mut event_loop: EventLoop<DrmState> =
EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
let handle = event_loop.handle();
// Sesión: pausa/activación al conmutar de VT.
handle
.insert_source(session_notifier, |event, _, state: &mut DrmState| match event {
SessionEvent::PauseSession => state.pause_session(),
SessionEvent::ActivateSession => state.resume_session(),
})
.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!("mirada-compositor · frame_submitted: {e}");
}
state.pending_flip = false;
}
DrmEvent::Error(e) => eprintln!("mirada-compositor · DRM: {e}"),
})
.map_err(|e| format!("insert drm: {e}"))?;
// Teclado y ratón vía libinput. Guardamos un clon del contexto (es
// un manejador con contador de referencias) para suspenderlo y
// reanudarlo al conmutar de VT.
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")?;
let libinput_handle = libinput.clone();
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()? {
eprintln!("mirada-compositor · cliente Wayland conectado.");
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}"))?;
// Tiquet del greeter (modo DM): al llegar, el traspaso a la sesión.
// El hilo lector del greeter despierta el bucle por este canal.
if let Some(rx) = greeter_rx {
handle
.insert_source(rx, |event, _, state: &mut DrmState| {
if let TicketEvent::Msg(ticket) = event {
state.app.complete_greeter_handoff(ticket);
}
})
.map_err(|e| format!("insert greeter: {e}"))?;
}
// Tope de tiempo opcional: `MIRADA_DRM_TIMEOUT=<segundos>` cierra el
// compositor solo (0 o sin definir = sin tope). El teclado ya
// funciona — `Super+Shift+e` o `Ctrl+C` son la salida normal.
let timeout_secs: u64 = std::env::var("MIRADA_DRM_TIMEOUT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(0);
println!("──────────────────────────────────────────────────");
println!("mirada-compositor · escritorio en marcha sobre «{out_name}».");
println!(" Lanza un cliente: WAYLAND_DISPLAY={socket_name} foot");
println!(" Salir: Super+Shift+e · o Ctrl+C en esta TTY.");
if timeout_secs > 0 {
println!(" Se cerrará solo a los {timeout_secs}s (MIRADA_DRM_TIMEOUT=0 lo quita).");
}
let mut state = DrmState {
app,
display,
drm,
compositor,
renderer,
libinput: libinput_handle,
active: true,
pending_flip: false,
keymap_path,
keymap_watch,
ctl,
start: Instant::now(),
last_windows: 0,
cursor_id: Id::new(),
last_pointer_window: None,
output_size: (mode_w as f64, mode_h as f64),
};
let signal = event_loop.get_signal();
event_loop
.run(None, &mut state, |state| {
let timed_out =
timeout_secs > 0 && state.start.elapsed() > Duration::from_secs(timeout_secs);
if !state.app.running || timed_out {
if timed_out {
println!("mirada-compositor · tope de tiempo — cerrando.");
}
signal.stop();
}
})
.map_err(|e| format!("el bucle de eventos falló: {e}"))?;
println!("mirada-compositor · adiós.");
Ok(())
}