1pub 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
29pub fn log(msg: &str) {
32 unsafe { sys::log(msg.as_ptr(), msg.len() as u32) }
33}
34
35#[macro_export]
37macro_rules! log {
38 ($($t:tt)*) => { $crate::log(&::std::format!($($t)*)) };
39}
40
41pub fn time_ms() -> f64 {
43 unsafe { sys::time_ms() }
44}
45
46pub 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
58pub fn has(name: &str) -> bool {
62 unsafe { sys::has(name.as_ptr(), name.len() as u32) == 1 }
63}
64
65pub fn set_frame_rate(hz: f64) {
67 unsafe { sys::set_frame_rate(hz) }
68}
69
70pub 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#[macro_export]
87macro_rules! title {
88 ($t:literal) => {
89 #[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
108pub 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
119pub fn set_fullscreen(on: bool) -> bool {
126 has("gasm.set_fullscreen") && unsafe { sys::set_fullscreen(on as u32) } == 1
127}
128
129pub fn is_fullscreen() -> bool {
132 has("gasm.is_fullscreen") && unsafe { sys::is_fullscreen() } == 1
133}
134
135pub 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
146pub fn exit(code: i32) -> ! {
148 unsafe { sys::proc_exit(code) }
149}
150
151pub 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 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 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#[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
208pub fn pad(player: u32) -> Buttons {
210 Buttons(unsafe { sys::input_pad(player) })
211}
212
213pub 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
227pub 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
245pub 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
251pub 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
262pub 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
274pub 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
282pub 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 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
303 pub enum Error {
304 Key,
306 Size,
308 Quota,
310 Io,
312 }
313
314 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 pub fn set(key: &str, value: &[u8]) -> bool {
327 try_set(key, value).is_ok()
328 }
329
330 pub fn delete(key: &str) -> bool {
332 unsafe { sys::storage_delete(key.as_ptr(), key.len() as u32) == 0 }
333 }
334
335 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
352pub mod clipboard {
356 use crate::sys;
357
358 pub fn available() -> bool {
360 crate::has("gasm:clipboard")
361 }
362
363 pub fn set_text(text: &str) -> bool {
365 unsafe { sys::clipboard_set_text(text.as_ptr(), text.len() as u32) == 0 }
366 }
367
368 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
380pub mod files {
384 use crate::sys;
385
386 pub fn available() -> bool {
388 crate::has("gasm:files")
389 }
390
391 #[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 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 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
421pub mod input {
433 use crate::sys;
434
435 pub const KEYS_RAW: u32 = sys::GASM_INPUT_KEYS_RAW;
437 pub const POINTER_HIDDEN: u32 = sys::GASM_INPUT_POINTER_HIDDEN;
439 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 pub fn set_mode(flags: u32) {
454 unsafe { sys::input_mode(flags) }
455 }
456
457 #[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 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 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 pub x: f32,
496 pub y: f32,
497 pub frame_x: f32,
499 pub frame_y: f32,
500 pub dx: f32,
502 pub dy: f32,
503 pub wheel_x: f32,
505 pub wheel_y: f32,
506 pub buttons: u32,
507 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 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 #[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 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 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
586pub use sys::keys;
588
589pub 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 pub const STAGE_VERTEX: u32 = sys::GASM_STAGE_VERTEX;
618 pub const STAGE_FRAGMENT: u32 = sys::GASM_STAGE_FRAGMENT;
619 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 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 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 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 pub fn create_pipeline(json: &str) -> Pipeline {
665 Pipeline(unsafe { sys::gfx_create_pipeline(json.as_ptr(), json.len() as u32) })
666 }
667 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 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 pub fn create_texture(json: &str) -> Texture {
679 Texture(unsafe { sys::gfx_create_texture(json.as_ptr(), json.len() as u32) })
680 }
681 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 pub fn create_sampler(json: &str) -> Sampler {
687 Sampler(unsafe { sys::gfx_create_sampler(json.as_ptr(), json.len() as u32) })
688 }
689 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 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 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 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 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 pub fn destroy(h: impl Handle) {
734 unsafe { sys::gfx_destroy(h.raw()) }
735 }
736}
737
738pub 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 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)) }
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 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 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
803pub mod fetch {
818 use crate::sys;
819
820 pub fn available() -> bool {
822 crate::has("gasm:fetch")
823 }
824
825 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
826 pub enum State {
827 Pending,
829 Headers,
831 Done,
833 Failed,
835 }
836
837 #[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 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 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 #[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 pub fn status(&self) -> i32 {
921 unsafe { sys::fetch_status(self.0) }
922 }
923 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 pub fn header(&self, name: &str) -> Option<String> {
936 self.headers()?.into_iter().find(|(k, _)| k == name).map(|(_, v)| v)
937 }
938 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 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
962pub mod media {
975 use crate::sys;
976
977 pub fn available() -> bool {
979 crate::has("gasm:media")
980 }
981
982 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
983 pub enum State {
984 Connecting,
986 Playing,
987 Paused,
988 Failed,
990 Ended,
992 }
993
994 #[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 #[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 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 #[derive(Debug)]
1028 pub struct Stream(i32);
1029
1030 impl Stream {
1031 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 pub fn set_volume(&self, gain: f32) {
1057 unsafe { sys::media_set_volume(self.0, gain) }
1058 }
1059 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 pub fn error(&self) -> String {
1078 text(|p, c| unsafe { sys::media_error(self.0, p, c) })
1079 }
1080 pub fn position(&self) -> f64 {
1082 unsafe { sys::media_position(self.0) }
1083 }
1084 pub fn duration(&self) -> Option<f64> {
1086 let d = unsafe { sys::media_duration(self.0) };
1087 (d >= 0.0).then_some(d)
1088 }
1089 pub fn metadata_version(&self) -> u32 {
1091 unsafe { sys::media_metadata_version(self.0) }
1092 }
1093 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 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 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
1165pub trait Game: Sized + 'static {
1169 fn init() -> Result<Self, String>;
1170 fn frame(&mut self);
1171 fn exit(&mut self) {}
1174}
1175
1176#[doc(hidden)]
1177pub struct GameCell<T>(pub UnsafeCell<Option<T>>);
1178#[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#[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#[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#[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#[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
1246pub mod main_loop {
1274 #![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 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 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 static mut WAIT: fn() = wait_impl;
1333
1334 pub fn wait_frame() {
1336 if crate::thread::available() {
1337 return crate::thread::wait_frame(); }
1339 let f = unsafe { std::ptr::read_volatile(&raw const WAIT) };
1340 f()
1341 }
1342
1343 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 #[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 #[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 #[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#[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#[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#[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}