1use 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::angle::NativeWindow;
18use crate::gfx::Gfx;
19use crate::host::{self, Game, Gamepad, KEY_STATE_BYTES, Pointer, RawInput, Stop};
20use crate::keymap;
21use crate::present::Present;
22use crate::session::Session;
23use crate::splash;
24
25pub struct Options {
26 pub size: (u32, u32),
28 pub keymap: keymap::Keymap,
29 pub mute: bool,
30 pub present: Present,
32 pub screenshot: Option<(u64, String)>,
34 pub splash: bool,
36}
37
38struct Splash {
40 frame: u32,
41 next: Instant,
42 compiling: Option<std::thread::JoinHandle<Result<wasmtime::Module, Stop>>>,
43 module: Option<Result<wasmtime::Module, Stop>>,
44 skip: bool,
46 audio: Option<Box<dyn AudioOut>>,
48 gfx: Gfx,
49 gl: Option<crate::angle::Angle>,
50}
51
52fn native_window(window: &Window) -> Result<NativeWindow, String> {
54 use winit::raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawDisplayHandle, RawWindowHandle};
55 let w = window.window_handle().map_err(|e| e.to_string())?.as_raw();
56 let d = window.display_handle().map_err(|e| e.to_string())?.as_raw();
57 Ok(match (w, d) {
58 (RawWindowHandle::AppKit(h), _) => NativeWindow::AppKit(h.ns_view.as_ptr()),
59 (RawWindowHandle::Win32(h), _) => NativeWindow::Win32(h.hwnd.get() as *mut std::ffi::c_void),
60 (RawWindowHandle::Xlib(h), RawDisplayHandle::Xlib(d)) => {
61 NativeWindow::Xlib(d.display.map_or(std::ptr::null_mut(), |p| p.as_ptr()), h.window)
62 }
63 (RawWindowHandle::Wayland(_), RawDisplayHandle::Wayland(d)) => NativeWindow::Wayland(d.display.as_ptr()),
64 _ => return Err("gasm:gl: this kind of window isn't supported".into()),
65 })
66}
67
68pub fn run(session: Session, opts: Options) -> Result<i32, String> {
71 let mut builder = EventLoop::builder();
72 #[cfg(all(unix, not(target_os = "macos")))]
74 if session.uses_gl() {
75 use winit::platform::x11::EventLoopBuilderExtX11;
76 builder.with_x11();
77 }
78 let event_loop = builder.build().map_err(|e| e.to_string())?;
79 let default_title = host::static_title(&session.wasm).unwrap_or_else(|| {
81 std::path::Path::new(&session.name).file_stem().map_or(session.name.clone(), |s| s.to_string_lossy().into_owned())
82 });
83 let mut app = App {
84 title: default_title.clone(),
85 default_title,
86 fps: 0,
87 opts,
88 session: Some(session),
89 window: None,
90 game: None,
91 keys: HashSet::new(),
92 typed: String::new(),
93 key_events: Vec::new(),
94 esc_down: None,
95 mouse: MouseState::default(),
96 applied_mode: 0,
97 pointer_flags: 0,
98 gilrs: Gilrs::new().ok(),
99 next: Instant::now(),
100 fps_t: Instant::now(),
101 fps_n: 0,
102 result: Ok(0),
103 frames_run: 0,
104 audio_stream: None,
105 splash: None,
106 };
107 event_loop.run_app(&mut app).map_err(|e| e.to_string())?;
108 app.result
109}
110
111fn gamepad_pads(gilrs: &mut Gilrs) -> ([u32; 4], usize) {
113 const MAP: &[(Button, u32)] = &[
114 (Button::East, 1 << 0),
115 (Button::South, 1 << 1),
116 (Button::North, 1 << 2),
117 (Button::West, 1 << 3),
118 (Button::LeftTrigger, 1 << 4),
119 (Button::RightTrigger, 1 << 5),
120 (Button::Select, 1 << 6),
121 (Button::Start, 1 << 7),
122 (Button::DPadUp, 1 << 8),
123 (Button::DPadDown, 1 << 9),
124 (Button::DPadLeft, 1 << 10),
125 (Button::DPadRight, 1 << 11),
126 ];
127 while gilrs.next_event().is_some() {}
128 let mut pads = [0u32; 4];
129 let connected = gilrs.gamepads().count();
130 for (i, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
131 pads[i] = MAP.iter().filter(|(b, _)| pad.is_pressed(*b)).fold(0, |m, (_, bit)| m | bit);
132 let (x, y) = (pad.value(gilrs::Axis::LeftStickX), pad.value(gilrs::Axis::LeftStickY));
133 if x < -0.5 { pads[i] |= 1 << 10 }
134 if x > 0.5 { pads[i] |= 1 << 11 }
135 if y > 0.5 { pads[i] |= 1 << 8 }
136 if y < -0.5 { pads[i] |= 1 << 9 }
137 }
138 (pads, connected)
139}
140
141#[derive(Default)]
143struct MouseState {
144 x: f32,
145 y: f32,
146 inside: bool,
147 buttons: u32,
148 pressed: u32,
150 released: u32,
151 dx: f32,
152 dy: f32,
153 wheel_x: f32,
154 wheel_y: f32,
155}
156
157const ESC_HOLD: Duration = Duration::from_millis(1000);
159
160fn mouse_bit(b: MouseButton) -> u32 {
161 match b {
162 MouseButton::Left => 1,
163 MouseButton::Right => 2,
164 MouseButton::Middle => 4,
165 MouseButton::Back => 8,
166 MouseButton::Forward => 16,
167 MouseButton::Other(_) => 0,
168 }
169}
170
171fn gamepads_raw(gilrs: &Gilrs) -> [Gamepad; 4] {
174 use gilrs::{Axis, MappingSource};
175 const BUTTONS: [Button; 17] = [
176 Button::South, Button::East, Button::West, Button::North, Button::LeftTrigger, Button::RightTrigger,
177 Button::LeftTrigger2, Button::RightTrigger2, Button::Select, Button::Start, Button::LeftThumb,
178 Button::RightThumb, Button::DPadUp, Button::DPadDown, Button::DPadLeft, Button::DPadRight, Button::Mode,
179 ];
180 let mut out: [Gamepad; 4] = Default::default();
181 for (slot, (_, pad)) in gilrs.gamepads().take(4).enumerate() {
182 let g = &mut out[slot];
183 g.connected = true;
184 g.name = pad.name().to_owned();
185 if pad.mapping_source() != MappingSource::None {
186 g.standard = true;
187 g.buttons = BUTTONS.iter().map(|b| pad.button_data(*b).map_or(0.0, |d| d.value())).collect();
188 g.axes = vec![pad.value(Axis::LeftStickX), -pad.value(Axis::LeftStickY), pad.value(Axis::RightStickX), -pad.value(Axis::RightStickY)];
190 } else {
191 let mut b: Vec<_> = pad.state().buttons().map(|(c, d)| (c.into_u32(), d.value())).collect();
192 let mut a: Vec<_> = pad.state().axes().map(|(c, d)| (c.into_u32(), d.value())).collect();
193 b.sort_by_key(|x| x.0);
194 a.sort_by_key(|x| x.0);
195 g.buttons = b.into_iter().map(|x| x.1).take(host::GAMEPAD_BUTTONS).collect();
196 g.axes = a.into_iter().map(|x| x.1).take(host::GAMEPAD_AXES).collect();
197 }
198 }
199 out
200}
201
202struct App {
203 frames_run: u64,
205 title: String,
207 default_title: String,
208 fps: u32,
210 opts: Options,
211 session: Option<Session>,
213 window: Option<Arc<Window>>,
214 game: Option<Game>,
215 keys: HashSet<KeyCode>,
216 typed: String,
218 key_events: Vec<(u16, bool)>,
220 esc_down: Option<Instant>,
222 mouse: MouseState,
223 applied_mode: u32,
225 pointer_flags: u32,
227 gilrs: Option<Gilrs>,
228 next: Instant,
229 fps_t: Instant,
230 fps_n: u32,
231 result: Result<i32, String>,
232 audio_stream: Option<AudioStream>,
234 splash: Option<Splash>,
235}
236
237impl App {
238 fn stop(&mut self, el: &ActiveEventLoop, result: Result<i32, String>) {
239 self.result = result;
240 self.game = None;
243 el.exit();
244 }
245
246 fn show_title(&self) {
250 if let Some(w) = &self.window {
251 w.set_title(&format!("{} — gasm — {} fps", self.title, self.fps));
252 }
253 }
254
255 fn apply_input_mode(&mut self) {
256 let (Some(game), Some(w)) = (&mut self.game, &self.window) else { return };
257 let want = game.with_host(|h| h.input_mode) & 6;
258 if want == self.applied_mode {
259 return;
260 }
261 self.applied_mode = want;
262 let locked = want & 4 != 0
263 && w.set_cursor_grab(CursorGrabMode::Locked).or_else(|_| w.set_cursor_grab(CursorGrabMode::Confined)).is_ok();
264 if want & 4 == 0 {
265 let _ = w.set_cursor_grab(CursorGrabMode::None);
266 }
267 w.set_cursor_visible(want == 0);
269 self.pointer_flags = if want != 0 { 2 } else { 0 } | if locked { 4 } else { 0 };
270 }
271
272 fn quit(&mut self, el: &ActiveEventLoop) {
274 if let Some(g) = &mut self.game {
275 g.exit();
276 }
277 self.stop(el, Ok(0));
278 }
279
280 fn tick_splash(&mut self, el: &ActiveEventLoop) {
283 let Some(sp) = &mut self.splash else { return };
284 if Instant::now() < sp.next {
285 return el.set_control_flow(ControlFlow::WaitUntil(sp.next));
286 }
287 if sp.compiling.as_ref().is_some_and(|t| t.is_finished()) {
288 let t = sp.compiling.take().expect("compile thread");
289 sp.module = Some(t.join().unwrap_or_else(|_| Err(Stop::Trap("the compiler thread panicked".into()))));
290 }
291 let ready = sp.module.is_some();
292 if sp.skip {
293 sp.frame = if ready { splash::FRAMES } else { sp.frame.max(splash::HOLD) };
294 }
295 if sp.frame >= splash::FRAMES && ready {
296 let sp = self.splash.take().expect("splash");
297 return self.start_game(el, sp.module.expect("compiled"), sp.audio, sp.gfx, sp.gl);
298 }
299 let rgba = splash::frame(sp.frame);
300 match &sp.gl {
301 Some(angle) => angle.show_image(&rgba, splash::W as u32, splash::H as u32),
302 None => sp.gfx.present_video(&rgba, splash::W as u32, splash::H as u32, None),
303 }
304 if sp.frame != splash::HOLD || ready {
305 sp.frame += 1;
306 }
307 sp.next += Duration::from_secs_f64(1.0 / 60.0);
308 if sp.next < Instant::now() {
309 sp.next = Instant::now();
310 }
311 el.set_control_flow(ControlFlow::WaitUntil(sp.next));
312 }
313
314 fn start_game(&mut self, el: &ActiveEventLoop, module: Result<wasmtime::Module, Stop>, audio: Option<Box<dyn AudioOut>>, gfx: Gfx, gl: Option<crate::angle::Angle>) {
316 let Some(session) = self.session.take() else { return };
317 let started = match module {
318 Ok(m) => session.start_compiled(m, audio, gfx, gl),
319 Err(e) => Err(e),
320 };
321 match started {
322 Ok(g) => self.game = Some(g),
323 Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
324 Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
325 }
326 self.typed.clear();
328 self.key_events.clear();
329 self.mouse.pressed = 0;
330 self.mouse.released = 0;
331 self.next = Instant::now();
332 el.set_control_flow(ControlFlow::WaitUntil(self.next));
333 }
334
335 fn tick(&mut self, el: &ActiveEventLoop) {
336 if self.splash.is_some() {
337 if self.esc_down.is_some_and(|t| t.elapsed() >= ESC_HOLD) {
338 return self.quit(el);
339 }
340 return self.tick_splash(el);
341 }
342 let Some(game) = &mut self.game else { return };
343 let period = Duration::from_secs_f64(1.0 / game.with_host(|h| h.frame_rate));
344 let now = Instant::now();
345 if now >= self.next {
346 let behind = ((now - self.next).as_secs_f64() / period.as_secs_f64()) as u32 + 1;
348 let steps = behind.min(4);
349 if self.esc_down.is_some_and(|t| t.elapsed() >= ESC_HOLD) {
351 return self.quit(el);
352 }
353 let mut keys = [0u8; KEY_STATE_BYTES];
354 for &c in &self.keys {
355 let k = keymap::gasm_key(c);
356 if k != 0 {
357 keys[k as usize / 8] |= 1 << (k % 8);
358 }
359 }
360 let (mut pads, gamepads) = self.gilrs.as_mut().map(gamepad_pads).unwrap_or_default();
362 let raw_pads = self.gilrs.as_ref().map(gamepads_raw).unwrap_or_default();
363 if game.with_host(|h| h.input_mode) & 1 == 0 {
365 let kb = keymap::pads(&self.opts.keymap, &self.keys, gamepads);
366 for (p, k) in pads.iter_mut().zip(kb) {
367 *p |= k;
368 }
369 }
370 let drawable = game.with_host(|h| h.gl.size().unwrap_or_else(|| h.gfx.size()));
371 for k in 0..steps {
372 game.with_host(|host| {
373 host.pads = pads;
374 let first = k == 0;
376 host.text = Some(if first { std::mem::take(&mut self.typed) } else { String::new() });
377 let m = &mut self.mouse;
378 let pointer = Pointer {
379 x: m.x,
380 y: m.y,
381 dx: if first { m.dx } else { 0.0 },
382 dy: if first { m.dy } else { 0.0 },
383 wheel_x: if first { m.wheel_x } else { 0.0 },
384 wheel_y: if first { m.wheel_y } else { 0.0 },
385 buttons: m.buttons,
386 pressed: if first { m.pressed } else { 0 },
387 released: if first { m.released } else { 0 },
388 flags: m.inside as u32 | self.pointer_flags,
389 drawable: (drawable.0 as f32, drawable.1 as f32),
390 integer_scale: self.opts.present.integer_scale,
391 };
392 if first {
393 (m.dx, m.dy, m.wheel_x, m.wheel_y, m.pressed, m.released) = (0.0, 0.0, 0.0, 0.0, 0, 0);
394 }
395 if first {
396 host.input = RawInput { keys: Some(keys), key_events: std::mem::take(&mut self.key_events), pointer: Some(pointer), gamepads: Some(raw_pads.clone()) };
397 } else {
398 host.input.key_events.clear();
399 host.input.pointer = Some(pointer);
400 }
401 host.show_frame = k + 1 == steps;
402 host.catch_up = k + 1 < steps;
403 host.gfx.used = false;
404 });
405 let r = game.frame();
406 self.frames_run += 1;
407 let shot = self.opts.screenshot.as_ref().filter(|(n, _)| *n == self.frames_run).map(|(_, p)| p.clone());
408 let img = game.with_host(|h| {
409 let img = shot.as_ref().and_then(|_| h.gl.read_frame());
410 let show = h.show_frame;
411 h.gl.end_frame(show);
412 img
413 });
414 if let Some(path) = shot {
415 let r = match img {
416 Some((w, h, rgba)) => crate::headless::write_png(&path, w, h, &rgba).map(|_| eprintln!("[gasm] wrote {path}")),
417 None => Err("--window-screenshot: only gasm:gl games".into()),
418 };
419 return match r {
420 Ok(()) => self.quit(el),
421 Err(e) => self.stop(el, Err(e)),
422 };
423 }
424 match r {
425 Ok(()) => {}
426 Err(Stop::Exit(code)) => return self.stop(el, Ok(code)),
427 Err(Stop::Trap(e)) => return self.stop(el, Err(e)),
428 }
429 self.next += period;
430 self.fps_n += 1;
431 }
432 if behind > 4 {
433 self.next = Instant::now() + period;
434 }
435 let title = game.with_host(|host| {
437 if !host.gfx.used && host.width > 0 {
438 let (w, h) = (host.width as u32, host.height as u32);
439 let rgba = std::mem::take(&mut host.rgba);
440 host.gfx.present_video(&rgba, w, h, host.aspect);
441 host.rgba = rgba;
442 }
443 std::mem::take(&mut host.title_changed).then(|| host.title.clone())
444 });
445 if let Some(t) = title {
446 self.title = t.unwrap_or_else(|| self.default_title.clone());
447 self.show_title();
448 }
449 }
450 self.apply_input_mode();
451 if let Some(s) = &mut self.audio_stream {
452 s.check(); }
454 if self.fps_t.elapsed() >= Duration::from_secs(1) {
455 self.fps = self.fps_n;
456 self.show_title();
457 self.fps_n = 0;
458 self.fps_t = Instant::now();
459 }
460 el.set_control_flow(ControlFlow::WaitUntil(self.next));
461 }
462}
463
464impl ApplicationHandler for App {
465 fn resumed(&mut self, el: &ActiveEventLoop) {
466 if self.window.is_some() {
467 return;
468 }
469 let attrs = Window::default_attributes()
470 .with_title(format!("{} — gasm", self.title))
471 .with_inner_size(LogicalSize::new(self.opts.size.0, self.opts.size.1));
472 let window = match el.create_window(attrs) {
473 Ok(w) => Arc::new(w),
474 Err(e) => return self.stop(el, Err(format!("cannot create window: {e}"))),
475 };
476 let uses_gl = self.session.as_ref().is_some_and(|s| s.uses_gl());
478 let mut gl = None;
479 let gfx = if uses_gl {
480 let native = match native_window(&window) {
481 Ok(n) => n,
482 Err(e) => return self.stop(el, Err(e)),
483 };
484 match self.session.as_ref().map(|s| s.open_gl(Some(native), (0, 0))) {
485 Some(Ok(a)) => gl = Some(a),
486 Some(Err(e)) => return self.stop(el, Err(format!("cannot initialise OpenGL ES (gasm:gl): {e}"))),
487 None => return,
488 }
489 Gfx::null()
490 } else {
491 match Gfx::for_window(window.clone()) {
492 Ok(mut g) => {
493 g.present = self.opts.present;
494 g
495 }
496 Err(e) => return self.stop(el, Err(format!("cannot initialise GPU: {e}"))),
497 }
498 };
499 let audio: Option<Box<dyn AudioOut>> = if self.opts.mute {
500 None
501 } else {
502 match AudioSink::open() {
503 Ok((a, stream)) => {
504 eprintln!("[gasm] audio: {} Hz", a.device_rate());
505 self.audio_stream = Some(stream);
506 Some(Box::new(a))
507 }
508 Err(e) => {
509 eprintln!("[gasm] audio disabled: {e}");
510 None
511 }
512 }
513 };
514 self.window = Some(window);
515 let Some(session) = self.session.as_ref() else { return };
516 if self.opts.splash {
517 let compiling = Some(session.compile_in_background());
519 let next = Instant::now();
520 self.splash = Some(Splash { frame: 0, next, compiling, module: None, skip: false, audio, gfx, gl });
521 el.set_control_flow(ControlFlow::WaitUntil(next));
522 } else {
523 let module = crate::host::Game::compile(&session.wasm, session.load.allow_precompiled);
524 self.start_game(el, module, audio, gfx, gl);
525 }
526 }
527
528 fn window_event(&mut self, el: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
529 match event {
530 WindowEvent::CloseRequested => self.quit(el),
531 WindowEvent::Resized(size) => {
532 if let Some(g) = &mut self.game {
533 g.with_host(|h| h.gfx.resize(size.width, size.height));
534 }
535 if let Some(sp) = &mut self.splash {
536 sp.gfx.resize(size.width, size.height);
537 }
538 }
539 WindowEvent::Focused(false) => {
540 for c in self.keys.drain() {
542 let k = keymap::gasm_key(c);
543 if k != 0 {
544 self.key_events.push((k, false));
545 }
546 }
547 self.esc_down = None;
548 self.mouse.inside = false;
549 self.mouse.released |= self.mouse.buttons;
550 self.mouse.buttons = 0;
551 }
552 WindowEvent::CursorMoved { position, .. } => {
553 self.mouse.x = position.x as f32;
554 self.mouse.y = position.y as f32;
555 self.mouse.inside = true;
556 }
557 WindowEvent::CursorEntered { .. } => self.mouse.inside = true,
558 WindowEvent::CursorLeft { .. } => self.mouse.inside = false,
559 WindowEvent::MouseInput { state, button, .. } => {
560 if let Some(sp) = &mut self.splash {
561 sp.skip |= state == ElementState::Pressed;
562 }
563 let bit = mouse_bit(button);
564 match state {
565 ElementState::Pressed => {
566 self.mouse.buttons |= bit;
567 self.mouse.pressed |= bit;
568 }
569 ElementState::Released => {
570 self.mouse.buttons &= !bit;
571 self.mouse.released |= bit;
572 }
573 }
574 }
575 WindowEvent::MouseWheel { delta, .. } => {
576 let (x, y) = match delta {
578 MouseScrollDelta::LineDelta(x, y) => (x, y),
579 MouseScrollDelta::PixelDelta(p) => (p.x as f32 / 100.0, p.y as f32 / 100.0),
580 };
581 self.mouse.wheel_x -= x;
582 self.mouse.wheel_y -= y;
583 }
584 WindowEvent::KeyboardInput { event, .. } => {
585 if let Some(sp) = &mut self.splash {
586 sp.skip |= event.state == ElementState::Pressed;
587 }
588 if let PhysicalKey::Code(code) = event.physical_key {
589 if code == KeyCode::Escape && !event.repeat {
590 self.esc_down = (event.state == ElementState::Pressed).then(Instant::now);
591 }
592 let k = keymap::gasm_key(code);
593 if !event.repeat && k != 0 {
594 self.key_events.push((k, event.state == ElementState::Pressed));
595 }
596 match event.state {
597 ElementState::Pressed => self.keys.insert(code),
598 ElementState::Released => self.keys.remove(&code),
599 };
600 }
601 if event.state == ElementState::Pressed {
602 match &event.logical_key {
603 Key::Named(NamedKey::Enter) => self.typed.push('\n'),
604 Key::Named(NamedKey::Backspace) => self.typed.push('\u{8}'),
605 _ => {
606 if let Some(t) = &event.text {
607 self.typed.extend(t.chars().filter(|c| !c.is_control()));
608 }
609 }
610 }
611 }
612 }
613 WindowEvent::RedrawRequested => self.tick(el),
614 _ => {}
615 }
616 }
617
618 fn device_event(&mut self, _el: &ActiveEventLoop, _id: DeviceId, event: DeviceEvent) {
619 if let DeviceEvent::MouseMotion { delta } = event
621 && self.window.as_ref().is_some_and(|w| w.has_focus())
622 {
623 self.mouse.dx += delta.0 as f32;
624 self.mouse.dy += delta.1 as f32;
625 }
626 }
627
628 fn about_to_wait(&mut self, el: &ActiveEventLoop) {
629 self.tick(el);
630 }
631}
632