Skip to main content

gasm_host/
host.rs

1//! The gasm ABI v0 host: instantiates a guest and implements the "gasm",
2//! "gasm:gfx", "gasm:net" and "gasm:storage" imports, plus the WASI subset in
3//! [`crate::wasi`].
4
5use std::collections::{HashMap, HashSet};
6use std::future::Future;
7use std::pin::Pin;
8use std::sync::{Arc, OnceLock};
9use std::task::Poll;
10use std::time::{Duration, Instant};
11
12use wasmtime::{Caller, Config, Engine, Instance, Linker, Memory, Module, Store, TypedFunc, bail, format_err};
13
14use crate::assets::Assets;
15use crate::audio::AudioOut;
16use crate::gfx::Gfx;
17use crate::net::Net;
18use crate::present::letterbox;
19use crate::storage::Storage;
20use crate::switching::{self, Cmd, Exchange, Job};
21use crate::wasi::{self, Splitmix};
22
23pub const ABI_VERSION: i32 = 0;
24
25/// FNV-1a 32-bit; mirrored in runners/web/gasm-host.js for cross-runner checks.
26#[derive(Clone, Copy)]
27pub struct Fnv(pub u32);
28
29impl Default for Fnv {
30    fn default() -> Self {
31        Fnv(0x811c_9dc5)
32    }
33}
34
35impl Fnv {
36    pub fn new() -> Self {
37        Self::default()
38    }
39    pub fn update(&mut self, bytes: &[u8]) {
40        let mut h = self.0;
41        for &b in bytes {
42            h ^= b as u32;
43            h = h.wrapping_mul(0x0100_0193);
44        }
45        self.0 = h;
46    }
47}
48
49/// Headless virtual time: derived from the frame number, monotonic when the
50/// guest changes its frame rate. Same arithmetic in gasm-host.js
51/// (`VirtualClock`) and the `gasm::native` stub.
52#[derive(Clone, Copy)]
53pub struct VirtualClock {
54    base_ms: f64,
55    base_frame: u64,
56    rate: f64,
57}
58
59impl Default for VirtualClock {
60    fn default() -> Self {
61        VirtualClock { base_ms: 0.0, base_frame: 0, rate: 60.0 }
62    }
63}
64
65impl VirtualClock {
66    /// Time at the start of `frame` (frames are counted from 0) at frame rate `rate`.
67    pub fn at(&mut self, frame: u64, rate: f64) -> f64 {
68        if rate != self.rate {
69            self.base_ms += (frame - self.base_frame) as f64 * 1000.0 / self.rate;
70            self.base_frame = frame;
71            self.rate = rate;
72        }
73        self.base_ms + (frame - self.base_frame) as f64 * 1000.0 / self.rate
74    }
75}
76
77/// Raw input for one frame, set by the runner before each `gasm_frame`.
78/// `None` fields mean the runner has no such device (the import returns -1).
79#[derive(Default, Clone)]
80pub struct RawInput {
81    /// held keys, bit k%8 of byte k/8 = GASM_KEY code k
82    pub keys: Option<[u8; KEY_STATE_BYTES]>,
83    /// (code, down) since the previous frame, in order
84    pub key_events: Vec<(u16, bool)>,
85    pub pointer: Option<Pointer>,
86    pub gamepads: Option<[Gamepad; 4]>,
87}
88
89pub const KEY_STATE_BYTES: usize = 32;
90pub const POINTER_BYTES: usize = 48;
91pub const GAMEPAD_BYTES: usize = 204;
92pub const GAMEPAD_BUTTONS: usize = 32;
93pub const GAMEPAD_AXES: usize = 16;
94
95#[derive(Default, Clone, Copy)]
96pub struct Pointer {
97    /// position in drawable pixels; the frame position is derived by the host
98    pub x: f32,
99    pub y: f32,
100    pub dx: f32,
101    pub dy: f32,
102    pub wheel_x: f32,
103    pub wheel_y: f32,
104    pub buttons: u32,
105    pub pressed: u32,
106    pub released: u32,
107    /// GASM_POINTER_INSIDE | IS_HIDDEN | IS_LOCKED
108    pub flags: u32,
109    /// drawable size the position refers to
110    pub drawable: (f32, f32),
111    /// the runner shows 2D frames at whole multiples (`--integer-scale`)
112    pub integer_scale: bool,
113}
114
115#[derive(Default, Clone)]
116pub struct Gamepad {
117    pub connected: bool,
118    pub standard: bool,
119    pub buttons: Vec<f32>,
120    pub axes: Vec<f32>,
121    pub name: String,
122}
123
124/// Map a drawable position into the last video_present frame (letterboxed as the
125/// runners display it, `present::letterbox`). Same arithmetic in runners/web/lib/input.js.
126pub fn frame_position(x: f32, y: f32, drawable: (f32, f32), frame: (usize, usize), integer_scale: bool, aspect: Option<(u32, u32)>) -> (f32, f32) {
127    if frame.0 == 0 || frame.1 == 0 || drawable.0 <= 0.0 || drawable.1 <= 0.0 {
128        return (x, y);
129    }
130    let (ox, oy, sx, sy) = letterbox((drawable.0 as f64, drawable.1 as f64), (frame.0 as f64, frame.1 as f64), integer_scale, aspect);
131    (((x as f64 - ox) / sx) as f32, ((y as f64 - oy) / sy) as f32)
132}
133
134/// Longest title `set_title` keeps, in UTF-8 bytes.
135pub const TITLE_MAX_BYTES: usize = 256;
136
137/// A guest's `set_title` text as runners show it: control characters (Unicode
138/// Cc) and bidi controls (U+202A–U+202E, U+2066–U+2069) removed, then cut to
139/// 256 bytes at a character boundary. None: empty, the runner's default.
140/// Same rules in runners/web/lib/host.js `cleanTitle`.
141pub fn clean_title(text: &str) -> Option<String> {
142    let mut out = String::new();
143    for c in text.chars().filter(|&c| !c.is_control() && !matches!(c, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')) {
144        if out.len() + c.len_utf8() > TITLE_MAX_BYTES {
145            break;
146        }
147        out.push(c);
148    }
149    (!out.is_empty()).then_some(out)
150}
151
152/// The payload of custom section `name` in a wasm binary (None for a .cwasm or
153/// a malformed module: the loader reports those).
154pub fn custom_section<'a>(wasm: &'a [u8], name: &str) -> Option<&'a [u8]> {
155    fn leb(b: &[u8], at: &mut usize) -> Option<usize> {
156        let (mut v, mut shift) = (0usize, 0);
157        loop {
158            let byte = *b.get(*at)?;
159            *at += 1;
160            v |= ((byte & 0x7f) as usize).checked_shl(shift)?;
161            if byte & 0x80 == 0 {
162                return Some(v);
163            }
164            shift += 7;
165            if shift > 28 {
166                return None;
167            }
168        }
169    }
170    if wasm.get(..4)? != b"\0asm" {
171        return None;
172    }
173    let mut at = 8;
174    while at < wasm.len() {
175        let id = wasm[at];
176        at += 1;
177        let size = leb(wasm, &mut at)?;
178        let body = wasm.get(at..at.checked_add(size)?)?;
179        at += size;
180        if id == 0 {
181            let mut p = 0;
182            let n = leb(body, &mut p)?;
183            if body.get(p..p.checked_add(n)?)? == name.as_bytes() {
184                return body.get(p + n..);
185            }
186        }
187    }
188    None
189}
190
191/// The guest's built-in title (custom section `gasm.title`, UTF-8), cleaned like
192/// set_title: the default title before (or without) set_title.
193pub fn static_title(wasm: &[u8]) -> Option<String> {
194    clean_title(std::str::from_utf8(custom_section(wasm, "gasm.title")?).ok()?)
195}
196
197pub struct Host {
198    memory: Option<Memory>,
199    start: Instant,
200    /// Headless runs use frame-derived time so results are reproducible (also
201    /// the WASI clocks).
202    pub virtual_time_ms: Option<f64>,
203    /// Headless runs: a fixed `random_get` sequence; None = OS randomness.
204    pub random: Option<Splitmix>,
205    pub frame_rate: f64,
206    pub pads: [u32; 4],
207    /// UTF-8 typed since the previous frame (set by the runner before each frame);
208    /// None: no keyboard, text_input returns -1
209    pub text: Option<String>,
210    pub input: RawInput,
211    /// GASM_INPUT_* flags requested by the guest (input_mode)
212    pub input_mode: u32,
213    pub assets: Assets,
214    pub params: HashMap<String, String>,
215    pub audio: Option<Box<dyn AudioOut>>,
216    pub gfx: Gfx,
217    /// gasm:gl state (the null GL: natively, gl guests run headless)
218    pub gl: crate::gl::Gl,
219    /// frames run so far (gl queries and syncs are ready from the next frame on)
220    pub frame_index: u64,
221    pub net: Net,
222    pub storage: Storage,
223    /// false during catch-up frames: gfx begin_frame returns 0
224    pub show_frame: bool,
225    audio_rate: u32,
226    audio_channels: u32,
227    /// imports this runner provides ("module" and "module.function"), for `has`
228    provided: HashSet<String>,
229    /// Last presented frame as RGBA8, tightly packed.
230    pub rgba: Vec<u8>,
231    pub width: usize,
232    pub height: usize,
233    pub frames_presented: u64,
234    /// video_set_aspect (num, den); None: square pixels
235    pub aspect: Option<(u32, u32)>,
236    /// gasm_run guests: what the runner asks of the suspended guest (yield_frame)
237    pub(crate) run: Option<Arc<Exchange>>,
238    /// set_title (cleaned); None: the runner's default
239    pub title: Option<String>,
240    /// set_title changed `title` since the runner last looked (it clears this)
241    pub title_changed: bool,
242    pub hashing: bool,
243    pub video_hash: Fnv,
244    pub audio_hash: Fnv,
245    pub audio_frames: u64,
246}
247
248impl Host {
249    pub fn new(
250        assets: impl Into<Assets>,
251        params: HashMap<String, String>,
252        audio: Option<Box<dyn AudioOut>>,
253        gfx: Gfx,
254        net: Net,
255        storage: Storage,
256    ) -> Self {
257        Host {
258            memory: None,
259            start: Instant::now(),
260            virtual_time_ms: None,
261            random: None,
262            frame_rate: 60.0,
263            pads: [0; 4],
264            text: None,
265            input: RawInput::default(),
266            input_mode: 0,
267            assets: assets.into(),
268            params,
269            audio,
270            gfx,
271            gl: Default::default(),
272            frame_index: 0,
273            net,
274            storage,
275            show_frame: true,
276            audio_rate: 44100,
277            audio_channels: 2,
278            provided: HashSet::new(),
279            title: None,
280            title_changed: false,
281            aspect: None,
282            run: None,
283            rgba: Vec::new(),
284            width: 0,
285            height: 0,
286            frames_presented: 0,
287            hashing: true,
288            video_hash: Fnv::new(),
289            audio_hash: Fnv::new(),
290            audio_frames: 0,
291        }
292    }
293
294    /// Reproducible mode for headless runs: virtual time (starting at 0) and a
295    /// fixed random sequence.
296    pub fn set_reproducible(&mut self) {
297        self.virtual_time_ms = Some(0.0);
298        self.random = Some(Splitmix(0));
299    }
300
301    pub(crate) fn started(&self) -> Instant {
302        self.start
303    }
304}
305
306/// Borrow `len` bytes at guest offset `ptr`, trapping the guest if out of bounds.
307pub(crate) fn guest_slice(mem: &[u8], ptr: u32, len: u64) -> wasmtime::Result<&[u8]> {
308    let start = ptr as u64;
309    let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
310    if end > mem.len() as u64 {
311        bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
312    }
313    Ok(&mem[start as usize..end as usize])
314}
315
316/// Mutable [`guest_slice`].
317pub(crate) fn guest_slice_mut(mem: &mut [u8], ptr: u32, len: u64) -> wasmtime::Result<&mut [u8]> {
318    let start = ptr as u64;
319    let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
320    if end > mem.len() as u64 {
321        bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
322    }
323    Ok(&mut mem[start as usize..end as usize])
324}
325
326/// The "copied only if it fits" convention: copy `src` to `dst` if `src.len() <= cap`;
327/// returns the full length either way.
328fn copy_if_fits(mem: &mut [u8], dst: u32, cap: u32, src: &[u8]) -> wasmtime::Result<i32> {
329    if src.len() <= cap as usize {
330        guest_slice_mut(mem, dst, src.len() as u64)?.copy_from_slice(src);
331    }
332    Ok(src.len() as i32)
333}
334
335pub(crate) fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<Memory> {
336    caller.data().memory.ok_or_else(|| format_err!("guest memory not yet available"))
337}
338
339fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
340    let mem = memory(caller)?;
341    let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
342    std::str::from_utf8(bytes).map(str::to_owned).map_err(|e| format_err!("string argument is not UTF-8: {e}"))
343}
344
345/// The pointer() record (GASM_POINTER_OFF_* layout).
346fn pointer_bytes(p: &Pointer, frame: (usize, usize), aspect: Option<(u32, u32)>) -> [u8; POINTER_BYTES] {
347    let (fx, fy) = frame_position(p.x, p.y, p.drawable, frame, p.integer_scale, aspect);
348    let mut b = [0u8; POINTER_BYTES];
349    for (i, v) in [p.x, p.y, fx, fy, p.dx, p.dy, p.wheel_x, p.wheel_y].into_iter().enumerate() {
350        b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
351    }
352    for (i, v) in [p.buttons, p.pressed, p.released, p.flags].into_iter().enumerate() {
353        b[32 + i * 4..36 + i * 4].copy_from_slice(&v.to_le_bytes());
354    }
355    b
356}
357
358/// The gamepad() record (GASM_GAMEPAD_OFF_* layout).
359fn gamepad_bytes(g: &Gamepad) -> [u8; GAMEPAD_BYTES] {
360    let mut b = [0u8; GAMEPAD_BYTES];
361    let nb = g.buttons.len().min(GAMEPAD_BUTTONS);
362    let na = g.axes.len().min(GAMEPAD_AXES);
363    let flags = if g.connected { 1 | if g.standard { 2 } else { 0 } } else { 0u32 };
364    for (i, v) in [flags, nb as u32, na as u32].into_iter().enumerate() {
365        b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
366    }
367    for (i, v) in g.buttons.iter().take(nb).enumerate() {
368        b[12 + i * 4..16 + i * 4].copy_from_slice(&v.to_le_bytes());
369    }
370    for (i, v) in g.axes.iter().take(na).enumerate() {
371        b[140 + i * 4..144 + i * 4].copy_from_slice(&v.to_le_bytes());
372    }
373    b
374}
375
376fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
377    linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
378        let mem = memory(&caller)?;
379        let msg = guest_slice(mem.data(&caller), ptr, len as u64)?;
380        eprintln!("[guest] {}", String::from_utf8_lossy(msg));
381        Ok(())
382    })?;
383
384    linker.func_wrap("gasm", "has", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
385        let name = guest_str(&caller, ptr, len)?;
386        Ok(caller.data().provided.contains(&name) as i32)
387    })?;
388
389    linker.func_wrap("gasm", "video_set_aspect", |mut caller: Caller<'_, Host>, num: u32, den: u32| -> wasmtime::Result<()> {
390        let aspect = match (num, den) {
391            (0, 0) => None,
392            (1..=65535, 1..=65535) if num as u64 * 8 >= den as u64 && den as u64 * 8 >= num as u64 => Some((num, den)),
393            _ => bail!("video_set_aspect: invalid ratio {num}:{den}"),
394        };
395        caller.data_mut().aspect = aspect;
396        Ok(())
397    })?;
398
399    linker.func_wrap("gasm", "set_title", |mut caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
400        let title = clean_title(&guest_str(&caller, ptr, len)?);
401        let h = caller.data_mut();
402        if h.title != title {
403            h.title = title;
404            h.title_changed = true;
405        }
406        Ok(())
407    })?;
408
409    linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
410        let h = caller.data();
411        h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
412    })?;
413
414    linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
415        if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
416            caller.data_mut().frame_rate = hz;
417        }
418    })?;
419
420    linker.func_wrap(
421        "gasm",
422        "video_present",
423        |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
424            let (w, h, stride) = (w as usize, h as usize, stride as usize);
425            if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
426                bail!("video_present: bad geometry {w}x{h} stride {stride}");
427            }
428            let mem = memory(&caller)?;
429            let (data, host) = mem.data_and_store_mut(&mut caller);
430            let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
431            let row = w * 4;
432            host.rgba.resize(row * h, 0);
433            if stride == row {
434                host.rgba.copy_from_slice(src);
435            } else {
436                for (y, dst) in host.rgba.chunks_exact_mut(row).enumerate() {
437                    dst.copy_from_slice(&src[y * stride..y * stride + row]);
438                }
439            }
440            if host.hashing {
441                host.video_hash.update(&host.rgba);
442            }
443            host.width = w;
444            host.height = h;
445            host.frames_presented += 1;
446            Ok(())
447        },
448    )?;
449
450    linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
451        let h = caller.data_mut();
452        if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
453            h.audio_rate = rate;
454            h.audio_channels = channels;
455            if let Some(a) = &mut h.audio {
456                a.configure(rate, channels);
457            }
458        }
459    })?;
460
461    linker.func_wrap(
462        "gasm",
463        "audio_push",
464        |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
465            let mem = memory(&caller)?;
466            let (data, host) = mem.data_and_store_mut(&mut caller);
467            let len = frames as u64 * host.audio_channels as u64 * 4;
468            let bytes = guest_slice(data, ptr, len)?;
469            if host.hashing {
470                host.audio_hash.update(bytes);
471            }
472            host.audio_frames += frames as u64;
473            if let Some(a) = &mut host.audio {
474                a.push_le_f32(bytes);
475            }
476            Ok(())
477        },
478    )?;
479
480    linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
481        caller.data().pads.get(player as usize).copied().unwrap_or(0)
482    })?;
483
484    linker.func_wrap(
485        "gasm",
486        "text_input",
487        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
488            let mem = memory(&caller)?;
489            let (data, host) = mem.data_and_store_mut(&mut caller);
490            let Some(t) = &host.text else { return Ok(-1) };
491            copy_if_fits(data, dst, cap, t.as_bytes())
492        },
493    )?;
494
495    linker.func_wrap("gasm", "input_mode", |mut caller: Caller<'_, Host>, flags: u32| {
496        caller.data_mut().input_mode = flags & 7;
497    })?;
498
499    linker.func_wrap(
500        "gasm",
501        "key_state",
502        |mut caller: Caller<'_, Host>, dst: u32, len: u32| -> wasmtime::Result<i32> {
503            let mem = memory(&caller)?;
504            let (data, host) = mem.data_and_store_mut(&mut caller);
505            let Some(keys) = host.input.keys else { return Ok(-1) };
506            let n = (len as usize).min(KEY_STATE_BYTES);
507            guest_slice_mut(data, dst, n as u64)?.copy_from_slice(&keys[..n]);
508            Ok(KEY_STATE_BYTES as i32)
509        },
510    )?;
511
512    linker.func_wrap(
513        "gasm",
514        "key_events",
515        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
516            let mem = memory(&caller)?;
517            let (data, host) = mem.data_and_store_mut(&mut caller);
518            if host.input.keys.is_none() {
519                return Ok(-1);
520            }
521            let events = &host.input.key_events;
522            let len = events.len() * 4;
523            if len <= cap as usize {
524                let out = guest_slice_mut(data, dst, len as u64)?;
525                for (b, &(c, d)) in out.chunks_exact_mut(4).zip(events) {
526                    let c = c.to_le_bytes();
527                    b.copy_from_slice(&[c[0], c[1], d as u8, 0]);
528                }
529            }
530            Ok(len as i32)
531        },
532    )?;
533
534    linker.func_wrap(
535        "gasm",
536        "pointer",
537        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
538            let mem = memory(&caller)?;
539            let (data, host) = mem.data_and_store_mut(&mut caller);
540            let Some(p) = &host.input.pointer else { return Ok(-1) };
541            if cap as usize >= POINTER_BYTES {
542                let b = pointer_bytes(p, (host.width, host.height), host.aspect);
543                guest_slice_mut(data, dst, POINTER_BYTES as u64)?.copy_from_slice(&b);
544            }
545            Ok(POINTER_BYTES as i32)
546        },
547    )?;
548
549    linker.func_wrap(
550        "gasm",
551        "gamepad",
552        |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
553            let mem = memory(&caller)?;
554            let (data, host) = mem.data_and_store_mut(&mut caller);
555            let (Some(pads), true) = (&host.input.gamepads, slot < 4) else { return Ok(-1) };
556            if cap as usize >= GAMEPAD_BYTES {
557                let b = gamepad_bytes(&pads[slot as usize]);
558                guest_slice_mut(data, dst, GAMEPAD_BYTES as u64)?.copy_from_slice(&b);
559            }
560            Ok(GAMEPAD_BYTES as i32)
561        },
562    )?;
563
564    linker.func_wrap(
565        "gasm",
566        "gamepad_name",
567        |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
568            let mem = memory(&caller)?;
569            let (data, host) = mem.data_and_store_mut(&mut caller);
570            let Some(g) = host.input.gamepads.as_ref().and_then(|p| p.get(slot as usize)).filter(|g| g.connected) else { return Ok(-1) };
571            copy_if_fits(data, dst, cap, g.name.as_bytes())
572        },
573    )?;
574
575    linker.func_wrap(
576        "gasm",
577        "param",
578        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
579            let name = guest_str(&caller, ptr, len)?;
580            let mem = memory(&caller)?;
581            let (data, host) = mem.data_and_store_mut(&mut caller);
582            let Some(v) = host.params.get(&name) else { return Ok(-1) };
583            copy_if_fits(data, dst, cap, v.as_bytes())
584        },
585    )?;
586
587    // Assets: n = bytes available from offset (clamped to the request) is bounds-checked
588    // against guest memory before anything is read.
589    fn read_asset(caller: &mut Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32) -> wasmtime::Result<i32> {
590        let name = guest_str(caller, ptr, len)?;
591        let mem = memory(caller)?;
592        let (data, host) = mem.data_and_store_mut(caller);
593        let Some(asset) = host.assets.get(&name) else { return Ok(-1) };
594        let n = asset.size().saturating_sub(offset).min(cap as u64);
595        let out = guest_slice_mut(data, dst, n)?;
596        Ok(asset.read_at(offset, out) as i32)
597    }
598
599    linker.func_wrap(
600        "gasm",
601        "asset_read_at",
602        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
603            read_asset(&mut caller, ptr, len, offset as u64, dst, cap)
604        },
605    )?;
606
607    linker.func_wrap(
608        "gasm",
609        "asset_read_at64",
610        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32| -> wasmtime::Result<i32> {
611            read_asset(&mut caller, ptr, len, offset, dst, cap)
612        },
613    )?;
614
615    linker.func_wrap(
616        "gasm",
617        "asset_read",
618        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
619            read_asset(&mut caller, ptr, len, 0, dst, cap)
620        },
621    )?;
622
623    linker.func_wrap("gasm", "asset_count", |caller: Caller<'_, Host>| -> u32 { caller.data().assets.names().len() as u32 })?;
624
625    linker.func_wrap(
626        "gasm",
627        "asset_name",
628        |mut caller: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
629            let mem = memory(&caller)?;
630            let (data, host) = mem.data_and_store_mut(&mut caller);
631            let Some(n) = host.assets.names().get(index as usize) else { return Ok(-1) };
632            copy_if_fits(data, dst, cap, n.as_bytes())
633        },
634    )?;
635
636    linker.func_wrap(
637        "gasm",
638        "asset_size",
639        |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
640            let name = guest_str(&caller, ptr, len)?;
641            Ok(match caller.data().assets.size(&name) {
642                None => -1,
643                Some(n) if n > i32::MAX as u64 => -2,
644                Some(n) => n as i32,
645            })
646        },
647    )?;
648
649    linker.func_wrap(
650        "gasm",
651        "asset_size64",
652        |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i64> {
653            let name = guest_str(&caller, ptr, len)?;
654            Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i64))
655        },
656    )?;
657    Ok(())
658}
659
660fn trap(e: String) -> wasmtime::Error {
661    format_err!("{e}")
662}
663
664fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
665    const M: &str = "gasm:gfx";
666    linker.func_wrap(M, "width", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().0 })?;
667    linker.func_wrap(M, "height", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().1 })?;
668    linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
669        let src = guest_str(&c, ptr, len)?;
670        c.data_mut().gfx.create_shader(&src).map_err(trap)
671    })?;
672    linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
673        c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
674    })?;
675    linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
676        let json = guest_str(&c, ptr, len)?;
677        c.data_mut().gfx.create_pipeline(&json).map_err(trap)
678    })?;
679    linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
680        let json = guest_str(&c, ptr, len)?;
681        c.data_mut().gfx.create_bind_group(&json).map_err(trap)
682    })?;
683    linker.func_wrap(
684        M,
685        "write_buffer",
686        |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
687            let mem = memory(&c)?;
688            let (data, host) = mem.data_and_store_mut(&mut c);
689            let bytes = guest_slice(data, ptr, len as u64)?;
690            host.gfx.write_buffer(buf, offset, bytes).map_err(trap)?;
691            if host.hashing {
692                host.video_hash.update(bytes);
693            }
694            Ok(())
695        },
696    )?;
697    linker.func_wrap(M, "create_bind_group_layout", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
698        let json = guest_str(&c, ptr, len)?;
699        c.data_mut().gfx.create_bind_group_layout(&json).map_err(trap)
700    })?;
701    linker.func_wrap(M, "create_texture", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
702        let json = guest_str(&c, ptr, len)?;
703        c.data_mut().gfx.create_texture(&json).map_err(trap)
704    })?;
705    linker.func_wrap(M, "create_sampler", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
706        let json = guest_str(&c, ptr, len)?;
707        c.data_mut().gfx.create_sampler(&json).map_err(trap)
708    })?;
709    linker.func_wrap(
710        M,
711        "write_texture",
712        |mut c: Caller<'_, Host>, tex: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
713            let mem = memory(&c)?;
714            let (data, host) = mem.data_and_store_mut(&mut c);
715            let bytes = guest_slice(data, ptr, len as u64)?;
716            host.gfx.write_texture(tex, mip, x, y, w, h, bytes).map_err(trap)?;
717            if host.hashing {
718                // header (little-endian u32s) + payload, so the target region counts too
719                for v in [tex, mip, x, y, w, h] {
720                    host.video_hash.update(&v.to_le_bytes());
721                }
722                host.video_hash.update(bytes);
723            }
724            Ok(())
725        },
726    )?;
727    linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
728        let h = c.data_mut();
729        let show = h.show_frame;
730        h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
731    })?;
732    linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
733        c.data_mut().gfx.set_pipeline(p).map_err(trap)
734    })?;
735    linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
736        c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
737    })?;
738    linker.func_wrap(
739        M,
740        "set_bind_group_offsets",
741        |mut c: Caller<'_, Host>, i: u32, bg: u32, ptr: u32, count: u32| -> wasmtime::Result<()> {
742            let mem = memory(&c)?;
743            let (data, host) = mem.data_and_store_mut(&mut c);
744            let bytes = guest_slice(data, ptr, count as u64 * 4)?;
745            let mut offsets = [0u32; 32];
746            if count as usize > offsets.len() {
747                bail!("gfx.set_bind_group_offsets: {count} offsets (a bind group has at most {})", offsets.len());
748            }
749            for (o, b) in offsets.iter_mut().zip(bytes.chunks_exact(4)) {
750                *o = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
751            }
752            host.gfx.set_bind_group_offsets(i, bg, &offsets[..count as usize]).map_err(trap)
753        },
754    )?;
755    linker.func_wrap(
756        M,
757        "set_viewport",
758        |mut c: Caller<'_, Host>, x: f32, y: f32, w: f32, h: f32, min: f32, max: f32| -> wasmtime::Result<()> {
759            c.data_mut().gfx.set_viewport(x, y, w, h, min, max).map_err(trap)
760        },
761    )?;
762    linker.func_wrap(M, "set_scissor_rect", |mut c: Caller<'_, Host>, x: u32, y: u32, w: u32, h: u32| -> wasmtime::Result<()> {
763        c.data_mut().gfx.set_scissor_rect(x, y, w, h).map_err(trap)
764    })?;
765    linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
766        c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
767    })?;
768    linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
769        c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
770    })?;
771    linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| -> wasmtime::Result<()> {
772        c.data_mut().gfx.draw(vc, ic, fv, fi).map_err(trap)
773    })?;
774    linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| -> wasmtime::Result<()> {
775        c.data_mut().gfx.draw_indexed(ic, n, first, base, fi).map_err(trap)
776    })?;
777    linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
778        let h = c.data_mut();
779        h.frames_presented += 1;
780        h.gfx.end_frame().map_err(trap)
781    })?;
782    linker.func_wrap(M, "destroy", |mut c: Caller<'_, Host>, handle: u32| -> wasmtime::Result<()> {
783        c.data_mut().gfx.destroy(handle).map_err(trap)
784    })?;
785    Ok(())
786}
787
788fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
789    const M: &str = "gasm:net";
790    linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
791        let url = guest_str(&c, ptr, len)?;
792        Ok(c.data_mut().net.open(&url))
793    })?;
794    linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> { c.data().net.state(h).map_err(trap) })?;
795    linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
796        let mem = memory(&c)?;
797        let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
798        if msg.is_empty() {
799            c.data().net.state(h).map_err(trap)?; // still validates the handle
800            return Ok(-1);
801        }
802        c.data_mut().net.send(h, msg).map_err(trap)
803    })?;
804    linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
805        let mem = memory(&c)?;
806        let (data, host) = mem.data_and_store_mut(&mut c);
807        let msg = match host.net.peek(h).map_err(trap)? {
808            Ok(m) => m,
809            Err(code) => return Ok(code),
810        };
811        let n = copy_if_fits(data, dst, cap, msg)?;
812        if n as usize <= cap as usize {
813            host.net.pop(h);
814        }
815        Ok(n)
816    })?;
817    linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().net.close(h).map_err(trap) })?;
818    Ok(())
819}
820
821fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
822    const M: &str = "gasm:storage";
823    linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
824        let key = guest_str(&c, kp, kl)?;
825        let mem = memory(&c)?;
826        let (data, host) = mem.data_and_store_mut(&mut c);
827        let Some(v) = host.storage.get(&key) else { return Ok(-1) };
828        copy_if_fits(data, dst, cap, v)
829    })?;
830    linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
831        let key = guest_str(&c, kp, kl)?;
832        let mem = memory(&c)?;
833        let (data, host) = mem.data_and_store_mut(&mut c);
834        let value = guest_slice(data, vp, vl as u64)?;
835        Ok(match host.storage.set(&key, value) {
836            Ok(()) => 0,
837            Err(e) => {
838                eprintln!("[gasm] storage: {key}: {e}");
839                e.code()
840            }
841        })
842    })?;
843    linker.func_wrap(M, "count", |mut c: Caller<'_, Host>| -> u32 { c.data_mut().storage.keys().len() as u32 })?;
844    linker.func_wrap(M, "key", |mut c: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
845        let mem = memory(&c)?;
846        let (data, host) = mem.data_and_store_mut(&mut c);
847        let Some(k) = host.storage.keys().get(index as usize) else { return Ok(-1) };
848        copy_if_fits(data, dst, cap, k.as_bytes())
849    })?;
850    linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
851        let key = guest_str(&c, kp, kl)?;
852        Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
853    })?;
854    Ok(())
855}
856
857/// Why a guest call ended.
858#[derive(Debug)]
859pub enum Stop {
860    /// The guest called proc_exit(code).
861    Exit(i32),
862    Trap(String),
863}
864
865impl std::fmt::Display for Stop {
866    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
867        match self {
868            Stop::Exit(code) => write!(f, "guest exited with code {code}"),
869            Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
870        }
871    }
872}
873
874impl std::error::Error for Stop {}
875
876fn classify(e: wasmtime::Error) -> Stop {
877    if let Some(exit) = e.downcast_ref::<wasi::Exit>() {
878        return Stop::Exit(exit.0);
879    }
880    if e.downcast_ref::<switching::Quit>().is_some() {
881        return Stop::Exit(0);
882    }
883    if e.downcast_ref::<wasmtime::Trap>() == Some(&wasmtime::Trap::Interrupt) {
884        return Stop::Trap(format!("a guest call ran longer than the time limit (stuck in a loop?); see --call-timeout\n{e:?}"));
885    }
886    Stop::Trap(format!("{e:?}"))
887}
888
889/// Epoch ticks per second (the watchdog thread's rate).
890const TICKS_PER_SEC: u64 = 10;
891
892/// The process-wide engine: the same configuration for compiling, precompiling
893/// and loading (`.cwasm` files must match it). Epoch interruption lets a guest
894/// call that never returns be stopped (`LoadOptions::call_timeout`). Calls can be
895/// sync (`gasm_frame` guests) or async (`gasm_run` guests: stack switching).
896pub fn engine() -> &'static Engine {
897    static ENGINE: OnceLock<Engine> = OnceLock::new();
898    ENGINE.get_or_init(new_engine)
899}
900
901fn new_engine() -> Engine {
902    let mut config = Config::new();
903    config.epoch_interruption(true);
904    // a gasm_run guest's whole run is on this stack: the default wasm stack limit plus room
905    config.async_stack_size(4 << 20);
906    let engine = Engine::new(&config).expect("wasmtime engine");
907    let ticker = engine.clone();
908    std::thread::Builder::new()
909        .name("gasm-epoch".into())
910        .spawn(move || {
911            loop {
912                std::thread::sleep(Duration::from_millis(1000 / TICKS_PER_SEC));
913                ticker.increment_epoch();
914            }
915        })
916        .expect("epoch thread");
917    engine
918}
919
920/// Ahead-of-time compile a guest to native code for this host (no JIT needed at load).
921pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
922    engine().precompile_module(wasm)
923}
924
925/// How to load a guest.
926#[derive(Clone, Copy)]
927pub struct LoadOptions {
928    /// Accept a precompiled `.cwasm` (native code: only files you made yourself
929    /// with `gasm-run --compile`). Otherwise only `.wasm` modules load.
930    pub allow_precompiled: bool,
931    /// Trap a single guest call (init, a frame, exit) that runs longer than this.
932    pub call_timeout: Option<Duration>,
933    /// Run guests that export `gasm_run` that way (the runner switches stacks).
934    /// Off: always `gasm_frame` (tests of the Asyncify path).
935    pub stack_switching: bool,
936}
937
938impl Default for LoadOptions {
939    fn default() -> Self {
940        LoadOptions { allow_precompiled: false, call_timeout: Some(Duration::from_secs(30)), stack_switching: true }
941    }
942}
943
944/// The running `gasm_run` call: owns the store until the run ends.
945type Running = Pin<Box<dyn Future<Output = (Store<Host>, wasmtime::Result<i32>)>>>;
946
947/// A running guest. `gasm_frame` guests are called once per frame; `gasm_run`
948/// guests run in one suspended call (stack switching) that each [`Game::frame`]
949/// resumes until its next `yield_frame`.
950pub struct Game {
951    /// None while a `gasm_run` call is running (the call owns it): use [`Game::with_host`]
952    store: Option<Store<Host>>,
953    frame: TypedFunc<(), ()>,
954    exit: Option<TypedFunc<(), ()>>,
955    deadline: u64,
956    /// `gasm_run` (async engine)
957    run: Option<TypedFunc<(), i32>>,
958    running: Option<Running>,
959    exchange: Option<Arc<Exchange>>,
960    /// the guest exited or trapped: it is never called again
961    ended: bool,
962    /// the guest imports gasm:gl (the native window has no GL backend yet)
963    gl: bool,
964}
965
966impl Game {
967    pub fn load(wasm: &[u8], host: Host, opts: LoadOptions) -> Result<Game, Stop> {
968        Self::load_inner(wasm, host, opts).map_err(classify)
969    }
970
971    fn load_inner(wasm: &[u8], host: Host, opts: LoadOptions) -> wasmtime::Result<Game> {
972        let module = if wasm.starts_with(b"\0asm") {
973            Module::new(engine(), wasm)?
974        } else if opts.allow_precompiled {
975            // Precompiled artifact from `gasm-run --compile`: native code, trusted
976            // like the runner itself (unlike .wasm, which is sandboxed).
977            unsafe { Module::deserialize(engine(), wasm)? }
978        } else {
979            bail!("not a wasm module (a precompiled .cwasm runs as native code: pass --allow-precompiled if you made it yourself)");
980        };
981        let gl = module.imports().any(|i| i.module() == "gasm:gl");
982        if gl && module.imports().any(|i| i.module() == "gasm:gfx") {
983            bail!("a module imports gasm:gfx or gasm:gl, not both");
984        }
985        // guests that export gasm_run run in one async call (stack switching)
986        let has_run = module.get_export("gasm_run").is_some();
987        let switching = has_run && opts.stack_switching;
988        if has_run && !switching && module.imports().any(|i| i.module() == "asyncify") {
989            // made without wasm-opt --asyncify: its gasm_frame can't suspend
990            bail!("this module is a run build (no Asyncify): it needs stack switching (gasm_run); use the game's Asyncify build");
991        }
992        let engine = engine();
993        let mut linker: Linker<Host> = Linker::new(engine);
994        add_gasm_imports(&mut linker)?;
995        if switching {
996            switching::add_yield_async(&mut linker)?;
997        } else {
998            switching::add_yield_sync(&mut linker)?;
999        }
1000        add_gfx_imports(&mut linker)?;
1001        crate::gl::add_gl_imports(&mut linker)?;
1002        add_net_imports(&mut linker)?;
1003        add_storage_imports(&mut linker)?;
1004        let mut store = Store::new(engine, host);
1005        // what `has` reports: the gasm modules and functions above (not WASI stubs or traps)
1006        let mut provided = HashSet::new();
1007        for (m, f, _) in linker.iter(&mut store) {
1008            provided.insert(m.to_owned());
1009            provided.insert(format!("{m}.{f}"));
1010        }
1011        for f in wasi::IMPLEMENTED {
1012            provided.insert(format!("{}.{f}", wasi::MODULE));
1013        }
1014        provided.insert(wasi::MODULE.to_owned());
1015        store.data_mut().provided = provided;
1016        wasi::add_to_linker(&mut linker, &module)?;
1017        // Imports this runner doesn't know (e.g. JS glue some Rust crates pull in)
1018        // link as traps: harmless unless the guest actually calls them.
1019        linker.define_unknown_imports_as_traps(&module)?;
1020
1021        let deadline = opts.call_timeout.map_or(u64::MAX / 2, |t| (t.as_millis() as u64 * TICKS_PER_SEC).div_ceil(1000).max(1));
1022        store.epoch_deadline_trap();
1023        store.set_epoch_deadline(deadline);
1024        let exchange = switching.then(|| Exchange::new(deadline));
1025        store.data_mut().run = exchange.clone();
1026        let instance: Instance = if switching {
1027            switching::block_on(linker.instantiate_async(&mut store, &module))??
1028        } else {
1029            linker.instantiate(&mut store, &module)?
1030        };
1031        let memory = instance
1032            .get_memory(&mut store, "memory")
1033            .ok_or_else(|| format_err!("guest does not export `memory`"))?;
1034        store.data_mut().memory = Some(memory);
1035
1036        // a call that must not suspend, on either engine
1037        fn call<R: wasmtime::WasmResults + Send + Sync>(store: &mut Store<Host>, f: TypedFunc<(), R>, switching: bool) -> wasmtime::Result<R> {
1038            if switching { switching::block_on(f.call_async(store, ()))? } else { f.call(store, ()) }
1039        }
1040        if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
1041            store.set_epoch_deadline(deadline);
1042            call(&mut store, init, switching)?;
1043        }
1044        let version = instance.get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?;
1045        let version = call(&mut store, version, switching)?;
1046        if version != ABI_VERSION {
1047            bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
1048        }
1049        store.set_epoch_deadline(deadline);
1050        let init = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?;
1051        let rc = call(&mut store, init, switching)?;
1052        if rc != 0 {
1053            bail!("gasm_init failed with code {rc}");
1054        }
1055        let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
1056        let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
1057        let run = if switching { Some(instance.get_typed_func::<(), i32>(&mut store, "gasm_run")?) } else { None };
1058        Ok(Game { store: Some(store), frame, exit, deadline, run, running: None, exchange, ended: false, gl })
1059    }
1060
1061    /// Whether the guest imports gasm:gl.
1062    pub fn uses_gl(&self) -> bool {
1063        self.gl
1064    }
1065
1066    /// Whether the guest runs through `gasm_run` (the runner switches stacks).
1067    pub fn switching(&self) -> bool {
1068        self.run.is_some()
1069    }
1070
1071    /// One frame: call `gasm_frame`, or resume `gasm_run` until its next `yield_frame`.
1072    pub fn frame(&mut self) -> Result<(), Stop> {
1073        if self.ended {
1074            return Err(Stop::Trap("the guest is not running".into()));
1075        }
1076        let Some(run) = self.run.clone() else {
1077            let store = self.store.as_mut().expect("store");
1078            store.set_epoch_deadline(self.deadline);
1079            let r = self.frame.call(&mut *store, ()).map_err(classify);
1080            store.data_mut().frame_index += 1;
1081            self.ended = r.is_err();
1082            return r;
1083        };
1084        match &self.exchange {
1085            Some(ex) if self.running.is_some() => ex.set(Cmd::Resume),
1086            _ => {
1087                // frame 0 starts the run; the call owns the store from now on
1088                let mut store = self.store.take().expect("store");
1089                store.set_epoch_deadline(self.deadline);
1090                self.running = Some(Box::pin(async move {
1091                    let r = run.call_async(&mut store, ()).await;
1092                    (store, r)
1093                }));
1094            }
1095        }
1096        self.poll()
1097    }
1098
1099    /// Poll the running `gasm_run` call: Ok when it suspended again, the reason when it ended.
1100    fn poll(&mut self) -> Result<(), Stop> {
1101        let Some(running) = self.running.as_mut() else { return Ok(()) };
1102        match switching::poll_once(running.as_mut()) {
1103            Poll::Pending => Ok(()),
1104            Poll::Ready((store, r)) => {
1105                self.store = Some(store);
1106                self.running = None;
1107                self.ended = true;
1108                Err(match r {
1109                    Ok(code) => Stop::Exit(code),
1110                    Err(e) => classify(e),
1111                })
1112            }
1113        }
1114    }
1115
1116    /// Best-effort "the player is quitting" notification (optional `gasm_exit` export).
1117    /// A suspended `gasm_run` guest gets it on top of its run, which then ends.
1118    pub fn exit(&mut self) {
1119        if self.running.is_some() {
1120            if let Some(ex) = &self.exchange {
1121                ex.set(Cmd::Exit);
1122            }
1123            if let Err(Stop::Trap(t)) = self.poll() {
1124                eprintln!("[gasm] gasm_exit trapped: {t}");
1125            }
1126            if self.running.is_some() {
1127                // gasm_exit yielded: give up on the run (the store stays with it)
1128                self.running = None;
1129            }
1130            self.exit = None;
1131            return;
1132        }
1133        if let (Some(f), Some(store)) = (self.exit.take(), self.store.as_mut()) {
1134            store.set_epoch_deadline(self.deadline);
1135            let r = if self.run.is_some() { switching::block_on(f.call_async(&mut *store, ())).and_then(|r| r) } else { f.call(&mut *store, ()) };
1136            if let Err(e) = r {
1137                if let Stop::Trap(t) = classify(e) {
1138                    eprintln!("[gasm] gasm_exit trapped: {t}");
1139                }
1140            }
1141        }
1142    }
1143
1144    /// Run `f` on the host: directly, or (a suspended `gasm_run` guest) through
1145    /// the guest's `yield_frame`, which owns the store meanwhile. Runners use this
1146    /// between frames for everything they read or set.
1147    pub fn with_host<R>(&mut self, f: impl FnOnce(&mut Host) -> R) -> R {
1148        if let Some(store) = &mut self.store {
1149            return f(store.data_mut());
1150        }
1151        let (Some(running), Some(ex)) = (self.running.as_mut(), &self.exchange) else {
1152            panic!("with_host: the guest's store is gone (gasm_exit yielded)");
1153        };
1154        let mut f = Some(f);
1155        let mut out = None;
1156        let mut job = |h: &mut Host| out = Some((f.take().expect("job runs once"))(h));
1157        let ptr: *mut (dyn FnMut(&mut Host) + '_) = &mut job;
1158        // SAFETY: the pointer is only used by yield_frame during the poll below,
1159        // while `job` is alive on this stack; it is dropped before we return.
1160        let ptr: *mut (dyn FnMut(&mut Host) + 'static) = unsafe { std::mem::transmute(ptr) };
1161        ex.set(Cmd::Job(Job(ptr)));
1162        let polled = switching::poll_once(running.as_mut());
1163        ex.set(Cmd::None);
1164        match polled {
1165            Poll::Pending => {}
1166            Poll::Ready(_) => unreachable!("yield_frame stays suspended after a job"),
1167        }
1168        out.expect("yield_frame ran the job")
1169    }
1170
1171    /// The host of a guest that isn't suspended in `gasm_run` (every `gasm_frame`
1172    /// guest; a `gasm_run` guest before its first frame and after it ended).
1173    /// Otherwise use [`Game::with_host`].
1174    pub fn host(&self) -> &Host {
1175        self.store.as_ref().expect("Game::host: a gasm_run guest is running; use Game::with_host").data()
1176    }
1177
1178    pub fn host_mut(&mut self) -> &mut Host {
1179        self.store.as_mut().expect("Game::host_mut: a gasm_run guest is running; use Game::with_host").data_mut()
1180    }
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185    use super::*;
1186
1187    #[test]
1188    fn titles_are_cleaned_and_cut() {
1189        assert_eq!(clean_title("A\u{7}B\u{202e}C\u{2069}").as_deref(), Some("ABC"));
1190        assert_eq!(clean_title("\n\t"), None);
1191        // 200 two-byte characters: cut at 256 bytes, on a character boundary
1192        assert_eq!(clean_title(&"é".repeat(200)).map(|t| t.len()), Some(256));
1193    }
1194
1195    const RUN_GUEST: &str = r#"(module
1196      (import "gasm" "yield_frame" (func $yield))
1197      (memory (export "memory") 1)
1198      (global $n (mut i32) (i32.const 0))
1199      (func (export "gasm_abi_version") (result i32) i32.const 0)
1200      (func (export "gasm_init") (result i32) i32.const 0)
1201      (func (export "gasm_frame") call $yield)
1202      (func (export "gasm_exit") (global.set $n (i32.const 100)))
1203      (func (export "frames") (result i32) global.get $n)
1204      (func (export "gasm_run") (result i32)
1205        (loop $l
1206          (global.set $n (i32.add (global.get $n) (i32.const 1)))
1207          call $yield
1208          (br_if $l (i32.lt_u (global.get $n) (i32.const 3))))
1209        i32.const 7))"#;
1210
1211    fn host() -> Host {
1212        Host::new(Assets::new(), HashMap::new(), None, Gfx::null(), Net::new(false), Storage::memory())
1213    }
1214
1215    #[test]
1216    fn gasm_run_guests_suspend_between_frames() {
1217        let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1218        assert!(g.switching());
1219        g.with_host(|h| h.frame_rate = 30.0); // before the first frame: the store is ours
1220        assert!(g.frame().is_ok()); // n = 1, suspended in yield_frame
1221        // suspended: the host is reached through the guest's yield_frame
1222        assert_eq!(g.with_host(|h| h.frame_rate), 30.0);
1223        assert!(g.frame().is_ok()); // n = 2
1224        assert!(g.frame().is_ok()); // n = 3
1225        // the loop ends: gasm_run returns 7, the exit code
1226        assert!(matches!(g.frame(), Err(Stop::Exit(7))));
1227        assert!(g.frame().is_err(), "never called again");
1228    }
1229
1230    #[test]
1231    fn exit_runs_on_top_of_a_suspended_run() {
1232        let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1233        g.frame().unwrap();
1234        g.exit();
1235        // the store is back after the run ended; gasm_exit ran (n = 100)
1236        assert!(g.store.is_some());
1237    }
1238
1239    #[test]
1240    fn yield_frame_outside_gasm_run_traps() {
1241        let opts = LoadOptions { stack_switching: false, ..LoadOptions::default() };
1242        let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), opts).unwrap();
1243        assert!(!g.switching());
1244        match g.frame() {
1245            Err(Stop::Trap(t)) => assert!(t.contains("only inside gasm_run"), "{t}"),
1246            other => panic!("expected a trap, got {:?}", other.err()),
1247        }
1248    }
1249
1250    #[test]
1251    fn custom_sections_are_found() {
1252        // magic + version, a type section (id 1, empty), then custom "gasm.title" = "Hi\0"
1253        let mut wasm = b"\0asm\x01\0\0\0\x01\x01\0".to_vec();
1254        wasm.extend_from_slice(&[0, 14, 10]);
1255        wasm.extend_from_slice(b"gasm.titleHi\0");
1256        assert_eq!(custom_section(&wasm, "gasm.title"), Some(&b"Hi\0"[..]));
1257        assert_eq!(static_title(&wasm).as_deref(), Some("Hi"));
1258        assert_eq!(custom_section(&wasm, "other"), None);
1259        assert_eq!(custom_section(&wasm[..wasm.len() - 2], "gasm.title"), None); // truncated
1260        assert_eq!(custom_section(b"not wasm", "gasm.title"), None);
1261    }
1262}