gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main llimphi-registro/src/lib.rs Rust · 456 siq'ikuna · 17.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — 132 miembros, 79 widgets, la galería vuelve y licencia dual d8edd9a9c · Sergio ·

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}