tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definiciones

⎇ main llimphi-ui/src/content_type.rs Rust · 223 líneas · 10.0 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1//! Pista `wp_content_type_v1` — la app le dice al compositor **qué** está
2//! mostrando (foto / video / juego), para que él decida su política.
3//!
4//! **Por qué existe.** El compositor toma decisiones caras sin saber qué hay en
5//! pantalla: mirada anima su fondo de marca, y ese latido fuerza una
6//! recomposición completa. El 2026-07-30 se midió que eso hundía al compositor a
7//! ~18 render/s y dejaba a `media` por debajo de los 24 fps del clip, hasta que
8//! su watchdog de A/V respawneaba ffmpeg cada ~13 s. mirada ya sabe ceder ante
9//! contenido con ritmo propio (`fondo_sin_publico`, y también `apply_vrr` /
10//! `apply_tearing`), pero **nadie se lo declaraba**: la pista es este protocolo,
11//! y winit no lo habla.
12//!
13//! **Cómo cruza el muro winit↔wayland.** winit es dueño de la conexión y no
14//! expone su registry, así que se monta una segunda `Connection` sobre el
15//! **mismo** `wl_display` (`Backend::from_foreign_display`) y se envuelve la
16//! `wl_surface` de winit por `ObjectId::from_ptr`. Es exactamente el patrón que
17//! validó el spike `llimphi-video-plane` para meterle un subsurface a una
18//! ventana winit. Los objetos nuevos van a nuestra propia cola de eventos, así
19//! que no le robamos nada a winit.
20//!
21//! **Cuándo surte efecto.** `set_content_type` es estado de superficie
22//! **doble-buffereado**: el compositor recién lo lee en el próximo `commit` (así
23//! lo hace mirada, desde el estado cacheado). No commiteamos nosotros —sería
24//! pisarle el turno a wgpu—, así que la pista viaja con el siguiente cuadro que
25//! pinte la app. Para un reproductor eso es inmediato; una app quieta la aplica
26//! cuando repinte.
27
28#[cfg(target_os = "linux")]
29mod imp {
30    use std::cell::RefCell;
31    use std::mem::ManuallyDrop;
32
33    use llimphi_hal::winit::raw_window_handle::{
34        HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle,
35    };
36    use llimphi_hal::winit::window::Window;
37    use wayland_client::backend::{Backend, ObjectId};
38    use wayland_client::protocol::wl_registry;
39    use wayland_client::protocol::wl_surface::WlSurface;
40    use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle};
41    use wayland_protocols::wp::content_type::v1::client::wp_content_type_manager_v1::WpContentTypeManagerV1;
42    use wayland_protocols::wp::content_type::v1::client::wp_content_type_v1::{
43        Type as WpType, WpContentTypeV1,
44    };
45
46    use crate::ContentType;
47
48    #[derive(Default)]
49    struct Globals {
50        manager: Option<WpContentTypeManagerV1>,
51    }
52
53    impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
54        fn event(
55            state: &mut Self,
56            registry: &wl_registry::WlRegistry,
57            event: wl_registry::Event,
58            _: &(),
59            _: &Connection,
60            qh: &QueueHandle<Self>,
61        ) {
62            if let wl_registry::Event::Global { name, interface, version } = event {
63                if interface == WpContentTypeManagerV1::interface().name {
64                    state.manager = Some(registry.bind(name, version.min(1), qh, ()));
65                }
66            }
67        }
68    }
69
70    // Ninguna de las dos interfaces emite eventos; el `Dispatch` es puro trámite.
71    wayland_client::delegate_noop!(Globals: ignore WpContentTypeManagerV1);
72    wayland_client::delegate_noop!(Globals: ignore WpContentTypeV1);
73
74    /// **Por qué los cuatro campos de wayland van en `ManuallyDrop`.** Todo
75    /// esto cuelga del `wl_display` de winit, que es prestado. El `Drop` de la
76    /// conexión llama `wl_proxy_destroy` por cada proxy vivo, y este `Puente`
77    /// vive en un `thread_local` del hilo principal: su destructor corre
78    /// **después** de que `main` volvió, o sea después de que winit cerró el
79    /// display. Eso es exactamente el `SIGSEGV` que el hilo de `file_drop`
80    /// estaba dando de verdad al cerrar cualquier ventana (medido el
81    /// 2026-09-03: 4 de 8). Acá todavía no se vio porque el puente sólo nace
82    /// en las apps que declaran tipo de contenido —las de video—, pero es la
83    /// misma bomba con la mecha más corta. No destruir nada es lo correcto:
84    /// cuando el display se va, se lleva los proxies igual.
85    struct Puente {
86        conn: ManuallyDrop<Connection>,
87        queue: ManuallyDrop<EventQueue<Globals>>,
88        globals: ManuallyDrop<Globals>,
89        ct: ManuallyDrop<WpContentTypeV1>,
90        /// Puntero de la `wl_surface` sobre la que se creó, para detectar que la
91        /// ventana cambió por debajo (suspend/resume recrea la superficie).
92        surface_ptr: usize,
93        /// `None` = todavía no se declaró nada. Que el PRIMER valor viaje
94        /// siempre —aunque sea `None`— deja al compositor y a la app en el
95        /// mismo estado sin depender de cuál es el default del protocolo.
96        ultimo: Option<ContentType>,
97    }
98
99    impl Puente {
100        fn nuevo(window: &Window) -> Result<Self, String> {
101            let RawDisplayHandle::Wayland(dh) = window
102                .display_handle()
103                .map_err(|e| format!("display_handle: {e}"))?
104                .as_raw()
105            else {
106                return Err("no estamos bajo Wayland".into());
107            };
108            let RawWindowHandle::Wayland(wh) = window
109                .window_handle()
110                .map_err(|e| format!("window_handle: {e}"))?
111                .as_raw()
112            else {
113                return Err("no estamos bajo Wayland".into());
114            };
115
116            // SAFETY: el puntero sale del display handle vivo de winit, que es
117            // dueño de la conexión y la mantiene abierta mientras exista la
118            // ventana. `from_foreign_display` no toma posesión: comparte.
119            let backend = unsafe { Backend::from_foreign_display(dh.display.as_ptr().cast()) };
120            let conn = Connection::from_backend(backend);
121            let mut queue: EventQueue<Globals> = conn.new_event_queue();
122            let qh = queue.handle();
123            let _registry = conn.display().get_registry(&qh, ());
124            let mut globals = Globals::default();
125            queue
126                .roundtrip(&mut globals)
127                .map_err(|e| format!("roundtrip del registry: {e}"))?;
128            let manager = globals
129                .manager
130                .clone()
131                .ok_or("el compositor no expone wp_content_type_manager_v1")?;
132
133            let surface_ptr = wh.surface.as_ptr();
134            // SAFETY: mismo razonamiento — la `wl_surface` es la de winit, viva
135            // mientras viva la ventana, y sobre el mismo backend.
136            let id = unsafe { ObjectId::from_ptr(WlSurface::interface(), surface_ptr.cast()) }
137                .map_err(|e| format!("ObjectId::from_ptr(surface): {e}"))?;
138            let surface = WlSurface::from_id(&conn, id)
139                .map_err(|e| format!("WlSurface::from_id: {e}"))?;
140            let ct = manager.get_surface_content_type(&surface, &qh, ());
141
142            Ok(Self {
143                conn: ManuallyDrop::new(conn),
144                queue: ManuallyDrop::new(queue),
145                globals: ManuallyDrop::new(globals),
146                ct: ManuallyDrop::new(ct),
147                surface_ptr: surface_ptr as usize,
148                ultimo: None,
149            })
150        }
151
152        fn declarar(&mut self, kind: ContentType) {
153            if self.ultimo == Some(kind) {
154                return; // el protocolo es idempotente, pero no gastamos el viaje.
155            }
156            self.ultimo = Some(kind);
157            self.ct.set_content_type(match kind {
158                ContentType::None => WpType::None,
159                ContentType::Photo => WpType::Photo,
160                ContentType::Video => WpType::Video,
161                ContentType::Game => WpType::Game,
162            });
163            // La petición sale ya; el compositor la aplicará en el próximo commit
164            // de la app (ver la nota del módulo).
165            let _ = self.conn.flush();
166            // Drenar lo que el registry haya mandado (globals nuevos), para que la
167            // cola no crezca sin que nadie la lea nunca.
168            let _ = self.queue.dispatch_pending(&mut *self.globals);
169        }
170    }
171
172    thread_local! {
173        /// `None` = todavía no se intentó. `Some(None)` = no hay puente posible
174        /// (no es Wayland, o el compositor no expone el global) y **no se
175        /// reintenta**: no tiene sentido pagar un roundtrip por cuadro.
176        /// `Some(Some(_))` = puente vivo.
177        static PUENTE: RefCell<Option<Option<Puente>>> = const { RefCell::new(None) };
178    }
179
180    pub(crate) fn aplicar(window: &Window, kind: ContentType) {
181        PUENTE.with(|slot| {
182            let mut slot = slot.borrow_mut();
183            // Si la superficie cambió bajo nuestros pies (resume recrea la
184            // ventana), el puente viejo apunta a un objeto muerto: se rehace.
185            if let Some(Some(p)) = slot.as_ref() {
186                let actual = window
187                    .window_handle()
188                    .ok()
189                    .and_then(|h| match h.as_raw() {
190                        RawWindowHandle::Wayland(wh) => Some(wh.surface.as_ptr() as usize),
191                        _ => None,
192                    });
193                if actual != Some(p.surface_ptr) {
194                    *slot = None;
195                }
196            }
197            let entrada = slot.get_or_insert_with(|| match Puente::nuevo(window) {
198                Ok(p) => Some(p),
199                Err(e) => {
200                    // Informativo, una sola vez: no es un fallo de la app. Bajo
201                    // X11 o un compositor sin el global es lo esperado.
202                    llimphi_hal::avisar!("llimphi: sin pista de tipo de contenido ({e})");
203                    None
204                }
205            });
206            if let Some(p) = entrada.as_mut() {
207                p.declarar(kind);
208            }
209        });
210    }
211}
212
213#[cfg(not(target_os = "linux"))]
214mod imp {
215    use llimphi_hal::winit::window::Window;
216
217    use crate::ContentType;
218
219    /// Fuera de Linux no hay Wayland: la pista no existe y declarar es un no-op.
220    pub(crate) fn aplicar(_window: &Window, _kind: ContentType) {}
221}
222
223pub(crate) use imp::aplicar;