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
137pub fn clean_title(text: &str) -> Option<String> {
142 let mut out = String::new();
143 for c in text.chars().filter(|&c| !c.is_control() && !matches!(c, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')) {
144 if out.len() + c.len_utf8() > TITLE_MAX_BYTES {
145 break;
146 }
147 out.push(c);
148 }
149 (!out.is_empty()).then_some(out)
150}
151
152pub fn custom_section<'a>(wasm: &'a [u8], name: &str) -> Option<&'a [u8]> {
155 fn leb(b: &[u8], at: &mut usize) -> Option<usize> {
156 let (mut v, mut shift) = (0usize, 0);
157 loop {
158 let byte = *b.get(*at)?;
159 *at += 1;
160 v |= ((byte & 0x7f) as usize).checked_shl(shift)?;
161 if byte & 0x80 == 0 {
162 return Some(v);
163 }
164 shift += 7;
165 if shift > 28 {
166 return None;
167 }
168 }
169 }
170 if wasm.get(..4)? != b"\0asm" {
171 return None;
172 }
173 let mut at = 8;
174 while at < wasm.len() {
175 let id = wasm[at];
176 at += 1;
177 let size = leb(wasm, &mut at)?;
178 let body = wasm.get(at..at.checked_add(size)?)?;
179 at += size;
180 if id == 0 {
181 let mut p = 0;
182 let n = leb(body, &mut p)?;
183 if body.get(p..p.checked_add(n)?)? == name.as_bytes() {
184 return body.get(p + n..);
185 }
186 }
187 }
188 None
189}
190
191pub fn static_title(wasm: &[u8]) -> Option<String> {
194 clean_title(std::str::from_utf8(custom_section(wasm, "gasm.title")?).ok()?)
195}
196
197pub struct Host {
198 memory: Option<Memory>,
199 start: Instant,
200 pub virtual_time_ms: Option<f64>,
203 pub random: Option<Splitmix>,
205 pub frame_rate: f64,
206 pub pads: [u32; 4],
207 pub text: Option<String>,
210 pub input: RawInput,
211 pub input_mode: u32,
213 pub assets: Assets,
214 pub params: HashMap<String, String>,
215 pub audio: Option<Box<dyn AudioOut>>,
216 pub gfx: Gfx,
217 pub gl: crate::gl::Gl,
219 pub frame_index: u64,
221 pub net: Net,
222 pub storage: Storage,
223 pub show_frame: bool,
225 audio_rate: u32,
226 audio_channels: u32,
227 provided: HashSet<String>,
229 pub rgba: Vec<u8>,
231 pub width: usize,
232 pub height: usize,
233 pub frames_presented: u64,
234 pub aspect: Option<(u32, u32)>,
236 pub(crate) run: Option<Arc<Exchange>>,
238 pub title: Option<String>,
240 pub title_changed: bool,
242 pub hashing: bool,
243 pub video_hash: Fnv,
244 pub audio_hash: Fnv,
245 pub audio_frames: u64,
246}
247
248impl Host {
249 pub fn new(
250 assets: impl Into<Assets>,
251 params: HashMap<String, String>,
252 audio: Option<Box<dyn AudioOut>>,
253 gfx: Gfx,
254 net: Net,
255 storage: Storage,
256 ) -> Self {
257 Host {
258 memory: None,
259 start: Instant::now(),
260 virtual_time_ms: None,
261 random: None,
262 frame_rate: 60.0,
263 pads: [0; 4],
264 text: None,
265 input: RawInput::default(),
266 input_mode: 0,
267 assets: assets.into(),
268 params,
269 audio,
270 gfx,
271 gl: Default::default(),
272 frame_index: 0,
273 net,
274 storage,
275 show_frame: true,
276 audio_rate: 44100,
277 audio_channels: 2,
278 provided: HashSet::new(),
279 title: None,
280 title_changed: false,
281 aspect: None,
282 run: None,
283 rgba: Vec::new(),
284 width: 0,
285 height: 0,
286 frames_presented: 0,
287 hashing: true,
288 video_hash: Fnv::new(),
289 audio_hash: Fnv::new(),
290 audio_frames: 0,
291 }
292 }
293
294 pub fn set_reproducible(&mut self) {
297 self.virtual_time_ms = Some(0.0);
298 self.random = Some(Splitmix(0));
299 }
300
301 pub(crate) fn started(&self) -> Instant {
302 self.start
303 }
304}
305
306pub(crate) fn guest_slice(mem: &[u8], ptr: u32, len: u64) -> wasmtime::Result<&[u8]> {
308 let start = ptr as u64;
309 let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
310 if end > mem.len() as u64 {
311 bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
312 }
313 Ok(&mem[start as usize..end as usize])
314}
315
316pub(crate) fn guest_slice_mut(mem: &mut [u8], ptr: u32, len: u64) -> wasmtime::Result<&mut [u8]> {
318 let start = ptr as u64;
319 let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
320 if end > mem.len() as u64 {
321 bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
322 }
323 Ok(&mut mem[start as usize..end as usize])
324}
325
326fn copy_if_fits(mem: &mut [u8], dst: u32, cap: u32, src: &[u8]) -> wasmtime::Result<i32> {
329 if src.len() <= cap as usize {
330 guest_slice_mut(mem, dst, src.len() as u64)?.copy_from_slice(src);
331 }
332 Ok(src.len() as i32)
333}
334
335pub(crate) fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<Memory> {
336 caller.data().memory.ok_or_else(|| format_err!("guest memory not yet available"))
337}
338
339fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
340 let mem = memory(caller)?;
341 let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
342 std::str::from_utf8(bytes).map(str::to_owned).map_err(|e| format_err!("string argument is not UTF-8: {e}"))
343}
344
345fn pointer_bytes(p: &Pointer, frame: (usize, usize), aspect: Option<(u32, u32)>) -> [u8; POINTER_BYTES] {
347 let (fx, fy) = frame_position(p.x, p.y, p.drawable, frame, p.integer_scale, aspect);
348 let mut b = [0u8; POINTER_BYTES];
349 for (i, v) in [p.x, p.y, fx, fy, p.dx, p.dy, p.wheel_x, p.wheel_y].into_iter().enumerate() {
350 b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
351 }
352 for (i, v) in [p.buttons, p.pressed, p.released, p.flags].into_iter().enumerate() {
353 b[32 + i * 4..36 + i * 4].copy_from_slice(&v.to_le_bytes());
354 }
355 b
356}
357
358fn gamepad_bytes(g: &Gamepad) -> [u8; GAMEPAD_BYTES] {
360 let mut b = [0u8; GAMEPAD_BYTES];
361 let nb = g.buttons.len().min(GAMEPAD_BUTTONS);
362 let na = g.axes.len().min(GAMEPAD_AXES);
363 let flags = if g.connected { 1 | if g.standard { 2 } else { 0 } } else { 0u32 };
364 for (i, v) in [flags, nb as u32, na as u32].into_iter().enumerate() {
365 b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
366 }
367 for (i, v) in g.buttons.iter().take(nb).enumerate() {
368 b[12 + i * 4..16 + i * 4].copy_from_slice(&v.to_le_bytes());
369 }
370 for (i, v) in g.axes.iter().take(na).enumerate() {
371 b[140 + i * 4..144 + i * 4].copy_from_slice(&v.to_le_bytes());
372 }
373 b
374}
375
376fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
377 linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
378 let mem = memory(&caller)?;
379 let msg = guest_slice(mem.data(&caller), ptr, len as u64)?;
380 eprintln!("[guest] {}", String::from_utf8_lossy(msg));
381 Ok(())
382 })?;
383
384 linker.func_wrap("gasm", "has", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
385 let name = guest_str(&caller, ptr, len)?;
386 Ok(caller.data().provided.contains(&name) as i32)
387 })?;
388
389 linker.func_wrap("gasm", "video_set_aspect", |mut caller: Caller<'_, Host>, num: u32, den: u32| -> wasmtime::Result<()> {
390 let aspect = match (num, den) {
391 (0, 0) => None,
392 (1..=65535, 1..=65535) if num as u64 * 8 >= den as u64 && den as u64 * 8 >= num as u64 => Some((num, den)),
393 _ => bail!("video_set_aspect: invalid ratio {num}:{den}"),
394 };
395 caller.data_mut().aspect = aspect;
396 Ok(())
397 })?;
398
399 linker.func_wrap("gasm", "set_title", |mut caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
400 let title = clean_title(&guest_str(&caller, ptr, len)?);
401 let h = caller.data_mut();
402 if h.title != title {
403 h.title = title;
404 h.title_changed = true;
405 }
406 Ok(())
407 })?;
408
409 linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
410 let h = caller.data();
411 h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
412 })?;
413
414 linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
415 if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
416 caller.data_mut().frame_rate = hz;
417 }
418 })?;
419
420 linker.func_wrap(
421 "gasm",
422 "video_present",
423 |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
424 let (w, h, stride) = (w as usize, h as usize, stride as usize);
425 if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
426 bail!("video_present: bad geometry {w}x{h} stride {stride}");
427 }
428 let mem = memory(&caller)?;
429 let (data, host) = mem.data_and_store_mut(&mut caller);
430 let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
431 let row = w * 4;
432 host.rgba.resize(row * h, 0);
433 if stride == row {
434 host.rgba.copy_from_slice(src);
435 } else {
436 for (y, dst) in host.rgba.chunks_exact_mut(row).enumerate() {
437 dst.copy_from_slice(&src[y * stride..y * stride + row]);
438 }
439 }
440 if host.hashing {
441 host.video_hash.update(&host.rgba);
442 }
443 host.width = w;
444 host.height = h;
445 host.frames_presented += 1;
446 Ok(())
447 },
448 )?;
449
450 linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
451 let h = caller.data_mut();
452 if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
453 h.audio_rate = rate;
454 h.audio_channels = channels;
455 if let Some(a) = &mut h.audio {
456 a.configure(rate, channels);
457 }
458 }
459 })?;
460
461 linker.func_wrap(
462 "gasm",
463 "audio_push",
464 |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
465 let mem = memory(&caller)?;
466 let (data, host) = mem.data_and_store_mut(&mut caller);
467 let len = frames as u64 * host.audio_channels as u64 * 4;
468 let bytes = guest_slice(data, ptr, len)?;
469 if host.hashing {
470 host.audio_hash.update(bytes);
471 }
472 host.audio_frames += frames as u64;
473 if let Some(a) = &mut host.audio {
474 a.push_le_f32(bytes);
475 }
476 Ok(())
477 },
478 )?;
479
480 linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
481 caller.data().pads.get(player as usize).copied().unwrap_or(0)
482 })?;
483
484 linker.func_wrap(
485 "gasm",
486 "text_input",
487 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
488 let mem = memory(&caller)?;
489 let (data, host) = mem.data_and_store_mut(&mut caller);
490 let Some(t) = &host.text else { return Ok(-1) };
491 copy_if_fits(data, dst, cap, t.as_bytes())
492 },
493 )?;
494
495 linker.func_wrap("gasm", "input_mode", |mut caller: Caller<'_, Host>, flags: u32| {
496 caller.data_mut().input_mode = flags & 7;
497 })?;
498
499 linker.func_wrap(
500 "gasm",
501 "key_state",
502 |mut caller: Caller<'_, Host>, dst: u32, len: u32| -> wasmtime::Result<i32> {
503 let mem = memory(&caller)?;
504 let (data, host) = mem.data_and_store_mut(&mut caller);
505 let Some(keys) = host.input.keys else { return Ok(-1) };
506 let n = (len as usize).min(KEY_STATE_BYTES);
507 guest_slice_mut(data, dst, n as u64)?.copy_from_slice(&keys[..n]);
508 Ok(KEY_STATE_BYTES as i32)
509 },
510 )?;
511
512 linker.func_wrap(
513 "gasm",
514 "key_events",
515 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
516 let mem = memory(&caller)?;
517 let (data, host) = mem.data_and_store_mut(&mut caller);
518 if host.input.keys.is_none() {
519 return Ok(-1);
520 }
521 let events = &host.input.key_events;
522 let len = events.len() * 4;
523 if len <= cap as usize {
524 let out = guest_slice_mut(data, dst, len as u64)?;
525 for (b, &(c, d)) in out.chunks_exact_mut(4).zip(events) {
526 let c = c.to_le_bytes();
527 b.copy_from_slice(&[c[0], c[1], d as u8, 0]);
528 }
529 }
530 Ok(len as i32)
531 },
532 )?;
533
534 linker.func_wrap(
535 "gasm",
536 "pointer",
537 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
538 let mem = memory(&caller)?;
539 let (data, host) = mem.data_and_store_mut(&mut caller);
540 let Some(p) = &host.input.pointer else { return Ok(-1) };
541 if cap as usize >= POINTER_BYTES {
542 let b = pointer_bytes(p, (host.width, host.height), host.aspect);
543 guest_slice_mut(data, dst, POINTER_BYTES as u64)?.copy_from_slice(&b);
544 }
545 Ok(POINTER_BYTES as i32)
546 },
547 )?;
548
549 linker.func_wrap(
550 "gasm",
551 "gamepad",
552 |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
553 let mem = memory(&caller)?;
554 let (data, host) = mem.data_and_store_mut(&mut caller);
555 let (Some(pads), true) = (&host.input.gamepads, slot < 4) else { return Ok(-1) };
556 if cap as usize >= GAMEPAD_BYTES {
557 let b = gamepad_bytes(&pads[slot as usize]);
558 guest_slice_mut(data, dst, GAMEPAD_BYTES as u64)?.copy_from_slice(&b);
559 }
560 Ok(GAMEPAD_BYTES as i32)
561 },
562 )?;
563
564 linker.func_wrap(
565 "gasm",
566 "gamepad_name",
567 |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
568 let mem = memory(&caller)?;
569 let (data, host) = mem.data_and_store_mut(&mut caller);
570 let Some(g) = host.input.gamepads.as_ref().and_then(|p| p.get(slot as usize)).filter(|g| g.connected) else { return Ok(-1) };
571 copy_if_fits(data, dst, cap, g.name.as_bytes())
572 },
573 )?;
574
575 linker.func_wrap(
576 "gasm",
577 "param",
578 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
579 let name = guest_str(&caller, ptr, len)?;
580 let mem = memory(&caller)?;
581 let (data, host) = mem.data_and_store_mut(&mut caller);
582 let Some(v) = host.params.get(&name) else { return Ok(-1) };
583 copy_if_fits(data, dst, cap, v.as_bytes())
584 },
585 )?;
586
587 fn read_asset(caller: &mut Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32) -> wasmtime::Result<i32> {
590 let name = guest_str(caller, ptr, len)?;
591 let mem = memory(caller)?;
592 let (data, host) = mem.data_and_store_mut(caller);
593 let Some(asset) = host.assets.get(&name) else { return Ok(-1) };
594 let n = asset.size().saturating_sub(offset).min(cap as u64);
595 let out = guest_slice_mut(data, dst, n)?;
596 Ok(asset.read_at(offset, out) as i32)
597 }
598
599 linker.func_wrap(
600 "gasm",
601 "asset_read_at",
602 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
603 read_asset(&mut caller, ptr, len, offset as u64, dst, cap)
604 },
605 )?;
606
607 linker.func_wrap(
608 "gasm",
609 "asset_read_at64",
610 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32| -> wasmtime::Result<i32> {
611 read_asset(&mut caller, ptr, len, offset, dst, cap)
612 },
613 )?;
614
615 linker.func_wrap(
616 "gasm",
617 "asset_read",
618 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
619 read_asset(&mut caller, ptr, len, 0, dst, cap)
620 },
621 )?;
622
623 linker.func_wrap("gasm", "asset_count", |caller: Caller<'_, Host>| -> u32 { caller.data().assets.names().len() as u32 })?;
624
625 linker.func_wrap(
626 "gasm",
627 "asset_name",
628 |mut caller: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
629 let mem = memory(&caller)?;
630 let (data, host) = mem.data_and_store_mut(&mut caller);
631 let Some(n) = host.assets.names().get(index as usize) else { return Ok(-1) };
632 copy_if_fits(data, dst, cap, n.as_bytes())
633 },
634 )?;
635
636 linker.func_wrap(
637 "gasm",
638 "asset_size",
639 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
640 let name = guest_str(&caller, ptr, len)?;
641 Ok(match caller.data().assets.size(&name) {
642 None => -1,
643 Some(n) if n > i32::MAX as u64 => -2,
644 Some(n) => n as i32,
645 })
646 },
647 )?;
648
649 linker.func_wrap(
650 "gasm",
651 "asset_size64",
652 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i64> {
653 let name = guest_str(&caller, ptr, len)?;
654 Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i64))
655 },
656 )?;
657 Ok(())
658}
659
660fn trap(e: String) -> wasmtime::Error {
661 format_err!("{e}")
662}
663
664fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
665 const M: &str = "gasm:gfx";
666 linker.func_wrap(M, "width", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().0 })?;
667 linker.func_wrap(M, "height", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().1 })?;
668 linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
669 let src = guest_str(&c, ptr, len)?;
670 c.data_mut().gfx.create_shader(&src).map_err(trap)
671 })?;
672 linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
673 c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
674 })?;
675 linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
676 let json = guest_str(&c, ptr, len)?;
677 c.data_mut().gfx.create_pipeline(&json).map_err(trap)
678 })?;
679 linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
680 let json = guest_str(&c, ptr, len)?;
681 c.data_mut().gfx.create_bind_group(&json).map_err(trap)
682 })?;
683 linker.func_wrap(
684 M,
685 "write_buffer",
686 |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
687 let mem = memory(&c)?;
688 let (data, host) = mem.data_and_store_mut(&mut c);
689 let bytes = guest_slice(data, ptr, len as u64)?;
690 host.gfx.write_buffer(buf, offset, bytes).map_err(trap)?;
691 if host.hashing {
692 host.video_hash.update(bytes);
693 }
694 Ok(())
695 },
696 )?;
697 linker.func_wrap(M, "create_bind_group_layout", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
698 let json = guest_str(&c, ptr, len)?;
699 c.data_mut().gfx.create_bind_group_layout(&json).map_err(trap)
700 })?;
701 linker.func_wrap(M, "create_texture", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
702 let json = guest_str(&c, ptr, len)?;
703 c.data_mut().gfx.create_texture(&json).map_err(trap)
704 })?;
705 linker.func_wrap(M, "create_sampler", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
706 let json = guest_str(&c, ptr, len)?;
707 c.data_mut().gfx.create_sampler(&json).map_err(trap)
708 })?;
709 linker.func_wrap(
710 M,
711 "write_texture",
712 |mut c: Caller<'_, Host>, tex: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
713 let mem = memory(&c)?;
714 let (data, host) = mem.data_and_store_mut(&mut c);
715 let bytes = guest_slice(data, ptr, len as u64)?;
716 host.gfx.write_texture(tex, mip, x, y, w, h, bytes).map_err(trap)?;
717 if host.hashing {
718 for v in [tex, mip, x, y, w, h] {
720 host.video_hash.update(&v.to_le_bytes());
721 }
722 host.video_hash.update(bytes);
723 }
724 Ok(())
725 },
726 )?;
727 linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
728 let h = c.data_mut();
729 let show = h.show_frame;
730 h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
731 })?;
732 linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
733 c.data_mut().gfx.set_pipeline(p).map_err(trap)
734 })?;
735 linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
736 c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
737 })?;
738 linker.func_wrap(
739 M,
740 "set_bind_group_offsets",
741 |mut c: Caller<'_, Host>, i: u32, bg: u32, ptr: u32, count: u32| -> wasmtime::Result<()> {
742 let mem = memory(&c)?;
743 let (data, host) = mem.data_and_store_mut(&mut c);
744 let bytes = guest_slice(data, ptr, count as u64 * 4)?;
745 let mut offsets = [0u32; 32];
746 if count as usize > offsets.len() {
747 bail!("gfx.set_bind_group_offsets: {count} offsets (a bind group has at most {})", offsets.len());
748 }
749 for (o, b) in offsets.iter_mut().zip(bytes.chunks_exact(4)) {
750 *o = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
751 }
752 host.gfx.set_bind_group_offsets(i, bg, &offsets[..count as usize]).map_err(trap)
753 },
754 )?;
755 linker.func_wrap(
756 M,
757 "set_viewport",
758 |mut c: Caller<'_, Host>, x: f32, y: f32, w: f32, h: f32, min: f32, max: f32| -> wasmtime::Result<()> {
759 c.data_mut().gfx.set_viewport(x, y, w, h, min, max).map_err(trap)
760 },
761 )?;
762 linker.func_wrap(M, "set_scissor_rect", |mut c: Caller<'_, Host>, x: u32, y: u32, w: u32, h: u32| -> wasmtime::Result<()> {
763 c.data_mut().gfx.set_scissor_rect(x, y, w, h).map_err(trap)
764 })?;
765 linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
766 c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
767 })?;
768 linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
769 c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
770 })?;
771 linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| -> wasmtime::Result<()> {
772 c.data_mut().gfx.draw(vc, ic, fv, fi).map_err(trap)
773 })?;
774 linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| -> wasmtime::Result<()> {
775 c.data_mut().gfx.draw_indexed(ic, n, first, base, fi).map_err(trap)
776 })?;
777 linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
778 let h = c.data_mut();
779 h.frames_presented += 1;
780 h.gfx.end_frame().map_err(trap)
781 })?;
782 linker.func_wrap(M, "destroy", |mut c: Caller<'_, Host>, handle: u32| -> wasmtime::Result<()> {
783 c.data_mut().gfx.destroy(handle).map_err(trap)
784 })?;
785 Ok(())
786}
787
788fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
789 const M: &str = "gasm:net";
790 linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
791 let url = guest_str(&c, ptr, len)?;
792 Ok(c.data_mut().net.open(&url))
793 })?;
794 linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> { c.data().net.state(h).map_err(trap) })?;
795 linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
796 let mem = memory(&c)?;
797 let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
798 if msg.is_empty() {
799 c.data().net.state(h).map_err(trap)?; return Ok(-1);
801 }
802 c.data_mut().net.send(h, msg).map_err(trap)
803 })?;
804 linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
805 let mem = memory(&c)?;
806 let (data, host) = mem.data_and_store_mut(&mut c);
807 let msg = match host.net.peek(h).map_err(trap)? {
808 Ok(m) => m,
809 Err(code) => return Ok(code),
810 };
811 let n = copy_if_fits(data, dst, cap, msg)?;
812 if n as usize <= cap as usize {
813 host.net.pop(h);
814 }
815 Ok(n)
816 })?;
817 linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().net.close(h).map_err(trap) })?;
818 Ok(())
819}
820
821fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
822 const M: &str = "gasm:storage";
823 linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
824 let key = guest_str(&c, kp, kl)?;
825 let mem = memory(&c)?;
826 let (data, host) = mem.data_and_store_mut(&mut c);
827 let Some(v) = host.storage.get(&key) else { return Ok(-1) };
828 copy_if_fits(data, dst, cap, v)
829 })?;
830 linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
831 let key = guest_str(&c, kp, kl)?;
832 let mem = memory(&c)?;
833 let (data, host) = mem.data_and_store_mut(&mut c);
834 let value = guest_slice(data, vp, vl as u64)?;
835 Ok(match host.storage.set(&key, value) {
836 Ok(()) => 0,
837 Err(e) => {
838 eprintln!("[gasm] storage: {key}: {e}");
839 e.code()
840 }
841 })
842 })?;
843 linker.func_wrap(M, "count", |mut c: Caller<'_, Host>| -> u32 { c.data_mut().storage.keys().len() as u32 })?;
844 linker.func_wrap(M, "key", |mut c: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
845 let mem = memory(&c)?;
846 let (data, host) = mem.data_and_store_mut(&mut c);
847 let Some(k) = host.storage.keys().get(index as usize) else { return Ok(-1) };
848 copy_if_fits(data, dst, cap, k.as_bytes())
849 })?;
850 linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
851 let key = guest_str(&c, kp, kl)?;
852 Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
853 })?;
854 Ok(())
855}
856
857#[derive(Debug)]
859pub enum Stop {
860 Exit(i32),
862 Trap(String),
863}
864
865impl std::fmt::Display for Stop {
866 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
867 match self {
868 Stop::Exit(code) => write!(f, "guest exited with code {code}"),
869 Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
870 }
871 }
872}
873
874impl std::error::Error for Stop {}
875
876fn classify(e: wasmtime::Error) -> Stop {
877 if let Some(exit) = e.downcast_ref::<wasi::Exit>() {
878 return Stop::Exit(exit.0);
879 }
880 if e.downcast_ref::<switching::Quit>().is_some() {
881 return Stop::Exit(0);
882 }
883 if e.downcast_ref::<wasmtime::Trap>() == Some(&wasmtime::Trap::Interrupt) {
884 return Stop::Trap(format!("a guest call ran longer than the time limit (stuck in a loop?); see --call-timeout\n{e:?}"));
885 }
886 Stop::Trap(format!("{e:?}"))
887}
888
889const TICKS_PER_SEC: u64 = 10;
891
892pub fn engine() -> &'static Engine {
897 static ENGINE: OnceLock<Engine> = OnceLock::new();
898 ENGINE.get_or_init(new_engine)
899}
900
901fn new_engine() -> Engine {
902 let mut config = Config::new();
903 config.epoch_interruption(true);
904 config.async_stack_size(4 << 20);
906 let engine = Engine::new(&config).expect("wasmtime engine");
907 let ticker = engine.clone();
908 std::thread::Builder::new()
909 .name("gasm-epoch".into())
910 .spawn(move || {
911 loop {
912 std::thread::sleep(Duration::from_millis(1000 / TICKS_PER_SEC));
913 ticker.increment_epoch();
914 }
915 })
916 .expect("epoch thread");
917 engine
918}
919
920pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
922 engine().precompile_module(wasm)
923}
924
925#[derive(Clone, Copy)]
927pub struct LoadOptions {
928 pub allow_precompiled: bool,
931 pub call_timeout: Option<Duration>,
933 pub stack_switching: bool,
936}
937
938impl Default for LoadOptions {
939 fn default() -> Self {
940 LoadOptions { allow_precompiled: false, call_timeout: Some(Duration::from_secs(30)), stack_switching: true }
941 }
942}
943
944type Running = Pin<Box<dyn Future<Output = (Store<Host>, wasmtime::Result<i32>)>>>;
946
947pub struct Game {
951 store: Option<Store<Host>>,
953 frame: TypedFunc<(), ()>,
954 exit: Option<TypedFunc<(), ()>>,
955 deadline: u64,
956 run: Option<TypedFunc<(), i32>>,
958 running: Option<Running>,
959 exchange: Option<Arc<Exchange>>,
960 ended: bool,
962 gl: bool,
964}
965
966impl Game {
967 pub fn load(wasm: &[u8], host: Host, opts: LoadOptions) -> Result<Game, Stop> {
968 Self::load_inner(wasm, host, opts).map_err(classify)
969 }
970
971 fn load_inner(wasm: &[u8], host: Host, opts: LoadOptions) -> wasmtime::Result<Game> {
972 let module = if wasm.starts_with(b"\0asm") {
973 Module::new(engine(), wasm)?
974 } else if opts.allow_precompiled {
975 unsafe { Module::deserialize(engine(), wasm)? }
978 } else {
979 bail!("not a wasm module (a precompiled .cwasm runs as native code: pass --allow-precompiled if you made it yourself)");
980 };
981 let gl = module.imports().any(|i| i.module() == "gasm:gl");
982 if gl && module.imports().any(|i| i.module() == "gasm:gfx") {
983 bail!("a module imports gasm:gfx or gasm:gl, not both");
984 }
985 let has_run = module.get_export("gasm_run").is_some();
987 let switching = has_run && opts.stack_switching;
988 if has_run && !switching && module.imports().any(|i| i.module() == "asyncify") {
989 bail!("this module is a run build (no Asyncify): it needs stack switching (gasm_run); use the game's Asyncify build");
991 }
992 let engine = engine();
993 let mut linker: Linker<Host> = Linker::new(engine);
994 add_gasm_imports(&mut linker)?;
995 if switching {
996 switching::add_yield_async(&mut linker)?;
997 } else {
998 switching::add_yield_sync(&mut linker)?;
999 }
1000 add_gfx_imports(&mut linker)?;
1001 crate::gl::add_gl_imports(&mut linker)?;
1002 add_net_imports(&mut linker)?;
1003 add_storage_imports(&mut linker)?;
1004 let mut store = Store::new(engine, host);
1005 let mut provided = HashSet::new();
1007 for (m, f, _) in linker.iter(&mut store) {
1008 provided.insert(m.to_owned());
1009 provided.insert(format!("{m}.{f}"));
1010 }
1011 for f in wasi::IMPLEMENTED {
1012 provided.insert(format!("{}.{f}", wasi::MODULE));
1013 }
1014 provided.insert(wasi::MODULE.to_owned());
1015 store.data_mut().provided = provided;
1016 wasi::add_to_linker(&mut linker, &module)?;
1017 linker.define_unknown_imports_as_traps(&module)?;
1020
1021 let deadline = opts.call_timeout.map_or(u64::MAX / 2, |t| (t.as_millis() as u64 * TICKS_PER_SEC).div_ceil(1000).max(1));
1022 store.epoch_deadline_trap();
1023 store.set_epoch_deadline(deadline);
1024 let exchange = switching.then(|| Exchange::new(deadline));
1025 store.data_mut().run = exchange.clone();
1026 let instance: Instance = if switching {
1027 switching::block_on(linker.instantiate_async(&mut store, &module))??
1028 } else {
1029 linker.instantiate(&mut store, &module)?
1030 };
1031 let memory = instance
1032 .get_memory(&mut store, "memory")
1033 .ok_or_else(|| format_err!("guest does not export `memory`"))?;
1034 store.data_mut().memory = Some(memory);
1035
1036 fn call<R: wasmtime::WasmResults + Send + Sync>(store: &mut Store<Host>, f: TypedFunc<(), R>, switching: bool) -> wasmtime::Result<R> {
1038 if switching { switching::block_on(f.call_async(store, ()))? } else { f.call(store, ()) }
1039 }
1040 if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
1041 store.set_epoch_deadline(deadline);
1042 call(&mut store, init, switching)?;
1043 }
1044 let version = instance.get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?;
1045 let version = call(&mut store, version, switching)?;
1046 if version != ABI_VERSION {
1047 bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
1048 }
1049 store.set_epoch_deadline(deadline);
1050 let init = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?;
1051 let rc = call(&mut store, init, switching)?;
1052 if rc != 0 {
1053 bail!("gasm_init failed with code {rc}");
1054 }
1055 let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
1056 let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
1057 let run = if switching { Some(instance.get_typed_func::<(), i32>(&mut store, "gasm_run")?) } else { None };
1058 Ok(Game { store: Some(store), frame, exit, deadline, run, running: None, exchange, ended: false, gl })
1059 }
1060
1061 pub fn uses_gl(&self) -> bool {
1063 self.gl
1064 }
1065
1066 pub fn switching(&self) -> bool {
1068 self.run.is_some()
1069 }
1070
1071 pub fn frame(&mut self) -> Result<(), Stop> {
1073 if self.ended {
1074 return Err(Stop::Trap("the guest is not running".into()));
1075 }
1076 let Some(run) = self.run.clone() else {
1077 let store = self.store.as_mut().expect("store");
1078 store.set_epoch_deadline(self.deadline);
1079 let r = self.frame.call(&mut *store, ()).map_err(classify);
1080 store.data_mut().frame_index += 1;
1081 self.ended = r.is_err();
1082 return r;
1083 };
1084 match &self.exchange {
1085 Some(ex) if self.running.is_some() => ex.set(Cmd::Resume),
1086 _ => {
1087 let mut store = self.store.take().expect("store");
1089 store.set_epoch_deadline(self.deadline);
1090 self.running = Some(Box::pin(async move {
1091 let r = run.call_async(&mut store, ()).await;
1092 (store, r)
1093 }));
1094 }
1095 }
1096 self.poll()
1097 }
1098
1099 fn poll(&mut self) -> Result<(), Stop> {
1101 let Some(running) = self.running.as_mut() else { return Ok(()) };
1102 match switching::poll_once(running.as_mut()) {
1103 Poll::Pending => Ok(()),
1104 Poll::Ready((store, r)) => {
1105 self.store = Some(store);
1106 self.running = None;
1107 self.ended = true;
1108 Err(match r {
1109 Ok(code) => Stop::Exit(code),
1110 Err(e) => classify(e),
1111 })
1112 }
1113 }
1114 }
1115
1116 pub fn exit(&mut self) {
1119 if self.running.is_some() {
1120 if let Some(ex) = &self.exchange {
1121 ex.set(Cmd::Exit);
1122 }
1123 if let Err(Stop::Trap(t)) = self.poll() {
1124 eprintln!("[gasm] gasm_exit trapped: {t}");
1125 }
1126 if self.running.is_some() {
1127 self.running = None;
1129 }
1130 self.exit = None;
1131 return;
1132 }
1133 if let (Some(f), Some(store)) = (self.exit.take(), self.store.as_mut()) {
1134 store.set_epoch_deadline(self.deadline);
1135 let r = if self.run.is_some() { switching::block_on(f.call_async(&mut *store, ())).and_then(|r| r) } else { f.call(&mut *store, ()) };
1136 if let Err(e) = r {
1137 if let Stop::Trap(t) = classify(e) {
1138 eprintln!("[gasm] gasm_exit trapped: {t}");
1139 }
1140 }
1141 }
1142 }
1143
1144 pub fn with_host<R>(&mut self, f: impl FnOnce(&mut Host) -> R) -> R {
1148 if let Some(store) = &mut self.store {
1149 return f(store.data_mut());
1150 }
1151 let (Some(running), Some(ex)) = (self.running.as_mut(), &self.exchange) else {
1152 panic!("with_host: the guest's store is gone (gasm_exit yielded)");
1153 };
1154 let mut f = Some(f);
1155 let mut out = None;
1156 let mut job = |h: &mut Host| out = Some((f.take().expect("job runs once"))(h));
1157 let ptr: *mut (dyn FnMut(&mut Host) + '_) = &mut job;
1158 let ptr: *mut (dyn FnMut(&mut Host) + 'static) = unsafe { std::mem::transmute(ptr) };
1161 ex.set(Cmd::Job(Job(ptr)));
1162 let polled = switching::poll_once(running.as_mut());
1163 ex.set(Cmd::None);
1164 match polled {
1165 Poll::Pending => {}
1166 Poll::Ready(_) => unreachable!("yield_frame stays suspended after a job"),
1167 }
1168 out.expect("yield_frame ran the job")
1169 }
1170
1171 pub fn host(&self) -> &Host {
1175 self.store.as_ref().expect("Game::host: a gasm_run guest is running; use Game::with_host").data()
1176 }
1177
1178 pub fn host_mut(&mut self) -> &mut Host {
1179 self.store.as_mut().expect("Game::host_mut: a gasm_run guest is running; use Game::with_host").data_mut()
1180 }
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use super::*;
1186
1187 #[test]
1188 fn titles_are_cleaned_and_cut() {
1189 assert_eq!(clean_title("A\u{7}B\u{202e}C\u{2069}").as_deref(), Some("ABC"));
1190 assert_eq!(clean_title("\n\t"), None);
1191 assert_eq!(clean_title(&"é".repeat(200)).map(|t| t.len()), Some(256));
1193 }
1194
1195 const RUN_GUEST: &str = r#"(module
1196 (import "gasm" "yield_frame" (func $yield))
1197 (memory (export "memory") 1)
1198 (global $n (mut i32) (i32.const 0))
1199 (func (export "gasm_abi_version") (result i32) i32.const 0)
1200 (func (export "gasm_init") (result i32) i32.const 0)
1201 (func (export "gasm_frame") call $yield)
1202 (func (export "gasm_exit") (global.set $n (i32.const 100)))
1203 (func (export "frames") (result i32) global.get $n)
1204 (func (export "gasm_run") (result i32)
1205 (loop $l
1206 (global.set $n (i32.add (global.get $n) (i32.const 1)))
1207 call $yield
1208 (br_if $l (i32.lt_u (global.get $n) (i32.const 3))))
1209 i32.const 7))"#;
1210
1211 fn host() -> Host {
1212 Host::new(Assets::new(), HashMap::new(), None, Gfx::null(), Net::new(false), Storage::memory())
1213 }
1214
1215 #[test]
1216 fn gasm_run_guests_suspend_between_frames() {
1217 let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1218 assert!(g.switching());
1219 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);
1223 assert!(g.frame().is_ok()); assert!(g.frame().is_ok()); assert!(matches!(g.frame(), Err(Stop::Exit(7))));
1227 assert!(g.frame().is_err(), "never called again");
1228 }
1229
1230 #[test]
1231 fn exit_runs_on_top_of_a_suspended_run() {
1232 let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), LoadOptions::default()).unwrap();
1233 g.frame().unwrap();
1234 g.exit();
1235 assert!(g.store.is_some());
1237 }
1238
1239 #[test]
1240 fn yield_frame_outside_gasm_run_traps() {
1241 let opts = LoadOptions { stack_switching: false, ..LoadOptions::default() };
1242 let mut g = Game::load(&wat::parse_str(RUN_GUEST).unwrap(), host(), opts).unwrap();
1243 assert!(!g.switching());
1244 match g.frame() {
1245 Err(Stop::Trap(t)) => assert!(t.contains("only inside gasm_run"), "{t}"),
1246 other => panic!("expected a trap, got {:?}", other.err()),
1247 }
1248 }
1249
1250 #[test]
1251 fn custom_sections_are_found() {
1252 let mut wasm = b"\0asm\x01\0\0\0\x01\x01\0".to_vec();
1254 wasm.extend_from_slice(&[0, 14, 10]);
1255 wasm.extend_from_slice(b"gasm.titleHi\0");
1256 assert_eq!(custom_section(&wasm, "gasm.title"), Some(&b"Hi\0"[..]));
1257 assert_eq!(static_title(&wasm).as_deref(), Some("Hi"));
1258 assert_eq!(custom_section(&wasm, "other"), None);
1259 assert_eq!(custom_section(&wasm[..wasm.len() - 2], "gasm.title"), None); assert_eq!(custom_section(b"not wasm", "gasm.title"), None);
1261 }
1262}