Skip to main content

gasm_host/
host.rs

1//! The gasm ABI v0 host: instantiates a guest and implements the "gasm",
2//! "gasm:gfx" and "gasm:net" imports.
3
4use std::collections::HashMap;
5use std::time::Instant;
6
7use wasmtime::{Caller, Engine, Instance, Linker, Memory, Module, Store, TypedFunc, bail, format_err};
8use wasmtime_wasi::WasiCtx;
9use wasmtime_wasi::p1::{self, WasiP1Ctx};
10
11use crate::assets::Assets;
12use crate::audio::AudioSink;
13use crate::gfx::Gfx;
14use crate::net::Net;
15use crate::storage::Storage;
16
17pub const ABI_VERSION: i32 = 0;
18
19/// FNV-1a 32-bit; mirrored in runners/web/gasm-host.js for cross-runner checks.
20#[derive(Clone, Copy)]
21pub struct Fnv(pub u32);
22
23impl Fnv {
24    pub fn new() -> Self {
25        Fnv(0x811c_9dc5)
26    }
27    pub fn update(&mut self, bytes: &[u8]) {
28        let mut h = self.0;
29        for &b in bytes {
30            h ^= b as u32;
31            h = h.wrapping_mul(0x0100_0193);
32        }
33        self.0 = h;
34    }
35}
36
37/// Raw input for one frame, set by the runner before each `gasm_frame`.
38/// `None` fields mean the runner has no such device (the import returns -1).
39#[derive(Default)]
40pub struct RawInput {
41    /// held keys, bit k = GASM_KEY code k
42    pub keys: Option<[u8; KEY_STATE_BYTES]>,
43    /// (code, down) since the previous frame, in order
44    pub key_events: Vec<(u16, bool)>,
45    pub pointer: Option<Pointer>,
46    pub gamepads: Option<[Gamepad; 4]>,
47}
48
49pub const KEY_STATE_BYTES: usize = 32;
50pub const POINTER_BYTES: usize = 48;
51pub const GAMEPAD_BYTES: usize = 204;
52pub const GAMEPAD_BUTTONS: usize = 32;
53pub const GAMEPAD_AXES: usize = 16;
54
55#[derive(Default, Clone, Copy)]
56pub struct Pointer {
57    /// position in drawable pixels; the frame position is derived by the host
58    pub x: f32,
59    pub y: f32,
60    pub dx: f32,
61    pub dy: f32,
62    pub wheel_x: f32,
63    pub wheel_y: f32,
64    pub buttons: u32,
65    pub pressed: u32,
66    pub released: u32,
67    /// GASM_POINTER_INSIDE | IS_HIDDEN | IS_LOCKED
68    pub flags: u32,
69    /// drawable size the position refers to
70    pub drawable: (f32, f32),
71}
72
73#[derive(Default, Clone)]
74pub struct Gamepad {
75    pub connected: bool,
76    pub standard: bool,
77    pub buttons: Vec<f32>,
78    pub axes: Vec<f32>,
79    pub name: String,
80}
81
82/// Map a drawable position into the last video_present frame (letterboxed as the
83/// runners display it). Same arithmetic in runners/web/gasm-host.js.
84pub fn frame_position(x: f32, y: f32, drawable: (f32, f32), frame: (usize, usize)) -> (f32, f32) {
85    if frame.0 == 0 || frame.1 == 0 || drawable.0 <= 0.0 || drawable.1 <= 0.0 {
86        return (x, y);
87    }
88    let (dw, dh, fw, fh) = (drawable.0 as f64, drawable.1 as f64, frame.0 as f64, frame.1 as f64);
89    let scale = (dw / fw).min(dh / fh);
90    let (ox, oy) = ((dw - fw * scale) / 2.0, (dh - fh * scale) / 2.0);
91    (((x as f64 - ox) / scale) as f32, ((y as f64 - oy) / scale) as f32)
92}
93
94pub struct Host {
95    wasi: WasiP1Ctx,
96    memory: Option<Memory>,
97    start: Instant,
98    /// Headless runs use frame-derived time so results are reproducible.
99    pub virtual_time_ms: Option<f64>,
100    pub frame_rate: f64,
101    pub pads: [u32; 4],
102    /// UTF-8 typed since the previous frame (set by the runner before each frame);
103    /// None: no keyboard, text_input returns -1
104    pub text: Option<String>,
105    pub input: RawInput,
106    /// GASM_INPUT_* flags requested by the guest (input_mode)
107    pub input_mode: u32,
108    pub assets: Assets,
109    /// sorted asset names, computed on first asset_count/asset_name
110    asset_names: Option<Vec<String>>,
111    pub params: HashMap<String, String>,
112    pub audio: Option<AudioSink>,
113    pub gfx: Gfx,
114    pub net: Net,
115    pub storage: Storage,
116    /// false during catch-up frames: gfx begin_frame returns 0
117    pub show_frame: bool,
118    audio_rate: u32,
119    audio_channels: u32,
120    /// Last presented frame as RGBA8, tightly packed.
121    pub rgba: Vec<u8>,
122    pub width: usize,
123    pub height: usize,
124    pub frames_presented: u64,
125    pub hashing: bool,
126    pub video_hash: Fnv,
127    pub audio_hash: Fnv,
128    pub audio_frames: u64,
129}
130
131impl Host {
132    pub fn new(
133        assets: impl Into<Assets>,
134        params: HashMap<String, String>,
135        audio: Option<AudioSink>,
136        gfx: Gfx,
137        net: Net,
138        storage: Storage,
139    ) -> Self {
140        let wasi = WasiCtx::builder().inherit_stdout().inherit_stderr().build_p1();
141        Host {
142            wasi,
143            memory: None,
144            start: Instant::now(),
145            virtual_time_ms: None,
146            frame_rate: 60.0,
147            pads: [0; 4],
148            text: None,
149            input: RawInput::default(),
150            input_mode: 0,
151            assets: assets.into(),
152            asset_names: None,
153            params,
154            audio,
155            gfx,
156            net,
157            storage,
158            show_frame: true,
159            audio_rate: 44100,
160            audio_channels: 2,
161            rgba: Vec::new(),
162            width: 0,
163            height: 0,
164            frames_presented: 0,
165            hashing: true,
166            video_hash: Fnv::new(),
167            audio_hash: Fnv::new(),
168            audio_frames: 0,
169        }
170    }
171}
172
173/// Borrow `len` bytes at guest offset `ptr`, trapping the guest if out of bounds.
174fn guest_slice<'a>(mem: &'a [u8], ptr: u32, len: u64) -> wasmtime::Result<&'a [u8]> {
175    let start = ptr as u64;
176    let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
177    if end > mem.len() as u64 {
178        bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
179    }
180    Ok(&mem[start as usize..end as usize])
181}
182
183fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<Memory> {
184    caller.data().memory.ok_or_else(|| format_err!("guest memory not yet available"))
185}
186
187fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
188    let mem = memory(caller)?;
189    let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
190    Ok(String::from_utf8_lossy(bytes).into_owned())
191}
192
193fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
194    linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| {
195        let msg = guest_str(&caller, ptr, len)?;
196        eprintln!("[guest] {msg}");
197        Ok(())
198    })?;
199
200    linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
201        let h = caller.data();
202        h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
203    })?;
204
205    linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
206        if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
207            caller.data_mut().frame_rate = hz;
208        }
209    })?;
210
211    linker.func_wrap(
212        "gasm",
213        "video_present",
214        |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
215            let (w, h, stride) = (w as usize, h as usize, stride as usize);
216            if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
217                bail!("video_present: bad geometry {w}x{h} stride {stride}");
218            }
219            let mem = memory(&caller)?;
220            let (data, host) = mem.data_and_store_mut(&mut caller);
221            let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
222            host.rgba.resize(w * h * 4, 0);
223            for y in 0..h {
224                let row = &src[y * stride..y * stride + w * 4];
225                host.rgba[y * w * 4..(y + 1) * w * 4].copy_from_slice(row);
226            }
227            if host.hashing {
228                host.video_hash.update(&host.rgba);
229            }
230            host.width = w;
231            host.height = h;
232            host.frames_presented += 1;
233            Ok(())
234        },
235    )?;
236
237    linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
238        let h = caller.data_mut();
239        if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
240            h.audio_rate = rate;
241            h.audio_channels = channels;
242            if let Some(a) = &mut h.audio {
243                a.configure(rate, channels);
244            }
245        }
246    })?;
247
248    linker.func_wrap(
249        "gasm",
250        "audio_push",
251        |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
252            let mem = memory(&caller)?;
253            let (data, host) = mem.data_and_store_mut(&mut caller);
254            let len = frames as u64 * host.audio_channels as u64 * 4;
255            let bytes = guest_slice(data, ptr, len)?;
256            if host.hashing {
257                host.audio_hash.update(bytes);
258            }
259            host.audio_frames += frames as u64;
260            if let Some(a) = &mut host.audio {
261                a.push_le_f32(bytes);
262            }
263            Ok(())
264        },
265    )?;
266
267    linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
268        caller.data().pads.get(player as usize).copied().unwrap_or(0)
269    })?;
270
271    linker.func_wrap(
272        "gasm",
273        "text_input",
274        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
275            let mem = memory(&caller)?;
276            let (data, host) = mem.data_and_store_mut(&mut caller);
277            let Some(t) = &host.text else { return Ok(-1) };
278            let t = t.as_bytes();
279            if t.len() <= cap as usize {
280                guest_slice(data, dst, t.len() as u64)?;
281                data[dst as usize..dst as usize + t.len()].copy_from_slice(t);
282            }
283            Ok(t.len() as i32)
284        },
285    )?;
286
287    linker.func_wrap("gasm", "input_mode", |mut caller: Caller<'_, Host>, flags: u32| {
288        caller.data_mut().input_mode = flags & 7;
289    })?;
290
291    linker.func_wrap(
292        "gasm",
293        "key_state",
294        |mut caller: Caller<'_, Host>, dst: u32, len: u32| -> wasmtime::Result<i32> {
295            let mem = memory(&caller)?;
296            let (data, host) = mem.data_and_store_mut(&mut caller);
297            let Some(keys) = host.input.keys else { return Ok(-1) };
298            let n = (len as usize).min(KEY_STATE_BYTES);
299            guest_slice(data, dst, n as u64)?;
300            data[dst as usize..dst as usize + n].copy_from_slice(&keys[..n]);
301            Ok(KEY_STATE_BYTES as i32)
302        },
303    )?;
304
305    linker.func_wrap(
306        "gasm",
307        "key_events",
308        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
309            let mem = memory(&caller)?;
310            let (data, host) = mem.data_and_store_mut(&mut caller);
311            if host.input.keys.is_none() {
312                return Ok(-1);
313            }
314            let bytes: Vec<u8> = host.input.key_events.iter().flat_map(|&(c, d)| { let c = c.to_le_bytes(); [c[0], c[1], d as u8, 0] }).collect();
315            if bytes.len() <= cap as usize {
316                guest_slice(data, dst, bytes.len() as u64)?;
317                data[dst as usize..dst as usize + bytes.len()].copy_from_slice(&bytes);
318            }
319            Ok(bytes.len() as i32)
320        },
321    )?;
322
323    linker.func_wrap(
324        "gasm",
325        "pointer",
326        |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
327            let mem = memory(&caller)?;
328            let (data, host) = mem.data_and_store_mut(&mut caller);
329            let Some(p) = host.input.pointer else { return Ok(-1) };
330            if cap as usize >= POINTER_BYTES {
331                let (fx, fy) = frame_position(p.x, p.y, p.drawable, (host.width, host.height));
332                let mut b = Vec::with_capacity(POINTER_BYTES);
333                for v in [p.x, p.y, fx, fy, p.dx, p.dy, p.wheel_x, p.wheel_y] {
334                    b.extend_from_slice(&v.to_le_bytes());
335                }
336                for v in [p.buttons, p.pressed, p.released, p.flags] {
337                    b.extend_from_slice(&v.to_le_bytes());
338                }
339                guest_slice(data, dst, POINTER_BYTES as u64)?;
340                data[dst as usize..dst as usize + POINTER_BYTES].copy_from_slice(&b);
341            }
342            Ok(POINTER_BYTES as i32)
343        },
344    )?;
345
346    linker.func_wrap(
347        "gasm",
348        "gamepad",
349        |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
350            let mem = memory(&caller)?;
351            let (data, host) = mem.data_and_store_mut(&mut caller);
352            let (Some(pads), true) = (&host.input.gamepads, slot < 4) else { return Ok(-1) };
353            if cap as usize >= GAMEPAD_BYTES {
354                let g = &pads[slot as usize];
355                let nb = g.buttons.len().min(GAMEPAD_BUTTONS);
356                let na = g.axes.len().min(GAMEPAD_AXES);
357                let flags = if g.connected { 1 | if g.standard { 2 } else { 0 } } else { 0u32 };
358                let mut b = Vec::with_capacity(GAMEPAD_BYTES);
359                for v in [flags, nb as u32, na as u32] {
360                    b.extend_from_slice(&v.to_le_bytes());
361                }
362                for i in 0..GAMEPAD_BUTTONS {
363                    b.extend_from_slice(&g.buttons.get(i).copied().unwrap_or(0.0).to_le_bytes());
364                }
365                for i in 0..GAMEPAD_AXES {
366                    b.extend_from_slice(&g.axes.get(i).copied().unwrap_or(0.0).to_le_bytes());
367                }
368                guest_slice(data, dst, GAMEPAD_BYTES as u64)?;
369                data[dst as usize..dst as usize + GAMEPAD_BYTES].copy_from_slice(&b);
370            }
371            Ok(GAMEPAD_BYTES as i32)
372        },
373    )?;
374
375    linker.func_wrap(
376        "gasm",
377        "gamepad_name",
378        |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
379            let mem = memory(&caller)?;
380            let (data, host) = mem.data_and_store_mut(&mut caller);
381            let Some(g) = host.input.gamepads.as_ref().and_then(|p| p.get(slot as usize)).filter(|g| g.connected) else { return Ok(-1) };
382            let n = g.name.as_bytes();
383            if n.len() <= cap as usize {
384                guest_slice(data, dst, n.len() as u64)?;
385                data[dst as usize..dst as usize + n.len()].copy_from_slice(n);
386            }
387            Ok(n.len() as i32)
388        },
389    )?;
390
391    linker.func_wrap(
392        "gasm",
393        "param",
394        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
395            let name = guest_str(&caller, ptr, len)?;
396            let mem = memory(&caller)?;
397            let (data, host) = mem.data_and_store_mut(&mut caller);
398            let Some(v) = host.params.get(&name) else { return Ok(-1) };
399            let v = v.as_bytes();
400            if v.len() <= cap as usize {
401                guest_slice(data, dst, v.len() as u64)?;
402                data[dst as usize..dst as usize + v.len()].copy_from_slice(v);
403            }
404            Ok(v.len() as i32)
405        },
406    )?;
407
408    linker.func_wrap(
409        "gasm",
410        "asset_read_at",
411        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
412            let name = guest_str(&caller, ptr, len)?;
413            let mem = memory(&caller)?;
414            let (data, host) = mem.data_and_store_mut(&mut caller);
415            let Some(size) = host.assets.size(&name) else { return Ok(-1) };
416            let n = size.saturating_sub(offset as u64).min(cap as u64);
417            guest_slice(data, dst, n)?; // bounds check before touching guest memory
418            let dst = &mut data[dst as usize..dst as usize + n as usize];
419            Ok(host.assets.read_at(&name, offset as u64, dst).map_or(-1, |k| k as i32))
420        },
421    )?;
422
423    linker.func_wrap("gasm", "asset_count", |mut caller: Caller<'_, Host>| -> u32 {
424        let h = caller.data_mut();
425        h.asset_names.get_or_insert_with(|| h.assets.names()).len() as u32
426    })?;
427
428    linker.func_wrap(
429        "gasm",
430        "asset_name",
431        |mut caller: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
432            let mem = memory(&caller)?;
433            let (data, host) = mem.data_and_store_mut(&mut caller);
434            let names = host.asset_names.get_or_insert_with(|| host.assets.names());
435            let Some(n) = names.get(index as usize) else { return Ok(-1) };
436            let n = n.as_bytes();
437            if n.len() <= cap as usize {
438                guest_slice(data, dst, n.len() as u64)?;
439                data[dst as usize..dst as usize + n.len()].copy_from_slice(n);
440            }
441            Ok(n.len() as i32)
442        },
443    )?;
444
445    linker.func_wrap(
446        "gasm",
447        "asset_size",
448        |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
449            let name = guest_str(&caller, ptr, len)?;
450            Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i32))
451        },
452    )?;
453
454    linker.func_wrap(
455        "gasm",
456        "asset_read",
457        |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
458            let name = guest_str(&caller, ptr, len)?;
459            let mem = memory(&caller)?;
460            let (data, host) = mem.data_and_store_mut(&mut caller);
461            let Some(size) = host.assets.size(&name) else { return Ok(-1) };
462            let n = size.min(cap as u64);
463            guest_slice(data, dst, n)?;
464            let dst = &mut data[dst as usize..dst as usize + n as usize];
465            Ok(host.assets.read_at(&name, 0, dst).map_or(-1, |k| k as i32))
466        },
467    )?;
468    Ok(())
469}
470
471/// Ahead-of-time compile a guest to native code for this host (no JIT needed at load).
472pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
473    Engine::default().precompile_module(wasm)
474}
475
476fn trap(e: String) -> wasmtime::Error {
477    format_err!("{e}")
478}
479
480fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
481    const M: &str = "gasm:gfx";
482    linker.func_wrap(M, "width", |c: Caller<'_, Host>| c.data().gfx.size().0)?;
483    linker.func_wrap(M, "height", |c: Caller<'_, Host>| c.data().gfx.size().1)?;
484    linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
485        let src = guest_str(&c, ptr, len)?;
486        c.data_mut().gfx.create_shader(&src).map_err(trap)
487    })?;
488    linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
489        c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
490    })?;
491    linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
492        let json = guest_str(&c, ptr, len)?;
493        c.data_mut().gfx.create_pipeline(&json).map_err(trap)
494    })?;
495    linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
496        let json = guest_str(&c, ptr, len)?;
497        c.data_mut().gfx.create_bind_group(&json).map_err(trap)
498    })?;
499    linker.func_wrap(
500        M,
501        "write_buffer",
502        |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
503            let mem = memory(&c)?;
504            let (data, host) = mem.data_and_store_mut(&mut c);
505            let bytes = guest_slice(data, ptr, len as u64)?;
506            if host.hashing {
507                host.video_hash.update(bytes);
508            }
509            host.gfx.write_buffer(buf, offset, bytes).map_err(trap)
510        },
511    )?;
512    linker.func_wrap(M, "create_bind_group_layout", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
513        let json = guest_str(&c, ptr, len)?;
514        c.data_mut().gfx.create_bind_group_layout(&json).map_err(trap)
515    })?;
516    linker.func_wrap(M, "create_texture", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
517        let json = guest_str(&c, ptr, len)?;
518        c.data_mut().gfx.create_texture(&json).map_err(trap)
519    })?;
520    linker.func_wrap(M, "create_sampler", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
521        let json = guest_str(&c, ptr, len)?;
522        c.data_mut().gfx.create_sampler(&json).map_err(trap)
523    })?;
524    linker.func_wrap(
525        M,
526        "write_texture",
527        |mut c: Caller<'_, Host>, tex: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
528            let mem = memory(&c)?;
529            let (data, host) = mem.data_and_store_mut(&mut c);
530            let bytes = guest_slice(data, ptr, len as u64)?;
531            host.gfx.write_texture(tex, mip, x, y, w, h, bytes).map_err(trap)?;
532            if host.hashing {
533                // header (little-endian u32s) + payload, so the target region counts too
534                for v in [tex, mip, x, y, w, h] {
535                    host.video_hash.update(&v.to_le_bytes());
536                }
537                host.video_hash.update(bytes);
538            }
539            Ok(())
540        },
541    )?;
542    linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
543        let h = c.data_mut();
544        let show = h.show_frame;
545        h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
546    })?;
547    linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
548        c.data_mut().gfx.set_pipeline(p).map_err(trap)
549    })?;
550    linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
551        c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
552    })?;
553    linker.func_wrap(
554        M,
555        "set_bind_group_offsets",
556        |mut c: Caller<'_, Host>, i: u32, bg: u32, ptr: u32, count: u32| -> wasmtime::Result<()> {
557            let mem = memory(&c)?;
558            let (data, host) = mem.data_and_store_mut(&mut c);
559            let bytes = guest_slice(data, ptr, count as u64 * 4)?;
560            let offsets: Vec<u32> = bytes.chunks_exact(4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
561            host.gfx.set_bind_group_offsets(i, bg, &offsets).map_err(trap)
562        },
563    )?;
564    linker.func_wrap(
565        M,
566        "set_viewport",
567        |mut c: Caller<'_, Host>, x: f32, y: f32, w: f32, h: f32, min: f32, max: f32| -> wasmtime::Result<()> {
568            c.data_mut().gfx.set_viewport(x, y, w, h, min, max).map_err(trap)
569        },
570    )?;
571    linker.func_wrap(M, "set_scissor_rect", |mut c: Caller<'_, Host>, x: u32, y: u32, w: u32, h: u32| -> wasmtime::Result<()> {
572        c.data_mut().gfx.set_scissor_rect(x, y, w, h).map_err(trap)
573    })?;
574    linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
575        c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
576    })?;
577    linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
578        c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
579    })?;
580    linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| {
581        c.data_mut().gfx.draw(vc, ic, fv, fi)
582    })?;
583    linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| {
584        c.data_mut().gfx.draw_indexed(ic, n, first, base, fi)
585    })?;
586    linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
587        let h = c.data_mut();
588        h.frames_presented += 1;
589        h.gfx.end_frame().map_err(trap)
590    })?;
591    Ok(())
592}
593
594fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
595    const M: &str = "gasm:net";
596    linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
597        let url = guest_str(&c, ptr, len)?;
598        Ok(c.data_mut().net.open(&url))
599    })?;
600    linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| c.data().net.state(h))?;
601    linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
602        if len == 0 {
603            return Ok(-1);
604        }
605        let mem = memory(&c)?;
606        let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
607        Ok(c.data_mut().net.send(h, msg))
608    })?;
609    linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
610        let mem = memory(&c)?;
611        let (data, host) = mem.data_and_store_mut(&mut c);
612        let msg = match host.net.peek(h) {
613            Ok(m) => m,
614            Err(code) => return Ok(code),
615        };
616        let n = msg.len();
617        if n <= cap as usize {
618            guest_slice(data, dst, n as u64)?;
619            data[dst as usize..dst as usize + n].copy_from_slice(msg);
620            host.net.pop(h);
621        }
622        Ok(n as i32)
623    })?;
624    linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| c.data_mut().net.close(h))?;
625    Ok(())
626}
627
628fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
629    const M: &str = "gasm:storage";
630    linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
631        let key = guest_str(&c, kp, kl)?;
632        let mem = memory(&c)?;
633        let (data, host) = mem.data_and_store_mut(&mut c);
634        let Some(v) = host.storage.get(&key) else { return Ok(-1) };
635        if v.len() <= cap as usize {
636            guest_slice(data, dst, v.len() as u64)?;
637            data[dst as usize..dst as usize + v.len()].copy_from_slice(v);
638        }
639        Ok(v.len() as i32)
640    })?;
641    linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
642        let key = guest_str(&c, kp, kl)?;
643        let mem = memory(&c)?;
644        let (data, host) = mem.data_and_store_mut(&mut c);
645        let value = guest_slice(data, vp, vl as u64)?;
646        Ok(match host.storage.set(&key, value) {
647            Ok(()) => 0,
648            Err(e) => {
649                eprintln!("[gasm] storage: {e}");
650                -1
651            }
652        })
653    })?;
654    linker.func_wrap(M, "count", |c: Caller<'_, Host>| c.data().storage.keys().len() as u32)?;
655    linker.func_wrap(M, "key", |mut c: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
656        let mem = memory(&c)?;
657        let (data, host) = mem.data_and_store_mut(&mut c);
658        let keys = host.storage.keys();
659        let Some(k) = keys.get(index as usize) else { return Ok(-1) };
660        let k = k.as_bytes();
661        if k.len() <= cap as usize {
662            guest_slice(data, dst, k.len() as u64)?;
663            data[dst as usize..dst as usize + k.len()].copy_from_slice(k);
664        }
665        Ok(k.len() as i32)
666    })?;
667    linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
668        let key = guest_str(&c, kp, kl)?;
669        Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
670    })?;
671    Ok(())
672}
673
674/// Why a guest call ended.
675#[derive(Debug)]
676pub enum Stop {
677    /// The guest called proc_exit(code).
678    Exit(i32),
679    Trap(String),
680}
681
682impl std::fmt::Display for Stop {
683    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
684        match self {
685            Stop::Exit(code) => write!(f, "guest exited with code {code}"),
686            Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
687        }
688    }
689}
690
691impl std::error::Error for Stop {}
692
693fn classify(e: wasmtime::Error) -> Stop {
694    match e.downcast_ref::<wasmtime_wasi::I32Exit>() {
695        Some(exit) => Stop::Exit(exit.0),
696        None => Stop::Trap(format!("{e:?}")),
697    }
698}
699
700pub struct Game {
701    pub store: Store<Host>,
702    frame: TypedFunc<(), ()>,
703    exit: Option<TypedFunc<(), ()>>,
704}
705
706impl Game {
707    pub fn load(wasm: &[u8], host: Host) -> Result<Game, Stop> {
708        Self::load_inner(wasm, host).map_err(classify)
709    }
710
711    fn load_inner(wasm: &[u8], host: Host) -> wasmtime::Result<Game> {
712        let engine = Engine::default();
713        let module = if wasm.starts_with(b"\0asm") {
714            Module::new(&engine, wasm)?
715        } else {
716            // Precompiled artifact from `gasm-run --compile`. Only load files you
717            // produced yourself: native code is trusted, unlike .wasm.
718            unsafe { Module::deserialize(&engine, wasm)? }
719        };
720        let mut linker: Linker<Host> = Linker::new(&engine);
721        p1::add_to_linker_sync(&mut linker, |h: &mut Host| &mut h.wasi)?;
722        add_gasm_imports(&mut linker)?;
723        add_gfx_imports(&mut linker)?;
724        add_net_imports(&mut linker)?;
725        add_storage_imports(&mut linker)?;
726        // Imports this runner doesn't know (e.g. JS glue some Rust crates pull in)
727        // link as traps: harmless unless the guest actually calls them.
728        linker.define_unknown_imports_as_traps(&module)?;
729
730        let mut store = Store::new(&engine, host);
731        let instance: Instance = linker.instantiate(&mut store, &module)?;
732        let memory = instance
733            .get_memory(&mut store, "memory")
734            .ok_or_else(|| format_err!("guest does not export `memory`"))?;
735        store.data_mut().memory = Some(memory);
736
737        if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
738            init.call(&mut store, ())?;
739        }
740        let version = instance
741            .get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?
742            .call(&mut store, ())?;
743        if version != ABI_VERSION {
744            bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
745        }
746        let rc = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?.call(&mut store, ())?;
747        if rc != 0 {
748            bail!("gasm_init failed with code {rc}");
749        }
750        let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
751        let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
752        Ok(Game { store, frame, exit })
753    }
754
755    pub fn frame(&mut self) -> Result<(), Stop> {
756        self.frame.call(&mut self.store, ()).map_err(classify)
757    }
758
759    /// Best-effort "the player is quitting" notification (optional `gasm_exit` export).
760    pub fn exit(&mut self) {
761        if let Some(f) = self.exit.take() {
762            if let Err(e) = f.call(&mut self.store, ()) {
763                if let Stop::Trap(t) = classify(e) {
764                    eprintln!("[gasm] gasm_exit trapped: {t}");
765                }
766            }
767        }
768    }
769
770    pub fn host(&self) -> &Host {
771        self.store.data()
772    }
773
774    pub fn host_mut(&mut self) -> &mut Host {
775        self.store.data_mut()
776    }
777}