gioser

tawasuyu/brahman

Rust 459TOML 177Markdown 49JSON 176207 Definitions

feat(mirada-compositor): backend DRM — fase 2a, pipeline de render

Sobre el bring-up de la fase 1, drm_backend.rs monta ahora el pipeline
gráfico completo y lo prueba:

- elige la salida conectada (conector + CRTC + modo)
- GBM + EGL + GlesRenderer
- GbmAllocator + GbmFramebufferExporter + DrmCompositor para esa salida
- bucle calloop sincronizado al VBlank (DrmDeviceNotifier): pinta la
  pantalla de colores ~6 s y para (con tope de 10 s anti-cuelgue)

Es un test de hardware: si la pantalla cambia de color, EGL, GBM, el
modeset y el page-flip funcionan. Compila y pasa clippy aquí; se
ejecuta y depura en la máquina con GPU por logs. La fase 2b será el
bucle Wayland completo (clientes + libinput + composición de ventanas).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

f8cb80d867c6be249dcf7e094b088585929ddd59 · by sergio · (5 months ago) · parent 6c3a86fbe · browse the tree at this commit

Semantic delta · 4

crates/apps/mirada-compositor/src/drm_backend.rs

1 files · +191 −73

diff --git a/crates/apps/mirada-compositor/src/drm_backend.rs b/crates/apps/mirada-compositor/src/drm_backend.rs
index f5e4ae3..254bfc6 100644
--- a/crates/apps/mirada-compositor/src/drm_backend.rs
+++ b/crates/apps/mirada-compositor/src/drm_backend.rs
@@ -1,120 +1,238 @@
 //! `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.
+//! sesión gráfica anfitriona: corre directo sobre una TTY.
 //!
-//! Se construye por fases para poder verificarlo en hardware real
-//! paso a paso:
+//! Por fases, para verificarlo en hardware real paso a paso:
 //!
-//! - **Fase 1 — bring-up** (esto): abre la sesión (`libseat`), encuentra
-//!   la GPU, abre el dispositivo DRM y enumera las salidas físicas. No
-//!   compone nada todavía; sólo comprueba —y registra en el log— que la
-//!   ruta de hardware funciona en tu máquina.
-//! - **Fase 2** (siguiente): GBM + EGL + `GlesRenderer`, un
-//!   `DrmCompositor` por salida, `libinput` para el teclado, y el bucle
-//!   `calloop` que compone de verdad.
+//! - **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.
 //!
-//! Todo va instrumentado con `println!`/`eprintln!` para que, al
-//! correrlo, se pueda copiar la salida y diagnosticar sin tener el
-//! hardware delante.
+//! Todo con logs para diagnosticar sin el hardware delante.
 
 use std::error::Error;
+use std::time::{Duration, Instant};
 
-use smithay::backend::drm::{DrmDevice, DrmDeviceFd};
+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::renderer::element::surface::WaylandSurfaceRenderElement;
+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::udev;
+use smithay::output::OutputModeSource;
+use smithay::reexports::calloop::EventLoop;
 use smithay::reexports::drm::control::connector::State as ConnectorState;
 use smithay::reexports::drm::control::Device as ControlDevice;
 use smithay::reexports::rustix::fs::OFlags;
-use smithay::utils::DeviceFd;
+use smithay::utils::{DeviceFd, Scale, Size, Transform};
 
