# llimphi-wire-escena El IR de **escena** que cruza la frontera WASM: una lista plana de formas, pinceles y **texto sin shapear**, serializable con postcard, `no_std + alloc`. W4 de [`03_ukupacha/wawa/PLAN-LLIMPHI.md`](../../../03_ukupacha/wawa/PLAN-LLIMPHI.md). ## El problema Una app de wawa es un módulo `.wasm` en una jaula, y la penalidad del intérprete está medida: **30,3x**. La pregunta de W4 era dónde poner la frontera entre lo que corre enjaulado y lo que corre nativo. Las tres respuestas posibles, a 500 widgets: | | fps | catálogo | peso por app | |---|---|---|---| | Kernel puro (`llimphi-wire-view`) | 685 | ~15-18 de 41 entries | 12 KB | | **IR de escena** (este crate) | **77** | **los 69 widgets** | ~12 KB + geometría | | Jaula pura (rasterizar adentro) | 18 | los 69 widgets | 476 KB | El IR de escena no da velocidad nativa, pero da 77 fps **con el catálogo completo** — cómodo sobre 60, y 4x mejor que rasterizar en la jaula. La razón: el trabajo 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 sin despeinarse. ## El texto es lo único que no es un trazo Y es a propósito — es la conclusión que W4.1 midió: | 500 widgets, texto vivo | app shapea (IR con glifos) | **kernel shapea (IR con texto)** | |---|---|---| | total | 45,92 ms → **21,8 fps** | 14,06 ms → **71 fps** | Si la app shapeara y mandara glifos, `parley` pagaría el 30,3x y se llevaría el 72% del fotograma. Mandando **la cadena**, el kernel shapea nativo y el texto cuesta 1,09 ms en vez de 32,94. Por eso `OrdenWire::Texto` lleva cadena + estilo + rect, y **no hay variante de glifos**. El costo aceptado: `parley` y `swash` viven del lado nativo — o sea en Ring 0 cuando ese lado es el kernel. Es el mismo trato que ya se hizo con `vello_cpu`. ## Cuál de los dos IR usar ``` 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: sin gradientes, sombras, clip, z-index ni paint custom. 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. ## Forma `EscenaWire` es a la vez el formato del cable y una **grabadora con las ocho firmas de `vello::Scene`** — las mismas que espeja `llimphi_cpu::Escena`. Que las tres coincidan es lo que deja elegir backend con un alias de tipo en vez de con tres caminos de código paralelos. ```rust let mut escena = EscenaWire::new(); escena.fill(Fill::NonZero, Affine::IDENTITY, Color::BLACK, None, &rect); escena.texto(Texto { cadena: "hola".into(), estilo: EstiloTextoWire { tamano_px: 14.0, ..Default::default() }, color: [255, 255, 255, 255], ancho_max: None, xf: UbicacionWire::Punto([10.0, 4.0]), hint: true, }); let bytes = postcard::to_allocvec(&escena)?; // esto es lo que cruza ``` Del otro lado, `llimphi_wawa::reproducir` lo shapea y rasteriza nativo. ## Dos decisiones que la medición corrigió **El estilo se interna.** La primera versión metía los 17 campos de estilo en cada orden de texto; a 500 etiquetas el texto se llevaba **la mitad del cable** (31.890 de 64.393 B) y no por las cadenas —que son ~8 B— sino por repetir el estilo. En una UI real el estilo varía por *rol*, no por etiqueta, así que 500 etiquetas dan **una** entrada de tabla y un `u16` por orden. El precio: `append` tiene que remapear los índices de la hija, o un subárbol cacheado se repinta con la tipografía de otro. **La ubicación del texto elige representación.** `UbicacionWire` guarda un punto cuando el afín es una traslación pura —el caso normal, 8 B— y la matriz entera cuando hay rotación o escala —24 B, exacto—. Guardar siempre el punto habría pintado derecho el texto de un subárbol rotado *sin que nada avisara*; guardar siempre la matriz cuesta +17% de fotograma por un caso que casi nunca ocurre. **La imagen no viaja.** `OrdenWire::Imagen` lleva una huella FNV-1a de 64 bits del blob, no los píxeles — mandar píxeles sería exactamente el 768 KB/fotograma de la «jaula pura» que W4 descarta. Una imagen de 4 MB pesa menos de 64 B en el cable. Transferir el blob la primera vez es un canal aparte. ## Peso medido `cargo test -p llimphi-wire-escena --release -- --nocapture`, @480x400: | widgets | órdenes | bytes | KB | vs mandar píxeles | |---|---|---|---|---| | 50 | 100 | 4.729 | 4,6 | **162x** menos | | 200 | 400 | 18.930 | 18,5 | **41x** menos | | 500 | 1.000 | 47.430 | 46,3 | **16x** menos | ## Bare-metal ```bash cargo +nightly check -p llimphi-wire-escena --target x86_64-unknown-none \ --no-default-features --features libm -Z build-std=core,alloc ``` Mismo trato de features que `llimphi-cpu`, y por la misma razón: el perfil del crate tiene que coincidir con el de `kurbo` en el mismo build. En el workspace `vello` fuerza `std`; en wawa no hay vello que unifique nada. ## Los dos extremos del cable Éste es el cable; los enchufes viven aparte, y cada uno en el lado al que pertenece: | | dónde | qué hace | |---|---|---| | **encoder** | [`llimphi-jaula`](../llimphi-jaula/LEEME.md) | corre el compositor en la jaula y graba acá | | **reproductor** | [`llimphi-wawa`](../llimphi-wawa/LEEME.md) | deserializa, shapea nativo y rasteriza | Que ninguno de los dos esté en este crate no es prolijidad: **no pueden compilarse juntos**. `llimphi-text` tiene un `compile_error!` que lo impide, porque son dos procesos en dos lados de una frontera y fingir lo contrario escondería el único error que importa — shapear del lado equivocado.