Skip to main content

gasm_host/
window.rs

1//! The windowed runner: winit window, wgpu surface, cpal audio, gilrs gamepads,
2//! keyboard layouts, fixed-timestep frames with catch-up, and the hold-Escape quit.
3
4use std::collections::HashSet;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use gilrs::{Button, Gilrs};
9use winit::application::ApplicationHandler;
10use winit::dpi::LogicalSize;
11use winit::event::{DeviceEvent, DeviceId, ElementState, MouseButton, MouseScrollDelta, WindowEvent};
12use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop};
13use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey};
14use winit::window::{CursorGrabMode, Window, WindowId};
15
16use crate::audio::{AudioOut, AudioSink, AudioStream};
17use crate::gfx::Gfx;
18use crate::host::{self, Game, Gamepad, KEY_STATE_BYTES, Pointer, RawInput, Stop};
19use crate::keymap;
20use crate::present::Present;
21use crate::session::Session;
22
23pub struct Options {
24    /// initial window size in logical pixels
25    pub size: (u32, u32),
26    pub keymap: keymap::Keymap,
27    pub mute: bool,
28    /// how 2D frames are shown
29    pub present: Present,
30}
31
32/// Open a window and play until the guest exits, traps or the player quits.
33/// Returns the guest's exit code (0 when the player quit).
34pub fn run(session: Session, opts: Options) -> Result<i32, String> {
35    let event_loop = EventLoop::new().map_err(|e| e.to_string())?;
36    // default title: the module file name without .wasm (set_title replaces it)
37    let default_title = host::static_title(&session.wasm).unwrap_or_else(|| {
38        std::path::Path::new(&session.name).file_stem().map_or(session.name.clone(), |s| s.to_string_lossy().into_owned())
39    });
40    let mut app = App {
41        title: default_title.clone(),
42        default_title,
43        fps: 0,
44        opts,
45        session: Some(session),
46        window: None,
47        game: None,
48        keys: HashSet::new(),
49        typed: String::new(),
50        key_events: Vec::new(),
51        esc_down: None,
52        mouse: MouseState::default(),
53        applied_mode: 0,
54        pointer_flags: 0,
55        gilrs: Gilrs::new().ok(),
56        next: Instant::now(),
57        fps_t: Instant::now(),
58        fps_n: 0,
59        result: Ok(0),
60        audio_stream: None,
61    };
62    event_loop.run_app(&mut app).map_err(|e| e.to_string())?;
63    app.result
64}
65
66/// Gamepads in connection order, plus how many are connected.
67fn gamepad_pads(gilrs: &mut Gilrs) -> ([u32; 4], usize) {
68    const MAP: &[(Button, u32)] = &[
69        (Button::East, 1 << 0),
70        (Button::South, 1 << 1),
71        (Button::North, 1 << 2),
72        (Button::West, 1 << 3),
73        (Button::LeftTrigger, 1 << 4),
74        (Button::RightTrigger, 1 << 5),
75        (Button::Select, 1 << 6),
76        (Button::Start, 1 << 7),
77        (Button::DPadUp, 1 << 8),
78        (Button::DPadDown, 1 << 9),
79        (Button::DPadLeft, 1 << 10),
80        (Button::DPadRight, 1 << 11),
81    ];
82    while gilrs.next_event().is_some() {}
83    let mut pads = [0u32; 4];
84    let connected = gilrs.gamepads().count();
85    for (i, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
86        pads[i] = MAP.iter().filter(|(b, _)| pad.is_pressed(*b)).fold(0, |m, (_, bit)| m | bit);
87        let (x, y) = (pad.value(gilrs::Axis::LeftStickX), pad.value(gilrs::Axis::LeftStickY));
88        if x < -0.5 { pads[i] |= 1 << 10 }
89        if x > 0.5 { pads[i] |= 1 << 11 }
90        if y > 0.5 { pads[i] |= 1 << 8 }
91        if y < -0.5 { pads[i] |= 1 << 9 }
92    }
93    (pads, connected)
94}
95
96/// Mouse input collected between frames (window pixels).
97#[derive(Default)]
98struct MouseState {
99    x: f32,
100    y: f32,
101    inside: bool,
102    buttons: u32,
103    /// buttons that went down / up since the last frame (a click inside one frame counts)
104    pressed: u32,
105    released: u32,
106    dx: f32,
107    dy: f32,
108    wheel_x: f32,
109    wheel_y: f32,
110}
111
112/// Hold Escape this long to quit (a tap goes to the game).
113const ESC_HOLD: Duration = Duration::from_millis(1000);
114
115fn mouse_bit(b: MouseButton) -> u32 {
116    match b {
117        MouseButton::Left => 1,
118        MouseButton::Right => 2,
119        MouseButton::Middle => 4,
120        MouseButton::Back => 8,
121        MouseButton::Forward => 16,
122        MouseButton::Other(_) => 0,
123    }
124}
125
126/// Raw gamepads (first 4 connected, the same order as the pads). Known
127/// controllers in the W3C standard layout; others raw, ordered by code.
128fn gamepads_raw(gilrs: &Gilrs) -> [Gamepad; 4] {
129    use gilrs::{Axis, MappingSource};
130    const BUTTONS: [Button; 17] = [
131        Button::South, Button::East, Button::West, Button::North, Button::LeftTrigger, Button::RightTrigger,
132        Button::LeftTrigger2, Button::RightTrigger2, Button::Select, Button::Start, Button::LeftThumb,
133        Button::RightThumb, Button::DPadUp, Button::DPadDown, Button::DPadLeft, Button::DPadRight, Button::Mode,
134    ];
135    let mut out: [Gamepad; 4] = Default::default();
136    for (slot, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
137        let g = &mut out[slot];
138        g.connected = true;
139        g.name = pad.name().to_owned();
140        if pad.mapping_source() != MappingSource::None {
141            g.standard = true;
142            g.buttons = BUTTONS.iter().map(|b| pad.button_data(*b).map_or(0.0, |d| d.value())).collect();
143            // W3C: y axes point down
144            g.axes = vec![pad.value(Axis::LeftStickX), -pad.value(Axis::LeftStickY), pad.value(Axis::RightStickX), -pad.value(Axis::RightStickY)];
145        } else {
146            let mut b: Vec<_> = pad.state().buttons().map(|(c, d)| (c.into_u32(), d.value())).collect();
147            let mut a: Vec<_> = pad.state().axes().map(|(c, d)| (c.into_u32(), d.value())).collect();
148            b.sort_by_key(|x| x.0);
149            a.sort_by_key(|x| x.0);
150            g.buttons = b.into_iter().map(|x| x.1).take(host::GAMEPAD_BUTTONS).collect();
151            g.axes = a.into_iter().map(|x| x.1).take(host::GAMEPAD_AXES).collect();
152        }
153    }
154    out
155}
156
157struct App {
158    /// what the window shows before " — gasm": set_title, else `default_title`
159    title: String,
160    default_title: String,
161    /// guest frames in the last second
162    fps: u32,
163    opts: Options,
164    /// taken when the window opens
165    session: Option<Session>,
166    window: Option<Arc<Window>>,
167    game: Option<Game>,
168    keys: HashSet<KeyCode>,
169    /// text typed since the last frame (for text_input)
170    typed: String,
171    /// raw key presses/releases since the last frame
172    key_events: Vec<(u16, bool)>,
173    /// Escape: a tap goes to the game, holding it quits
174    esc_down: Option<Instant>,
175    mouse: MouseState,
176    /// cursor mode currently applied to the window (GASM_INPUT_POINTER_*)
177    applied_mode: u32,
178    /// GASM_POINTER_IS_HIDDEN / IS_LOCKED as actually achieved
179    pointer_flags: u32,
180    gilrs: Option<Gilrs>,
181    next: Instant,
182    fps_t: Instant,
183    fps_n: u32,
184    result: Result<i32, String>,
185    /// the audio device stream (kept alive while the game plays)
186    audio_stream: Option<AudioStream>,
187}
188
189impl App {
190    fn stop(&mut self, el: &ActiveEventLoop, result: Result<i32, String>) {
191        self.result = result;
192        // the event loop can tick again before it ends: never call into a guest
193        // that has exited or trapped
194        self.game = None;
195        el.exit();
196    }
197
198    /// Hide / lock the cursor as the guest asked (input_mode), when it changes.
199    /// The window title: the game's (set_title or the file name), then the runner's own part,
200    /// which the guest can't change.
201    fn show_title(&self) {
202        if let Some(w) = &self.window {
203            w.set_title(&format!("{} — gasm — {} fps", self.title, self.fps));
204        }
205    }
206
207    fn apply_input_mode(&mut self) {
208        let (Some(game), Some(w)) = (&mut self.game, &self.window) else { return };
209        let want = game.with_host(|h| h.input_mode) & 6;
210        if want == self.applied_mode {
211            return;
212        }
213        self.applied_mode = want;
214        let locked = want & 4 != 0
215            && w.set_cursor_grab(CursorGrabMode::Locked).or_else(|_| w.set_cursor_grab(CursorGrabMode::Confined)).is_ok();
216        if want & 4 == 0 {
217            let _ = w.set_cursor_grab(CursorGrabMode::None);
218        }
219        // locked implies hidden
220        w.set_cursor_visible(want == 0);
221        self.pointer_flags = if want != 0 { 2 } else { 0 } | if locked { 4 } else { 0 };
222    }
223
224    /// The player closed the window / held Esc: let the game save, then stop.
225    fn quit(&mut self, el: &ActiveEventLoop) {
226        if let Some(g) = &mut self.game {
227            g.exit();
228        }
229        self.stop(el, Ok(0));
230    }
231
232    fn tick(&mut self, el: &ActiveEventLoop) {
233        let Some(game) = &mut self.game else { return };
234        let period = Duration::from_secs_f64(1.0 / game.with_host(|h| h.frame_rate));
235        let now = Instant::now();
236        if now >= self.next {
237            // Fixed timestep: catch up at most 4 frames, only the last one is shown.
238            let behind = ((now - self.next).as_secs_f64() / period.as_secs_f64()) as u32 + 1;
239            let steps = behind.min(4);
240            // Escape held long enough: quit (a short tap went to the game as a key)
241            if self.esc_down.is_some_and(|t| t.elapsed() >= ESC_HOLD) {
242                return self.quit(el);
243            }
244            let mut keys = [0u8; KEY_STATE_BYTES];
245            for &c in &self.keys {
246                let k = keymap::gasm_key(c);
247                if k != 0 {
248                    keys[k as usize / 8] |= 1 << (k % 8);
249                }
250            }
251            // gamepads are sampled once per tick: pads and raw state agree within a batch
252            let (mut pads, gamepads) = self.gilrs.as_mut().map(gamepad_pads).unwrap_or_default();
253            let raw_pads = self.gilrs.as_ref().map(gamepads_raw).unwrap_or_default();
254            // a guest that reads the keyboard itself (KEYS_RAW) gets no keymap pads
255            if game.with_host(|h| h.input_mode) & 1 == 0 {
256                let kb = keymap::pads(&self.opts.keymap, &self.keys, gamepads);
257                for (p, k) in pads.iter_mut().zip(kb) {
258                    *p |= k;
259                }
260            }
261            let drawable = game.with_host(|h| h.gfx.size());
262            for k in 0..steps {
263                game.with_host(|host| {
264                host.pads = pads;
265                // typed text, key events and relative motion go to the first frame of a catch-up batch
266                let first = k == 0;
267                host.text = Some(if first { std::mem::take(&mut self.typed) } else { String::new() });
268                let m = &mut self.mouse;
269                let pointer = Pointer {
270                    x: m.x,
271                    y: m.y,
272                    dx: if first { m.dx } else { 0.0 },
273                    dy: if first { m.dy } else { 0.0 },
274                    wheel_x: if first { m.wheel_x } else { 0.0 },
275                    wheel_y: if first { m.wheel_y } else { 0.0 },
276                    buttons: m.buttons,
277                    pressed: if first { m.pressed } else { 0 },
278                    released: if first { m.released } else { 0 },
279                    flags: m.inside as u32 | self.pointer_flags,
280                    drawable: (drawable.0 as f32, drawable.1 as f32),
281                    integer_scale: self.opts.present.integer_scale,
282                };
283                if first {
284                    (m.dx, m.dy, m.wheel_x, m.wheel_y, m.pressed, m.released) = (0.0, 0.0, 0.0, 0.0, 0, 0);
285                }
286                if first {
287                    host.input = RawInput { keys: Some(keys), key_events: std::mem::take(&mut self.key_events), pointer: Some(pointer), gamepads: Some(raw_pads.clone()) };
288                } else {
289                    host.input.key_events.clear();
290                    host.input.pointer = Some(pointer);
291                }
292                host.show_frame = k + 1 == steps;
293                host.gfx.used = false;
294                });
295                match game.frame() {
296                    Ok(()) => {}
297                    Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
298                    Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
299                }
300                self.next += period;
301                self.fps_n += 1;
302            }
303            if behind > 4 {
304                self.next = Instant::now() + period;
305            }
306            // 2D guests: show the last video_present frame.
307            let title = game.with_host(|host| {
308                if !host.gfx.used && host.width > 0 {
309                    let (w, h) = (host.width as u32, host.height as u32);
310                    let rgba = std::mem::take(&mut host.rgba);
311                    host.gfx.present_video(&rgba, w, h, host.aspect);
312                    host.rgba = rgba;
313                }
314                std::mem::take(&mut host.title_changed).then(|| host.title.clone())
315            });
316            if let Some(t) = title {
317                self.title = t.unwrap_or_else(|| self.default_title.clone());
318                self.show_title();
319            }
320        }
321        self.apply_input_mode();
322        if self.fps_t.elapsed() >= Duration::from_secs(1) {
323            self.fps = self.fps_n;
324            self.show_title();
325            self.fps_n = 0;
326            self.fps_t = Instant::now();
327        }
328        el.set_control_flow(ControlFlow::WaitUntil(self.next));
329    }
330}
331
332impl ApplicationHandler for App {
333    fn resumed(&mut self, el: &ActiveEventLoop) {
334        if self.window.is_some() {
335            return;
336        }
337        let attrs = Window::default_attributes()
338            .with_title(format!("{} — gasm", self.title))
339            .with_inner_size(LogicalSize::new(self.opts.size.0, self.opts.size.1));
340        let window = match el.create_window(attrs) {
341            Ok(w) => Arc::new(w),
342            Err(e) => return self.stop(el, Err(format!("cannot create window: {e}"))),
343        };
344        let gfx = match Gfx::for_window(window.clone()) {
345            Ok(mut g) => {
346                g.present = self.opts.present;
347                g
348            }
349            Err(e) => return self.stop(el, Err(format!("cannot initialise GPU: {e}"))),
350        };
351        let audio: Option<Box<dyn AudioOut>> = if self.opts.mute {
352            None
353        } else {
354            match AudioSink::open() {
355                Ok((a, stream)) => {
356                    eprintln!("[gasm] audio: {} Hz", a.device_rate());
357                    self.audio_stream = Some(stream);
358                    Some(Box::new(a))
359                }
360                Err(e) => {
361                    eprintln!("[gasm] audio disabled: {e}");
362                    None
363                }
364            }
365        };
366        let Some(session) = self.session.take() else { return };
367        match session.start(audio, gfx, false, false) {
368            // no native GL backend yet (design/gasm-gl.md): browser and headless only
369            Ok(g) if g.uses_gl() => {
370                return self.stop(el, Err("this game uses gasm:gl (OpenGL ES), which gasm-run supports headless only for now; play it in the browser".into()));
371            }
372            Ok(g) => self.game = Some(g),
373            Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
374            Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
375        }
376        self.window = Some(window);
377        self.next = Instant::now();
378        el.set_control_flow(ControlFlow::WaitUntil(self.next));
379    }
380
381    fn window_event(&mut self, el: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
382        match event {
383            WindowEvent::CloseRequested => self.quit(el),
384            WindowEvent::Resized(size) => {
385                if let Some(g) = &mut self.game {
386                    g.with_host(|h| h.gfx.resize(size.width, size.height));
387                }
388            }
389            WindowEvent::Focused(false) => {
390                // nothing stays held while another app has the input
391                for c in self.keys.drain() {
392                    let k = keymap::gasm_key(c);
393                    if k != 0 {
394                        self.key_events.push((k, false));
395                    }
396                }
397                self.esc_down = None;
398                self.mouse.inside = false;
399                self.mouse.released |= self.mouse.buttons;
400                self.mouse.buttons = 0;
401            }
402            WindowEvent::CursorMoved { position, .. } => {
403                self.mouse.x = position.x as f32;
404                self.mouse.y = position.y as f32;
405                self.mouse.inside = true;
406            }
407            WindowEvent::CursorEntered { .. } => self.mouse.inside = true,
408            WindowEvent::CursorLeft { .. } => self.mouse.inside = false,
409            WindowEvent::MouseInput { state, button, .. } => {
410                let bit = mouse_bit(button);
411                match state {
412                    ElementState::Pressed => {
413                        self.mouse.buttons |= bit;
414                        self.mouse.pressed |= bit;
415                    }
416                    ElementState::Released => {
417                        self.mouse.buttons &= !bit;
418                        self.mouse.released |= bit;
419                    }
420                }
421            }
422            WindowEvent::MouseWheel { delta, .. } => {
423                // gasm: about 1 per wheel notch, y > 0 = down (W3C sign); winit's is the opposite
424                let (x, y) = match delta {
425                    MouseScrollDelta::LineDelta(x, y) => (x, y),
426                    MouseScrollDelta::PixelDelta(p) => (p.x as f32 / 100.0, p.y as f32 / 100.0),
427                };
428                self.mouse.wheel_x -= x;
429                self.mouse.wheel_y -= y;
430            }
431            WindowEvent::KeyboardInput { event, .. } => {
432                if let PhysicalKey::Code(code) = event.physical_key {
433                    if code == KeyCode::Escape && !event.repeat {
434                        self.esc_down = (event.state == ElementState::Pressed).then(Instant::now);
435                    }
436                    let k = keymap::gasm_key(code);
437                    if !event.repeat && k != 0 {
438                        self.key_events.push((k, event.state == ElementState::Pressed));
439                    }
440                    match event.state {
441                        ElementState::Pressed => self.keys.insert(code),
442                        ElementState::Released => self.keys.remove(&code),
443                    };
444                }
445                if event.state == ElementState::Pressed {
446                    match &event.logical_key {
447                        Key::Named(NamedKey::Enter) => self.typed.push('\n'),
448                        Key::Named(NamedKey::Backspace) => self.typed.push('\u{8}'),
449                        _ => {
450                            if let Some(t) = &event.text {
451                                self.typed.extend(t.chars().filter(|c| !c.is_control()));
452                            }
453                        }
454                    }
455                }
456            }
457            WindowEvent::RedrawRequested => self.tick(el),
458            _ => {}
459        }
460    }
461
462    fn device_event(&mut self, _el: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) {
463        // raw (unaccelerated) motion, also while the cursor is locked
464        if let DeviceEvent::MouseMotion { delta } = event
465            && self.window.as_ref().is_some_and(|w| w.has_focus())
466        {
467            self.mouse.dx += delta.0 as f32;
468            self.mouse.dy += delta.1 as f32;
469        }
470    }
471
472    fn about_to_wait(&mut self, el: &ActiveEventLoop) {
473        self.tick(el);
474    }
475}
476