Skip to main content

gasm/
native.rs

1//! In-process stub host for running a guest natively (non-wasm targets).
2//!
3//! It implements the same ABI as the runners' headless mode: no display or
4//! audio, virtual time, and FNV-1a hashes over presented frames, audio and gfx
5//! buffer writes. A harness sets assets/params/input, calls the guest's
6//! `gasm_init`/`gasm_frame`, then reads [`stats`]. Hashes match
7//! `gasm-run --headless` for deterministic guests.
8
9use std::sync::Mutex;
10
11struct State {
12    frame_rate: f64,
13    frames: u64,
14    assets: Vec<(String, Vec<u8>)>,
15    params: Vec<(String, String)>,
16    storage: Vec<(String, Vec<u8>)>,
17    pads: [u32; 4],
18    text: String,
19    input_mode: u32,
20    channels: u32,
21    hashing: bool,
22    video_hash: u32,
23    audio_hash: u32,
24    presented: u64,
25    size: (u32, u32),
26    audio_frames: u64,
27    next_handle: u32,
28}
29
30static STATE: Mutex<State> = Mutex::new(State {
31    frame_rate: 60.0,
32    frames: 0,
33    assets: Vec::new(),
34    params: Vec::new(),
35    storage: Vec::new(),
36    pads: [0; 4],
37    text: String::new(),
38    input_mode: 0,
39    channels: 2,
40    hashing: true,
41    video_hash: FNV_INIT,
42    audio_hash: FNV_INIT,
43    presented: 0,
44    size: (0, 0),
45    audio_frames: 0,
46    next_handle: 1,
47});
48
49const FNV_INIT: u32 = 0x811c_9dc5;
50fn fnv(mut h: u32, bytes: &[u8]) -> u32 {
51    for &b in bytes {
52        h = (h ^ b as u32).wrapping_mul(0x0100_0193);
53    }
54    h
55}
56
57fn with<R>(f: impl FnOnce(&mut State) -> R) -> R {
58    f(&mut STATE.lock().unwrap())
59}
60
61/// Harness API.
62pub fn set_asset(name: &str, bytes: Vec<u8>) {
63    with(|s| s.assets.push((name.into(), bytes)));
64}
65pub fn set_param(name: &str, value: &str) {
66    with(|s| s.params.push((name.into(), value.into())));
67}
68pub fn set_pad(player: usize, buttons: u32) {
69    with(|s| s.pads[player] = buttons);
70}
71/// Text returned by `text_input` until the next [`end_frame`].
72pub fn set_text(text: &str) {
73    with(|s| s.text = text.into());
74}
75pub fn set_hashing(on: bool) {
76    with(|s| s.hashing = on);
77}
78/// Call after each gasm_frame so time_ms advances.
79pub fn end_frame() {
80    with(|s| {
81        s.frames += 1;
82        s.text.clear();
83    });
84}
85
86pub struct Stats {
87    pub frame_rate: f64,
88    pub presented: u64,
89    pub width: u32,
90    pub height: u32,
91    pub video_hash: u32,
92    pub audio_hash: u32,
93    pub audio_frames: u64,
94}
95
96pub fn stats() -> Stats {
97    with(|s| Stats {
98        frame_rate: s.frame_rate,
99        presented: s.presented,
100        width: s.size.0,
101        height: s.size.1,
102        video_hash: s.video_hash,
103        audio_hash: s.audio_hash,
104        audio_frames: s.audio_frames,
105    })
106}
107
108/// Same signatures as the wasm imports (see sys.rs).
109#[allow(clippy::missing_safety_doc)]
110pub mod abi {
111    use super::*;
112
113    unsafe fn bytes<'a>(ptr: *const u8, len: u32) -> &'a [u8] {
114        if len == 0 { &[] } else { unsafe { std::slice::from_raw_parts(ptr, len as usize) } }
115    }
116    unsafe fn text(ptr: *const u8, len: u32) -> String {
117        String::from_utf8_lossy(unsafe { bytes(ptr, len) }).into_owned()
118    }
119    unsafe fn copy_out(src: &[u8], dst: *mut u8, cap: u32) -> i32 {
120        if src.len() <= cap as usize && !src.is_empty() {
121            unsafe { std::ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len()) };
122        }
123        src.len() as i32
124    }
125
126    pub unsafe fn log(ptr: *const u8, len: u32) {
127        eprintln!("[guest] {}", unsafe { text(ptr, len) });
128    }
129    pub unsafe fn time_ms() -> f64 {
130        with(|s| s.frames as f64 * 1000.0 / s.frame_rate)
131    }
132    pub unsafe fn set_frame_rate(hz: f64) {
133        with(|s| s.frame_rate = hz);
134    }
135    pub unsafe fn video_present(ptr: *const u8, w: u32, h: u32, stride: u32) {
136        with(|s| {
137            if s.hashing {
138                for y in 0..h {
139                    let row = unsafe { bytes(ptr.add((y * stride) as usize), w * 4) };
140                    s.video_hash = fnv(s.video_hash, row);
141                }
142            }
143            s.size = (w, h);
144            s.presented += 1;
145        })
146    }
147    pub unsafe fn audio_config(_rate: u32, channels: u32) {
148        with(|s| s.channels = channels);
149    }
150    pub unsafe fn audio_push(ptr: *const f32, frames: u32) {
151        with(|s| {
152            if s.hashing {
153                let b = unsafe { bytes(ptr as *const u8, frames * s.channels * 4) };
154                s.audio_hash = fnv(s.audio_hash, b);
155            }
156            s.audio_frames += frames as u64;
157        })
158    }
159    pub unsafe fn input_pad(player: u32) -> u32 {
160        with(|s| s.pads.get(player as usize).copied().unwrap_or(0))
161    }
162    // raw input: like a headless run with an empty script (keyboard and pointer
163    // present, nothing pressed, pointer at 0,0 in a 1280x720 drawable, no gamepads)
164    pub unsafe fn input_mode(flags: u32) {
165        with(|s| s.input_mode = flags & 7);
166    }
167    pub unsafe fn key_state(dst: *mut u8, len: u32) -> i32 {
168        let n = (len as usize).min(32);
169        unsafe { std::ptr::write_bytes(dst, 0, n) };
170        32
171    }
172    pub unsafe fn key_events(_: *mut u8, _: u32) -> i32 {
173        0
174    }
175    pub unsafe fn pointer(dst: *mut u8, cap: u32) -> i32 {
176        if cap >= 48 {
177            let (fw, fh, mode) = with(|s| (s.size.0 as f64, s.size.1 as f64, s.input_mode));
178            let (fx, fy) = if fw > 0.0 && fh > 0.0 {
179                let scale = (1280.0 / fw).min(720.0 / fh);
180                ((-(1280.0 - fw * scale) / 2.0 / scale) as f32, (-(720.0 - fh * scale) / 2.0 / scale) as f32)
181            } else {
182                (0.0, 0.0)
183            };
184            let mut b = [0u8; 48];
185            b[8..12].copy_from_slice(&fx.to_le_bytes());
186            b[12..16].copy_from_slice(&fy.to_le_bytes());
187            b[44..48].copy_from_slice(&(1 | (mode & 6)).to_le_bytes());
188            unsafe { std::ptr::copy_nonoverlapping(b.as_ptr(), dst, 48) };
189        }
190        48
191    }
192    pub unsafe fn gamepad(slot: u32, dst: *mut u8, cap: u32) -> i32 {
193        if slot > 3 {
194            return -1;
195        }
196        if cap >= 204 {
197            unsafe { std::ptr::write_bytes(dst, 0, 204) };
198        }
199        204
200    }
201    pub unsafe fn gamepad_name(_: u32, _: *mut u8, _: u32) -> i32 {
202        -1
203    }
204    pub unsafe fn text_input(dst: *mut u8, cap: u32) -> i32 {
205        with(|s| unsafe { copy_out(s.text.as_bytes(), dst, cap) })
206    }
207    pub unsafe fn asset_size(name: *const u8, len: u32) -> i32 {
208        let n = unsafe { text(name, len) };
209        with(|s| s.assets.iter().find(|a| a.0 == n).map_or(-1, |a| a.1.len() as i32))
210    }
211    pub unsafe fn asset_count() -> u32 {
212        with(|s| s.assets.len() as u32)
213    }
214    pub unsafe fn asset_name(index: u32, dst: *mut u8, cap: u32) -> i32 {
215        with(|s| {
216            let mut names: Vec<&str> = s.assets.iter().map(|a| a.0.as_str()).collect();
217            names.sort();
218            match names.get(index as usize) {
219                Some(n) => unsafe { copy_out(n.as_bytes(), dst, cap) },
220                None => -1,
221            }
222        })
223    }
224    pub unsafe fn asset_read(name: *const u8, len: u32, dst: *mut u8, cap: u32) -> i32 {
225        let n = unsafe { text(name, len) };
226        with(|s| match s.assets.iter().find(|a| a.0 == n) {
227            Some(a) => {
228                let k = a.1.len().min(cap as usize);
229                unsafe { std::ptr::copy_nonoverlapping(a.1.as_ptr(), dst, k) };
230                k as i32
231            }
232            None => -1,
233        })
234    }
235    pub unsafe fn asset_read_at(name: *const u8, len: u32, offset: u32, dst: *mut u8, cap: u32) -> i32 {
236        let n = unsafe { text(name, len) };
237        with(|s| match s.assets.iter().find(|a| a.0 == n) {
238            Some(a) => {
239                let start = (offset as usize).min(a.1.len());
240                let k = (a.1.len() - start).min(cap as usize);
241                unsafe { std::ptr::copy_nonoverlapping(a.1[start..].as_ptr(), dst, k) };
242                k as i32
243            }
244            None => -1,
245        })
246    }
247
248    // storage: in memory (like headless runners)
249    pub unsafe fn storage_get(key: *const u8, len: u32, dst: *mut u8, cap: u32) -> i32 {
250        let k = unsafe { text(key, len) };
251        with(|s| match s.storage.iter().find(|e| e.0 == k) {
252            Some(e) => unsafe { copy_out(&e.1, dst, cap) },
253            None => -1,
254        })
255    }
256    pub unsafe fn storage_set(key: *const u8, len: u32, data: *const u8, data_len: u32) -> i32 {
257        let k = unsafe { text(key, len) };
258        let v = unsafe { bytes(data, data_len) }.to_vec();
259        with(|s| {
260            s.storage.retain(|e| e.0 != k);
261            s.storage.push((k, v));
262            0
263        })
264    }
265    pub unsafe fn storage_count() -> u32 {
266        with(|s| s.storage.len() as u32)
267    }
268    pub unsafe fn storage_key(index: u32, dst: *mut u8, cap: u32) -> i32 {
269        with(|s| {
270            let mut keys: Vec<&str> = s.storage.iter().map(|e| e.0.as_str()).collect();
271            keys.sort();
272            match keys.get(index as usize) {
273                Some(k) => unsafe { copy_out(k.as_bytes(), dst, cap) },
274                None => -1,
275            }
276        })
277    }
278    pub unsafe fn storage_delete(key: *const u8, len: u32) -> i32 {
279        let k = unsafe { text(key, len) };
280        with(|s| {
281            let before = s.storage.len();
282            s.storage.retain(|e| e.0 != k);
283            if s.storage.len() < before { 0 } else { -1 }
284        })
285    }
286
287    pub unsafe fn param(name: *const u8, len: u32, dst: *mut u8, cap: u32) -> i32 {
288        let n = unsafe { text(name, len) };
289        with(|s| match s.params.iter().find(|p| p.0 == n) {
290            Some(p) => unsafe { copy_out(p.1.as_bytes(), dst, cap) },
291            None => -1,
292        })
293    }
294
295    // gfx: null backend (hashes buffer writes, like headless runners)
296    fn handle() -> u32 {
297        with(|s| {
298            s.next_handle += 1;
299            s.next_handle - 1
300        })
301    }
302    pub unsafe fn gfx_width() -> u32 { 1280 }
303    pub unsafe fn gfx_height() -> u32 { 720 }
304    pub unsafe fn gfx_create_shader(_: *const u8, _: u32) -> u32 { handle() }
305    pub unsafe fn gfx_create_buffer(_: u32, _: u32) -> u32 { handle() }
306    pub unsafe fn gfx_create_pipeline(_: *const u8, _: u32) -> u32 { handle() }
307    pub unsafe fn gfx_create_bind_group(_: *const u8, _: u32) -> u32 { handle() }
308    pub unsafe fn gfx_create_bind_group_layout(_: *const u8, _: u32) -> u32 { handle() }
309    pub unsafe fn gfx_create_texture(_: *const u8, _: u32) -> u32 { handle() }
310    pub unsafe fn gfx_create_sampler(_: *const u8, _: u32) -> u32 { handle() }
311    #[allow(clippy::too_many_arguments)]
312    pub unsafe fn gfx_write_texture(t: u32, mip: u32, x: u32, y: u32, w: u32, h: u32, ptr: *const u8, len: u32) {
313        with(|s| {
314            if s.hashing {
315                for v in [t, mip, x, y, w, h] {
316                    s.video_hash = fnv(s.video_hash, &v.to_le_bytes());
317                }
318                s.video_hash = fnv(s.video_hash, unsafe { bytes(ptr, len) });
319            }
320        })
321    }
322    pub unsafe fn gfx_set_bind_group_offsets(_: u32, _: u32, _: *const u32, _: u32) {}
323    pub unsafe fn gfx_set_viewport(_: f32, _: f32, _: f32, _: f32, _: f32, _: f32) {}
324    pub unsafe fn gfx_set_scissor_rect(_: u32, _: u32, _: u32, _: u32) {}
325    pub unsafe fn gfx_write_buffer(_: u32, _: u32, ptr: *const u8, len: u32) {
326        with(|s| {
327            if s.hashing {
328                s.video_hash = fnv(s.video_hash, unsafe { bytes(ptr, len) });
329            }
330        })
331    }
332    pub unsafe fn gfx_begin_frame(_: f32, _: f32, _: f32, _: f32) -> u32 { 0 }
333    pub unsafe fn gfx_set_pipeline(_: u32) {}
334    pub unsafe fn gfx_set_bind_group(_: u32, _: u32) {}
335    pub unsafe fn gfx_set_vertex_buffer(_: u32, _: u32, _: u32) {}
336    pub unsafe fn gfx_set_index_buffer(_: u32, _: u32, _: u32) {}
337    pub unsafe fn gfx_draw(_: u32, _: u32, _: u32, _: u32) {}
338    pub unsafe fn gfx_draw_indexed(_: u32, _: u32, _: u32, _: i32, _: u32) {}
339    pub unsafe fn gfx_end_frame() {
340        with(|s| s.presented += 1);
341    }
342
343    // net: always denied natively
344    pub unsafe fn net_open(_: *const u8, _: u32) -> i32 { -1 }
345    pub unsafe fn net_state(_: i32) -> u32 { 3 }
346    pub unsafe fn net_send(_: i32, _: *const u8, _: u32) -> i32 { -1 }
347    pub unsafe fn net_recv(_: i32, _: *mut u8, _: u32) -> i32 { -1 }
348    pub unsafe fn net_close(_: i32) {}
349
350    pub unsafe fn proc_exit(code: i32) -> ! {
351        std::process::exit(code)
352    }
353}