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/// Text typed since the previous frame (`'\u{8}'` = backspace, `'\n'` = enter),
117/// or `None` if the runner has no keyboard.
118pub fn text_input() -> Option<String> {
119    let len = unsafe { sys::text_input(std::ptr::null_mut(), 0) };
120    if len < 0 {
121        return None;
122    }
123    let mut buf = vec![0u8; len as usize];
124    if len > 0 {
125        unsafe { sys::text_input(buf.as_mut_ptr(), len as u32) };
126    }
127    Some(String::from_utf8_lossy(&buf).into_owned())
128}
129
130/// All asset names, sorted (by UTF-8 bytes). Folder entries keep their on-disk case.
131pub fn asset_names() -> Vec<String> {
132    let n = unsafe { sys::asset_count() };
133    (0..n)
134        .filter_map(|i| {
135            let len = unsafe { sys::asset_name(i, std::ptr::null_mut(), 0) };
136            if len < 0 {
137                return None;
138            }
139            let mut buf = vec![0u8; len as usize];
140            if len > 0 {
141                unsafe { sys::asset_name(i, buf.as_mut_ptr(), len as u32) };
142            }
143            Some(String::from_utf8_lossy(&buf).into_owned())
144        })
145        .collect()
146}
147
148/// Read a whole asset.
149pub fn asset(name: &str) -> Option<Vec<u8>> {
150    let n = unsafe { sys::asset_size(name.as_ptr(), name.len() as u32) };
151    if n < 0 {
152        return None;
153    }
154    let mut buf = vec![0u8; n as usize];
155    let got = unsafe { sys::asset_read(name.as_ptr(), name.len() as u32, buf.as_mut_ptr(), n as u32) };
156    buf.truncate(got.max(0) as usize);
157    Some(buf)
158}
159
160/// Read up to `buf.len()` bytes of an asset starting at `offset` (for streaming
161/// large assets). Returns bytes read (0 at the end), or `None` if missing.
162pub fn asset_read_at(name: &str, offset: u32, buf: &mut [u8]) -> Option<usize> {
163    let n = unsafe {
164        sys::asset_read_at(name.as_ptr(), name.len() as u32, offset, buf.as_mut_ptr(), buf.len() as u32)
165    };
166    (n >= 0).then_some(n as usize)
167}
168
169// ---- storage ------------------------------------------------------------------------
170
171/// Persistent per-game key/value store (`gasm:storage`): saves, settings, scores.
172///
173/// Keys: 1–128 bytes of `[A-Za-z0-9._-]`. Values up to 1 MiB, 16 MiB per game.
174/// The runner picks the namespace; headless runs start with an empty store.
175pub mod storage {
176    use crate::sys;
177
178    pub fn get(key: &str) -> Option<Vec<u8>> {
179        let n = unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, std::ptr::null_mut(), 0) };
180        if n < 0 {
181            return None;
182        }
183        let mut buf = vec![0u8; n as usize];
184        unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, buf.as_mut_ptr(), n as u32) };
185        Some(buf)
186    }
187
188    /// Store a value. False on invalid key, size/quota limits or I/O error.
189    pub fn set(key: &str, value: &[u8]) -> bool {
190        unsafe { sys::storage_set(key.as_ptr(), key.len() as u32, value.as_ptr(), value.len() as u32) == 0 }
191    }
192
193    /// Delete a key. False if it didn't exist.
194    pub fn delete(key: &str) -> bool {
195        unsafe { sys::storage_delete(key.as_ptr(), key.len() as u32) == 0 }
196    }
197}
198
199// ---- gfx ---------------------------------------------------------------------------
200
201/// GPU rendering through `gasm:gfx` (a WebGPU subset; descriptors are JSON).
202pub mod gfx {
203    use crate::sys;
204
205    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
206    pub struct Shader(pub u32);
207    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
208    pub struct Buffer(pub u32);
209    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
210    pub struct Pipeline(pub u32);
211    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
212    pub struct BindGroup(pub u32);
213    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
214    pub struct BindGroupLayout(pub u32);
215    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
216    pub struct Texture(pub u32);
217    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
218    pub struct Sampler(pub u32);
219
220    pub const COPY_DST: u32 = 0x08;
221    pub const INDEX: u32 = 0x10;
222    pub const VERTEX: u32 = 0x20;
223    pub const UNIFORM: u32 = 0x40;
224    pub const STORAGE: u32 = 0x80;
225
226    /// Bind group layout entry visibility.
227    pub const STAGE_VERTEX: u32 = 0x1;
228    pub const STAGE_FRAGMENT: u32 = 0x2;
229    /// Dynamic offsets must be multiples of this.
230    pub const OFFSET_ALIGNMENT: u32 = 256;
231
232    #[derive(Clone, Copy)]
233    pub enum IndexFormat {
234        U16 = 0,
235        U32 = 1,
236    }
237
238    /// Plain-old-data that can be uploaded byte for byte.
239    ///
240    /// # Safety
241    /// Implementors must have no padding and no pointers.
242    pub unsafe trait Pod: Copy {}
243    unsafe impl Pod for u8 {}
244    unsafe impl Pod for u16 {}
245    unsafe impl Pod for u32 {}
246    unsafe impl Pod for f32 {}
247    unsafe impl<T: Pod, const N: usize> Pod for [T; N] {}
248
249    pub fn as_bytes<T: Pod>(data: &[T]) -> &[u8] {
250        unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
251    }
252
253    /// Drawable size in pixels.
254    pub fn size() -> (u32, u32) {
255        unsafe { (sys::gfx_width(), sys::gfx_height()) }
256    }
257    pub fn create_shader(wgsl: &str) -> Shader {
258        Shader(unsafe { sys::gfx_create_shader(wgsl.as_ptr(), wgsl.len() as u32) })
259    }
260    pub fn create_buffer(size: u32, usage: u32) -> Buffer {
261        Buffer(unsafe { sys::gfx_create_buffer(size, usage) })
262    }
263    /// WebGPU `GPURenderPipelineDescriptor` as JSON (handles as numbers,
264    /// color format `"surface"`, depth format `"depth24plus"`).
265    pub fn create_pipeline(json: &str) -> Pipeline {
266        Pipeline(unsafe { sys::gfx_create_pipeline(json.as_ptr(), json.len() as u32) })
267    }
268    /// `{"layout":L,"entries":[...]}` or `{"pipeline":P,"group":0,"entries":[...]}`; entries
269    /// `{"binding":0,"buffer":B,"offset":0,"size":N}`, `{"binding":1,"texture":T}`, `{"binding":2,"sampler":S}`.
270    pub fn create_bind_group(json: &str) -> BindGroup {
271        BindGroup(unsafe { sys::gfx_create_bind_group(json.as_ptr(), json.len() as u32) })
272    }
273    /// `{"entries":[{"binding":0,"visibility":STAGE_VERTEX,"buffer":{"type":"uniform","hasDynamicOffset":true}}, ...]}`.
274    /// Bind groups made from it work with every pipeline whose `"layout":[...]` lists it.
275    pub fn create_bind_group_layout(json: &str) -> BindGroupLayout {
276        BindGroupLayout(unsafe { sys::gfx_create_bind_group_layout(json.as_ptr(), json.len() as u32) })
277    }
278    /// `{"size":[w,h],"format":"rgba8unorm","mipLevelCount":n}`
279    pub fn create_texture(json: &str) -> Texture {
280        Texture(unsafe { sys::gfx_create_texture(json.as_ptr(), json.len() as u32) })
281    }
282    /// Upload a tightly packed RGBA8 region (`rgba.len() == w * h * 4`) of mip level `mip`.
283    pub fn write_texture(t: Texture, mip: u32, x: u32, y: u32, w: u32, h: u32, rgba: &[u8]) {
284        unsafe { sys::gfx_write_texture(t.0, mip, x, y, w, h, rgba.as_ptr(), rgba.len() as u32) }
285    }
286    /// `{"addressModeU":"repeat","magFilter":"linear","minFilter":"linear","mipmapFilter":"linear"}`
287    pub fn create_sampler(json: &str) -> Sampler {
288        Sampler(unsafe { sys::gfx_create_sampler(json.as_ptr(), json.len() as u32) })
289    }
290    /// Offset and byte length must be multiples of 4.
291    pub fn write_buffer<T: Pod>(buf: Buffer, offset: u32, data: &[T]) {
292        let b = as_bytes(data);
293        unsafe { sys::gfx_write_buffer(buf.0, offset, b.as_ptr(), b.len() as u32) }
294    }
295    /// Returns false if the runner will discard this frame (you may skip drawing).
296    pub fn begin_frame(clear: [f32; 4]) -> bool {
297        unsafe { sys::gfx_begin_frame(clear[0], clear[1], clear[2], clear[3]) != 0 }
298    }
299    pub fn set_pipeline(p: Pipeline) {
300        unsafe { sys::gfx_set_pipeline(p.0) }
301    }
302    pub fn set_bind_group(index: u32, bg: BindGroup) {
303        unsafe { sys::gfx_set_bind_group(index, bg.0) }
304    }
305    /// One offset (multiple of [`OFFSET_ALIGNMENT`]) per dynamic-offset entry, in binding order.
306    pub fn set_bind_group_offsets(index: u32, bg: BindGroup, offsets: &[u32]) {
307        unsafe { sys::gfx_set_bind_group_offsets(index, bg.0, offsets.as_ptr(), offsets.len() as u32) }
308    }
309    /// Viewport in drawable pixels (clamped to the drawable), depth range 0..=1.
310    pub fn set_viewport(x: f32, y: f32, w: f32, h: f32, min_depth: f32, max_depth: f32) {
311        unsafe { sys::gfx_set_viewport(x, y, w, h, min_depth, max_depth) }
312    }
313    /// Scissor rectangle in drawable pixels (clamped to the drawable).
314    pub fn set_scissor_rect(x: u32, y: u32, w: u32, h: u32) {
315        unsafe { sys::gfx_set_scissor_rect(x, y, w, h) }
316    }
317    pub fn set_vertex_buffer(slot: u32, buf: Buffer, offset: u32) {
318        unsafe { sys::gfx_set_vertex_buffer(slot, buf.0, offset) }
319    }
320    pub fn set_index_buffer(buf: Buffer, format: IndexFormat, offset: u32) {
321        unsafe { sys::gfx_set_index_buffer(buf.0, format as u32, offset) }
322    }
323    pub fn draw(vertices: u32, instances: u32, first_vertex: u32, first_instance: u32) {
324        unsafe { sys::gfx_draw(vertices, instances, first_vertex, first_instance) }
325    }
326    pub fn draw_indexed(indices: u32, instances: u32, first_index: u32, base_vertex: i32, first_instance: u32) {
327        unsafe { sys::gfx_draw_indexed(indices, instances, first_index, base_vertex, first_instance) }
328    }
329    pub fn end_frame() {
330        unsafe { sys::gfx_end_frame() }
331    }
332}
333
334// ---- net ---------------------------------------------------------------------------
335
336/// Message connections through `gasm:net` (WebSocket semantics, non-blocking).
337pub mod net {
338    use crate::sys;
339
340    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
341    pub enum State {
342        Connecting,
343        Open,
344        Closed,
345        Error,
346    }
347
348    pub enum Recv {
349        Message(Vec<u8>),
350        Empty,
351        Closed,
352    }
353
354    #[derive(Debug)]
355    pub struct Conn(i32);
356
357    impl Conn {
358        /// Open `ws://` / `wss://`. `None` if the runner denies it.
359        pub fn open(url: &str) -> Option<Conn> {
360            let h = unsafe { sys::net_open(url.as_ptr(), url.len() as u32) };
361            (h > 0).then_some(Conn(h))
362        }
363        pub fn state(&self) -> State {
364            match unsafe { sys::net_state(self.0) } {
365                0 => State::Connecting,
366                1 => State::Open,
367                2 => State::Closed,
368                _ => State::Error,
369            }
370        }
371        /// Send one message. False if the connection is not open.
372        pub fn send(&self, msg: &[u8]) -> bool {
373            !msg.is_empty() && unsafe { sys::net_send(self.0, msg.as_ptr(), msg.len() as u32) } == 0
374        }
375        /// Next queued message, if any.
376        pub fn recv(&self) -> Recv {
377            let mut buf = [0u8; 1024];
378            let n = unsafe { sys::net_recv(self.0, buf.as_mut_ptr(), buf.len() as u32) };
379            match n {
380                0 => Recv::Empty,
381                n if n < 0 => Recv::Closed,
382                n if n as usize <= buf.len() => Recv::Message(buf[..n as usize].to_vec()),
383                n => {
384                    let mut big = vec![0u8; n as usize];
385                    unsafe { sys::net_recv(self.0, big.as_mut_ptr(), n as u32) };
386                    Recv::Message(big)
387                }
388            }
389        }
390    }
391
392    impl Drop for Conn {
393        fn drop(&mut self) {
394            unsafe { sys::net_close(self.0) }
395        }
396    }
397}
398
399// ---- game entry points ----------------------------------------------------------------
400
401/// A gasm game. `init` runs once; `frame` runs at the configured frame rate.
402pub trait Game: Sized + 'static {
403    fn init() -> Result<Self, String>;
404    fn frame(&mut self);
405    /// Called (best effort) when the player closes the game: flush saves here.
406    /// Not called after `gasm::exit`, a trap, or a crash, so also save periodically.
407    fn exit(&mut self) {}
408}
409
410#[doc(hidden)]
411pub struct GameCell<T>(pub UnsafeCell<Option<T>>);
412// Wasm guests are single-threaded; the runner never calls exports concurrently.
413unsafe impl<T> Sync for GameCell<T> {}
414
415#[doc(hidden)]
416pub fn __init<G: Game>(cell: &GameCell<G>) -> i32 {
417    std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))));
418    match G::init() {
419        Ok(g) => {
420            unsafe { *cell.0.get() = Some(g) };
421            0
422        }
423        Err(e) => {
424            log(&format!("init failed: {e}"));
425            1
426        }
427    }
428}
429
430#[doc(hidden)]
431pub fn __frame<G: Game>(cell: &GameCell<G>) {
432    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
433        g.frame();
434    }
435}
436
437#[doc(hidden)]
438pub fn __exit<G: Game>(cell: &GameCell<G>) {
439    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
440        g.exit();
441    }
442}
443
444/// Export the gasm entry points for a [`Game`] type.
445#[macro_export]
446macro_rules! game {
447    ($t:ty) => {
448        static __GASM_GAME: $crate::GameCell<$t> = $crate::GameCell(::std::cell::UnsafeCell::new(None));
449
450        #[unsafe(no_mangle)]
451        pub extern "C" fn gasm_abi_version() -> i32 {
452            $crate::ABI_VERSION
453        }
454        #[unsafe(no_mangle)]
455        pub extern "C" fn gasm_init() -> i32 {
456            $crate::__init::<$t>(&__GASM_GAME)
457        }
458        #[unsafe(no_mangle)]
459        pub extern "C" fn gasm_frame() {
460            $crate::__frame::<$t>(&__GASM_GAME)
461        }
462        #[unsafe(no_mangle)]
463        pub extern "C" fn gasm_exit() {
464            $crate::__exit::<$t>(&__GASM_GAME)
465        }
466    };
467}