tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos
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}