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