-/// Arranca el Cuerpo sobre DRM/KMS — **fase 1: bring-up**.
-///
-/// Abre la sesión, localiza la GPU, abre el dispositivo DRM y enumera
-/// las salidas, dejando constancia de todo en el log. La composición
-/// real es la fase 2.
+/// El `DrmCompositor` concreto para una salida (un solo GPU, `()` de
+/// datos de usuario por cuadro).
+type Compositor =
+    DrmCompositor<GbmAllocator<DrmDeviceFd>, GbmFramebufferExporter<DrmDeviceFd>, (), DrmDeviceFd>;
+
+/// El estado del test de la fase 2a: lo comparten los callbacks de `calloop`.
+struct TestState {
+    compositor: Compositor,
+    renderer: GlesRenderer,
+    /// Cuántos cuadros se han pintado.
+    frames: u32,
+    /// Inicio del test, para un tope por tiempo (anti-cuelgue).
+    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.
+    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],
+        };
+        let elements: Vec<WaylandSurfaceRenderElement<GlesRenderer>> = Vec::new();
+        match self
+            .compositor
+            .render_frame::<_, _>(&mut self.renderer, &elements, color, FrameFlags::DEFAULT)
+        {
+            Ok(result) => {
+                if !result.is_empty {
+                    if let Err(e) = self.compositor.queue_frame(()) {
+                        eprintln!("      error al encolar el cuadro: {e}");
+                    }
+                }
+                self.frames += 1;
+            }
+            Err(e) => eprintln!("      error pintando el cuadro: {e}"),
+        }
+    }
+}
+
+/// Arranca el Cuerpo sobre DRM/KMS — fases 1 y 2a.
 pub fn run() -> Result<(), Box<dyn Error>> {
-    println!("mirada-compositor · backend DRM — fase 1 (bring-up).");
+    println!("mirada-compositor · backend DRM — fases 1 (bring-up) y 2a (render).");
     println!("──────────────────────────────────────────────────");
 
-    // 1 · La sesión. `libseat` nos da acceso a DRM y a los dispositivos
-    //     de entrada sin ser root — habla con `seatd` o con `logind`.
-    println!("[1/4] abriendo la sesión (libseat) …");
+    // 1 · Sesión.
+    println!("[1/7] abriendo la sesión (libseat) …");
     let (mut 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` corriendo?"
+             ¿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 · La GPU primaria del seat.
-    println!("[2/4] buscando la GPU primaria …");
+    // 2 · GPU primaria.
+    println!("[2/7] 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 · Abrir el dispositivo DRM a través de la sesión.
-    println!("[3/4] abriendo el dispositivo DRM …");
+    // 3 · Dispositivo DRM.
+    println!("[3/7] 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 (drm, _drm_notifier) =
-        DrmDevice::new(drm_fd, true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
+    let (mut drm, drm_notifier) =
+        DrmDevice::new(drm_fd.clone(), true).map_err(|e| format!("DrmDevice::new falló: {e}"))?;
     println!("      dispositivo DRM listo.");
 
-    // 4 · Enumerar conectores: cada uno conectado es una salida física.
-    println!("[4/4] enumerando salidas …");
+    // 4 · Elegir la salida conectada: conector + CRTC + modo.
+    println!("[4/7] eligiendo salida (conector + CRTC + modo) …");
     let resources = drm
         .resource_handles()
         .map_err(|e| format!("no pude leer los recursos DRM: {e}"))?;
-    let mut connected = 0usize;
-    for &handle in resources.connectors() {
-        let info = match drm.get_connector(handle, false) {
-            Ok(info) => info,
-            Err(e) => {
-                eprintln!("      conector {handle:?}: error al leerlo: {e}");
-                continue;
-            }
+
+    let mut chosen = None;
+    for &conn_handle in resources.connectors() {
+        let conn = match drm.get_connector(conn_handle, false) {
+            Ok(c) => c,
+            Err(_) => continue,
         };
-        let name = format!("{:?}-{}", info.interface(), info.interface_id());
-        match info.state() {
-            ConnectorState::Connected => {
-                connected += 1;
-                match info.modes().first() {
-                    Some(mode) => {
-                        let (w, h) = mode.size();
-                        println!(
-                            "      · «{name}» CONECTADA — modo preferido {w}×{h} \
-                             @ {} Hz  ({} modos)",
-                            mode.vrefresh(),
-                            info.modes().len(),
-                        );
-                    }
-                    None => println!("      · «{name}» CONECTADA — sin modos anunciados"),
-                }
+        if conn.state() != ConnectorState::Connected {
+            continue;
+        }
+        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;
             }
-            ConnectorState::Disconnected => println!("      · «{name}» desconectada"),
-            other => println!("      · «{name}» — estado {other:?}"),
+            None => println!("      «{name}» sin CRTC libre — la salto"),
         }
     }
-    println!("      {connected} salida(s) conectada(s).");
+    let (conn_handle, crtc, mode, out_name) =
+        chosen.ok_or("ninguna salida conectada con CRTC disponible")?;
+
+    // 5 · GBM + EGL + GlesRenderer.
+    println!("[5/7] 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 · La superficie DRM y el DrmCompositor de esta salida.
+    println!("[6/7] 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 (mw, mh) = mode.size();
+    let mode_source = OutputModeSource::Static {
+        size: Size::from((mw as i32, mh 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 · 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<TestState> =
+        EventLoop::try_new().map_err(|e| format!("calloop falló: {e}"))?;
+    event_loop
+        .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}");
+                }
+                state.render();
+            }
+            DrmEvent::Error(e) => eprintln!("      DRM error: {e}"),
+        })
+        .map_err(|e| format!("no pude registrar el DRM en calloop: {e}"))?;
+
+    let mut state = TestState {
+        compositor,
+        renderer,
+        frames: 0,
+        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) {
+                signal.stop();
+            }
+        })
+        .map_err(|e| format!("el bucle de eventos falló: {e}"))?;
 
     println!("──────────────────────────────────────────────────");
-    if connected == 0 {
-        eprintln!("mirada-compositor · bring-up OK, pero no hay ninguna salida");
-        eprintln!("   conectada — sin pantalla no hay nada que componer.");
-    } else {
-        println!("mirada-compositor · bring-up DRM completado correctamente.");
-    }
-    println!(
-        "   La composición sobre DRM es la fase 2. Copia estos logs y\n   \
-         seguimos. Mientras, el backend winit ya funciona dentro de un\n   \
-         escritorio:  mirada-compositor --winit"
-    );
+    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).");
     Ok(())
 }