El repo publico se habia quedado en julio: el README anunciaba vello 0.7 + wgpu 27 (dos bumps atras) y faltaban los crates del ultimo mes. - refresco con scripts/actualizar-standalone.py --llimphi (cargo check OK) - llegan llimphi-cpu, llimphi-hybrid, llimphi-glifos y llimphi-wire-escena: los cuatro caminos a pixeles con un solo compositor - MANUAL.md / SDD.md / LEEME.md sincronizados con el monorepo - README: versiones al dia, install por crates.io (cargo add llimphi), seccion "beyond the 2D widget kit" y el indice de crates que apuntaba al propio README ahora apunta a MANUAL.md §19
llimphi-wire-escena
The scene IR that crosses the WASM boundary: a flat list of shapes, paints
and unshaped text, serialized with postcard, no_std + alloc.
Phase W4 of 03_ukupacha/wawa/PLAN-LLIMPHI.md.
The problem
A wawa app is a .wasm module in a sandbox, and the interpreter penalty is
measured: 30.3x. W4 asked where to put the boundary between sandboxed and
native. The three possible answers, at 500 widgets:
| fps | widget catalogue | per-app size | |
|---|---|---|---|
Kernel-side (llimphi-wire-view) |
685 | ~15-18 of 41 entries | 12 KB |
| Scene IR (this crate) | 77 | all 69 widgets | ~12 KB + geometry |
| Sandbox-side rasterizing | 18 | all 69 widgets | 476 KB |
The scene IR doesn't reach native speed, but it delivers 77 fps with the full catalogue — comfortably above 60, and 4x better than rasterizing inside the sandbox. The reason: the expensive work (flattening curves, generating strips, rasterizing) is 79-93% of the frame and lands entirely on the native side.
Text is the one thing that isn't a stroke
Deliberately — it is what W4.1 measured:
| 500 widgets, live text | app shapes (glyphs in IR) | kernel shapes (text in IR) |
|---|---|---|
| total | 45.92 ms → 21.8 fps | 14.06 ms → 71 fps |
If the app shaped and shipped glyphs, parley would pay the 30.3x penalty and
eat 72% of the frame. Shipping the string, the kernel shapes natively and
text costs 1.09 ms instead of 32.94. Hence OrdenWire::Texto carries string +
style + rect, and there is no glyph variant.
The accepted cost: parley and swash live on the native side — meaning Ring 0
when that side is the kernel. Same bargain already struck for vello_cpu.
Which IR to use
llimphi-wire-view tree of BOXES host runs layout AND paints
llimphi-wire-escena list of STROKES host only rasterizes
wire-view is smaller on the wire and lets the host decide the look, but only
expresses what its vocabulary names: no gradients, shadows, clipping, z-index or
custom paint. Nodegraph, table, terminal and text-editor are out by
construction. wire-escena has no such ceiling because it doesn't talk about
widgets: it talks about strokes, and every widget ends in strokes.
Shape
EscenaWire is simultaneously the wire format and a recorder carrying the
eight vello::Scene signatures — the same ones llimphi_cpu::Escena mirrors.
All three matching is what allows picking a backend with a type alias instead of
three parallel code paths.
let mut escena = EscenaWire::new();
escena.fill(Fill::NonZero, Affine::IDENTITY, Color::BLACK, None, &rect);
escena.texto(Texto {
cadena: "hello".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)?; // this is what crosses
On the other side, llimphi_wawa::reproducir shapes and rasterizes it natively.
Two decisions the measurement corrected
Styles are interned. The first version put all 17 style fields in every text
order; at 500 labels text took half the wire (31,890 of 64,393 B) — not
because of the strings (~8 B each) but from repeating the style. In a real UI
style varies by role, not per label, so 500 labels yield one table entry
and a u16 per order. The price: append must remap the child's indices, or a
cached subtree repaints with someone else's typeface.
Text location picks its representation. UbicacionWire stores a point when
the affine is a pure translation — the normal case, 8 B — and the full matrix
when there is rotation or scale — 24 B, exact. Always storing the point would
have painted a rotated subtree's text upright with nothing to warn you; always
storing the matrix costs +17% of the frame for a case that almost never happens.
Images don't travel. OrdenWire::Imagen carries a 64-bit FNV-1a fingerprint
of the blob, not the pixels — shipping pixels would be exactly the 768 KB/frame
of the "pure sandbox" option W4 rejects. A 4 MB image weighs under 64 B on the
wire. Transferring the blob once is a separate channel.
Measured size
cargo test -p llimphi-wire-escena --release -- --nocapture, at 480x400:
| widgets | orders | bytes | KB | vs shipping pixels |
|---|---|---|---|---|
| 50 | 100 | 4,729 | 4.6 | 162x smaller |
| 200 | 400 | 18,930 | 18.5 | 41x smaller |
| 500 | 1,000 | 47,430 | 46.3 | 16x smaller |
Bare metal
cargo +nightly check -p llimphi-wire-escena --target x86_64-unknown-none \
--no-default-features --features libm -Z build-std=core,alloc
Same feature deal as llimphi-cpu, for the same reason: this crate's profile
must match kurbo's in the same build. Inside the workspace vello forces
std; on wawa there is no vello to unify anything.
The two ends of the cable
This is the cable; the plugs live apart, each on the side it belongs to:
| where | what it does | |
|---|---|---|
| encoder | llimphi-jaula |
runs the compositor in the sandbox and records here |
| replayer | llimphi-wawa |
deserializes, shapes natively, rasterizes |
Neither living in this crate isn't tidiness: they cannot be compiled
together. llimphi-text carries a compile_error! preventing it, because they
are two processes on two sides of a boundary, and pretending otherwise would hide
the one mistake that matters — shaping on the wrong side.