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