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