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