tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0)
1//! **Sonda: qué hace el compositor con una capa que crece y todavía no pintó.**
2//!
3//! El defecto que persigue («la barra del input aparece arriba al desplegar el
4//! cajón, y a media pantalla al abrir el menú de inicio») dura un instante: una
5//! capa anclada ABAJO pide más alto (`set_size` + commit), el compositor
6//! reacomoda su geometría en el acto —el origen salta del pie al techo— y hasta
7//! que llega el buffer nuevo hay al menos un cuadro con el buffer VIEJO. Si el
8//! compositor lo dibuja en la esquina de la geometría nueva, esos 40 px de barra
9//! se ven **arriba de todo**.
10//!
11//! Cazar ese cuadro con pantallazos no funciona: dura menos que una captura, y
12//! no sale siempre. Esta sonda invierte el problema — se mete en ese estado **a
13//! propósito y se queda ahí** los segundos que uno le pida, sobre el compositor
14//! que ya está corriendo, sin reiniciar la sesión:
15//!
16//! 1. crea una capa anclada `BOTTOM|LEFT|RIGHT` de `--fine` px y la pinta;
17//! 2. pide `set_size(0, --large)` y **no pinta el buffer nuevo**;
18//! 3. se queda `--wait` segundos en ese estado (acking los `configure`);
19//! 4. recién entonces pinta el buffer grande y se va.
20//!
21//! Durante (3) el pantallazo contesta la pregunta sin ambigüedad: si la franja
22//! de color se ve **abajo**, el compositor pega el buffer a su borde anclado y el
23//! defecto está en otro lado; si se ve **arriba** (o a media pantalla), el que
24//! falla es el emplazamiento de la capa. Los dos colores son distintos a
25//! propósito: el fino es magenta, el grande es cian, así ninguna captura los
26//! confunde.
27//!
28//! ```bash
29//! cargo run -p llimphi-layer --example sonda_capa_crece -- --fine 40 --large 800 --wait 20
30//! ```
31
32use smithay_client_toolkit::{
33 compositor::{CompositorHandler, CompositorState},
34 delegate_compositor, delegate_layer, delegate_output, delegate_registry, delegate_shm,
35 output::{OutputHandler, OutputState},
36 registry::{ProvidesRegistryState, RegistryState},
37 registry_handlers,
38 shell::{
39 wlr_layer::{
40 Anchor, KeyboardInteractivity, Layer, LayerShell, LayerShellHandler, LayerSurface,
41 LayerSurfaceConfigure,
42 },
43 WaylandSurface,
44 },
45 shm::{slot::SlotPool, Shm, ShmHandler},
46};
47use std::time::{Duration, Instant};
48use wayland_client::{
49 globals::registry_queue_init,
50 protocol::{wl_output, wl_shm, wl_surface},
51 Connection, QueueHandle,
52};
53
54/// En qué tramo de la secuencia va la sonda.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56enum Paso {
57 /// Pintó la franja fina; espera para pedir el alto grande.
58 Fino,
59 /// Pidió el alto grande y **no** pinta: es el estado que se investiga.
60 Creciendo,
61 /// Pintó el buffer grande; espera para irse.
62 Grande,
63}
64
65struct Sonda {
66 registry: RegistryState,
67 output: OutputState,
68 shm: Shm,
69 pool: SlotPool,
70 capa: LayerSurface,
71 ancho: u32,
72 /// Alto del buffer que hay pintado AHORA (no el configurado).
73 alto_buffer: u32,
74 fino: u32,
75 grande: u32,
76 paso: Paso,
77 desde: Instant,
78 t0: Instant,
79 espera: u64,
80 salir: bool,
81 /// Dónde dejar los pantallazos que la sonda se saca a sí misma, y cuáles ya
82 /// sacó. Que los saque ELLA es la mitad que importa: un `sleep` de afuera
83 /// nunca cae dentro del estado, y el estado es todo el punto de la sonda.
84 shots: Option<String>,
85 shot_hecho: [bool; 3],
86 /// Último tamaño que mandó el compositor por `configure`.
87 ultimo_configure: (u32, u32),
88 /// Hay que encadenar otro frame-callback en esta vuelta del bucle.
89 pide_cuadro: bool,
90}
91
92impl Sonda {
93 /// Dispara `hapiy` sin esperarlo: el pantallazo tiene que salir DENTRO del
94 /// estado, y bloquear el bucle acá lo dejaría sin acking mientras tanto.
95 fn pantallazo(&mut self, n: usize, nombre: &str) {
96 if self.shot_hecho[n] {
97 return;
98 }
99 self.shot_hecho[n] = true;
100 let Some(dir) = self.shots.clone() else { return };
101 let ruta = format!("{dir}/{nombre}.png");
102 match std::process::Command::new("hapiy").arg("-o").arg(&ruta).spawn() {
103 Ok(_) => self.traza(&format!("pantallazo → {ruta}")),
104 Err(e) => eprintln!("sonda: no pude sacar el pantallazo: {e}"),
105 }
106 }
107
108 fn traza(&self, que: &str) {
109 println!(
110 "[{:>7.3}s] {que} · configure={}x{} buffer={}x{} paso={:?}",
111 self.t0.elapsed().as_secs_f32(),
112 self.ultimo_configure.0,
113 self.ultimo_configure.1,
114 self.ancho,
115 self.alto_buffer,
116 self.paso
117 );
118 }
119
120 /// Pinta una franja opaca del alto pedido: relleno + un borde blanco de 2 px
121 /// para que el canto se distinga del fondo en cualquier pantallazo.
122 fn pintar(&mut self, alto: u32, (r, g, b): (u8, u8, u8)) {
123 let (w, h) = (self.ancho.max(1), alto.max(1));
124 let stride = w as i32 * 4;
125 let Ok((buffer, lienzo)) =
126 self.pool.create_buffer(w as i32, h as i32, stride, wl_shm::Format::Argb8888)
127 else {
128 eprintln!("sonda: no pude crear el buffer {w}x{h}");
129 return;
130 };
131 for y in 0..h {
132 for x in 0..w {
133 let borde = y < 2 || y + 2 >= h || x < 2 || x + 2 >= w;
134 let px = ((y * w + x) * 4) as usize;
135 let (cr, cg, cb) = if borde { (255, 255, 255) } else { (r, g, b) };
136 // Argb8888 en little-endian: B, G, R, A.
137 lienzo[px] = cb;
138 lienzo[px + 1] = cg;
139 lienzo[px + 2] = cr;
140 lienzo[px + 3] = 255;
141 }
142 }
143 let surface = self.capa.wl_surface();
144 surface.damage_buffer(0, 0, w as i32, h as i32);
145 let _ = buffer.attach_to(surface);
146 surface.commit();
147 self.alto_buffer = h;
148 self.pide_cuadro = true;
149 }
150}
151
152impl CompositorHandler for Sonda {
153 fn scale_factor_changed(
154 &mut self,
155 _: &Connection,
156 _: &QueueHandle<Self>,
157 _: &wl_surface::WlSurface,
158 _: i32,
159 ) {
160 }
161 fn transform_changed(
162 &mut self,
163 _: &Connection,
164 _: &QueueHandle<Self>,
165 _: &wl_surface::WlSurface,
166 _: wl_output::Transform,
167 ) {
168 }
169 fn frame(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &wl_surface::WlSurface, _: u32) {
170 // Encadenar el pedido es lo que mantiene vivo el bucle. Es un commit
171 // **sin buffer**: durante «Creciendo» no puede colarse una pintada, que
172 // es la condición entera del experimento.
173 self.pide_cuadro = true;
174 }
175 fn surface_enter(
176 &mut self,
177 _: &Connection,
178 _: &QueueHandle<Self>,
179 _: &wl_surface::WlSurface,
180 _: &wl_output::WlOutput,
181 ) {
182 }
183 fn surface_leave(
184 &mut self,
185 _: &Connection,
186 _: &QueueHandle<Self>,
187 _: &wl_surface::WlSurface,
188 _: &wl_output::WlOutput,
189 ) {
190 }
191}
192
193impl OutputHandler for Sonda {
194 fn output_state(&mut self) -> &mut OutputState {
195 &mut self.output
196 }
197 fn new_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
198 fn update_output(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
199 fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_output::WlOutput) {}
200}
201
202impl ShmHandler for Sonda {
203 fn shm_state(&mut self) -> &mut Shm {
204 &mut self.shm
205 }
206}
207
208impl LayerShellHandler for Sonda {
209 fn closed(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &LayerSurface) {
210 self.salir = true;
211 }
212
213 fn configure(
214 &mut self,
215 _: &Connection,
216 _: &QueueHandle<Self>,
217 _: &LayerSurface,
218 cfg: LayerSurfaceConfigure,
219 _: u32,
220 ) {
221 let (w, h) = cfg.new_size;
222 self.ultimo_configure = (w, h);
223 if w > 0 {
224 self.ancho = w;
225 }
226 match self.paso {
227 // El primer `configure` es el que nos dice el ancho: recién ahí se
228 // puede pintar la franja fina.
229 Paso::Fino if self.alto_buffer == 0 => {
230 self.traza("configure inicial → pinto el fino (magenta)");
231 let fino = self.fino;
232 self.pintar(fino, (255, 0, 200));
233 }
234 // **El corazón de la sonda.** Llegó el `configure` con el alto grande
235 // y NO pintamos: así es exactamente como está la barra de pata durante
236 // el cuadro que se ve mal, pero sostenido en el tiempo.
237 Paso::Creciendo => {
238 self.traza("configure GRANDE — NO pinto (estado bajo observación)");
239 }
240 _ => self.traza("configure"),
241 }
242 }
243}
244
245delegate_compositor!(Sonda);
246delegate_output!(Sonda);
247delegate_shm!(Sonda);
248delegate_layer!(Sonda);
249delegate_registry!(Sonda);
250
251impl ProvidesRegistryState for Sonda {
252 fn registry(&mut self) -> &mut RegistryState {
253 &mut self.registry
254 }
255 registry_handlers![OutputState];
256}
257
258fn arg(nombre: &str, def: u64) -> u64 {
259 let args: Vec<String> = std::env::args().collect();
260 args.iter()
261 .position(|a| a == nombre)
262 .and_then(|i| args.get(i + 1))
263 .and_then(|v| v.parse().ok())
264 .unwrap_or(def)
265}
266
267fn main() {
268 let fino = arg("--fine", 40) as u32;
269 let grande = arg("--large", 800) as u32;
270 let espera = arg("--wait", 20);
271
272 let conn = Connection::connect_to_env().expect("sin compositor wayland");
273 let (globals, mut cola) = registry_queue_init(&conn).expect("registry");
274 let qh = cola.handle();
275
276 let compositor = CompositorState::bind(&globals, &qh).expect("wl_compositor");
277 let layer_shell = LayerShell::bind(&globals, &qh).expect("zwlr_layer_shell_v1");
278 let shm = Shm::bind(&globals, &qh).expect("wl_shm");
279 let pool = SlotPool::new(4096 * 64 * 4, &shm).expect("slot pool");
280
281 let surface = compositor.create_surface(&qh);
282 let capa = layer_shell.create_layer_surface(
283 &qh,
284 surface,
285 Layer::Overlay,
286 Some("sonda-capa-crece"),
287 None,
288 );
289 // Misma postura que la barra de pata: anclada ABAJO y a los dos costados,
290 // grosor fijo, sin zona exclusiva (flota sobre lo que haya).
291 capa.set_anchor(Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT);
292 capa.set_size(0, fino);
293 capa.set_exclusive_zone(0);
294 capa.set_keyboard_interactivity(KeyboardInteractivity::None);
295 capa.commit();
296
297 let ahora = Instant::now();
298 let mut sonda = Sonda {
299 registry: RegistryState::new(&globals),
300 output: OutputState::new(&globals, &qh),
301 shm,
302 pool,
303 capa,
304 ancho: 0,
305 alto_buffer: 0,
306 fino,
307 grande,
308 paso: Paso::Fino,
309 desde: ahora,
310 t0: ahora,
311 espera,
312 salir: false,
313 shots: std::env::args()
314 .position(|a| a == "--shots")
315 .and_then(|i| std::env::args().nth(i + 1)),
316 shot_hecho: [false; 3],
317 ultimo_configure: (0, 0),
318 pide_cuadro: false,
319 };
320
321 println!("sonda-capa-crece · fino={fino}px grande={grande}px espera={espera}s");
322 println!(" franja MAGENTA = el buffer fino · CIAN = el buffer grande");
323 println!(" la pregunta: durante «Creciendo», ¿el magenta se ve ABAJO o ARRIBA?");
324
325 // El primer callback: de acá en más se re-pide solo.
326 sonda.pide_cuadro = true;
327 while !sonda.salir {
328 if std::mem::take(&mut sonda.pide_cuadro) {
329 let s = sonda.capa.wl_surface().clone();
330 s.frame(&qh, s.clone());
331 s.commit();
332 }
333 cola.blocking_dispatch(&mut sonda).expect("dispatch");
334 match sonda.paso {
335 Paso::Fino if sonda.alto_buffer > 0 && sonda.desde.elapsed() >= Duration::from_secs(2) && !sonda.shot_hecho[0] => {
336 sonda.pantallazo(0, "sonda-fino");
337 }
338 Paso::Fino if sonda.alto_buffer > 0 && sonda.desde.elapsed() >= Duration::from_secs(3) => {
339 sonda.paso = Paso::Creciendo;
340 sonda.desde = Instant::now();
341 // El pedido, SIN buffer nuevo: es el commit que mueve la geometría.
342 sonda.capa.set_size(0, sonda.grande);
343 sonda.capa.commit();
344 sonda.traza("PEDÍ el alto grande (sin pintar) — estado bajo observación");
345 }
346 // Un segundo adentro del estado: ya se asentó el `configure` y el
347 // buffer sigue siendo el chico. Éste es EL pantallazo.
348 Paso::Creciendo if sonda.desde.elapsed() >= Duration::from_secs(1) && !sonda.shot_hecho[1] => {
349 sonda.pantallazo(1, "sonda-creciendo");
350 }
351 Paso::Creciendo if sonda.desde.elapsed() >= Duration::from_secs(sonda.espera) => {
352 sonda.paso = Paso::Grande;
353 sonda.desde = Instant::now();
354 let grande = sonda.grande;
355 sonda.pintar(grande, (0, 220, 255));
356 sonda.traza("pinté el buffer grande (cian)");
357 }
358 Paso::Grande if sonda.desde.elapsed() >= Duration::from_secs(1) && !sonda.shot_hecho[2] => {
359 sonda.pantallazo(2, "sonda-grande");
360 }
361 Paso::Grande if sonda.desde.elapsed() >= Duration::from_secs(3) => {
362 sonda.traza("listo, me voy");
363 sonda.salir = true;
364 }
365 _ => {}
366 }
367 }
368}