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/// Does the runner provide an import? A module (`"gasm:gfx"`) or a function in
43/// one (`"gasm.asset_size64"`, `"gasm:gfx.destroy"`). Calling an import the
44/// runner lacks traps, so probe optional features first.
45pub fn has(name: &str) -> bool {
46    unsafe { sys::has(name.as_ptr(), name.len() as u32) == 1 }
47}
48
49/// Rate at which the runner calls `frame()`. Default 60.
50pub fn set_frame_rate(hz: f64) {
51    unsafe { sys::set_frame_rate(hz) }
52}
53
54/// Show `video_present` frames at display aspect `num:den` (e.g. 4:3 for a
55/// 320×200 game drawn for a CRT) instead of square pixels; `(0, 0)` resets.
56/// Returns false on runners without it: the game then corrects the aspect itself
57/// (or accepts square pixels). Display only: hashes don't change.
58pub fn video_set_aspect(num: u32, den: u32) -> bool {
59    static SUPPORTED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
60    let ok = *SUPPORTED.get_or_init(|| has("gasm.video_set_aspect"));
61    if ok {
62        unsafe { sys::video_set_aspect(num, den) }
63    }
64    ok
65}
66
67/// The game's built-in title (custom section `gasm.title`): runners show it
68/// (and launchers list it) before the game runs and while it hasn't called
69/// [`set_title`]. Use once, at the top level: `gasm::title!("Sumo");`
70#[macro_export]
71macro_rules! title {
72    ($t:literal) => {
73        // a wasm custom section; native builds of a game (tests, parity) just keep the bytes
74        #[cfg_attr(target_arch = "wasm32", unsafe(link_section = "gasm.title"))]
75        #[used]
76        static __GASM_TITLE: [u8; $t.len()] = $crate::__title_bytes($t);
77    };
78}
79
80#[doc(hidden)]
81pub const fn __title_bytes<const N: usize>(s: &str) -> [u8; N] {
82    let b = s.as_bytes();
83    let mut out = [0u8; N];
84    let mut i = 0;
85    while i < N {
86        out[i] = b[i];
87        i += 1;
88    }
89    out
90}
91
92/// Name the game's window or browser tab (runners add their own suffix, e.g.
93/// "DOOM — gasm"). Control characters are dropped, at most 256 bytes are kept,
94/// and `""` restores the default (the file name). Does nothing on runners
95/// without it.
96pub fn set_title(title: &str) {
97    static SUPPORTED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
98    if *SUPPORTED.get_or_init(|| has("gasm.set_title")) {
99        unsafe { sys::set_title(title.as_ptr(), title.len() as u32) }
100    }
101}
102
103/// Launch parameter (`--param k=v` natively, `?k=v` in the browser).
104pub fn param(name: &str) -> Option<String> {
105    let len = unsafe { sys::param(name.as_ptr(), name.len() as u32, std::ptr::null_mut(), 0) };
106    if len < 0 {
107        return None;
108    }
109    let mut buf = vec![0u8; len as usize];
110    unsafe { sys::param(name.as_ptr(), name.len() as u32, buf.as_mut_ptr(), len as u32) };
111    Some(String::from_utf8_lossy(&buf).into_owned())
112}
113
114/// End the game (WASI `proc_exit`). Runners treat code 0 as a clean exit.
115pub fn exit(code: i32) -> ! {
116    unsafe { sys::proc_exit(code) }
117}
118
119// ---- video / audio / input / assets --------------------------------------------
120
121/// Present an RGBA8 frame (`stride` bytes per row; 1-4096 pixels each way).
122pub fn present(rgba: &[u8], width: u32, height: u32, stride: u32) {
123    let need = (stride as u64)
124        .checked_mul(height.saturating_sub(1) as u64)
125        .and_then(|n| n.checked_add(width as u64 * 4))
126        .filter(|_| width > 0 && height > 0 && stride as u64 >= width as u64 * 4);
127    assert!(need.is_some_and(|n| rgba.len() as u64 >= n), "present: bad geometry or buffer too small");
128    unsafe { sys::video_present(rgba.as_ptr(), width, height, stride) }
129}
130
131pub mod audio {
132    use std::sync::atomic::{AtomicU32, Ordering};
133    static CHANNELS: AtomicU32 = AtomicU32::new(2);
134
135    /// Format for [`push`]: sample rate (8–192 kHz) and 1 or 2 channels. Other
136    /// values are ignored (by the runner too): the format stays as it was.
137    pub fn config(sample_rate: u32, channels: u32) {
138        if (8000..=192_000).contains(&sample_rate) && (channels == 1 || channels == 2) {
139            CHANNELS.store(channels, Ordering::Relaxed);
140        }
141        unsafe { crate::sys::audio_config(sample_rate, channels) }
142    }
143
144    /// Queue interleaved f32 samples in [-1, 1] (whole frames: a length that is a
145    /// multiple of the channel count).
146    pub fn push(samples: &[f32]) {
147        let ch = CHANNELS.load(Ordering::Relaxed);
148        debug_assert!(samples.len() % ch as usize == 0, "audio::push: not a whole number of frames");
149        unsafe { crate::sys::audio_push(samples.as_ptr(), samples.len() as u32 / ch) }
150    }
151}
152
153/// Virtual gamepad buttons (bit positions per spec/ABI.md).
154#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
155pub struct Buttons(pub u32);
156
157impl Buttons {
158    pub const A: u32 = sys::GASM_BTN_A;
159    pub const B: u32 = sys::GASM_BTN_B;
160    pub const X: u32 = sys::GASM_BTN_X;
161    pub const Y: u32 = sys::GASM_BTN_Y;
162    pub const L: u32 = sys::GASM_BTN_L;
163    pub const R: u32 = sys::GASM_BTN_R;
164    pub const SELECT: u32 = sys::GASM_BTN_SELECT;
165    pub const START: u32 = sys::GASM_BTN_START;
166    pub const UP: u32 = sys::GASM_BTN_UP;
167    pub const DOWN: u32 = sys::GASM_BTN_DOWN;
168    pub const LEFT: u32 = sys::GASM_BTN_LEFT;
169    pub const RIGHT: u32 = sys::GASM_BTN_RIGHT;
170
171    pub fn held(self, mask: u32) -> bool {
172        self.0 & mask != 0
173    }
174}
175
176/// Buttons held on virtual pad `player` (0..=3) this frame.
177pub fn pad(player: u32) -> Buttons {
178    Buttons(unsafe { sys::input_pad(player) })
179}
180
181/// Text typed since the previous frame (`'\u{8}'` = backspace, `'\n'` = enter),
182/// or `None` if the runner has no keyboard.
183pub fn text_input() -> Option<String> {
184    let len = unsafe { sys::text_input(std::ptr::null_mut(), 0) };
185    if len < 0 {
186        return None;
187    }
188    let mut buf = vec![0u8; len as usize];
189    if len > 0 {
190        unsafe { sys::text_input(buf.as_mut_ptr(), len as u32) };
191    }
192    Some(String::from_utf8_lossy(&buf).into_owned())
193}
194
195/// All asset names, sorted (by UTF-8 bytes). Folder entries keep their on-disk case.
196pub fn asset_names() -> Vec<String> {
197    let n = unsafe { sys::asset_count() };
198    (0..n)
199        .filter_map(|i| {
200            let len = unsafe { sys::asset_name(i, std::ptr::null_mut(), 0) };
201            if len < 0 {
202                return None;
203            }
204            let mut buf = vec![0u8; len as usize];
205            if len > 0 {
206                unsafe { sys::asset_name(i, buf.as_mut_ptr(), len as u32) };
207            }
208            Some(String::from_utf8_lossy(&buf).into_owned())
209        })
210        .collect()
211}
212
213/// Size of an asset in bytes (any size), or `None` if it doesn't exist.
214pub fn asset_size(name: &str) -> Option<u64> {
215    let n = unsafe { sys::asset_size64(name.as_ptr(), name.len() as u32) };
216    (n >= 0).then_some(n as u64)
217}
218
219/// Read a whole asset (it must fit in memory: under 2 GiB).
220pub fn asset(name: &str) -> Option<Vec<u8>> {
221    let n = unsafe { sys::asset_size(name.as_ptr(), name.len() as u32) };
222    if n < 0 {
223        return None;
224    }
225    let mut buf = vec![0u8; n as usize];
226    let got = unsafe { sys::asset_read(name.as_ptr(), name.len() as u32, buf.as_mut_ptr(), n as u32) };
227    buf.truncate(got.max(0) as usize);
228    Some(buf)
229}
230
231/// Read up to `buf.len()` bytes of an asset starting at `offset` (for streaming
232/// large assets). Returns bytes read (0 at the end), or `None` if missing.
233pub fn asset_read_at(name: &str, offset: u64, buf: &mut [u8]) -> Option<usize> {
234    let len = buf.len().min(u32::MAX as usize) as u32;
235    let n = unsafe { sys::asset_read_at64(name.as_ptr(), name.len() as u32, offset, buf.as_mut_ptr(), len) };
236    (n >= 0).then_some(n as usize)
237}
238
239// ---- storage ------------------------------------------------------------------------
240
241/// Persistent per-game key/value store (`gasm:storage`): saves, settings, scores.
242///
243/// Keys: 1–128 bytes of `[A-Za-z0-9._-]`. Values up to 1 MiB, 16 MiB per game.
244/// The runner picks the namespace; headless runs start with an empty store.
245pub mod storage {
246    use crate::sys;
247
248    pub fn get(key: &str) -> Option<Vec<u8>> {
249        let n = unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, std::ptr::null_mut(), 0) };
250        if n < 0 {
251            return None;
252        }
253        let mut buf = vec![0u8; n as usize];
254        unsafe { sys::storage_get(key.as_ptr(), key.len() as u32, buf.as_mut_ptr(), n as u32) };
255        Some(buf)
256    }
257
258    /// Why [`try_set`] failed.
259    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
260    pub enum Error {
261        /// not 1–128 bytes of `[A-Za-z0-9._-]`
262        Key,
263        /// value over 1 MiB
264        Size,
265        /// the game's 16 MiB are used up
266        Quota,
267        /// the runner couldn't write it
268        Io,
269    }
270
271    /// Store a value.
272    pub fn try_set(key: &str, value: &[u8]) -> Result<(), Error> {
273        match unsafe { sys::storage_set(key.as_ptr(), key.len() as u32, value.as_ptr(), value.len() as u32) } {
274            sys::GASM_STORAGE_OK => Ok(()),
275            sys::GASM_STORAGE_ERR_KEY => Err(Error::Key),
276            sys::GASM_STORAGE_ERR_SIZE => Err(Error::Size),
277            sys::GASM_STORAGE_ERR_QUOTA => Err(Error::Quota),
278            _ => Err(Error::Io),
279        }
280    }
281
282    /// Store a value. False on invalid key, size/quota limits or I/O error ([`try_set`] says which).
283    pub fn set(key: &str, value: &[u8]) -> bool {
284        try_set(key, value).is_ok()
285    }
286
287    /// Delete a key. False if it didn't exist.
288    pub fn delete(key: &str) -> bool {
289        unsafe { sys::storage_delete(key.as_ptr(), key.len() as u32) == 0 }
290    }
291
292    /// All keys of this game, sorted.
293    pub fn keys() -> Vec<String> {
294        let n = unsafe { sys::storage_count() };
295        (0..n)
296            .filter_map(|i| {
297                let len = unsafe { sys::storage_key(i, std::ptr::null_mut(), 0) };
298                if len < 0 {
299                    return None;
300                }
301                let mut buf = vec![0u8; len as usize];
302                unsafe { sys::storage_key(i, buf.as_mut_ptr(), len as u32) };
303                Some(String::from_utf8_lossy(&buf).into_owned())
304            })
305            .collect()
306    }
307}
308
309// ---- raw input ---------------------------------------------------------------------
310
311/// Raw keyboard, pointer and gamepads (next to the virtual [`pad`]s).
312///
313/// ```ignore
314/// use gasm::{input, keys};
315/// input::set_mode(input::KEYS_RAW);              // read the keyboard directly, no keymap pads
316/// let k = input::keys().unwrap_or_default();
317/// if k.held(keys::SHIFT_LEFT) && k.held(keys::ARROW_LEFT) { … }
318/// if let Some(p) = input::pointer() && p.pressed & input::MOUSE_LEFT != 0 { click(p.frame_x, p.frame_y) }
319/// ```
320pub mod input {
321    use crate::sys;
322
323    /// The runner stops mapping the keyboard to pads (gamepads still map).
324    pub const KEYS_RAW: u32 = sys::GASM_INPUT_KEYS_RAW;
325    /// Hide the system cursor over the game.
326    pub const POINTER_HIDDEN: u32 = sys::GASM_INPUT_POINTER_HIDDEN;
327    /// Capture the pointer for relative motion (best effort; the browser needs a click).
328    pub const POINTER_LOCKED: u32 = sys::GASM_INPUT_POINTER_LOCKED;
329
330    pub const MOUSE_LEFT: u32 = sys::GASM_MOUSE_LEFT;
331    pub const MOUSE_RIGHT: u32 = sys::GASM_MOUSE_RIGHT;
332    pub const MOUSE_MIDDLE: u32 = sys::GASM_MOUSE_MIDDLE;
333    pub const MOUSE_BACK: u32 = sys::GASM_MOUSE_BACK;
334    pub const MOUSE_FORWARD: u32 = sys::GASM_MOUSE_FORWARD;
335
336    const KEY_BYTES: usize = sys::GASM_KEY_STATE_BYTES as usize;
337    const POINTER_BYTES: usize = sys::GASM_POINTER_BYTES as usize;
338    const GAMEPAD_BYTES: usize = sys::GASM_GAMEPAD_BYTES as usize;
339
340    /// `KEYS_RAW | POINTER_HIDDEN | POINTER_LOCKED`, any combination.
341    pub fn set_mode(flags: u32) {
342        unsafe { sys::input_mode(flags) }
343    }
344
345    /// Keys held this frame; index with [`crate::keys`] constants.
346    #[derive(Clone, Copy, Default, PartialEq, Eq)]
347    pub struct Keys(pub [u8; KEY_BYTES]);
348
349    impl Keys {
350        pub fn held(&self, code: u32) -> bool {
351            (code as usize) < KEY_BYTES * 8 && self.0[code as usize / 8] & (1 << (code % 8)) != 0
352        }
353    }
354
355    /// `None` if the runner has no keyboard.
356    pub fn keys() -> Option<Keys> {
357        let mut k = Keys::default();
358        (unsafe { sys::key_state(k.0.as_mut_ptr(), KEY_BYTES as u32) } >= 0).then_some(k)
359    }
360
361    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
362    pub struct KeyEvent {
363        pub code: u32,
364        pub down: bool,
365    }
366
367    /// Presses and releases since the previous frame, in order (`None`: no keyboard).
368    pub fn key_events() -> Option<Vec<KeyEvent>> {
369        let n = unsafe { sys::key_events(std::ptr::null_mut(), 0) };
370        if n < 0 {
371            return None;
372        }
373        let mut b = vec![0u8; n as usize];
374        if n > 0 {
375            unsafe { sys::key_events(b.as_mut_ptr(), n as u32) };
376        }
377        Some(b.chunks_exact(sys::GASM_KEY_EVENT_BYTES as usize).map(|e| KeyEvent { code: u16::from_le_bytes([e[0], e[1]]) as u32, down: e[2] != 0 }).collect())
378    }
379
380    #[derive(Clone, Copy, Debug, Default, PartialEq)]
381    pub struct Pointer {
382        /// position in drawable pixels (gfx width/height space)
383        pub x: f32,
384        pub y: f32,
385        /// position in the last presented frame's pixels (2D games)
386        pub frame_x: f32,
387        pub frame_y: f32,
388        /// relative motion since the previous frame (also while locked)
389        pub dx: f32,
390        pub dy: f32,
391        /// about 1 per wheel notch; y > 0 = down
392        pub wheel_x: f32,
393        pub wheel_y: f32,
394        pub buttons: u32,
395        /// went down / up since the previous frame (a quick click shows in both)
396        pub pressed: u32,
397        pub released: u32,
398        pub flags: u32,
399    }
400
401    impl Pointer {
402        pub fn inside(&self) -> bool {
403            self.flags & 1 != 0
404        }
405        pub fn locked(&self) -> bool {
406            self.flags & 4 != 0
407        }
408    }
409
410    /// `None` if the runner has no pointer.
411    pub fn pointer() -> Option<Pointer> {
412        let mut b = [0u8; POINTER_BYTES];
413        if unsafe { sys::pointer(b.as_mut_ptr(), POINTER_BYTES as u32) } < 0 {
414            return None;
415        }
416        let f = |o: u32| { let i = o as usize; f32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) };
417        let u = |o: u32| { let i = o as usize; u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) };
418        Some(Pointer {
419            x: f(sys::GASM_POINTER_OFF_X), y: f(sys::GASM_POINTER_OFF_Y),
420            frame_x: f(sys::GASM_POINTER_OFF_FX), frame_y: f(sys::GASM_POINTER_OFF_FY),
421            dx: f(sys::GASM_POINTER_OFF_DX), dy: f(sys::GASM_POINTER_OFF_DY),
422            wheel_x: f(sys::GASM_POINTER_OFF_WHEEL_X), wheel_y: f(sys::GASM_POINTER_OFF_WHEEL_Y),
423            buttons: u(sys::GASM_POINTER_OFF_BUTTONS), pressed: u(sys::GASM_POINTER_OFF_PRESSED),
424            released: u(sys::GASM_POINTER_OFF_RELEASED), flags: u(sys::GASM_POINTER_OFF_FLAGS),
425        })
426    }
427
428    /// Raw gamepad or joystick. With `standard`, W3C order: buttons 0 south, 1 east,
429    /// 2 west, 3 north, 4/5 shoulders, 6/7 triggers, 8 select, 9 start, 10/11 sticks,
430    /// 12-15 d-pad, 16 home; axes 0/1 left stick, 2/3 right stick (y > 0 = down).
431    #[derive(Clone, Debug, Default, PartialEq)]
432    pub struct Gamepad {
433        pub connected: bool,
434        pub standard: bool,
435        pub buttons: Vec<f32>,
436        pub axes: Vec<f32>,
437    }
438
439    /// Slot 0-3 (connection order). `None` if the runner has no gamepad support.
440    pub fn gamepad(slot: u32) -> Option<Gamepad> {
441        let mut b = [0u8; GAMEPAD_BYTES];
442        if unsafe { sys::gamepad(slot, b.as_mut_ptr(), GAMEPAD_BYTES as u32) } < 0 {
443            return None;
444        }
445        let u = |i: usize| u32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
446        let f = |i: usize| f32::from_le_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]);
447        let (flags, nb, na) = (
448            u(sys::GASM_GAMEPAD_OFF_FLAGS as usize),
449            (u(sys::GASM_GAMEPAD_OFF_BUTTON_COUNT as usize) as usize).min(sys::GASM_GAMEPAD_BUTTONS as usize),
450            (u(sys::GASM_GAMEPAD_OFF_AXIS_COUNT as usize) as usize).min(sys::GASM_GAMEPAD_AXES as usize),
451        );
452        Some(Gamepad {
453            connected: flags & sys::GASM_GAMEPAD_CONNECTED != 0,
454            standard: flags & sys::GASM_GAMEPAD_STANDARD != 0,
455            buttons: (0..nb).map(|i| f(sys::GASM_GAMEPAD_OFF_BUTTONS as usize + i * 4)).collect(),
456            axes: (0..na).map(|i| f(sys::GASM_GAMEPAD_OFF_AXES as usize + i * 4)).collect(),
457        })
458    }
459
460    /// Device name, if a gamepad is connected in `slot`.
461    pub fn gamepad_name(slot: u32) -> Option<String> {
462        let n = unsafe { sys::gamepad_name(slot, std::ptr::null_mut(), 0) };
463        if n < 0 {
464            return None;
465        }
466        let mut b = vec![0u8; n as usize];
467        if n > 0 {
468            unsafe { sys::gamepad_name(slot, b.as_mut_ptr(), n as u32) };
469        }
470        Some(String::from_utf8_lossy(&b).into_owned())
471    }
472}
473
474/// Raw key codes (`GASM_KEY_*`, W3C `KeyboardEvent.code` names in [`keys::NAMES`]).
475pub use sys::keys;
476
477// ---- gfx ---------------------------------------------------------------------------
478
479/// GPU rendering through `gasm:gfx` (a WebGPU subset; descriptors are JSON).
480pub mod gfx {
481    use crate::sys;
482
483    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
484    pub struct Shader(pub u32);
485    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
486    pub struct Buffer(pub u32);
487    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
488    pub struct Pipeline(pub u32);
489    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
490    pub struct BindGroup(pub u32);
491    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
492    pub struct BindGroupLayout(pub u32);
493    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
494    pub struct Texture(pub u32);
495    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
496    pub struct Sampler(pub u32);
497
498    pub const COPY_DST: u32 = sys::GASM_BUF_COPY_DST;
499    pub const INDEX: u32 = sys::GASM_BUF_INDEX;
500    pub const VERTEX: u32 = sys::GASM_BUF_VERTEX;
501    pub const UNIFORM: u32 = sys::GASM_BUF_UNIFORM;
502    pub const STORAGE: u32 = sys::GASM_BUF_STORAGE;
503
504    /// Bind group layout entry visibility.
505    pub const STAGE_VERTEX: u32 = sys::GASM_STAGE_VERTEX;
506    pub const STAGE_FRAGMENT: u32 = sys::GASM_STAGE_FRAGMENT;
507    /// Dynamic offsets must be multiples of this.
508    pub const OFFSET_ALIGNMENT: u32 = 256;
509
510    #[derive(Clone, Copy)]
511    pub enum IndexFormat {
512        U16 = sys::GASM_INDEX_U16 as isize,
513        U32 = sys::GASM_INDEX_U32 as isize,
514    }
515
516    /// Any object handle (for [`destroy`]).
517    pub trait Handle {
518        fn raw(&self) -> u32;
519    }
520    macro_rules! handles {
521        ($($t:ident),*) => { $(impl Handle for $t { fn raw(&self) -> u32 { self.0 } })* };
522    }
523    handles!(Shader, Buffer, Pipeline, BindGroup, BindGroupLayout, Texture, Sampler);
524
525    /// Plain-old-data that can be uploaded byte for byte.
526    ///
527    /// # Safety
528    /// Implementors must have no padding and no pointers.
529    pub unsafe trait Pod: Copy {}
530    unsafe impl Pod for u8 {}
531    unsafe impl Pod for u16 {}
532    unsafe impl Pod for u32 {}
533    unsafe impl Pod for f32 {}
534    unsafe impl<T: Pod, const N: usize> Pod for [T; N] {}
535
536    pub fn as_bytes<T: Pod>(data: &[T]) -> &[u8] {
537        unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
538    }
539
540    /// Drawable size in pixels.
541    pub fn size() -> (u32, u32) {
542        unsafe { (sys::gfx_width(), sys::gfx_height()) }
543    }
544    pub fn create_shader(wgsl: &str) -> Shader {
545        Shader(unsafe { sys::gfx_create_shader(wgsl.as_ptr(), wgsl.len() as u32) })
546    }
547    pub fn create_buffer(size: u32, usage: u32) -> Buffer {
548        Buffer(unsafe { sys::gfx_create_buffer(size, usage) })
549    }
550    /// WebGPU `GPURenderPipelineDescriptor` as JSON (handles as numbers,
551    /// color format `"surface"`, depth format `"depth24plus"`).
552    pub fn create_pipeline(json: &str) -> Pipeline {
553        Pipeline(unsafe { sys::gfx_create_pipeline(json.as_ptr(), json.len() as u32) })
554    }
555    /// `{"layout":L,"entries":[...]}` or `{"pipeline":P,"group":0,"entries":[...]}`; entries
556    /// `{"binding":0,"buffer":B,"offset":0,"size":N}`, `{"binding":1,"texture":T}`, `{"binding":2,"sampler":S}`.
557    pub fn create_bind_group(json: &str) -> BindGroup {
558        BindGroup(unsafe { sys::gfx_create_bind_group(json.as_ptr(), json.len() as u32) })
559    }
560    /// `{"entries":[{"binding":0,"visibility":STAGE_VERTEX,"buffer":{"type":"uniform","hasDynamicOffset":true}}, ...]}`.
561    /// Bind groups made from it work with every pipeline whose `"layout":[...]` lists it.
562    pub fn create_bind_group_layout(json: &str) -> BindGroupLayout {
563        BindGroupLayout(unsafe { sys::gfx_create_bind_group_layout(json.as_ptr(), json.len() as u32) })
564    }
565    /// `{"size":[w,h],"format":"rgba8unorm","mipLevelCount":n}`
566    pub fn create_texture(json: &str) -> Texture {
567        Texture(unsafe { sys::gfx_create_texture(json.as_ptr(), json.len() as u32) })
568    }
569    /// Upload a tightly packed RGBA8 region (`rgba.len() == w * h * 4`) of mip level `mip`.
570    pub fn write_texture(t: Texture, mip: u32, x: u32, y: u32, w: u32, h: u32, rgba: &[u8]) {
571        unsafe { sys::gfx_write_texture(t.0, mip, x, y, w, h, rgba.as_ptr(), rgba.len() as u32) }
572    }
573    /// `{"addressModeU":"repeat","magFilter":"linear","minFilter":"linear","mipmapFilter":"linear"}`
574    pub fn create_sampler(json: &str) -> Sampler {
575        Sampler(unsafe { sys::gfx_create_sampler(json.as_ptr(), json.len() as u32) })
576    }
577    /// Offset and byte length must be multiples of 4.
578    pub fn write_buffer<T: Pod>(buf: Buffer, offset: u32, data: &[T]) {
579        let b = as_bytes(data);
580        unsafe { sys::gfx_write_buffer(buf.0, offset, b.as_ptr(), b.len() as u32) }
581    }
582    /// Returns false if the runner will discard this frame (you may skip drawing).
583    pub fn begin_frame(clear: [f32; 4]) -> bool {
584        unsafe { sys::gfx_begin_frame(clear[0], clear[1], clear[2], clear[3]) != 0 }
585    }
586    pub fn set_pipeline(p: Pipeline) {
587        unsafe { sys::gfx_set_pipeline(p.0) }
588    }
589    pub fn set_bind_group(index: u32, bg: BindGroup) {
590        unsafe { sys::gfx_set_bind_group(index, bg.0) }
591    }
592    /// One offset (multiple of [`OFFSET_ALIGNMENT`]) per dynamic-offset entry, in binding order.
593    pub fn set_bind_group_offsets(index: u32, bg: BindGroup, offsets: &[u32]) {
594        unsafe { sys::gfx_set_bind_group_offsets(index, bg.0, offsets.as_ptr(), offsets.len() as u32) }
595    }
596    /// Viewport in drawable pixels (clamped to the drawable), depth range 0..=1.
597    pub fn set_viewport(x: f32, y: f32, w: f32, h: f32, min_depth: f32, max_depth: f32) {
598        unsafe { sys::gfx_set_viewport(x, y, w, h, min_depth, max_depth) }
599    }
600    /// Scissor rectangle in drawable pixels (clamped to the drawable).
601    pub fn set_scissor_rect(x: u32, y: u32, w: u32, h: u32) {
602        unsafe { sys::gfx_set_scissor_rect(x, y, w, h) }
603    }
604    pub fn set_vertex_buffer(slot: u32, buf: Buffer, offset: u32) {
605        unsafe { sys::gfx_set_vertex_buffer(slot, buf.0, offset) }
606    }
607    pub fn set_index_buffer(buf: Buffer, format: IndexFormat, offset: u32) {
608        unsafe { sys::gfx_set_index_buffer(buf.0, format as u32, offset) }
609    }
610    pub fn draw(vertices: u32, instances: u32, first_vertex: u32, first_instance: u32) {
611        unsafe { sys::gfx_draw(vertices, instances, first_vertex, first_instance) }
612    }
613    pub fn draw_indexed(indices: u32, instances: u32, first_index: u32, base_vertex: i32, first_instance: u32) {
614        unsafe { sys::gfx_draw_indexed(indices, instances, first_index, base_vertex, first_instance) }
615    }
616    pub fn end_frame() {
617        unsafe { sys::gfx_end_frame() }
618    }
619    /// Release an object (its handle traps from now on); objects made from it stay
620    /// valid. Check `gasm::has("gasm:gfx.destroy")` first on older runners.
621    pub fn destroy(h: impl Handle) {
622        unsafe { sys::gfx_destroy(h.raw()) }
623    }
624}
625
626// ---- net ---------------------------------------------------------------------------
627
628/// Message connections through `gasm:net` (WebSocket semantics, non-blocking).
629pub mod net {
630    use crate::sys;
631
632    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
633    pub enum State {
634        Connecting,
635        Open,
636        Closed,
637        Error,
638    }
639
640    pub enum Recv {
641        Message(Vec<u8>),
642        Empty,
643        Closed,
644    }
645
646    #[derive(Debug)]
647    pub struct Conn(i32);
648
649    impl Conn {
650        /// Open `ws://` / `wss://`. `None` if the runner denies it.
651        pub fn open(url: &str) -> Option<Conn> {
652            let h = unsafe { sys::net_open(url.as_ptr(), url.len() as u32) };
653            (h > 0).then_some(Conn(h))
654        }
655        pub fn state(&self) -> State {
656            match unsafe { sys::net_state(self.0) } {
657                0 => State::Connecting,
658                1 => State::Open,
659                2 => State::Closed,
660                _ => State::Error,
661            }
662        }
663        /// Send one message. False if the connection is not open.
664        pub fn send(&self, msg: &[u8]) -> bool {
665            !msg.is_empty() && unsafe { sys::net_send(self.0, msg.as_ptr(), msg.len() as u32) } == 0
666        }
667        /// Next queued message, if any.
668        pub fn recv(&self) -> Recv {
669            let mut buf = [0u8; 1024];
670            let n = unsafe { sys::net_recv(self.0, buf.as_mut_ptr(), buf.len() as u32) };
671            match n {
672                0 => Recv::Empty,
673                n if n < 0 => Recv::Closed,
674                n if n as usize <= buf.len() => Recv::Message(buf[..n as usize].to_vec()),
675                n => {
676                    let mut big = vec![0u8; n as usize];
677                    unsafe { sys::net_recv(self.0, big.as_mut_ptr(), n as u32) };
678                    Recv::Message(big)
679                }
680            }
681        }
682    }
683
684    impl Drop for Conn {
685        fn drop(&mut self) {
686            unsafe { sys::net_close(self.0) }
687        }
688    }
689}
690
691// ---- game entry points ----------------------------------------------------------------
692
693/// A gasm game. `init` runs once; `frame` runs at the configured frame rate.
694pub trait Game: Sized + 'static {
695    fn init() -> Result<Self, String>;
696    fn frame(&mut self);
697    /// Called (best effort) when the player closes the game: flush saves here.
698    /// Not called after `gasm::exit`, a trap, or a crash, so also save periodically.
699    fn exit(&mut self) {}
700}
701
702#[doc(hidden)]
703pub struct GameCell<T>(pub UnsafeCell<Option<T>>);
704// Wasm guests are single-threaded and the runner never calls exports concurrently;
705// natively (tests), the exports must not be called from several threads at once.
706unsafe impl<T: Send> Sync for GameCell<T> {}
707
708/// # Safety
709/// Not reentrant: call only from the exports [`game!`] defines, never concurrently.
710#[doc(hidden)]
711pub unsafe fn __init<G: Game>(cell: &GameCell<G>) -> i32 {
712    std::panic::set_hook(Box::new(|info| log(&format!("panic: {info}"))));
713    match G::init() {
714        Ok(g) => {
715            unsafe { *cell.0.get() = Some(g) };
716            0
717        }
718        Err(e) => {
719            log(&format!("init failed: {e}"));
720            1
721        }
722    }
723}
724
725/// # Safety
726/// As [`__init`].
727#[doc(hidden)]
728pub unsafe fn __frame<G: Game>(cell: &GameCell<G>) {
729    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
730        g.frame();
731    }
732}
733
734/// # Safety
735/// As [`__init`].
736#[doc(hidden)]
737pub unsafe fn __exit<G: Game>(cell: &GameCell<G>) {
738    if let Some(g) = unsafe { (*cell.0.get()).as_mut() } {
739        g.exit();
740    }
741}
742
743/// Export the gasm entry points for a [`Game`] type.
744#[macro_export]
745macro_rules! game {
746    ($t:ty) => {
747        static __GASM_GAME: $crate::GameCell<$t> = $crate::GameCell(::std::cell::UnsafeCell::new(None));
748
749        #[unsafe(no_mangle)]
750        pub extern "C" fn gasm_abi_version() -> i32 {
751            $crate::ABI_VERSION
752        }
753        #[unsafe(no_mangle)]
754        pub extern "C" fn gasm_init() -> i32 {
755            unsafe { $crate::__init::<$t>(&__GASM_GAME) }
756        }
757        #[unsafe(no_mangle)]
758        pub extern "C" fn gasm_frame() {
759            unsafe { $crate::__frame::<$t>(&__GASM_GAME) }
760        }
761        #[unsafe(no_mangle)]
762        pub extern "C" fn gasm_exit() {
763            unsafe { $crate::__exit::<$t>(&__GASM_GAME) }
764        }
765    };
766}
767
768// ---- own main loop -------------------------------------------------------------------
769
770/// Games that keep their own main loop (`loop { update(); draw(); wait_frame(); }`)
771/// instead of implementing [`Game`]. Same mechanism as the C SDK's `gasm_loop.h`:
772/// [`main_loop!`] exports the entry points and runs your function on the first frame;
773/// [`main_loop::wait_frame`] suspends it until the next frame with Binaryen's Asyncify,
774/// inside the module. Build, then post-process the `.wasm`:
775///
776/// ```text
777/// wasm-opt game.wasm --asyncify --pass-arg=asyncify-removelist@gasm_frame,gasm_loop_frame -O2 -o game.wasm
778/// ```
779///
780/// ```ignore
781/// fn run() -> i32 {
782///     loop {
783///         if gasm::pad(0).held(gasm::Buttons::START) { return 0; }
784///         draw();
785///         gasm::main_loop::wait_frame();
786///     }
787/// }
788/// gasm::main_loop!(run);
789/// ```
790///
791/// `main_loop!(run, on_exit)` also exports `gasm_exit` (the player is quitting: flush
792/// saves). wasm32 only: native builds of such a game run `run` straight through.
793pub mod main_loop {
794    // the suspend/resume state only exists on wasm32
795    #![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
796
797    #[cfg(target_arch = "wasm32")]
798    mod asyncify {
799        #[link(wasm_import_module = "asyncify")]
800        unsafe extern "C" {
801            pub fn start_unwind(data: *mut u8);
802            pub fn stop_unwind();
803            pub fn start_rewind(data: *mut u8);
804            pub fn stop_rewind();
805        }
806    }
807
808    const STACK: usize = 1024 * 1024;
809
810    #[repr(C)]
811    struct Data {
812        cur: *mut u8,
813        end: *mut u8,
814    }
815
816    // One thread; these are only touched from the frame loop below.
817    static mut DATA: Data = Data { cur: std::ptr::null_mut(), end: std::ptr::null_mut() };
818    static mut STACK_BUF: [u8; STACK] = [0; STACK];
819    static mut REWINDING: bool = false;
820    static mut UNWINDING: bool = false;
821    static mut STARTED: bool = false;
822    static mut FINISHED: bool = false;
823    static mut EXIT_CODE: i32 = 0;
824    static mut MAIN: Option<fn() -> i32> = None;
825    static mut FRAMES: u32 = 0;
826
827    fn wait_impl() {
828        #[cfg(target_arch = "wasm32")]
829        unsafe {
830            if REWINDING {
831                asyncify::stop_rewind();
832                REWINDING = false;
833                FRAMES += 1;
834                return;
835            }
836            DATA.cur = (&raw mut STACK_BUF).cast::<u8>();
837            DATA.end = DATA.cur.add(STACK);
838            UNWINDING = true;
839            asyncify::start_unwind((&raw mut DATA).cast::<u8>());
840        }
841    }
842
843    // Reached through a pointer: Asyncify instruments the callers of indirect calls,
844    // while wait_impl itself stays plain (like an import).
845    static mut WAIT: fn() = wait_impl;
846
847    /// Return to the runner; continues in the next frame (input and time are per frame).
848    pub fn wait_frame() {
849        let f = unsafe { std::ptr::read_volatile(&raw const WAIT) };
850        f()
851    }
852
853    /// Frames waited so far.
854    pub fn frames() -> u32 {
855        unsafe { FRAMES }
856    }
857
858    #[doc(hidden)]
859    pub fn set_main(f: fn() -> i32) {
860        unsafe { MAIN = Some(f) }
861    }
862
863    // Must be instrumented (and not inlined into the frame export): an unwind returns
864    // from here at once instead of looking like the game's function ended.
865    #[inline(never)]
866    fn run_main() {
867        unsafe {
868            let rc = MAIN.map_or(0, |f| f());
869            if !UNWINDING {
870                EXIT_CODE = rc;
871                FINISHED = true;
872            }
873        }
874    }
875
876    /// The frame driver, inlined into the `gasm_loop_frame` export that
877    /// [`main_loop!`](crate::main_loop!) defines: that export is on Asyncify's
878    /// remove list (it is the one frame that must not unwind). Games that don't
879    /// use the macro don't get it (nor the asyncify imports).
880    #[doc(hidden)]
881    #[inline(always)]
882    pub fn __loop_frame() {
883        unsafe {
884            if FINISHED {
885                return;
886            }
887            #[cfg(target_arch = "wasm32")]
888            {
889                if !STARTED {
890                    STARTED = true;
891                } else {
892                    REWINDING = true;
893                    asyncify::start_rewind((&raw mut DATA).cast::<u8>());
894                }
895                run_main();
896                if UNWINDING {
897                    asyncify::stop_unwind();
898                    UNWINDING = false;
899                    return;
900                }
901            }
902            #[cfg(not(target_arch = "wasm32"))]
903            {
904                STARTED = true;
905                run_main();
906            }
907            if FINISHED {
908                crate::exit(EXIT_CODE);
909            }
910        }
911    }
912}
913
914/// Export the gasm entry points for a game with its own main loop (see [`main_loop`]).
915#[macro_export]
916macro_rules! main_loop {
917    ($main:path) => {
918        #[unsafe(no_mangle)]
919        pub extern "C" fn gasm_abi_version() -> i32 {
920            $crate::ABI_VERSION
921        }
922        #[unsafe(no_mangle)]
923        pub extern "C" fn gasm_init() -> i32 {
924            $crate::main_loop::set_main($main);
925            0
926        }
927        #[unsafe(no_mangle)]
928        pub extern "C" fn gasm_frame() {
929            gasm_loop_frame()
930        }
931        #[unsafe(no_mangle)]
932        #[inline(never)]
933        pub extern "C" fn gasm_loop_frame() {
934            $crate::main_loop::__loop_frame()
935        }
936    };
937    ($main:path, $exit:path) => {
938        $crate::main_loop!($main);
939        #[unsafe(no_mangle)]
940        pub extern "C" fn gasm_exit() {
941            $exit()
942        }
943    };
944}