tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual
1// =============================================================================
2// llimphi-registro :: la escena de una app, escrita a un archivo
3// -----------------------------------------------------------------------------
4// Esto NO es un formato nuevo. `llimphi-wire-escena` ya define qué es un cuadro
5// y `llimphi-cable` ya lo enmarca para mandarlo por un socket; acá el mismo
6// cuadro va a un archivo, con el tiempo al lado. El reproductor del otro lado
7// tampoco es nuevo: es `llimphi-reproductor`, el mismo que usa el anfitrión
8// vivo y el kernel de wawa.
9//
10// Para qué: un registro es un MASTER. Se rasteriza después, offline, a la
11// resolución que haga falta, con el tema que haga falta, y de ahí sale el mp4.
12// Una captura de pantalla ya está revelada — cambiar el tema pide volver a
13// grabar con la persona delante. Un registro se vuelve a revelar solo.
14//
15// ## El formato, entero
16//
17// MAGIA (6) VERSION u16 LE
18// [u32 LE n][n bytes postcard de `Cabecera`]
19// ── y después, uno por cuadro: ──
20// [u32 LE n][u32 LE t_ms][n bytes postcard de `EscenaWire`]
21//
22// El `t_ms` va AFUERA del postcard y no adentro de un struct `Marco`, por una
23// razón que se paga en cada cuadro: `info` y el deduplicado necesitan comparar
24// y medir el cuerpo **sin deserializar la escena**, que es la parte cara. Con
25// el tiempo adentro, cada medición sería un parseo completo del cuadro.
26//
27// ## Por qué el cuadro repetido no se escribe
28//
29// Un escritorio pasa la mayor parte del tiempo quieto, y el huésped compone
30// cuando pasa algo — pero un `update` que no cambia nada visible igual manda su
31// cuadro. Escribirlo sería pagar una escena entera por un píxel idéntico. Se
32// compara el cuerpo serializado contra el anterior y, si es el mismo, se cuenta
33// y no se escribe: el que reproduce sostiene el último hasta el `t_ms`
34// siguiente, así que **no se pierde nada** — es la misma película con menos
35// bytes. Ésa es toda la compresión que hay acá, y es la que importa.
36//
37// Lo que este crate NO hace: no dibuja (eso es `llimphi-reproductor`), no habla
38// por ningún socket (eso es `llimphi-cable`) y no sabe qué es ffmpeg (eso es
39// `shared/foreign-av`). Enmarca, ordena por tiempo y cuenta.
40// =============================================================================
41
42#![forbid(unsafe_code)]
43
44use std::io::{self, Read, Write};
45
46use llimphi_wire_escena::EscenaWire;
47use serde::{Deserialize, Serialize};
48
49/// Los seis bytes del frente. Sirven para lo de siempre: que abrir el archivo
50/// equivocado falle **en la primera línea** y no veinte cuadros después con un
51/// postcard que se deserializa a cualquier cosa.
52pub const MAGIA: [u8; 6] = *b"LLSCN\0";
53
54/// Versión del contenedor. Sube cuando cambia la disposición de los bloques —
55/// no cuando cambia `EscenaWire`, que tiene su propia compatibilidad.
56pub const VERSION: u16 = 1;
57
58/// Tope de un cuadro, en bytes. Mismo criterio que `llimphi_cable::MAX_MARCO`:
59/// no es un límite de diseño sino la guarda contra un `u32` de basura que
60/// pediría alocar cuatro gigas antes de darse cuenta de que el archivo está
61/// roto.
62pub const MAX_CUADRO: usize = 64 * 1024 * 1024;
63
64/// Lo que hay que saber del registro antes de mirar un solo cuadro.
65///
66/// El tamaño va acá y no en cada cuadro porque el huésped compone en su propio
67/// origen `(0,0)` y no sabe dónde lo pusieron: el rectángulo es del que grabó.
68#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
69pub struct Cabecera {
70 /// Ancho del tablado en píxeles lógicos.
71 pub ancho: u32,
72 /// Alto del tablado en píxeles lógicos.
73 pub alto: u32,
74 /// A qué cadencia se pensó revelar esto. Es una sugerencia: los cuadros
75 /// llevan su `t_ms` y `film` remuestrea a los fps que le pidan.
76 pub fps_sugerido: u32,
77 /// Qué se grabó, para que el archivo se explique solo dentro de un año.
78 pub app: String,
79 /// Con qué tema se compuso. Anotado y no impuesto: el mismo registro se
80 /// puede revelar con otro, y saber cuál era el original es el dato que
81 /// después no está en ninguna parte.
82 pub tema: String,
83}
84
85impl Default for Cabecera {
86 fn default() -> Self {
87 Self {
88 ancho: 1280,
89 alto: 720,
90 fps_sugerido: 30,
91 app: String::new(),
92 tema: String::new(),
93 }
94 }
95}
96
97fn invalido<E: core::fmt::Display>(e: E) -> io::Error {
98 io::Error::new(io::ErrorKind::InvalidData, e.to_string())
99}
100
101// ─────────────────────────────────────────────────────────────────────────────
102// Grabar
103// ─────────────────────────────────────────────────────────────────────────────
104
105/// Escribe un registro. Genérico sobre el destino para que los tests no toquen
106/// el disco: un `Vec<u8>` es un destino igual de válido que un archivo.
107pub struct Grabador<W: Write> {
108 destino: W,
109 cuadros: u64,
110 repetidos: u64,
111 bytes: u64,
112 /// El cuerpo del último cuadro ESCRITO, para el deduplicado. `None` hasta
113 /// el primero: un `Vec` vacío como centinela haría que una escena que
114 /// serializa vacía se tragara el primer cuadro sin decir nada.
115 ultimo: Option<Vec<u8>>,
116 ultimo_t: u32,
117}
118
119impl<W: Write> Grabador<W> {
120 /// Abre el registro: magia, versión y cabecera.
121 pub fn nuevo(mut destino: W, cabecera: &Cabecera) -> io::Result<Self> {
122 destino.write_all(&MAGIA)?;
123 destino.write_all(&VERSION.to_le_bytes())?;
124 let cuerpo = postcard::to_stdvec(cabecera).map_err(invalido)?;
125 destino.write_all(&(cuerpo.len() as u32).to_le_bytes())?;
126 destino.write_all(&cuerpo)?;
127 Ok(Self {
128 destino,
129 cuadros: 0,
130 repetidos: 0,
131 bytes: 0,
132 ultimo: None,
133 ultimo_t: 0,
134 })
135 }
136
137 /// Anota un cuadro en `t_ms`. Devuelve `false` si era idéntico al anterior
138 /// y no se escribió — que es lo normal y no un error.
139 pub fn cuadro(&mut self, t_ms: u32, escena: &EscenaWire) -> io::Result<bool> {
140 let cuerpo = postcard::to_stdvec(escena).map_err(invalido)?;
141 if cuerpo.len() > MAX_CUADRO {
142 return Err(invalido(format!(
143 "cuadro de {} bytes, tope {MAX_CUADRO}",
144 cuerpo.len()
145 )));
146 }
147 if self.ultimo.as_deref() == Some(cuerpo.as_slice()) {
148 self.repetidos += 1;
149 return Ok(false);
150 }
151 self.destino.write_all(&(cuerpo.len() as u32).to_le_bytes())?;
152 self.destino.write_all(&t_ms.to_le_bytes())?;
153 self.destino.write_all(&cuerpo)?;
154 self.cuadros += 1;
155 self.bytes += cuerpo.len() as u64;
156 self.ultimo_t = t_ms;
157 self.ultimo = Some(cuerpo);
158 Ok(true)
159 }
160
161 /// Cuadros efectivamente escritos.
162 pub fn cuadros(&self) -> u64 {
163 self.cuadros
164 }
165
166 /// Cuadros que llegaron y no se escribieron por idénticos. Es el número que
167 /// dice cuánto ahorró el deduplicado, y sin él «grabó poco» y «no cambió
168 /// nada» se ven igual.
169 pub fn repetidos(&self) -> u64 {
170 self.repetidos
171 }
172
173 /// Bytes de escena escritos (sin contar cabecera ni marcos).
174 pub fn bytes(&self) -> u64 {
175 self.bytes
176 }
177
178 /// El `t_ms` del último cuadro escrito: la duración útil del registro.
179 pub fn ultimo_t_ms(&self) -> u32 {
180 self.ultimo_t
181 }
182
183 /// Cierra y devuelve el destino, para que el llamador lo sincronice.
184 pub fn cerrar(mut self) -> io::Result<W> {
185 self.destino.flush()?;
186 Ok(self.destino)
187 }
188}
189
190// ─────────────────────────────────────────────────────────────────────────────
191// Leer
192// ─────────────────────────────────────────────────────────────────────────────
193
194/// Lee un registro cuadro por cuadro.
195pub struct Lector<R: Read> {
196 origen: R,
197}
198
199impl<R: Read> Lector<R> {
200 /// Abre y valida el frente. Devuelve la cabecera y el lector parado en el
201 /// primer cuadro.
202 pub fn abrir(mut origen: R) -> io::Result<(Cabecera, Self)> {
203 let mut magia = [0u8; 6];
204 origen.read_exact(&mut magia)?;
205 if magia != MAGIA {
206 return Err(invalido("esto no es un registro de escena (magia)"));
207 }
208 let mut v = [0u8; 2];
209 origen.read_exact(&mut v)?;
210 let version = u16::from_le_bytes(v);
211 if version != VERSION {
212 return Err(invalido(format!(
213 "registro versión {version}, este binario lee la {VERSION}"
214 )));
215 }
216 let mut n = [0u8; 4];
217 origen.read_exact(&mut n)?;
218 let n = u32::from_le_bytes(n) as usize;
219 if n > MAX_CUADRO {
220 return Err(invalido("cabecera absurda"));
221 }
222 let mut cuerpo = vec![0u8; n];
223 origen.read_exact(&mut cuerpo)?;
224 let cab: Cabecera = postcard::from_bytes(&cuerpo).map_err(invalido)?;
225 Ok((cab, Self { origen }))
226 }
227
228 /// El próximo cuadro **sin deserializar**: `(t_ms, cuerpo)`.
229 ///
230 /// Es el que usa `info`, y por eso existe: medir un registro de mil cuadros
231 /// no tiene por qué costar mil parseos de escena.
232 pub fn crudo(&mut self) -> Option<io::Result<(u32, Vec<u8>)>> {
233 let mut n = [0u8; 4];
234 // El final del archivo es un final, no un error: acá se distingue «no
235 // hay más» de «se cortó en el medio», que son cosas distintas y una de
236 // las dos hay que reportarla.
237 match self.origen.read_exact(&mut n) {
238 Ok(()) => {}
239 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return None,
240 Err(e) => return Some(Err(e)),
241 }
242 let n = u32::from_le_bytes(n) as usize;
243 if n > MAX_CUADRO {
244 return Some(Err(invalido(format!("cuadro de {n} bytes, tope {MAX_CUADRO}"))));
245 }
246 let mut t = [0u8; 4];
247 if let Err(e) = self.origen.read_exact(&mut t) {
248 return Some(Err(e));
249 }
250 let mut cuerpo = vec![0u8; n];
251 if let Err(e) = self.origen.read_exact(&mut cuerpo) {
252 return Some(Err(e));
253 }
254 Some(Ok((u32::from_le_bytes(t), cuerpo)))
255 }
256
257 /// El próximo cuadro, ya escena.
258 pub fn siguiente(&mut self) -> Option<io::Result<(u32, EscenaWire)>> {
259 match self.crudo()? {
260 Ok((t, cuerpo)) => Some(
261 postcard::from_bytes::<EscenaWire>(&cuerpo)
262 .map(|e| (t, e))
263 .map_err(invalido),
264 ),
265 Err(e) => Some(Err(e)),
266 }
267 }
268}
269
270// ─────────────────────────────────────────────────────────────────────────────
271// Medir
272// ─────────────────────────────────────────────────────────────────────────────
273
274/// Lo que pesa un registro, para poder compararlo con un video sin creerle a
275/// nadie. Es la Regla 8 aplicada a esta pregunta: «¿sale más liviano que un
276/// mp4?» se contesta con bytes por segundo, no con una opinión.
277#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
278pub struct Resumen {
279 pub cuadros: u64,
280 pub bytes: u64,
281 pub menor: usize,
282 pub mayor: usize,
283 pub dur_ms: u32,
284}
285
286impl Resumen {
287 /// Bytes por cuadro, promedio.
288 pub fn por_cuadro(&self) -> u64 {
289 if self.cuadros == 0 {
290 0
291 } else {
292 self.bytes / self.cuadros
293 }
294 }
295
296 /// Bytes por segundo de película. **Éste** es el número que se carea contra
297 /// el bitrate de un mp4; los bytes por cuadro no dicen nada si el
298 /// escritorio estuvo quieto media hora.
299 pub fn por_segundo(&self) -> u64 {
300 if self.dur_ms == 0 {
301 0
302 } else {
303 self.bytes * 1000 / self.dur_ms as u64
304 }
305 }
306}
307
308/// Recorre el registro entero contando. No deserializa ninguna escena.
309pub fn resumir<R: Read>(lector: &mut Lector<R>) -> io::Result<Resumen> {
310 let mut r = Resumen {
311 menor: usize::MAX,
312 ..Default::default()
313 };
314 while let Some(paso) = lector.crudo() {
315 let (t, cuerpo) = paso?;
316 r.cuadros += 1;
317 r.bytes += cuerpo.len() as u64;
318 r.menor = r.menor.min(cuerpo.len());
319 r.mayor = r.mayor.max(cuerpo.len());
320 r.dur_ms = r.dur_ms.max(t);
321 }
322 if r.cuadros == 0 {
323 r.menor = 0;
324 }
325 Ok(r)
326}
327
328/// Los cuadros de un registro, en memoria, listos para remuestrear.
329///
330/// `film` necesita ir y venir por la línea de tiempo —el cuadro que toca en
331/// `t = i/fps` es el último cuyo `t_ms <= t`— y eso no se puede hacer con un
332/// lector de una sola pasada.
333pub fn cargar<R: Read>(lector: &mut Lector<R>) -> io::Result<Vec<(u32, EscenaWire)>> {
334 let mut v = Vec::new();
335 while let Some(paso) = lector.siguiente() {
336 v.push(paso?);
337 }
338 Ok(v)
339}
340
341/// Qué cuadro se ve en `t_ms`: el último que empezó antes o justo ahí.
342///
343/// Devuelve `None` sólo si el registro está vacío o si `t_ms` cae **antes** del
344/// primer cuadro, que es un caso real —un registro puede empezar en 40 ms— y no
345/// se resuelve mostrando el cuadro 0 como si hubiera estado ahí desde el
346/// principio.
347pub fn cuadro_en(cuadros: &[(u32, EscenaWire)], t_ms: u32) -> Option<usize> {
348 let mut elegido = None;
349 for (i, (t, _)) in cuadros.iter().enumerate() {
350 if *t <= t_ms {
351 elegido = Some(i);
352 } else {
353 break;
354 }
355 }
356 elegido
357}
358
359#[cfg(test)]
360mod pruebas {
361 use super::*;
362
363 /// Una escena con `n` órdenes distinguibles. `pop_layer` alcanza: lo que
364 /// estos tests miran es el TAMAÑO y la identidad del cuerpo serializado, no
365 /// el dibujo — de dibujar ya hay tests en `llimphi-reproductor`.
366 fn escena(n: usize) -> EscenaWire {
367 let mut e = EscenaWire::new();
368 for _ in 0..n {
369 e.pop_layer();
370 }
371 e
372 }
373
374 #[test]
375 fn ida_y_vuelta_con_cabecera() {
376 let cab = Cabecera {
377 ancho: 800,
378 alto: 600,
379 fps_sugerido: 24,
380 app: "nada".into(),
381 tema: "oscuro".into(),
382 };
383 let mut g = Grabador::nuevo(Vec::new(), &cab).unwrap();
384 g.cuadro(0, &escena(1)).unwrap();
385 g.cuadro(33, &escena(2)).unwrap();
386 let bytes = g.cerrar().unwrap();
387
388 let (leida, mut l) = Lector::abrir(&bytes[..]).unwrap();
389 assert_eq!(leida, cab, "la cabecera vuelve igual");
390 let (t0, _) = l.siguiente().unwrap().unwrap();
391 let (t1, _) = l.siguiente().unwrap().unwrap();
392 assert_eq!((t0, t1), (0, 33));
393 assert!(l.siguiente().is_none(), "y se terminó, sin error");
394 }
395
396 #[test]
397 fn el_cuadro_repetido_no_se_escribe() {
398 let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap();
399 assert!(g.cuadro(0, &escena(3)).unwrap(), "el primero siempre va");
400 assert!(!g.cuadro(16, &escena(3)).unwrap(), "idéntico: no");
401 assert!(!g.cuadro(33, &escena(3)).unwrap(), "idéntico: no");
402 assert!(g.cuadro(50, &escena(4)).unwrap(), "cambió: sí");
403 assert_eq!((g.cuadros(), g.repetidos()), (2, 2));
404 }
405
406 #[test]
407 fn el_primer_cuadro_nunca_se_deduplica() {
408 // Brazo de control del centinela: con un `Vec` vacío en vez de `None`,
409 // una escena que serializara a cero bytes se perdería sin decir nada.
410 let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap();
411 assert!(g.cuadro(0, &EscenaWire::default()).unwrap());
412 assert_eq!(g.cuadros(), 1);
413 }
414
415 #[test]
416 fn un_archivo_ajeno_falla_en_la_primera_linea() {
417 let basura = b"no soy un registro de nada, de veras que no".to_vec();
418 let r = Lector::abrir(&basura[..]);
419 assert!(r.is_err(), "la magia no casa");
420 }
421
422 #[test]
423 fn un_cuadro_cortado_es_un_error_y_no_un_final() {
424 let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap();
425 g.cuadro(0, &escena(2)).unwrap();
426 let bytes = g.cerrar().unwrap();
427 let cortado = &bytes[..bytes.len() - 3];
428 let (_, mut l) = Lector::abrir(cortado).unwrap();
429 let r = l.siguiente().expect("hay un cuadro empezado");
430 assert!(r.is_err(), "medio cuadro no es un cuadro");
431 }
432
433 #[test]
434 fn resumen_cuenta_sin_deserializar() {
435 let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap();
436 g.cuadro(0, &escena(1)).unwrap();
437 g.cuadro(500, &escena(9)).unwrap();
438 let bytes = g.cerrar().unwrap();
439 let (_, mut l) = Lector::abrir(&bytes[..]).unwrap();
440 let r = resumir(&mut l).unwrap();
441 assert_eq!(r.cuadros, 2);
442 assert_eq!(r.dur_ms, 500);
443 assert!(r.menor <= r.mayor && r.menor > 0);
444 assert_eq!(r.por_segundo(), r.bytes * 2, "500 ms → el doble por segundo");
445 }
446
447 #[test]
448 fn el_cuadro_que_toca_es_el_ultimo_que_empezo() {
449 let cuadros = vec![(0, escena(1)), (100, escena(2)), (300, escena(3))];
450 assert_eq!(cuadro_en(&cuadros, 0), Some(0));
451 assert_eq!(cuadro_en(&cuadros, 99), Some(0), "se sostiene el anterior");
452 assert_eq!(cuadro_en(&cuadros, 100), Some(1));
453 assert_eq!(cuadro_en(&cuadros, 5_000), Some(2), "y el último no se acaba");
454 assert_eq!(cuadro_en(&[], 0), None);
455 }
456}