tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-video-plane/src/main.rs Rust · 918 siq'ikuna · 34.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Spike de "atachá video en llimphi": video como `wl_subsurface` real sobre
2//! la `wl_surface` de winit, para que mirada lo componga (Fase 1) y a futuro lo
3//! escanee en un plano de overlay, **sin pasar por la textura intermedia de
4//! vello**.
5//!
6//! Muro que valida (Fase 0, ya OK): winit no expone subsurfaces, así que se
7//! monta una **segunda** conexión `wayland-client` sobre el MISMO `wl_display`
8//! ajeno (`Backend::from_foreign_display`), se envuelve la `wl_surface` de winit
9//! por `ObjectId::from_ptr`, y se le crea una `wl_subsurface` hija. `set_desync`
10//! → el hijo commitea solo; posición/orden se latchean en el commit del padre
11//! (que hace winit al presentar; el subsurface trackea al padre al mover).
12//!
13//! Dos modos:
14//!   - **sin argumentos** → cuadro de color sólido (valida el subsurface).
15//!   - **`subsurface-spike <archivo>`** → frames reales de ese video (Fase 1a),
16//!     decodificados por `foreign-av`, subidos al subsurface por shm (3 buffers
17//!     round-robin, RGBA→ARGB8888). La UI winit sólo limpia el fondo.
18//!
19//! Teclas: B = subsurface debajo/encima del padre · T = fondo UI
20//! transparente/opaco (el "hueco" donde asoma el plano) · ESC = salir.
21
22use std::os::fd::{AsFd, BorrowedFd};
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use gbm::{BufferObject, BufferObjectFlags, Device as GbmDevice, Format as GbmFormat};
27use media_core::FrameSource;
28use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle};
29use wayland_client::backend::{Backend, ObjectId};
30use wayland_client::protocol::{
31    wl_buffer::WlBuffer,
32    wl_compositor::WlCompositor,
33    wl_region::WlRegion,
34    wl_registry,
35    wl_shm::{self, WlShm},
36    wl_shm_pool::WlShmPool,
37    wl_subcompositor::WlSubcompositor,
38    wl_subsurface::WlSubsurface,
39    wl_surface::WlSurface,
40};
41use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle};
42use wayland_protocols::wp::linux_dmabuf::zv1::client::{
43    zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1},
44    zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1,
45};
46use winit::application::ApplicationHandler;
47use winit::event::{ElementState, KeyEvent, WindowEvent};
48use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
49use winit::keyboard::{Key, NamedKey};
50use winit::window::{Window, WindowId};
51
52// Modo cuadro sólido (sin archivo).
53const SOLID_W: i32 = 400;
54const SOLID_H: i32 = 300;
55const SOLID_X: i32 = 200;
56const SOLID_Y: i32 = 150;
57const DEFAULT_WIN: (u32, u32) = (800, 600);
58
59/// Cuántos buffers shm rotamos. Con 3 en round-robin nunca reescribimos uno que
60/// el compositor todavía tenga tomado (a 30 fps lo libera en ~1 frame ≪ 3), así
61/// que no hace falta rastrear `wl_buffer.release`.
62const RING: usize = 3;
63
64// ---------------------------------------------------------------------------
65// Estado wayland del lado del subsurface (segunda conexión, display ajeno).
66// ---------------------------------------------------------------------------
67
68#[derive(Default)]
69struct WlGlobals {
70    compositor: Option<WlCompositor>,
71    subcompositor: Option<WlSubcompositor>,
72    shm: Option<WlShm>,
73    dmabuf: Option<ZwpLinuxDmabufV1>,
74}
75
76impl Dispatch<wl_registry::WlRegistry, ()> for WlGlobals {
77    fn event(
78        state: &mut Self,
79        registry: &wl_registry::WlRegistry,
80        event: wl_registry::Event,
81        _: &(),
82        _: &Connection,
83        qh: &QueueHandle<Self>,
84    ) {
85        if let wl_registry::Event::Global { name, interface, version } = event {
86            match &interface[..] {
87                "wl_compositor" => {
88                    state.compositor = Some(registry.bind(name, version.min(4), qh, ()));
89                }
90                "wl_subcompositor" => {
91                    state.subcompositor = Some(registry.bind(name, 1, qh, ()));
92                }
93                "wl_shm" => {
94                    state.shm = Some(registry.bind(name, 1, qh, ()));
95                }
96                "zwp_linux_dmabuf_v1" => {
97                    // v3+ para `add` con modifier; v4 trae feedback (no lo usamos).
98                    state.dmabuf = Some(registry.bind(name, version.min(4), qh, ()));
99                }
100                _ => {}
101            }
102        }
103    }
104}
105
106/// Impls `Dispatch` vacíos: no nos interesan los eventos de estos objetos.
107macro_rules! empty_dispatch {
108    ($($t:ty),* $(,)?) => { $(
109        impl Dispatch<$t, ()> for WlGlobals {
110            fn event(
111                _: &mut Self,
112                _: &$t,
113                _: <$t as Proxy>::Event,
114                _: &(),
115                _: &Connection,
116                _: &QueueHandle<Self>,
117            ) {}
118        }
119    )* };
120}
121empty_dispatch!(
122    WlCompositor,
123    WlSubcompositor,
124    WlShm,
125    WlShmPool,
126    WlBuffer,
127    WlSurface,
128    WlSubsurface,
129    WlRegion,
130    ZwpLinuxDmabufV1,
131    ZwpLinuxBufferParamsV1,
132);
133
134// ---------------------------------------------------------------------------
135// Alimentador de frames de video al subsurface por shm (round-robin).
136// ---------------------------------------------------------------------------
137
138/// Backend de almacenamiento de los frames: shm (CPU, jamás escanea a plano) o
139/// dmabuf (buffers `gbm` LINEAR scanout-capable → mirada los puede promover a un
140/// plano de overlay del DRM). En ambos el `Feeder` escribe el frame por CPU; la
141/// diferencia es qué puede hacer el compositor con el resultado.
142enum Ring {
143    /// Mapeo único del pool shm (RING × frame contiguos). Puntero crudo: la app
144    /// es mono-hilo, no cruza threads.
145    Shm { map: *mut u8, frame_bytes: usize },
146    /// Un `BufferObject` gbm por slot del ring. El `Device` se retiene (los bo
147    /// comparten su `gbm_device` por refcount ffi, pero el Rust `Device` posee el
148    /// destructor). Se escribe por `map_mut` (sync ioctls incluidos).
149    Dmabuf {
150        _device: GbmDevice<std::fs::File>,
151        bos: Vec<BufferObject<()>>,
152    },
153}
154
155struct Feeder {
156    source: Box<dyn FrameSource + Send>,
157    child: WlSurface,
158    conn: Connection,
159    ring: Ring,
160    w: i32,
161    h: i32,
162    buffers: Vec<WlBuffer>,
163    next: usize,
164    rgba: Vec<u8>,
165    last: Instant,
166    interval: Duration,
167    /// Contador de throughput real (frames commiteados por ventana de ~1s).
168    frames_win: u32,
169    fps_t0: Instant,
170}
171
172impl Feeder {
173    /// Tira del próximo frame (si toca por cadencia) y lo commitea al hijo.
174    fn pump(&mut self, now: Instant) {
175        let dt = now.saturating_duration_since(self.last);
176        self.last = now;
177        let Some((w, h)) = self.source.tick(dt, &mut self.rgba) else {
178            return;
179        };
180        if w as i32 != self.w || h as i32 != self.h {
181            // El spike fija tamaño al abrir; un cambio de resolución en vuelo no
182            // se maneja acá.
183            return;
184        }
185        let idx = self.next;
186        self.next = (self.next + 1) % self.buffers.len();
187
188        // RGBA (del FrameSource) → XRGB8888 (bytes B,G,R,X en memoria LE).
189        let px = (w * h) as usize;
190        match &mut self.ring {
191            Ring::Shm { map, frame_bytes } => {
192                let base = unsafe { map.add(idx * *frame_bytes) as *mut u32 };
193                for i in 0..px {
194                    let r = self.rgba[i * 4] as u32;
195                    let g = self.rgba[i * 4 + 1] as u32;
196                    let b = self.rgba[i * 4 + 2] as u32;
197                    unsafe { base.add(i).write((r << 16) | (g << 8) | b) };
198                }
199            }
200            Ring::Dmabuf { bos, .. } => {
201                // `map_mut` respeta el stride del mapeo (puede diferir de w*4).
202                let rgba = &self.rgba;
203                let _ = bos[idx].map_mut(0, 0, w, h, |m| {
204                    let stride = m.stride() as usize;
205                    let buf = m.buffer_mut();
206                    for y in 0..h as usize {
207                        let dst_row = y * stride;
208                        let src_row = y * w as usize * 4;
209                        for x in 0..w as usize {
210                            let s = src_row + x * 4;
211                            let r = rgba[s] as u32;
212                            let g = rgba[s + 1] as u32;
213                            let b = rgba[s + 2] as u32;
214                            let word = (r << 16) | (g << 8) | b;
215                            let d = dst_row + x * 4;
216                            buf[d..d + 4].copy_from_slice(&word.to_ne_bytes());
217                        }
218                    }
219                });
220            }
221        }
222
223        self.child.attach(Some(&self.buffers[idx]), 0, 0);
224        self.child.damage_buffer(0, 0, self.w, self.h);
225        self.child.commit();
226        let _ = self.conn.flush();
227
228        // Throughput real (gateado por VIDEO_PLANE_FPS): frames commiteados/s.
229        self.frames_win += 1;
230        if self.fps_t0.elapsed() >= Duration::from_secs(1) {
231            if std::env::var_os("VIDEO_PLANE_FPS").is_some() {
232                eprintln!("[spike] pump: {} fps reales", self.frames_win);
233            }
234            self.frames_win = 0;
235            self.fps_t0 = Instant::now();
236        }
237    }
238}
239
240// ---------------------------------------------------------------------------
241// El plano: subsurface + su contenido (cuadro sólido o alimentador de video).
242// ---------------------------------------------------------------------------
243
244enum Content {
245    Solid(#[allow(dead_code)] WlBuffer),
246    Video(Feeder),
247}
248
249struct VideoPlane {
250    conn: Connection,
251    queue: EventQueue<WlGlobals>,
252    globals: WlGlobals,
253    parent: WlSurface,
254    _child: WlSurface,
255    subsurface: WlSubsurface,
256    content: Content,
257    below: bool,
258}
259
260impl VideoPlane {
261    /// Monta la segunda conexión sobre el display de winit y crea el subsurface.
262    /// `video` = fuente ya abierta (y sus dims/fps) para el modo video; `None`
263    /// = cuadro sólido.
264    unsafe fn atachar(
265        window: &Window,
266        video: Option<(Box<dyn FrameSource + Send>, u32, u32, f32)>,
267    ) -> Result<Self, String> {
268        let RawDisplayHandle::Wayland(dh) = window
269            .display_handle()
270            .map_err(|e| format!("display_handle: {e}"))?
271            .as_raw()
272        else {
273            return Err("no estamos bajo Wayland (display handle no-Wayland)".into());
274        };
275        let RawWindowHandle::Wayland(wh) = window
276            .window_handle()
277            .map_err(|e| format!("window_handle: {e}"))?
278            .as_raw()
279        else {
280            return Err("no estamos bajo Wayland (window handle no-Wayland)".into());
281        };
282        let display_ptr = dh.display.as_ptr();
283        let surface_ptr = wh.surface.as_ptr();
284
285        let backend = unsafe { Backend::from_foreign_display(display_ptr.cast()) };
286        let conn = Connection::from_backend(backend);
287        let mut queue: EventQueue<WlGlobals> = conn.new_event_queue();
288        let qh = queue.handle();
289
290        let display = conn.display();
291        let _registry = display.get_registry(&qh, ());
292        let mut globals = WlGlobals::default();
293        queue
294            .roundtrip(&mut globals)
295            .map_err(|e| format!("roundtrip registry: {e}"))?;
296        let compositor = globals.compositor.clone().ok_or("falta wl_compositor")?;
297        let subcompositor = globals.subcompositor.clone().ok_or("falta wl_subcompositor")?;
298        let shm = globals.shm.clone().ok_or("falta wl_shm")?;
299
300        let parent_id = unsafe {
301            ObjectId::from_ptr(WlSurface::interface(), surface_ptr.cast())
302                .map_err(|e| format!("ObjectId::from_ptr(parent): {e}"))?
303        };
304        let parent = WlSurface::from_id(&conn, parent_id)
305            .map_err(|e| format!("WlSurface::from_id(parent): {e}"))?;
306
307        let child = compositor.create_surface(&qh, ());
308        let subsurface = subcompositor.get_subsurface(&child, &parent, &qh, ());
309        subsurface.set_desync();
310
311        let (content, sw, sh, sx, sy) = match video {
312            Some((source, w, h, fps)) => {
313                // Backend: dmabuf (default, F1b) salvo VIDEO_PLANE_SHM=1 o que el
314                // compositor no exponga linux-dmabuf.
315                let forzar_shm = std::env::var_os("VIDEO_PLANE_SHM").is_some();
316                let (ring, buffers) = match (forzar_shm, globals.dmabuf.clone()) {
317                    (false, Some(dmabuf)) => match crear_ring_dmabuf(&dmabuf, &qh, w as i32, h as i32) {
318                        Ok(rb) => {
319                            eprintln!("[spike] backend = DMABUF (gbm LINEAR, scanout-capable)");
320                            rb
321                        }
322                        Err(e) => {
323                            eprintln!("[spike] dmabuf falló ({e}) — caigo a shm");
324                            let (map, frame_bytes, buffers) =
325                                crear_ring_shm(&shm, &qh, w as i32, h as i32)?;
326                            (Ring::Shm { map, frame_bytes }, buffers)
327                        }
328                    },
329                    _ => {
330                        eprintln!("[spike] backend = SHM (jamás promueve a plano)");
331                        let (map, frame_bytes, buffers) =
332                            crear_ring_shm(&shm, &qh, w as i32, h as i32)?;
333                        (Ring::Shm { map, frame_bytes }, buffers)
334                    }
335                };
336                let fps = if fps.is_finite() && fps > 1.0 { fps } else { 30.0 };
337                let feeder = Feeder {
338                    source,
339                    child: child.clone(),
340                    conn: conn.clone(),
341                    ring,
342                    w: w as i32,
343                    h: h as i32,
344                    buffers,
345                    next: 0,
346                    rgba: Vec::new(),
347                    last: Instant::now(),
348                    interval: Duration::from_secs_f32(1.0 / fps),
349                    frames_win: 0,
350                    fps_t0: Instant::now(),
351                };
352                eprintln!("[spike] video {w}x{h} @ {fps:.1}fps atachado al subsurface");
353                (Content::Video(feeder), w as i32, h as i32, 0, 0)
354            }
355            None => {
356                let buffer = crear_buffer_solido(&shm, &qh, SOLID_W, SOLID_H)?;
357                child.attach(Some(&buffer), 0, 0);
358                child.damage_buffer(0, 0, SOLID_W, SOLID_H);
359                child.commit();
360                (Content::Solid(buffer), SOLID_W, SOLID_H, SOLID_X, SOLID_Y)
361            }
362        };
363
364        subsurface.set_position(sx, sy);
365        // Región opaca: condición para que mirada lo mande a un plano de overlay
366        // (Fase 1b). No cambia la visibilidad ahora.
367        let region = compositor.create_region(&qh, ());
368        region.add(0, 0, sw, sh);
369        child.set_opaque_region(Some(&region));
370        region.destroy();
371        child.commit();
372
373        conn.flush().map_err(|e| format!("flush: {e}"))?;
374
375        Ok(Self {
376            conn,
377            queue,
378            globals,
379            parent,
380            _child: child,
381            subsurface,
382            content,
383            below: false,
384        })
385    }
386
387    fn frame_interval(&self) -> Option<Duration> {
388        match &self.content {
389            Content::Video(f) => Some(f.interval),
390            Content::Solid(_) => None,
391        }
392    }
393
394    fn set_below(&mut self, below: bool) {
395        self.below = below;
396        if below {
397            self.subsurface.place_below(&self.parent);
398        } else {
399            self.subsurface.place_above(&self.parent);
400        }
401        let _ = self.conn.flush();
402    }
403
404    /// Empuja frames de video (si aplica) y drena eventos de la queue.
405    fn pump(&mut self, now: Instant) {
406        if let Content::Video(f) = &mut self.content {
407            f.pump(now);
408        }
409        let _ = self.queue.dispatch_pending(&mut self.globals);
410        let _ = self.conn.flush();
411    }
412}
413
414// ---------------------------------------------------------------------------
415// Helpers shm
416// ---------------------------------------------------------------------------
417
418/// Crea un memfd de `size` bytes y lo mapea. Devuelve (fd, puntero).
419fn memfd_map(size: usize) -> Result<(i32, *mut u8), String> {
420    let fd = unsafe { libc::memfd_create(c"llimphi-video-plane".as_ptr(), 0) };
421    if fd < 0 {
422        return Err("memfd_create falló".into());
423    }
424    if unsafe { libc::ftruncate(fd, size as libc::off_t) } != 0 {
425        unsafe { libc::close(fd) };
426        return Err("ftruncate falló".into());
427    }
428    let map = unsafe {
429        libc::mmap(
430            std::ptr::null_mut(),
431            size,
432            libc::PROT_READ | libc::PROT_WRITE,
433            libc::MAP_SHARED,
434            fd,
435            0,
436        )
437    };
438    if map == libc::MAP_FAILED {
439        unsafe { libc::close(fd) };
440        return Err("mmap falló".into());
441    }
442    Ok((fd, map as *mut u8))
443}
444
445/// Pool shm con `RING` buffers XRGB8888 de `w×h` contiguos. Devuelve el mapeo,
446/// el tamaño de un frame, y los `wl_buffer`.
447fn crear_ring_shm(
448    shm: &WlShm,
449    qh: &QueueHandle<WlGlobals>,
450    w: i32,
451    h: i32,
452) -> Result<(*mut u8, usize, Vec<WlBuffer>), String> {
453    let stride = w * 4;
454    let frame_bytes = (stride * h) as usize;
455    let total = frame_bytes * RING;
456    let (fd, map) = memfd_map(total)?;
457    let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
458    let pool = shm.create_pool(borrowed, total as i32, qh, ());
459    let buffers = (0..RING)
460        .map(|i| {
461            pool.create_buffer(
462                (i * frame_bytes) as i32,
463                w,
464                h,
465                stride,
466                wl_shm::Format::Xrgb8888,
467                qh,
468                (),
469            )
470        })
471        .collect();
472    unsafe { libc::close(fd) };
473    pool.destroy();
474    Ok((map, frame_bytes, buffers))
475}
476
477/// Ring de `RING` buffers **dmabuf** (gbm LINEAR, XRGB8888) de `w×h`, envueltos
478/// como `wl_buffer` por `zwp_linux_dmabuf`. A diferencia del shm, son
479/// scanout-capable: mirada los puede promover a un plano de overlay del DRM.
480/// Devuelve el `Ring::Dmabuf` (retiene device+bos vivos) y los `wl_buffer`.
481fn crear_ring_dmabuf(
482    dmabuf: &ZwpLinuxDmabufV1,
483    qh: &QueueHandle<WlGlobals>,
484    w: i32,
485    h: i32,
486) -> Result<(Ring, Vec<WlBuffer>), String> {
487    // Nodo de render de la GPU: el mismo Intel que usa mirada, así el buffer
488    // LINEAR es importable para composición y escaneable en un plano.
489    let file = std::fs::OpenOptions::new()
490        .read(true)
491        .write(true)
492        .open("/dev/dri/renderD128")
493        .map_err(|e| format!("abrir renderD128: {e}"))?;
494    let device = GbmDevice::new(file).map_err(|e| format!("gbm device: {e}"))?;
495
496    // SIN `SCANOUT`: un render node (renderD128) no puede asignar buffers de
497    // scanout (EACCES — eso es del card node con DRM master, que tiene mirada).
498    // No hace falta: mirada importa el dmabuf y forja SU framebuffer en card1; lo
499    // que lo hace escaneable es el modifier **LINEAR**, no el flag del cliente.
500    // `WRITE` da EINVAL en Intel (es para gbm_bo_write, no para map); para mapear
501    // por CPU basta `LINEAR`. Probamos combos en orden hasta que uno asigne.
502    let combos: [(&str, BufferObjectFlags); 3] = [
503        ("LINEAR|RENDERING", BufferObjectFlags::LINEAR | BufferObjectFlags::RENDERING),
504        ("LINEAR", BufferObjectFlags::LINEAR),
505        ("RENDERING", BufferObjectFlags::RENDERING),
506    ];
507    let fourcc = GbmFormat::Xrgb8888 as u32;
508
509    let mut bos: Vec<BufferObject<()>> = Vec::with_capacity(RING);
510    let mut buffers: Vec<WlBuffer> = Vec::with_capacity(RING);
511    for i in 0..RING {
512        let bo = {
513            let mut hecho: Option<BufferObject<()>> = None;
514            let mut ultimo = String::new();
515            for (nombre, flags) in combos {
516                match device.create_buffer_object::<()>(w as u32, h as u32, GbmFormat::Xrgb8888, flags) {
517                    Ok(b) => {
518                        if i == 0 {
519                            eprintln!("[spike] gbm flags = {nombre}");
520                        }
521                        hecho = Some(b);
522                        break;
523                    }
524                    Err(e) => ultimo = format!("{nombre}: {e}"),
525                }
526            }
527            hecho.ok_or_else(|| format!("gbm bo[{i}] (ningún combo): {ultimo}"))?
528        };
529        let fd = bo.fd().map_err(|_| format!("gbm bo[{i}].fd inválido"))?;
530        let modifier: u64 = bo.modifier().into();
531        let params = dmabuf.create_params(qh, ());
532        params.add(
533            fd.as_fd(),
534            0,
535            bo.offset(0),
536            bo.stride(),
537            (modifier >> 32) as u32,
538            (modifier & 0xffff_ffff) as u32,
539        );
540        // create_immed (v2+): sin roundtrip. El compositor dupea el fd; si el
541        // formato/modifier no le sirve manda un protocol error (lo veríamos).
542        let buffer = params.create_immed(
543            w,
544            h,
545            fourcc,
546            zwp_linux_buffer_params_v1::Flags::empty(),
547            qh,
548            (),
549        );
550        params.destroy();
551        drop(fd); // ya dupeado en create_immed; el bo mantiene el buffer real.
552        if i == 0 {
553            eprintln!(
554                "[spike] dmabuf bo {w}x{h} stride={} offset={} modifier={modifier:#x} fourcc={fourcc:#x}",
555                bo.stride(),
556                bo.offset(0),
557            );
558        }
559        bos.push(bo);
560        buffers.push(buffer);
561    }
562    Ok((Ring::Dmabuf { _device: device, bos }, buffers))
563}
564
565/// Buffer shm de color sólido (modo sin archivo).
566fn crear_buffer_solido(
567    shm: &WlShm,
568    qh: &QueueHandle<WlGlobals>,
569    w: i32,
570    h: i32,
571) -> Result<WlBuffer, String> {
572    let stride = w * 4;
573    let size = (stride * h) as usize;
574    let (fd, map) = memfd_map(size)?;
575    let px = map as *mut u32;
576    for i in 0..(w * h) as isize {
577        unsafe { px.offset(i).write(0x00FF_7800) }; // naranja
578    }
579    let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
580    let pool = shm.create_pool(borrowed, size as i32, qh, ());
581    let buffer = pool.create_buffer(0, w, h, stride, wl_shm::Format::Xrgb8888, qh, ());
582    unsafe { libc::close(fd) };
583    let _ = map;
584    pool.destroy();
585    Ok(buffer)
586}
587
588// ---------------------------------------------------------------------------
589// UI winit + wgpu (limpia el fondo; el "hueco" = fondo transparente)
590// ---------------------------------------------------------------------------
591
592struct Gpu {
593    surface: wgpu::Surface<'static>,
594    device: wgpu::Device,
595    queue: wgpu::Queue,
596    config: wgpu::SurfaceConfiguration,
597}
598
599impl Gpu {
600    fn new(window: Arc<Window>) -> Result<Self, String> {
601        let instance = {
602            let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
603            desc.backends = wgpu::Backends::PRIMARY;
604            wgpu::Instance::new(desc)
605        };
606        let surface = instance
607            .create_surface(window.clone())
608            .map_err(|e| format!("create_surface: {e}"))?;
609        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
610            power_preference: wgpu::PowerPreference::LowPower,
611            force_fallback_adapter: false,
612            compatible_surface: Some(&surface),
613        }))
614        .map_err(|e| format!("request_adapter: {e}"))?;
615        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
616            label: Some("video-plane-spike"),
617            required_features: wgpu::Features::empty(),
618            required_limits: wgpu::Limits::default(),
619            memory_hints: wgpu::MemoryHints::default(),
620            experimental_features: wgpu::ExperimentalFeatures::default(),
621            trace: wgpu::Trace::Off,
622        }))
623        .map_err(|e| format!("request_device: {e}"))?;
624
625        let caps = surface.get_capabilities(&adapter);
626        let format = caps
627            .formats
628            .iter()
629            .copied()
630            .find(|f| matches!(f, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm))
631            .unwrap_or(caps.formats[0]);
632        let alpha_mode = caps
633            .alpha_modes
634            .iter()
635            .copied()
636            .find(|a| matches!(a, wgpu::CompositeAlphaMode::PreMultiplied))
637            .unwrap_or(caps.alpha_modes[0]);
638        eprintln!("[spike] wgpu formato={format:?} alpha={alpha_mode:?}");
639        let size = window.inner_size();
640        let config = wgpu::SurfaceConfiguration {
641            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
642            format,
643            width: size.width.max(1),
644            height: size.height.max(1),
645            present_mode: wgpu::PresentMode::Fifo,
646            desired_maximum_frame_latency: 2,
647            alpha_mode,
648            view_formats: vec![],
649        };
650        surface.configure(&device, &config);
651        Ok(Self { surface, device, queue, config })
652    }
653
654    fn resize(&mut self, w: u32, h: u32) {
655        self.config.width = w.max(1);
656        self.config.height = h.max(1);
657        self.surface.configure(&self.device, &self.config);
658    }
659
660    fn render(&self, transparent: bool) {
661        // wgpu 29 devuelve un enum de estado, no un Result. `Suboptimal` trae
662        // textura válida (en wgpu 27 llegaba como `Ok` con un flag al lado).
663        let frame = match self.surface.get_current_texture() {
664            wgpu::CurrentSurfaceTexture::Success(f)
665            | wgpu::CurrentSurfaceTexture::Suboptimal(f) => f,
666            _ => return,
667        };
668        let view = frame
669            .texture
670            .create_view(&wgpu::TextureViewDescriptor::default());
671        let mut enc = self
672            .device
673            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
674        let clear = if transparent {
675            wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 0.0 }
676        } else {
677            wgpu::Color { r: 0.05, g: 0.07, b: 0.12, a: 1.0 }
678        };
679        {
680            let _pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
681                label: None,
682                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
683                    view: &view,
684                    resolve_target: None,
685                    depth_slice: None,
686                    ops: wgpu::Operations {
687                        load: wgpu::LoadOp::Clear(clear),
688                        store: wgpu::StoreOp::Store,
689                    },
690                })],
691                depth_stencil_attachment: None,
692                timestamp_writes: None,
693                occlusion_query_set: None,
694                multiview_mask: None,
695            });
696        }
697        self.queue.submit(Some(enc.finish()));
698        frame.present();
699    }
700}
701
702// ---------------------------------------------------------------------------
703// App
704// ---------------------------------------------------------------------------
705
706struct SpikeApp {
707    /// Fuente de video ya abierta (o None para el cuadro sólido). Se consume al
708    /// crear el plano.
709    pending_video: Option<(Box<dyn FrameSource + Send>, u32, u32, f32)>,
710    win_size: (u32, u32),
711    window: Option<Arc<Window>>,
712    gpu: Option<Gpu>,
713    plane: Option<VideoPlane>,
714    transparent: bool,
715    interval: Option<Duration>,
716}
717
718impl ApplicationHandler for SpikeApp {
719    fn resumed(&mut self, el: &ActiveEventLoop) {
720        if self.window.is_some() {
721            return;
722        }
723        let attrs = Window::default_attributes()
724            .with_title("llimphi-video-plane · subsurface spike")
725            .with_transparent(true)
726            .with_inner_size(winit::dpi::LogicalSize::new(self.win_size.0, self.win_size.1));
727        let window = Arc::new(el.create_window(attrs).expect("create_window"));
728
729        let gpu = match Gpu::new(window.clone()) {
730            Ok(g) => g,
731            Err(e) => {
732                eprintln!("[spike] GPU falló: {e}");
733                el.exit();
734                return;
735            }
736        };
737        // Primer frame: crea/commitea el padre antes de atachar el subsurface.
738        gpu.render(self.transparent);
739
740        let video = self.pending_video.take();
741        match unsafe { VideoPlane::atachar(&window, video) } {
742            Ok(p) => {
743                self.interval = p.frame_interval();
744                eprintln!("[spike] subsurface OK. B=abajo/arriba, T=transparente, ESC=salir.");
745                self.plane = Some(p);
746            }
747            Err(e) => eprintln!("[spike] atachar subsurface FALLÓ: {e}"),
748        }
749
750        self.gpu = Some(gpu);
751        self.window = Some(window);
752        // NO pedimos redraw: el padre es un host transparente estático. Redibujarlo
753        // por frame bloquea el loop en `present` (Fifo) cuando mirada no manda
754        // frame-callbacks (ventana no visible). El video lo bombea `about_to_wait`
755        // (Poll) sobre el subsurface, que commitea independiente (desync).
756    }
757
758    fn window_event(&mut self, el: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
759        match event {
760            WindowEvent::CloseRequested => el.exit(),
761            WindowEvent::Resized(size) => {
762                if let Some(g) = &mut self.gpu {
763                    g.resize(size.width, size.height);
764                }
765                if let Some(w) = &self.window {
766                    w.request_redraw();
767                }
768            }
769            WindowEvent::RedrawRequested => {
770                // Sólo si el compositor lo pide (frame-callback real): repintamos el
771                // padre. NO re-pedimos redraw (evita el bloqueo en `present`).
772                if let Some(g) = &self.gpu {
773                    g.render(self.transparent);
774                }
775            }
776            WindowEvent::KeyboardInput {
777                event: KeyEvent { logical_key, state: ElementState::Pressed, .. },
778                ..
779            } => match logical_key {
780                Key::Named(NamedKey::Escape) => el.exit(),
781                Key::Character(c) if c.eq_ignore_ascii_case("b") => {
782                    if let Some(p) = &mut self.plane {
783                        let below = !p.below;
784                        p.set_below(below);
785                        eprintln!("[spike] subsurface {}", if below { "DEBAJO" } else { "ENCIMA" });
786                    }
787                    if let Some(w) = &self.window {
788                        w.request_redraw();
789                    }
790                }
791                Key::Character(c) if c.eq_ignore_ascii_case("t") => {
792                    self.transparent = !self.transparent;
793                    eprintln!("[spike] fondo {}", if self.transparent { "TRANSPARENTE" } else { "opaco" });
794                    if let Some(w) = &self.window {
795                        w.request_redraw();
796                    }
797                }
798                _ => {}
799            },
800            _ => {}
801        }
802    }
803
804    /// Despertado por el hilo timer (via `EventLoopProxy::send_event`) a cadencia
805    /// de video: bombea un frame al subsurface. Este es el ÚNICO driver del pump —
806    /// ni el `WaitUntil` ni los frame-callbacks del padre despiertan el loop bajo
807    /// mirada, pero `send_event` sí. Mono-core, sin busy-loop → el decoder respira.
808    fn user_event(&mut self, _el: &ActiveEventLoop, _event: ()) {
809        if let Some(p) = &mut self.plane {
810            p.pump(Instant::now());
811        }
812    }
813}
814
815/// Fuente **sintética**: patrón animado (tablero diagonal en movimiento +
816/// gradiente) a `w×h`. Decoupla la medición del pipeline dmabuf de foreign-av,
817/// que se estanca al manejarlo sin el sistema de clock/sync de media-app. Emite
818/// un frame por tick (el hilo timer del loop pacea la cadencia real).
819struct SyntheticSource {
820    w: u32,
821    h: u32,
822    t: f32,
823}
824
825impl FrameSource for SyntheticSource {
826    fn tick(&mut self, dt: Duration, buf: &mut Vec<u8>) -> Option<(u32, u32)> {
827        self.t += dt.as_secs_f32();
828        let (w, h) = (self.w as usize, self.h as usize);
829        buf.resize(w * h * 4, 0);
830        let phase = (self.t * 120.0) as i32; // scroll ~120 px/s
831        for y in 0..h {
832            let gy = ((y * 255) / h) as u8;
833            for x in 0..w {
834                let band = (((x as i32 + y as i32 + phase) / 32) & 1) as u8;
835                let i = (y * w + x) * 4;
836                buf[i] = if band == 0 { 40 } else { 220 };
837                buf[i + 1] = ((x * 255) / w) as u8;
838                buf[i + 2] = gy;
839                buf[i + 3] = 255;
840            }
841        }
842        Some((self.w, self.h))
843    }
844}
845
846/// Abre `path` por foreign-av y arma un `FrameSource` + dims + fps.
847fn abrir_video(path: &str) -> Result<(Box<dyn FrameSource + Send>, u32, u32, f32), String> {
848    let info = foreign_av::probe(path).map_err(|e| format!("probe: {e:?}"))?;
849    let session = foreign_av::MediaSession::open(info).map_err(|e| format!("open: {e:?}"))?;
850    let src = foreign_av::FfmpegVideoSource::from_session(session)
851        .map_err(|e| format!("video source: {e:?}"))?;
852    let (w, h) = src.dimensions();
853    let fps = src.fps();
854    Ok((Box::new(src), w, h, fps))
855}
856
857fn main() {
858    let arg = std::env::args().nth(1);
859    let (pending_video, win_size) = match arg.as_deref() {
860        // Modo medición: fuente sintética a 30 fps, sin foreign-av.
861        Some("test") => {
862            let (w, h, fps) = (1024u32, 576u32, 30.0f32);
863            let src: Box<dyn FrameSource + Send> = Box::new(SyntheticSource { w, h, t: 0.0 });
864            eprintln!("[spike] modo TEST: fuente sintética {w}x{h} @ {fps}fps");
865            (Some((src, w, h, fps)), (w, h))
866        }
867        Some(path) => match abrir_video(path) {
868            Ok(v) => {
869                let (_, w, h, _) = &v;
870                let win = ((*w).min(1600).max(320), (*h).min(900).max(240));
871                (Some(v), win)
872            }
873            Err(e) => {
874                eprintln!("[spike] no pude abrir {path}: {e} — caigo a cuadro sólido");
875                (None, DEFAULT_WIN)
876            }
877        },
878        None => {
879            eprintln!("[spike] sin archivo → cuadro sólido. Uso: subsurface-spike <video>");
880            (None, DEFAULT_WIN)
881        }
882    };
883
884    // Intervalo del video → cadencia del hilo timer que despierta el loop.
885    let interval = pending_video.as_ref().map(|(_, _, _, fps)| {
886        let f = if fps.is_finite() && *fps > 1.0 { *fps } else { 30.0 };
887        Duration::from_secs_f32(1.0 / f)
888    });
889
890    let el = EventLoop::<()>::with_user_event()
891        .build()
892        .expect("event loop");
893    // Wait: el loop bloquea entre despertadas (sin quemar CPU). Lo despierta el
894    // hilo timer via send_event, no el WaitUntil interno (que no dispara aquí).
895    el.set_control_flow(ControlFlow::Wait);
896    // Hilo timer: reloj de pared que patea el loop a cadencia de video. Único
897    // mecanismo que despierta winit bajo mirada de forma fiable.
898    if let Some(iv) = interval {
899        let proxy = el.create_proxy();
900        std::thread::spawn(move || loop {
901            std::thread::sleep(iv);
902            if proxy.send_event(()).is_err() {
903                break; // el loop murió → salimos.
904            }
905        });
906    }
907
908    let mut app = SpikeApp {
909        pending_video,
910        win_size,
911        window: None,
912        gpu: None,
913        plane: None,
914        transparent: false,
915        interval,
916    };
917    el.run_app(&mut app).expect("run_app");
918}