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>
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user