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/// The system time zone's offset from UTC now, in minutes east (daylight saving included).
138#[cfg(unix)]
139fn utc_offset_minutes() -> i32 {
140    let now = unsafe { libc::time(std::ptr::null_mut()) };
141    let mut tm: libc::tm = unsafe { std::mem::zeroed() };
142    if unsafe { libc::localtime_r(&now, &mut tm) }.is_null() {
143        return 0;
144    }
145    (tm.tm_gmtoff / 60) as i32
146}
147
148#[cfg(windows)]
149fn utc_offset_minutes() -> i32 {
150    use windows_sys::Win32::System::Time::{GetTimeZoneInformation, TIME_ZONE_INFORMATION};
151    let mut tz: TIME_ZONE_INFORMATION = unsafe { std::mem::zeroed() };
152    // UTC = local + bias: the standard or daylight bias by the current state
153    let bias = match unsafe { GetTimeZoneInformation(&mut tz) } {
154        1 => tz.Bias + tz.StandardBias, // TIME_ZONE_ID_STANDARD
155        2 => tz.Bias + tz.DaylightBias, // TIME_ZONE_ID_DAYLIGHT
156        0 => tz.Bias,                   // TIME_ZONE_ID_UNKNOWN: no daylight saving
157        _ => 0,
158    };
159    -bias
160}
161
162#[cfg(not(any(unix, windows)))]
163fn utc_offset_minutes() -> i32 {
164    0
165}
166
167/// A guest's `set_title` text as runners show it: control characters (Unicode
168/// Cc) and bidi controls (U+202A–U+202E, U+2066–U+2069) removed, then cut to
169/// 256 bytes at a character boundary. None: empty, the runner's default.
170/// Same rules in runners/web/lib/host.js `cleanTitle`.
171pub fn clean_title(text: &str) -> Option<String> {
172    let mut out = String::new();
173    for c in text.chars().filter(|&c| !c.is_control() && !matches!(c, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')) {
174        if out.len() + c.len_utf8() > TITLE_MAX_BYTES {
175            break;
176        }
177        out.push(c);
178    }
179    (!out.is_empty()).then_some(out)
180}
181
182/// The payload of custom section `name` in a wasm binary (None for a .cwasm or
183/// a malformed module: the loader reports those).
184pub fn custom_section<'a>(wasm: &'a [u8], name: &str) -> Option<&'a [u8]> {
185    fn leb(b: &[u8], at: &mut usize) -> Option<usize> {
186        let (mut v, mut shift) = (0usize, 0);
187        loop {
188            let byte = *b.get(*at)?;
189            *at += 1;
190            v |= ((byte & 0x7f) as usize).checked_shl(shift)?;
191            if byte & 0x80 == 0 {
192                return Some(v);
193            }
194            shift += 7;
195            if shift > 28 {
196                return None;
197            }
198        }
199    }
200    if wasm.get(..4)? != b"\0asm" {
201        return None;
202    }
203    let mut at = 8;
204    while at < wasm.len() {
205        let id = wasm[at];
206        at += 1;
207        let size = leb(wasm, &mut at)?;
208        let body = wasm.get(at..at.checked_add(size)?)?;
209        at += size;
210        if id == 0 {
211            let mut p = 0;
212            let n = leb(body, &mut p)?;
213            if body.get(p..p.checked_add(n)?)? == name.as_bytes() {
214                return body.get(p + n..);
215            }
216        }
217    }
218    None
219}
220
221/// The guest's built-in title (custom section `gasm.title`, UTF-8), cleaned like
222/// set_title: the default title before (or without) set_title.
223pub fn static_title(wasm: &[u8]) -> Option<String> {
224    clean_title(std::str::from_utf8(custom_section(wasm, "gasm.title")?).ok()?)
225}
226
227/// The guest's memory cap (`--memory-limit`): growing past it traps, with a message
228/// that says so (the JS runner checks after each guest call, with the same message).
229pub struct MemoryLimit(pub Option<usize>);
230
231impl wasmtime::ResourceLimiter for MemoryLimit {
232    fn memory_growing(&mut self, _current: usize, desired: usize, _maximum: Option<usize>) -> wasmtime::Result<bool> {
233        match self.0 {
234            Some(limit) if desired > limit => bail!("{}", memory_limit_message(limit)),
235            _ => Ok(true),
236        }
237    }
238    fn table_growing(&mut self, _current: usize, _desired: usize, _maximum: Option<usize>) -> wasmtime::Result<bool> {
239        Ok(true)
240    }
241}
242
243/// The trap message when a guest needs more memory than allowed (also the JS runner's).
244pub fn memory_limit_message(limit: usize) -> String {
245    format!("the game needs more memory than its limit ({} MiB; see --memory-limit)", limit >> 20)
246}
247
248/// The default memory cap: far above what the games here use (Godot's 3D example: 39 MiB).
249pub const DEFAULT_MEMORY_LIMIT: usize = 1 << 30;
250
251pub struct Host {
252    memory: Option<GuestMemory>,
253    start: Instant,
254    /// Headless runs use frame-derived time so results are reproducible (also
255    /// the WASI clocks).
256    pub virtual_time_ms: Option<f64>,
257    /// Headless runs: a fixed `random_get` sequence; None = OS randomness.
258    pub random: Option<Splitmix>,
259    pub frame_rate: f64,
260    pub pads: [u32; 4],
261    /// UTF-8 typed since the previous frame (set by the runner before each frame);
262    /// None: no keyboard, text_input returns -1
263    pub text: Option<String>,
264    pub input: RawInput,
265    /// GASM_INPUT_* flags requested by the guest (input_mode)
266    pub input_mode: u32,
267    pub assets: Assets,
268    pub params: HashMap<String, String>,
269    pub audio: Option<Box<dyn AudioOut>>,
270    pub gfx: Gfx,
271    /// gasm:gl state (the null GL: natively, gl guests run headless)
272    pub gl: crate::gl::Gl,
273    /// frames run so far (gl queries and syncs are ready from the next frame on)
274    pub frame_index: u64,
275    pub net: Net,
276    /// gasm:fetch (denied until the runner sets a policy: `Fetch::new`)
277    pub fetch: crate::fetch::Fetch,
278    /// gasm:media: the game's audio streams
279    pub media: crate::media::Media,
280    /// gasm:clipboard: what the game may read this frame and what it copied
281    pub clipboard: Clipboard,
282    /// gasm:files: saves queued for the player (written between frames)
283    pub files: crate::files::Files,
284    /// the manifest's `requires`: the game is refused before it starts if one is missing
285    pub requires: Vec<String>,
286    /// wasi-threads: spawning and the shared memory (None: a module without threads)
287    threads: Option<std::sync::Arc<crate::threads::Threads>>,
288    /// `--profile`: the main thread's guest stacks, sampled every millisecond
289    pub profile: Option<Profile>,
290    pub storage: Storage,
291    /// false during catch-up frames: gfx begin_frame returns 0
292    pub show_frame: bool,
293    audio_rate: u32,
294    audio_channels: u32,
295    /// imports this runner provides ("module" and "module.function"), for `has`
296    provided: HashSet<String>,
297    /// Last presented frame as RGBA8, tightly packed.
298    pub rgba: Vec<u8>,
299    pub width: usize,
300    pub height: usize,
301    pub frames_presented: u64,
302    /// video_set_aspect (num, den); None: square pixels
303    pub aspect: Option<(u32, u32)>,
304    /// gasm_run guests: what the runner asks of the suspended guest (yield_frame)
305    pub(crate) run: Option<Arc<Exchange>>,
306    /// set_title (cleaned); None: the runner's default
307    pub title: Option<String>,
308    /// set_title changed `title` since the runner last looked (it clears this)
309    pub title_changed: bool,
310    /// set_fullscreen can be honoured here (the window runner sets it; headless: false)
311    pub fullscreen_capable: bool,
312    /// shown fullscreen now (is_fullscreen; the window runner keeps it up to date)
313    pub fullscreen: bool,
314    /// set_fullscreen asked for this since the runner last looked (it takes it)
315    pub fullscreen_request: Option<bool>,
316    pub hashing: bool,
317    pub video_hash: Fnv,
318    pub audio_hash: Fnv,
319    pub audio_frames: u64,
320    /// the guest's memory cap (`LoadOptions::memory_limit`)
321    pub memory_limit: MemoryLimit,
322    /// a catch-up frame (the runner shows only the last of a batch): gasm:gl `frame_shown` is 0
323    pub catch_up: bool,
324}
325
326impl Host {
327    /// A worker thread's host (threads.rs): compute only, the main host's clock origin.
328    pub(crate) fn worker(threads: std::sync::Arc<crate::threads::Threads>, start: Instant, assets: Assets) -> Host {
329        let mut h = Host::new(assets, HashMap::new(), None, Gfx::null(), Net::new(false), Storage::memory());
330        h.start = start;
331        h.threads = Some(threads);
332        h
333    }
334
335    pub(crate) fn threads(&self) -> Option<&std::sync::Arc<crate::threads::Threads>> {
336        self.threads.as_ref()
337    }
338
339    pub(crate) fn set_memory(&mut self, m: GuestMemory) {
340        self.memory = Some(m);
341    }
342
343    pub fn new(
344        assets: impl Into<Assets>,
345        params: HashMap<String, String>,
346        audio: Option<Box<dyn AudioOut>>,
347        gfx: Gfx,
348        net: Net,
349        storage: Storage,
350    ) -> Self {
351        Host {
352            memory: None,
353            start: Instant::now(),
354            virtual_time_ms: None,
355            random: None,
356            frame_rate: 60.0,
357            pads: [0; 4],
358            text: None,
359            input: RawInput::default(),
360            input_mode: 0,
361            assets: assets.into(),
362            params,
363            audio,
364            gfx,
365            gl: Default::default(),
366            fetch: Default::default(),
367            media: Default::default(),
368            clipboard: Default::default(),
369            files: Default::default(),
370            requires: Vec::new(),
371            threads: None,
372            profile: None,
373            memory_limit: MemoryLimit(Some(DEFAULT_MEMORY_LIMIT)),
374            catch_up: false,
375            frame_index: 0,
376            net,
377            storage,
378            show_frame: true,
379            audio_rate: 44100,
380            audio_channels: 2,
381            provided: HashSet::new(),
382            title: None,
383            title_changed: false,
384            fullscreen_capable: false,
385            fullscreen: false,
386            fullscreen_request: None,
387            aspect: None,
388            run: None,
389            rgba: Vec::new(),
390            width: 0,
391            height: 0,
392            frames_presented: 0,
393            hashing: true,
394            video_hash: Fnv::new(),
395            audio_hash: Fnv::new(),
396            audio_frames: 0,
397        }
398    }
399
400    /// Reproducible mode for headless runs: virtual time (starting at 0) and a
401    /// fixed random sequence.
402    pub fn set_reproducible(&mut self) {
403        self.virtual_time_ms = Some(0.0);
404        self.random = Some(Splitmix(0));
405    }
406
407    pub(crate) fn started(&self) -> Instant {
408        self.start
409    }
410}
411
412/// Whether a module imports anything from `module` (e.g. "gasm:gl"), read before
413/// loading it. A .cwasm is checked only with `allow_precompiled` (it is native code).
414pub fn imports_module(wasm: &[u8], module: &str, allow_precompiled: bool) -> bool {
415    if !wasm.starts_with(b"\0asm") {
416        if !allow_precompiled {
417            return false;
418        }
419        return unsafe { Module::deserialize(engine(), wasm) }.is_ok_and(|m| m.imports().any(|i| i.module() == module));
420    }
421    fn leb(b: &[u8], p: &mut usize) -> Option<u32> {
422        let (mut v, mut shift) = (0u32, 0);
423        loop {
424            let byte = *b.get(*p)?;
425            *p += 1;
426            v |= ((byte & 0x7f) as u32) << shift;
427            if byte & 0x80 == 0 {
428                return Some(v);
429            }
430            shift += 7;
431            if shift > 28 {
432                return None;
433            }
434        }
435    }
436    fn name<'a>(b: &'a [u8], p: &mut usize) -> Option<&'a [u8]> {
437        let n = leb(b, p)? as usize;
438        let s = b.get(*p..*p + n)?;
439        *p += n;
440        Some(s)
441    }
442    let scan = || -> Option<bool> {
443        let mut p = 8;
444        while p < wasm.len() {
445            let id = wasm[p];
446            p += 1;
447            let size = leb(wasm, &mut p)? as usize;
448            let end = p.checked_add(size)?;
449            if id == 2 {
450                let mut q = p;
451                for _ in 0..leb(wasm, &mut q)? {
452                    if name(wasm, &mut q)? == module.as_bytes() {
453                        return Some(true);
454                    }
455                    name(wasm, &mut q)?;
456                    match *wasm.get(q)? {
457                        0 => { q += 1; leb(wasm, &mut q)?; }                       // func: type index
458                        1 => { q += 2; let f = wasm[q - 1]; leb(wasm, &mut q)?; if f & 1 != 0 { leb(wasm, &mut q)?; } } // table
459                        2 => { q += 1; let f = *wasm.get(q)?; q += 1; leb(wasm, &mut q)?; if f & 1 != 0 { leb(wasm, &mut q)?; } } // memory
460                        3 => q += 3,                                                 // global: type, mutability
461                        4 => { q += 2; leb(wasm, &mut q)?; }                         // tag
462                        _ => return None,
463                    }
464                }
465                return Some(false);
466            }
467            p = end;
468        }
469        Some(false)
470    };
471    scan().unwrap_or(false)
472}
473
474/// Borrow `len` bytes at guest offset `ptr`, trapping the guest if out of bounds.
475pub(crate) fn guest_slice(mem: &[u8], ptr: u32, len: u64) -> wasmtime::Result<&[u8]> {
476    let start = ptr as u64;
477    let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
478    if end > mem.len() as u64 {
479        bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
480    }
481    Ok(&mem[start as usize..end as usize])
482}
483
484/// Mutable [`guest_slice`].
485pub(crate) fn guest_slice_mut(mem: &mut [u8], ptr: u32, len: u64) -> wasmtime::Result<&mut [u8]> {
486    let start = ptr as u64;
487    let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
488    if end > mem.len() as u64 {
489        bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
490    }
491    Ok(&mut mem[start as usize..end as usize])
492}
493
494/// The "copied only if it fits" convention: copy `src` to `dst` if `src.len() <= cap`;
495/// returns the full length either way.
496/// gasm:clipboard's state. The runner owns the system clipboard: it sets `pasted` for
497/// the frame that carries the player's paste key press (cleared after it) and takes
498/// `copied` after each frame. Headless runs never paste.
499#[derive(Default)]
500pub struct Clipboard {
501    pub pasted: Option<String>,
502    pub copied: Option<String>,
503}
504
505/// The largest text gasm:clipboard passes either way
506pub const CLIPBOARD_MAX: usize = 1 << 20;
507
508fn copy_if_fits(mem: &mut [u8], dst: u32, cap: u32, src: &[u8]) -> wasmtime::Result<i32> {
509    if src.len() <= cap as usize {
510        guest_slice_mut(mem, dst, src.len() as u64)?.copy_from_slice(src);
511    }
512    Ok(src.len() as i32)
513}
514
515/// The guest's linear memory: its own, or a shared one (wasi-threads: every thread's
516/// instance imports the same). Host functions use it the same way either way.
517#[derive(Clone)]
518pub(crate) enum GuestMemory {
519    Plain(Memory),
520    Shared(wasmtime::SharedMemory),
521}
522
523impl GuestMemory {
524    pub fn data<'a, T: 'static>(&self, store: impl Into<wasmtime::StoreContext<'a, T>>) -> &'a [u8] {
525        match self {
526            GuestMemory::Plain(m) => m.data(store),
527            // SAFETY: shared memory is racy by nature (as in any native threaded program);
528            // the guest synchronizes its threads, and the host only reads/writes the
529            // ranges a call names, as it does for plain memory
530            GuestMemory::Shared(m) => unsafe { std::slice::from_raw_parts(m.data().as_ptr() as *const u8, m.data().len()) },
531        }
532    }
533
534    pub fn data_mut<'a, T: 'static>(&self, store: impl Into<wasmtime::StoreContextMut<'a, T>>) -> &'a mut [u8] {
535        match self {
536            GuestMemory::Plain(m) => m.data_mut(store),
537            // SAFETY: as in data()
538            GuestMemory::Shared(m) => unsafe { std::slice::from_raw_parts_mut(m.data().as_ptr() as *mut u8, m.data().len()) },
539        }
540    }
541
542    pub fn data_and_store_mut<'a, S: wasmtime::AsContextMut + 'a>(&self, store: &'a mut S) -> (&'a mut [u8], &'a mut S::Data)
543    where
544        S::Data: 'static,
545    {
546        match self {
547            GuestMemory::Plain(m) => m.data_and_store_mut(store),
548            GuestMemory::Shared(m) => {
549                // SAFETY: the shared memory isn't the store's, so its bytes and the store's
550                // data don't alias; `store` is borrowed for 'a, which bounds both (as in data())
551                let data = unsafe { std::slice::from_raw_parts_mut(m.data().as_ptr() as *mut u8, m.data().len()) };
552                let host: *mut S::Data = store.as_context_mut().data_mut();
553                (data, unsafe { &mut *host })
554            }
555        }
556    }
557}
558
559pub(crate) fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<GuestMemory> {
560    caller.data().memory.clone().ok_or_else(|| format_err!("guest memory not yet available"))
561}
562
563fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
564    let mem = memory(caller)?;
565    let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
566    std::str::from_utf8(bytes).map(str::to_owned).map_err(|e| format_err!("string argument is not UTF-8: {e}"))
567}
568
569/// The pointer() record (GASM_POINTER_OFF_* layout).
570fn pointer_bytes(p: &Pointer, frame: (usize, usize), aspect: Option<(u32, u32)>) -> [u8; POINTER_BYTES] {
571    let (fx, fy) = frame_position(p.x, p.y, p.drawable, frame, p.integer_scale, aspect);
572    let mut b = [0u8; POINTER_BYTES];
573    for (i, v) in [p.x, p.y, fx, fy, p.dx, p.dy, p.wheel_x, p.wheel_y].into_iter().enumerate() {
574        b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
575    }
576    for (i, v) in [p.buttons, p.pressed, p.released, p.flags].into_iter().enumerate() {
577        b[32 + i * 4..36 + i * 4].copy_from_slice(&v.to_le_bytes());
578    }
579    b
580}
581
582/// The gamepad() record (GASM_GAMEPAD_OFF_* layout).
583fn gamepad_bytes(g: &Gamepad) -> [u8; GAMEPAD_BYTES] {
584    let mut b = [0u8; GAMEPAD_BYTES];
585    let nb = g.buttons.len().min(GAMEPAD_BUTTONS);
586    let na = g.axes.len().min(GAMEPAD_AXES);
587    let flags = if g.connected { 1 | if g.standard { 2 } else { 0 } } else { 0u32 };
588    for (i, v) in [flags, nb as u32, na as u32].into_iter().enumerate() {
589        b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
590    }
591    for (i, v) in g.buttons.iter().take(nb).enumerate() {
592        b[12 + i * 4..16 + i * 4].copy_from_slice(&v.to_le_bytes());
593    }
594    for (i, v) in g.axes.iter().take(na).enumerate() {
595        b[140 + i * 4..144 + i * 4].copy_from_slice(&v.to_le_bytes());
596    }
597    b
598}
599
600fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
601    linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
602        let mem = memory(&caller)?;
603        let msg = guest_slice(mem.data(&caller), ptr, len as u64)?;
604        eprintln!("[guest] {}", String::from_utf8_lossy(msg));
605        Ok(())
606    })?;
607
608    linker.func_wrap("gasm", "has", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
609        let name = guest_str(&caller, ptr, len)?;
610        Ok(caller.data().provided.contains(&name) as i32)
611    })?;
612
613    linker.func_wrap("gasm", "video_set_aspect", |mut caller: Caller<'_, Host>, num: u32, den: u32| -> wasmtime::Result<()> {
614        let aspect = match (num, den) {
615            (0, 0) => None,
616            (1..=65535, 1..=65535) if num as u64 * 8 >= den as u64 && den as u64 * 8 >= num as u64 => Some((num, den)),
617            _ => bail!("video_set_aspect: invalid ratio {num}:{den}"),
618        };
619        caller.data_mut().aspect = aspect;
620        Ok(())
621    })?;
622
623    linker.func_wrap("gasm", "set_title", |mut caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
624        let title = clean_title(&guest_str(&caller, ptr, len)?);
625        let h = caller.data_mut();
626        if h.title != title {
627            h.title = title;
628            h.title_changed = true;
629        }
630        Ok(())
631    })?;
632
633    // fullscreen: a request the runner applies after the frame (or not: headless runs)
634    linker.func_wrap("gasm", "set_fullscreen", |mut caller: Caller<'_, Host>, on: u32| -> i32 {
635        let h = caller.data_mut();
636        if !h.fullscreen_capable {
637            return 0;
638        }
639        h.fullscreen_request = Some(on != 0);
640        1
641    })?;
642    linker.func_wrap("gasm", "is_fullscreen", |caller: Caller<'_, Host>| -> u32 { caller.data().fullscreen as u32 })?;
643
644    linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
645        let h = caller.data();
646        h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
647    })?;
648
649    linker.func_wrap("gasm", "max_threads", |caller: Caller<'_, Host>| -> u32 {
650        caller.data().threads().map_or(0, |t| t.limit() as u32)
651    })?;
652
653    linker.func_wrap("gasm", "utc_offset_minutes", |caller: Caller<'_, Host>| -> i32 {
654        // headless runs are reproducible: UTC, like their clocks
655        if caller.data().virtual_time_ms.is_some() { 0 } else { utc_offset_minutes() }
656    })?;
657
658    linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
659        if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
660            caller.data_mut().frame_rate = hz;
661        }
662    })?;
663
664    linker.func_wrap(
665        "gasm",
666        "video_present",
667        |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
668            let (w, h, stride) = (w as usize, h as usize, stride as usize);
669            if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
670                bail!("video_present: bad geometry {w}x{h} stride {stride}");
671            }
672            let mem = memory(&caller)?;
673            let (data, host) = mem.data_and_store_mut(&mut caller);
674            let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
675            let row = w * 4;
676            host.rgba.resize(row * h, 0);
677            if stride == row {
678                host.rgba.copy_from_slice(src);
679            } else {
680                for (y, dst) in host.rgba.chunks_exact_mut(row).enumerate() {
681                    dst.copy_from_slice(&src[y * stride..y * stride + row]);
682                }
683            }
684            if host.hashing {
685                host.video_hash.update(&host.rgba);
686            }
687            host.width = w;
688            host.height = h;
689            host.frames_presented += 1;
690            Ok(())
691        },
692    )?;
693
694    linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
695        let h = caller.data_mut();
696        if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
697            h.audio_rate = rate;
698            h.audio_channels = channels;
699            if let Some(a) = &mut h.audio {
700                a.configure(rate, channels);
701            }
702        }
703    })?;
704
705    linker.func_wrap(
706        "gasm",
707        "audio_push",
708        |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
709            let mem = memory(&caller)?;
710            let (data, host) = mem.data_and_store_mut(&mut caller);
711            let len = frames as u64 * host.audio_channels as u64 * 4;
712            let bytes = guest_slice(data, ptr, len)?;
713            if host.hashing {
714                host.audio_hash.update(bytes);
715            }
716            host.audio_frames += frames as u64;
717            if let Some(a) = &mut host.audio {
718                a.push_le_f32(bytes);
719            }
720            Ok(())
721        },
722    )?;
723
724    linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
725        caller.data().pads.get(player as usize).copied().unwrap_or(0)
726    })?;
727
728    linker.func_wrap(
729        "gasm",
730        "text_input",
731        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
732            let mem = memory(&caller)?;
733            let (data, host) = mem.data_and_store_mut(&mut caller);
734            let Some(t) = &host.text else { return Ok(-1) };
735            copy_if_fits(data, dst, cap, t.as_bytes())
736        },
737    )?;
738
739    linker.func_wrap("gasm", "input_mode", |mut caller: Caller<'_, Host>, flags: u32| {
740        caller.data_mut().input_mode = flags & 7;
741    })?;
742
743    linker.func_wrap(
744        "gasm",
745        "key_state",
746        |mut caller: Caller<'_, Host>, dst: u32, len: u32| -> wasmtime::Result<i32> {
747            let mem = memory(&caller)?;
748            let (data, host) = mem.data_and_store_mut(&mut caller);
749            let Some(keys) = host.input.keys else { return Ok(-1) };
750            let n = (len as usize).min(KEY_STATE_BYTES);
751            guest_slice_mut(data, dst, n as u64)?.copy_from_slice(&keys[..n]);
752            Ok(KEY_STATE_BYTES as i32)
753        },
754    )?;
755
756    linker.func_wrap(
757        "gasm",
758        "key_events",
759        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
760            let mem = memory(&caller)?;
761            let (data, host) = mem.data_and_store_mut(&mut caller);
762            if host.input.keys.is_none() {
763                return Ok(-1);
764            }
765            let events = &host.input.key_events;
766            let len = events.len() * 4;
767            if len <= cap as usize {
768                let out = guest_slice_mut(data, dst, len as u64)?;
769                for (b, &(c, d)) in out.chunks_exact_mut(4).zip(events) {
770                    let c = c.to_le_bytes();
771                    b.copy_from_slice(&[c[0], c[1], d as u8, 0]);
772                }
773            }
774            Ok(len as i32)
775        },
776    )?;
777
778    linker.func_wrap(
779        "gasm",
780        "pointer",
781        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
782            let mem = memory(&caller)?;
783            let (data, host) = mem.data_and_store_mut(&mut caller);
784            let Some(p) = &host.input.pointer else { return Ok(-1) };
785            if cap as usize >= POINTER_BYTES {
786                let b = pointer_bytes(p, (host.width, host.height), host.aspect);
787                guest_slice_mut(data, dst, POINTER_BYTES as u64)?.copy_from_slice(&b);
788            }
789            Ok(POINTER_BYTES as i32)
790        },
791    )?;
792
793    linker.func_wrap(
794        "gasm",
795        "gamepad",
796        |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
797            let mem = memory(&caller)?;
798            let (data, host) = mem.data_and_store_mut(&mut caller);
799            let (Some(pads), true) = (&host.input.gamepads, slot < 4) else { return Ok(-1) };
800            if cap as usize >= GAMEPAD_BYTES {
801                let b = gamepad_bytes(&pads[slot as usize]);
802                guest_slice_mut(data, dst, GAMEPAD_BYTES as u64)?.copy_from_slice(&b);
803            }
804            Ok(GAMEPAD_BYTES as i32)
805        },
806    )?;
807
808    linker.func_wrap(
809        "gasm",
810        "gamepad_name",
811        |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
812            let mem = memory(&caller)?;
813            let (data, host) = mem.data_and_store_mut(&mut caller);
814            let Some(g) = host.input.gamepads.as_ref().and_then(|p| p.get(slot as usize)).filter(|g| g.connected) else { return Ok(-1) };
815            copy_if_fits(data, dst, cap, g.name.as_bytes())
816        },
817    )?;
818
819    linker.func_wrap(
820        "gasm",
821        "param",
822        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
823            let name = guest_str(&caller, ptr, len)?;
824            let mem = memory(&caller)?;
825            let (data, host) = mem.data_and_store_mut(&mut caller);
826            let Some(v) = host.params.get(&name) else { return Ok(-1) };
827            copy_if_fits(data, dst, cap, v.as_bytes())
828        },
829    )?;
830
831    // Assets: n = bytes available from offset (clamped to the request) is bounds-checked
832    // against guest memory before anything is read.
833    fn read_asset(caller: &mut Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32) -> wasmtime::Result<i32> {
834        let name = guest_str(caller, ptr, len)?;
835        let mem = memory(caller)?;
836        let (data, host) = mem.data_and_store_mut(caller);
837        let Some(asset) = host.assets.get(&name) else { return Ok(-1) };
838        let n = asset.size().saturating_sub(offset).min(cap as u64);
839        let out = guest_slice_mut(data, dst, n)?;
840        Ok(asset.read_at(offset, out) as i32)
841    }
842
843    linker.func_wrap(
844        "gasm",
845        "asset_read_at",
846        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
847            read_asset(&mut caller, ptr, len, offset as u64, dst, cap)
848        },
849    )?;
850
851    linker.func_wrap(
852        "gasm",
853        "asset_read_at64",
854        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32| -> wasmtime::Result<i32> {
855            read_asset(&mut caller, ptr, len, offset, dst, cap)
856        },
857    )?;
858
859    linker.func_wrap(
860        "gasm",
861        "asset_read",
862        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
863            read_asset(&mut caller, ptr, len, 0, dst, cap)
864        },
865    )?;
866
867    linker.func_wrap("gasm", "asset_count", |caller: Caller<'_, Host>| -> u32 { caller.data().assets.names().len() as u32 })?;
868
869    linker.func_wrap(
870        "gasm",
871        "asset_name",
872        |mut caller: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
873            let mem = memory(&caller)?;
874            let (data, host) = mem.data_and_store_mut(&mut caller);
875            let Some(n) = host.assets.names().get(index as usize) else { return Ok(-1) };
876            copy_if_fits(data, dst, cap, n.as_bytes())
877        },
878    )?;
879
880    linker.func_wrap(
881        "gasm",
882        "asset_size",
883        |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
884            let name = guest_str(&caller, ptr, len)?;
885            Ok(match caller.data().assets.size(&name) {
886                None => -1,
887                Some(n) if n > i32::MAX as u64 => -2,
888                Some(n) => n as i32,
889            })
890        },
891    )?;
892
893    linker.func_wrap(
894        "gasm",
895        "asset_size64",
896        |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i64> {
897            let name = guest_str(&caller, ptr, len)?;
898            Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i64))
899        },
900    )?;
901
902    linker.func_wrap(
903        "gasm",
904        "asset_version",
905        |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
906            let name = guest_str(&caller, ptr, len)?;
907            Ok(caller.data().assets.version(&name).map_or(-1, |v| v.min(i32::MAX as u32) as i32))
908        },
909    )?;
910    Ok(())
911}
912
913fn trap(e: String) -> wasmtime::Error {
914    format_err!("{e}")
915}
916
917fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
918    const M: &str = "gasm:gfx";
919    linker.func_wrap(M, "width", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().0 })?;
920    linker.func_wrap(M, "height", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().1 })?;
921    linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
922        let src = guest_str(&c, ptr, len)?;
923        c.data_mut().gfx.create_shader(&src).map_err(trap)
924    })?;
925    linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
926        c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
927    })?;
928    linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
929        let json = guest_str(&c, ptr, len)?;
930        c.data_mut().gfx.create_pipeline(&json).map_err(trap)
931    })?;
932    linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
933        let json = guest_str(&c, ptr, len)?;
934        c.data_mut().gfx.create_bind_group(&json).map_err(trap)
935    })?;
936    linker.func_wrap(
937        M,
938        "write_buffer",
939        |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
940            let mem = memory(&c)?;
941            let (data, host) = mem.data_and_store_mut(&mut c);
942            let bytes = guest_slice(data, ptr, len as u64)?;
943            host.gfx.write_buffer(buf, offset, bytes).map_err(trap)?;
944            if host.hashing {
945                host.video_hash.update(bytes);
946            }
947            Ok(())
948        },
949    )?;
950    linker.func_wrap(M, "create_bind_group_layout", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
951        let json = guest_str(&c, ptr, len)?;
952        c.data_mut().gfx.create_bind_group_layout(&json).map_err(trap)
953    })?;
954    linker.func_wrap(M, "create_texture", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
955        let json = guest_str(&c, ptr, len)?;
956        c.data_mut().gfx.create_texture(&json).map_err(trap)
957    })?;
958    linker.func_wrap(M, "create_sampler", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
959        let json = guest_str(&c, ptr, len)?;
960        c.data_mut().gfx.create_sampler(&json).map_err(trap)
961    })?;
962    linker.func_wrap(
963        M,
964        "write_texture",
965        |mut c: Caller<'_, Host>, tex: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
966            let mem = memory(&c)?;
967            let (data, host) = mem.data_and_store_mut(&mut c);
968            let bytes = guest_slice(data, ptr, len as u64)?;
969            host.gfx.write_texture(tex, mip, x, y, w, h, bytes).map_err(trap)?;
970            if host.hashing {
971                // header (little-endian u32s) + payload, so the target region counts too
972                for v in [tex, mip, x, y, w, h] {
973                    host.video_hash.update(&v.to_le_bytes());
974                }
975                host.video_hash.update(bytes);
976            }
977            Ok(())
978        },
979    )?;
980    linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
981        let h = c.data_mut();
982        let show = h.show_frame;
983        h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
984    })?;
985    linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
986        c.data_mut().gfx.set_pipeline(p).map_err(trap)
987    })?;
988    linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
989        c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
990    })?;
991    linker.func_wrap(
992        M,
993        "set_bind_group_offsets",
994        |mut c: Caller<'_, Host>, i: u32, bg: u32, ptr: u32, count: u32| -> wasmtime::Result<()> {
995            let mem = memory(&c)?;
996            let (data, host) = mem.data_and_store_mut(&mut c);
997            let bytes = guest_slice(data, ptr, count as u64 * 4)?;
998            let mut offsets = [0u32; 32];
999            if count as usize > offsets.len() {
1000                bail!("gfx.set_bind_group_offsets: {count} offsets (a bind group has at most {})", offsets.len());
1001            }
1002            for (o, b) in offsets.iter_mut().zip(bytes.chunks_exact(4)) {
1003                *o = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
1004            }
1005            host.gfx.set_bind_group_offsets(i, bg, &offsets[..count as usize]).map_err(trap)
1006        },
1007    )?;
1008    linker.func_wrap(
1009        M,
1010        "set_viewport",
1011        |mut c: Caller<'_, Host>, x: f32, y: f32, w: f32, h: f32, min: f32, max: f32| -> wasmtime::Result<()> {
1012            c.data_mut().gfx.set_viewport(x, y, w, h, min, max).map_err(trap)
1013        },
1014    )?;
1015    linker.func_wrap(M, "set_scissor_rect", |mut c: Caller<'_, Host>, x: u32, y: u32, w: u32, h: u32| -> wasmtime::Result<()> {
1016        c.data_mut().gfx.set_scissor_rect(x, y, w, h).map_err(trap)
1017    })?;
1018    linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
1019        c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
1020    })?;
1021    linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
1022        c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
1023    })?;
1024    linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| -> wasmtime::Result<()> {
1025        c.data_mut().gfx.draw(vc, ic, fv, fi).map_err(trap)
1026    })?;
1027    linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| -> wasmtime::Result<()> {
1028        c.data_mut().gfx.draw_indexed(ic, n, first, base, fi).map_err(trap)
1029    })?;
1030    linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
1031        let h = c.data_mut();
1032        h.frames_presented += 1;
1033        h.gfx.end_frame().map_err(trap)
1034    })?;
1035    linker.func_wrap(M, "destroy", |mut c: Caller<'_, Host>, handle: u32| -> wasmtime::Result<()> {
1036        c.data_mut().gfx.destroy(handle).map_err(trap)
1037    })?;
1038    Ok(())
1039}
1040
1041fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1042    const M: &str = "gasm:net";
1043    linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
1044        let url = guest_str(&c, ptr, len)?;
1045        Ok(c.data_mut().net.open(&url))
1046    })?;
1047    linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> { c.data().net.state(h).map_err(trap) })?;
1048    linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
1049        let mem = memory(&c)?;
1050        let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
1051        if msg.is_empty() {
1052            c.data().net.state(h).map_err(trap)?; // still validates the handle
1053            return Ok(-1);
1054        }
1055        c.data_mut().net.send(h, msg).map_err(trap)
1056    })?;
1057    linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1058        let mem = memory(&c)?;
1059        let (data, host) = mem.data_and_store_mut(&mut c);
1060        let msg = match host.net.peek(h).map_err(trap)? {
1061            Ok(m) => m,
1062            Err(code) => return Ok(code),
1063        };
1064        let n = copy_if_fits(data, dst, cap, msg)?;
1065        if n as usize <= cap as usize {
1066            host.net.pop(h);
1067        }
1068        Ok(n)
1069    })?;
1070    linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().net.close(h).map_err(trap) })?;
1071    Ok(())
1072}
1073
1074fn add_fetch_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1075    const M: &str = "gasm:fetch";
1076    linker.func_wrap(M, "request", |mut c: Caller<'_, Host>, dp: u32, dl: u32, bp: u32, bl: u32| -> wasmtime::Result<i32> {
1077        let desc = guest_str(&c, dp, dl)?;
1078        let mem = memory(&c)?;
1079        let body = guest_slice(mem.data(&c), bp, bl as u64)?.to_vec();
1080        let h = c.data_mut();
1081        let frame = h.frame_index;
1082        Ok(h.fetch.request(&desc, body, frame))
1083    })?;
1084    linker.func_wrap(M, "state", |mut c: Caller<'_, Host>, r: i32| -> wasmtime::Result<u32> {
1085        let h = c.data_mut();
1086        let frame = h.frame_index;
1087        h.fetch.state(r, frame).map_err(trap)
1088    })?;
1089    linker.func_wrap(M, "status", |mut c: Caller<'_, Host>, r: i32| -> wasmtime::Result<i32> {
1090        let h = c.data_mut();
1091        let frame = h.frame_index;
1092        h.fetch.status(r, frame).map_err(trap)
1093    })?;
1094    linker.func_wrap(M, "headers", |mut c: Caller<'_, Host>, r: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1095        let mem = memory(&c)?;
1096        let (data, host) = mem.data_and_store_mut(&mut c);
1097        let frame = host.frame_index;
1098        match host.fetch.headers(r, frame).map_err(trap)? {
1099            Some(text) => copy_if_fits(data, dst, cap, text.as_bytes()),
1100            None => Ok(-1),
1101        }
1102    })?;
1103    linker.func_wrap(M, "read", |mut c: Caller<'_, Host>, r: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1104        let mem = memory(&c)?;
1105        guest_slice(mem.data(&c), dst, cap as u64)?; // the whole destination must be guest memory
1106        let (data, host) = mem.data_and_store_mut(&mut c);
1107        let frame = host.frame_index;
1108        match host.fetch.read(r, (cap as usize).min(i32::MAX as usize), frame).map_err(trap)? {
1109            Ok(bytes) => {
1110                guest_slice_mut(data, dst, bytes.len() as u64)?.copy_from_slice(&bytes);
1111                Ok(bytes.len() as i32)
1112            }
1113            Err(()) => Ok(-1),
1114        }
1115    })?;
1116    linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, r: i32| -> wasmtime::Result<()> { c.data_mut().fetch.close(r).map_err(trap) })?;
1117    Ok(())
1118}
1119
1120fn add_media_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1121    const M: &str = "gasm:media";
1122    linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, dp: u32, dl: u32| -> wasmtime::Result<i32> {
1123        let desc = guest_str(&c, dp, dl)?;
1124        Ok(c.data_mut().media.open(&desc))
1125    })?;
1126    linker.func_wrap(M, "play", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().media.play(h).map_err(trap) })?;
1127    linker.func_wrap(M, "pause", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().media.pause(h).map_err(trap) })?;
1128    linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().media.close(h).map_err(trap) })?;
1129    linker.func_wrap(M, "set_volume", |mut c: Caller<'_, Host>, h: i32, gain: f32| -> wasmtime::Result<()> {
1130        c.data_mut().media.set_volume(h, gain).map_err(trap)
1131    })?;
1132    linker.func_wrap(M, "set_lowpass", |mut c: Caller<'_, Host>, h: i32, hz: f32| -> wasmtime::Result<()> {
1133        c.data_mut().media.set_lowpass(h, hz).map_err(trap)
1134    })?;
1135    linker.func_wrap(M, "caps", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> { c.data_mut().media.caps(h).map_err(trap) })?;
1136    linker.func_wrap(M, "state", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> { c.data_mut().media.state(h).map_err(trap) })?;
1137    linker.func_wrap(M, "error", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1138        let mem = memory(&c)?;
1139        let (data, host) = mem.data_and_store_mut(&mut c);
1140        let text = host.media.error(h).map_err(trap)?;
1141        copy_if_fits(data, dst, cap, text.as_bytes())
1142    })?;
1143    linker.func_wrap(M, "position", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<f64> { c.data_mut().media.position(h).map_err(trap) })?;
1144    linker.func_wrap(M, "duration", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<f64> { c.data_mut().media.duration(h).map_err(trap) })?;
1145    linker.func_wrap(M, "metadata", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1146        let mem = memory(&c)?;
1147        let (data, host) = mem.data_and_store_mut(&mut c);
1148        let json = host.media.metadata(h).map_err(trap)?;
1149        copy_if_fits(data, dst, cap, json.as_bytes())
1150    })?;
1151    linker.func_wrap(M, "metadata_version", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> {
1152        c.data_mut().media.metadata_version(h).map_err(trap)
1153    })?;
1154    Ok(())
1155}
1156
1157/// Every gasm import this runner has.
1158fn add_all_imports(linker: &mut Linker<Host>, switching: bool) -> wasmtime::Result<()> {
1159    add_gasm_imports(linker)?;
1160    if switching {
1161        switching::add_yield_async(linker)?;
1162    } else {
1163        switching::add_yield_sync(linker)?;
1164    }
1165    add_gfx_imports(linker)?;
1166    crate::gl::add_gl_imports(linker)?;
1167    add_net_imports(linker)?;
1168    add_fetch_imports(linker)?;
1169    add_media_imports(linker)?;
1170    add_storage_imports(linker)?;
1171    add_clipboard_imports(linker)?;
1172    add_files_imports(linker)?;
1173    Ok(())
1174}
1175
1176/// What `has` reports: the gasm modules and functions linked (not WASI stubs or traps).
1177fn provided_by(linker: &Linker<Host>, store: &mut Store<Host>) -> HashSet<String> {
1178    let mut provided = HashSet::new();
1179    for (m, f, _) in linker.iter(&mut *store) {
1180        provided.insert(m.to_owned());
1181        provided.insert(format!("{m}.{f}"));
1182    }
1183    for f in wasi::IMPLEMENTED {
1184        provided.insert(format!("{}.{f}", wasi::MODULE));
1185    }
1186    provided.insert(wasi::MODULE.to_owned());
1187    provided
1188}
1189
1190/// What this runner provides (`gasm.has`), without a game (`gasm-run --info`).
1191pub fn provided() -> HashSet<String> {
1192    let mut linker: Linker<Host> = Linker::new(engine());
1193    if add_all_imports(&mut linker, false).is_err() {
1194        return HashSet::new();
1195    }
1196    let host = Host::new(crate::assets::Assets::new(), HashMap::new(), None, crate::gfx::Gfx::null(), Net::new(false), crate::storage::Storage::memory());
1197    let mut store = Store::new(engine(), host);
1198    provided_by(&linker, &mut store)
1199}
1200
1201fn add_clipboard_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1202    const M: &str = "gasm:clipboard";
1203    linker.func_wrap(M, "set_text", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
1204        if len as usize > CLIPBOARD_MAX {
1205            return Ok(-1);
1206        }
1207        let text = guest_str(&c, ptr, len)?;
1208        c.data_mut().clipboard.copied = Some(text);
1209        Ok(0)
1210    })?;
1211    linker.func_wrap(M, "get_text", |mut c: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1212        let mem = memory(&c)?;
1213        let (data, host) = mem.data_and_store_mut(&mut c);
1214        match &host.clipboard.pasted {
1215            Some(t) => copy_if_fits(data, dst, cap, t.as_bytes()),
1216            None => Ok(-1),
1217        }
1218    })?;
1219    Ok(())
1220}
1221
1222fn add_files_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1223    const M: &str = "gasm:files";
1224    linker.func_wrap(M, "save", |mut c: Caller<'_, Host>, np: u32, nl: u32, mp: u32, ml: u32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
1225        let (name, mime) = (guest_str(&c, np, nl)?, guest_str(&c, mp, ml)?);
1226        let mem = memory(&c)?;
1227        let (data, host) = mem.data_and_store_mut(&mut c);
1228        let bytes = guest_slice(data, ptr, len as u64)?;
1229        Ok(host.files.save(&name, &mime, bytes))
1230    })?;
1231    linker.func_wrap(M, "state", |c: Caller<'_, Host>, handle: i32| -> wasmtime::Result<i32> {
1232        c.data().files.state(handle).ok_or_else(|| format_err!("gasm:files: invalid handle {handle}"))
1233    })?;
1234    Ok(())
1235}
1236
1237fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1238    const M: &str = "gasm:storage";
1239    linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1240        let key = guest_str(&c, kp, kl)?;
1241        let mem = memory(&c)?;
1242        let (data, host) = mem.data_and_store_mut(&mut c);
1243        let Some(v) = host.storage.get(&key) else { return Ok(-1) };
1244        copy_if_fits(data, dst, cap, v)
1245    })?;
1246    linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
1247        let key = guest_str(&c, kp, kl)?;
1248        let mem = memory(&c)?;
1249        let (data, host) = mem.data_and_store_mut(&mut c);
1250        let value = guest_slice(data, vp, vl as u64)?;
1251        Ok(match host.storage.set(&key, value) {
1252            Ok(()) => 0,
1253            Err(e) => {
1254                eprintln!("[gasm] storage: {key}: {e}");
1255                e.code()
1256            }
1257        })
1258    })?;
1259    linker.func_wrap(M, "count", |mut c: Caller<'_, Host>| -> u32 { c.data_mut().storage.keys().len() as u32 })?;
1260    linker.func_wrap(M, "key", |mut c: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1261        let mem = memory(&c)?;
1262        let (data, host) = mem.data_and_store_mut(&mut c);
1263        let Some(k) = host.storage.keys().get(index as usize) else { return Ok(-1) };
1264        copy_if_fits(data, dst, cap, k.as_bytes())
1265    })?;
1266    linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
1267        let key = guest_str(&c, kp, kl)?;
1268        Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
1269    })?;
1270    Ok(())
1271}
1272
1273/// Why a guest call ended.
1274#[derive(Debug)]
1275pub enum Stop {
1276    /// The guest called proc_exit(code).
1277    Exit(i32),
1278    Trap(String),
1279}
1280
1281impl std::fmt::Display for Stop {
1282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1283        match self {
1284            Stop::Exit(code) => write!(f, "guest exited with code {code}"),
1285            Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
1286        }
1287    }
1288}
1289
1290impl std::error::Error for Stop {}
1291
1292fn classify(e: wasmtime::Error) -> Stop {
1293    if let Some(exit) = e.downcast_ref::<wasi::Exit>() {
1294        return Stop::Exit(exit.0);
1295    }
1296    if e.downcast_ref::<switching::Quit>().is_some() {
1297        return Stop::Exit(0);
1298    }
1299    if e.downcast_ref::<wasmtime::Trap>() == Some(&wasmtime::Trap::Interrupt) {
1300        return Stop::Trap(format!("a guest call ran longer than the time limit (stuck in a loop?); see --call-timeout\n{e:?}"));
1301    }
1302    Stop::Trap(format!("{e:?}"))
1303}
1304
1305/// Epoch ticks per second (the watchdog thread's rate).
1306const TICKS_PER_SEC: u64 = 10;
1307
1308/// The process-wide engine: the same configuration for compiling, precompiling
1309/// and loading (`.cwasm` files must match it). Epoch interruption lets a guest
1310/// call that never returns be stopped (`LoadOptions::call_timeout`). Calls can be
1311/// sync (`gasm_frame` guests) or async (`gasm_run` guests: stack switching).
1312pub fn engine() -> &'static Engine {
1313    static ENGINE: OnceLock<Engine> = OnceLock::new();
1314    ENGINE.get_or_init(new_engine)
1315}
1316
1317fn new_engine() -> Engine {
1318    let mut config = Config::new();
1319    config.epoch_interruption(true);
1320    // wasi-threads guests (shared memory); modules without one are unaffected
1321    config.shared_memory(true);
1322    // a gasm_run guest's whole run is on this stack: the default wasm stack limit plus room
1323    config.async_stack_size(4 << 20);
1324    let engine = Engine::new(&config).expect("wasmtime engine");
1325    let ticker = engine.clone();
1326    std::thread::Builder::new()
1327        .name("gasm-epoch".into())
1328        .spawn(move || {
1329            loop {
1330                std::thread::sleep(Duration::from_millis(1000 / TICKS_PER_SEC));
1331                ticker.increment_epoch();
1332            }
1333        })
1334        .expect("epoch thread");
1335    engine
1336}
1337
1338/// `--profile <out.json>`: wasmtime's `GuestProfiler` sampling the main thread's guest
1339/// stacks every millisecond (an extra epoch ticker; call timeouts are off meanwhile),
1340/// written in the Firefox Profiler's format when the game ends. Only guest frames are
1341/// recorded, and wasmtime checks the epoch only in guest code: the milliseconds a call
1342/// into the runner (gasm:gl, ANGLE, ...) took are samples of the guest stack it returns
1343/// to, so they count for the function that called it. The time inside those calls is
1344/// measured too (the call hook) and printed with the file.
1345pub struct Profile {
1346    out: std::path::PathBuf,
1347    profiler: Option<wasmtime::GuestProfiler>,
1348    /// host calls in progress (the call hook)
1349    host_depth: u32,
1350    host_since: Option<Instant>,
1351    host_time: Duration,
1352    start: Instant,
1353    last: Option<Instant>,
1354    samples: u64,
1355}
1356
1357impl Profile {
1358    pub fn new(out: std::path::PathBuf) -> Profile {
1359        Profile { out, profiler: None, host_depth: 0, host_since: None, host_time: Duration::ZERO, start: Instant::now(), last: None, samples: 0 }
1360    }
1361}
1362
1363impl Drop for Profile {
1364    fn drop(&mut self) {
1365        let Some(p) = self.profiler.take() else { return };
1366        let written = std::fs::File::create(&self.out).map_err(wasmtime::Error::from).and_then(|f| p.finish(std::io::BufWriter::new(f)));
1367        match written {
1368            Ok(()) => eprintln!(
1369                "[gasm] profile: {} samples in {:.1} s, {:.1} % of it inside calls to the runner -> {} (open it at https://profiler.firefox.com)",
1370                self.samples,
1371                self.start.elapsed().as_secs_f64(),
1372                self.host_time.as_secs_f64() * 100.0 / self.start.elapsed().as_secs_f64().max(1e-9),
1373                self.out.display()
1374            ),
1375            Err(e) => eprintln!("[gasm] profile: can't write {}: {e}", self.out.display()),
1376        }
1377    }
1378}
1379
1380const PROFILE_INTERVAL: Duration = Duration::from_millis(1);
1381
1382/// The profiler's clock: the engine's epoch every millisecond (on top of the usual ticker).
1383fn start_profile_ticker() {
1384    static STARTED: OnceLock<()> = OnceLock::new();
1385    STARTED.get_or_init(|| {
1386        let ticker = engine().clone();
1387        std::thread::Builder::new()
1388            .name("gasm-profile".into())
1389            .spawn(move || {
1390                loop {
1391                    std::thread::sleep(PROFILE_INTERVAL);
1392                    ticker.increment_epoch();
1393                }
1394            })
1395            .expect("profile thread");
1396    });
1397}
1398
1399/// Ahead-of-time compile a guest to native code for this host (no JIT needed at load).
1400pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
1401    engine().precompile_module(wasm)
1402}
1403
1404/// How to load a guest.
1405#[derive(Clone, Copy)]
1406pub struct LoadOptions {
1407    /// Accept a precompiled `.cwasm` (native code: only files you made yourself
1408    /// with `gasm-run --compile`). Otherwise only `.wasm` modules load.
1409    pub allow_precompiled: bool,
1410    /// Trap a single guest call (init, a frame, exit) that runs longer than this.
1411    pub call_timeout: Option<Duration>,
1412    /// Run guests that export `gasm_run` that way (the runner switches stacks).
1413    /// Off: always `gasm_frame` (tests of the Asyncify path).
1414    pub stack_switching: bool,
1415    /// Largest guest memory in bytes (None: the engine's maximum, 4 GiB for wasm32).
1416    pub memory_limit: Option<usize>,
1417    /// Most worker threads a wasi-threads guest may have at once (0: `thread-spawn` fails,
1418    /// as in reproducible runs).
1419    pub threads: usize,
1420}
1421
1422impl Default for LoadOptions {
1423    fn default() -> Self {
1424        LoadOptions { allow_precompiled: false, call_timeout: Some(Duration::from_secs(30)), stack_switching: true, memory_limit: Some(DEFAULT_MEMORY_LIMIT), threads: 0 }
1425    }
1426}
1427
1428/// The running `gasm_run` call: owns the store until the run ends.
1429type Running = Pin<Box<dyn Future<Output = (Store<Host>, wasmtime::Result<i32>)>>>;
1430
1431/// A running guest. `gasm_frame` guests are called once per frame; `gasm_run`
1432/// guests run in one suspended call (stack switching) that each [`Game::frame`]
1433/// resumes until its next `yield_frame`.
1434pub struct Game {
1435    /// None while a `gasm_run` call is running (the call owns it): use [`Game::with_host`]
1436    store: Option<Store<Host>>,
1437    frame: TypedFunc<(), ()>,
1438    exit: Option<TypedFunc<(), ()>>,
1439    deadline: u64,
1440    /// `gasm_run` (async engine)
1441    run: Option<TypedFunc<(), i32>>,
1442    running: Option<Running>,
1443    exchange: Option<Arc<Exchange>>,
1444    /// the guest exited or trapped: it is never called again
1445    ended: bool,
1446    /// the guest imports gasm:gl (the native window has no GL backend yet)
1447    gl: bool,
1448    /// files re-read as assets when they change (`--watch-asset`), checked before each frame
1449    watches: Vec<crate::assets::AssetWatch>,
1450}
1451
1452impl Game {
1453    pub fn load(wasm: &[u8], host: Host, opts: LoadOptions) -> Result<Game, Stop> {
1454        let module = Self::compile(wasm, opts.allow_precompiled)?;
1455        Self::load_module(module, host, opts)
1456    }
1457
1458    /// Compile a module (or load a `.cwasm`) for [`Game::load_module`]: the slow
1459    /// part of loading, and possible on another thread.
1460    pub fn compile(wasm: &[u8], allow_precompiled: bool) -> Result<Module, Stop> {
1461        let module = if wasm.starts_with(b"\0asm") {
1462            Module::new(engine(), wasm)
1463        } else if allow_precompiled {
1464            // Precompiled artifact from `gasm-run --compile`: native code, trusted
1465            // like the runner itself (unlike .wasm, which is sandboxed).
1466            unsafe { Module::deserialize(engine(), wasm) }
1467        } else {
1468            Err(format_err!("not a wasm module (a precompiled .cwasm runs as native code: pass --allow-precompiled if you made it yourself)"))
1469        };
1470        module.map_err(classify)
1471    }
1472
1473    /// Instantiate a compiled module and run its init.
1474    pub fn load_module(module: Module, host: Host, opts: LoadOptions) -> Result<Game, Stop> {
1475        Self::load_inner(module, host, opts).map_err(classify)
1476    }
1477
1478    fn load_inner(module: Module, host: Host, opts: LoadOptions) -> wasmtime::Result<Game> {
1479        let gl = module.imports().any(|i| i.module() == "gasm:gl");
1480        if gl && module.imports().any(|i| i.module() == "gasm:gfx") {
1481            bail!("a module imports gasm:gfx or gasm:gl, not both");
1482        }
1483        // guests that export gasm_run run in one async call (stack switching)
1484        let has_run = module.get_export("gasm_run").is_some();
1485        let switching = has_run && opts.stack_switching;
1486        if has_run && !switching && module.imports().any(|i| i.module() == "asyncify") {
1487            // made without wasm-opt --asyncify: its gasm_frame can't suspend
1488            bail!("this module is a run build (no Asyncify): it needs stack switching (gasm_run); use the game's Asyncify build");
1489        }
1490        let engine = engine();
1491        let mut linker: Linker<Host> = Linker::new(engine);
1492        add_all_imports(&mut linker, switching)?;
1493        let mut store = Store::new(engine, host);
1494        store.data_mut().memory_limit = MemoryLimit(opts.memory_limit);
1495        store.limiter(|h| &mut h.memory_limit);
1496        let provided = provided_by(&linker, &mut store);
1497        // the manifest's requirements, before the guest runs at all
1498        let missing: Vec<&String> = store.data().requires.iter().filter(|r| !provided.contains(r.as_str())).collect();
1499        if !missing.is_empty() {
1500            bail!("this game needs {}, which this runner (gasm-run {}) doesn't have", missing.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "), env!("CARGO_PKG_VERSION"));
1501        }
1502        store.data_mut().provided = provided;
1503        wasi::add_to_linker(&mut linker, &module)?;
1504        // wasi-threads: a shared memory the module imports (every thread's instance gets
1505        // the same one) and thread-spawn
1506        let threads = match crate::threads::shared_memory_import(&module) {
1507            Some(ty) => {
1508                if switching {
1509                    bail!("a module with threads (shared memory) can't use gasm_run (stack switching)");
1510                }
1511                let mem = wasmtime::SharedMemory::new(engine, ty)?;
1512                let import = module.imports().find(|i| matches!(i.ty(), wasmtime::ExternType::Memory(_))).expect("memory import");
1513                linker.define(&mut store, import.module(), import.name(), mem.clone())?;
1514                let t = crate::threads::Threads::new(engine, &module, mem, opts.threads, store.data().start);
1515                store.data_mut().threads = Some(t.clone());
1516                Some(t)
1517            }
1518            None => None,
1519        };
1520        crate::threads::add_to_linker(&mut linker)?;
1521        // Imports this runner doesn't know (e.g. JS glue some Rust crates pull in)
1522        // link as traps: harmless unless the guest actually calls them.
1523        linker.define_unknown_imports_as_traps(&module)?;
1524        if let Some(t) = &threads {
1525            t.set_linker(linker.clone());
1526        }
1527
1528        let profiling = store.data().profile.is_some();
1529        let deadline = if profiling {
1530            1 // (a sample at every tick: the callback below continues, so no call times out)
1531        } else {
1532            opts.call_timeout.map_or(u64::MAX / 2, |t| (t.as_millis() as u64 * TICKS_PER_SEC).div_ceil(1000).max(1))
1533        };
1534        store.epoch_deadline_trap();
1535        if profiling {
1536            let p = wasmtime::GuestProfiler::new(engine, "game", PROFILE_INTERVAL, [("game".to_string(), module.clone())])?;
1537            store.data_mut().profile.as_mut().expect("profile").profiler = Some(p);
1538            store.epoch_deadline_callback(|mut ctx| {
1539                let now = Instant::now();
1540                let (prof, last) = match ctx.data_mut().profile.as_mut() {
1541                    Some(pr) => (pr.profiler.take(), pr.last.replace(now)),
1542                    None => (None, None),
1543                };
1544                if let Some(mut prof) = prof {
1545                    // one sample a millisecond since the last one (ticks missed inside a call
1546                    // to the runner come here, when it returns), at most a second's worth
1547                    let n = last.map_or(1, |l| ((now - l).as_micros() / 1000).clamp(1, 1000) as u64);
1548                    for _ in 0..n {
1549                        prof.sample(&ctx, PROFILE_INTERVAL);
1550                    }
1551                    let pr = ctx.data_mut().profile.as_mut().expect("profile");
1552                    pr.profiler = Some(prof);
1553                    pr.samples += n;
1554                }
1555                Ok(wasmtime::UpdateDeadline::Continue(1))
1556            });
1557            store.call_hook(|mut ctx, kind| {
1558                if let Some(pr) = ctx.data_mut().profile.as_mut() {
1559                    match kind {
1560                        wasmtime::CallHook::CallingHost => {
1561                            pr.host_depth += 1;
1562                            if pr.host_depth == 1 {
1563                                pr.host_since = Some(Instant::now());
1564                            }
1565                        }
1566                        wasmtime::CallHook::ReturningFromHost if pr.host_depth > 0 => {
1567                            pr.host_depth -= 1;
1568                            if pr.host_depth == 0 {
1569                                pr.host_time += pr.host_since.take().map_or(Duration::ZERO, |t| t.elapsed());
1570                            }
1571                        }
1572                        _ => {}
1573                    }
1574                }
1575                Ok(())
1576            });
1577            start_profile_ticker();
1578        }
1579        store.set_epoch_deadline(deadline);
1580        let exchange = switching.then(|| Exchange::new(deadline));
1581        store.data_mut().run = exchange.clone();
1582        let instance: Instance = if switching {
1583            switching::block_on(linker.instantiate_async(&mut store, &module))??
1584        } else {
1585            linker.instantiate(&mut store, &module)?
1586        };
1587        let memory = match instance.get_memory(&mut store, "memory") {
1588            Some(m) => GuestMemory::Plain(m),
1589            None => threads.as_ref().map(|t| GuestMemory::Shared(t.memory().clone())).ok_or_else(|| format_err!("guest does not export `memory`"))?,
1590        };
1591        store.data_mut().memory = Some(memory);
1592
1593        // a call that must not suspend, on either engine
1594        fn call<R: wasmtime::WasmResults + Send + Sync>(store: &mut Store<Host>, f: TypedFunc<(), R>, switching: bool) -> wasmtime::Result<R> {
1595            if switching { switching::block_on(f.call_async(store, ()))? } else { f.call(store, ()) }
1596        }
1597        if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
1598            store.set_epoch_deadline(deadline);
1599            call(&mut store, init, switching)?;
1600        }
1601        let version = instance.get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?;
1602        let version = call(&mut store, version, switching)?;
1603        if version != ABI_VERSION {
1604            bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
1605        }
1606        store.set_epoch_deadline(deadline);
1607        let init = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?;
1608        let rc = call(&mut store, init, switching)?;
1609        if rc != 0 {
1610            bail!("gasm_init failed with code {rc}");
1611        }
1612        let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
1613        let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
1614        let run = if switching { Some(instance.get_typed_func::<(), i32>(&mut store, "gasm_run")?) } else { None };
1615        Ok(Game { store: Some(store), frame, exit, deadline, run, running: None, exchange, ended: false, gl, watches: Vec::new() })
1616    }
1617
1618    /// Add or replace an asset between frames: the guest sees it from the next
1619    /// frame on, with a new `gasm.asset_version`. Returns that version.
1620    pub fn set_asset(&mut self, name: &str, bytes: Vec<u8>) -> u32 {
1621        self.with_host(|h| h.assets.set(name, bytes))
1622    }
1623
1624    /// Remove an asset between frames. False if there was none.
1625    pub fn remove_asset(&mut self, name: &str) -> bool {
1626        self.with_host(|h| h.assets.remove(name))
1627    }
1628
1629    /// Re-read `path` as asset `name` whenever the file changes (in place or by a
1630    /// rename), checked before each frame.
1631    pub fn watch_asset(&mut self, name: &str, path: &std::path::Path) {
1632        self.add_watch(crate::assets::AssetWatch::new(name, path));
1633    }
1634
1635    /// [`Game::watch_asset`] with a watch made earlier (when the asset was first opened).
1636    pub fn add_watch(&mut self, watch: crate::assets::AssetWatch) {
1637        self.watches.push(watch);
1638    }
1639
1640    /// Whether the guest imports gasm:gl.
1641    pub fn uses_gl(&self) -> bool {
1642        self.gl
1643    }
1644
1645    /// Whether the guest runs through `gasm_run` (the runner switches stacks).
1646    pub fn switching(&self) -> bool {
1647        self.run.is_some()
1648    }
1649
1650    /// One frame: call `gasm_frame`, or resume `gasm_run` until its next `yield_frame`.
1651    pub fn frame(&mut self) -> Result<(), Stop> {
1652        if self.ended {
1653            return Err(Stop::Trap("the guest is not running".into()));
1654        }
1655        // the last frame's saves (gasm:files)
1656        self.with_host(|h| h.files.flush());
1657        if !self.watches.is_empty() {
1658            let mut watches = std::mem::take(&mut self.watches);
1659            self.with_host(|h| watches.iter_mut().for_each(|w| _ = w.poll(&mut h.assets)));
1660            self.watches = watches;
1661        }
1662        let Some(run) = self.run.clone() else {
1663            let store = self.store.as_mut().expect("store");
1664            store.set_epoch_deadline(self.deadline);
1665            let mut r = self.frame.call(&mut *store, ()).map_err(classify);
1666            store.data_mut().frame_index += 1;
1667            // a worker thread that trapped or exited ends the game too (wasi-threads)
1668            if let (Ok(()), Some(end)) = (&r, store.data().threads.as_ref().and_then(|t| t.ended())) {
1669                r = Err(match end {
1670                    crate::threads::ThreadEnd::Exit(c) => Stop::Exit(c),
1671                    crate::threads::ThreadEnd::Trap(t) => Stop::Trap(t),
1672                });
1673            }
1674            self.ended = r.is_err();
1675            return r;
1676        };
1677        match &self.exchange {
1678            Some(ex) if self.running.is_some() => ex.set(Cmd::Resume),
1679            _ => {
1680                // frame 0 starts the run; the call owns the store from now on
1681                let mut store = self.store.take().expect("store");
1682                store.set_epoch_deadline(self.deadline);
1683                self.running = Some(Box::pin(async move {
1684                    let r = run.call_async(&mut store, ()).await;
1685                    (store, r)
1686                }));
1687            }
1688        }
1689        self.poll()
1690    }
1691
1692    /// Poll the running `gasm_run` call: Ok when it suspended again, the reason when it ended.
1693    fn poll(&mut self) -> Result<(), Stop> {
1694        let Some(running) = self.running.as_mut() else { return Ok(()) };
1695        match switching::poll_once(running.as_mut()) {
1696            Poll::Pending => Ok(()),
1697            Poll::Ready((store, r)) => {
1698                self.store = Some(store);
1699                self.running = None;
1700                self.ended = true;
1701                Err(match r {
1702                    Ok(code) => Stop::Exit(code),
1703                    Err(e) => classify(e),
1704                })
1705            }
1706        }
1707    }
1708
1709    /// Best-effort "the player is quitting" notification (optional `gasm_exit` export).
1710    /// A suspended `gasm_run` guest gets it on top of its run, which then ends.
1711    pub fn exit(&mut self) {
1712        if self.store.is_some() || self.running.is_some() {
1713            self.with_host(|h| h.files.flush());
1714        }
1715        if self.running.is_some() {
1716            if let Some(ex) = &self.exchange {
1717                ex.set(Cmd::Exit);
1718            }
1719            if let Err(Stop::Trap(t)) = self.poll() {
1720                eprintln!("[gasm] gasm_exit trapped: {t}");
1721            }
1722            if self.running.is_some() {
1723                // gasm_exit yielded: give up on the run (the store stays with it)
1724                self.running = None;
1725            }
1726            self.exit = None;
1727            return;
1728        }
1729        if let (Some(f), Some(store)) = (self.exit.take(), self.store.as_mut()) {
1730            store.set_epoch_deadline(self.deadline);
1731            let r = if self.run.is_some() { switching::block_on(f.call_async(&mut *store, ())).and_then(|r| r) } else { f.call(&mut *store, ()) };
1732            if let Err(e) = r {
1733                if let Stop::Trap(t) = classify(e) {
1734                    eprintln!("[gasm] gasm_exit trapped: {t}");
1735                }
1736            }
1737            store.data_mut().files.flush(); // saved on the way out
1738        }
1739    }
1740
1741    /// Run `f` on the host: directly, or (a suspended `gasm_run` guest) through
1742    /// the guest's `yield_frame`, which owns the store meanwhile. Runners use this
1743    /// between frames for everything they read or set.
1744    pub fn with_host<R>(&mut self, f: impl FnOnce(&mut Host) -> R) -> R {
1745        if let Some(store) = &mut self.store {
1746            return f(store.data_mut());
1747        }
1748        let (Some(running), Some(ex)) = (self.running.as_mut(), &self.exchange) else {
1749            panic!("with_host: the guest's store is gone (gasm_exit yielded)");
1750        };
1751        let mut f = Some(f);
1752        let mut out = None;
1753        let mut job = |h: &mut Host| out = Some((f.take().expect("job runs once"))(h));
1754        let ptr: *mut (dyn FnMut(&mut Host) + '_) = &mut job;
1755        // SAFETY: the pointer is only used by yield_frame during the poll below,
1756        // while `job` is alive on this stack; it is dropped before we return.
1757        let ptr: *mut (dyn FnMut(&mut Host) + 'static) = unsafe { std::mem::transmute(ptr) };
1758        ex.set(Cmd::Job(Job(ptr)));
1759        let polled = switching::poll_once(running.as_mut());
1760        ex.set(Cmd::None);
1761        match polled {
1762            Poll::Pending => {}
1763            Poll::Ready(_) => unreachable!("yield_frame stays suspended after a job"),
1764        }
1765        out.expect("yield_frame ran the job")
1766    }
1767
1768    /// The host of a guest that isn't suspended in `gasm_run` (every `gasm_frame`
1769    /// guest; a `gasm_run` guest before its first frame and after it ended).
1770    /// Otherwise use [`Game::with_host`].
1771    pub fn host(&self) -> &Host {
1772        self.store.as_ref().expect("Game::host: a gasm_run guest is running; use Game::with_host").data()
1773    }
1774
1775    pub fn host_mut(&mut self) -> &mut Host {
1776        self.store.as_mut().expect("Game::host_mut: a gasm_run guest is running; use Game::with_host").data_mut()
1777    }
1778}
1779
1780#[cfg(test)]
1781mod tests {
1782    use super::*;
1783
1784    #[test]
1785    fn titles_are_cleaned_and_cut() {
1786        assert_eq!(clean_title("A\u{7}B\u{202e}C\u{2069}").as_deref(), Some("ABC"));
1787        assert_eq!(clean_title("\n\t"), None);
1788        // 200 two-byte characters: cut at 256 bytes, on a character boundary
1789        assert_eq!(clean_title(&"é".repeat(200)).map(|t| t.len()), Some(256));
1790    }
1791
1792    const RUN_GUEST: &str = r#"(module
1793      (import "gasm" "yield_frame" (func $yield))
1794      (memory (export "memory") 1)
1795      (global $n (mut i32) (i32.const 0))
1796      (func (export "gasm_abi_version") (result i32) i32.const 0)
1797      (func (export "gasm_init") (result i32) i32.const 0)
1798      (func (export "gasm_frame") call $yield)
1799      (func (export "gasm_exit") (global.set $n (i32.const 100)))
1800      (func (export "frames") (result i32) global.get $n)
1801      (func (export "gasm_run") (result i32)
1802        (loop $l
1803          (global.set $n (i32.add (global.get $n) (i32.const 1)))
1804          call $yield
1805          (br_if $l (i32.lt_u (global.get $n) (i32.const 3))))
1806        i32.const 7))"#;
1807
1808    fn host() -> Host {
1809        Host::new(Assets::new(), HashMap::new(), None, Gfx::null(), Net::new(false), Storage::memory())
1810    }
1811
1812    #[test]
1813    fn gasm_run_guests_suspend_between_frames() {
1814        let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1815        assert!(g.switching());
1816        g.with_host(|h| h.frame_rate = 30.0); // before the first frame: the store is ours
1817        assert!(g.frame().is_ok()); // n = 1, suspended in yield_frame
1818        // suspended: the host is reached through the guest's yield_frame
1819        assert_eq!(g.with_host(|h| h.frame_rate), 30.0);
1820        assert!(g.frame().is_ok()); // n = 2
1821        assert!(g.frame().is_ok()); // n = 3
1822        // the loop ends: gasm_run returns 7, the exit code
1823        assert!(matches!(g.frame(), Err(Stop::Exit(7))));
1824        assert!(g.frame().is_err(), "never called again");
1825    }
1826
1827    #[test]
1828    fn exit_runs_on_top_of_a_suspended_run() {
1829        let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1830        g.frame().unwrap();
1831        g.exit();
1832        // the store is back after the run ended; gasm_exit ran (n = 100)
1833        assert!(g.store.is_some());
1834    }
1835
1836    #[test]
1837    fn yield_frame_outside_gasm_run_traps() {
1838        let opts = LoadOptions { stack_switching: false, ..LoadOptions::default() };
1839        let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), opts).unwrap();
1840        assert!(!g.switching());
1841        match g.frame() {
1842            Err(Stop::Trap(t)) => assert!(t.contains("only inside gasm_run"), "{t}"),
1843            other => panic!("expected a trap, got {:?}", other.err()),
1844        }
1845    }
1846
1847    #[test]
1848    fn custom_sections_are_found() {
1849        // magic + version, a type section (id 1, empty), then custom "gasm.title" = "Hi\0"
1850        let mut wasm = b"\0asm\x01\0\0\0\x01\x01\0".to_vec();
1851        wasm.extend_from_slice(&[0, 14, 10]);
1852        wasm.extend_from_slice(b"gasm.titleHi\0");
1853        assert_eq!(custom_section(&wasm, "gasm.title"), Some(&b"Hi\0"[..]));
1854        assert_eq!(static_title(&wasm).as_deref(), Some("Hi"));
1855        assert_eq!(custom_section(&wasm, "other"), None);
1856        assert_eq!(custom_section(&wasm[..wasm.len() - 2], "gasm.title"), None); // truncated
1857        assert_eq!(custom_section(b"not wasm", "gasm.title"), None);
1858    }
1859}