1use std::collections::{HashMap, HashSet};
6use std::sync::OnceLock;
7use std::time::{Duration, Instant};
8
9use wasmtime::{Caller, Config, Engine, Instance, Linker, Memory, Module, Store, TypedFunc, bail, format_err};
10
11use crate::assets::Assets;
12use crate::audio::AudioOut;
13use crate::gfx::Gfx;
14use crate::net::Net;
15use crate::present::letterbox;
16use crate::storage::Storage;
17use crate::wasi::{self, Splitmix};
18
19pub const ABI_VERSION: i32 = 0;
20
21#[derive(Clone, Copy)]
23pub struct Fnv(pub u32);
24
25impl Default for Fnv {
26 fn default() -> Self {
27 Fnv(0x811c_9dc5)
28 }
29}
30
31impl Fnv {
32 pub fn new() -> Self {
33 Self::default()
34 }
35 pub fn update(&mut self, bytes: &[u8]) {
36 let mut h = self.0;
37 for &b in bytes {
38 h ^= b as u32;
39 h = h.wrapping_mul(0x0100_0193);
40 }
41 self.0 = h;
42 }
43}
44
45#[derive(Clone, Copy)]
49pub struct VirtualClock {
50 base_ms: f64,
51 base_frame: u64,
52 rate: f64,
53}
54
55impl Default for VirtualClock {
56 fn default() -> Self {
57 VirtualClock { base_ms: 0.0, base_frame: 0, rate: 60.0 }
58 }
59}
60
61impl VirtualClock {
62 pub fn at(&mut self, frame: u64, rate: f64) -> f64 {
64 if rate != self.rate {
65 self.base_ms += (frame - self.base_frame) as f64 * 1000.0 / self.rate;
66 self.base_frame = frame;
67 self.rate = rate;
68 }
69 self.base_ms + (frame - self.base_frame) as f64 * 1000.0 / self.rate
70 }
71}
72
73#[derive(Default, Clone)]
76pub struct RawInput {
77 pub keys: Option<[u8; KEY_STATE_BYTES]>,
79 pub key_events: Vec<(u16, bool)>,
81 pub pointer: Option<Pointer>,
82 pub gamepads: Option<[Gamepad; 4]>,
83}
84
85pub const KEY_STATE_BYTES: usize = 32;
86pub const POINTER_BYTES: usize = 48;
87pub const GAMEPAD_BYTES: usize = 204;
88pub const GAMEPAD_BUTTONS: usize = 32;
89pub const GAMEPAD_AXES: usize = 16;
90
91#[derive(Default, Clone, Copy)]
92pub struct Pointer {
93 pub x: f32,
95 pub y: f32,
96 pub dx: f32,
97 pub dy: f32,
98 pub wheel_x: f32,
99 pub wheel_y: f32,
100 pub buttons: u32,
101 pub pressed: u32,
102 pub released: u32,
103 pub flags: u32,
105 pub drawable: (f32, f32),
107 pub integer_scale: bool,
109}
110
111#[derive(Default, Clone)]
112pub struct Gamepad {
113 pub connected: bool,
114 pub standard: bool,
115 pub buttons: Vec<f32>,
116 pub axes: Vec<f32>,
117 pub name: String,
118}
119
120pub fn frame_position(x: f32, y: f32, drawable: (f32, f32), frame: (usize, usize), integer_scale: bool, aspect: Option<(u32, u32)>) -> (f32, f32) {
123 if frame.0 == 0 || frame.1 == 0 || drawable.0 <= 0.0 || drawable.1 <= 0.0 {
124 return (x, y);
125 }
126 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);
127 (((x as f64 - ox) / sx) as f32, ((y as f64 - oy) / sy) as f32)
128}
129
130pub const TITLE_MAX_BYTES: usize = 256;
132
133pub fn clean_title(text: &str) -> Option<String> {
138 let mut out = String::new();
139 for c in text.chars().filter(|&c| !c.is_control() && !matches!(c, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')) {
140 if out.len() + c.len_utf8() > TITLE_MAX_BYTES {
141 break;
142 }
143 out.push(c);
144 }
145 (!out.is_empty()).then_some(out)
146}
147
148pub fn custom_section<'a>(wasm: &'a [u8], name: &str) -> Option<&'a [u8]> {
151 fn leb(b: &[u8], at: &mut usize) -> Option<usize> {
152 let (mut v, mut shift) = (0usize, 0);
153 loop {
154 let byte = *b.get(*at)?;
155 *at += 1;
156 v |= ((byte & 0x7f) as usize).checked_shl(shift)?;
157 if byte & 0x80 == 0 {
158 return Some(v);
159 }
160 shift += 7;
161 if shift > 28 {
162 return None;
163 }
164 }
165 }
166 if wasm.get(..4)? != b"\0asm" {
167 return None;
168 }
169 let mut at = 8;
170 while at < wasm.len() {
171 let id = wasm[at];
172 at += 1;
173 let size = leb(wasm, &mut at)?;
174 let body = wasm.get(at..at.checked_add(size)?)?;
175 at += size;
176 if id == 0 {
177 let mut p = 0;
178 let n = leb(body, &mut p)?;
179 if body.get(p..p.checked_add(n)?)? == name.as_bytes() {
180 return body.get(p + n..);
181 }
182 }
183 }
184 None
185}
186
187pub fn static_title(wasm: &[u8]) -> Option<String> {
190 clean_title(std::str::from_utf8(custom_section(wasm, "gasm.title")?).ok()?)
191}
192
193pub struct Host {
194 memory: Option<Memory>,
195 start: Instant,
196 pub virtual_time_ms: Option<f64>,
199 pub random: Option<Splitmix>,
201 pub frame_rate: f64,
202 pub pads: [u32; 4],
203 pub text: Option<String>,
206 pub input: RawInput,
207 pub input_mode: u32,
209 pub assets: Assets,
210 pub params: HashMap<String, String>,
211 pub audio: Option<Box<dyn AudioOut>>,
212 pub gfx: Gfx,
213 pub net: Net,
214 pub storage: Storage,
215 pub show_frame: bool,
217 audio_rate: u32,
218 audio_channels: u32,
219 provided: HashSet<String>,
221 pub rgba: Vec<u8>,
223 pub width: usize,
224 pub height: usize,
225 pub frames_presented: u64,
226 pub aspect: Option<(u32, u32)>,
228 pub title: Option<String>,
230 pub title_changed: bool,
232 pub hashing: bool,
233 pub video_hash: Fnv,
234 pub audio_hash: Fnv,
235 pub audio_frames: u64,
236}
237
238impl Host {
239 pub fn new(
240 assets: impl Into<Assets>,
241 params: HashMap<String, String>,
242 audio: Option<Box<dyn AudioOut>>,
243 gfx: Gfx,
244 net: Net,
245 storage: Storage,
246 ) -> Self {
247 Host {
248 memory: None,
249 start: Instant::now(),
250 virtual_time_ms: None,
251 random: None,
252 frame_rate: 60.0,
253 pads: [0; 4],
254 text: None,
255 input: RawInput::default(),
256 input_mode: 0,
257 assets: assets.into(),
258 params,
259 audio,
260 gfx,
261 net,
262 storage,
263 show_frame: true,
264 audio_rate: 44100,
265 audio_channels: 2,
266 provided: HashSet::new(),
267 title: None,
268 title_changed: false,
269 aspect: None,
270 rgba: Vec::new(),
271 width: 0,
272 height: 0,
273 frames_presented: 0,
274 hashing: true,
275 video_hash: Fnv::new(),
276 audio_hash: Fnv::new(),
277 audio_frames: 0,
278 }
279 }
280
281 pub fn set_reproducible(&mut self) {
284 self.virtual_time_ms = Some(0.0);
285 self.random = Some(Splitmix(0));
286 }
287
288 pub(crate) fn started(&self) -> Instant {
289 self.start
290 }
291}
292
293pub(crate) fn guest_slice(mem: &[u8], ptr: u32, len: u64) -> wasmtime::Result<&[u8]> {
295 let start = ptr as u64;
296 let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
297 if end > mem.len() as u64 {
298 bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
299 }
300 Ok(&mem[start as usize..end as usize])
301}
302
303pub(crate) fn guest_slice_mut(mem: &mut [u8], ptr: u32, len: u64) -> wasmtime::Result<&mut [u8]> {
305 let start = ptr as u64;
306 let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
307 if end > mem.len() as u64 {
308 bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
309 }
310 Ok(&mut mem[start as usize..end as usize])
311}
312
313fn copy_if_fits(mem: &mut [u8], dst: u32, cap: u32, src: &[u8]) -> wasmtime::Result<i32> {
316 if src.len() <= cap as usize {
317 guest_slice_mut(mem, dst, src.len() as u64)?.copy_from_slice(src);
318 }
319 Ok(src.len() as i32)
320}
321
322pub(crate) fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<Memory> {
323 caller.data().memory.ok_or_else(|| format_err!("guest memory not yet available"))
324}
325
326fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
327 let mem = memory(caller)?;
328 let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
329 std::str::from_utf8(bytes).map(str::to_owned).map_err(|e| format_err!("string argument is not UTF-8: {e}"))
330}
331
332fn pointer_bytes(p: &Pointer, frame: (usize, usize), aspect: Option<(u32, u32)>) -> [u8; POINTER_BYTES] {
334 let (fx, fy) = frame_position(p.x, p.y, p.drawable, frame, p.integer_scale, aspect);
335 let mut b = [0u8; POINTER_BYTES];
336 for (i, v) in [p.x, p.y, fx, fy, p.dx, p.dy, p.wheel_x, p.wheel_y].into_iter().enumerate() {
337 b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
338 }
339 for (i, v) in [p.buttons, p.pressed, p.released, p.flags].into_iter().enumerate() {
340 b[32 + i * 4..36 + i * 4].copy_from_slice(&v.to_le_bytes());
341 }
342 b
343}
344
345fn gamepad_bytes(g: &Gamepad) -> [u8; GAMEPAD_BYTES] {
347 let mut b = [0u8; GAMEPAD_BYTES];
348 let nb = g.buttons.len().min(GAMEPAD_BUTTONS);
349 let na = g.axes.len().min(GAMEPAD_AXES);
350 let flags = if g.connected { 1 | if g.standard { 2 } else { 0 } } else { 0u32 };
351 for (i, v) in [flags, nb as u32, na as u32].into_iter().enumerate() {
352 b[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
353 }
354 for (i, v) in g.buttons.iter().take(nb).enumerate() {
355 b[12 + i * 4..16 + i * 4].copy_from_slice(&v.to_le_bytes());
356 }
357 for (i, v) in g.axes.iter().take(na).enumerate() {
358 b[140 + i * 4..144 + i * 4].copy_from_slice(&v.to_le_bytes());
359 }
360 b
361}
362
363fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
364 linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
365 let mem = memory(&caller)?;
366 let msg = guest_slice(mem.data(&caller), ptr, len as u64)?;
367 eprintln!("[guest] {}", String::from_utf8_lossy(msg));
368 Ok(())
369 })?;
370
371 linker.func_wrap("gasm", "has", |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
372 let name = guest_str(&caller, ptr, len)?;
373 Ok(caller.data().provided.contains(&name) as i32)
374 })?;
375
376 linker.func_wrap("gasm", "video_set_aspect", |mut caller: Caller<'_, Host>, num: u32, den: u32| -> wasmtime::Result<()> {
377 let aspect = match (num, den) {
378 (0, 0) => None,
379 (1..=65535, 1..=65535) if num as u64 * 8 >= den as u64 && den as u64 * 8 >= num as u64 => Some((num, den)),
380 _ => bail!("video_set_aspect: invalid ratio {num}:{den}"),
381 };
382 caller.data_mut().aspect = aspect;
383 Ok(())
384 })?;
385
386 linker.func_wrap("gasm", "set_title", |mut caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<()> {
387 let title = clean_title(&guest_str(&caller, ptr, len)?);
388 let h = caller.data_mut();
389 if h.title != title {
390 h.title = title;
391 h.title_changed = true;
392 }
393 Ok(())
394 })?;
395
396 linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
397 let h = caller.data();
398 h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
399 })?;
400
401 linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
402 if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
403 caller.data_mut().frame_rate = hz;
404 }
405 })?;
406
407 linker.func_wrap(
408 "gasm",
409 "video_present",
410 |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
411 let (w, h, stride) = (w as usize, h as usize, stride as usize);
412 if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
413 bail!("video_present: bad geometry {w}x{h} stride {stride}");
414 }
415 let mem = memory(&caller)?;
416 let (data, host) = mem.data_and_store_mut(&mut caller);
417 let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
418 let row = w * 4;
419 host.rgba.resize(row * h, 0);
420 if stride == row {
421 host.rgba.copy_from_slice(src);
422 } else {
423 for (y, dst) in host.rgba.chunks_exact_mut(row).enumerate() {
424 dst.copy_from_slice(&src[y * stride..y * stride + row]);
425 }
426 }
427 if host.hashing {
428 host.video_hash.update(&host.rgba);
429 }
430 host.width = w;
431 host.height = h;
432 host.frames_presented += 1;
433 Ok(())
434 },
435 )?;
436
437 linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
438 let h = caller.data_mut();
439 if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
440 h.audio_rate = rate;
441 h.audio_channels = channels;
442 if let Some(a) = &mut h.audio {
443 a.configure(rate, channels);
444 }
445 }
446 })?;
447
448 linker.func_wrap(
449 "gasm",
450 "audio_push",
451 |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
452 let mem = memory(&caller)?;
453 let (data, host) = mem.data_and_store_mut(&mut caller);
454 let len = frames as u64 * host.audio_channels as u64 * 4;
455 let bytes = guest_slice(data, ptr, len)?;
456 if host.hashing {
457 host.audio_hash.update(bytes);
458 }
459 host.audio_frames += frames as u64;
460 if let Some(a) = &mut host.audio {
461 a.push_le_f32(bytes);
462 }
463 Ok(())
464 },
465 )?;
466
467 linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
468 caller.data().pads.get(player as usize).copied().unwrap_or(0)
469 })?;
470
471 linker.func_wrap(
472 "gasm",
473 "text_input",
474 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
475 let mem = memory(&caller)?;
476 let (data, host) = mem.data_and_store_mut(&mut caller);
477 let Some(t) = &host.text else { return Ok(-1) };
478 copy_if_fits(data, dst, cap, t.as_bytes())
479 },
480 )?;
481
482 linker.func_wrap("gasm", "input_mode", |mut caller: Caller<'_, Host>, flags: u32| {
483 caller.data_mut().input_mode = flags & 7;
484 })?;
485
486 linker.func_wrap(
487 "gasm",
488 "key_state",
489 |mut caller: Caller<'_, Host>, dst: u32, len: u32| -> wasmtime::Result<i32> {
490 let mem = memory(&caller)?;
491 let (data, host) = mem.data_and_store_mut(&mut caller);
492 let Some(keys) = host.input.keys else { return Ok(-1) };
493 let n = (len as usize).min(KEY_STATE_BYTES);
494 guest_slice_mut(data, dst, n as u64)?.copy_from_slice(&keys[..n]);
495 Ok(KEY_STATE_BYTES as i32)
496 },
497 )?;
498
499 linker.func_wrap(
500 "gasm",
501 "key_events",
502 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
503 let mem = memory(&caller)?;
504 let (data, host) = mem.data_and_store_mut(&mut caller);
505 if host.input.keys.is_none() {
506 return Ok(-1);
507 }
508 let events = &host.input.key_events;
509 let len = events.len() * 4;
510 if len <= cap as usize {
511 let out = guest_slice_mut(data, dst, len as u64)?;
512 for (b, &(c, d)) in out.chunks_exact_mut(4).zip(events) {
513 let c = c.to_le_bytes();
514 b.copy_from_slice(&[c[0], c[1], d as u8, 0]);
515 }
516 }
517 Ok(len as i32)
518 },
519 )?;
520
521 linker.func_wrap(
522 "gasm",
523 "pointer",
524 |mut caller: Caller<'_, Host>, dst: u32, cap: u32| -> wasmtime::Result<i32> {
525 let mem = memory(&caller)?;
526 let (data, host) = mem.data_and_store_mut(&mut caller);
527 let Some(p) = &host.input.pointer else { return Ok(-1) };
528 if cap as usize >= POINTER_BYTES {
529 let b = pointer_bytes(p, (host.width, host.height), host.aspect);
530 guest_slice_mut(data, dst, POINTER_BYTES as u64)?.copy_from_slice(&b);
531 }
532 Ok(POINTER_BYTES as i32)
533 },
534 )?;
535
536 linker.func_wrap(
537 "gasm",
538 "gamepad",
539 |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
540 let mem = memory(&caller)?;
541 let (data, host) = mem.data_and_store_mut(&mut caller);
542 let (Some(pads), true) = (&host.input.gamepads, slot < 4) else { return Ok(-1) };
543 if cap as usize >= GAMEPAD_BYTES {
544 let b = gamepad_bytes(&pads[slot as usize]);
545 guest_slice_mut(data, dst, GAMEPAD_BYTES as u64)?.copy_from_slice(&b);
546 }
547 Ok(GAMEPAD_BYTES as i32)
548 },
549 )?;
550
551 linker.func_wrap(
552 "gasm",
553 "gamepad_name",
554 |mut caller: Caller<'_, Host>, slot: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
555 let mem = memory(&caller)?;
556 let (data, host) = mem.data_and_store_mut(&mut caller);
557 let Some(g) = host.input.gamepads.as_ref().and_then(|p| p.get(slot as usize)).filter(|g| g.connected) else { return Ok(-1) };
558 copy_if_fits(data, dst, cap, g.name.as_bytes())
559 },
560 )?;
561
562 linker.func_wrap(
563 "gasm",
564 "param",
565 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
566 let name = guest_str(&caller, ptr, len)?;
567 let mem = memory(&caller)?;
568 let (data, host) = mem.data_and_store_mut(&mut caller);
569 let Some(v) = host.params.get(&name) else { return Ok(-1) };
570 copy_if_fits(data, dst, cap, v.as_bytes())
571 },
572 )?;
573
574 fn read_asset(caller: &mut Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32) -> wasmtime::Result<i32> {
577 let name = guest_str(caller, ptr, len)?;
578 let mem = memory(caller)?;
579 let (data, host) = mem.data_and_store_mut(caller);
580 let Some(asset) = host.assets.get(&name) else { return Ok(-1) };
581 let n = asset.size().saturating_sub(offset).min(cap as u64);
582 let out = guest_slice_mut(data, dst, n)?;
583 Ok(asset.read_at(offset, out) as i32)
584 }
585
586 linker.func_wrap(
587 "gasm",
588 "asset_read_at",
589 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
590 read_asset(&mut caller, ptr, len, offset as u64, dst, cap)
591 },
592 )?;
593
594 linker.func_wrap(
595 "gasm",
596 "asset_read_at64",
597 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u64, dst: u32, cap: u32| -> wasmtime::Result<i32> {
598 read_asset(&mut caller, ptr, len, offset, dst, cap)
599 },
600 )?;
601
602 linker.func_wrap(
603 "gasm",
604 "asset_read",
605 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
606 read_asset(&mut caller, ptr, len, 0, dst, cap)
607 },
608 )?;
609
610 linker.func_wrap("gasm", "asset_count", |caller: Caller<'_, Host>| -> u32 { caller.data().assets.names().len() as u32 })?;
611
612 linker.func_wrap(
613 "gasm",
614 "asset_name",
615 |mut caller: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
616 let mem = memory(&caller)?;
617 let (data, host) = mem.data_and_store_mut(&mut caller);
618 let Some(n) = host.assets.names().get(index as usize) else { return Ok(-1) };
619 copy_if_fits(data, dst, cap, n.as_bytes())
620 },
621 )?;
622
623 linker.func_wrap(
624 "gasm",
625 "asset_size",
626 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
627 let name = guest_str(&caller, ptr, len)?;
628 Ok(match caller.data().assets.size(&name) {
629 None => -1,
630 Some(n) if n > i32::MAX as u64 => -2,
631 Some(n) => n as i32,
632 })
633 },
634 )?;
635
636 linker.func_wrap(
637 "gasm",
638 "asset_size64",
639 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i64> {
640 let name = guest_str(&caller, ptr, len)?;
641 Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i64))
642 },
643 )?;
644 Ok(())
645}
646
647fn trap(e: String) -> wasmtime::Error {
648 format_err!("{e}")
649}
650
651fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
652 const M: &str = "gasm:gfx";
653 linker.func_wrap(M, "width", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().0 })?;
654 linker.func_wrap(M, "height", |c: Caller<'_, Host>| -> u32 { c.data().gfx.size().1 })?;
655 linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
656 let src = guest_str(&c, ptr, len)?;
657 c.data_mut().gfx.create_shader(&src).map_err(trap)
658 })?;
659 linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
660 c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
661 })?;
662 linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
663 let json = guest_str(&c, ptr, len)?;
664 c.data_mut().gfx.create_pipeline(&json).map_err(trap)
665 })?;
666 linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
667 let json = guest_str(&c, ptr, len)?;
668 c.data_mut().gfx.create_bind_group(&json).map_err(trap)
669 })?;
670 linker.func_wrap(
671 M,
672 "write_buffer",
673 |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
674 let mem = memory(&c)?;
675 let (data, host) = mem.data_and_store_mut(&mut c);
676 let bytes = guest_slice(data, ptr, len as u64)?;
677 host.gfx.write_buffer(buf, offset, bytes).map_err(trap)?;
678 if host.hashing {
679 host.video_hash.update(bytes);
680 }
681 Ok(())
682 },
683 )?;
684 linker.func_wrap(M, "create_bind_group_layout", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
685 let json = guest_str(&c, ptr, len)?;
686 c.data_mut().gfx.create_bind_group_layout(&json).map_err(trap)
687 })?;
688 linker.func_wrap(M, "create_texture", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
689 let json = guest_str(&c, ptr, len)?;
690 c.data_mut().gfx.create_texture(&json).map_err(trap)
691 })?;
692 linker.func_wrap(M, "create_sampler", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
693 let json = guest_str(&c, ptr, len)?;
694 c.data_mut().gfx.create_sampler(&json).map_err(trap)
695 })?;
696 linker.func_wrap(
697 M,
698 "write_texture",
699 |mut c: Caller<'_, Host>, tex: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
700 let mem = memory(&c)?;
701 let (data, host) = mem.data_and_store_mut(&mut c);
702 let bytes = guest_slice(data, ptr, len as u64)?;
703 host.gfx.write_texture(tex, mip, x, y, w, h, bytes).map_err(trap)?;
704 if host.hashing {
705 for v in [tex, mip, x, y, w, h] {
707 host.video_hash.update(&v.to_le_bytes());
708 }
709 host.video_hash.update(bytes);
710 }
711 Ok(())
712 },
713 )?;
714 linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
715 let h = c.data_mut();
716 let show = h.show_frame;
717 h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
718 })?;
719 linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
720 c.data_mut().gfx.set_pipeline(p).map_err(trap)
721 })?;
722 linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
723 c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
724 })?;
725 linker.func_wrap(
726 M,
727 "set_bind_group_offsets",
728 |mut c: Caller<'_, Host>, i: u32, bg: u32, ptr: u32, count: u32| -> wasmtime::Result<()> {
729 let mem = memory(&c)?;
730 let (data, host) = mem.data_and_store_mut(&mut c);
731 let bytes = guest_slice(data, ptr, count as u64 * 4)?;
732 let mut offsets = [0u32; 32];
733 if count as usize > offsets.len() {
734 bail!("gfx.set_bind_group_offsets: {count} offsets (a bind group has at most {})", offsets.len());
735 }
736 for (o, b) in offsets.iter_mut().zip(bytes.chunks_exact(4)) {
737 *o = u32::from_le_bytes([b[0], b[1], b[2], b[3]]);
738 }
739 host.gfx.set_bind_group_offsets(i, bg, &offsets[..count as usize]).map_err(trap)
740 },
741 )?;
742 linker.func_wrap(
743 M,
744 "set_viewport",
745 |mut c: Caller<'_, Host>, x: f32, y: f32, w: f32, h: f32, min: f32, max: f32| -> wasmtime::Result<()> {
746 c.data_mut().gfx.set_viewport(x, y, w, h, min, max).map_err(trap)
747 },
748 )?;
749 linker.func_wrap(M, "set_scissor_rect", |mut c: Caller<'_, Host>, x: u32, y: u32, w: u32, h: u32| -> wasmtime::Result<()> {
750 c.data_mut().gfx.set_scissor_rect(x, y, w, h).map_err(trap)
751 })?;
752 linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
753 c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
754 })?;
755 linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
756 c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
757 })?;
758 linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| -> wasmtime::Result<()> {
759 c.data_mut().gfx.draw(vc, ic, fv, fi).map_err(trap)
760 })?;
761 linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| -> wasmtime::Result<()> {
762 c.data_mut().gfx.draw_indexed(ic, n, first, base, fi).map_err(trap)
763 })?;
764 linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
765 let h = c.data_mut();
766 h.frames_presented += 1;
767 h.gfx.end_frame().map_err(trap)
768 })?;
769 linker.func_wrap(M, "destroy", |mut c: Caller<'_, Host>, handle: u32| -> wasmtime::Result<()> {
770 c.data_mut().gfx.destroy(handle).map_err(trap)
771 })?;
772 Ok(())
773}
774
775fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
776 const M: &str = "gasm:net";
777 linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
778 let url = guest_str(&c, ptr, len)?;
779 Ok(c.data_mut().net.open(&url))
780 })?;
781 linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| -> wasmtime::Result<u32> { c.data().net.state(h).map_err(trap) })?;
782 linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
783 let mem = memory(&c)?;
784 let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
785 if msg.is_empty() {
786 c.data().net.state(h).map_err(trap)?; return Ok(-1);
788 }
789 c.data_mut().net.send(h, msg).map_err(trap)
790 })?;
791 linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
792 let mem = memory(&c)?;
793 let (data, host) = mem.data_and_store_mut(&mut c);
794 let msg = match host.net.peek(h).map_err(trap)? {
795 Ok(m) => m,
796 Err(code) => return Ok(code),
797 };
798 let n = copy_if_fits(data, dst, cap, msg)?;
799 if n as usize <= cap as usize {
800 host.net.pop(h);
801 }
802 Ok(n)
803 })?;
804 linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| -> wasmtime::Result<()> { c.data_mut().net.close(h).map_err(trap) })?;
805 Ok(())
806}
807
808fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
809 const M: &str = "gasm:storage";
810 linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
811 let key = guest_str(&c, kp, kl)?;
812 let mem = memory(&c)?;
813 let (data, host) = mem.data_and_store_mut(&mut c);
814 let Some(v) = host.storage.get(&key) else { return Ok(-1) };
815 copy_if_fits(data, dst, cap, v)
816 })?;
817 linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
818 let key = guest_str(&c, kp, kl)?;
819 let mem = memory(&c)?;
820 let (data, host) = mem.data_and_store_mut(&mut c);
821 let value = guest_slice(data, vp, vl as u64)?;
822 Ok(match host.storage.set(&key, value) {
823 Ok(()) => 0,
824 Err(e) => {
825 eprintln!("[gasm] storage: {key}: {e}");
826 e.code()
827 }
828 })
829 })?;
830 linker.func_wrap(M, "count", |mut c: Caller<'_, Host>| -> u32 { c.data_mut().storage.keys().len() as u32 })?;
831 linker.func_wrap(M, "key", |mut c: Caller<'_, Host>, index: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
832 let mem = memory(&c)?;
833 let (data, host) = mem.data_and_store_mut(&mut c);
834 let Some(k) = host.storage.keys().get(index as usize) else { return Ok(-1) };
835 copy_if_fits(data, dst, cap, k.as_bytes())
836 })?;
837 linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
838 let key = guest_str(&c, kp, kl)?;
839 Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
840 })?;
841 Ok(())
842}
843
844#[derive(Debug)]
846pub enum Stop {
847 Exit(i32),
849 Trap(String),
850}
851
852impl std::fmt::Display for Stop {
853 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
854 match self {
855 Stop::Exit(code) => write!(f, "guest exited with code {code}"),
856 Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
857 }
858 }
859}
860
861impl std::error::Error for Stop {}
862
863fn classify(e: wasmtime::Error) -> Stop {
864 if let Some(exit) = e.downcast_ref::<wasi::Exit>() {
865 return Stop::Exit(exit.0);
866 }
867 if e.downcast_ref::<wasmtime::Trap>() == Some(&wasmtime::Trap::Interrupt) {
868 return Stop::Trap(format!("a guest call ran longer than the time limit (stuck in a loop?); see --call-timeout\n{e:?}"));
869 }
870 Stop::Trap(format!("{e:?}"))
871}
872
873const TICKS_PER_SEC: u64 = 10;
875
876pub fn engine() -> &'static Engine {
880 static ENGINE: OnceLock<Engine> = OnceLock::new();
881 ENGINE.get_or_init(|| {
882 let mut config = Config::new();
883 config.epoch_interruption(true);
884 let engine = Engine::new(&config).expect("wasmtime engine");
885 let ticker = engine.clone();
886 std::thread::Builder::new()
887 .name("gasm-epoch".into())
888 .spawn(move || {
889 loop {
890 std::thread::sleep(Duration::from_millis(1000 / TICKS_PER_SEC));
891 ticker.increment_epoch();
892 }
893 })
894 .expect("epoch thread");
895 engine
896 })
897}
898
899pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
901 engine().precompile_module(wasm)
902}
903
904#[derive(Clone, Copy)]
906pub struct LoadOptions {
907 pub allow_precompiled: bool,
910 pub call_timeout: Option<Duration>,
912}
913
914impl Default for LoadOptions {
915 fn default() -> Self {
916 LoadOptions { allow_precompiled: false, call_timeout: Some(Duration::from_secs(30)) }
917 }
918}
919
920pub struct Game {
921 pub store: Store<Host>,
922 frame: TypedFunc<(), ()>,
923 exit: Option<TypedFunc<(), ()>>,
924 deadline: u64,
925}
926
927impl Game {
928 pub fn load(wasm: &[u8], host: Host, opts: LoadOptions) -> Result<Game, Stop> {
929 Self::load_inner(wasm, host, opts).map_err(classify)
930 }
931
932 fn load_inner(wasm: &[u8], host: Host, opts: LoadOptions) -> wasmtime::Result<Game> {
933 let engine = engine();
934 let module = if wasm.starts_with(b"\0asm") {
935 Module::new(engine, wasm)?
936 } else if opts.allow_precompiled {
937 unsafe { Module::deserialize(engine, wasm)? }
940 } else {
941 bail!("not a wasm module (a precompiled .cwasm runs as native code: pass --allow-precompiled if you made it yourself)");
942 };
943 let mut linker: Linker<Host> = Linker::new(engine);
944 add_gasm_imports(&mut linker)?;
945 add_gfx_imports(&mut linker)?;
946 add_net_imports(&mut linker)?;
947 add_storage_imports(&mut linker)?;
948 let mut store = Store::new(engine, host);
949 let mut provided = HashSet::new();
951 for (m, f, _) in linker.iter(&mut store) {
952 provided.insert(m.to_owned());
953 provided.insert(format!("{m}.{f}"));
954 }
955 for f in wasi::IMPLEMENTED {
956 provided.insert(format!("{}.{f}", wasi::MODULE));
957 }
958 provided.insert(wasi::MODULE.to_owned());
959 store.data_mut().provided = provided;
960 wasi::add_to_linker(&mut linker, &module)?;
961 linker.define_unknown_imports_as_traps(&module)?;
964
965 let deadline = opts.call_timeout.map_or(u64::MAX / 2, |t| (t.as_millis() as u64 * TICKS_PER_SEC).div_ceil(1000).max(1));
966 store.epoch_deadline_trap();
967 store.set_epoch_deadline(deadline);
968 let instance: Instance = linker.instantiate(&mut store, &module)?;
969 let memory = instance
970 .get_memory(&mut store, "memory")
971 .ok_or_else(|| format_err!("guest does not export `memory`"))?;
972 store.data_mut().memory = Some(memory);
973
974 if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
975 store.set_epoch_deadline(deadline);
976 init.call(&mut store, ())?;
977 }
978 let version = instance
979 .get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?
980 .call(&mut store, ())?;
981 if version != ABI_VERSION {
982 bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
983 }
984 store.set_epoch_deadline(deadline);
985 let rc = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?.call(&mut store, ())?;
986 if rc != 0 {
987 bail!("gasm_init failed with code {rc}");
988 }
989 let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
990 let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
991 Ok(Game { store, frame, exit, deadline })
992 }
993
994 pub fn frame(&mut self) -> Result<(), Stop> {
995 self.store.set_epoch_deadline(self.deadline);
996 self.frame.call(&mut self.store, ()).map_err(classify)
997 }
998
999 pub fn exit(&mut self) {
1001 if let Some(f) = self.exit.take() {
1002 self.store.set_epoch_deadline(self.deadline);
1003 if let Err(e) = f.call(&mut self.store, ()) {
1004 if let Stop::Trap(t) = classify(e) {
1005 eprintln!("[gasm] gasm_exit trapped: {t}");
1006 }
1007 }
1008 }
1009 }
1010
1011 pub fn host(&self) -> &Host {
1012 self.store.data()
1013 }
1014
1015 pub fn host_mut(&mut self) -> &mut Host {
1016 self.store.data_mut()
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023
1024 #[test]
1025 fn titles_are_cleaned_and_cut() {
1026 assert_eq!(clean_title("A\u{7}B\u{202e}C\u{2069}").as_deref(), Some("ABC"));
1027 assert_eq!(clean_title("\n\t"), None);
1028 assert_eq!(clean_title(&"é".repeat(200)).map(|t| t.len()), Some(256));
1030 }
1031
1032 #[test]
1033 fn custom_sections_are_found() {
1034 let mut wasm = b"\0asm\x01\0\0\0\x01\x01\0".to_vec();
1036 wasm.extend_from_slice(&[0, 14, 10]);
1037 wasm.extend_from_slice(b"gasm.titleHi\0");
1038 assert_eq!(custom_section(&wasm, "gasm.title"), Some(&b"Hi\0"[..]));
1039 assert_eq!(static_title(&wasm).as_deref(), Some("Hi"));
1040 assert_eq!(custom_section(&wasm, "other"), None);
1041 assert_eq!(custom_section(&wasm[..wasm.len() - 2], "gasm.title"), None); assert_eq!(custom_section(b"not wasm", "gasm.title"), None);
1043 }
1044}