// ============================================================================= // llimphi-wire-escena :: el IR de ESCENA que cruza la frontera // ----------------------------------------------------------------------------- // W4 del PLAN-LLIMPHI es la pregunta «¿dónde rasteriza una app de wawa?», y // W4.1 la contestó con números. Este crate es esa respuesta hecha tipos. // // Las tres arquitecturas que el plan midió, a 500 widgets: // // Kernel puro (wire-view) 685 fps pero sólo ~15-18 de 41 entries del DSL // IR de escena 77 fps con los 69 widgets ← ésta // Jaula pura 18 fps con 476 KB de peso por app // // El «IR de escena» es esto: la app corre `view` y `layout` dentro de la jaula // —barato— y emite una LISTA PLANA de formas + pinceles + texto. El kernel la // rasteriza nativo. Lo caro (aplanar curvas, generar strips, rasterizar) es el // 79-93% del fotograma y cae entero del lado nativo; la app sólo arma // geometría, que aguanta el 30,3x del intérprete sin despeinarse. // // **La diferencia con `llimphi-wire-view`** —el otro IR del repo, y hay que // saber cuál usar— es de nivel de abstracción, no de calidad: // // llimphi-wire-view árbol de CAJAS el host corre layout Y pinta // llimphi-wire-escena lista de TRAZOS el host sólo rasteriza // // wire-view es más chico en el cable y deja al host decidir el aspecto, pero // sólo expresa lo que su vocabulario nombra: no hay gradientes, sombras, clip, // z-index ni paint custom, así que nodegraph, table, terminal y text-editor // quedan afuera por construcción. wire-escena no tiene ese techo porque no // habla de widgets: habla de trazos, y todo widget termina en trazos. // // ## Lo único que NO es un trazo: el texto // // Y es a propósito. Ver el encabezado de [`texto`] — es la decisión que W4.1 // midió como 3,3x. Resumen: si la app shapeara y mandara glifos, parley pagaría // el 30,3x del intérprete y se comería el 72% del fotograma. Mandando la cadena, // el kernel shapea nativo y el texto cuesta 1,09 ms en vez de 32,94. // // ## Qué no está acá // // El **encoder desde el compositor** (que `paint` sepa escribir en una // `EscenaWire` en vez de en una `vello::Scene`) y el **reproductor** hacia // `llimphi_cpu::Escena`. Son los dos extremos del cable; éste es el cable. // ============================================================================= #![cfg_attr(not(any(feature = "std", test)), no_std)] extern crate alloc; use alloc::vec::Vec; use kurbo::{Affine, Shape}; use peniko::{Brush, BrushRef, Color, Fill, Mix}; use serde::{Deserialize, Serialize}; pub mod forma; pub mod pincel; pub mod texto; pub use forma::{afin_a_wire, wire_a_afin, FormaKurbo, FormaWire, SegWire, UbicacionWire}; pub use pincel::{ blob_de, color_a_wire, huella_imagen, imagen_de_blob, wire_a_color, BlobImagen, ClaseDegradeWire, ExtiendeWire, ParadaWire, PincelWire, Rgba, }; pub use texto::{ AlineacionWire, EstiloRangoWire, EstiloTextoWire, MedidaTexto, OrdenTexto, PeticionMedida, RangoWire, Texto, }; /// Regla de relleno. Espeja `peniko::Fill`. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ReglaWire { #[default] DistintoDeCero, ParImpar, } impl ReglaWire { fn de(f: Fill) -> Self { match f { Fill::NonZero => Self::DistintoDeCero, Fill::EvenOdd => Self::ParImpar, } } /// La regla del lado nativo. pub fn a_peniko(self) -> Fill { match self { Self::DistintoDeCero => Fill::NonZero, Self::ParImpar => Fill::EvenOdd, } } } /// Modo de mezcla, en los dos bytes que `peniko` ya usa internamente. /// /// `Mix` y `Compose` son `#[repr(u8)]` con discriminantes explícitos y estables /// (están fijados por la spec de compositing del W3C, no por peniko), así que /// viajar como pareja de bytes no es un truco frágil: es su representación real. /// La reconstrucción es un match exhaustivo —16 y 14 variantes— y un byte fuera /// de rango cae a `Normal`/`SrcOver`, que es lo que un lector viejo debe hacer /// con un modo que todavía no conoce. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct MezclaWire { pub mezcla: u8, pub compone: u8, } impl Default for MezclaWire { fn default() -> Self { Self { mezcla: peniko::Mix::Normal as u8, compone: peniko::Compose::SrcOver as u8, } } } impl MezclaWire { fn de(bm: peniko::BlendMode) -> Self { Self { mezcla: bm.mix as u8, compone: bm.compose as u8, } } /// El modo de mezcla del lado nativo. pub fn a_peniko(self) -> peniko::BlendMode { use peniko::{Compose, Mix}; let mix = match self.mezcla { 0 => Mix::Normal, 1 => Mix::Multiply, 2 => Mix::Screen, 3 => Mix::Overlay, 4 => Mix::Darken, 5 => Mix::Lighten, 6 => Mix::ColorDodge, 7 => Mix::ColorBurn, 8 => Mix::HardLight, 9 => Mix::SoftLight, 10 => Mix::Difference, 11 => Mix::Exclusion, 12 => Mix::Hue, 13 => Mix::Saturation, 14 => Mix::Color, 15 => Mix::Luminosity, _ => Mix::Normal, }; let compose = match self.compone { 0 => Compose::Clear, 1 => Compose::Copy, 2 => Compose::Dest, 3 => Compose::SrcOver, 4 => Compose::DestOver, 5 => Compose::SrcIn, 6 => Compose::DestIn, 7 => Compose::SrcOut, 8 => Compose::DestOut, 9 => Compose::SrcAtop, 10 => Compose::DestAtop, 11 => Compose::Xor, 12 => Compose::Plus, 13 => Compose::PlusLighter, _ => Compose::SrcOver, }; peniko::BlendMode { mix, compose } } } /// Una orden de dibujo lista para viajar. /// /// Espeja `llimphi_cpu::Orden` una a una, con dos diferencias que son el crate /// entero: los tipos son serializables y en f32, y `Glifos` se reemplaza por /// [`OrdenWire::Texto`] — que lleva la cadena, no los glifos. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum OrdenWire { Rellenar { regla: ReglaWire, xf: [f32; 6], pincel: PincelWire, xf_pincel: Option<[f32; 6]>, forma: FormaWire, }, Trazar { /// Ancho de la línea. El resto de `kurbo::Stroke` (joins, caps, dash) /// no viaja todavía: el compositor traza contornos de caja y de foco, /// donde los defaults alcanzan. Sumarlo es agregar campos, no rediseñar. ancho: f32, xf: [f32; 6], pincel: PincelWire, xf_pincel: Option<[f32; 6]>, forma: FormaWire, }, AbrirCapa { regla: ReglaWire, mezcla: MezclaWire, alfa: f32, xf: [f32; 6], recorte: FormaWire, }, AbrirMascaraLuminancia { regla: ReglaWire, alfa: f32, xf: [f32; 6], recorte: FormaWire, }, CerrarCapa, /// Imagen por huella — los píxeles no viajan por acá (ver [`pincel`]). Imagen { huella: u64, xf: [f32; 6], }, RectDifuso { xf: [f32; 6], rect: [f32; 4], color: Rgba, radio: f32, desvio: f32, }, /// **El punto de W4**: texto como texto, sin shapear. Texto(OrdenTexto), } /// La escena serializable: una lista plana de órdenes, en orden de emisión. /// /// Es a la vez el formato del cable y una GRABADORA con las firmas de /// `vello::Scene`, igual que `llimphi_cpu::Escena`. Que las tres compartan /// firma es lo que deja elegir el backend con un alias de tipo (el truco de /// W3.b) en vez de con tres caminos de código paralelos. #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct EscenaWire { ordenes: Vec, /// Los estilos de texto distintos que usa esta escena, internados. /// /// Una pantalla se pinta con un puñado de estilos y cientos de etiquetas; /// ver el porqué medido en [`OrdenTexto`]. La deduplicación es un barrido /// lineal a propósito: con una tabla de menos de treinta entradas comparar /// es más barato que hashear, y evita meter `hashbrown` en un crate que va /// a correr adentro de una jaula WASM donde cada byte de código pesa. estilos: Vec, /// Órdenes que el IR no supo expresar. /// /// Existe porque un IR que descarta en silencio miente por omisión: la /// escena llega «bien» y la pantalla sale distinta, sin nada que lo diga. /// Con el contador, el que reproduce puede avisar. descartadas: u32, /// **Los píxeles de las imágenes que esta escena estrena**, por huella. /// /// Las órdenes viajan por huella ([`OrdenWire::Imagen`]) y los píxeles por /// acá, **una sola vez**: dentro de una escena se deduplica sola, y entre /// escenas lo hace quien envía, que es el único que sabe qué le mandó ya al /// anfitrión (ver `Jaula::escena_para_enviar` en `llimphi-jaula`). /// /// Sin esto el reproductor no tenía de dónde sacar los píxeles y **no /// dibujaba ninguna imagen** — las contaba en `imagenes_sin_blob` y seguía /// de largo. En el explorador enjaulado eso era toda miniatura y toda previa /// en negro. blobs: Vec, /// Blobs que **no se mandaron por pasarse de** [`TOPE_BLOB`]. Un cuadro que /// no entra en el marco es un cable muerto; una imagen que no se ve, con su /// contador, es un defecto que se puede nombrar. blobs_enormes: u32, } /// Tope de un blob de imagen, en bytes. /// /// El marco del cable tiene techo (`llimphi_cable::MAX_MARCO`, 64 MiB) y una /// escena lleva varias imágenes: una foto de 8000×6000 en RGBA son 192 MB y se /// llevaría puesto el cuadro entero — o sea la pantalla, no la imagen. Con el /// tope, lo que no entra no se manda y se **cuenta**. pub const TOPE_BLOB: usize = 16 * 1024 * 1024; impl EscenaWire { /// Una escena vacía. pub fn new() -> Self { Self::default() } /// Descarta lo grabado conservando la capacidad. Se llama entre fotogramas. pub fn reset(&mut self) { self.ordenes.clear(); self.estilos.clear(); self.descartadas = 0; } /// La tabla de estilos de texto internados. La indexa `OrdenTexto::estilo`. pub fn estilos(&self) -> &[EstiloTextoWire] { &self.estilos } /// El estilo de una orden de texto, resuelto. /// /// Devuelve `None` si el índice no existe — un IR corrupto o de una versión /// que no conocemos. El reproductor decide: saltear el texto o usar el /// default. Lo que no puede es entrar en pánico dentro del kernel. pub fn estilo(&self, idx: u16) -> Option<&EstiloTextoWire> { self.estilos.get(idx as usize) } /// Interna un estilo y devuelve su índice. fn internar(&mut self, estilo: &EstiloTextoWire) -> u16 { if let Some(i) = self.estilos.iter().position(|e| e == estilo) { return i as u16; } // Con más de 65.535 estilos distintos en una sola escena algo está muy // mal río arriba; se reusa el último en vez de romper el fotograma. if self.estilos.len() >= u16::MAX as usize { self.descartadas += 1; return (self.estilos.len() - 1) as u16; } self.estilos.push(estilo.clone()); (self.estilos.len() - 1) as u16 } /// Las órdenes grabadas, en orden. pub fn ordenes(&self) -> &[OrdenWire] { &self.ordenes } /// Cuántas órdenes tiene. pub fn len(&self) -> usize { self.ordenes.len() } /// ¿No se grabó nada? pub fn is_empty(&self) -> bool { self.ordenes.is_empty() } /// Cuántas órdenes se descartaron por no ser expresables. pub fn descartadas(&self) -> u32 { self.descartadas } /// Cuántos segmentos de camino acumula. Para medir el peso de una escena /// sin serializarla. pub fn segmentos(&self) -> usize { self.ordenes .iter() .map(|o| match o { OrdenWire::Rellenar { forma, .. } | OrdenWire::Trazar { forma, .. } => { forma.segmentos() } OrdenWire::AbrirCapa { recorte, .. } | OrdenWire::AbrirMascaraLuminancia { recorte, .. } => recorte.segmentos(), _ => 0, }) .sum() } /// Cuántas órdenes de texto lleva. La estadística que importa para W4.1. pub fn textos(&self) -> usize { self.ordenes .iter() .filter(|o| matches!(o, OrdenWire::Texto(_))) .count() } // ------------------------------------------------------------------------- // Las firmas de `vello::Scene`, para que el compositor no note la // diferencia. Ver `llimphi_cpu::Escena`, que hace lo mismo del otro lado. // ------------------------------------------------------------------------- /// Rellena una forma. Espeja `vello::Scene::fill`. pub fn fill<'b>( &mut self, style: Fill, transform: Affine, brush: impl Into>, brush_transform: Option, shape: &impl Shape, ) { let brush = brush.into().to_owned(); // **Un relleno CON pincel de imagen también pierde los píxeles**, y por // el mismo camino que `draw_image`: `PincelWire::de` se queda con la // huella y tira el buffer. Si no se graba acá, un relleno con textura // sale en negro aunque la imagen suelta se vea. if let peniko::Brush::Image(img) = &brush { let huella = huella_imagen(img); self.recordar_blob(huella, img); } let pincel = PincelWire::de(&brush); self.ordenes.push(OrdenWire::Rellenar { regla: ReglaWire::de(style), xf: afin_a_wire(transform), pincel, xf_pincel: brush_transform.map(afin_a_wire), forma: FormaWire::de(shape), }); } /// Traza el contorno de una forma. Espeja `vello::Scene::stroke`. pub fn stroke<'b>( &mut self, style: &kurbo::Stroke, transform: Affine, brush: impl Into>, brush_transform: Option, shape: &impl Shape, ) { let pincel = PincelWire::de(&brush.into().to_owned()); self.ordenes.push(OrdenWire::Trazar { ancho: style.width as f32, xf: afin_a_wire(transform), pincel, xf_pincel: brush_transform.map(afin_a_wire), forma: FormaWire::de(shape), }); } /// Abre una capa recortada. Espeja `vello::Scene::push_layer`. pub fn push_layer( &mut self, clip_style: Fill, blend: impl Into, alpha: f32, transform: Affine, clip: &impl Shape, ) { self.ordenes.push(OrdenWire::AbrirCapa { regla: ReglaWire::de(clip_style), mezcla: MezclaWire::de(blend.into()), alfa: alpha.clamp(0.0, 1.0), xf: afin_a_wire(transform), recorte: FormaWire::de(clip), }); } /// Abre una capa que enmascara por luminancia. pub fn push_luminance_mask_layer( &mut self, clip_style: Fill, alpha: f32, transform: Affine, clip: &impl Shape, ) { self.ordenes.push(OrdenWire::AbrirMascaraLuminancia { regla: ReglaWire::de(clip_style), alfa: alpha.clamp(0.0, 1.0), xf: afin_a_wire(transform), recorte: FormaWire::de(clip), }); } /// Cierra la última capa abierta. pub fn pop_layer(&mut self) { self.ordenes.push(OrdenWire::CerrarCapa); } /// Dibuja una imagen: la **orden** va por huella y los **píxeles** por la /// tabla de blobs, una sola vez. Ver [`Self::blobs`]. pub fn draw_image(&mut self, image: &peniko::ImageBrush, transform: Affine) { let huella = huella_imagen(image); self.recordar_blob(huella, image); self.ordenes.push(OrdenWire::Imagen { huella, xf: afin_a_wire(transform), }); } /// Guarda los píxeles de `image` si esta escena todavía no los trae. /// /// El barrido es lineal por lo mismo que el de estilos: una pantalla usa un /// puñado de imágenes distintas y comparar u64 es más barato que hashear en /// un crate que corre adentro de una jaula WASM. fn recordar_blob(&mut self, huella: u64, image: &peniko::ImageBrush) { if self.blobs.iter().any(|b| b.huella == huella) { return; } let b = pincel::blob_de(image); if b.datos.len() > TOPE_BLOB { self.blobs_enormes += 1; return; } self.blobs.push(b); } /// Los píxeles que esta escena trae, por huella. pub fn blobs(&self) -> &[BlobImagen] { &self.blobs } /// Cuántos blobs se dejaron afuera por pasarse de [`TOPE_BLOB`]. pub fn blobs_enormes(&self) -> u32 { self.blobs_enormes } /// **Deja sólo los blobs que `conservar` acepte.** /// /// Lo usa quien envía para no remandar lo que el anfitrión ya tiene: la /// escena se arma de cero en cada fotograma y no sabe nada de la conexión, /// así que la memoria de «esto ya lo mandé» vive del lado del que manda. pub fn retener_blobs(&mut self, mut conservar: impl FnMut(u64) -> bool) { self.blobs.retain(|b| conservar(b.huella)); } /// Dibuja un rectángulo redondeado desenfocado — la sombra. pub fn draw_blurred_rounded_rect( &mut self, transform: Affine, rect: kurbo::Rect, brush: Color, radius: f64, std_dev: f64, ) { self.ordenes.push(OrdenWire::RectDifuso { xf: afin_a_wire(transform), rect: [ rect.x0 as f32, rect.y0 as f32, rect.x1 as f32, rect.y1 as f32, ], color: color_a_wire(brush), radio: radius as f32, desvio: std_dev as f32, }); } /// **La orden que justifica el crate**: texto sin shapear. /// /// No espeja `vello::Scene::draw_glyphs` porque no puede: aquella recibe /// glifos ya resueltos, y el punto entero de W4.1 es no resolverlos de este /// lado. pub fn texto(&mut self, texto: Texto) { if texto.cadena.is_empty() { return; } let estilo = self.internar(&texto.estilo); self.ordenes.push(OrdenWire::Texto(OrdenTexto { cadena: texto.cadena, estilo, color: texto.color, ancho_max: texto.ancho_max, xf: texto.xf, hint: texto.hint, })); } /// Incorpora una escena hija, opcionalmente transformada. /// Espeja `vello::Scene::append`. /// /// Los índices de estilo de la hija se **remapean** contra la tabla de la /// madre. Es el precio de internar, y no es opcional: pegar las órdenes tal /// cual haría que cada texto de la hija apuntara a un estilo ajeno, con lo /// que un subárbol cacheado se repintaría con la tipografía de otro. El /// remapeo es una tabla de traducción por escena hija, no por orden. pub fn append(&mut self, otra: &Self, transform: Option) { self.descartadas += otra.descartadas; let mapa: Vec = otra .estilos .iter() .map(|e| self.internar(e)) .collect(); for o in &otra.ordenes { let mut o = match transform { None => o.clone(), Some(t) => trasladar(o, t), }; if let OrdenWire::Texto(t) = &mut o { t.estilo = mapa.get(t.estilo as usize).copied().unwrap_or(0); } self.ordenes.push(o); } } /// Anota que una orden no se pudo expresar. Lo llama el encoder cuando se /// topa con algo que el IR todavía no nombra. pub fn descartar(&mut self) { self.descartadas += 1; } /// Empuja una orden ya armada, sin pasar por los constructores. /// /// Salida de emergencia para el encoder —que a veces ya tiene la /// `OrdenWire` hecha— y para los tests que necesitan fabricar una escena /// inválida a propósito. **No interna estilos**: una orden de texto empujada /// así lleva el índice que le pusieron, exista o no en la tabla. Ésa es /// justamente la propiedad que hace falta para probar que el reproductor no /// confía en el cable. pub fn empujar_crudo(&mut self, orden: OrdenWire) { self.ordenes.push(orden); } } /// Aplica una transformación a una orden ya grabada. /// /// Se compone POR IZQUIERDA (`t * xf`), igual que en `llimphi_cpu`: la de la /// madre se aplica DESPUÉS de la de la hija. La del pincel se deja como está — /// es relativa a la forma, no al mundo. fn trasladar(orden: &OrdenWire, t: Affine) -> OrdenWire { let mut o = orden.clone(); let componer = |xf: &mut [f32; 6]| *xf = afin_a_wire(t * wire_a_afin(*xf)); match &mut o { OrdenWire::Rellenar { xf, .. } | OrdenWire::Trazar { xf, .. } | OrdenWire::AbrirCapa { xf, .. } | OrdenWire::AbrirMascaraLuminancia { xf, .. } | OrdenWire::Imagen { xf, .. } | OrdenWire::RectDifuso { xf, .. } => componer(xf), // El texto se compone por la MISMA matriz que todo lo demás; lo único // distinto es que su ubicación elige representación (ver // `UbicacionWire`), así que componer puede devolverla más barata. OrdenWire::Texto(t_orden) => t_orden.xf = t_orden.xf.componer(t), OrdenWire::CerrarCapa => {} } o } /// Modo de mezcla normal, el default. Atajo para el encoder. pub fn mezcla_normal() -> MezclaWire { MezclaWire::de(peniko::BlendMode::from(Mix::Normal)) } /// Un pincel sólido, el atajo más usado. pub fn solido(c: Color) -> Brush { Brush::Solid(c) } #[cfg(test)] mod pruebas_blobs { use super::*; /// Una imagen chiquita, del formato que usan las miniaturas del explorador. fn imagen(w: u32, h: u32, relleno: u8) -> peniko::ImageBrush { peniko::ImageBrush::new(peniko::ImageData { data: peniko::Blob::from(alloc::vec![relleno; (w * h * 4) as usize]), format: peniko::ImageFormat::Rgba8, alpha_type: peniko::ImageAlphaType::Alpha, width: w, height: h, }) } /// **Los píxeles cruzan.** Era lo único que faltaba: la orden viajaba por /// huella y del otro lado no había de dónde sacar la imagen, así que el /// reproductor la contaba y NO la dibujaba — toda miniatura en negro. #[test] fn los_pixeles_de_una_imagen_sobreviven_el_cable() { let img = imagen(4, 4, 0xAB); let mut e = EscenaWire::new(); e.draw_image(&img, Affine::IDENTITY); assert_eq!(e.blobs().len(), 1, "la escena tiene que traer el blob"); let bytes = postcard::to_allocvec(&e).expect("serializa"); let ida: EscenaWire = postcard::from_bytes(&bytes).expect("deserializa"); let b = &ida.blobs()[0]; assert_eq!(b.huella, huella_imagen(&img)); assert_eq!((b.ancho, b.alto), (4, 4)); assert_eq!(b.datos.len(), 4 * 4 * 4); // Y se reconstruye igual del otro lado: si el formato o el alfa se // perdieran, la imagen saldría con los canales cambiados en vez de no // salir — un defecto mucho más difícil de ver. let vuelta = imagen_de_blob(b); assert_eq!(huella_imagen(&vuelta), huella_imagen(&img)); } /// **La misma imagen dos veces pesa una.** Una grilla de miniaturas dibuja /// la misma textura en muchas órdenes; mandarla por orden sería multiplicar /// el cuadro por la cantidad de celdas. #[test] fn la_misma_imagen_no_se_guarda_dos_veces() { let img = imagen(4, 4, 7); let mut e = EscenaWire::new(); e.draw_image(&img, Affine::IDENTITY); e.draw_image(&img, Affine::translate((10.0, 0.0))); assert_eq!(e.blobs().len(), 1); assert_eq!(e.ordenes().len(), 2, "las ÓRDENES sí son dos"); } /// **Y entre cuadros la dedup la hace quien envía.** `retener_blobs` es lo /// que deja mandar los píxeles una sola vez por conexión: sin esto, cada /// fotograma remandaría todas las miniaturas visibles. #[test] fn retener_blobs_deja_pasar_solo_lo_que_no_se_mando() { let mut e = EscenaWire::new(); e.draw_image(&imagen(2, 2, 1), Affine::IDENTITY); e.draw_image(&imagen(2, 2, 2), Affine::IDENTITY); assert_eq!(e.blobs().len(), 2); let primera = e.blobs()[0].huella; e.retener_blobs(|h| h != primera); assert_eq!(e.blobs().len(), 1); assert_ne!(e.blobs()[0].huella, primera); } /// **Lo que no entra en el marco se CUENTA, no se manda.** Un cuadro que se /// pasa del tope del cable no pierde una imagen: pierde la pantalla. #[test] fn un_blob_enorme_no_viaja_y_queda_contado() { // 2048×2048 RGBA = 16 MiB, justo arriba del tope. let grande = imagen(2048, 2049, 0); let mut e = EscenaWire::new(); e.draw_image(&grande, Affine::IDENTITY); assert_eq!(e.blobs().len(), 0, "no viaja"); assert_eq!(e.blobs_enormes(), 1, "pero se dice"); assert_eq!(e.ordenes().len(), 1, "la orden va igual: el hueco se ve"); } } #[cfg(test)] mod pruebas;