gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main llimphi-jaula/src/medidor.rs Rust · 218 lines · 8.4 KB History Who touched what raw

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1// =============================================================================
2//  medidor :: preguntarle las medidas al kernel  (W4, el lado app)
3// -----------------------------------------------------------------------------
4//  El `Typesetter` de la jaula no shapea: pregunta. Este módulo es la pregunta
5//  concreta cuando el que contesta es wawa.
6//
7//  El porqué está en W4.1 y conviene tenerlo a mano, porque la tentación de
8//  «medir acá nomás, total es una cuenta» es real: shapear adentro de la jaula
9//  paga el 30,3x del intérprete, y el fotograma de una pantalla densa con texto
10//  vivo pasa de 71 fps a 21,8. Medir ES shapear — parley resuelve ligaduras,
11//  bidi y selección de cara para saber cuánto ocupa una cadena—, así que medir
12//  adentro sería tirar la decisión entera de W4 por una comodidad.
13//
14//  La forma del contrato:
15//
16//    app                                  kernel
17//    ───                                  ──────
18//    PeticionMedida ─postcard─►
19//                                         parley, con la caché del sistema
20//                              ◄─12 bytes─ MedidaTexto (3 f32 LE)
21//
22//  La respuesta es de tamaño fijo a propósito: el kernel la escribe sin alocar.
23// =============================================================================
24
25use alloc::vec;
26
27use llimphi_text::wire::{Medida, Peticion};
28use llimphi_wire_escena::{MedidaTexto, PeticionMedida};
29
30// La capacidad que el kernel inyecta. Ausente en cualquier otro anfitrión.
31//
32// `wasm_import_module = "wawa"` es lo que la ata a la matriz de capacidades: si
33// el kernel no registró esta función, el módulo NO instancia. La frontera es
34// física, no una comprobación en tiempo de ejecución — no hay tabla que
35// escalar porque no hay tabla.
36#[cfg(target_arch = "wasm32")]
37#[link(wasm_import_module = "wawa")]
38extern "C" {
39    fn sys_medir_texto(peticion_ptr: u32, peticion_len: u32, salida_ptr: u32) -> i32;
40}
41
42/// Tope del búfer de petición. Espeja el que el kernel impone; tenerlo de los
43/// dos lados evita hacer un viaje que ya se sabe que va a fallar.
44const MAX_BYTES_PETICION: usize = 8 * 1024;
45
46/// El medidor de wawa: el que se le pasa a [`crate::Jaula::new`] corriendo
47/// dentro del kernel.
48///
49/// ```ignore
50/// let mut jaula = Jaula::new(ancho, alto, llimphi_jaula::medidor::wawa);
51/// ```
52///
53/// Devuelve una medida en cero **sólo** si el kernel rechazó la petición (cuota
54/// agotada, payload inválido). El contador de
55/// [`crate::Jaula::medidas_estimadas`] no cubre este caso porque acá sí hay
56/// medidor; quien necesite distinguirlo tiene [`medir`], que devuelve el código.
57pub fn wawa(p: &Peticion<'_>) -> Medida {
58    medir(p).unwrap_or(Medida {
59        width: 0.0,
60        full_width: 0.0,
61        height: 0.0,
62    })
63}
64
65/// Como [`wawa`] pero devolviendo `None` cuando el kernel rechazó la petición.
66///
67/// La distinción importa: una medida en cero y una medida que no se pudo obtener
68/// producen el mismo layout colapsado, pero sólo una de las dos es un bug de la
69/// app. Quien quiera reaccionar —reintentar el fotograma siguiente, recortar la
70/// cadena— necesita saber cuál fue.
71pub fn medir(p: &Peticion<'_>) -> Option<Medida> {
72    medir_con(p, invocar)
73}
74
75/// [`medir`] con **el cruce como argumento**.
76///
77/// El marshalling —armar la `PeticionMedida`, serializarla, comprobar el tope,
78/// leer los 12 bytes de vuelta— es el mismo sea cual sea el anfitrión; lo único
79/// que cambia es CÓMO se cruza. Esa separación ya estaba insinuada en `invocar`
80/// («aislado en una función para que el resto del módulo sea código común»);
81/// esto la hace pública, que es lo que hacía falta cuando apareció el segundo
82/// anfitrión: **Linux**, donde el cruce es un socket y no una syscall
83/// (`02_ruway/llimphi/SDD-JAULA-LINUX.md`).
84///
85/// Importa que sea compartido y no copiado: si el huésped de Linux armara su
86/// propia petición, cualquier campo que se agregue a `EstiloTextoWire` quedaría
87/// sin viajar de un solo lado, y el síntoma sería texto que se mide distinto
88/// según quién lo hospeda — la clase de falla que no se ve hasta que se ve.
89///
90/// `cruzar` devuelve 0 si contestó y llenó la salida; cualquier otra cosa es un
91/// rechazo y acá se vuelve `None` — **nunca** un número inventado que parezca
92/// una medida buena.
93pub fn medir_con(
94    p: &Peticion<'_>,
95    mut cruzar: impl FnMut(&[u8], &mut [u8]) -> i32,
96) -> Option<Medida> {
97    let peticion = PeticionMedida {
98        cadena: alloc::string::String::from(p.cadena),
99        estilo: p.estilo.clone(),
100        ancho_max: p.ancho_max,
101    };
102
103    let bytes = postcard::to_allocvec(&peticion).ok()?;
104    if bytes.is_empty() || bytes.len() > MAX_BYTES_PETICION {
105        return None;
106    }
107
108    let mut salida = vec![0u8; MedidaTexto::BYTES];
109    let codigo = cruzar(&bytes, &mut salida);
110    if codigo != 0 {
111        return None;
112    }
113
114    let mut fijo = [0u8; MedidaTexto::BYTES];
115    fijo.copy_from_slice(&salida);
116    let m = MedidaTexto::de_bytes(&fijo);
117    Some(Medida {
118        width: m.width,
119        full_width: m.full_width,
120        height: m.height,
121    })
122}
123
124/// El cruce en sí. Aislado en una función para que el resto del módulo sea
125/// código común y sólo esto dependa del anfitrión.
126#[cfg(target_arch = "wasm32")]
127fn invocar(peticion: &[u8], salida: &mut [u8]) -> i32 {
128    // SEGURIDAD: los dos punteros describen NUESTRA memoria lineal y el kernel
129    // valida los rangos contra sus límites reales antes de leer o escribir un
130    // solo byte — no confía en que el runtime lo haya hecho. Un rango que se
131    // desborde aborta este módulo, que es lo correcto: la culpa sería nuestra.
132    unsafe {
133        sys_medir_texto(
134            peticion.as_ptr() as u32,
135            peticion.len() as u32,
136            salida.as_mut_ptr() as u32,
137        )
138    }
139}
140
141/// Fuera de wasm no hay kernel al que preguntarle.
142///
143/// Existe para que el crate compile y se pueda testear en el anfitrión sin
144/// `#[cfg]` regados por todos lados. Devuelve `Ausente` (-1), o sea que
145/// [`medir`] da `None` y [`wawa`] da cero — nunca un número inventado que
146/// parezca una medida buena.
147#[cfg(not(target_arch = "wasm32"))]
148fn invocar(_peticion: &[u8], _salida: &mut [u8]) -> i32 {
149    -1
150}
151
152#[cfg(test)]
153mod pruebas {
154    use super::*;
155
156    fn peticion() -> PeticionMedida {
157        PeticionMedida {
158            cadena: alloc::string::String::from("hola"),
159            estilo: Default::default(),
160            ancho_max: None,
161        }
162    }
163
164    /// El cruce enchufado contesta, y la medida llega entera de vuelta.
165    #[test]
166    fn el_cruce_enchufado_devuelve_la_medida() {
167        let p = peticion();
168        let pet = Peticion {
169            cadena: &p.cadena,
170            estilo: &p.estilo,
171            ancho_max: None,
172        };
173        let esperada = MedidaTexto { width: 12.5, full_width: 13.0, height: 7.25 };
174        let m = medir_con(&pet, |entrada, salida| {
175            // El que contesta recibe la petición SERIALIZADA — es lo que va a
176            // viajar por el socket, no un struct prestado.
177            assert!(!entrada.is_empty(), "la petición viaja en bytes");
178            salida.copy_from_slice(&esperada.a_bytes());
179            0
180        })
181        .expect("el cruce contestó 0");
182        assert_eq!(m.width, 12.5);
183        assert_eq!(m.full_width, 13.0);
184        assert_eq!(m.height, 7.25);
185    }
186
187    /// **Un rechazo no inventa una medida.** Es la diferencia entre un layout
188    /// colapsado que se puede detectar y uno que miente con números plausibles.
189    #[test]
190    fn un_cruce_que_rechaza_no_devuelve_numeros() {
191        let p = peticion();
192        let pet = Peticion {
193            cadena: &p.cadena,
194            estilo: &p.estilo,
195            ancho_max: None,
196        };
197        // Escribe basura Y devuelve error: si `medir_con` mirara la salida antes
198        // que el código, este test la dejaría pasar como medida.
199        let m = medir_con(&pet, |_e, salida| {
200            salida.fill(0xFF);
201            -1
202        });
203        assert!(m.is_none(), "código distinto de 0 = sin medida, no una medida rara");
204    }
205
206    /// Fuera de wasm no hay kernel: el camino por defecto contesta `None`, no un
207    /// cero que parezca una cadena vacía.
208    #[test]
209    fn sin_anfitrion_no_hay_medida() {
210        let p = peticion();
211        let pet = Peticion {
212            cadena: &p.cadena,
213            estilo: &p.estilo,
214            ancho_max: None,
215        };
216        assert!(medir(&pet).is_none());
217    }
218}