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<GuestMemory>,
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 media: crate::media::Media,
280 pub clipboard: Clipboard,
282 pub files: crate::files::Files,
284 pub requires: Vec<String>,
286 threads: Option<std::sync::Arc<crate::threads::Threads>>,
288 pub profile: Option<Profile>,
290 pub storage: Storage,
291 pub show_frame: bool,
293 audio_rate: u32,
294 audio_channels: u32,
295 provided: HashSet<String>,
297 pub rgba: Vec<u8>,
299 pub width: usize,
300 pub height: usize,
301 pub frames_presented: u64,
302 pub aspect: Option<(u32, u32)>,
304 pub(crate) run: Option<Arc<Exchange>>,
306 pub title: Option<String>,
308 pub title_changed: bool,
310 pub fullscreen_capable: bool,
312 pub fullscreen: bool,
314 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 pub memory_limit: MemoryLimit,
322 pub catch_up: bool,
324}
325
326impl Host {
327 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 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
412pub 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)?; } 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,
463 }
464 }
465 return Some(false);
466 }
467 p = end;
468 }
469 Some(false)
470 };
471 scan().unwrap_or(false)
472}
473
474pub(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
484pub(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#[derive(Default)]
500pub struct Clipboard {
501 pub pasted: Option<String>,
502 pub copied: Option<String>,
503}
504
505pub 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#[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 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 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 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
569fn 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
582fn 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 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 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 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 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)?; 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)?; 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
1157fn 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
1176fn 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
1190pub 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#[derive(Debug)]
1275pub enum Stop {
1276 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
1305const TICKS_PER_SEC: u64 = 10;
1307
1308pub 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 config.shared_memory(true);
1322 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
1338pub struct Profile {
1346 out: std::path::PathBuf,
1347 profiler: Option<wasmtime::GuestProfiler>,
1348 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
1382fn 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
1399pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
1401 engine().precompile_module(wasm)
1402}
1403
1404#[derive(Clone, Copy)]
1406pub struct LoadOptions {
1407 pub allow_precompiled: bool,
1410 pub call_timeout: Option<Duration>,
1412 pub stack_switching: bool,
1415 pub memory_limit: Option<usize>,
1417 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
1428type Running = Pin<Box<dyn Future<Output = (Store<Host>, wasmtime::Result<i32>)>>>;
1430
1431pub struct Game {
1435 store: Option<Store<Host>>,
1437 frame: TypedFunc<(), ()>,
1438 exit: Option<TypedFunc<(), ()>>,
1439 deadline: u64,
1440 run: Option<TypedFunc<(), i32>>,
1442 running: Option<Running>,
1443 exchange: Option<Arc<Exchange>>,
1444 ended: bool,
1446 gl: bool,
1448 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 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 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 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 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 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 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 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 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 } 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 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 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 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 pub fn remove_asset(&mut self, name: &str) -> bool {
1626 self.with_host(|h| h.assets.remove(name))
1627 }
1628
1629 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 pub fn add_watch(&mut self, watch: crate::assets::AssetWatch) {
1637 self.watches.push(watch);
1638 }
1639
1640 pub fn uses_gl(&self) -> bool {
1642 self.gl
1643 }
1644
1645 pub fn switching(&self) -> bool {
1647 self.run.is_some()
1648 }
1649
1650 pub fn frame(&mut self) -> Result<(), Stop> {
1652 if self.ended {
1653 return Err(Stop::Trap("the guest is not running".into()));
1654 }
1655 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 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 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 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 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 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(); }
1739 }
1740
1741 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 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 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 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); assert!(g.frame().is_ok()); assert_eq!(g.with_host(|h| h.frame_rate), 30.0);
1820 assert!(g.frame().is_ok()); assert!(g.frame().is_ok()); 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 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 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); assert_eq!(custom_section(b"not wasm", "gasm.title"), None);
1858 }
1859}