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//! Huella de una [`Escena`]: ¿va a pintar lo mismo que la vez pasada?
2//!
3//! Existe para **saltear una rasterización**. El caso que la motiva es la capa
4//! over del reproductor: la barra de controles se rasteriza entera cada cuadro
5//! —1,3–1,7 ms de los ~5 ms del cuadro, ver `02_ruway/media/RENDIMIENTO.md`—
6//! aunque en régimen no cambie un píxel entre cuadro y cuadro. Pintar la escena
7//! es barato (~0,2 ms); lo caro es el submit de compute de vello. Con una huella
8//! podemos pintar siempre, comparar, y rasterizar sólo cuando de verdad cambió.
9//!
10//! # Por qué se hashea el *encoding* y no el árbol de nodos
11//!
12//! La alternativa era hashear los campos de los `MountedNode` (lo que hace
13//! `boundary_hash` para los `repaint_boundary`). Se descartó por el modo de
14//! fallo: un acierto indebido **congela la barra**, y la lista de campos de un
15//! nodo crece cada vez que llimphi suma una prop — cada prop que alguien olvide
16//! agregar al hash es un cuadro viejo servido como bueno, en silencio.
17//!
18//! El encoding de vello es lo que la GPU va a leer: si dos encodings son iguales
19//! byte a byte, los píxeles son iguales **por construcción**. No hay una segunda
20//! idea nuestra de «qué importa» que pueda quedar corta.
21//!
22//! # La red: desestructurar exhaustivo
23//!
24//! Las estructuras de vello que no son POD (`GlyphRun`, `Patch`) se hashean
25//! **desestructurando todos sus campos por nombre**. Si vello suma uno en una
26//! versión futura, esto **no compila** — que es exactamente lo que se quiere:
27//! el olvido se paga en el build y no en un cuadro congelado en la máquina del
28//! usuario.
29//!
30//! # Fail-safe
31//!
32//! Devuelve `Option`: `None` significa «no sé, rasterizá igual». El perfil
33//! `hybrid` (la escena es una grabadora, no un encoding de vello) devuelve
34//! siempre `None` y con eso queda exactamente en el comportamiento de hoy.
35
36// Toda la maquinaria de hash es del perfil vello: en `hybrid` no hay encoding
37// que recorrer y el módulo queda en tres funciones que no devuelven nada útil.
38#[cfg(not(feature = "hybrid"))]
39use core::hash::{BuildHasher, Hasher};
40
41/// Huella del contenido de la escena, o `None` si este backend no sabe darla.
42///
43/// Dos escenas con la misma huella pintan lo mismo. La recíproca no se promete:
44/// dos escenas que pintan lo mismo pueden dar huellas distintas (un reordenado
45/// de las órdenes, por ejemplo) — y eso sólo cuesta una rasterización de más.
46#[cfg(not(feature = "hybrid"))]
47pub fn huella_escena(escena: &crate::Escena) -> Option<u64> {
48 let mut h = foldhash::fast::FixedState::default().build_hasher();
49 hash_encoding(escena.encoding(), &mut h);
50 Some(h.finish())
51}
52
53/// En el perfil `hybrid` la escena es la grabadora de `llimphi-cpu`, no un
54/// encoding de vello: no hay huella y quien pregunta rasteriza como siempre.
55#[cfg(feature = "hybrid")]
56pub fn huella_escena(_escena: &crate::Escena) -> Option<u64> {
57 None
58}
59
60/// Huella **desglosada por flujo** del encoding. Sólo para diagnosticar: cuando
61/// un caché no acierta y no se sabe por qué, esto dice **cuál** de las seis
62/// corrientes de vello cambió — y con eso se sabe si late la geometría, el color
63/// o el texto, sin adivinar leyendo la app.
64#[cfg(not(feature = "hybrid"))]
65pub fn huella_por_tramo(escena: &crate::Escena) -> [(&'static str, u64); 6] {
66 let e = escena.encoding();
67 macro_rules! tramo {
68 ($nombre:literal, $h:ident => $cuerpo:block) => {{
69 let mut $h = foldhash::fast::FixedState::default().build_hasher();
70 $cuerpo
71 ($nombre, $h.finish())
72 }};
73 }
74 [
75 tramo!("path_tags", h => { for t in &e.path_tags { h.write_u64(t.0 as u64); } }),
76 tramo!("path_data", h => { for v in &e.path_data { h.write_u64(*v as u64); } }),
77 tramo!("draw_tags", h => { for t in &e.draw_tags { h.write_u64(t.0 as u64); } }),
78 tramo!("draw_data", h => { for v in &e.draw_data { h.write_u64(*v as u64); } }),
79 tramo!("transforms", h => { for t in &e.transforms { hash_transform(t, &mut h); } }),
80 tramo!("resources", h => { hash_resources(&e.resources, &mut h); }),
81 ]
82}
83
84/// Sin encoding de vello no hay desglose posible.
85#[cfg(feature = "hybrid")]
86pub fn huella_por_tramo(_escena: &crate::Escena) -> [(&'static str, u64); 6] {
87 [("n/a", 0); 6]
88}
89
90/// Las coordenadas crudas del flujo de paths, como `f32`. **Sólo diagnóstico**:
91/// cuando `path_data` cambia entre dos cuadros, esto dice *qué número* se movió
92/// —y el valor suele delatar al culpable de una ojeada.
93#[cfg(not(feature = "hybrid"))]
94pub fn coords_de_paths(escena: &crate::Escena) -> Vec<f32> {
95 escena
96 .encoding()
97 .path_data
98 .iter()
99 .map(|v| f32::from_bits(*v))
100 .collect()
101}
102
103/// Sin encoding de vello no hay coordenadas que mirar.
104#[cfg(feature = "hybrid")]
105pub fn coords_de_paths(_escena: &crate::Escena) -> Vec<f32> {
106 Vec::new()
107}
108
109#[cfg(not(feature = "hybrid"))]
110fn hash_encoding(e: &vello_encoding::Encoding, h: &mut impl Hasher) {
111 // Desestructurado a propósito: un campo nuevo en `Encoding` rompe el build.
112 let vello_encoding::Encoding {
113 path_tags,
114 path_data,
115 draw_tags,
116 draw_data,
117 transforms,
118 styles,
119 resources,
120 n_paths,
121 n_path_segments,
122 n_clips,
123 n_open_clips,
124 flags,
125 } = e;
126
127 // Los cuatro flujos POD van por su representación entera. `path_data` y
128 // `draw_data` ya son `u32` (vello los guarda así justamente para poder
129 // compararlos bit a bit).
130 path_tags.len().hash_u(h);
131 for t in path_tags {
132 (t.0 as u64).hash_u(h);
133 }
134 path_data.len().hash_u(h);
135 for v in path_data {
136 (*v as u64).hash_u(h);
137 }
138 draw_tags.len().hash_u(h);
139 for t in draw_tags {
140 (t.0 as u64).hash_u(h);
141 }
142 draw_data.len().hash_u(h);
143 for v in draw_data {
144 (*v as u64).hash_u(h);
145 }
146 transforms.len().hash_u(h);
147 for t in transforms {
148 hash_transform(t, h);
149 }
150 styles.len().hash_u(h);
151 for s in styles {
152 let vello_encoding::Style {
153 flags_and_miter_limit,
154 line_width,
155 } = s;
156 (*flags_and_miter_limit as u64).hash_u(h);
157 (line_width.to_bits() as u64).hash_u(h);
158 }
159
160 hash_resources(resources, h);
161
162 for v in [n_paths, n_path_segments, n_clips, n_open_clips, flags] {
163 (*v as u64).hash_u(h);
164 }
165}
166
167#[cfg(not(feature = "hybrid"))]
168fn hash_transform(t: &vello_encoding::Transform, h: &mut impl Hasher) {
169 let vello_encoding::Transform {
170 matrix,
171 translation,
172 } = t;
173 for v in matrix {
174 (v.to_bits() as u64).hash_u(h);
175 }
176 for v in translation {
177 (v.to_bits() as u64).hash_u(h);
178 }
179}
180
181#[cfg(not(feature = "hybrid"))]
182fn hash_resources(r: &vello_encoding::Resources, h: &mut impl Hasher) {
183 use vello::peniko::color::cache_key::BitHash;
184 let vello_encoding::Resources {
185 patches,
186 color_stops,
187 glyphs,
188 glyph_runs,
189 normalized_coords,
190 } = r;
191
192 patches.len().hash_u(h);
193 for p in patches {
194 hash_patch(p, h);
195 }
196 color_stops.len().hash_u(h);
197 for s in color_stops {
198 // `bit_hash` es la definición **de peniko** de «mismo stop»: si peniko
199 // suma un campo al color, esto lo cubre sin que nadie se acuerde.
200 s.bit_hash(h);
201 }
202 glyphs.len().hash_u(h);
203 for g in glyphs {
204 let vello_encoding::Glyph { id, x, y } = g;
205 (*id as u64).hash_u(h);
206 (x.to_bits() as u64).hash_u(h);
207 (y.to_bits() as u64).hash_u(h);
208 }
209 glyph_runs.len().hash_u(h);
210 for gr in glyph_runs {
211 hash_glyph_run(gr, h);
212 }
213 normalized_coords.len().hash_u(h);
214 for c in normalized_coords {
215 (*c as i64 as u64).hash_u(h);
216 }
217}
218
219#[cfg(not(feature = "hybrid"))]
220fn hash_patch(p: &vello_encoding::Patch, h: &mut impl Hasher) {
221 use vello_encoding::Patch;
222 core::mem::discriminant(p).hash_u_disc(h);
223 match p {
224 Patch::Ramp {
225 draw_data_offset,
226 stops,
227 extend,
228 } => {
229 (*draw_data_offset as u64).hash_u(h);
230 (stops.start as u64).hash_u(h);
231 (stops.end as u64).hash_u(h);
232 (*extend as u8 as u64).hash_u(h);
233 }
234 Patch::GlyphRun { index } => {
235 (*index as u64).hash_u(h);
236 }
237 Patch::Image {
238 draw_data_offset,
239 image,
240 } => {
241 (*draw_data_offset as u64).hash_u(h);
242 // El **id del blob**, no los píxeles: es un contador global de
243 // peniko, O(1), y sobrevive a los clones (que es como una app pasa
244 // la misma imagen cuadro a cuadro). Dos blobs distintos con los
245 // mismos bytes dan ids distintos: un fallo de más, nunca un acierto
246 // indebido. Mismo criterio que `render::hash_image`.
247 let vello::peniko::ImageData {
248 data,
249 format,
250 alpha_type,
251 width,
252 height,
253 } = image;
254 data.id().hash_u(h);
255 (*format as u8 as u64).hash_u(h);
256 (*alpha_type as u8 as u64).hash_u(h);
257 (*width as u64).hash_u(h);
258 (*height as u64).hash_u(h);
259 }
260 }
261}
262
263#[cfg(not(feature = "hybrid"))]
264fn hash_glyph_run(gr: &vello_encoding::GlyphRun, h: &mut impl Hasher) {
265 let vello_encoding::GlyphRun {
266 font,
267 transform,
268 glyph_transform,
269 brush_transform,
270 font_size,
271 font_embolden,
272 hint,
273 normalized_coords,
274 style,
275 glyphs,
276 stream_offsets,
277 } = gr;
278
279 // La fuente por el id de su blob + el índice dentro del archivo.
280 font.data.id().hash_u(h);
281 (font.index as u64).hash_u(h);
282 hash_transform(transform, h);
283 for t in [glyph_transform, brush_transform] {
284 match t {
285 Some(t) => {
286 1u64.hash_u(h);
287 hash_transform(t, h);
288 }
289 None => 0u64.hash_u(h),
290 }
291 }
292 (font_size.to_bits() as u64).hash_u(h);
293 let vello_encoding::FontEmbolden {
294 amount,
295 join,
296 miter_limit,
297 tolerance,
298 } = font_embolden;
299 let vello::kurbo::Diagonal2 { xx, yy } = amount;
300 xx.to_bits().hash_u(h);
301 yy.to_bits().hash_u(h);
302 (*join as u8 as u64).hash_u(h);
303 miter_limit.to_bits().hash_u(h);
304 tolerance.to_bits().hash_u(h);
305 (*hint as u64).hash_u(h);
306 (normalized_coords.start as u64).hash_u(h);
307 (normalized_coords.end as u64).hash_u(h);
308 hash_estilo_peniko(style, h);
309 (glyphs.start as u64).hash_u(h);
310 (glyphs.end as u64).hash_u(h);
311 let vello_encoding::StreamOffsets {
312 path_tags,
313 path_data,
314 draw_tags,
315 draw_data,
316 transforms,
317 styles,
318 } = stream_offsets;
319 for v in [
320 path_tags, path_data, draw_tags, draw_data, transforms, styles,
321 ] {
322 (*v as u64).hash_u(h);
323 }
324}
325
326/// El `style` de un `GlyphRun` es el de **peniko** (relleno o trazo), no el
327/// `Style` empaquetado del encoding: son dos tipos distintos con el mismo
328/// nombre y el compilador ya lo cazó una vez acá.
329#[cfg(not(feature = "hybrid"))]
330fn hash_estilo_peniko(s: &vello::peniko::Style, h: &mut impl Hasher) {
331 use vello::peniko::Style;
332 core::mem::discriminant(s).hash_u_disc(h);
333 match s {
334 Style::Fill(f) => (*f as u8 as u64).hash_u(h),
335 Style::Stroke(st) => {
336 let vello::kurbo::Stroke {
337 width,
338 join,
339 miter_limit,
340 start_cap,
341 end_cap,
342 dash_pattern,
343 dash_offset,
344 } = st;
345 width.to_bits().hash_u(h);
346 (*join as u8 as u64).hash_u(h);
347 miter_limit.to_bits().hash_u(h);
348 (*start_cap as u8 as u64).hash_u(h);
349 (*end_cap as u8 as u64).hash_u(h);
350 dash_pattern.len().hash_u(h);
351 for d in dash_pattern.iter() {
352 d.to_bits().hash_u(h);
353 }
354 dash_offset.to_bits().hash_u(h);
355 }
356 }
357}
358
359/// Azúcar para no repetir `Hash::hash(&x, h)` cuarenta veces con casteos.
360#[cfg(not(feature = "hybrid"))]
361trait HashU {
362 fn hash_u(self, h: &mut impl Hasher);
363}
364#[cfg(not(feature = "hybrid"))]
365impl HashU for u64 {
366 fn hash_u(self, h: &mut impl Hasher) {
367 h.write_u64(self);
368 }
369}
370#[cfg(not(feature = "hybrid"))]
371impl HashU for usize {
372 fn hash_u(self, h: &mut impl Hasher) {
373 h.write_u64(self as u64);
374 }
375}
376
377/// El discriminante de un enum no expone su valor; se hashea con `Hash`.
378#[cfg(not(feature = "hybrid"))]
379trait HashDisc {
380 fn hash_u_disc(self, h: &mut impl Hasher);
381}
382#[cfg(not(feature = "hybrid"))]
383impl<T> HashDisc for core::mem::Discriminant<T> {
384 fn hash_u_disc(self, h: &mut impl Hasher) {
385 core::hash::Hash::hash(&self, h);
386 }
387}
388
389#[cfg(all(test, not(feature = "hybrid")))]
390mod tests {
391 use super::*;
392 use vello::kurbo::{Affine, Rect};
393 use vello::peniko::{color::palette, Fill};
394
395 fn escena(color: vello::peniko::Color, ancho: f64) -> crate::Escena {
396 let mut s = crate::Escena::new();
397 s.fill(
398 Fill::NonZero,
399 Affine::IDENTITY,
400 color,
401 None,
402 &Rect::new(0.0, 0.0, ancho, 10.0),
403 );
404 s
405 }
406
407 #[test]
408 fn la_misma_escena_da_la_misma_huella() {
409 let a = huella_escena(&escena(palette::css::RED, 40.0));
410 let b = huella_escena(&escena(palette::css::RED, 40.0));
411 assert!(a.is_some());
412 assert_eq!(a, b);
413 }
414
415 #[test]
416 fn cambiar_el_color_cambia_la_huella() {
417 let a = huella_escena(&escena(palette::css::RED, 40.0));
418 let b = huella_escena(&escena(palette::css::BLUE, 40.0));
419 assert_ne!(a, b, "un color distinto tiene que ser un fallo de caché");
420 }
421
422 #[test]
423 fn cambiar_la_geometria_cambia_la_huella() {
424 let a = huella_escena(&escena(palette::css::RED, 40.0));
425 let b = huella_escena(&escena(palette::css::RED, 41.0));
426 assert_ne!(a, b, "un rect más ancho tiene que ser un fallo de caché");
427 }
428
429 #[test]
430 fn la_escena_vacia_tiene_huella_y_difiere_de_la_pintada() {
431 let vacia = huella_escena(&crate::Escena::new());
432 assert!(vacia.is_some());
433 assert_ne!(vacia, huella_escena(&escena(palette::css::RED, 40.0)));
434 }
435
436 /// El caso que motiva todo: una barra de progreso que avanza **tiene** que
437 /// dar huella distinta, o se congela en pantalla.
438 #[test]
439 fn una_barra_que_avanza_no_da_acierto() {
440 let mut previa = huella_escena(&escena(palette::css::WHITE, 100.0));
441 for px in 101..110 {
442 let nueva = huella_escena(&escena(palette::css::WHITE, px as f64));
443 assert_ne!(previa, nueva, "la barra a {px} px repitió huella");
444 previa = nueva;
445 }
446 }
447}