1use std::collections::BTreeMap;
15use std::sync::Mutex;
16
17struct State {
18 frame_rate: f64,
19 frames: u64,
20 clock: (f64, u64, f64),
21 now_ms: f64,
22 assets: BTreeMap<String, Vec<u8>>,
24 params: BTreeMap<String, String>,
25 storage: BTreeMap<String, Vec<u8>>,
26 storage_used: usize,
27 saves: u32,
29 pads: [u32; 4],
30 text: String,
31 input_mode: u32,
32 channels: u32,
33 hashing: bool,
34 video_hash: u32,
35 audio_hash: u32,
36 presented: u64,
37 size: (u32, u32),
38 audio_frames: u64,
39 gfx: Vec<GfxObj>,
41}
42
43#[derive(Clone, Copy, PartialEq)]
44enum GfxObj {
45 Buffer(u64),
46 Other,
47 Destroyed,
48}
49
50static STATE: Mutex<State> = Mutex::new(State {
51 frame_rate: 60.0,
52 frames: 0,
53 clock: (0.0, 0, 60.0),
54 now_ms: 0.0,
55 assets: BTreeMap::new(),
56 params: BTreeMap::new(),
57 storage: BTreeMap::new(),
58 storage_used: 0,
59 saves: 0,
60 pads: [0; 4],
61 text: String::new(),
62 input_mode: 0,
63 channels: 2,
64 hashing: true,
65 video_hash: FNV_INIT,
66 audio_hash: FNV_INIT,
67 presented: 0,
68 size: (0, 0),
69 audio_frames: 0,
70 gfx: Vec::new(),
71});
72
73const FNV_INIT: u32 = 0x811c_9dc5;
74const STORAGE_MAX_VALUE: usize = 1 << 20;
75const STORAGE_QUOTA: usize = 16 << 20;
76
77fn fnv(mut h: u32, bytes: &[u8]) -> u32 {
78 for &b in bytes {
79 h = (h ^ b as u32).wrapping_mul(0x0100_0193);
80 }
81 h
82}
83
84fn with<R>(f: impl FnOnce(&mut State) -> R) -> R {
85 f(&mut STATE.lock().unwrap_or_else(|e| e.into_inner()))
86}
87
88fn valid_key(k: &str) -> bool {
89 !k.is_empty()
90 && k.len() <= 128
91 && k.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
92 && k != "."
93 && k != ".."
94}
95
96pub fn set_asset(name: &str, bytes: Vec<u8>) {
98 with(|s| s.assets.insert(name.into(), bytes));
99}
100pub fn set_param(name: &str, value: &str) {
101 with(|s| s.params.insert(name.into(), value.into()));
102}
103pub fn set_pad(player: usize, buttons: u32) {
104 with(|s| s.pads[player] = buttons);
105}
106pub fn set_text(text: &str) {
108 with(|s| s.text = text.into());
109}
110pub fn set_hashing(on: bool) {
111 with(|s| s.hashing = on);
112}
113
114fn tick(s: &mut State) {
117 let (base_ms, base_frame, rate) = &mut s.clock;
118 if s.frame_rate != *rate {
119 *base_ms += (s.frames - *base_frame) as f64 * 1000.0 / *rate;
120 *base_frame = s.frames;
121 *rate = s.frame_rate;
122 }
123 s.now_ms = *base_ms + (s.frames - *base_frame) as f64 * 1000.0 / *rate;
124}
125
126pub fn begin_frame() {
128 with(tick);
129}
130
131pub fn end_frame() {
133 with(|s| {
134 s.frames += 1;
135 s.text.clear();
136 tick(s);
137 });
138}
139
140pub struct Stats {
141 pub frame_rate: f64,
142 pub presented: u64,
143 pub width: u32,
144 pub height: u32,
145 pub video_hash: u32,
146 pub audio_hash: u32,
147 pub audio_frames: u64,
148}
149
150pub fn stats() -> Stats {
151 with(|s| Stats {
152 frame_rate: s.frame_rate,
153 presented: s.presented,
154 width: s.size.0,
155 height: s.size.1,
156 video_hash: s.video_hash,
157 audio_hash: s.audio_hash,
158 audio_frames: s.audio_frames,
159 })
160}
161
162#[allow(clippy::missing_safety_doc)]
164pub mod abi {
165 use super::*;
166
167 unsafe fn bytes<'a>(ptr: *const u8, len: u32) -> &'a [u8] {
168 if len == 0 { &[] } else { unsafe { std::slice::from_raw_parts(ptr, len as usize) } }
169 }
170 unsafe fn text(ptr: *const u8, len: u32) -> String {
172 String::from_utf8(unsafe { bytes(ptr, len) }.to_vec()).expect("string argument is not UTF-8")
173 }
174 unsafe fn copy_out(src: &[u8], dst: *mut u8, cap: u32) -> i32 {
175 if src.len() <= cap as usize && !src.is_empty() {
176 unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len()) };
177 }
178 src.len() as i32
179 }
180
181 pub unsafe fn log(ptr: *const u8, len: u32) {
182 eprintln!("[guest] {}", String::from_utf8_lossy(unsafe { bytes(ptr, len) }));
183 }
184 pub unsafe fn has(ptr: *const u8, len: u32) -> i32 {
185 let name = unsafe { text(ptr, len) };
186 crate::sys::IMPORTS.contains(&name.as_str()) as i32 }
188 pub unsafe fn time_ms() -> f64 {
189 with(|s| s.now_ms)
190 }
191 pub unsafe fn max_threads() -> u32 {
193 0
194 }
195 pub unsafe fn utc_offset_minutes() -> i32 {
196 0
197 }
198 pub unsafe fn video_set_aspect(num: u32, den: u32) {
199 let ok = (num, den) == (0, 0) || ((1..=65535).contains(&num) && (1..=65535).contains(&den) && num as u64 * 8 >= den as u64 && den as u64 * 8 >= num as u64);
200 assert!(ok, "video_set_aspect: invalid ratio {num}:{den}");
201 }
202 pub unsafe fn yield_frame() {
205 super::end_frame();
206 }
207 pub unsafe fn set_fullscreen(_on: u32) -> i32 {
209 0
210 }
211 pub unsafe fn is_fullscreen() -> u32 {
212 0
213 }
214 pub unsafe fn set_title(ptr: *const u8, len: u32) {
215 eprintln!("[gasm] title: {}", unsafe { text(ptr, len) });
216 }
217 pub unsafe fn set_frame_rate(hz: f64) {
218 if hz.is_finite() && (1.0..=1000.0).contains(&hz) {
219 with(|s| s.frame_rate = hz);
220 }
221 }
222 pub unsafe fn video_present(ptr: *const u8, w: u32, h: u32, stride: u32) {
223 assert!(w > 0 && h > 0 && w <= 4096 && h <= 4096 && stride >= w * 4, "video_present: bad geometry {w}x{h} stride {stride}");
224 with(|s| {
225 if s.hashing {
226 for y in 0..h {
227 let row = unsafe { bytes(ptr.add((y * stride) as usize), w * 4) };
228 s.video_hash = fnv(s.video_hash, row);
229 }
230 }
231 s.size = (w, h);
232 s.presented += 1;
233 })
234 }
235 pub unsafe fn audio_config(rate: u32, channels: u32) {
236 if (8000..=192_000).contains(&rate) && (channels == 1 || channels == 2) {
237 with(|s| s.channels = channels);
238 }
239 }
240 pub unsafe fn audio_push(ptr: *const f32, frames: u32) {
241 with(|s| {
242 if s.hashing {
243 let b = unsafe { bytes(ptr as *const u8, frames * s.channels * 4) };
244 s.audio_hash = fnv(s.audio_hash, b);
245 }
246 s.audio_frames += frames as u64;
247 })
248 }
249 pub unsafe fn input_pad(player: u32) -> u32 {
250 with(|s| s.pads.get(player as usize).copied().unwrap_or(0))
251 }
252 pub unsafe fn input_mode(flags: u32) {
255 with(|s| s.input_mode = flags & 7);
256 }
257 pub unsafe fn key_state(dst: *mut u8, len: u32) -> i32 {
258 let n = (len as usize).min(32);
259 unsafe { std::ptr::write_bytes(dst, 0, n) };
260 32
261 }
262 pub unsafe fn key_events(_: *mut u8, _: u32) -> i32 {
263 0
264 }
265 pub unsafe fn pointer(dst: *mut u8, cap: u32) -> i32 {
266 if cap >= 48 {
267 let (fw, fh, mode) = with(|s| (s.size.0 as f64, s.size.1 as f64, s.input_mode));
268 let (fx, fy) = if fw > 0.0 && fh > 0.0 {
269 let scale = (1280.0 / fw).min(720.0 / fh);
270 ((-(1280.0 - fw * scale) / 2.0 / scale) as f32, (-(720.0 - fh * scale) / 2.0 / scale) as f32)
271 } else {
272 (0.0, 0.0)
273 };
274 let mut b = [0u8; 48];
275 b[8..12].copy_from_slice(&fx.to_le_bytes());
276 b[12..16].copy_from_slice(&fy.to_le_bytes());
277 b[44..48].copy_from_slice(&(1 | (mode & 6)).to_le_bytes());
278 unsafe { std::ptr::copy_nonoverlapping(b.as_ptr(), dst, 48) };
279 }
280 48
281 }
282 pub unsafe fn gamepad(slot: u32, dst: *mut u8, cap: u32) -> i32 {
283 if slot > 3 {
284 return -1;
285 }
286 if cap >= 204 {
287 unsafe { std::ptr::write_bytes(dst, 0, 204) };
288 }
289 204
290 }
291 pub unsafe fn gamepad_name(_: u32, _: *mut u8, _: u32) -> i32 {
292 -1
293 }
294 pub unsafe fn text_input(dst: *mut u8, cap: u32) -> i32 {
295 with(|s| unsafe { copy_out(s.text.as_bytes(), dst, cap) })
296 }
297 pub unsafe fn asset_size(name: *const u8, len: u32) -> i32 {
298 let n = unsafe { text(name, len) };
299 with(|s| match s.assets.get(&n) {
300 None => -1,
301 Some(a) if a.len() > i32::MAX as usize => -2,
302 Some(a) => a.len() as i32,
303 })
304 }
305 pub unsafe fn asset_size64(name: *const u8, len: u32) -> i64 {
306 let n = unsafe { text(name, len) };
307 with(|s| s.assets.get(&n).map_or(-1, |a| a.len() as i64))
308 }
309 pub unsafe fn asset_version(name: *const u8, len: u32) -> i32 {
311 let n = unsafe { text(name, len) };
312 with(|s| if s.assets.contains_key(&n) { 0 } else { -1 })
313 }
314 pub unsafe fn asset_count() -> u32 {
315 with(|s| s.assets.len() as u32)
316 }
317 pub unsafe fn asset_name(index: u32, dst: *mut u8, cap: u32) -> i32 {
318 with(|s| match s.assets.keys().nth(index as usize) {
319 Some(n) => unsafe { copy_out(n.as_bytes(), dst, cap) },
320 None => -1,
321 })
322 }
323 pub unsafe fn asset_read(name: *const u8, len: u32, dst: *mut u8, cap: u32) -> i32 {
324 unsafe { asset_read_at64(name, len, 0, dst, cap) }
325 }
326 pub unsafe fn asset_read_at(name: *const u8, len: u32, offset: u32, dst: *mut u8, cap: u32) -> i32 {
327 unsafe { asset_read_at64(name, len, offset as u64, dst, cap) }
328 }
329 pub unsafe fn asset_read_at64(name: *const u8, len: u32, offset: u64, dst: *mut u8, cap: u32) -> i32 {
330 let n = unsafe { text(name, len) };
331 with(|s| match s.assets.get(&n) {
332 Some(a) => {
333 let start = offset.min(a.len() as u64) as usize;
334 let k = (a.len() - start).min(cap as usize);
335 unsafe { std::ptr::copy_nonoverlapping(a[start..].as_ptr(), dst, k) };
336 k as i32
337 }
338 None => -1,
339 })
340 }
341
342 pub unsafe fn storage_get(key: *const u8, len: u32, dst: *mut u8, cap: u32) -> i32 {
344 let k = unsafe { text(key, len) };
345 with(|s| match s.storage.get(&k) {
346 Some(v) => unsafe { copy_out(v, dst, cap) },
347 None => -1,
348 })
349 }
350 pub unsafe fn storage_set(key: *const u8, len: u32, data: *const u8, data_len: u32) -> i32 {
351 let k = unsafe { text(key, len) };
352 let v = unsafe { bytes(data, data_len) }.to_vec();
353 if !valid_key(&k) {
354 return crate::sys::GASM_STORAGE_ERR_KEY;
355 }
356 if v.len() > STORAGE_MAX_VALUE {
357 return crate::sys::GASM_STORAGE_ERR_SIZE;
358 }
359 with(|s| {
360 let old = s.storage.get(&k).map_or(0, |o| k.len() + o.len());
361 let used = s.storage_used - old + k.len() + v.len();
362 if used > STORAGE_QUOTA {
363 return crate::sys::GASM_STORAGE_ERR_QUOTA;
364 }
365 s.storage_used = used;
366 s.storage.insert(k, v);
367 0
368 })
369 }
370 pub unsafe fn clipboard_set_text(_text: *const u8, len: u32) -> i32 {
372 if len > 1 << 20 { -1 } else { 0 }
373 }
374 pub unsafe fn clipboard_get_text(_dst: *mut u8, _cap: u32) -> i32 {
375 -1
376 }
377 pub unsafe fn files_save(_name: *const u8, name_len: u32, _mime: *const u8, _mime_len: u32, _data: *const u8, len: u32) -> i32 {
379 if name_len == 0 || name_len > 255 || len as usize > 256 << 20 {
380 return -1;
381 }
382 with(|s| {
383 s.saves += 1;
384 s.saves as i32
385 })
386 }
387 pub unsafe fn files_state(handle: i32) -> i32 {
388 with(|s| {
389 assert!(handle > 0 && handle as u32 <= s.saves, "gasm:files: invalid handle {handle}");
390 crate::sys::GASM_FILES_SAVED
391 })
392 }
393 pub unsafe fn storage_count() -> u32 {
394 with(|s| s.storage.len() as u32)
395 }
396 pub unsafe fn storage_key(index: u32, dst: *mut u8, cap: u32) -> i32 {
397 with(|s| match s.storage.keys().nth(index as usize) {
398 Some(k) => unsafe { copy_out(k.as_bytes(), dst, cap) },
399 None => -1,
400 })
401 }
402 pub unsafe fn storage_delete(key: *const u8, len: u32) -> i32 {
403 let k = unsafe { text(key, len) };
404 with(|s| match s.storage.remove(&k) {
405 Some(v) => {
406 s.storage_used -= k.len() + v.len();
407 0
408 }
409 None => -1,
410 })
411 }
412
413 pub unsafe fn param(name: *const u8, len: u32, dst: *mut u8, cap: u32) -> i32 {
414 let n = unsafe { text(name, len) };
415 with(|s| match s.params.get(&n) {
416 Some(v) => unsafe { copy_out(v.as_bytes(), dst, cap) },
417 None => -1,
418 })
419 }
420
421 fn add(obj: GfxObj) -> u32 {
424 with(|s| {
425 s.gfx.push(obj);
426 s.gfx.len() as u32
427 })
428 }
429 fn live(s: &State, h: u32) -> GfxObj {
430 match s.gfx.get((h as usize).wrapping_sub(1)) {
431 Some(GfxObj::Destroyed) => panic!("gfx: handle {h} was destroyed"),
432 Some(o) => *o,
433 None => panic!("gfx: invalid handle {h}"),
434 }
435 }
436 fn buffer_size(s: &State, h: u32) -> u64 {
437 match live(s, h) {
438 GfxObj::Buffer(n) => n,
439 _ => panic!("gfx: handle {h} is not a buffer"),
440 }
441 }
442 pub unsafe fn gfx_width() -> u32 { 1280 }
443 pub unsafe fn gfx_height() -> u32 { 720 }
444 pub unsafe fn gfx_create_shader(_: *const u8, _: u32) -> u32 { add(GfxObj::Other) }
445 pub unsafe fn gfx_create_buffer(size: u32, _: u32) -> u32 {
446 assert!(size > 0 && size % 4 == 0, "gfx.create_buffer: size {size} must be a non-zero multiple of 4");
447 add(GfxObj::Buffer(size as u64))
448 }
449 pub unsafe fn gfx_create_pipeline(_: *const u8, _: u32) -> u32 { add(GfxObj::Other) }
450 pub unsafe fn gfx_create_bind_group(_: *const u8, _: u32) -> u32 { add(GfxObj::Other) }
451 pub unsafe fn gfx_create_bind_group_layout(_: *const u8, _: u32) -> u32 { add(GfxObj::Other) }
452 pub unsafe fn gfx_create_texture(_: *const u8, _: u32) -> u32 { add(GfxObj::Other) }
453 pub unsafe fn gfx_create_sampler(_: *const u8, _: u32) -> u32 { add(GfxObj::Other) }
454 #[allow(clippy::too_many_arguments)]
455 pub unsafe fn gfx_write_texture(t: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: *const u8, len: u32) {
456 with(|s| {
457 live(s, t);
458 if s.hashing {
459 for v in [t, mip, x, y, w, h] {
460 s.video_hash = fnv(s.video_hash, &v.to_le_bytes());
461 }
462 s.video_hash = fnv(s.video_hash, unsafe { bytes(ptr, len) });
463 }
464 })
465 }
466 pub unsafe fn gfx_set_bind_group_offsets(_: u32, bg: u32, _: *const u32, _: u32) {
467 with(|s| live(s, bg));
468 }
469 pub unsafe fn gfx_set_viewport(_: f32, _: f32, _: f32, _: f32, _: f32, _: f32) {}
470 pub unsafe fn gfx_set_scissor_rect(_: u32, _: u32, _: u32, _: u32) {}
471 pub unsafe fn gfx_write_buffer(buf: u32, offset: u32, ptr: *const u8, len: u32) {
472 assert!(offset % 4 == 0 && len % 4 == 0, "gfx.write_buffer: offset {offset} and length {len} must be multiples of 4");
473 with(|s| {
474 let size = buffer_size(s, buf);
475 assert!(offset as u64 + len as u64 <= size, "gfx.write_buffer: {offset}+{len} exceeds buffer size {size}");
476 if s.hashing {
477 s.video_hash = fnv(s.video_hash, unsafe { bytes(ptr, len) });
478 }
479 })
480 }
481 pub unsafe fn gfx_begin_frame(_: f32, _: f32, _: f32, _: f32) -> u32 { 0 }
482 pub unsafe fn gfx_set_pipeline(p: u32) {
483 with(|s| live(s, p));
484 }
485 pub unsafe fn gfx_set_bind_group(_: u32, bg: u32) {
486 with(|s| live(s, bg));
487 }
488 pub unsafe fn gfx_set_vertex_buffer(_: u32, buf: u32, offset: u32) {
489 with(|s| assert!(offset as u64 <= buffer_size(s, buf), "gfx.set_vertex_buffer: offset {offset} outside the buffer"));
490 }
491 pub unsafe fn gfx_set_index_buffer(buf: u32, format: u32, offset: u32) {
492 assert!(format <= 1, "gfx.set_index_buffer: format {format}");
493 with(|s| assert!(offset as u64 <= buffer_size(s, buf), "gfx.set_index_buffer: offset {offset} outside the buffer"));
494 }
495 pub unsafe fn gfx_draw(_: u32, _: u32, _: u32, _: u32) {}
496 pub unsafe fn gfx_draw_indexed(_: u32, _: u32, _: u32, _: i32, _: u32) {}
497 pub unsafe fn gfx_end_frame() {
498 with(|s| s.presented += 1);
499 }
500 pub unsafe fn gfx_destroy(h: u32) {
501 with(|s| {
502 live(s, h);
503 s.gfx[h as usize - 1] = GfxObj::Destroyed;
504 })
505 }
506
507 pub unsafe fn net_open(_: *const u8, _: u32) -> i32 { -1 }
509 pub unsafe fn net_state(h: i32) -> u32 { panic!("gasm:net: invalid connection handle {h}") }
510 pub unsafe fn net_send(h: i32, _: *const u8, _: u32) -> i32 { panic!("gasm:net: invalid connection handle {h}") }
511 pub unsafe fn net_recv(h: i32, _: *mut u8, _: u32) -> i32 { panic!("gasm:net: invalid connection handle {h}") }
512 pub unsafe fn net_close(h: i32) { panic!("gasm:net: invalid connection handle {h}") }
513 pub unsafe fn fetch_request(_: *const u8, _: u32, _: *const u8, _: u32) -> i32 { -1 }
515 pub unsafe fn fetch_state(h: i32) -> u32 { panic!("gasm:fetch: invalid handle {h}") }
516 pub unsafe fn fetch_status(h: i32) -> i32 { panic!("gasm:fetch: invalid handle {h}") }
517 pub unsafe fn fetch_headers(h: i32, _: *mut u8, _: u32) -> i32 { panic!("gasm:fetch: invalid handle {h}") }
518 pub unsafe fn fetch_read(h: i32, _: *mut u8, _: u32) -> i32 { panic!("gasm:fetch: invalid handle {h}") }
519 pub unsafe fn fetch_close(h: i32) { panic!("gasm:fetch: invalid handle {h}") }
520
521 pub unsafe fn media_open(_: *const u8, _: u32) -> i32 { -1 }
523 pub unsafe fn media_play(h: i32) { panic!("gasm:media: invalid handle {h}") }
524 pub unsafe fn media_pause(h: i32) { panic!("gasm:media: invalid handle {h}") }
525 pub unsafe fn media_close(h: i32) { panic!("gasm:media: invalid handle {h}") }
526 pub unsafe fn media_set_volume(h: i32, _: f32) { panic!("gasm:media: invalid handle {h}") }
527 pub unsafe fn media_set_lowpass(h: i32, _: f32) { panic!("gasm:media: invalid handle {h}") }
528 pub unsafe fn media_caps(h: i32) -> u32 { panic!("gasm:media: invalid handle {h}") }
529 pub unsafe fn media_state(h: i32) -> u32 { panic!("gasm:media: invalid handle {h}") }
530 pub unsafe fn media_error(h: i32, _: *mut u8, _: u32) -> i32 {
531 if h == -1 { 0 } else { panic!("gasm:media: invalid handle {h}") }
532 }
533 pub unsafe fn media_position(h: i32) -> f64 { panic!("gasm:media: invalid handle {h}") }
534 pub unsafe fn media_duration(h: i32) -> f64 { panic!("gasm:media: invalid handle {h}") }
535 pub unsafe fn media_metadata(h: i32, _: *mut u8, _: u32) -> i32 { panic!("gasm:media: invalid handle {h}") }
536 pub unsafe fn media_metadata_version(h: i32) -> u32 { panic!("gasm:media: invalid handle {h}") }
537
538 #[allow(clippy::too_many_arguments)]
539 mod gl {
540 include!("native_gl.rs");
541 }
542 pub use gl::*;
543
544 pub unsafe fn proc_exit(code: i32) -> ! {
545 std::process::exit(code)
546 }
547}