gioser

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-raster/src/huella.rs Rust · 447 lines · 14.7 KB History Who touched what raw

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

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}