//! Spike de "atachá video en llimphi": video como `wl_subsurface` real sobre //! la `wl_surface` de winit, para que mirada lo componga (Fase 1) y a futuro lo //! escanee en un plano de overlay, **sin pasar por la textura intermedia de //! vello**. //! //! Muro que valida (Fase 0, ya OK): winit no expone subsurfaces, así que se //! monta una **segunda** conexión `wayland-client` sobre el MISMO `wl_display` //! ajeno (`Backend::from_foreign_display`), se envuelve la `wl_surface` de winit //! por `ObjectId::from_ptr`, y se le crea una `wl_subsurface` hija. `set_desync` //! → el hijo commitea solo; posición/orden se latchean en el commit del padre //! (que hace winit al presentar; el subsurface trackea al padre al mover). //! //! Dos modos: //! - **sin argumentos** → cuadro de color sólido (valida el subsurface). //! - **`subsurface-spike `** → frames reales de ese video (Fase 1a), //! decodificados por `foreign-av`, subidos al subsurface por shm (3 buffers //! round-robin, RGBA→ARGB8888). La UI winit sólo limpia el fondo. //! //! Teclas: B = subsurface debajo/encima del padre · T = fondo UI //! transparente/opaco (el "hueco" donde asoma el plano) · ESC = salir. use std::os::fd::{AsFd, BorrowedFd}; use std::sync::Arc; use std::time::{Duration, Instant}; use gbm::{BufferObject, BufferObjectFlags, Device as GbmDevice, Format as GbmFormat}; use media_core::FrameSource; use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle}; use wayland_client::backend::{Backend, ObjectId}; use wayland_client::protocol::{ wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_region::WlRegion, wl_registry, wl_shm::{self, WlShm}, wl_shm_pool::WlShmPool, wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, wl_surface::WlSurface, }; use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle}; use wayland_protocols::wp::linux_dmabuf::zv1::client::{ zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, }; use winit::application::ApplicationHandler; use winit::event::{ElementState, KeyEvent, WindowEvent}; use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; use winit::keyboard::{Key, NamedKey}; use winit::window::{Window, WindowId}; // Modo cuadro sólido (sin archivo). const SOLID_W: i32 = 400; const SOLID_H: i32 = 300; const SOLID_X: i32 = 200; const SOLID_Y: i32 = 150; const DEFAULT_WIN: (u32, u32) = (800, 600); /// Cuántos buffers shm rotamos. Con 3 en round-robin nunca reescribimos uno que /// el compositor todavía tenga tomado (a 30 fps lo libera en ~1 frame ≪ 3), así /// que no hace falta rastrear `wl_buffer.release`. const RING: usize = 3; // --------------------------------------------------------------------------- // Estado wayland del lado del subsurface (segunda conexión, display ajeno). // --------------------------------------------------------------------------- #[derive(Default)] struct WlGlobals { compositor: Option, subcompositor: Option, shm: Option, dmabuf: Option, } impl Dispatch for WlGlobals { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { if let wl_registry::Event::Global { name, interface, version } = event { match &interface[..] { "wl_compositor" => { state.compositor = Some(registry.bind(name, version.min(4), qh, ())); } "wl_subcompositor" => { state.subcompositor = Some(registry.bind(name, 1, qh, ())); } "wl_shm" => { state.shm = Some(registry.bind(name, 1, qh, ())); } "zwp_linux_dmabuf_v1" => { // v3+ para `add` con modifier; v4 trae feedback (no lo usamos). state.dmabuf = Some(registry.bind(name, version.min(4), qh, ())); } _ => {} } } } } /// Impls `Dispatch` vacíos: no nos interesan los eventos de estos objetos. macro_rules! empty_dispatch { ($($t:ty),* $(,)?) => { $( impl Dispatch<$t, ()> for WlGlobals { fn event( _: &mut Self, _: &$t, _: <$t as Proxy>::Event, _: &(), _: &Connection, _: &QueueHandle, ) {} } )* }; } empty_dispatch!( WlCompositor, WlSubcompositor, WlShm, WlShmPool, WlBuffer, WlSurface, WlSubsurface, WlRegion, ZwpLinuxDmabufV1, ZwpLinuxBufferParamsV1, ); // --------------------------------------------------------------------------- // Alimentador de frames de video al subsurface por shm (round-robin). // --------------------------------------------------------------------------- /// Backend de almacenamiento de los frames: shm (CPU, jamás escanea a plano) o /// dmabuf (buffers `gbm` LINEAR scanout-capable → mirada los puede promover a un /// plano de overlay del DRM). En ambos el `Feeder` escribe el frame por CPU; la /// diferencia es qué puede hacer el compositor con el resultado. enum Ring { /// Mapeo único del pool shm (RING × frame contiguos). Puntero crudo: la app /// es mono-hilo, no cruza threads. Shm { map: *mut u8, frame_bytes: usize }, /// Un `BufferObject` gbm por slot del ring. El `Device` se retiene (los bo /// comparten su `gbm_device` por refcount ffi, pero el Rust `Device` posee el /// destructor). Se escribe por `map_mut` (sync ioctls incluidos). Dmabuf { _device: GbmDevice, bos: Vec>, }, } struct Feeder { source: Box, child: WlSurface, conn: Connection, ring: Ring, w: i32, h: i32, buffers: Vec, next: usize, rgba: Vec, last: Instant, interval: Duration, /// Contador de throughput real (frames commiteados por ventana de ~1s). frames_win: u32, fps_t0: Instant, } impl Feeder { /// Tira del próximo frame (si toca por cadencia) y lo commitea al hijo. fn pump(&mut self, now: Instant) { let dt = now.saturating_duration_since(self.last); self.last = now; let Some((w, h)) = self.source.tick(dt, &mut self.rgba) else { return; }; if w as i32 != self.w || h as i32 != self.h { // El spike fija tamaño al abrir; un cambio de resolución en vuelo no // se maneja acá. return; } let idx = self.next; self.next = (self.next + 1) % self.buffers.len(); // RGBA (del FrameSource) → XRGB8888 (bytes B,G,R,X en memoria LE). let px = (w * h) as usize; match &mut self.ring { Ring::Shm { map, frame_bytes } => { let base = unsafe { map.add(idx * *frame_bytes) as *mut u32 }; for i in 0..px { let r = self.rgba[i * 4] as u32; let g = self.rgba[i * 4 + 1] as u32; let b = self.rgba[i * 4 + 2] as u32; unsafe { base.add(i).write((r << 16) | (g << 8) | b) }; } } Ring::Dmabuf { bos, .. } => { // `map_mut` respeta el stride del mapeo (puede diferir de w*4). let rgba = &self.rgba; let _ = bos[idx].map_mut(0, 0, w, h, |m| { let stride = m.stride() as usize; let buf = m.buffer_mut(); for y in 0..h as usize { let dst_row = y * stride; let src_row = y * w as usize * 4; for x in 0..w as usize { let s = src_row + x * 4; let r = rgba[s] as u32; let g = rgba[s + 1] as u32; let b = rgba[s + 2] as u32; let word = (r << 16) | (g << 8) | b; let d = dst_row + x * 4; buf[d..d + 4].copy_from_slice(&word.to_ne_bytes()); } } }); } } self.child.attach(Some(&self.buffers[idx]), 0, 0); self.child.damage_buffer(0, 0, self.w, self.h); self.child.commit(); let _ = self.conn.flush(); // Throughput real (gateado por VIDEO_PLANE_FPS): frames commiteados/s. self.frames_win += 1; if self.fps_t0.elapsed() >= Duration::from_secs(1) { if std::env::var_os("VIDEO_PLANE_FPS").is_some() { eprintln!("[spike] pump: {} fps reales", self.frames_win); } self.frames_win = 0; self.fps_t0 = Instant::now(); } } } // --------------------------------------------------------------------------- // El plano: subsurface + su contenido (cuadro sólido o alimentador de video). // --------------------------------------------------------------------------- enum Content { Solid(#[allow(dead_code)] WlBuffer), Video(Feeder), } struct VideoPlane { conn: Connection, queue: EventQueue, globals: WlGlobals, parent: WlSurface, _child: WlSurface, subsurface: WlSubsurface, content: Content, below: bool, } impl VideoPlane { /// Monta la segunda conexión sobre el display de winit y crea el subsurface. /// `video` = fuente ya abierta (y sus dims/fps) para el modo video; `None` /// = cuadro sólido. unsafe fn atachar( window: &Window, video: Option<(Box, u32, u32, f32)>, ) -> Result { let RawDisplayHandle::Wayland(dh) = window .display_handle() .map_err(|e| format!("display_handle: {e}"))? .as_raw() else { return Err("no estamos bajo Wayland (display handle no-Wayland)".into()); }; let RawWindowHandle::Wayland(wh) = window .window_handle() .map_err(|e| format!("window_handle: {e}"))? .as_raw() else { return Err("no estamos bajo Wayland (window handle no-Wayland)".into()); }; let display_ptr = dh.display.as_ptr(); let surface_ptr = wh.surface.as_ptr(); let backend = unsafe { Backend::from_foreign_display(display_ptr.cast()) }; let conn = Connection::from_backend(backend); let mut queue: EventQueue = conn.new_event_queue(); let qh = queue.handle(); let display = conn.display(); let _registry = display.get_registry(&qh, ()); let mut globals = WlGlobals::default(); queue .roundtrip(&mut globals) .map_err(|e| format!("roundtrip registry: {e}"))?; let compositor = globals.compositor.clone().ok_or("falta wl_compositor")?; let subcompositor = globals.subcompositor.clone().ok_or("falta wl_subcompositor")?; let shm = globals.shm.clone().ok_or("falta wl_shm")?; let parent_id = unsafe { ObjectId::from_ptr(WlSurface::interface(), surface_ptr.cast()) .map_err(|e| format!("ObjectId::from_ptr(parent): {e}"))? }; let parent = WlSurface::from_id(&conn, parent_id) .map_err(|e| format!("WlSurface::from_id(parent): {e}"))?; let child = compositor.create_surface(&qh, ()); let subsurface = subcompositor.get_subsurface(&child, &parent, &qh, ()); subsurface.set_desync(); let (content, sw, sh, sx, sy) = match video { Some((source, w, h, fps)) => { // Backend: dmabuf (default, F1b) salvo VIDEO_PLANE_SHM=1 o que el // compositor no exponga linux-dmabuf. let forzar_shm = std::env::var_os("VIDEO_PLANE_SHM").is_some(); let (ring, buffers) = match (forzar_shm, globals.dmabuf.clone()) { (false, Some(dmabuf)) => match crear_ring_dmabuf(&dmabuf, &qh, w as i32, h as i32) { Ok(rb) => { eprintln!("[spike] backend = DMABUF (gbm LINEAR, scanout-capable)"); rb } Err(e) => { eprintln!("[spike] dmabuf falló ({e}) — caigo a shm"); let (map, frame_bytes, buffers) = crear_ring_shm(&shm, &qh, w as i32, h as i32)?; (Ring::Shm { map, frame_bytes }, buffers) } }, _ => { eprintln!("[spike] backend = SHM (jamás promueve a plano)"); let (map, frame_bytes, buffers) = crear_ring_shm(&shm, &qh, w as i32, h as i32)?; (Ring::Shm { map, frame_bytes }, buffers) } }; let fps = if fps.is_finite() && fps > 1.0 { fps } else { 30.0 }; let feeder = Feeder { source, child: child.clone(), conn: conn.clone(), ring, w: w as i32, h: h as i32, buffers, next: 0, rgba: Vec::new(), last: Instant::now(), interval: Duration::from_secs_f32(1.0 / fps), frames_win: 0, fps_t0: Instant::now(), }; eprintln!("[spike] video {w}x{h} @ {fps:.1}fps atachado al subsurface"); (Content::Video(feeder), w as i32, h as i32, 0, 0) } None => { let buffer = crear_buffer_solido(&shm, &qh, SOLID_W, SOLID_H)?; child.attach(Some(&buffer), 0, 0); child.damage_buffer(0, 0, SOLID_W, SOLID_H); child.commit(); (Content::Solid(buffer), SOLID_W, SOLID_H, SOLID_X, SOLID_Y) } }; subsurface.set_position(sx, sy); // Región opaca: condición para que mirada lo mande a un plano de overlay // (Fase 1b). No cambia la visibilidad ahora. let region = compositor.create_region(&qh, ()); region.add(0, 0, sw, sh); child.set_opaque_region(Some(®ion)); region.destroy(); child.commit(); conn.flush().map_err(|e| format!("flush: {e}"))?; Ok(Self { conn, queue, globals, parent, _child: child, subsurface, content, below: false, }) } fn frame_interval(&self) -> Option { match &self.content { Content::Video(f) => Some(f.interval), Content::Solid(_) => None, } } fn set_below(&mut self, below: bool) { self.below = below; if below { self.subsurface.place_below(&self.parent); } else { self.subsurface.place_above(&self.parent); } let _ = self.conn.flush(); } /// Empuja frames de video (si aplica) y drena eventos de la queue. fn pump(&mut self, now: Instant) { if let Content::Video(f) = &mut self.content { f.pump(now); } let _ = self.queue.dispatch_pending(&mut self.globals); let _ = self.conn.flush(); } } // --------------------------------------------------------------------------- // Helpers shm // --------------------------------------------------------------------------- /// Crea un memfd de `size` bytes y lo mapea. Devuelve (fd, puntero). fn memfd_map(size: usize) -> Result<(i32, *mut u8), String> { let fd = unsafe { libc::memfd_create(c"llimphi-video-plane".as_ptr(), 0) }; if fd < 0 { return Err("memfd_create falló".into()); } if unsafe { libc::ftruncate(fd, size as libc::off_t) } != 0 { unsafe { libc::close(fd) }; return Err("ftruncate falló".into()); } let map = unsafe { libc::mmap( std::ptr::null_mut(), size, libc::PROT_READ | libc::PROT_WRITE, libc::MAP_SHARED, fd, 0, ) }; if map == libc::MAP_FAILED { unsafe { libc::close(fd) }; return Err("mmap falló".into()); } Ok((fd, map as *mut u8)) } /// Pool shm con `RING` buffers XRGB8888 de `w×h` contiguos. Devuelve el mapeo, /// el tamaño de un frame, y los `wl_buffer`. fn crear_ring_shm( shm: &WlShm, qh: &QueueHandle, w: i32, h: i32, ) -> Result<(*mut u8, usize, Vec), String> { let stride = w * 4; let frame_bytes = (stride * h) as usize; let total = frame_bytes * RING; let (fd, map) = memfd_map(total)?; let borrowed = unsafe { BorrowedFd::borrow_raw(fd) }; let pool = shm.create_pool(borrowed, total as i32, qh, ()); let buffers = (0..RING) .map(|i| { pool.create_buffer( (i * frame_bytes) as i32, w, h, stride, wl_shm::Format::Xrgb8888, qh, (), ) }) .collect(); unsafe { libc::close(fd) }; pool.destroy(); Ok((map, frame_bytes, buffers)) } /// Ring de `RING` buffers **dmabuf** (gbm LINEAR, XRGB8888) de `w×h`, envueltos /// como `wl_buffer` por `zwp_linux_dmabuf`. A diferencia del shm, son /// scanout-capable: mirada los puede promover a un plano de overlay del DRM. /// Devuelve el `Ring::Dmabuf` (retiene device+bos vivos) y los `wl_buffer`. fn crear_ring_dmabuf( dmabuf: &ZwpLinuxDmabufV1, qh: &QueueHandle, w: i32, h: i32, ) -> Result<(Ring, Vec), String> { // Nodo de render de la GPU: el mismo Intel que usa mirada, así el buffer // LINEAR es importable para composición y escaneable en un plano. let file = std::fs::OpenOptions::new() .read(true) .write(true) .open("/dev/dri/renderD128") .map_err(|e| format!("abrir renderD128: {e}"))?; let device = GbmDevice::new(file).map_err(|e| format!("gbm device: {e}"))?; // SIN `SCANOUT`: un render node (renderD128) no puede asignar buffers de // scanout (EACCES — eso es del card node con DRM master, que tiene mirada). // No hace falta: mirada importa el dmabuf y forja SU framebuffer en card1; lo // que lo hace escaneable es el modifier **LINEAR**, no el flag del cliente. // `WRITE` da EINVAL en Intel (es para gbm_bo_write, no para map); para mapear // por CPU basta `LINEAR`. Probamos combos en orden hasta que uno asigne. let combos: [(&str, BufferObjectFlags); 3] = [ ("LINEAR|RENDERING", BufferObjectFlags::LINEAR | BufferObjectFlags::RENDERING), ("LINEAR", BufferObjectFlags::LINEAR), ("RENDERING", BufferObjectFlags::RENDERING), ]; let fourcc = GbmFormat::Xrgb8888 as u32; let mut bos: Vec> = Vec::with_capacity(RING); let mut buffers: Vec = Vec::with_capacity(RING); for i in 0..RING { let bo = { let mut hecho: Option> = None; let mut ultimo = String::new(); for (nombre, flags) in combos { match device.create_buffer_object::<()>(w as u32, h as u32, GbmFormat::Xrgb8888, flags) { Ok(b) => { if i == 0 { eprintln!("[spike] gbm flags = {nombre}"); } hecho = Some(b); break; } Err(e) => ultimo = format!("{nombre}: {e}"), } } hecho.ok_or_else(|| format!("gbm bo[{i}] (ningún combo): {ultimo}"))? }; let fd = bo.fd().map_err(|_| format!("gbm bo[{i}].fd inválido"))?; let modifier: u64 = bo.modifier().into(); let params = dmabuf.create_params(qh, ()); params.add( fd.as_fd(), 0, bo.offset(0), bo.stride(), (modifier >> 32) as u32, (modifier & 0xffff_ffff) as u32, ); // create_immed (v2+): sin roundtrip. El compositor dupea el fd; si el // formato/modifier no le sirve manda un protocol error (lo veríamos). let buffer = params.create_immed( w, h, fourcc, zwp_linux_buffer_params_v1::Flags::empty(), qh, (), ); params.destroy(); drop(fd); // ya dupeado en create_immed; el bo mantiene el buffer real. if i == 0 { eprintln!( "[spike] dmabuf bo {w}x{h} stride={} offset={} modifier={modifier:#x} fourcc={fourcc:#x}", bo.stride(), bo.offset(0), ); } bos.push(bo); buffers.push(buffer); } Ok((Ring::Dmabuf { _device: device, bos }, buffers)) } /// Buffer shm de color sólido (modo sin archivo). fn crear_buffer_solido( shm: &WlShm, qh: &QueueHandle, w: i32, h: i32, ) -> Result { let stride = w * 4; let size = (stride * h) as usize; let (fd, map) = memfd_map(size)?; let px = map as *mut u32; for i in 0..(w * h) as isize { unsafe { px.offset(i).write(0x00FF_7800) }; // naranja } let borrowed = unsafe { BorrowedFd::borrow_raw(fd) }; let pool = shm.create_pool(borrowed, size as i32, qh, ()); let buffer = pool.create_buffer(0, w, h, stride, wl_shm::Format::Xrgb8888, qh, ()); unsafe { libc::close(fd) }; let _ = map; pool.destroy(); Ok(buffer) } // --------------------------------------------------------------------------- // UI winit + wgpu (limpia el fondo; el "hueco" = fondo transparente) // --------------------------------------------------------------------------- struct Gpu { surface: wgpu::Surface<'static>, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration, } impl Gpu { fn new(window: Arc) -> Result { let instance = { let mut desc = wgpu::InstanceDescriptor::new_without_display_handle(); desc.backends = wgpu::Backends::PRIMARY; wgpu::Instance::new(desc) }; let surface = instance .create_surface(window.clone()) .map_err(|e| format!("create_surface: {e}"))?; let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::LowPower, force_fallback_adapter: false, compatible_surface: Some(&surface), })) .map_err(|e| format!("request_adapter: {e}"))?; let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { label: Some("video-plane-spike"), required_features: wgpu::Features::empty(), required_limits: wgpu::Limits::default(), memory_hints: wgpu::MemoryHints::default(), experimental_features: wgpu::ExperimentalFeatures::default(), trace: wgpu::Trace::Off, })) .map_err(|e| format!("request_device: {e}"))?; let caps = surface.get_capabilities(&adapter); let format = caps .formats .iter() .copied() .find(|f| matches!(f, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm)) .unwrap_or(caps.formats[0]); let alpha_mode = caps .alpha_modes .iter() .copied() .find(|a| matches!(a, wgpu::CompositeAlphaMode::PreMultiplied)) .unwrap_or(caps.alpha_modes[0]); eprintln!("[spike] wgpu formato={format:?} alpha={alpha_mode:?}"); let size = window.inner_size(); let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format, width: size.width.max(1), height: size.height.max(1), present_mode: wgpu::PresentMode::Fifo, desired_maximum_frame_latency: 2, alpha_mode, view_formats: vec![], }; surface.configure(&device, &config); Ok(Self { surface, device, queue, config }) } fn resize(&mut self, w: u32, h: u32) { self.config.width = w.max(1); self.config.height = h.max(1); self.surface.configure(&self.device, &self.config); } fn render(&self, transparent: bool) { // wgpu 29 devuelve un enum de estado, no un Result. `Suboptimal` trae // textura válida (en wgpu 27 llegaba como `Ok` con un flag al lado). let frame = match self.surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(f) | wgpu::CurrentSurfaceTexture::Suboptimal(f) => f, _ => return, }; let view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); let mut enc = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); let clear = if transparent { wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 } } else { wgpu::Color { r: 0.05, g: 0.07, b: 0.12, a: 1.0 } }; { let _pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor { label: None, color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, depth_slice: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(clear), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); } self.queue.submit(Some(enc.finish())); frame.present(); } } // --------------------------------------------------------------------------- // App // --------------------------------------------------------------------------- struct SpikeApp { /// Fuente de video ya abierta (o None para el cuadro sólido). Se consume al /// crear el plano. pending_video: Option<(Box, u32, u32, f32)>, win_size: (u32, u32), window: Option>, gpu: Option, plane: Option, transparent: bool, interval: Option, } impl ApplicationHandler for SpikeApp { fn resumed(&mut self, el: &ActiveEventLoop) { if self.window.is_some() { return; } let attrs = Window::default_attributes() .with_title("llimphi-video-plane · subsurface spike") .with_transparent(true) .with_inner_size(winit::dpi::LogicalSize::new(self.win_size.0, self.win_size.1)); let window = Arc::new(el.create_window(attrs).expect("create_window")); let gpu = match Gpu::new(window.clone()) { Ok(g) => g, Err(e) => { eprintln!("[spike] GPU falló: {e}"); el.exit(); return; } }; // Primer frame: crea/commitea el padre antes de atachar el subsurface. gpu.render(self.transparent); let video = self.pending_video.take(); match unsafe { VideoPlane::atachar(&window, video) } { Ok(p) => { self.interval = p.frame_interval(); eprintln!("[spike] subsurface OK. B=abajo/arriba, T=transparente, ESC=salir."); self.plane = Some(p); } Err(e) => eprintln!("[spike] atachar subsurface FALLÓ: {e}"), } self.gpu = Some(gpu); self.window = Some(window); // NO pedimos redraw: el padre es un host transparente estático. Redibujarlo // por frame bloquea el loop en `present` (Fifo) cuando mirada no manda // frame-callbacks (ventana no visible). El video lo bombea `about_to_wait` // (Poll) sobre el subsurface, que commitea independiente (desync). } fn window_event(&mut self, el: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { match event { WindowEvent::CloseRequested => el.exit(), WindowEvent::Resized(size) => { if let Some(g) = &mut self.gpu { g.resize(size.width, size.height); } if let Some(w) = &self.window { w.request_redraw(); } } WindowEvent::RedrawRequested => { // Sólo si el compositor lo pide (frame-callback real): repintamos el // padre. NO re-pedimos redraw (evita el bloqueo en `present`). if let Some(g) = &self.gpu { g.render(self.transparent); } } WindowEvent::KeyboardInput { event: KeyEvent { logical_key, state: ElementState::Pressed, .. }, .. } => match logical_key { Key::Named(NamedKey::Escape) => el.exit(), Key::Character(c) if c.eq_ignore_ascii_case("b") => { if let Some(p) = &mut self.plane { let below = !p.below; p.set_below(below); eprintln!("[spike] subsurface {}", if below { "DEBAJO" } else { "ENCIMA" }); } if let Some(w) = &self.window { w.request_redraw(); } } Key::Character(c) if c.eq_ignore_ascii_case("t") => { self.transparent = !self.transparent; eprintln!("[spike] fondo {}", if self.transparent { "TRANSPARENTE" } else { "opaco" }); if let Some(w) = &self.window { w.request_redraw(); } } _ => {} }, _ => {} } } /// Despertado por el hilo timer (via `EventLoopProxy::send_event`) a cadencia /// de video: bombea un frame al subsurface. Este es el ÚNICO driver del pump — /// ni el `WaitUntil` ni los frame-callbacks del padre despiertan el loop bajo /// mirada, pero `send_event` sí. Mono-core, sin busy-loop → el decoder respira. fn user_event(&mut self, _el: &ActiveEventLoop, _event: ()) { if let Some(p) = &mut self.plane { p.pump(Instant::now()); } } } /// Fuente **sintética**: patrón animado (tablero diagonal en movimiento + /// gradiente) a `w×h`. Decoupla la medición del pipeline dmabuf de foreign-av, /// que se estanca al manejarlo sin el sistema de clock/sync de media-app. Emite /// un frame por tick (el hilo timer del loop pacea la cadencia real). struct SyntheticSource { w: u32, h: u32, t: f32, } impl FrameSource for SyntheticSource { fn tick(&mut self, dt: Duration, buf: &mut Vec) -> Option<(u32, u32)> { self.t += dt.as_secs_f32(); let (w, h) = (self.w as usize, self.h as usize); buf.resize(w * h * 4, 0); let phase = (self.t * 120.0) as i32; // scroll ~120 px/s for y in 0..h { let gy = ((y * 255) / h) as u8; for x in 0..w { let band = (((x as i32 + y as i32 + phase) / 32) & 1) as u8; let i = (y * w + x) * 4; buf[i] = if band == 0 { 40 } else { 220 }; buf[i + 1] = ((x * 255) / w) as u8; buf[i + 2] = gy; buf[i + 3] = 255; } } Some((self.w, self.h)) } } /// Abre `path` por foreign-av y arma un `FrameSource` + dims + fps. fn abrir_video(path: &str) -> Result<(Box, u32, u32, f32), String> { let info = foreign_av::probe(path).map_err(|e| format!("probe: {e:?}"))?; let session = foreign_av::MediaSession::open(info).map_err(|e| format!("open: {e:?}"))?; let src = foreign_av::FfmpegVideoSource::from_session(session) .map_err(|e| format!("video source: {e:?}"))?; let (w, h) = src.dimensions(); let fps = src.fps(); Ok((Box::new(src), w, h, fps)) } fn main() { let arg = std::env::args().nth(1); let (pending_video, win_size) = match arg.as_deref() { // Modo medición: fuente sintética a 30 fps, sin foreign-av. Some("test") => { let (w, h, fps) = (1024u32, 576u32, 30.0f32); let src: Box = Box::new(SyntheticSource { w, h, t: 0.0 }); eprintln!("[spike] modo TEST: fuente sintética {w}x{h} @ {fps}fps"); (Some((src, w, h, fps)), (w, h)) } Some(path) => match abrir_video(path) { Ok(v) => { let (_, w, h, _) = &v; let win = ((*w).min(1600).max(320), (*h).min(900).max(240)); (Some(v), win) } Err(e) => { eprintln!("[spike] no pude abrir {path}: {e} — caigo a cuadro sólido"); (None, DEFAULT_WIN) } }, None => { eprintln!("[spike] sin archivo → cuadro sólido. Uso: subsurface-spike