tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ 168cd187519c0ad575d3ce63baee2f164c38d9d1 llimphi-wire-escena/README.md Markdown · 130 líneas · 5.5 KB Historia crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1# llimphi-wire-escena
2
3The **scene** IR that crosses the WASM boundary: a flat list of shapes, paints
4and **unshaped text**, serialized with postcard, `no_std + alloc`.
5
6Phase W4 of [`03_ukupacha/wawa/PLAN-LLIMPHI.md`](../../../03_ukupacha/wawa/PLAN-LLIMPHI.md).
7
8## The problem
9
10A wawa app is a `.wasm` module in a sandbox, and the interpreter penalty is
11measured: **30.3x**. W4 asked where to put the boundary between sandboxed and
12native. The three possible answers, at 500 widgets:
13
14| | fps | widget catalogue | per-app size |
15|---|---|---|---|
16| Kernel-side (`llimphi-wire-view`) | 685 | ~15-18 of 41 entries | 12 KB |
17| **Scene IR** (this crate) | **77** | **all 69 widgets** | ~12 KB + geometry |
18| Sandbox-side rasterizing | 18 | all 69 widgets | 476 KB |
19
20The scene IR doesn't reach native speed, but it delivers 77 fps **with the full
21catalogue** — comfortably above 60, and 4x better than rasterizing inside the
22sandbox. The reason: the expensive work (flattening curves, generating strips,
23rasterizing) is 79-93% of the frame and lands entirely on the native side.
24
25## Text is the one thing that isn't a stroke
26
27Deliberately — it is what W4.1 measured:
28
29| 500 widgets, live text | app shapes (glyphs in IR) | **kernel shapes (text in IR)** |
30|---|---|---|
31| total | 45.92 ms → **21.8 fps** | 14.06 ms → **71 fps** |
32
33If the app shaped and shipped glyphs, `parley` would pay the 30.3x penalty and
34eat 72% of the frame. Shipping **the string**, the kernel shapes natively and
35text costs 1.09 ms instead of 32.94. Hence `OrdenWire::Texto` carries string +
36style + rect, and there is **no glyph variant**.
37
38The accepted cost: `parley` and `swash` live on the native side — meaning Ring 0
39when that side is the kernel. Same bargain already struck for `vello_cpu`.
40
41## Which IR to use
42
43```
44llimphi-wire-view    tree of BOXES     host runs layout AND paints
45llimphi-wire-escena  list of STROKES   host only rasterizes
46```
47
48`wire-view` is smaller on the wire and lets the host decide the look, but only
49expresses what its vocabulary names: no gradients, shadows, clipping, z-index or
50custom paint. Nodegraph, table, terminal and text-editor are out **by
51construction**. `wire-escena` has no such ceiling because it doesn't talk about
52widgets: it talks about strokes, and every widget ends in strokes.
53
54## Shape
55
56`EscenaWire` is simultaneously the wire format and a **recorder carrying the
57eight `vello::Scene` signatures** — the same ones `llimphi_cpu::Escena` mirrors.
58All three matching is what allows picking a backend with a type alias instead of
59three parallel code paths.
60
61```rust
62let mut escena = EscenaWire::new();
63escena.fill(Fill::NonZero, Affine::IDENTITY, Color::BLACK, None, &rect);
64escena.texto(Texto {
65    cadena: "hello".into(),
66    estilo: EstiloTextoWire { tamano_px: 14.0, ..Default::default() },
67    color: [255, 255, 255, 255],
68    ancho_max: None,
69    xf: UbicacionWire::Punto([10.0, 4.0]),
70    hint: true,
71});
72let bytes = postcard::to_allocvec(&escena)?;   // this is what crosses
73```
74
75On the other side, `llimphi_wawa::reproducir` shapes and rasterizes it natively.
76
77## Two decisions the measurement corrected
78
79**Styles are interned.** The first version put all 17 style fields in every text
80order; at 500 labels text took **half the wire** (31,890 of 64,393 B) — not
81because of the strings (~8 B each) but from repeating the style. In a real UI
82style varies by *role*, not per label, so 500 labels yield **one** table entry
83and a `u16` per order. The price: `append` must remap the child's indices, or a
84cached subtree repaints with someone else's typeface.
85
86**Text location picks its representation.** `UbicacionWire` stores a point when
87the affine is a pure translation — the normal case, 8 B — and the full matrix
88when there is rotation or scale — 24 B, exact. Always storing the point would
89have painted a rotated subtree's text upright *with nothing to warn you*; always
90storing the matrix costs +17% of the frame for a case that almost never happens.
91
92**Images don't travel.** `OrdenWire::Imagen` carries a 64-bit FNV-1a fingerprint
93of the blob, not the pixels — shipping pixels would be exactly the 768 KB/frame
94of the "pure sandbox" option W4 rejects. A 4 MB image weighs under 64 B on the
95wire. Transferring the blob once is a separate channel.
96
97## Measured size
98
99`cargo test -p llimphi-wire-escena --release -- --nocapture`, at 480x400:
100
101| widgets | orders | bytes | KB | vs shipping pixels |
102|---|---|---|---|---|
103| 50 | 100 | 4,729 | 4.6 | **162x** smaller |
104| 200 | 400 | 18,930 | 18.5 | **41x** smaller |
105| 500 | 1,000 | 47,430 | 46.3 | **16x** smaller |
106
107## Bare metal
108
109```bash
110cargo +nightly check -p llimphi-wire-escena --target x86_64-unknown-none \
111  --no-default-features --features libm -Z build-std=core,alloc
112```
113
114Same feature deal as `llimphi-cpu`, for the same reason: this crate's profile
115must match `kurbo`'s in the same build. Inside the workspace `vello` forces
116`std`; on wawa there is no vello to unify anything.
117
118## The two ends of the cable
119
120This is the cable; the plugs live apart, each on the side it belongs to:
121
122| | where | what it does |
123|---|---|---|
124| **encoder** | [`llimphi-jaula`](../llimphi-jaula/README.md) | runs the compositor in the sandbox and records here |
125| **replayer** | [`llimphi-wawa`](../llimphi-wawa/README.md) | deserializes, shapes natively, rasterizes |
126
127Neither living in this crate isn't tidiness: **they cannot be compiled
128together**. `llimphi-text` carries a `compile_error!` preventing it, because they
129are two processes on two sides of a boundary, and pretending otherwise would hide
130the one mistake that matters — shaping on the wrong side.

README.md

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:

fpswidget catalogueper-app size
Kernel-side (llimphi-wire-view)685~15-18 of 41 entries12 KB
Scene IR (this crate)77all 69 widgets~12 KB + geometry
Sandbox-side rasterizing18all 69 widgets476 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 textapp shapes (glyphs in IR)kernel shapes (text in IR)
total45.92 ms → 21.8 fps14.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:

widgetsordersbytesKBvs shipping pixels
501004,7294.6162x smaller
20040018,93018.541x smaller
5001,00047,43046.316x 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:

wherewhat it does
encoderllimphi-jaularuns the compositor in the sandbox and records here
replayerllimphi-wawadeserializes, 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.