1use 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#[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#[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 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#[derive(Default, Clone)]
80pub struct RawInput {
81 pub keys: Option<[u8; KEY_STATE_BYTES]>,
83 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 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 pub flags: u32,
109 pub drawable: (f32, f32),
111 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
124pub 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
134pub const TITLE_MAX_BYTES: usize = 256;
136
137#[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 let bias = match unsafe { GetTimeZoneInformation(&mut tz) } {
154 1 => tz.Bias + tz.StandardBias, 2 => tz.Bias + tz.DaylightBias, 0 => tz.Bias, _ => 0,
158 };
159 -bias
160}
161
162#[cfg(not(any(unix, windows)))]
163fn utc_offset_minutes() -> i32 {
164 0
165}
166
167pub 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
182pub 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
221pub fn static_title(wasm: &[u8]) -> Option<String> {
224 clean_title(std::str::from_utf8(custom_section(wasm, "gasm.title")?).ok()?)
225}
226
227pub 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
243pub 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
248pub const DEFAULT_MEMORY_LIMIT: usize = 1 << 30;
250
251pub struct Host {
252 memory: Option<Memory>,
253 start: Instant,
254 pub virtual_time_ms: Option<f64>,
257 pub random: Option<Splitmix>,
259 pub frame_rate: f64,
260 pub pads: [u32; 4],
261 pub text: Option<String>,
264 pub input: RawInput,
265 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 pub gl: crate::gl::Gl,
273 pub frame_index: u64,
275 pub net: Net,
276 pub fetch: crate::fetch::Fetch,
278 pub clipboard: Clipboard,
280 pub files: crate::files::Files,
282 pub storage: Storage,
283 pub show_frame: bool,
285 audio_rate: u32,
286 audio_channels: u32,
287 provided: HashSet<String>,
289 pub rgba: Vec<u8>,
291 pub width: usize,
292 pub height: usize,
293 pub frames_presented: u64,
294 pub aspect: Option<(u32, u32)>,
296 pub(crate) run: Option<Arc<Exchange>>,
298 pub title: Option<String>,
300 pub title_changed: bool,
302 pub hashing: bool,
303 pub video_hash: Fnv,
304 pub audio_hash: Fnv,
305 pub audio_frames: u64,
306 pub memory_limit: MemoryLimit,
308 pub catch_up: bool,
310}
311
312impl Host {
313 pub fn new(
314 assets: impl Into<Assets>,
315 params: HashMap<String, String>,
316 audio: Option<Box<dyn AudioOut>>,
317 gfx: Gfx,
318 net: Net,
319 storage: Storage,
320 ) -> Self {
321 Host {
322 memory: None,
323 start: Instant::now(),
324 virtual_time_ms: None,
325 random: None,
326 frame_rate: 60.0,
327 pads: [0; 4],
328 text: None,
329 input: RawInput::default(),
330 input_mode: 0,
331 assets: assets.into(),
332 params,
333 audio,
334 gfx,
335 gl: Default::default(),
336 fetch: Default::default(),
337 clipboard: Default::default(),
338 files: Default::default(),
339 memory_limit: MemoryLimit(Some(DEFAULT_MEMORY_LIMIT)),
340 catch_up: false,
341 frame_index: 0,
342 net,
343 storage,
344 show_frame: true,
345 audio_rate: 44100,
346 audio_channels: 2,
347 provided: HashSet::new(),
348 title: None,
349 title_changed: false,
350 aspect: None,
351 run: None,
352 rgba: Vec::new(),
353 width: 0,
354 height: 0,
355 frames_presented: 0,
356 hashing: true,
357 video_hash: Fnv::new(),
358 audio_hash: Fnv::new(),
359 audio_frames: 0,
360 }
361 }
362
363 pub fn set_reproducible(&mut self) {
366 self.virtual_time_ms = Some(0.0);
367 self.random = Some(Splitmix(0));
368 }
369
370 pub(crate) fn started(&self) -> Instant {
371 self.start
372 }
373}
374
375pub fn imports_module(wasm: &[u8], module: &str, allow_precompiled: bool) -> bool {
378 if !wasm.starts_with(b"\0asm") {
379 if !allow_precompiled {
380 return false;
381 }
382 return unsafe { Module::deserialize(engine(), wasm) }.is_ok_and(|m| m.imports().any(|i| i.module() == module));
383 }
384 fn leb(b: &[u8], p: &mut usize) -> Option<u32> {
385 let (mut v, mut shift) = (0u32, 0);
386 loop {
387 let byte = *b.get(*p)?;
388 *p += 1;
389 v |= ((byte & 0x7f) as u32) << shift;
390 if byte & 0x80 == 0 {
391 return Some(v);
392 }
393 shift += 7;
394 if shift > 28 {
395 return None;
396 }
397 }
398 }
399 fn name<'a>(b: &'a [u8], p: &mut usize) -> Option<&'a [u8]> {
400 let n = leb(b, p)? as usize;
401 let s = b.get(*p..*p + n)?;
402 *p += n;
403 Some(s)
404 }
405 let scan = || -> Option<bool> {
406 let mut p = 8;
407 while p < wasm.len() {
408 let id = wasm[p];
409 p += 1;
410 let size = leb(wasm, &mut p)? as usize;
411 let end = p.checked_add(size)?;
412 if id == 2 {
413 let mut q = p;
414 for _ in 0..leb(wasm, &mut q)? {
415 if name(wasm, &mut q)? == module.as_bytes() {
416 return Some(true);
417 }
418 name(wasm, &mut q)?;
419 match *wasm.get(q)? {
420 0 => { q += 1; leb(wasm, &mut q)?; } 1 => { q += 2; let f = wasm[q - 1]; leb(wasm, &mut q)?; if f & 1 != 0 { leb(wasm, &mut q)?; } } 2 => { q += 1; let f = *wasm.get(q)?; q += 1; leb(wasm, &mut q)?; if f & 1 != 0 { leb(wasm, &mut q)?; } } 3 => q += 3, 4 => { q += 2; leb(wasm, &mut q)?; } _ => return None,
426 }
427 }
428 return Some(false);
429 }
430 p = end;
431 }
432 Some(false)
433 };
434 scan().unwrap_or(false)
435}
436
437pub(crate) fn guest_slice(mem: &[u8], ptr: u32, len: u64) -> wasmtime::Result<&[u8]> {
439 let start = ptr as u64;
440 let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
441 if end > mem.len() as u64 {
442 bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
443 }
444 Ok(&mem[start as usize..end as usize])
445}
446
447pub(crate) fn guest_slice_mut(mem: &mut [u8], ptr: u32, len: u64) -> wasmtime::Result<&mut [u8]> {
449 let start = ptr as u64;
450 let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
451 if end > mem.len() as u64 {
452 bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
453 }
454 Ok(&mut mem[start as usize..end as usize])
455}
456
457#[derive(Default)]
463pub struct Clipboard {
464 pub pasted: Option<String>,
465 pub copied: Option<String>,
466}
467
468pub const CLIPBOARD_MAX: usize = 1 << 20;
470
471fn copy_if_fits(mem: &mut [u8], dst: u32, cap: u32, src: &[u8]) -> wasmtime::Result<i32> {
472 if src.len() <= cap as usize {
473 guest_slice_mut(mem, dst, src.len() as u64)?.copy_from_slice(src);
474 }
475 Ok(src.len() as i32)
476}
477
478pub(crate) fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<Memory> {
479 caller.data().memory.ok_or_else(|| format_err!("guest memory not yet available"))
480}
481
482fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
483 let mem = memory(caller)?;
484 let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
485 std::str::from_utf8(bytes).map(str::to_owned).map_err(|e| format_err!("string argument is not UTF-8: {e}"))
486}
487
488fn pointer_bytes(p: &Pointer, frame: (usize, usize), aspect: Option<(u32, u32)>) -> [u8; POINTER_BYTES] {
490 let (fx, fy) = frame_position(p.x, p.y, p.drawable, frame, p.integer_scale, aspect);
491 let mut b = [0u8; POINTER_BYTES];
492 for (i, v) in [p.x, p.y, fx, fy, p.dx, p.dy, p.wheel_x, p.wheel_y].into_iter().enumerate() {
493 b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
494 }
495 for (i, v) in [p.buttons, p.pressed, p.released, p.flags].into_iter().enumerate() {
496 b[32 + i * 4..36 + i * 4].copy_from_slice(&v.to_le_bytes());
497 }
498 b
499}
500
501fn gamepad_bytes(g: &Gamepad) -> [u8; GAMEPAD_BYTES] {
503 let mut b = [0u8; GAMEPAD_BYTES];
504 let nb = g.buttons.len().min(GAMEPAD_BUTTONS);
505 let na = g.axes.len().min(GAMEPAD_AXES);
506 let flags = if g.connected { 1 | if g.standard { 2 } else { 0 } } else { 0u32 };
507 for (i, v) in [flags, nb as u32, na as u32].into_iter().enumerate() {
508 b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
509 }
510 for (i, v) in g.buttons.iter().take(nb).enumerate() {
511 b[12 + i * 4..16 + i * 4].copy_from_slice(&v.to_le_bytes());
512 }
513 for (i, v) in g.axes.iter().take(na).enumerate() {
514 b[140 + i * 4..144 + i * 4].copy_from_slice(&v.to_le_bytes());
515 }
516 b
517}
518
519fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
520 linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
521 let mem = memory(&caller)?;
522 let msg = guest_slice(mem.data(&caller), ptr, len as u64)?;
523 eprintln!("[guest] {}", String::from_utf8_lossy(msg));
524 Ok(())
525 })?;
526
527 linker.func_wrap("gasm", "has", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
528 let name = guest_str(&caller, ptr, len)?;
529 Ok(caller.data().provided.contains(&name) as i32)
530 })?;
531
532 linker.func_wrap("gasm", "video_set_aspect", |mut caller: Caller<'_, Host>, num: u32, den: u32| -> wasmtime::Result<()> {
533 let aspect = match (num, den) {
534 (0, 0) => None,
535 (1..=65535, 1..=65535) if num as u64 * 8 >= den as u64 && den as u64 * 8 >= num as u64 => Some((num, den)),
536 _ => bail!("video_set_aspect: invalid ratio {num}:{den}"),
537 };
538 caller.data_mut().aspect = aspect;
539 Ok(())
540 })?;
541
542 linker.func_wrap("gasm", "set_title", |mut caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
543 let title = clean_title(&guest_str(&caller, ptr, len)?);
544 let h = caller.data_mut();
545 if h.title != title {
546 h.title = title;
547 h.title_changed = true;
548 }
549 Ok(())
550 })?;
551
552 linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
553 let h = caller.data();
554 h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
555 })?;
556
557 linker.func_wrap("gasm", "utc_offset_minutes", |caller: Caller<'_, Host>| -> i32 {
558 if caller.data().virtual_time_ms.is_some() { 0 } else { utc_offset_minutes() }
560 })?;
561
562 linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
563 if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
564 caller.data_mut().frame_rate = hz;
565 }
566 })?;
567
568 linker.func_wrap(
569 "gasm",
570 "video_present",
571 |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
572 let (w, h, stride) = (w as usize, h as usize, stride as usize);
573 if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
574 bail!("video_present: bad geometry {w}x{h} stride {stride}");
575 }
576 let mem = memory(&caller)?;
577 let (data, host) = mem.data_and_store_mut(&mut caller);
578 let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
579 let row = w * 4;
580 host.rgba.resize(row * h, 0);
581 if stride == row {
582 host.rgba.copy_from_slice(src);
583 } else {
584 for (y, dst) in host.rgba.chunks_exact_mut(row).enumerate() {
585 dst.copy_from_slice(&src[y * stride..y * stride + row]);
586 }
587 }
588 if host.hashing {
589 host.video_hash.update(&host.rgba);
590 }
591 host.width = w;
592 host.height = h;
593 host.frames_presented += 1;
594 Ok(())
595 },
596 )?;
597
598 linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
599 let h = caller.data_mut();
600 if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
601 h.audio_rate = rate;
602 h.audio_channels = channels;
603 if let Some(a) = &mut h.audio {
604 a.configure(rate, channels);
605 }
606 }
607 })?;
608
609 linker.func_wrap(
610 "gasm",
611 "audio_push",
612 |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
613 let mem = memory(&caller)?;
614 let (data, host) = mem.data_and_store_mut(&mut caller);
615 let len = frames as u64 * host.audio_channels as u64 * 4;
616 let bytes = guest_slice(data, ptr, len)?;
617 if host.hashing {
618 host.audio_hash.update(bytes);
619 }
620 host.audio_frames += frames as u64;
621 if let Some(a) = &mut host.audio {
622 a.push_le_f32(bytes);
623 }
624 Ok(())
625 },
626 )?;
627
628 linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
629 caller.data().pads.get(player as usize).copied().unwrap_or(0)
630 })?;
631
632 linker.func_wrap(
633 "gasm",
634 "text_input",
635 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
636 let mem = memory(&caller)?;
637 let (data, host) = mem.data_and_store_mut(&mut caller);
638 let Some(t) = &host.text else { return Ok(-1) };
639 copy_if_fits(data, dst, cap, t.as_bytes())
640 },
641 )?;
642
643 linker.func_wrap("gasm", "input_mode", |mut caller: Caller<'_, Host>, flags: u32| {
644 caller.data_mut().input_mode = flags & 7;
645 })?;
646
647 linker.func_wrap(
648 "gasm",
649 "key_state",
650 |mut caller: Caller<'_, Host>, dst: u32, len: u32| -> wasmtime::Result<i32> {
651 let mem = memory(&caller)?;
652 let (data, host) = mem.data_and_store_mut(&mut caller);
653 let Some(keys) = host.input.keys else { return Ok(-1) };
654 let n = (len as usize).min(KEY_STATE_BYTES);
655 guest_slice_mut(data, dst, n as u64)?.copy_from_slice(&keys[..n]);
656 Ok(KEY_STATE_BYTES as i32)
657 },
658 )?;
659
660 linker.func_wrap(
661 "gasm",
662 "key_events",
663 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
664 let mem = memory(&caller)?;
665 let (data, host) = mem.data_and_store_mut(&mut caller);
666 if host.input.keys.is_none() {
667 return Ok(-1);
668 }
669 let events = &host.input.key_events;
670 let len = events.len() * 4;
671 if len <= cap as usize {
672 let out = guest_slice_mut(data, dst, len as u64)?;
673 for (b, &(c, d)) in out.chunks_exact_mut(4).zip(events) {
674 let c = c.to_le_bytes();
675 b.copy_from_slice(&[c[0], c[1], d as u8, 0]);
676 }
677 }
678 Ok(len as i32)
679 },
680 )?;
681
682 linker.func_wrap(
683 "gasm",
684 "pointer",
685 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
686 let mem = memory(&caller)?;
687 let (data, host) = mem.data_and_store_mut(&mut caller);
688 let Some(p) = &host.input.pointer else { return Ok(-1) };
689 if cap as usize >= POINTER_BYTES {
690 let b = pointer_bytes(p, (host.width, host.height), host.aspect);
691 guest_slice_mut(data, dst, POINTER_BYTES as u64)?.copy_from_slice(&b);
692 }
693 Ok(POINTER_BYTES as i32)
694 },
695 )?;
696
697 linker.func_wrap(
698 "gasm",
699 "gamepad",
700 |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
701 let mem = memory(&caller)?;
702 let (data, host) = mem.data_and_store_mut(&mut caller);
703 let (Some(pads), true) = (&host.input.gamepads, slot < 4) else { return Ok(-1) };
704 if cap as usize >= GAMEPAD_BYTES {
705 let b = gamepad_bytes(&pads[slot as usize]);
706 guest_slice_mut(data, dst, GAMEPAD_BYTES as u64)?.copy_from_slice(&b);
707 }
708 Ok(GAMEPAD_BYTES as i32)
709 },
710 )?;
711
712 linker.func_wrap(
713 "gasm",
714 "gamepad_name",
715 |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
716 let mem = memory(&caller)?;
717 let (data, host) = mem.data_and_store_mut(&mut caller);
718 let Some(g) = host.input.gamepads.as_ref().and_then(|p| p.get(slot as usize)).filter(|g| g.connected) else { return Ok(-1) };
719 copy_if_fits(data, dst, cap, g.name.as_bytes())
720 },
721 )?;
722
723 linker.func_wrap(
724 "gasm",
725 "param",
726 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
727 let name = guest_str(&caller, ptr, len)?;
728 let mem = memory(&caller)?;
729 let (data, host) = mem.data_and_store_mut(&mut caller);
730 let Some(v) = host.params.get(&name) else { return Ok(-1) };
731 copy_if_fits(data, dst, cap, v.as_bytes())
732 },
733 )?;
734
735 fn read_asset(caller: &mut Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32) -> wasmtime::Result<i32> {
738 let name = guest_str(caller, ptr, len)?;
739 let mem = memory(caller)?;
740 let (data, host) = mem.data_and_store_mut(caller);
741 let Some(asset) = host.assets.get(&name) else { return Ok(-1) };
742 let n = asset.size().saturating_sub(offset).min(cap as u64);
743 let out = guest_slice_mut(data, dst, n)?;
744 Ok(asset.read_at(offset, out) as i32)
745 }
746
747 linker.func_wrap(
748 "gasm",
749 "asset_read_at",
750 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
751 read_asset(&mut caller, ptr, len, offset as u64, dst, cap)
752 },
753 )?;
754
755 linker.func_wrap(
756 "gasm",
757 "asset_read_at64",
758 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32| -> wasmtime::Result<i32> {
759 read_asset(&mut caller, ptr, len, offset, dst, cap)
760 },
761 )?;
762
763 linker.func_wrap(
764 "gasm",
765 "asset_read",
766 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
767 read_asset(&mut caller, ptr, len, 0, dst, cap)
768 },
769 )?;
770
771 linker.func_wrap("gasm", "asset_count", |caller: Caller<'_, Host>| -> u32 { caller.data().assets.names().len() as u32 })?;
772
773 linker.func_wrap(
774 "gasm",
775 "asset_name",
776 |mut caller: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
777 let mem = memory(&caller)?;
778 let (data, host) = mem.data_and_store_mut(&mut caller);
779 let Some(n) = host.assets.names().get(index as usize) else { return Ok(-1) };
780 copy_if_fits(data, dst, cap, n.as_bytes())
781 },
782 )?;
783
784 linker.func_wrap(
785 "gasm",
786 "asset_size",
787 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
788 let name = guest_str(&caller, ptr, len)?;
789 Ok(match caller.data().assets.size(&name) {
790 None => -1,
791 Some(n) if n > i32::MAX as u64 => -2,
792 Some(n) => n as i32,
793 })
794 },
795 )?;
796
797 linker.func_wrap(
798 "gasm",
799 "asset_size64",
800 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i64> {
801 let name = guest_str(&caller, ptr, len)?;
802 Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i64))
803 },
804 )?;
805
806 linker.func_wrap(
807 "gasm",
808 "asset_version",
809 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
810 let name = guest_str(&caller, ptr, len)?;
811 Ok(caller.data().assets.version(&name).map_or(-1, |v| v.min(i32::MAX as u32) as i32))
812 },
813 )?;
814 Ok(())
815}
816
817fn trap(e: String) -> wasmtime::Error {
818 format_err!("{e}")
819}
820
821fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
822 const M: &str = "gasm:gfx";
823 linker.func_wrap(M, "width", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().0 })?;
824 linker.func_wrap(M, "height", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().1 })?;
825 linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
826 let src = guest_str(&c, ptr, len)?;
827 c.data_mut().gfx.create_shader(&src).map_err(trap)
828 })?;
829 linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
830 c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
831 })?;
832 linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
833 let json = guest_str(&c, ptr, len)?;
834 c.data_mut().gfx.create_pipeline(&json).map_err(trap)
835 })?;
836 linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
837 let json = guest_str(&c, ptr, len)?;
838 c.data_mut().gfx.create_bind_group(&json).map_err(trap)
839 })?;
840 linker.func_wrap(
841 M,
842 "write_buffer",
843 |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
844 let mem = memory(&c)?;
845 let (data, host) = mem.data_and_store_mut(&mut c);
846 let bytes = guest_slice(data, ptr, len as u64)?;
847 host.gfx.write_buffer(buf, offset, bytes).map_err(trap)?;
848 if host.hashing {
849 host.video_hash.update(bytes);
850 }
851 Ok(())
852 },
853 )?;
854 linker.func_wrap(M, "create_bind_group_layout", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
855 let json = guest_str(&c, ptr, len)?;
856 c.data_mut().gfx.create_bind_group_layout(&json).map_err(trap)
857 })?;
858 linker.func_wrap(M, "create_texture", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
859 let json = guest_str(&c, ptr, len)?;
860 c.data_mut().gfx.create_texture(&json).map_err(trap)
861 })?;
862 linker.func_wrap(M, "create_sampler", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
863 let json = guest_str(&c, ptr, len)?;
864 c.data_mut().gfx.create_sampler(&json).map_err(trap)
865 })?;
866 linker.func_wrap(
867 M,
868 "write_texture",
869 |mut c: Caller<'_, Host>, tex: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
870 let mem = memory(&c)?;
871 let (data, host) = mem.data_and_store_mut(&mut c);
872 let bytes = guest_slice(data, ptr, len as u64)?;
873 host.gfx.write_texture(tex, mip, x, y, w, h, bytes).map_err(trap)?;
874 if host.hashing {
875 for v in [tex, mip, x, y, w, h] {
877 host.video_hash.update(&v.to_le_bytes());
878 }
879 host.video_hash.update(bytes);
880 }
881 Ok(())
882 },
883 )?;
884 linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
885 let h = c.data_mut();
886 let show = h.show_frame;
887 h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
888 })?;
889 linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
890 c.data_mut().gfx.set_pipeline(p).map_err(trap)
891 })?;
892 linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
893 c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
894 })?;
895 linker.func_wrap(
896 M,
897 "set_bind_group_offsets",
898 |mut c: Caller<'_, Host>, i: u32, bg: u32, ptr: u32, count: u32| -> wasmtime::Result<()> {
899 let mem = memory(&c)?;
900 let (data, host) = mem.data_and_store_mut(&mut c);
901 let bytes = guest_slice(data, ptr, count as u64 * 4)?;
902 let mut offsets = [0u32; 32];
903 if count as usize > offsets.len() {
904 bail!("gfx.set_bind_group_offsets: {count} offsets (a bind group has at most {})", offsets.len());
905 }
906 for (o, b) in offsets.iter_mut().zip(bytes.chunks_exact(4)) {
907 *o = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
908 }
909 host.gfx.set_bind_group_offsets(i, bg, &offsets[..count as usize]).map_err(trap)
910 },
911 )?;
912 linker.func_wrap(
913 M,
914 "set_viewport",
915 |mut c: Caller<'_, Host>, x: f32, y: f32, w: f32, h: f32, min: f32, max: f32| -> wasmtime::Result<()> {
916 c.data_mut().gfx.set_viewport(x, y, w, h, min, max).map_err(trap)
917 },
918 )?;
919 linker.func_wrap(M, "set_scissor_rect", |mut c: Caller<'_, Host>, x: u32, y: u32, w: u32, h: u32| -> wasmtime::Result<()> {
920 c.data_mut().gfx.set_scissor_rect(x, y, w, h).map_err(trap)
921 })?;
922 linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
923 c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
924 })?;
925 linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
926 c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
927 })?;
928 linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| -> wasmtime::Result<()> {
929 c.data_mut().gfx.draw(vc, ic, fv, fi).map_err(trap)
930 })?;
931 linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| -> wasmtime::Result<()> {
932 c.data_mut().gfx.draw_indexed(ic, n, first, base, fi).map_err(trap)
933 })?;
934 linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
935 let h = c.data_mut();
936 h.frames_presented += 1;
937 h.gfx.end_frame().map_err(trap)
938 })?;
939 linker.func_wrap(M, "destroy", |mut c: Caller<'_, Host>, handle: u32| -> wasmtime::Result<()> {
940 c.data_mut().gfx.destroy(handle).map_err(trap)
941 })?;
942 Ok(())
943}
944
945fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
946 const M: &str = "gasm:net";
947 linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
948 let url = guest_str(&c, ptr, len)?;
949 Ok(c.data_mut().net.open(&url))
950 })?;
951 linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> { c.data().net.state(h).map_err(trap) })?;
952 linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
953 let mem = memory(&c)?;
954 let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
955 if msg.is_empty() {
956 c.data().net.state(h).map_err(trap)?; return Ok(-1);
958 }
959 c.data_mut().net.send(h, msg).map_err(trap)
960 })?;
961 linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
962 let mem = memory(&c)?;
963 let (data, host) = mem.data_and_store_mut(&mut c);
964 let msg = match host.net.peek(h).map_err(trap)? {
965 Ok(m) => m,
966 Err(code) => return Ok(code),
967 };
968 let n = copy_if_fits(data, dst, cap, msg)?;
969 if n as usize <= cap as usize {
970 host.net.pop(h);
971 }
972 Ok(n)
973 })?;
974 linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().net.close(h).map_err(trap) })?;
975 Ok(())
976}
977
978fn add_fetch_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
979 const M: &str = "gasm:fetch";
980 linker.func_wrap(M, "request", |mut c: Caller<'_, Host>, dp: u32, dl: u32, bp: u32, bl: u32| -> wasmtime::Result<i32> {
981 let desc = guest_str(&c, dp, dl)?;
982 let mem = memory(&c)?;
983 let body = guest_slice(mem.data(&c), bp, bl as u64)?.to_vec();
984 let h = c.data_mut();
985 let frame = h.frame_index;
986 Ok(h.fetch.request(&desc, body, frame))
987 })?;
988 linker.func_wrap(M, "state", |mut c: Caller<'_, Host>, r: i32| -> wasmtime::Result<u32> {
989 let h = c.data_mut();
990 let frame = h.frame_index;
991 h.fetch.state(r, frame).map_err(trap)
992 })?;
993 linker.func_wrap(M, "status", |mut c: Caller<'_, Host>, r: i32| -> wasmtime::Result<i32> {
994 let h = c.data_mut();
995 let frame = h.frame_index;
996 h.fetch.status(r, frame).map_err(trap)
997 })?;
998 linker.func_wrap(M, "headers", |mut c: Caller<'_, Host>, r: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
999 let mem = memory(&c)?;
1000 let (data, host) = mem.data_and_store_mut(&mut c);
1001 let frame = host.frame_index;
1002 match host.fetch.headers(r, frame).map_err(trap)? {
1003 Some(text) => copy_if_fits(data, dst, cap, text.as_bytes()),
1004 None => Ok(-1),
1005 }
1006 })?;
1007 linker.func_wrap(M, "read", |mut c: Caller<'_, Host>, r: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1008 let mem = memory(&c)?;
1009 guest_slice(mem.data(&c), dst, cap as u64)?; let (data, host) = mem.data_and_store_mut(&mut c);
1011 let frame = host.frame_index;
1012 match host.fetch.read(r, (cap as usize).min(i32::MAX as usize), frame).map_err(trap)? {
1013 Ok(bytes) => {
1014 guest_slice_mut(data, dst, bytes.len() as u64)?.copy_from_slice(&bytes);
1015 Ok(bytes.len() as i32)
1016 }
1017 Err(()) => Ok(-1),
1018 }
1019 })?;
1020 linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, r: i32| -> wasmtime::Result<()> { c.data_mut().fetch.close(r).map_err(trap) })?;
1021 Ok(())
1022}
1023
1024fn add_clipboard_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1025 const M: &str = "gasm:clipboard";
1026 linker.func_wrap(M, "set_text", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
1027 if len as usize > CLIPBOARD_MAX {
1028 return Ok(-1);
1029 }
1030 let text = guest_str(&c, ptr, len)?;
1031 c.data_mut().clipboard.copied = Some(text);
1032 Ok(0)
1033 })?;
1034 linker.func_wrap(M, "get_text", |mut c: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1035 let mem = memory(&c)?;
1036 let (data, host) = mem.data_and_store_mut(&mut c);
1037 match &host.clipboard.pasted {
1038 Some(t) => copy_if_fits(data, dst, cap, t.as_bytes()),
1039 None => Ok(-1),
1040 }
1041 })?;
1042 Ok(())
1043}
1044
1045fn add_files_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1046 const M: &str = "gasm:files";
1047 linker.func_wrap(M, "save", |mut c: Caller<'_, Host>, np: u32, nl: u32, mp: u32, ml: u32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
1048 let (name, mime) = (guest_str(&c, np, nl)?, guest_str(&c, mp, ml)?);
1049 let mem = memory(&c)?;
1050 let (data, host) = mem.data_and_store_mut(&mut c);
1051 let bytes = guest_slice(data, ptr, len as u64)?;
1052 Ok(host.files.save(&name, &mime, bytes))
1053 })?;
1054 linker.func_wrap(M, "state", |c: Caller<'_, Host>, handle: i32| -> wasmtime::Result<i32> {
1055 c.data().files.state(handle).ok_or_else(|| format_err!("gasm:files: invalid handle {handle}"))
1056 })?;
1057 Ok(())
1058}
1059
1060fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
1061 const M: &str = "gasm:storage";
1062 linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1063 let key = guest_str(&c, kp, kl)?;
1064 let mem = memory(&c)?;
1065 let (data, host) = mem.data_and_store_mut(&mut c);
1066 let Some(v) = host.storage.get(&key) else { return Ok(-1) };
1067 copy_if_fits(data, dst, cap, v)
1068 })?;
1069 linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
1070 let key = guest_str(&c, kp, kl)?;
1071 let mem = memory(&c)?;
1072 let (data, host) = mem.data_and_store_mut(&mut c);
1073 let value = guest_slice(data, vp, vl as u64)?;
1074 Ok(match host.storage.set(&key, value) {
1075 Ok(()) => 0,
1076 Err(e) => {
1077 eprintln!("[gasm] storage: {key}: {e}");
1078 e.code()
1079 }
1080 })
1081 })?;
1082 linker.func_wrap(M, "count", |mut c: Caller<'_, Host>| -> u32 { c.data_mut().storage.keys().len() as u32 })?;
1083 linker.func_wrap(M, "key", |mut c: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
1084 let mem = memory(&c)?;
1085 let (data, host) = mem.data_and_store_mut(&mut c);
1086 let Some(k) = host.storage.keys().get(index as usize) else { return Ok(-1) };
1087 copy_if_fits(data, dst, cap, k.as_bytes())
1088 })?;
1089 linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
1090 let key = guest_str(&c, kp, kl)?;
1091 Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
1092 })?;
1093 Ok(())
1094}
1095
1096#[derive(Debug)]
1098pub enum Stop {
1099 Exit(i32),
1101 Trap(String),
1102}
1103
1104impl std::fmt::Display for Stop {
1105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1106 match self {
1107 Stop::Exit(code) => write!(f, "guest exited with code {code}"),
1108 Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
1109 }
1110 }
1111}
1112
1113impl std::error::Error for Stop {}
1114
1115fn classify(e: wasmtime::Error) -> Stop {
1116 if let Some(exit) = e.downcast_ref::<wasi::Exit>() {
1117 return Stop::Exit(exit.0);
1118 }
1119 if e.downcast_ref::<switching::Quit>().is_some() {
1120 return Stop::Exit(0);
1121 }
1122 if e.downcast_ref::<wasmtime::Trap>() == Some(&wasmtime::Trap::Interrupt) {
1123 return Stop::Trap(format!("a guest call ran longer than the time limit (stuck in a loop?); see --call-timeout\n{e:?}"));
1124 }
1125 Stop::Trap(format!("{e:?}"))
1126}
1127
1128const TICKS_PER_SEC: u64 = 10;
1130
1131pub fn engine() -> &'static Engine {
1136 static ENGINE: OnceLock<Engine> = OnceLock::new();
1137 ENGINE.get_or_init(new_engine)
1138}
1139
1140fn new_engine() -> Engine {
1141 let mut config = Config::new();
1142 config.epoch_interruption(true);
1143 config.async_stack_size(4 << 20);
1145 let engine = Engine::new(&config).expect("wasmtime engine");
1146 let ticker = engine.clone();
1147 std::thread::Builder::new()
1148 .name("gasm-epoch".into())
1149 .spawn(move || {
1150 loop {
1151 std::thread::sleep(Duration::from_millis(1000 / TICKS_PER_SEC));
1152 ticker.increment_epoch();
1153 }
1154 })
1155 .expect("epoch thread");
1156 engine
1157}
1158
1159pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
1161 engine().precompile_module(wasm)
1162}
1163
1164#[derive(Clone, Copy)]
1166pub struct LoadOptions {
1167 pub allow_precompiled: bool,
1170 pub call_timeout: Option<Duration>,
1172 pub stack_switching: bool,
1175 pub memory_limit: Option<usize>,
1177}
1178
1179impl Default for LoadOptions {
1180 fn default() -> Self {
1181 LoadOptions { allow_precompiled: false, call_timeout: Some(Duration::from_secs(30)), stack_switching: true, memory_limit: Some(DEFAULT_MEMORY_LIMIT) }
1182 }
1183}
1184
1185type Running = Pin<Box<dyn Future<Output = (Store<Host>, wasmtime::Result<i32>)>>>;
1187
1188pub struct Game {
1192 store: Option<Store<Host>>,
1194 frame: TypedFunc<(), ()>,
1195 exit: Option<TypedFunc<(), ()>>,
1196 deadline: u64,
1197 run: Option<TypedFunc<(), i32>>,
1199 running: Option<Running>,
1200 exchange: Option<Arc<Exchange>>,
1201 ended: bool,
1203 gl: bool,
1205 watches: Vec<crate::assets::AssetWatch>,
1207}
1208
1209impl Game {
1210 pub fn load(wasm: &[u8], host: Host, opts: LoadOptions) -> Result<Game, Stop> {
1211 let module = Self::compile(wasm, opts.allow_precompiled)?;
1212 Self::load_module(module, host, opts)
1213 }
1214
1215 pub fn compile(wasm: &[u8], allow_precompiled: bool) -> Result<Module, Stop> {
1218 let module = if wasm.starts_with(b"\0asm") {
1219 Module::new(engine(), wasm)
1220 } else if allow_precompiled {
1221 unsafe { Module::deserialize(engine(), wasm) }
1224 } else {
1225 Err(format_err!("not a wasm module (a precompiled .cwasm runs as native code: pass --allow-precompiled if you made it yourself)"))
1226 };
1227 module.map_err(classify)
1228 }
1229
1230 pub fn load_module(module: Module, host: Host, opts: LoadOptions) -> Result<Game, Stop> {
1232 Self::load_inner(module, host, opts).map_err(classify)
1233 }
1234
1235 fn load_inner(module: Module, host: Host, opts: LoadOptions) -> wasmtime::Result<Game> {
1236 let gl = module.imports().any(|i| i.module() == "gasm:gl");
1237 if gl && module.imports().any(|i| i.module() == "gasm:gfx") {
1238 bail!("a module imports gasm:gfx or gasm:gl, not both");
1239 }
1240 let has_run = module.get_export("gasm_run").is_some();
1242 let switching = has_run && opts.stack_switching;
1243 if has_run && !switching && module.imports().any(|i| i.module() == "asyncify") {
1244 bail!("this module is a run build (no Asyncify): it needs stack switching (gasm_run); use the game's Asyncify build");
1246 }
1247 let engine = engine();
1248 let mut linker: Linker<Host> = Linker::new(engine);
1249 add_gasm_imports(&mut linker)?;
1250 if switching {
1251 switching::add_yield_async(&mut linker)?;
1252 } else {
1253 switching::add_yield_sync(&mut linker)?;
1254 }
1255 add_gfx_imports(&mut linker)?;
1256 crate::gl::add_gl_imports(&mut linker)?;
1257 add_net_imports(&mut linker)?;
1258 add_fetch_imports(&mut linker)?;
1259 add_storage_imports(&mut linker)?;
1260 add_clipboard_imports(&mut linker)?;
1261 add_files_imports(&mut linker)?;
1262 let mut store = Store::new(engine, host);
1263 store.data_mut().memory_limit = MemoryLimit(opts.memory_limit);
1264 store.limiter(|h| &mut h.memory_limit);
1265 let mut provided = HashSet::new();
1267 for (m, f, _) in linker.iter(&mut store) {
1268 provided.insert(m.to_owned());
1269 provided.insert(format!("{m}.{f}"));
1270 }
1271 for f in wasi::IMPLEMENTED {
1272 provided.insert(format!("{}.{f}", wasi::MODULE));
1273 }
1274 provided.insert(wasi::MODULE.to_owned());
1275 store.data_mut().provided = provided;
1276 wasi::add_to_linker(&mut linker, &module)?;
1277 linker.define_unknown_imports_as_traps(&module)?;
1280
1281 let deadline = opts.call_timeout.map_or(u64::MAX / 2, |t| (t.as_millis() as u64 * TICKS_PER_SEC).div_ceil(1000).max(1));
1282 store.epoch_deadline_trap();
1283 store.set_epoch_deadline(deadline);
1284 let exchange = switching.then(|| Exchange::new(deadline));
1285 store.data_mut().run = exchange.clone();
1286 let instance: Instance = if switching {
1287 switching::block_on(linker.instantiate_async(&mut store, &module))??
1288 } else {
1289 linker.instantiate(&mut store, &module)?
1290 };
1291 let memory = instance
1292 .get_memory(&mut store, "memory")
1293 .ok_or_else(|| format_err!("guest does not export `memory`"))?;
1294 store.data_mut().memory = Some(memory);
1295
1296 fn call<R: wasmtime::WasmResults + Send + Sync>(store: &mut Store<Host>, f: TypedFunc<(), R>, switching: bool) -> wasmtime::Result<R> {
1298 if switching { switching::block_on(f.call_async(store, ()))? } else { f.call(store, ()) }
1299 }
1300 if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
1301 store.set_epoch_deadline(deadline);
1302 call(&mut store, init, switching)?;
1303 }
1304 let version = instance.get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?;
1305 let version = call(&mut store, version, switching)?;
1306 if version != ABI_VERSION {
1307 bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
1308 }
1309 store.set_epoch_deadline(deadline);
1310 let init = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?;
1311 let rc = call(&mut store, init, switching)?;
1312 if rc != 0 {
1313 bail!("gasm_init failed with code {rc}");
1314 }
1315 let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
1316 let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
1317 let run = if switching { Some(instance.get_typed_func::<(), i32>(&mut store, "gasm_run")?) } else { None };
1318 Ok(Game { store: Some(store), frame, exit, deadline, run, running: None, exchange, ended: false, gl, watches: Vec::new() })
1319 }
1320
1321 pub fn set_asset(&mut self, name: &str, bytes: Vec<u8>) -> u32 {
1324 self.with_host(|h| h.assets.set(name, bytes))
1325 }
1326
1327 pub fn remove_asset(&mut self, name: &str) -> bool {
1329 self.with_host(|h| h.assets.remove(name))
1330 }
1331
1332 pub fn watch_asset(&mut self, name: &str, path: &std::path::Path) {
1335 self.add_watch(crate::assets::AssetWatch::new(name, path));
1336 }
1337
1338 pub fn add_watch(&mut self, watch: crate::assets::AssetWatch) {
1340 self.watches.push(watch);
1341 }
1342
1343 pub fn uses_gl(&self) -> bool {
1345 self.gl
1346 }
1347
1348 pub fn switching(&self) -> bool {
1350 self.run.is_some()
1351 }
1352
1353 pub fn frame(&mut self) -> Result<(), Stop> {
1355 if self.ended {
1356 return Err(Stop::Trap("the guest is not running".into()));
1357 }
1358 self.with_host(|h| h.files.flush());
1360 if !self.watches.is_empty() {
1361 let mut watches = std::mem::take(&mut self.watches);
1362 self.with_host(|h| watches.iter_mut().for_each(|w| _ = w.poll(&mut h.assets)));
1363 self.watches = watches;
1364 }
1365 let Some(run) = self.run.clone() else {
1366 let store = self.store.as_mut().expect("store");
1367 store.set_epoch_deadline(self.deadline);
1368 let r = self.frame.call(&mut *store, ()).map_err(classify);
1369 store.data_mut().frame_index += 1;
1370 self.ended = r.is_err();
1371 return r;
1372 };
1373 match &self.exchange {
1374 Some(ex) if self.running.is_some() => ex.set(Cmd::Resume),
1375 _ => {
1376 let mut store = self.store.take().expect("store");
1378 store.set_epoch_deadline(self.deadline);
1379 self.running = Some(Box::pin(async move {
1380 let r = run.call_async(&mut store, ()).await;
1381 (store, r)
1382 }));
1383 }
1384 }
1385 self.poll()
1386 }
1387
1388 fn poll(&mut self) -> Result<(), Stop> {
1390 let Some(running) = self.running.as_mut() else { return Ok(()) };
1391 match switching::poll_once(running.as_mut()) {
1392 Poll::Pending => Ok(()),
1393 Poll::Ready((store, r)) => {
1394 self.store = Some(store);
1395 self.running = None;
1396 self.ended = true;
1397 Err(match r {
1398 Ok(code) => Stop::Exit(code),
1399 Err(e) => classify(e),
1400 })
1401 }
1402 }
1403 }
1404
1405 pub fn exit(&mut self) {
1408 if self.store.is_some() || self.running.is_some() {
1409 self.with_host(|h| h.files.flush());
1410 }
1411 if self.running.is_some() {
1412 if let Some(ex) = &self.exchange {
1413 ex.set(Cmd::Exit);
1414 }
1415 if let Err(Stop::Trap(t)) = self.poll() {
1416 eprintln!("[gasm] gasm_exit trapped: {t}");
1417 }
1418 if self.running.is_some() {
1419 self.running = None;
1421 }
1422 self.exit = None;
1423 return;
1424 }
1425 if let (Some(f), Some(store)) = (self.exit.take(), self.store.as_mut()) {
1426 store.set_epoch_deadline(self.deadline);
1427 let r = if self.run.is_some() { switching::block_on(f.call_async(&mut *store, ())).and_then(|r| r) } else { f.call(&mut *store, ()) };
1428 if let Err(e) = r {
1429 if let Stop::Trap(t) = classify(e) {
1430 eprintln!("[gasm] gasm_exit trapped: {t}");
1431 }
1432 }
1433 store.data_mut().files.flush(); }
1435 }
1436
1437 pub fn with_host<R>(&mut self, f: impl FnOnce(&mut Host) -> R) -> R {
1441 if let Some(store) = &mut self.store {
1442 return f(store.data_mut());
1443 }
1444 let (Some(running), Some(ex)) = (self.running.as_mut(), &self.exchange) else {
1445 panic!("with_host: the guest's store is gone (gasm_exit yielded)");
1446 };
1447 let mut f = Some(f);
1448 let mut out = None;
1449 let mut job = |h: &mut Host| out = Some((f.take().expect("job runs once"))(h));
1450 let ptr: *mut (dyn FnMut(&mut Host) + '_) = &mut job;
1451 let ptr: *mut (dyn FnMut(&mut Host) + 'static) = unsafe { std::mem::transmute(ptr) };
1454 ex.set(Cmd::Job(Job(ptr)));
1455 let polled = switching::poll_once(running.as_mut());
1456 ex.set(Cmd::None);
1457 match polled {
1458 Poll::Pending => {}
1459 Poll::Ready(_) => unreachable!("yield_frame stays suspended after a job"),
1460 }
1461 out.expect("yield_frame ran the job")
1462 }
1463
1464 pub fn host(&self) -> &Host {
1468 self.store.as_ref().expect("Game::host: a gasm_run guest is running; use Game::with_host").data()
1469 }
1470
1471 pub fn host_mut(&mut self) -> &mut Host {
1472 self.store.as_mut().expect("Game::host_mut: a gasm_run guest is running; use Game::with_host").data_mut()
1473 }
1474}
1475
1476#[cfg(test)]
1477mod tests {
1478 use super::*;
1479
1480 #[test]
1481 fn titles_are_cleaned_and_cut() {
1482 assert_eq!(clean_title("A\u{7}B\u{202e}C\u{2069}").as_deref(), Some("ABC"));
1483 assert_eq!(clean_title("\n\t"), None);
1484 assert_eq!(clean_title(&"é".repeat(200)).map(|t| t.len()), Some(256));
1486 }
1487
1488 const RUN_GUEST: &str = r#"(module
1489 (import "gasm" "yield_frame" (func $yield))
1490 (memory (export "memory") 1)
1491 (global $n (mut i32) (i32.const 0))
1492 (func (export "gasm_abi_version") (result i32) i32.const 0)
1493 (func (export "gasm_init") (result i32) i32.const 0)
1494 (func (export "gasm_frame") call $yield)
1495 (func (export "gasm_exit") (global.set $n (i32.const 100)))
1496 (func (export "frames") (result i32) global.get $n)
1497 (func (export "gasm_run") (result i32)
1498 (loop $l
1499 (global.set $n (i32.add (global.get $n) (i32.const 1)))
1500 call $yield
1501 (br_if $l (i32.lt_u (global.get $n) (i32.const 3))))
1502 i32.const 7))"#;
1503
1504 fn host() -> Host {
1505 Host::new(Assets::new(), HashMap::new(), None, Gfx::null(), Net::new(false), Storage::memory())
1506 }
1507
1508 #[test]
1509 fn gasm_run_guests_suspend_between_frames() {
1510 let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1511 assert!(g.switching());
1512 g.with_host(|h| h.frame_rate = 30.0); assert!(g.frame().is_ok()); assert_eq!(g.with_host(|h| h.frame_rate), 30.0);
1516 assert!(g.frame().is_ok()); assert!(g.frame().is_ok()); assert!(matches!(g.frame(), Err(Stop::Exit(7))));
1520 assert!(g.frame().is_err(), "never called again");
1521 }
1522
1523 #[test]
1524 fn exit_runs_on_top_of_a_suspended_run() {
1525 let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1526 g.frame().unwrap();
1527 g.exit();
1528 assert!(g.store.is_some());
1530 }
1531
1532 #[test]
1533 fn yield_frame_outside_gasm_run_traps() {
1534 let opts = LoadOptions { stack_switching: false, ..LoadOptions::default() };
1535 let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), opts).unwrap();
1536 assert!(!g.switching());
1537 match g.frame() {
1538 Err(Stop::Trap(t)) => assert!(t.contains("only inside gasm_run"), "{t}"),
1539 other => panic!("expected a trap, got {:?}", other.err()),
1540 }
1541 }
1542
1543 #[test]
1544 fn custom_sections_are_found() {
1545 let mut wasm = b"\0asm\x01\0\0\0\x01\x01\0".to_vec();
1547 wasm.extend_from_slice(&[0, 14, 10]);
1548 wasm.extend_from_slice(b"gasm.titleHi\0");
1549 assert_eq!(custom_section(&wasm, "gasm.title"), Some(&b"Hi\0"[..]));
1550 assert_eq!(static_title(&wasm).as_deref(), Some("Hi"));
1551 assert_eq!(custom_section(&wasm, "other"), None);
1552 assert_eq!(custom_section(&wasm[..wasm.len() - 2], "gasm.title"), None); assert_eq!(custom_section(b"not wasm", "gasm.title"), None);
1554 }
1555}