1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! DISPLAY — a pixel framebuffer surface that runs wasm cartridges.
//!
//! North star: Redox OS's **Orbital** display server. The canvas is the
//! screen (the scanout framebuffer); this module is the compositor /
//! display server; a wasm cartridge is an Orbital-style client app. The
//! `host_display` import module is the **Orbclient** analog — the draw
//! API a cartridge calls.
//!
//! ## Model: host-owned framebuffer + draw commands
//! A wasm module can't touch the canvas, GPU, or DOM — it only has linear
//! memory and the imports we grant it. So the host owns the framebuffer
//! and the cartridge issues draw commands into it, then flips it to the
//! screen. This fits rustlite (no arrays / raw memory needed — just
//! integer host calls), so an *agent-written* cartridge can draw.
//!
//! ## Cartridge ABI (`host_display`)
//! - `clear(rgb)` — fill the whole framebuffer (`0xRRGGBB`, opaque)
//! - `set_pixel(x, y, rgb)`
//! - `fill_rect(x, y, w, h, rgb)`
//! - `draw_char(x, y, codepoint, rgb, scale)` — one 5x7 glyph, scaled
//! - `draw_number(x, y, value, rgb, scale)` — a decimal integer
//! - `present()` — flush the framebuffer to the canvas
//! - `width() -> i32`, `height() -> i32`
//! - `pointer_x() -> i32`, `pointer_y() -> i32` — cursor position in
//! framebuffer coordinates (poll model, like Orbclient's event queue)
//! - `pointer_down() -> i32` — 1 while the primary button is pressed
//! - `state_get(slot) -> i32`, `state_set(slot, value)` — a 64-slot
//! integer register file that persists across `frame` calls (rustlite
//! has no globals, so this is how a cartridge keeps state)
//!
//! ## Cartridge ABI (`host_audio`) — Web Audio playback (see `bridge::audio`)
//! Module-qualified spelling: call `audio::tone(...)`, NOT a bare `tone`.
//! Integer ABI, fire-and-forget like `host_net`; silent until the first
//! user gesture (browser AudioContext rule).
//! - `tone(freq_hz, dur_ms, wave) -> handle` — `wave`: 0 sine, 1 square,
//! 2 sawtooth, 3 triangle; returns a voice handle >= 0, or -1
//! - `tone_at(freq_hz, dur_ms, wave, delay_ms) -> handle` — schedule a
//! tone `delay_ms` ahead (sequence a bar of notes from one `frame`)
//! - `noise(dur_ms) -> handle` — white-noise burst (hats / explosions)
//! - `stop(handle)` — stop one voice; `stop(-1)` stops every voice
//! - `set_volume(pct)` — master gain, `pct` clamped 0..=100
//!
//! A cartridge exports `memory` and either an animated `frame(t: i32)`
//! (driven by `requestAnimationFrame`, `t` = elapsed ms) or a one-shot
//! `render()`.
//!
//! The Closures here are the wasm↔host runtime bridge, not UI/DOM event
//! handling — a wasm import *must* be a JS function. They live only in
//! this module and never build DOM, so the app's "no imperative DOM"
//! rule is untouched.
//!
//! ## Module map
//! - [`worker`] — worker spawn / watchdog / run-outcome lifecycle + the
//! onmessage router (the off-main-thread containment, the brick fix).
//! - [`surface`] — canvas mount, overlay chrome, pointer/touch state,
//! embed-card plumbing, and the broadcast-composer UI.
//! - [`bridge`] — one module per host capability the worker round-trips to
//! the main thread (feed / compose / http / mp / chat / audio).
//! - The pure HTML→framebuffer rasterizer lives at [`crate::html_fb`]
//! (native-tested); this module only blits its output.
use ;
use *;
use ;
use ;
pub use prime_feed_permission_on_gesture;
pub use ;
/// DEFAULT logical framebuffer resolution: 512×512 (square). A cartridge that
/// does NOT export `dims()` renders at this size (backward compatible). The canvas
/// backing store is sized to this initially; CSS scales it up with
/// `image-rendering: pixelated` so individual pixels stay crisp.
///
/// ## Variable resolution
/// A cartridge MAY export `dims() -> i32` returning a packed `(w << 16) | h`
/// (width high 16 bits, height low 16). The WORKER reads it after instantiate,
/// allocates a framebuffer of that size, and STAMPS every `frame` message with
/// the actual `w`/`h`. The main thread ([`worker::blit_frame`]) sizes the
/// canvas backing store from those per-frame dims and builds the `ImageData`
/// at `w`×`h` — so the host adapts to whatever the cartridge declared without
/// the main thread ever calling `dims()` itself. Dimensions are validated +
/// clamped to `[16, 1024]` in the worker; out of range falls back to the
/// default. These consts remain the default + the composition/HTML-render path
/// (single fixed surface).
// Single-sourced from the loader (`rustlite::loader::DEFAULT_FB_*`), so the
// compile-check surface and the real display can't disagree about the default.
const FB_W: u32 = crateDEFAULT_FB_W as u32;
const FB_H: u32 = crateDEFAULT_FB_H as u32;
thread_local!
/// Instantiate `wasm_bytes` as a display cartridge in the display
/// overlay (swaps in the overlay + surface). Used by the `run_cartridge`
/// tool and opening a `.wasm`/`.rl` from the files modal.
pub async
/// A cartridge run that never went live: the stable `LH1xxx` runtime code
/// (when the worker/watchdog produced one) + the human detail. What the
/// `run_cartridge` tool folds into its structured error result.
pub
/// How long [`run_wasm_reporting`] waits for the cartridge's FIRST lifecycle
/// signal before giving up. Must exceed the worker watchdog's window
/// (`WATCHDOG_MS` + one `WATCHDOG_TICK_MS` poll = 2000ms) so a first-frame
/// hang is reported as the watchdog's coded LH1001 kill, not as this
/// wrapper's vaguer timeout.
const FIRST_SIGNAL_MS: u32 = 2600;
/// Run `wasm_bytes` as a cartridge INLINE in the chat transcript (issue #52a)
/// rather than auto-opening the fullscreen overlay: the user strongly prefers
/// inline-by-default, fullscreen opt-in. This stashes the bytes for the
/// `run_cartridge` inline card (`launch_pending_embed`, the SAME path
/// `embed_app` uses) AND remembers them so the card's [fullscreen] button can
/// relaunch the SAME cartridge into the overlay. It does NOT mount the overlay.
///
/// `run_cartridge`'s "report the first frame" contract (issue #7) can't be
/// honoured before the card paints (the canvas doesn't exist yet), so this
/// returns `Ok(())` once the bytes are stashed; the inline launch surfaces a
/// dead/blank canvas if the cartridge fails, the same way an `embed_app` card
/// does. Fire-and-forget overlay callers (public-face boot, opening a file)
/// keep using `run_wasm` / `run_wasm_reporting`.
pub
/// [`run_wasm`], but AWAIT the cartridge's first lifecycle signal and report
/// it (issue #7): `Ok(())` once the first frame (or a one-shot `done`)
/// lands, `Err(RunFailure)` when the worker posts a coded fatal error
/// (instantiate failure / trap / missing entry) or the watchdog kills a hung
/// first frame. The old fire-and-forget `run_wasm` told the agent "running
/// on display" even when the canvas was painting "CARTRIDGE STOPPED".
///
/// A healthy cartridge posts its first frame within a few ms, so the await
/// costs success paths almost nothing; only failures wait (bounded by
/// [`FIRST_SIGNAL_MS`]). Fire-and-forget callers (public-face boot, opening
/// a file) keep using `run_wasm` — the overlay is their reporting surface.
pub async
/// Mount the overlay + await the cartridge's first lifecycle signal — the
/// OVERLAY reporting path (issue #7), retained for callers that still want a
/// fullscreen run with a hard pass/fail (none ship today; kept so the
/// first-frame watchdog plumbing has a home and isn't dead code).
pub async
/// Instantiate `wasm_bytes` against an existing `#display-canvas`
/// already in the DOM (app mode — the subdomain booted straight into a
/// fullscreen cartridge, no overlay swap).
pub async
/// THE EMBED SEAM: run `wasm_bytes` as a cartridge targeting ANY canvas in
/// the DOM (not just the fullscreen `#display-canvas` overlay) — what the
/// `embed_app` agent tool uses to render another subdomain's published
/// cartridge as a live, interactive card INLINE in the chat transcript.
/// `run_in_root_canvas` is the thin specialization (it just resolves
/// `#display-canvas` first); both funnel into the SAME `run_with_ctx` →
/// `mod worker` path, so an embed and the overlay share the single `WORKER`
/// slot.
///
/// ## v1 constraint: ONE cartridge at a time (single worker)
/// There is exactly one [`worker::WORKER`] slot, so starting a cartridge here
/// REPLACES any cartridge already running — the overlay's, or a prior embed's.
/// A second `embed_app` in the same transcript supersedes the first (the first
/// card goes inert: its canvas keeps its last painted frame but stops
/// updating). That's acceptable for v1 (one live interactive embed); true
/// concurrent embeds need a per-canvas worker registry, tracked as follow-up.
///
/// ## Variable framebuffer resolution
/// The canvas BACKING STORE is sized to the DEFAULT [`FB_W`]×[`FB_H`] (512×512)
/// here as an initial size, but it is RESIZED to the cartridge's actual dims
/// the moment its first `frame` message arrives ([`worker::blit_frame`] reads
/// the `w`/`h` the worker stamped on each frame and resizes the canvas + builds
/// the `ImageData` at `w`×`h`). A cartridge declares its size by exporting
/// `dims() -> i32` (packed `(w<<16)|h`); with no such export it stays 512×512
/// (backward compatible). CSS scales the canvas ELEMENT to the card box with
/// `image-rendering: pixelated`; aspect-preserving letterboxing comes from the
/// stylesheet's `max-width/height:100%` + `object-fit` on the canvas element.
pub async
/// Render an HTML document into the framebuffer as pixels (no DOM, no
/// iframe) — the loader's "universal" path alongside `.wasm` cartridges.
/// A block-level subset (headings, paragraphs, lists, breaks) is laid out
/// with word-wrap and blitted via the bitmap font, monochrome. This is a
/// *snapshot* render: no CSS box model, images, colors, or scripts. Stops
/// any running cartridge first so its frame loop can't blit over the page.
pub
/// Render an HTML document into the **root** `#display-canvas` (app mode
/// — the subdomain booted straight into a fullscreen public face), the
/// HTML counterpart of [`run_in_root_canvas`]. Same block-level snapshot
/// render as [`render_html`], just targeting the already-mounted canvas
/// instead of swapping in the workshop view panel.
pub
/// Shared core: run the cartridge OFF the main thread in a Web Worker so a
/// hung/unbounded `frame()` can't freeze the app, and start the watchdog that
/// terminates a worker which stops posting frames. The worker re-implements the
/// `host_display` ABI faithfully (`web/cartridge-worker.js`); the main thread
/// only blits the framebuffer it posts, feeds input, and plays forwarded audio.
///
/// This is the BRICK FIX: a cartridge persisted as the subdomain's public face
/// can no longer wedge the tab (chat included) — synchronous wasm is
/// un-preemptable from JS, so the only containment is "run it elsewhere + be
/// able to kill it", which the worker + watchdog provide. The `?compose=`
/// composition path (`mount_composition`) runs in the SAME worker (issue #77) —
/// a composed child is untrusted wasm too, so it must be contained off-thread.
async
/// Composite several published cartridges into ONE framebuffer, iframe-free —
/// the `?compose=name1,name2,…` path (roadmap Track A). Runs in the SAME isolated
/// Web Worker + main-thread watchdog as the single-cartridge path (issue #77): a
/// composed child is UNTRUSTED wasm too, so a hung `frame()` must only stall the
/// worker, never the main thread. This previously ran each child's `frame()`
/// DIRECTLY on the main thread (an in-thread `start_compose_loop`), which had no
/// isolation and re-bricked the tab.
///
/// `names` are the requested subdomains in order. The main thread lays them out
/// in a near-square grid via the native-tested [`crate::compose::grid_viewports`]
/// and hands the tiles to the worker, which mounts each as a compose-tree child
/// and resolves its published on-chain `app.wasm` through the EXISTING
/// `compose_spawn` / `compose_bytes` round-trip (a child that hasn't published an
/// app just stays a black cell). Admission stays capped by the worker's mirror of
/// [`crate::compose::ComposeBudget`] so an attacker-chosen graph can't exhaust it.
pub async
thread_local!
/// Remember what cartridge is about to run (for crash-report context). Call at a
/// launch site BEFORE the worker can fail — `run_cartridge` (the source),
/// `embed_app` (the name), and the history resume path. Capped so a huge source
/// can't crowd the rest of the report (the proxy clamps the body anyway).
pub
/// The current cartridge reference (for `worker::record_outcome`'s crash report).
pub
// NOTE: the old single-cartridge in-thread `start_frame_loop` AND the in-thread
// `start_compose_loop` (the `?compose=` compositor) were removed — BOTH the
// single-cartridge path and the `?compose=` composition now run in a Web Worker
// (see `mod worker` + `web/cartridge-worker.js`) so a hung `frame()` can't freeze
// the main thread. The compose tree (recursion, budget caps, focus) lives in the
// worker; the main thread only blits frames, forwards input, and runs the
// watchdog. This closed issue #77 (untrusted compose wasm on the main thread).
/// Stop any running cartridge (e.g. when the surface is closed).
pub