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