751416252f
Sin decoración, las ventanas se confundían entre sí. Ahora el backend DRM dibuja un marco fino alrededor de cada ventana: azul la que tiene el foco del teclado, gris las demás. - `ManagedWindow` gana `focused: bool` (lo fija `exec_op` al atender `BodyOp::Focus`/`Unfocus`) y `borders: [SolidColorBuffer; 4]` — un búfer por lado, cada uno con su `Id` estable para el seguimiento de daño; `SolidColorBuffer` sube su contador sólo si tamaño o color cambian, así un marco quieto no fuerza recomposición. - El enum `Frame` pasa de `Cursor` a `Solid`: una variante de color sólido que sirve para el cursor y para los marcos (dos variantes con el mismo tipo chocarían en el `From` que genera `render_elements!`). - `render()` en dos pasos: refresca los búferes (tamaño = contenido, color = foco) y luego arma los elementos. El marco va metido hacia adentro, sobre el borde de la superficie, así no pisa al vecino. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
798 lines
32 KiB
Rust
798 lines
32 KiB
Rust
//! `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, MotionEvent};
|
||
use smithay::output::OutputModeSource;
|
||
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, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER,
|
||
};
|
||
|
||
use mirada_brain::{BodyEvent, CtlReply, Keymap, Rect};
|
||
|
||
use crate::{combo_string, send_frames_surface_tree, App, 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>,
|
||
compositor: Compositor,
|
||
renderer: GlesRenderer,
|
||
/// `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.pending_flip {
|
||
return; // aún esperamos el VBlank del cuadro anterior
|
||
}
|
||
|
||
// 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 {
|
||
continue;
|
||
}
|
||
let (x, y) = crate::render_loc(w);
|
||
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();
|
||
|
||
let (cx, cy) = self.app.pointer_loc;
|
||
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,
|
||
)));
|
||
|
||
let mut shown: Vec<_> = self.app.windows.iter().filter(|w| w.visible).collect();
|
||
shown.sort_by_key(|w| !w.floating);
|
||
for w in &shown {
|
||
let (x, y) = crate::render_loc(w);
|
||
let (sw, sh) = crate::surface_px_size(w).unwrap_or(w.size);
|
||
let rects = border_rects(x, y, sw, sh);
|
||
// El marco, encima de la propia superficie de la ventana.
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// 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);
|
||
(
|
||
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);
|
||
|
||
// Foco-sigue-ratón: al pasar a otra ventana, que el Cerebro la enfoque.
|
||
let hovered = hit.map(|i| self.app.windows[i].id);
|
||
if hovered != self.last_pointer_window {
|
||
self.last_pointer_window = hovered;
|
||
if let Some(id) = hovered {
|
||
let ev = self.app.body.pointer_enter(id);
|
||
self.app.brain_feed(ev);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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 (las flotantes ganan 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| !self.app.windows[i].floating);
|
||
idx.into_iter().find(|&i| {
|
||
let w = &self.app.windows[i];
|
||
let (lx, ly) = crate::render_loc(w);
|
||
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.
|
||
pub fn run() -> 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()?;
|
||
// 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 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}");
|
||
|
||
// 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);
|
||
}
|
||
|
||
// 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 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!("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.
|
||
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()? {
|
||
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}"))?;
|
||
|
||
// 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,
|
||
compositor,
|
||
renderer,
|
||
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(())
|
||
}
|