1use std::collections::HashMap;
5use std::time::Instant;
6
7use wasmtime::{Caller, Engine, Instance, Linker, Memory, Module, Store, TypedFunc, bail, format_err};
8use wasmtime_wasi::WasiCtx;
9use wasmtime_wasi::p1::{self, WasiP1Ctx};
10
11use crate::assets::Assets;
12use crate::audio::AudioSink;
13use crate::gfx::Gfx;
14use crate::net::Net;
15use crate::storage::Storage;
16
17pub const ABI_VERSION: i32 = 0;
18
19#[derive(Clone, Copy)]
21pub struct Fnv(pub u32);
22
23impl Fnv {
24 pub fn new() -> Self {
25 Fnv(0x811c_9dc5)
26 }
27 pub fn update(&mut self, bytes: &[u8]) {
28 let mut h = self.0;
29 for &b in bytes {
30 h ^= b as u32;
31 h = h.wrapping_mul(0x0100_0193);
32 }
33 self.0 = h;
34 }
35}
36
37pub struct Host {
38 wasi: WasiP1Ctx,
39 memory: Option<Memory>,
40 start: Instant,
41 pub virtual_time_ms: Option<f64>,
43 pub frame_rate: f64,
44 pub pads: [u32; 4],
45 pub assets: Assets,
46 pub params: HashMap<String, String>,
47 pub audio: Option<AudioSink>,
48 pub gfx: Gfx,
49 pub net: Net,
50 pub storage: Storage,
51 pub show_frame: bool,
53 audio_rate: u32,
54 audio_channels: u32,
55 pub rgba: Vec<u8>,
57 pub width: usize,
58 pub height: usize,
59 pub frames_presented: u64,
60 pub hashing: bool,
61 pub video_hash: Fnv,
62 pub audio_hash: Fnv,
63 pub audio_frames: u64,
64}
65
66impl Host {
67 pub fn new(
68 assets: impl Into<Assets>,
69 params: HashMap<String, String>,
70 audio: Option<AudioSink>,
71 gfx: Gfx,
72 net: Net,
73 storage: Storage,
74 ) -> Self {
75 let wasi = WasiCtx::builder().inherit_stdout().inherit_stderr().build_p1();
76 Host {
77 wasi,
78 memory: None,
79 start: Instant::now(),
80 virtual_time_ms: None,
81 frame_rate: 60.0,
82 pads: [0; 4],
83 assets: assets.into(),
84 params,
85 audio,
86 gfx,
87 net,
88 storage,
89 show_frame: true,
90 audio_rate: 44100,
91 audio_channels: 2,
92 rgba: Vec::new(),
93 width: 0,
94 height: 0,
95 frames_presented: 0,
96 hashing: true,
97 video_hash: Fnv::new(),
98 audio_hash: Fnv::new(),
99 audio_frames: 0,
100 }
101 }
102}
103
104fn guest_slice<'a>(mem: &'a [u8], ptr: u32, len: u64) -> wasmtime::Result<&'a [u8]> {
106 let start = ptr as u64;
107 let end = start.checked_add(len).ok_or_else(|| format_err!("guest pointer overflow"))?;
108 if end > mem.len() as u64 {
109 bail!("guest access out of bounds: {start:#x}..{end:#x} (memory is {:#x})", mem.len());
110 }
111 Ok(&mem[start as usize..end as usize])
112}
113
114fn memory(caller: &Caller<'_, Host>) -> wasmtime::Result<Memory> {
115 caller.data().memory.ok_or_else(|| format_err!("guest memory not yet available"))
116}
117
118fn guest_str(caller: &Caller<'_, Host>, ptr: u32, len: u32) -> wasmtime::Result<String> {
119 let mem = memory(caller)?;
120 let bytes = guest_slice(mem.data(caller), ptr, len as u64)?;
121 Ok(String::from_utf8_lossy(bytes).into_owned())
122}
123
124fn add_gasm_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
125 linker.func_wrap("gasm", "log", |caller: Caller<'_, Host>, ptr: u32, len: u32| {
126 let msg = guest_str(&caller, ptr, len)?;
127 eprintln!("[guest] {msg}");
128 Ok(())
129 })?;
130
131 linker.func_wrap("gasm", "time_ms", |caller: Caller<'_, Host>| -> f64 {
132 let h = caller.data();
133 h.virtual_time_ms.unwrap_or_else(|| h.start.elapsed().as_secs_f64() * 1000.0)
134 })?;
135
136 linker.func_wrap("gasm", "set_frame_rate", |mut caller: Caller<'_, Host>, hz: f64| {
137 if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
138 caller.data_mut().frame_rate = hz;
139 }
140 })?;
141
142 linker.func_wrap(
143 "gasm",
144 "video_present",
145 |mut caller: Caller<'_, Host>, ptr: u32, w: u32, h: u32, stride: u32| -> wasmtime::Result<()> {
146 let (w, h, stride) = (w as usize, h as usize, stride as usize);
147 if w == 0 || h == 0 || w > 4096 || h > 4096 || stride < w * 4 {
148 bail!("video_present: bad geometry {w}x{h} stride {stride}");
149 }
150 let mem = memory(&caller)?;
151 let (data, host) = mem.data_and_store_mut(&mut caller);
152 let src = guest_slice(data, ptr, (stride * (h - 1) + w * 4) as u64)?;
153 host.rgba.resize(w * h * 4, 0);
154 for y in 0..h {
155 let row = &src[y * stride..y * stride + w * 4];
156 host.rgba[y * w * 4..(y + 1) * w * 4].copy_from_slice(row);
157 }
158 if host.hashing {
159 host.video_hash.update(&host.rgba);
160 }
161 host.width = w;
162 host.height = h;
163 host.frames_presented += 1;
164 Ok(())
165 },
166 )?;
167
168 linker.func_wrap("gasm", "audio_config", |mut caller: Caller<'_, Host>, rate: u32, channels: u32| {
169 let h = caller.data_mut();
170 if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
171 h.audio_rate = rate;
172 h.audio_channels = channels;
173 if let Some(a) = &mut h.audio {
174 a.configure(rate, channels);
175 }
176 }
177 })?;
178
179 linker.func_wrap(
180 "gasm",
181 "audio_push",
182 |mut caller: Caller<'_, Host>, ptr: u32, frames: u32| -> wasmtime::Result<()> {
183 let mem = memory(&caller)?;
184 let (data, host) = mem.data_and_store_mut(&mut caller);
185 let len = frames as u64 * host.audio_channels as u64 * 4;
186 let bytes = guest_slice(data, ptr, len)?;
187 if host.hashing {
188 host.audio_hash.update(bytes);
189 }
190 host.audio_frames += frames as u64;
191 if let Some(a) = &mut host.audio {
192 a.push_le_f32(bytes);
193 }
194 Ok(())
195 },
196 )?;
197
198 linker.func_wrap("gasm", "input_pad", |caller: Caller<'_, Host>, player: u32| -> u32 {
199 caller.data().pads.get(player as usize).copied().unwrap_or(0)
200 })?;
201
202 linker.func_wrap(
203 "gasm",
204 "param",
205 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
206 let name = guest_str(&caller, ptr, len)?;
207 let mem = memory(&caller)?;
208 let (data, host) = mem.data_and_store_mut(&mut caller);
209 let Some(v) = host.params.get(&name) else { return Ok(-1) };
210 let v = v.as_bytes();
211 if v.len() <= cap as usize {
212 guest_slice(data, dst, v.len() as u64)?;
213 data[dst as usize..dst as usize + v.len()].copy_from_slice(v);
214 }
215 Ok(v.len() as i32)
216 },
217 )?;
218
219 linker.func_wrap(
220 "gasm",
221 "asset_read_at",
222 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, offset: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
223 let name = guest_str(&caller, ptr, len)?;
224 let mem = memory(&caller)?;
225 let (data, host) = mem.data_and_store_mut(&mut caller);
226 let Some(size) = host.assets.size(&name) else { return Ok(-1) };
227 let n = size.saturating_sub(offset as u64).min(cap as u64);
228 guest_slice(data, dst, n)?; let dst = &mut data[dst as usize..dst as usize + n as usize];
230 Ok(host.assets.read_at(&name, offset as u64, dst).map_or(-1, |k| k as i32))
231 },
232 )?;
233
234 linker.func_wrap(
235 "gasm",
236 "asset_size",
237 |caller: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
238 let name = guest_str(&caller, ptr, len)?;
239 Ok(caller.data().assets.size(&name).map_or(-1, |n| n as i32))
240 },
241 )?;
242
243 linker.func_wrap(
244 "gasm",
245 "asset_read",
246 |mut caller: Caller<'_, Host>, ptr: u32, len: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
247 let name = guest_str(&caller, ptr, len)?;
248 let mem = memory(&caller)?;
249 let (data, host) = mem.data_and_store_mut(&mut caller);
250 let Some(size) = host.assets.size(&name) else { return Ok(-1) };
251 let n = size.min(cap as u64);
252 guest_slice(data, dst, n)?;
253 let dst = &mut data[dst as usize..dst as usize + n as usize];
254 Ok(host.assets.read_at(&name, 0, dst).map_or(-1, |k| k as i32))
255 },
256 )?;
257 Ok(())
258}
259
260pub fn precompile(wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
262 Engine::default().precompile_module(wasm)
263}
264
265fn trap(e: String) -> wasmtime::Error {
266 format_err!("{e}")
267}
268
269fn add_gfx_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
270 const M: &str = "gasm:gfx";
271 linker.func_wrap(M, "width", |c: Caller<'_, Host>| c.data().gfx.size().0)?;
272 linker.func_wrap(M, "height", |c: Caller<'_, Host>| c.data().gfx.size().1)?;
273 linker.func_wrap(M, "create_shader", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
274 let src = guest_str(&c, ptr, len)?;
275 c.data_mut().gfx.create_shader(&src).map_err(trap)
276 })?;
277 linker.func_wrap(M, "create_buffer", |mut c: Caller<'_, Host>, size: u32, usage: u32| -> wasmtime::Result<u32> {
278 c.data_mut().gfx.create_buffer(size, usage).map_err(trap)
279 })?;
280 linker.func_wrap(M, "create_pipeline", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
281 let json = guest_str(&c, ptr, len)?;
282 c.data_mut().gfx.create_pipeline(&json).map_err(trap)
283 })?;
284 linker.func_wrap(M, "create_bind_group", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<u32> {
285 let json = guest_str(&c, ptr, len)?;
286 c.data_mut().gfx.create_bind_group(&json).map_err(trap)
287 })?;
288 linker.func_wrap(
289 M,
290 "write_buffer",
291 |mut c: Caller<'_, Host>, buf: u32, offset: u32, ptr: u32, len: u32| -> wasmtime::Result<()> {
292 let mem = memory(&c)?;
293 let (data, host) = mem.data_and_store_mut(&mut c);
294 let bytes = guest_slice(data, ptr, len as u64)?;
295 if host.hashing {
296 host.video_hash.update(bytes);
297 }
298 host.gfx.write_buffer(buf, offset, bytes).map_err(trap)
299 },
300 )?;
301 linker.func_wrap(M, "begin_frame", |mut c: Caller<'_, Host>, r: f32, g: f32, b: f32, a: f32| -> wasmtime::Result<u32> {
302 let h = c.data_mut();
303 let show = h.show_frame;
304 h.gfx.begin_frame([r, g, b, a], show).map(u32::from).map_err(trap)
305 })?;
306 linker.func_wrap(M, "set_pipeline", |mut c: Caller<'_, Host>, p: u32| -> wasmtime::Result<()> {
307 c.data_mut().gfx.set_pipeline(p).map_err(trap)
308 })?;
309 linker.func_wrap(M, "set_bind_group", |mut c: Caller<'_, Host>, i: u32, bg: u32| -> wasmtime::Result<()> {
310 c.data_mut().gfx.set_bind_group(i, bg).map_err(trap)
311 })?;
312 linker.func_wrap(M, "set_vertex_buffer", |mut c: Caller<'_, Host>, slot: u32, b: u32, off: u32| -> wasmtime::Result<()> {
313 c.data_mut().gfx.set_vertex_buffer(slot, b, off).map_err(trap)
314 })?;
315 linker.func_wrap(M, "set_index_buffer", |mut c: Caller<'_, Host>, b: u32, fmt: u32, off: u32| -> wasmtime::Result<()> {
316 c.data_mut().gfx.set_index_buffer(b, fmt, off).map_err(trap)
317 })?;
318 linker.func_wrap(M, "draw", |mut c: Caller<'_, Host>, vc: u32, ic: u32, fv: u32, fi: u32| {
319 c.data_mut().gfx.draw(vc, ic, fv, fi)
320 })?;
321 linker.func_wrap(M, "draw_indexed", |mut c: Caller<'_, Host>, ic: u32, n: u32, first: u32, base: i32, fi: u32| {
322 c.data_mut().gfx.draw_indexed(ic, n, first, base, fi)
323 })?;
324 linker.func_wrap(M, "end_frame", |mut c: Caller<'_, Host>| -> wasmtime::Result<()> {
325 let h = c.data_mut();
326 h.frames_presented += 1;
327 h.gfx.end_frame().map_err(trap)
328 })?;
329 Ok(())
330}
331
332fn add_net_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
333 const M: &str = "gasm:net";
334 linker.func_wrap(M, "open", |mut c: Caller<'_, Host>, ptr: u32, len: u32| -> wasmtime::Result<i32> {
335 let url = guest_str(&c, ptr, len)?;
336 Ok(c.data_mut().net.open(&url))
337 })?;
338 linker.func_wrap(M, "state", |c: Caller<'_, Host>, h: i32| c.data().net.state(h))?;
339 linker.func_wrap(M, "send", |mut c: Caller<'_, Host>, h: i32, ptr: u32, len: u32| -> wasmtime::Result<i32> {
340 if len == 0 {
341 return Ok(-1);
342 }
343 let mem = memory(&c)?;
344 let msg = guest_slice(mem.data(&c), ptr, len as u64)?.to_vec();
345 Ok(c.data_mut().net.send(h, msg))
346 })?;
347 linker.func_wrap(M, "recv", |mut c: Caller<'_, Host>, h: i32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
348 let mem = memory(&c)?;
349 let (data, host) = mem.data_and_store_mut(&mut c);
350 let msg = match host.net.peek(h) {
351 Ok(m) => m,
352 Err(code) => return Ok(code),
353 };
354 let n = msg.len();
355 if n <= cap as usize {
356 guest_slice(data, dst, n as u64)?;
357 data[dst as usize..dst as usize + n].copy_from_slice(msg);
358 host.net.pop(h);
359 }
360 Ok(n as i32)
361 })?;
362 linker.func_wrap(M, "close", |mut c: Caller<'_, Host>, h: i32| c.data_mut().net.close(h))?;
363 Ok(())
364}
365
366fn add_storage_imports(linker: &mut Linker<Host>) -> wasmtime::Result<()> {
367 const M: &str = "gasm:storage";
368 linker.func_wrap(M, "get", |mut c: Caller<'_, Host>, kp: u32, kl: u32, dst: u32, cap: u32| -> wasmtime::Result<i32> {
369 let key = guest_str(&c, kp, kl)?;
370 let mem = memory(&c)?;
371 let (data, host) = mem.data_and_store_mut(&mut c);
372 let Some(v) = host.storage.get(&key) else { return Ok(-1) };
373 if v.len() <= cap as usize {
374 guest_slice(data, dst, v.len() as u64)?;
375 data[dst as usize..dst as usize + v.len()].copy_from_slice(v);
376 }
377 Ok(v.len() as i32)
378 })?;
379 linker.func_wrap(M, "set", |mut c: Caller<'_, Host>, kp: u32, kl: u32, vp: u32, vl: u32| -> wasmtime::Result<i32> {
380 let key = guest_str(&c, kp, kl)?;
381 let mem = memory(&c)?;
382 let (data, host) = mem.data_and_store_mut(&mut c);
383 let value = guest_slice(data, vp, vl as u64)?;
384 Ok(match host.storage.set(&key, value) {
385 Ok(()) => 0,
386 Err(e) => {
387 eprintln!("[gasm] storage: {e}");
388 -1
389 }
390 })
391 })?;
392 linker.func_wrap(M, "delete", |mut c: Caller<'_, Host>, kp: u32, kl: u32| -> wasmtime::Result<i32> {
393 let key = guest_str(&c, kp, kl)?;
394 Ok(if c.data_mut().storage.delete(&key) { 0 } else { -1 })
395 })?;
396 Ok(())
397}
398
399#[derive(Debug)]
401pub enum Stop {
402 Exit(i32),
404 Trap(String),
405}
406
407impl std::fmt::Display for Stop {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 match self {
410 Stop::Exit(code) => write!(f, "guest exited with code {code}"),
411 Stop::Trap(msg) => write!(f, "guest trapped: {msg}"),
412 }
413 }
414}
415
416impl std::error::Error for Stop {}
417
418fn classify(e: wasmtime::Error) -> Stop {
419 match e.downcast_ref::<wasmtime_wasi::I32Exit>() {
420 Some(exit) => Stop::Exit(exit.0),
421 None => Stop::Trap(format!("{e:?}")),
422 }
423}
424
425pub struct Game {
426 pub store: Store<Host>,
427 frame: TypedFunc<(), ()>,
428 exit: Option<TypedFunc<(), ()>>,
429}
430
431impl Game {
432 pub fn load(wasm: &[u8], host: Host) -> Result<Game, Stop> {
433 Self::load_inner(wasm, host).map_err(classify)
434 }
435
436 fn load_inner(wasm: &[u8], host: Host) -> wasmtime::Result<Game> {
437 let engine = Engine::default();
438 let module = if wasm.starts_with(b"\0asm") {
439 Module::new(&engine, wasm)?
440 } else {
441 unsafe { Module::deserialize(&engine, wasm)? }
444 };
445 let mut linker: Linker<Host> = Linker::new(&engine);
446 p1::add_to_linker_sync(&mut linker, |h: &mut Host| &mut h.wasi)?;
447 add_gasm_imports(&mut linker)?;
448 add_gfx_imports(&mut linker)?;
449 add_net_imports(&mut linker)?;
450 add_storage_imports(&mut linker)?;
451 linker.define_unknown_imports_as_traps(&module)?;
454
455 let mut store = Store::new(&engine, host);
456 let instance: Instance = linker.instantiate(&mut store, &module)?;
457 let memory = instance
458 .get_memory(&mut store, "memory")
459 .ok_or_else(|| format_err!("guest does not export `memory`"))?;
460 store.data_mut().memory = Some(memory);
461
462 if let Ok(init) = instance.get_typed_func::<(), ()>(&mut store, "_initialize") {
463 init.call(&mut store, ())?;
464 }
465 let version = instance
466 .get_typed_func::<(), i32>(&mut store, "gasm_abi_version")?
467 .call(&mut store, ())?;
468 if version != ABI_VERSION {
469 bail!("guest targets gasm ABI v{version}, runner implements v{ABI_VERSION}");
470 }
471 let rc = instance.get_typed_func::<(), i32>(&mut store, "gasm_init")?.call(&mut store, ())?;
472 if rc != 0 {
473 bail!("gasm_init failed with code {rc}");
474 }
475 let frame = instance.get_typed_func::<(), ()>(&mut store, "gasm_frame")?;
476 let exit = instance.get_typed_func::<(), ()>(&mut store, "gasm_exit").ok();
477 Ok(Game { store, frame, exit })
478 }
479
480 pub fn frame(&mut self) -> Result<(), Stop> {
481 self.frame.call(&mut self.store, ()).map_err(classify)
482 }
483
484 pub fn exit(&mut self) {
486 if let Some(f) = self.exit.take() {
487 if let Err(e) = f.call(&mut self.store, ()) {
488 if let Stop::Trap(t) = classify(e) {
489 eprintln!("[gasm] gasm_exit trapped: {t}");
490 }
491 }
492 }
493 }
494
495 pub fn host(&self) -> &Host {
496 self.store.data()
497 }
498
499 pub fn host_mut(&mut self) -> &mut Host {
500 self.store.data_mut()
501 }
502}