Files
Sergio d251eb32b6 chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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
2026-08-06 17:26:11 +00:00
..

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.