Skip to main content

gasm/
lib.rs

1//! Rust bindings for the gasm game ABI v0 — see `spec/ABI.md`.
2//!
3//! ```ignore
4//! struct MyGame { t: u32 }
5//! impl gasm::Game for MyGame {
6//!     fn init() -> Result<Self, String> { gasm::set_frame_rate(60.0); Ok(MyGame { t: 0 }) }
7//!     fn frame(&mut self) { self.t += 1; /* simulate, draw */ }
8//! }
9//! gasm::game!(MyGame);
10//! ```
11//!
12//! Build with `--target wasm32-unknown-unknown` and `crate-type = ["cdylib"]`.
13//! On native targets the same code links against an in-process stub host
14//! ([`native`]) for tests and benchmarks.
15
16pub mod sys;
17
18#[cfg(not(target_arch = "wasm32"))]
19pub mod native;
20
21use std::cell::UnsafeCell;
22
23pub const ABI_VERSION: i32 = 0;
24
25// ---- core --------------------------------------------------------------------
26
27pub fn log(msg: &str) {
28    unsafe { sys::log(msg.as_ptr(), msg.len() as u32) }
29}
30
31/// `format!`-style logging: `gasm::log!("x = {x}")`.
32#[macro_export]
33macro_rules! log {
34    ($($t:tt)*) => { $crate::log(&::std::format!($($t)*)) };
35}
36
37/// Monotonic milliseconds (virtual, frame-derived time in headless runs).
38pub fn time_ms() -> f64 {
39    unsafe { sys::time_ms() }
40}
41
42/// Rate at which the runner calls `frame()`. Default 60.
43pub fn set_frame_rate(hz: f64) {
44    unsafe { sys::set_frame_rate(hz) }
45}
46
47/// Launch parameter (`--param k=v` natively, `?k=v` in the browser).
48pub fn param(name: &str) -> Option<String> {
49    let len = unsafe { sys::param(name.as_ptr(), name.len() as u32, std::ptr::null_mut(), 0) };
50    if len < 0 {
51        return None;
52    }
53    let mut buf = vec![0u8; len as usize];
54    unsafe { sys::param(name.as_ptr(), name.len() as u32, buf.as_mut_ptr(), len as u32) };
55    Some(String::from_utf8_lossy(&buf).into_owned())
56}
57
58/// End the game (WASI `proc_exit`). Runners treat code 0 as a clean exit.
59pub fn exit(code: i32) -> ! {
60    unsafe { sys::proc_exit(code) }
61}
62
63// ---- video / audio / input / assets --------------------------------------------
64
65/// Present an RGBA8 frame (`stride` bytes per row).
66pub fn present(rgba: &[u8], width: u32, height: u32, stride: u32) {
67    assert!(rgba.len() >= (stride * (height - 1) + width * 4) as usize, "present: buffer too small");
68    unsafe { sys::video_present(rgba.as_ptr(), width, height, stride) }
69}
70
71pub mod audio {
72    use std::sync::atomic::{AtomicU32, Ordering};
73    static CHANNELS: AtomicU32 = AtomicU32::new(2);
74
75    /// Format for [`push`]: sample rate (8–192 kHz) and 1 or 2 channels.
76    pub fn config(sample_rate: u32, channels: u32) {
77        CHANNELS.store(channels, Ordering::Relaxed);
78        unsafe { crate::sys::audio_config(sample_rate, channels) }
79    }
80
81    /// Queue interleaved f32 samples in [-1, 1].
82    pub fn push(samples: &[f32]) {
83        let ch = CHANNELS.load(Ordering::Relaxed).max(1);
84        unsafe { crate::sys::audio_push(samples.as_ptr(), samples.len() as u32 / ch) }
85    }
86}
87
88/// Virtual gamepad buttons (bit positions per spec/ABI.md).
89#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90pub struct Buttons(pub u32);
91
92impl Buttons {
93    pub const A: u32 = 1 << 0;
94    pub const B: u32 = 1 << 1;
95    pub const X: u32 = 1 << 2;
96    pub const Y: u32 = 1 << 3;
97    pub const L: u32 = 1 << 4;
98    pub const R: u32 = 1 << 5;
99    pub const SELECT: u32 = 1 << 6;
100    pub const START: u32 = 1 << 7;
101    pub const UP: u32 = 1 << 8;
102    pub const DOWN: u32 = 1 << 9;
103    pub const LEFT: u32 = 1 << 10;
104    pub const RIGHT: u32 = 1 << 11;
105
106    pub fn held(self, mask: u32) -> bool {
107        self.0 & mask != 0
108    }
109}
110
111/// Buttons held on virtual pad `player` (0..=3) this frame.
112pub fn pad(player: u32) -> Buttons {
113    Buttons(unsafe { sys::input_pad(player) })
114}
115
116/// Read a whole asset.
117pub fn asset(name: &str) -> Option<Vec<u8>> {
118    let n = unsafe { sys::asset_size(name.as_ptr(), name.len() as u32) };
119    if n < 0 {
120        return None;
121    }
122    let mut buf = vec![0u8; n as usize];
123    let got = unsafe { sys::asset_read(name.as_ptr(), name.len() as u32, buf.as_mut_ptr(), n as u32) };
124    buf.truncate(got.max(0) as usize);
125    Some(buf)
126}
127
128/// Read up to `buf.len()` bytes of an asset starting at `offset` (for streaming
129/// large assets). Returns bytes read (0 at the end), or `None` if missing.
130pub fn asset_read_at(name: &str, offset: u32, buf: &mut [u8]) -> Option<usize> {
131    let n = unsafe {
132        sys::asset_read_at(name.as_ptr(), name.len() as u32, offset, buf.as_mut_ptr(), buf.len() as u32)
133    };
134    (n >= 0).then_some(n as usize)
135}
136
137// ---- storage ------------------------------------------------------------------------
138
139/// Persistent per-game key/value store (`gasm:storage`): saves, settings, scores.
140///
141/// Keys: 1–128 bytes of `[A-Za-z0-9._-]`. Values up to 1 MiB, 16 MiB per game.
142/// The runner picks the namespace; headless runs start with an empty store.
143pub mod storage {
144    use crate::sys;
145
146    pub fn get(key: &str) -> Option<Vec<u8>> {
147        let n = unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, std::ptr::null_mut(), 0) };
148        if n < 0 {
149            return None;
150        }
151        let mut buf = vec![0u8; n as usize];
152        unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, buf.as_mut_ptr(), n as u32) };
153        Some(buf)
154    }
155
156    /// Store a value. False on invalid key, size/quota limits or I/O error.
157    pub fn set(key: &str, value: &[u8]) -> bool {
158        unsafe { sys::storage_set(key.as_ptr(), key.len() as u32, value.as_ptr(), value.len() as u32) == 0 }
159    }
160
161    /// Delete a key. False if it didn't exist.
162    pub fn delete(key: &str) -> bool {
163        unsafe { sys::storage_delete(key.as_ptr(), key.len() as u32) == 0 }
164    }
165}
166
167// ---- gfx ---------------------------------------------------------------------------
168
169/// GPU rendering through `gasm:gfx` (a WebGPU subset; descriptors are JSON).
170pub mod gfx {
171    use crate::sys;
172
173    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
174    pub struct Shader(pub u32);
175    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
176    pub struct Buffer(pub u32);
177    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
178    pub struct Pipeline(pub u32);
179    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
180    pub struct BindGroup(pub u32);
181
182    pub const COPY_DST: u32 = 0x08;
183    pub const INDEX: u32 = 0x10;
184    pub const VERTEX: u32 = 0x20;
185    pub const UNIFORM: u32 = 0x40;
186
187    #[derive(Clone, Copy)]
188    pub enum IndexFormat {
189        U16 = 0,
190        U32 = 1,
191    }
192
193    /// Plain-old-data that can be uploaded byte for byte.
194    ///
195    /// # Safety
196    /// Implementors must have no padding and no pointers.
197    pub unsafe trait Pod: Copy {}
198    unsafe impl Pod for u8 {}
199    unsafe impl Pod for u16 {}
200    unsafe impl Pod for u32 {}
201    unsafe impl Pod for f32 {}
202    unsafe impl<T: Pod, const N: usize> Pod for [T; N] {}
203
204    pub fn as_bytes<T: Pod>(data: &[T]) -> &[u8] {
205        unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
206    }
207
208    /// Drawable size in pixels.
209    pub fn size() -> (u32, u32) {
210        unsafe { (sys::gfx_width(), sys::gfx_height()) }
211    }
212    pub fn create_shader(wgsl: &str) -> Shader {
213        Shader(unsafe { sys::gfx_create_shader(wgsl.as_ptr(), wgsl.len() as u32) })
214    }
215    pub fn create_buffer(size: u32, usage: u32) -> Buffer {
216        Buffer(unsafe { sys::gfx_create_buffer(size, usage) })
217    }
218    /// WebGPU `GPURenderPipelineDescriptor` as JSON (handles as numbers,
219    /// color format `"surface"`, depth format `"depth24plus"`).
220    pub fn create_pipeline(json: &str) -> Pipeline {
221        Pipeline(unsafe { sys::gfx_create_pipeline(json.as_ptr(), json.len() as u32) })
222    }
223    /// `{"pipeline":P,"group":0,"entries":[{"binding":0,"buffer":B,"offset":0,"size":N}]}`
224    pub fn create_bind_group(json: &str) -> BindGroup {
225        BindGroup(unsafe { sys::gfx_create_bind_group(json.as_ptr(), json.len() as u32) })
226    }
227    /// Offset and byte length must be multiples of 4.
228    pub fn write_buffer<T: Pod>(buf: Buffer, offset: u32, data: &[T]) {
229        let b = as_bytes(data);
230        unsafe { sys::gfx_write_buffer(buf.0, offset, b.as_ptr(), b.len() as u32) }
231    }
232    /// Returns false if the runner will discard this frame (you may skip drawing).
233    pub fn begin_frame(clear: [f32; 4]) -> bool {
234        unsafe { sys::gfx_begin_frame(clear[0], clear[1], clear[2], clear[3]) != 0 }
235    }
236    pub fn set_pipeline(p: Pipeline) {
237        unsafe { sys::gfx_set_pipeline(p.0) }
238    }
239    pub fn set_bind_group(index: u32, bg: BindGroup) {
240        unsafe { sys::gfx_set_bind_group(index, bg.0) }
241    }
242    pub fn set_vertex_buffer(slot: u32, buf: Buffer, offset: u32) {
243        unsafe { sys::gfx_set_vertex_buffer(slot, buf.0, offset) }
244    }
245    pub fn set_index_buffer(buf: Buffer, format: IndexFormat, offset: u32) {
246        unsafe { sys::gfx_set_index_buffer(buf.0, format as u32, offset) }
247    }
248    pub fn draw(vertices: u32, instances: u32, first_vertex: u32, first_instance: u32) {
249        unsafe { sys::gfx_draw(vertices, instances, first_vertex, first_instance) }
250    }
251    pub fn draw_indexed(indices: u32, instances: u32, first_index: u32, base_vertex: i32, first_instance: u32) {
252        unsafe { sys::gfx_draw_indexed(indices, instances, first_index, base_vertex, first_instance) }
253    }
254    pub fn end_frame() {
255        unsafe { sys::gfx_end_frame() }
256    }
257}
258
259// ---- net ---------------------------------------------------------------------------
260
261/// Message connections through `gasm:net` (WebSocket semantics, non-blocking).
262pub mod net {
263    use crate::sys;
264
265    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
266    pub enum State {
267        Connecting,
268        Open,
269        Closed,
270        Error,
271    }
272
273    pub enum Recv {
274        Message(Vec<u8>),
275        Empty,
276        Closed,
277    }
278
279    #[derive(Debug)]
280    pub struct Conn(i32);
281
282    impl Conn {
283        /// Open `ws://` / `wss://`. `None` if the runner denies it.
284        pub fn open(url: &str) -> Option<Conn> {
285            let h = unsafe { sys::net_open(url.as_ptr(), url.len() as u32) };
286            (h > 0).then_some(Conn(h))
287        }
288        pub fn state(&self) -> State {
289            match unsafe { sys::net_state(self.0) } {
290                0 => State::Connecting,
291                1 => State::Open,
292                2 => State::Closed,
293                _ => State::Error,
294            }
295        }
296        /// Send one message. False if the connection is not open.
297        pub fn send(&self, msg: &[u8]) -> bool {
298            !msg.is_empty() && unsafe { sys::net_send(self.0, msg.as_ptr(), msg.len() as u32) } == 0
299        }
300        /// Next queued message, if any.
301        pub fn recv(&self) -> Recv {
302            let mut buf = [0u8; 1024];
303            let n = unsafe { sys::net_recv(self.0, buf.as_mut_ptr(), buf.len() as u32) };
304            match n {
305                0 => Recv::Empty,
306                n if n < 0 => Recv::Closed,
307                n if n as usize <= buf.len() => Recv::Message(buf[..n as usize].to_vec()),
308                n => {
309                    let mut big = vec![0u8; n as usize];
310                    unsafe { sys::net_recv(self.0, big.as_mut_ptr(), n as u32) };
311                    Recv::Message(big)
312                }
313            }
314        }
315    }
316
317    impl Drop for Conn {
318        fn drop(&mut self) {
319            unsafe { sys::net_close(self.0) }
320        }
321    }
322}
323
324// ---- game entry points ----------------------------------------------------------------
325
326/// A gasm game. `init` runs once; `frame` runs at the configured frame rate.
327pub trait Game: Sized + 'static {
328    fn init() -> Result<Self, String>;
329    fn frame(&mut self);
330    /// Called (best effort) when the player closes the game: flush saves here.
331    /// Not called after `gasm::exit`, a trap, or a crash, so also save periodically.
332    fn exit(&mut self) {}
333}
334
335#[doc(hidden)]
336pub struct GameCell<T>(pub UnsafeCell<Option<T>>);
337// Wasm guests are single-threaded; the runner never calls exports concurrently.
338unsafe impl<T> Sync for GameCell<T> {}
339
340#[doc(hidden)]
341pub fn __init<G: Game>(cell: &GameCell<G>) -> i32 {
342    std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))));
343    match G::init() {
344        Ok(g) => {
345            unsafe { *cell.0.get() = Some(g) };
346            0
347        }
348        Err(e) => {
349            log(&format!("init failed: {e}"));
350            1
351        }
352    }
353}
354
355#[doc(hidden)]
356pub fn __frame<G: Game>(cell: &GameCell<G>) {
357    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
358        g.frame();
359    }
360}
361
362#[doc(hidden)]
363pub fn __exit<G: Game>(cell: &GameCell<G>) {
364    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
365        g.exit();
366    }
367}
368
369/// Export the gasm entry points for a [`Game`] type.
370#[macro_export]
371macro_rules! game {
372    ($t:ty) => {
373        static __GASM_GAME: $crate::GameCell<$t> = $crate::GameCell(::std::cell::UnsafeCell::new(None));
374
375        #[unsafe(no_mangle)]
376        pub extern "C" fn gasm_abi_version() -> i32 {
377            $crate::ABI_VERSION
378        }
379        #[unsafe(no_mangle)]
380        pub extern "C" fn gasm_init() -> i32 {
381            $crate::__init::<$t>(&__GASM_GAME)
382        }
383        #[unsafe(no_mangle)]
384        pub extern "C" fn gasm_frame() {
385            $crate::__frame::<$t>(&__GASM_GAME)
386        }
387        #[unsafe(no_mangle)]
388        pub extern "C" fn gasm_exit() {
389            $crate::__exit::<$t>(&__GASM_GAME)
390        }
391    };
392}