gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

⎇ 14d76bd4af13b219f0b162a54f09e784dea9aa8d llimphi-hal/src/lib.rs Rust · 823 líneas · 32.7 KB Historia Quién tocó qué crudo

feat: llimphi standalone — framework UI soberano extraído del monorepo e65e9cc62 · Sergio ·

1//! llimphi-hal — Puente al Silicio.
2//!
3//! Aísla el motor del sistema operativo. Pinta en ventana Wayland/X11
4//! (vía `mirada` en producción, vía `winit` en dev) o framebuffer directo
5//! del kernel `wawa` (TODO). Trait `Surface` abstracto + struct `Hal`
6//! que posee Instance/Adapter/Device/Queue de wgpu.
7
8use std::sync::Arc;
9
10pub use raw_window_handle;
11pub use wgpu;
12pub use winit;
13
14use winit::window::Window;
15
16/// Errores al adquirir un frame de la superficie.
17#[derive(Debug)]
18pub enum SurfaceError {
19    Lost,
20    Outdated,
21    OutOfMemory,
22    Timeout,
23    Other(String),
24}
25
26impl std::fmt::Display for SurfaceError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            Self::Lost => write!(f, "surface lost"),
30            Self::Outdated => write!(f, "surface outdated"),
31            Self::OutOfMemory => write!(f, "surface out of memory"),
32            Self::Timeout => write!(f, "surface timeout"),
33            Self::Other(s) => write!(f, "surface error: {s}"),
34        }
35    }
36}
37
38impl std::error::Error for SurfaceError {}
39
40/// Errores al construir Hal o crear una Surface.
41#[derive(Debug)]
42pub enum HalError {
43    NoAdapter,
44    RequestDevice(String),
45    CreateSurface(String),
46}
47
48impl std::fmt::Display for HalError {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::NoAdapter => write!(f, "no GPU adapter available"),
52            Self::RequestDevice(s) => write!(f, "request_device failed: {s}"),
53            Self::CreateSurface(s) => write!(f, "create_surface failed: {s}"),
54        }
55    }
56}
57
58impl std::error::Error for HalError {}
59
60/// Superficie gráfica donde llimphi pinta.
61///
62/// Vello (rasterizador) emite a una textura intermedia con storage binding
63/// (la única forma portable: los formatos de swapchain no aceptan writes
64/// de compute shader en muchos adapters). En `present` se blittea la
65/// intermedia al swapchain real y se hace el flip.
66///
67/// Implementaciones:
68/// - [`WinitSurface`]: ventana Wayland/X11 (dev + producción vía mirada).
69/// - `WawaFramebufferSurface` (TODO): framebuffer directo del kernel wawa.
70pub trait Surface {
71    fn size(&self) -> (u32, u32);
72    fn resize(&mut self, width: u32, height: u32);
73    /// Adquiere la textura intermedia donde el raster pinta este frame.
74    fn acquire(&mut self) -> Result<Frame, SurfaceError>;
75    /// Blittea la intermedia al swapchain y la presenta.
76    fn present(&mut self, frame: Frame, hal: &Hal);
77}
78
79/// Frame en curso. `view()` devuelve la textura intermedia (Rgba8Unorm,
80/// STORAGE_BINDING) lista para que vello escriba sobre ella.
81pub struct Frame {
82    surface_texture: wgpu::SurfaceTexture,
83    surface_view: wgpu::TextureView,
84    intermediate_view: wgpu::TextureView,
85    /// Textura secundaria para la capa de overlay (menús/paleta/modal)
86    /// cuando hay contenido `gpu_paint` que la taparía. El overlay se
87    /// rasteriza acá con fondo transparente y luego se compone con
88    /// alpha SOBRE la intermedia (que ya tiene UI + video). Ver
89    /// [`OverlayCompositor`] y el eventloop de `llimphi-ui`.
90    overlay_view: wgpu::TextureView,
91    width: u32,
92    height: u32,
93}
94
95impl Frame {
96    pub fn view(&self) -> &wgpu::TextureView {
97        &self.intermediate_view
98    }
99
100    /// Vista de la textura de overlay (mismo tamaño y formato que la
101    /// intermedia). Sólo se usa en el camino de compositing del overlay.
102    pub fn overlay_view(&self) -> &wgpu::TextureView {
103        &self.overlay_view
104    }
105
106    pub fn size(&self) -> (u32, u32) {
107        (self.width, self.height)
108    }
109}
110
111/// Estado wgpu compartido. Una instancia por proceso. `Device` y `Queue`
112/// son `Arc` internamente, así que clonar es barato.
113pub struct Hal {
114    pub instance: wgpu::Instance,
115    pub adapter: wgpu::Adapter,
116    pub device: wgpu::Device,
117    pub queue: wgpu::Queue,
118}
119
120impl Hal {
121    /// Construye Hal pidiendo un adapter compatible con una surface dada
122    /// (recomendado: pasar `Some(&surface)` para garantizar que el adapter
123    /// elegido sabe presentar a esa surface).
124    pub async fn new(
125        compatible_surface: Option<&wgpu::Surface<'static>>,
126    ) -> Result<Self, HalError> {
127        let opts = wgpu::RequestAdapterOptions {
128            power_preference: wgpu::PowerPreference::HighPerformance,
129            force_fallback_adapter: false,
130            compatible_surface,
131        };
132        // Preferimos backends PRIMARY (Vulkan/Metal/DX12). El backend GL de
133        // wgpu sobre Mesa/Wayland tiene un bug de teardown: al soltar la
134        // instancia, `eglTerminate` marshalea sobre una conexión Wayland ya
135        // muerta (`wl_proxy_marshal`) y revienta con SIGSEGV. Con
136        // `Backends::all()` (el default), wgpu puede elegir GL aun habiendo
137        // Vulkan, y la app crashea al cerrar/teardown. Forzamos PRIMARY; si la
138        // máquina no tiene Vulkan/Metal/DX12 (VM vieja, etc.) caemos a todos
139        // los backends —incluido GL— para no dejarla sin gráficos. En el
140        // camino de escritorio `compatible_surface` es `None` (la surface se
141        // crea después contra esta misma instancia), así que cambiar de
142        // instancia aquí es seguro.
143        let primary = wgpu::Instance::new(&wgpu::InstanceDescriptor {
144            backends: wgpu::Backends::PRIMARY,
145            ..Default::default()
146        });
147        let (instance, adapter) = match primary.request_adapter(&opts).await {
148            Some(a) => (primary, a),
149            None => {
150                let all = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
151                let a = all.request_adapter(&opts).await.ok_or(HalError::NoAdapter)?;
152                (all, a)
153            }
154        };
155        // `Limits::default()` cubre los 5 storage buffers/stage que vello
156        // necesita. `downlevel_defaults()` solo expone 4 y rompe el raster.
157        // Si el adapter no lo aguanta, `using_resolution` recorta lo recortable
158        // (texturas/buffers grandes) preservando los conteos mínimos.
159        let limits = wgpu::Limits::default().using_resolution(adapter.limits());
160        let (device, queue) = adapter
161            .request_device(
162                &wgpu::DeviceDescriptor {
163                    label: Some("llimphi-hal-device"),
164                    required_features: wgpu::Features::empty(),
165                    required_limits: limits,
166                    memory_hints: wgpu::MemoryHints::Performance,
167                },
168                None,
169            )
170            .await
171            .map_err(|e| HalError::RequestDevice(e.to_string()))?;
172        Ok(Self {
173            instance,
174            adapter,
175            device,
176            queue,
177        })
178    }
179
180    /// Construye el `Hal` **y** una [`RawSurface`] a la vez, eligiendo el adaptador
181    /// **compatible con esa surface** — el dispositivo que el compositor sabe
182    /// presentar. Es el camino correcto para el backend layer-shell de `pata`.
183    ///
184    /// El problema que resuelve: en sistemas multi-GPU (Optimus), pedir el
185    /// adaptador sin pista de surface (`new(None)` con `HighPerformance`) puede
186    /// elegir la dGPU mientras el compositor compone en la iGPU → los dmabuf
187    /// cruzan dispositivos y `get_capabilities` devuelve 0 formatos (la surface
188    /// "no expone formatos"). Pasar `compatible_surface` ata el adaptador al
189    /// dispositivo del compositor. Como la surface hace falta ANTES de pedir el
190    /// adaptador, y `new` crea la instancia internamente, este constructor une los
191    /// dos pasos.
192    ///
193    /// `make_target` reconstruye el `SurfaceTargetUnsafe` cada vez que se llama
194    /// (los `RawHandle` son `Copy`): `create_surface_unsafe` consume el target y
195    /// puede que probemos dos instancias (PRIMARY y, si no hay adaptador, todos
196    /// los backends — el GL de Mesa/Wayland revienta en teardown, por eso PRIMARY
197    /// primero, igual que [`Hal::new`]).
198    ///
199    /// # Safety
200    /// Los handles que produce `make_target` deben apuntar a objetos Wayland/…
201    /// vivos durante toda la vida de la `RawSurface` devuelta.
202    pub async unsafe fn new_for_raw_surface(
203        make_target: impl Fn() -> wgpu::SurfaceTargetUnsafe,
204        width: u32,
205        height: u32,
206    ) -> Result<(Self, RawSurface), HalError> {
207        // PRIMARY (Vulkan/Metal/DX12) primero; si no hay adaptador compatible, a
208        // todos los backends recreando instancia y surface.
209        let primary = wgpu::Instance::new(&wgpu::InstanceDescriptor {
210            backends: wgpu::Backends::PRIMARY,
211            ..Default::default()
212        });
213        let prim_surface = unsafe { primary.create_surface_unsafe(make_target()) }
214            .map_err(|e| HalError::CreateSurface(e.to_string()))?;
215        let prim_adapter = primary
216            .request_adapter(&wgpu::RequestAdapterOptions {
217                power_preference: wgpu::PowerPreference::HighPerformance,
218                force_fallback_adapter: false,
219                compatible_surface: Some(&prim_surface),
220            })
221            .await;
222        let (instance, adapter, wgpu_surface) = match prim_adapter {
223            Some(a) => (primary, a, prim_surface),
224            None => {
225                let all = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
226                let surface = unsafe { all.create_surface_unsafe(make_target()) }
227                    .map_err(|e| HalError::CreateSurface(e.to_string()))?;
228                let a = all
229                    .request_adapter(&wgpu::RequestAdapterOptions {
230                        power_preference: wgpu::PowerPreference::HighPerformance,
231                        force_fallback_adapter: false,
232                        compatible_surface: Some(&surface),
233                    })
234                    .await
235                    .ok_or(HalError::NoAdapter)?;
236                (all, a, surface)
237            }
238        };
239        let limits = wgpu::Limits::default().using_resolution(adapter.limits());
240        let (device, queue) = adapter
241            .request_device(
242                &wgpu::DeviceDescriptor {
243                    label: Some("llimphi-hal-device"),
244                    required_features: wgpu::Features::empty(),
245                    required_limits: limits,
246                    memory_hints: wgpu::MemoryHints::Performance,
247                },
248                None,
249            )
250            .await
251            .map_err(|e| HalError::RequestDevice(e.to_string()))?;
252        let hal = Self {
253            instance,
254            adapter,
255            device,
256            queue,
257        };
258        let surface = RawSurface::from_surface(&hal, wgpu_surface, width, height)?;
259        Ok((hal, surface))
260    }
261}
262
263/// Surface basada en `winit::window::Window`. Mantiene una textura
264/// intermedia `Rgba8Unorm` con storage binding (donde pinta vello) y
265/// un `TextureBlitter` que la copia al swapchain al presentar.
266pub struct WinitSurface {
267    _window: Arc<Window>,
268    surface: wgpu::Surface<'static>,
269    config: wgpu::SurfaceConfiguration,
270    device: wgpu::Device,
271    intermediate: wgpu::Texture,
272    intermediate_view: wgpu::TextureView,
273    /// Textura de la capa de overlay (ver [`Frame::overlay_view`]).
274    overlay: wgpu::Texture,
275    overlay_view: wgpu::TextureView,
276    blitter: wgpu::util::TextureBlitter,
277}
278
279const INTERMEDIATE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
280
281impl WinitSurface {
282    /// Constructor "feliz": crea la `wgpu::Surface` internamente.
283    /// Conveniente en desktop donde la secuencia normal es
284    /// `Hal::new(None)` → `WinitSurface::new(hal, window)`. **En Android
285    /// usar [`WinitSurface::from_surface`]** — allí la surface debe
286    /// existir antes del `request_adapter(compatible_surface=Some(...))`,
287    /// y crearla dos veces sobre la misma `ANativeWindow` falla con
288    /// `ERROR_NATIVE_WINDOW_IN_USE_KHR`.
289    pub fn new(hal: &Hal, window: Arc<Window>) -> Result<Self, HalError> {
290        let surface = hal
291            .instance
292            .create_surface(window.clone())
293            .map_err(|e| HalError::CreateSurface(e.to_string()))?;
294        Self::from_surface(hal, window, surface)
295    }
296
297    /// Constructor reutilizable: arma el `WinitSurface` envolviendo una
298    /// `wgpu::Surface` ya creada por el caller. Necesario en Android
299    /// porque el orden allí es:
300    ///
301    /// 1. `instance.create_surface(window)`
302    /// 2. `instance.request_adapter(compatible_surface=Some(&surface))`
303    /// 3. `adapter.request_device(...)`
304    /// 4. `WinitSurface::from_surface(hal, window, surface)`
305    ///
306    /// — no se puede dropear la surface entre 2 y 4 ni recrearla, porque
307    /// Android reserva la `ANativeWindow` por VkSurface y rechaza un
308    /// segundo `vkCreateAndroidSurfaceKHR` sobre la misma ventana.
309    pub fn from_surface(
310        hal: &Hal,
311        window: Arc<Window>,
312        surface: wgpu::Surface<'static>,
313    ) -> Result<Self, HalError> {
314        let size = window.inner_size();
315        let caps = surface.get_capabilities(&hal.adapter);
316        // Preferimos Bgra8Unorm o Rgba8Unorm (no sRGB) para que el blit
317        // desde la intermedia lineal preserve los valores tal cual.
318        let format = caps
319            .formats
320            .iter()
321            .copied()
322            .find(|f| matches!(f, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm))
323            .unwrap_or(caps.formats[0]);
324        let config = wgpu::SurfaceConfiguration {
325            // El swapchain solo necesita render-attachment: vello no escribe
326            // directo, escribe a la intermedia y luego se blittea.
327            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
328            format,
329            width: size.width.max(1),
330            height: size.height.max(1),
331            present_mode: choose_present_mode(&caps),
332            desired_maximum_frame_latency: 2,
333            alpha_mode: caps.alpha_modes[0],
334            view_formats: vec![],
335        };
336        surface.configure(&hal.device, &config);
337        let (intermediate, intermediate_view) =
338            create_intermediate(&hal.device, config.width, config.height);
339        let (overlay, overlay_view) =
340            create_intermediate(&hal.device, config.width, config.height);
341        let blitter = wgpu::util::TextureBlitter::new(&hal.device, format);
342        Ok(Self {
343            _window: window,
344            surface,
345            config,
346            device: hal.device.clone(),
347            intermediate,
348            intermediate_view,
349            overlay,
350            overlay_view,
351            blitter,
352        })
353    }
354
355    pub fn format(&self) -> wgpu::TextureFormat {
356        self.config.format
357    }
358}
359
360/// Surface sobre una `wgpu::Surface` creada desde **handles raw** (sin
361/// `winit::Window`): la usa el backend `wlr-layer-shell` de `pata` para pintar
362/// en una *layer surface* de Wayland (barras/paneles al nivel de eww/waybar).
363/// Misma mecánica que [`WinitSurface`] —intermedia `Rgba8Unorm` + blit al
364/// swapchain— pero el tamaño se pasa explícito porque no hay ventana que
365/// consultar. La `wgpu::Surface` la crea el caller (típicamente con
366/// `instance.create_surface_unsafe` desde los punteros `wl_display`/`wl_surface`).
367pub struct RawSurface {
368    surface: wgpu::Surface<'static>,
369    config: wgpu::SurfaceConfiguration,
370    device: wgpu::Device,
371    intermediate: wgpu::Texture,
372    intermediate_view: wgpu::TextureView,
373    overlay: wgpu::Texture,
374    overlay_view: wgpu::TextureView,
375    blitter: wgpu::util::TextureBlitter,
376}
377
378impl RawSurface {
379    /// Envuelve una `wgpu::Surface` ya creada, con el tamaño físico inicial.
380    pub fn from_surface(
381        hal: &Hal,
382        surface: wgpu::Surface<'static>,
383        width: u32,
384        height: u32,
385    ) -> Result<Self, HalError> {
386        let caps = surface.get_capabilities(&hal.adapter);
387        let info = hal.adapter.get_info();
388        // Si la superficie no expone formatos, el compositor no la soporta por
389        // este backend (Vulkan/GL WSI): error claro en vez de un panic por
390        // indexar `formats[0]` sobre una lista vacía.
391        let format = match caps
392            .formats
393            .iter()
394            .copied()
395            .find(|f| matches!(f, wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Rgba8Unorm))
396            .or_else(|| caps.formats.first().copied())
397        {
398            Some(f) => f,
399            None => {
400                return Err(HalError::CreateSurface(format!(
401                    "la superficie no expone formatos (adapter {:?}/{:?}): el compositor no la soporta por {:?} WSI",
402                    info.backend, info.device_type, info.backend
403                )))
404            }
405        };
406        let alpha_mode = caps
407            .alpha_modes
408            .first()
409            .copied()
410            .unwrap_or(wgpu::CompositeAlphaMode::Auto);
411        let config = wgpu::SurfaceConfiguration {
412            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
413            format,
414            width: width.max(1),
415            height: height.max(1),
416            present_mode: choose_present_mode(&caps),
417            desired_maximum_frame_latency: 2,
418            alpha_mode,
419            view_formats: vec![],
420        };
421        surface.configure(&hal.device, &config);
422        let (intermediate, intermediate_view) =
423            create_intermediate(&hal.device, config.width, config.height);
424        let (overlay, overlay_view) =
425            create_intermediate(&hal.device, config.width, config.height);
426        let blitter = wgpu::util::TextureBlitter::new(&hal.device, format);
427        Ok(Self {
428            surface,
429            config,
430            device: hal.device.clone(),
431            intermediate,
432            intermediate_view,
433            overlay,
434            overlay_view,
435            blitter,
436        })
437    }
438
439    pub fn format(&self) -> wgpu::TextureFormat {
440        self.config.format
441    }
442}
443
444impl Surface for RawSurface {
445    fn size(&self) -> (u32, u32) {
446        (self.config.width, self.config.height)
447    }
448
449    fn resize(&mut self, width: u32, height: u32) {
450        let (w, h) = (width.max(1), height.max(1));
451        // Sin cambio de tamaño NO reconfiguramos. El backend layer-shell de `pata`
452        // llama a `resize` en cada cuadro (no tiene eventos de resize como winit);
453        // reconfigurar el swapchain por cuadro lo reconstruye una y otra vez, y en
454        // Vulkan WSI eso **destruye el `wl_buffer` recién presentado antes de que el
455        // compositor lo componga** — wlroots lo tolera, smithay (mirada) no, y la
456        // superficie queda en negro (el compositor ve `buffer=None`).
457        if self.config.width == w && self.config.height == h {
458            return;
459        }
460        self.config.width = w;
461        self.config.height = h;
462        self.surface.configure(&self.device, &self.config);
463        let (tex, view) = create_intermediate(&self.device, self.config.width, self.config.height);
464        self.intermediate = tex;
465        self.intermediate_view = view;
466        let (otex, oview) =
467            create_intermediate(&self.device, self.config.width, self.config.height);
468        self.overlay = otex;
469        self.overlay_view = oview;
470    }
471
472    fn acquire(&mut self) -> Result<Frame, SurfaceError> {
473        let texture = match self.surface.get_current_texture() {
474            Ok(t) => t,
475            // El backend layer-shell no tiene un evento de resize que reconfigure
476            // el swapchain; si quedó obsoleto/perdido, lo reconstruimos aquí mismo
477            // y reintentamos una vez. Sin esto el panel quedaría en negro para
478            // siempre tras el primer `Outdated`.
479            Err(e @ (wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost)) => {
480                self.surface.configure(&self.device, &self.config);
481                self.surface.get_current_texture().map_err(|_| match e {
482                    wgpu::SurfaceError::Lost => SurfaceError::Lost,
483                    _ => SurfaceError::Outdated,
484                })?
485            }
486            Err(wgpu::SurfaceError::OutOfMemory) => return Err(SurfaceError::OutOfMemory),
487            Err(wgpu::SurfaceError::Timeout) => return Err(SurfaceError::Timeout),
488            Err(other) => return Err(SurfaceError::Other(format!("{other:?}"))),
489        };
490        let surface_view = texture
491            .texture
492            .create_view(&wgpu::TextureViewDescriptor::default());
493        Ok(Frame {
494            surface_texture: texture,
495            surface_view,
496            intermediate_view: self.intermediate_view.clone(),
497            overlay_view: self.overlay_view.clone(),
498            width: self.config.width,
499            height: self.config.height,
500        })
501    }
502
503    fn present(&mut self, frame: Frame, hal: &Hal) {
504        let mut encoder = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
505            label: Some("llimphi-blit-raw"),
506        });
507        self.blitter.copy(
508            &hal.device,
509            &mut encoder,
510            &frame.intermediate_view,
511            &frame.surface_view,
512        );
513        hal.queue.submit(std::iter::once(encoder.finish()));
514        frame.surface_texture.present();
515    }
516}
517
518/// Elige el modo de presentación del swapchain.
519///
520/// Default: **Mailbox** si el driver lo expone, sino **Fifo**. La razón es
521/// el cuelgue observado en las apps Llimphi (investigación 2026-05-30): con
522/// `Fifo`/`AutoVsync`, `surface.get_current_texture()` **bloquea** esperando
523/// el frame-callback del compositor Wayland — si el compositor no suelta un
524/// buffer, el hilo del UI queda dormido (CPU baja, deadlock aparente).
525/// `Mailbox` no bloquea (triple-buffer, descarta frames viejos), así que el
526/// loop nunca se queda esperando al compositor. `Fifo` está garantizado por
527/// spec como fallback.
528///
529/// Override por entorno para A/B sin recompilar (útil en la laptop con
530/// display real): `LLIMPHI_PRESENT_MODE = fifo | mailbox | immediate |
531/// fifo_relaxed`. Si el modo pedido no está soportado, se ignora y se aplica
532/// el default.
533fn choose_present_mode(caps: &wgpu::SurfaceCapabilities) -> wgpu::PresentMode {
534    use wgpu::PresentMode::{Fifo, FifoRelaxed, Immediate, Mailbox};
535    if let Ok(v) = std::env::var("LLIMPHI_PRESENT_MODE") {
536        let want = match v.trim().to_ascii_lowercase().as_str() {
537            "fifo" | "vsync" => Some(Fifo),
538            "fifo_relaxed" | "fiforelaxed" => Some(FifoRelaxed),
539            "mailbox" => Some(Mailbox),
540            "immediate" | "novsync" => Some(Immediate),
541            _ => None,
542        };
543        if let Some(m) = want {
544            if caps.present_modes.contains(&m) {
545                return m;
546            }
547        }
548    }
549    if caps.present_modes.contains(&Mailbox) {
550        Mailbox
551    } else {
552        Fifo
553    }
554}
555
556fn create_intermediate(
557    device: &wgpu::Device,
558    width: u32,
559    height: u32,
560) -> (wgpu::Texture, wgpu::TextureView) {
561    let texture = device.create_texture(&wgpu::TextureDescriptor {
562        label: Some("llimphi-intermediate"),
563        size: wgpu::Extent3d {
564            width,
565            height,
566            depth_or_array_layers: 1,
567        },
568        mip_level_count: 1,
569        sample_count: 1,
570        dimension: wgpu::TextureDimension::D2,
571        format: INTERMEDIATE_FORMAT,
572        // STORAGE_BINDING: vello escribe via compute shader.
573        // TEXTURE_BINDING: el blitter la lee como sampler source.
574        // RENDER_ATTACHMENT: render passes con clear-only (sin vello)
575        //   también escriben acá — desktop drivers lo tolerían sin este
576        //   flag, Adreno con validación estricta rechaza el frame.
577        usage: wgpu::TextureUsages::STORAGE_BINDING
578            | wgpu::TextureUsages::TEXTURE_BINDING
579            | wgpu::TextureUsages::RENDER_ATTACHMENT,
580        view_formats: &[],
581    });
582    let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
583    (texture, view)
584}
585
586/// Compositor de la capa de overlay: alpha-blittea una textura source (el
587/// overlay rasterizado por vello sobre fondo transparente) SOBRE una textura
588/// target (la intermedia, que ya tiene la UI principal + el video pintado por
589/// `gpu_paint`). Resuelve el z-order: sin esto, el blit de `gpu_paint` (video)
590/// queda encima de la capa vello del overlay y los menús se ven por debajo del
591/// video.
592///
593/// Es un pase de pantalla completa (triángulo) que samplea el source y lo
594/// emite con alpha-over. El factor de blend asume alpha **premultiplicado**
595/// (lo que produce vello); si en pantalla los menús se ven con halos oscuros o
596/// transparencia rara, exportar `LLIMPHI_OVERLAY_BLEND=straight` para usar
597/// alpha recto sin recompilar.
598pub struct OverlayCompositor {
599    pipeline: wgpu::RenderPipeline,
600    sampler: wgpu::Sampler,
601    bind_layout: wgpu::BindGroupLayout,
602}
603
604impl OverlayCompositor {
605    pub fn new(device: &wgpu::Device) -> Self {
606        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
607            label: Some("llimphi-overlay-composite"),
608            source: wgpu::ShaderSource::Wgsl(OVERLAY_COMPOSITE_WGSL.into()),
609        });
610        let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
611            label: Some("llimphi-overlay-bgl"),
612            entries: &[
613                wgpu::BindGroupLayoutEntry {
614                    binding: 0,
615                    visibility: wgpu::ShaderStages::FRAGMENT,
616                    ty: wgpu::BindingType::Texture {
617                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
618                        view_dimension: wgpu::TextureViewDimension::D2,
619                        multisampled: false,
620                    },
621                    count: None,
622                },
623                wgpu::BindGroupLayoutEntry {
624                    binding: 1,
625                    visibility: wgpu::ShaderStages::FRAGMENT,
626                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
627                    count: None,
628                },
629            ],
630        });
631        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
632            label: Some("llimphi-overlay-pl"),
633            bind_group_layouts: &[&bind_layout],
634            push_constant_ranges: &[],
635        });
636        // Alpha-over. `src_factor` distingue premultiplicado (One) de recto
637        // (SrcAlpha); el resto es siempre OneMinusSrcAlpha.
638        let straight = std::env::var("LLIMPHI_OVERLAY_BLEND")
639            .map(|v| v.trim().eq_ignore_ascii_case("straight"))
640            .unwrap_or(false);
641        let color_src = if straight {
642            wgpu::BlendFactor::SrcAlpha
643        } else {
644            wgpu::BlendFactor::One
645        };
646        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
647            label: Some("llimphi-overlay-pipe"),
648            layout: Some(&pipeline_layout),
649            vertex: wgpu::VertexState {
650                module: &shader,
651                entry_point: Some("vs"),
652                buffers: &[],
653                compilation_options: Default::default(),
654            },
655            fragment: Some(wgpu::FragmentState {
656                module: &shader,
657                entry_point: Some("fs"),
658                targets: &[Some(wgpu::ColorTargetState {
659                    format: INTERMEDIATE_FORMAT,
660                    blend: Some(wgpu::BlendState {
661                        color: wgpu::BlendComponent {
662                            src_factor: color_src,
663                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
664                            operation: wgpu::BlendOperation::Add,
665                        },
666                        alpha: wgpu::BlendComponent {
667                            src_factor: wgpu::BlendFactor::One,
668                            dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
669                            operation: wgpu::BlendOperation::Add,
670                        },
671                    }),
672                    write_mask: wgpu::ColorWrites::ALL,
673                })],
674                compilation_options: Default::default(),
675            }),
676            primitive: wgpu::PrimitiveState::default(),
677            depth_stencil: None,
678            multisample: wgpu::MultisampleState::default(),
679            multiview: None,
680            cache: None,
681        });
682        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
683            label: Some("llimphi-overlay-sampler"),
684            ..Default::default()
685        });
686        OverlayCompositor {
687            pipeline,
688            sampler,
689            bind_layout,
690        }
691    }
692
693    /// Compone `source` (overlay con fondo transparente) sobre `target` (la
694    /// intermedia), preservando el contenido previo del target (LoadOp::Load)
695    /// y mezclando con alpha. Graba un render pass en `encoder`.
696    pub fn composite(
697        &self,
698        device: &wgpu::Device,
699        encoder: &mut wgpu::CommandEncoder,
700        target: &wgpu::TextureView,
701        source: &wgpu::TextureView,
702    ) {
703        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
704            label: Some("llimphi-overlay-bg"),
705            layout: &self.bind_layout,
706            entries: &[
707                wgpu::BindGroupEntry {
708                    binding: 0,
709                    resource: wgpu::BindingResource::TextureView(source),
710                },
711                wgpu::BindGroupEntry {
712                    binding: 1,
713                    resource: wgpu::BindingResource::Sampler(&self.sampler),
714                },
715            ],
716        });
717        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
718            label: Some("llimphi-overlay-composite-pass"),
719            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
720                view: target,
721                resolve_target: None,
722                ops: wgpu::Operations {
723                    load: wgpu::LoadOp::Load,
724                    store: wgpu::StoreOp::Store,
725                },
726            })],
727            depth_stencil_attachment: None,
728            timestamp_writes: None,
729            occlusion_query_set: None,
730        });
731        pass.set_pipeline(&self.pipeline);
732        pass.set_bind_group(0, &bind_group, &[]);
733        pass.draw(0..3, 0..1);
734    }
735}
736
737/// Pase de pantalla completa que samplea la textura de overlay y la emite
738/// para alpha-over. Triángulo grande que cubre el viewport; UV mapea clip
739/// → texel 1:1 (Y invertida, igual que un blit estándar).
740const OVERLAY_COMPOSITE_WGSL: &str = r#"
741struct VsOut {
742    @builtin(position) pos: vec4<f32>,
743    @location(0) uv: vec2<f32>,
744};
745
746@vertex
747fn vs(@builtin(vertex_index) vi: u32) -> VsOut {
748    var corners = array<vec2<f32>, 3>(
749        vec2<f32>(-1.0, -1.0),
750        vec2<f32>( 3.0, -1.0),
751        vec2<f32>(-1.0,  3.0),
752    );
753    let xy = corners[vi];
754    var out: VsOut;
755    out.pos = vec4<f32>(xy, 0.0, 1.0);
756    out.uv = vec2<f32>((xy.x + 1.0) * 0.5, (1.0 - xy.y) * 0.5);
757    return out;
758}
759
760@group(0) @binding(0) var src_tex: texture_2d<f32>;
761@group(0) @binding(1) var src_samp: sampler;
762
763@fragment
764fn fs(in: VsOut) -> @location(0) vec4<f32> {
765    return textureSample(src_tex, src_samp, in.uv);
766}
767"#;
768
769impl Surface for WinitSurface {
770    fn size(&self) -> (u32, u32) {
771        (self.config.width, self.config.height)
772    }
773
774    fn resize(&mut self, width: u32, height: u32) {
775        self.config.width = width.max(1);
776        self.config.height = height.max(1);
777        self.surface.configure(&self.device, &self.config);
778        let (tex, view) = create_intermediate(&self.device, self.config.width, self.config.height);
779        self.intermediate = tex;
780        self.intermediate_view = view;
781        let (otex, oview) =
782            create_intermediate(&self.device, self.config.width, self.config.height);
783        self.overlay = otex;
784        self.overlay_view = oview;
785    }
786
787    fn acquire(&mut self) -> Result<Frame, SurfaceError> {
788        let texture = self.surface.get_current_texture().map_err(|e| match e {
789            wgpu::SurfaceError::Lost => SurfaceError::Lost,
790            wgpu::SurfaceError::Outdated => SurfaceError::Outdated,
791            wgpu::SurfaceError::OutOfMemory => SurfaceError::OutOfMemory,
792            wgpu::SurfaceError::Timeout => SurfaceError::Timeout,
793            other => SurfaceError::Other(format!("{other:?}")),
794        })?;
795        let surface_view = texture
796            .texture
797            .create_view(&wgpu::TextureViewDescriptor::default());
798        // `TextureView` envuelve un Arc — clonar es atomic-incref, no
799        // recrea la vista. La intermedia sólo cambia en `resize`.
800        Ok(Frame {
801            surface_texture: texture,
802            surface_view,
803            intermediate_view: self.intermediate_view.clone(),
804            overlay_view: self.overlay_view.clone(),
805            width: self.config.width,
806            height: self.config.height,
807        })
808    }
809
810    fn present(&mut self, frame: Frame, hal: &Hal) {
811        let mut encoder = hal.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
812            label: Some("llimphi-blit"),
813        });
814        self.blitter.copy(
815            &hal.device,
816            &mut encoder,
817            &frame.intermediate_view,
818            &frame.surface_view,
819        );
820        hal.queue.submit(std::iter::once(encoder.finish()));
821        frame.surface_texture.present();
822    }
823}