//! Huella de una [`Escena`]: ¿va a pintar lo mismo que la vez pasada? //! //! Existe para **saltear una rasterización**. El caso que la motiva es la capa //! over del reproductor: la barra de controles se rasteriza entera cada cuadro //! —1,3–1,7 ms de los ~5 ms del cuadro, ver `02_ruway/media/RENDIMIENTO.md`— //! aunque en régimen no cambie un píxel entre cuadro y cuadro. Pintar la escena //! es barato (~0,2 ms); lo caro es el submit de compute de vello. Con una huella //! podemos pintar siempre, comparar, y rasterizar sólo cuando de verdad cambió. //! //! # Por qué se hashea el *encoding* y no el árbol de nodos //! //! La alternativa era hashear los campos de los `MountedNode` (lo que hace //! `boundary_hash` para los `repaint_boundary`). Se descartó por el modo de //! fallo: un acierto indebido **congela la barra**, y la lista de campos de un //! nodo crece cada vez que llimphi suma una prop — cada prop que alguien olvide //! agregar al hash es un cuadro viejo servido como bueno, en silencio. //! //! El encoding de vello es lo que la GPU va a leer: si dos encodings son iguales //! byte a byte, los píxeles son iguales **por construcción**. No hay una segunda //! idea nuestra de «qué importa» que pueda quedar corta. //! //! # La red: desestructurar exhaustivo //! //! Las estructuras de vello que no son POD (`GlyphRun`, `Patch`) se hashean //! **desestructurando todos sus campos por nombre**. Si vello suma uno en una //! versión futura, esto **no compila** — que es exactamente lo que se quiere: //! el olvido se paga en el build y no en un cuadro congelado en la máquina del //! usuario. //! //! # Fail-safe //! //! Devuelve `Option`: `None` significa «no sé, rasterizá igual». El perfil //! `hybrid` (la escena es una grabadora, no un encoding de vello) devuelve //! siempre `None` y con eso queda exactamente en el comportamiento de hoy. // Toda la maquinaria de hash es del perfil vello: en `hybrid` no hay encoding // que recorrer y el módulo queda en tres funciones que no devuelven nada útil. #[cfg(not(feature = "hybrid"))] use core::hash::{BuildHasher, Hasher}; /// Huella del contenido de la escena, o `None` si este backend no sabe darla. /// /// Dos escenas con la misma huella pintan lo mismo. La recíproca no se promete: /// dos escenas que pintan lo mismo pueden dar huellas distintas (un reordenado /// de las órdenes, por ejemplo) — y eso sólo cuesta una rasterización de más. #[cfg(not(feature = "hybrid"))] pub fn huella_escena(escena: &crate::Escena) -> Option { let mut h = foldhash::fast::FixedState::default().build_hasher(); hash_encoding(escena.encoding(), &mut h); Some(h.finish()) } /// En el perfil `hybrid` la escena es la grabadora de `llimphi-cpu`, no un /// encoding de vello: no hay huella y quien pregunta rasteriza como siempre. #[cfg(feature = "hybrid")] pub fn huella_escena(_escena: &crate::Escena) -> Option { None } /// Huella **desglosada por flujo** del encoding. Sólo para diagnosticar: cuando /// un caché no acierta y no se sabe por qué, esto dice **cuál** de las seis /// corrientes de vello cambió — y con eso se sabe si late la geometría, el color /// o el texto, sin adivinar leyendo la app. #[cfg(not(feature = "hybrid"))] pub fn huella_por_tramo(escena: &crate::Escena) -> [(&'static str, u64); 6] { let e = escena.encoding(); macro_rules! tramo { ($nombre:literal, $h:ident => $cuerpo:block) => {{ let mut $h = foldhash::fast::FixedState::default().build_hasher(); $cuerpo ($nombre, $h.finish()) }}; } [ tramo!("path_tags", h => { for t in &e.path_tags { h.write_u64(t.0 as u64); } }), tramo!("path_data", h => { for v in &e.path_data { h.write_u64(*v as u64); } }), tramo!("draw_tags", h => { for t in &e.draw_tags { h.write_u64(t.0 as u64); } }), tramo!("draw_data", h => { for v in &e.draw_data { h.write_u64(*v as u64); } }), tramo!("transforms", h => { for t in &e.transforms { hash_transform(t, &mut h); } }), tramo!("resources", h => { hash_resources(&e.resources, &mut h); }), ] } /// Sin encoding de vello no hay desglose posible. #[cfg(feature = "hybrid")] pub fn huella_por_tramo(_escena: &crate::Escena) -> [(&'static str, u64); 6] { [("n/a", 0); 6] } /// Las coordenadas crudas del flujo de paths, como `f32`. **Sólo diagnóstico**: /// cuando `path_data` cambia entre dos cuadros, esto dice *qué número* se movió /// —y el valor suele delatar al culpable de una ojeada. #[cfg(not(feature = "hybrid"))] pub fn coords_de_paths(escena: &crate::Escena) -> Vec { escena .encoding() .path_data .iter() .map(|v| f32::from_bits(*v)) .collect() } /// Sin encoding de vello no hay coordenadas que mirar. #[cfg(feature = "hybrid")] pub fn coords_de_paths(_escena: &crate::Escena) -> Vec { Vec::new() } #[cfg(not(feature = "hybrid"))] fn hash_encoding(e: &vello_encoding::Encoding, h: &mut impl Hasher) { // Desestructurado a propósito: un campo nuevo en `Encoding` rompe el build. let vello_encoding::Encoding { path_tags, path_data, draw_tags, draw_data, transforms, styles, resources, n_paths, n_path_segments, n_clips, n_open_clips, flags, } = e; // Los cuatro flujos POD van por su representación entera. `path_data` y // `draw_data` ya son `u32` (vello los guarda así justamente para poder // compararlos bit a bit). path_tags.len().hash_u(h); for t in path_tags { (t.0 as u64).hash_u(h); } path_data.len().hash_u(h); for v in path_data { (*v as u64).hash_u(h); } draw_tags.len().hash_u(h); for t in draw_tags { (t.0 as u64).hash_u(h); } draw_data.len().hash_u(h); for v in draw_data { (*v as u64).hash_u(h); } transforms.len().hash_u(h); for t in transforms { hash_transform(t, h); } styles.len().hash_u(h); for s in styles { let vello_encoding::Style { flags_and_miter_limit, line_width, } = s; (*flags_and_miter_limit as u64).hash_u(h); (line_width.to_bits() as u64).hash_u(h); } hash_resources(resources, h); for v in [n_paths, n_path_segments, n_clips, n_open_clips, flags] { (*v as u64).hash_u(h); } } #[cfg(not(feature = "hybrid"))] fn hash_transform(t: &vello_encoding::Transform, h: &mut impl Hasher) { let vello_encoding::Transform { matrix, translation, } = t; for v in matrix { (v.to_bits() as u64).hash_u(h); } for v in translation { (v.to_bits() as u64).hash_u(h); } } #[cfg(not(feature = "hybrid"))] fn hash_resources(r: &vello_encoding::Resources, h: &mut impl Hasher) { use vello::peniko::color::cache_key::BitHash; let vello_encoding::Resources { patches, color_stops, glyphs, glyph_runs, normalized_coords, } = r; patches.len().hash_u(h); for p in patches { hash_patch(p, h); } color_stops.len().hash_u(h); for s in color_stops { // `bit_hash` es la definición **de peniko** de «mismo stop»: si peniko // suma un campo al color, esto lo cubre sin que nadie se acuerde. s.bit_hash(h); } glyphs.len().hash_u(h); for g in glyphs { let vello_encoding::Glyph { id, x, y } = g; (*id as u64).hash_u(h); (x.to_bits() as u64).hash_u(h); (y.to_bits() as u64).hash_u(h); } glyph_runs.len().hash_u(h); for gr in glyph_runs { hash_glyph_run(gr, h); } normalized_coords.len().hash_u(h); for c in normalized_coords { (*c as i64 as u64).hash_u(h); } } #[cfg(not(feature = "hybrid"))] fn hash_patch(p: &vello_encoding::Patch, h: &mut impl Hasher) { use vello_encoding::Patch; core::mem::discriminant(p).hash_u_disc(h); match p { Patch::Ramp { draw_data_offset, stops, extend, } => { (*draw_data_offset as u64).hash_u(h); (stops.start as u64).hash_u(h); (stops.end as u64).hash_u(h); (*extend as u8 as u64).hash_u(h); } Patch::GlyphRun { index } => { (*index as u64).hash_u(h); } Patch::Image { draw_data_offset, image, } => { (*draw_data_offset as u64).hash_u(h); // El **id del blob**, no los píxeles: es un contador global de // peniko, O(1), y sobrevive a los clones (que es como una app pasa // la misma imagen cuadro a cuadro). Dos blobs distintos con los // mismos bytes dan ids distintos: un fallo de más, nunca un acierto // indebido. Mismo criterio que `render::hash_image`. let vello::peniko::ImageData { data, format, alpha_type, width, height, } = image; data.id().hash_u(h); (*format as u8 as u64).hash_u(h); (*alpha_type as u8 as u64).hash_u(h); (*width as u64).hash_u(h); (*height as u64).hash_u(h); } } } #[cfg(not(feature = "hybrid"))] fn hash_glyph_run(gr: &vello_encoding::GlyphRun, h: &mut impl Hasher) { let vello_encoding::GlyphRun { font, transform, glyph_transform, brush_transform, font_size, font_embolden, hint, normalized_coords, style, glyphs, stream_offsets, } = gr; // La fuente por el id de su blob + el índice dentro del archivo. font.data.id().hash_u(h); (font.index as u64).hash_u(h); hash_transform(transform, h); for t in [glyph_transform, brush_transform] { match t { Some(t) => { 1u64.hash_u(h); hash_transform(t, h); } None => 0u64.hash_u(h), } } (font_size.to_bits() as u64).hash_u(h); let vello_encoding::FontEmbolden { amount, join, miter_limit, tolerance, } = font_embolden; let vello::kurbo::Diagonal2 { xx, yy } = amount; xx.to_bits().hash_u(h); yy.to_bits().hash_u(h); (*join as u8 as u64).hash_u(h); miter_limit.to_bits().hash_u(h); tolerance.to_bits().hash_u(h); (*hint as u64).hash_u(h); (normalized_coords.start as u64).hash_u(h); (normalized_coords.end as u64).hash_u(h); hash_estilo_peniko(style, h); (glyphs.start as u64).hash_u(h); (glyphs.end as u64).hash_u(h); let vello_encoding::StreamOffsets { path_tags, path_data, draw_tags, draw_data, transforms, styles, } = stream_offsets; for v in [ path_tags, path_data, draw_tags, draw_data, transforms, styles, ] { (*v as u64).hash_u(h); } } /// El `style` de un `GlyphRun` es el de **peniko** (relleno o trazo), no el /// `Style` empaquetado del encoding: son dos tipos distintos con el mismo /// nombre y el compilador ya lo cazó una vez acá. #[cfg(not(feature = "hybrid"))] fn hash_estilo_peniko(s: &vello::peniko::Style, h: &mut impl Hasher) { use vello::peniko::Style; core::mem::discriminant(s).hash_u_disc(h); match s { Style::Fill(f) => (*f as u8 as u64).hash_u(h), Style::Stroke(st) => { let vello::kurbo::Stroke { width, join, miter_limit, start_cap, end_cap, dash_pattern, dash_offset, } = st; width.to_bits().hash_u(h); (*join as u8 as u64).hash_u(h); miter_limit.to_bits().hash_u(h); (*start_cap as u8 as u64).hash_u(h); (*end_cap as u8 as u64).hash_u(h); dash_pattern.len().hash_u(h); for d in dash_pattern.iter() { d.to_bits().hash_u(h); } dash_offset.to_bits().hash_u(h); } } } /// Azúcar para no repetir `Hash::hash(&x, h)` cuarenta veces con casteos. #[cfg(not(feature = "hybrid"))] trait HashU { fn hash_u(self, h: &mut impl Hasher); } #[cfg(not(feature = "hybrid"))] impl HashU for u64 { fn hash_u(self, h: &mut impl Hasher) { h.write_u64(self); } } #[cfg(not(feature = "hybrid"))] impl HashU for usize { fn hash_u(self, h: &mut impl Hasher) { h.write_u64(self as u64); } } /// El discriminante de un enum no expone su valor; se hashea con `Hash`. #[cfg(not(feature = "hybrid"))] trait HashDisc { fn hash_u_disc(self, h: &mut impl Hasher); } #[cfg(not(feature = "hybrid"))] impl HashDisc for core::mem::Discriminant { fn hash_u_disc(self, h: &mut impl Hasher) { core::hash::Hash::hash(&self, h); } } #[cfg(all(test, not(feature = "hybrid")))] mod tests { use super::*; use vello::kurbo::{Affine, Rect}; use vello::peniko::{color::palette, Fill}; fn escena(color: vello::peniko::Color, ancho: f64) -> crate::Escena { let mut s = crate::Escena::new(); s.fill( Fill::NonZero, Affine::IDENTITY, color, None, &Rect::new(0.0, 0.0, ancho, 10.0), ); s } #[test] fn la_misma_escena_da_la_misma_huella() { let a = huella_escena(&escena(palette::css::RED, 40.0)); let b = huella_escena(&escena(palette::css::RED, 40.0)); assert!(a.is_some()); assert_eq!(a, b); } #[test] fn cambiar_el_color_cambia_la_huella() { let a = huella_escena(&escena(palette::css::RED, 40.0)); let b = huella_escena(&escena(palette::css::BLUE, 40.0)); assert_ne!(a, b, "un color distinto tiene que ser un fallo de caché"); } #[test] fn cambiar_la_geometria_cambia_la_huella() { let a = huella_escena(&escena(palette::css::RED, 40.0)); let b = huella_escena(&escena(palette::css::RED, 41.0)); assert_ne!(a, b, "un rect más ancho tiene que ser un fallo de caché"); } #[test] fn la_escena_vacia_tiene_huella_y_difiere_de_la_pintada() { let vacia = huella_escena(&crate::Escena::new()); assert!(vacia.is_some()); assert_ne!(vacia, huella_escena(&escena(palette::css::RED, 40.0))); } /// El caso que motiva todo: una barra de progreso que avanza **tiene** que /// dar huella distinta, o se congela en pantalla. #[test] fn una_barra_que_avanza_no_da_acierto() { let mut previa = huella_escena(&escena(palette::css::WHITE, 100.0)); for px in 101..110 { let nueva = huella_escena(&escena(palette::css::WHITE, px as f64)); assert_ne!(previa, nueva, "la barra a {px} px repitió huella"); previa = nueva; } } }