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