lf_gfx/
game.rs

1//! A 'Game' in this context is a program that uses both wgpu and winit.
2pub(crate) mod input;
3mod surface;
4pub(crate) mod window;
5
6use std::sync::{atomic::AtomicBool, Arc};
7
8use log::info;
9use winit::{
10    dpi::PhysicalPosition,
11    event::{DeviceEvent, Event, WindowEvent},
12    event_loop::{ActiveEventLoop, EventLoop},
13    keyboard::PhysicalKey,
14    window::Window,
15};
16
17use crate::{game::window::GameWindow, LfLimitsExt};
18
19use self::input::{InputMap, MouseInputType, VectorInputActivation, VectorInputType};
20
21/// A cloneable and distributable flag that can be cheaply queried to see if the game has exited.
22///
23/// The idea is to clone this into in every thread you spawn so that they can gracefully exit when the game does.
24#[derive(Clone)]
25pub struct ExitFlag {
26    inner: Arc<AtomicBool>,
27}
28
29impl ExitFlag {
30    fn new() -> Self {
31        Self {
32            inner: Arc::new(AtomicBool::new(false)),
33        }
34    }
35
36    pub fn get(&self) -> bool {
37        self.inner.load(std::sync::atomic::Ordering::SeqCst)
38    }
39
40    fn set(&self) {
41        self.inner.store(true, std::sync::atomic::Ordering::SeqCst)
42    }
43}
44
45#[derive(Debug, Clone, Copy)]
46pub enum InputMode {
47    /// Indicates that any keyboard, mouse or gamepad input should be captured by the input management system,
48    /// no raw input events should be passed to the game implementation, and the cursor should be hidden.
49    Exclusive,
50    /// Indicates that any keyboard, mouse or gamepad input should not be captured by the input management system,
51    /// all raw input events should be passed to the game implementation, and the cursor should be shown.
52    UI,
53    /// Indicates that keyboard, mouse or gamepad input should be captured both by the input management system,
54    /// and raw input events should be passed to the game implementation, and the cursor should be shown.
55    Unified,
56}
57impl InputMode {
58    fn should_hide_cursor(self) -> bool {
59        match self {
60            InputMode::Exclusive => true,
61            InputMode::UI => false,
62            InputMode::Unified => false,
63        }
64    }
65    fn should_handle_input(self) -> bool {
66        match self {
67            InputMode::Exclusive => true,
68            InputMode::UI => false,
69            InputMode::Unified => true,
70        }
71    }
72    fn should_propogate_raw_input(self) -> bool {
73        match self {
74            InputMode::Exclusive => false,
75            InputMode::UI => true,
76            InputMode::Unified => true,
77        }
78    }
79    fn should_lock_cursor(self) -> bool {
80        match self {
81            InputMode::Exclusive => true,
82            InputMode::UI => false,
83            InputMode::Unified => false,
84        }
85    }
86}
87
88/// A command sent to the game to change the game state
89pub enum GameCommand {
90    Exit,
91    SetInputMode(InputMode),
92    SetMouseSensitivity(f32),
93}
94
95pub struct GameData {
96    pub command_sender: flume::Sender<GameCommand>,
97    pub surface_format: wgpu::TextureFormat,
98    pub limits: wgpu::Limits,
99    pub size: winit::dpi::PhysicalSize<u32>,
100    pub window: GameWindow,
101    pub device: wgpu::Device,
102    pub queue: wgpu::Queue,
103    pub exit_flag: ExitFlag,
104}
105
106/// All of the callbacks required to implement a game. This API is built on top of a message passing
107/// event system, and so calls to the below methods may be made concurrently, in any order, and on
108/// different threads.
109pub trait Game: Sized {
110    /// Data processed before the window exists. This should be minimal and kept to `mpsc` message reception from initialiser threads.
111    type InitData;
112
113    type LinearInputType;
114    type VectorInputType;
115
116    fn title() -> impl Into<String>;
117
118    fn target_limits() -> wgpu::Limits {
119        wgpu::Limits::downlevel_webgl2_defaults()
120    }
121    fn default_inputs(&self) -> InputMap<Self::LinearInputType, Self::VectorInputType>;
122
123    fn init(data: &GameData, init: Self::InitData) -> anyhow::Result<Self>;
124
125    /// Allows you to intercept and cancel events, before passing them off to the standard event handler,
126    /// to allow for egui integration, among others.
127    ///
128    /// This method only receives input events if the cursor is not captured, to avoid UI glitches.
129    fn process_raw_event<'a, T>(&mut self, _: &GameData, event: Event<T>) -> Option<Event<T>> {
130        Some(event)
131    }
132
133    fn window_resize(&mut self, data: &GameData, new_size: winit::dpi::PhysicalSize<u32>);
134
135    fn handle_linear_input(
136        &mut self,
137        data: &GameData,
138        input: &Self::LinearInputType,
139        activation: input::LinearInputActivation,
140    );
141
142    fn handle_vector_input(
143        &mut self,
144        data: &GameData,
145        input: &Self::VectorInputType,
146        activation: input::VectorInputActivation,
147    );
148
149    /// Requests that the next frame is drawn into the view, pretty please :)
150    fn render_to(&mut self, data: &GameData, view: wgpu::TextureView);
151
152    /// Invoked when the window is told to close (i.e. x pressed, sigint, etc.) but not when
153    /// a synthetic exit is triggered by enqueuing `GameCommand::Exit`. To actually do something with the
154    /// user's request to quit, this method must enqueue `GameCommand::Exit`
155    fn user_exit_requested(&mut self, data: &GameData) {
156        let _ = data.command_sender.send(GameCommand::Exit);
157    }
158
159    /// Invoked right at the end of the program life, after the final frame is rendered.
160    fn finished(self, _: GameData) {}
161}
162
163/// All the data held by a program/game while running. `T` gives the top-level state for the game
164/// implementation
165pub(crate) struct GameState<T: Game> {
166    data: GameData,
167    game: T,
168    input_map: input::InputMap<T::LinearInputType, T::VectorInputType>,
169    command_receiver: flume::Receiver<GameCommand>,
170
171    surface: surface::ResizableSurface<'static>,
172
173    // While true, disallows cursor movement
174    input_mode: InputMode,
175    // The last position we saw the cursor at
176    last_cursor_position: PhysicalPosition<f64>,
177    // A multiplier, from pixels moved to intensity, clamped at 1.0
178    mouse_sensitivity: f32,
179}
180
181impl<T: Game + 'static> GameState<T> {
182    // Creating some of the wgpu types requires async code
183    async fn new(init: T::InitData, window: GameWindow) -> anyhow::Result<Self> {
184        let size = (&window).inner_size();
185
186        #[cfg(debug_assertions)]
187        let flags = wgpu::InstanceFlags::DEBUG | wgpu::InstanceFlags::VALIDATION;
188        #[cfg(not(debug_assertions))]
189        let flags = wgpu::InstanceFlags::DISCARD_HAL_LABELS;
190
191        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
192            backends: wgpu::Backends::from_env().unwrap_or_default(),
193            flags,
194            backend_options: wgpu::BackendOptions {
195                gl: wgpu::GlBackendOptions {
196                    gles_minor_version: wgpu::Gles3MinorVersion::Automatic,
197                    fence_behavior: wgpu::GlFenceBehavior::Normal,
198                },
199                dx12: wgpu::Dx12BackendOptions::from_env_or_default(),
200                noop: wgpu::NoopBackendOptions { enable: true },
201            },
202            memory_budget_thresholds: wgpu::MemoryBudgetThresholds {
203                for_resource_creation: None,
204                for_device_loss: None,
205            },
206        });
207
208        let surface = window.create_surface(&instance)?;
209
210        let adapter = instance
211            .request_adapter(&wgpu::RequestAdapterOptions {
212                power_preference: wgpu::PowerPreference::HighPerformance,
213                force_fallback_adapter: false,
214                compatible_surface: Some(&surface),
215            })
216            .await?;
217
218        let available_limits = if cfg!(target_arch = "wasm32") {
219            wgpu::Limits::downlevel_webgl2_defaults()
220        } else {
221            adapter.limits()
222        };
223
224        let target_limits = T::target_limits();
225        let required_limits = available_limits.intersection(&target_limits);
226
227        let mut required_features = wgpu::Features::empty();
228        // Assume integrated and virtual GPUs, and CPUs, are UMA
229        if adapter
230            .features()
231            .contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS)
232            && matches!(
233                adapter.get_info().device_type,
234                wgpu::DeviceType::IntegratedGpu
235                    | wgpu::DeviceType::Cpu
236                    | wgpu::DeviceType::VirtualGpu
237            )
238        {
239            required_features |= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
240        }
241        // Things that are always helpful
242        required_features |= adapter.features().intersection(
243            wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES,
244        );
245
246        info!("info: {:#?}", adapter.get_info());
247        info!("limits: {:#?}", adapter.limits());
248
249        let (device, queue) = adapter
250            .request_device(&wgpu::DeviceDescriptor {
251                required_features,
252                required_limits: required_limits.clone(),
253                label: None,
254                memory_hints: wgpu::MemoryHints::Performance,
255                experimental_features: wgpu::ExperimentalFeatures::disabled(),
256                trace: wgpu::Trace::Off,
257            })
258            .await?;
259
260        // Configure surface
261        let mut surface_config = surface
262            .get_default_config(&adapter, size.width, size.height)
263            .ok_or(anyhow::Error::msg("failed to get surface configuration"))?;
264        surface_config.present_mode = wgpu::PresentMode::AutoVsync;
265        surface.configure(&device, &surface_config);
266
267        let surface_caps = surface.get_capabilities(&adapter);
268
269        let surface_format = surface_caps
270            .formats
271            .iter()
272            .copied()
273            .filter(|f| f.is_srgb())
274            .next()
275            .unwrap_or(surface_caps.formats[0]);
276
277        let config = wgpu::SurfaceConfiguration {
278            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
279            format: surface_format,
280            width: size.width,
281            height: size.height,
282            present_mode: surface_caps.present_modes[0],
283            alpha_mode: surface_caps.alpha_modes[0],
284            view_formats: vec![],
285            desired_maximum_frame_latency: 2,
286        };
287        let surface = surface::ResizableSurface::new(surface, &device, config);
288
289        let (command_sender, command_receiver) = flume::unbounded();
290
291        // Some state can be set by commands to ensure valid initial state.
292        command_sender
293            .try_send(GameCommand::SetInputMode(InputMode::Unified))
294            .expect("unbounded queue held by this thread should send immediately");
295
296        let data = GameData {
297            command_sender,
298            surface_format,
299            limits: required_limits,
300            size,
301            window,
302            device,
303            queue,
304            exit_flag: ExitFlag::new(),
305        };
306        let game = T::init(&data, init)?;
307
308        // Gather inputs as a combination of registered user preferences and defaults.
309        let input_map = game.default_inputs();
310
311        Ok(Self {
312            data,
313            game,
314            surface,
315            command_receiver,
316            input_map,
317            input_mode: InputMode::Unified,
318            last_cursor_position: PhysicalPosition { x: 0.0, y: 0.0 },
319            mouse_sensitivity: 0.01,
320        })
321    }
322
323    pub(crate) fn run(init: T::InitData) {
324        let event_loop = EventLoop::new().expect("could not create game loop");
325
326        // Built on first `Event::Resumed`
327        // Taken out on `Event::LoopDestroyed`
328        let mut state: Option<Self> = None;
329        let (state_transmission, state_reception) = flume::bounded(1);
330        let mut init = Some((init, state_transmission));
331
332        event_loop
333            .run(move |event, window_target| {
334                if event == Event::LoopExiting {
335                    state.take().expect("loop is destroyed once").finished();
336                    return;
337                }
338
339                // Resume always emmitted to begin with - use it to begin an async method to create the game state.
340                if state.is_none() && event == Event::Resumed {
341                    if let Some((init, state_transmission)) = init.take() {
342                        async fn build_state<T: Game + 'static>(
343                            init: T::InitData,
344                            window: GameWindow,
345                            state_transmission: flume::Sender<GameState<T>>,
346                        ) {
347                            let state = GameState::<T>::new(init, window).await;
348                            let state = match state {
349                                Ok(state) => state,
350                                Err(err) => {
351                                    crate::alert_dialogue(&format!(
352                                        "Initialisation failure:\n{err}"
353                                    ));
354                                    panic!("{err}");
355                                }
356                            };
357                            state_transmission.try_send(state).unwrap();
358                        }
359
360                        let window = GameWindow::new::<T>(window_target);
361                        crate::block_on(build_state::<T>(init, window, state_transmission));
362                    }
363                }
364
365                // On any future events, check if the game state has been created and receive it.
366                let state = match state.as_mut() {
367                    None => {
368                        if let Ok(new_state) = state_reception.try_recv() {
369                            state = Some(new_state);
370                            state.as_mut().unwrap()
371                        } else {
372                            return;
373                        }
374                    }
375                    Some(state) => state,
376                };
377
378                state.receive_event(event, window_target);
379            })
380            .expect("run err");
381    }
382
383    fn is_input_event(event: &Event<()>) -> bool {
384        match event {
385            winit::event::Event::WindowEvent { event, .. } => match event {
386                WindowEvent::CursorMoved { .. }
387                | WindowEvent::CursorEntered { .. }
388                | WindowEvent::CursorLeft { .. }
389                | WindowEvent::MouseWheel { .. }
390                | WindowEvent::MouseInput { .. }
391                | WindowEvent::TouchpadPressure { .. }
392                | WindowEvent::AxisMotion { .. }
393                | WindowEvent::Touch(_)
394                | WindowEvent::KeyboardInput { .. }
395                | WindowEvent::ModifiersChanged(_)
396                | WindowEvent::Ime(_) => true,
397                _ => false,
398            },
399            winit::event::Event::DeviceEvent { event, .. } => match event {
400                DeviceEvent::MouseMotion { .. }
401                | DeviceEvent::MouseWheel { .. }
402                | DeviceEvent::Motion { .. }
403                | DeviceEvent::Button { .. }
404                | DeviceEvent::Key(_) => true,
405                _ => false,
406            },
407            _ => false,
408        }
409    }
410
411    fn receive_event(&mut self, mut event: Event<()>, window_target: &ActiveEventLoop) {
412        // Discard events that aren't for us
413        event = match event {
414            Event::WindowEvent { window_id, .. } if window_id != self.window().id() => return,
415            event => event,
416        };
417
418        // We filter all window events through the game to allow it to integrate with other libraries, such as egui.
419        // But only send keyboard and mouse input events to UI if the mouse isn't captured.
420        let should_send_input = self.input_mode.should_propogate_raw_input();
421        if should_send_input || !Self::is_input_event(&event) {
422            event = match self.game.process_raw_event(&self.data, event) {
423                None => return,
424                Some(event) => event,
425            };
426        }
427
428        self.process_event(event, window_target)
429    }
430
431    fn process_event(&mut self, event: Event<()>, window_target: &ActiveEventLoop) {
432        match event {
433            Event::WindowEvent { event, window_id } if window_id == self.window().id() => {
434                match event {
435                    WindowEvent::CloseRequested | WindowEvent::Destroyed => self.request_exit(),
436                    // (0, 0) means minimized on Windows.
437                    WindowEvent::Resized(winit::dpi::PhysicalSize {
438                        width: 0,
439                        height: 0,
440                    }) => {}
441                    WindowEvent::Resized(physical_size) => {
442                        log::debug!("Resized: {:?}", physical_size);
443                        self.resize(physical_size);
444                    }
445                    WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
446                        log::debug!("Scale Factor Changed: {:?}", scale_factor);
447                        //self.resize(*new_inner_size);
448                    }
449                    WindowEvent::KeyboardInput {
450                        device_id: _device_id,
451                        event,
452                        is_synthetic,
453                    } if !is_synthetic && !event.repeat => {
454                        if let PhysicalKey::Code(key) = event.physical_key {
455                            let activation = match event.state {
456                                winit::event::ElementState::Pressed => 1.0,
457                                winit::event::ElementState::Released => 0.0,
458                            };
459                            let activation = input::LinearInputActivation::try_from(activation)
460                                .expect("from const");
461                            self.linear_input(
462                                input::LinearInputType::KnownKeyboard(key.into()),
463                                activation,
464                            );
465                        } else {
466                            eprintln!("unknown key code, scan code: {:?}", event.physical_key)
467                        }
468                    }
469                    WindowEvent::CursorMoved {
470                        device_id: _device_id,
471                        position,
472                        ..
473                    } => {
474                        let delta_x = position.x - self.last_cursor_position.x;
475                        let delta_y = position.y - self.last_cursor_position.y;
476
477                        // Only trigger a single linear event, depending on the largest movement
478                        if delta_x.abs() > 2.0 || delta_y.abs() > 2.0 {
479                            self.process_linear_mouse_movement(delta_x, delta_y);
480                        }
481
482                        // Also trigger a vector input
483                        self.vector_input(
484                            VectorInputType::MouseMove,
485                            VectorInputActivation::clamp(
486                                delta_x as f32 * self.mouse_sensitivity,
487                                delta_y as f32 * self.mouse_sensitivity,
488                            ),
489                        );
490
491                        self.last_cursor_position = position.cast();
492
493                        // Winit doesn't support cursor locking on a lot of platforms, so do it manually.
494                        let should_lock_cursor = self.input_mode.should_lock_cursor();
495                        if should_lock_cursor {
496                            let mut center = self.data.window.inner_size();
497                            center.width /= 2;
498                            center.height /= 2;
499
500                            let old_pos = position.cast::<u32>();
501                            let new_pos = PhysicalPosition::new(center.width, center.height);
502
503                            if old_pos != new_pos {
504                                // Ignore result - if it doesn't work then there's not much we can do.
505                                let _ = self.data.window.set_cursor_position(new_pos);
506                            }
507
508                            self.last_cursor_position = new_pos.cast();
509                        }
510                    }
511                    WindowEvent::RedrawRequested => {
512                        let _ = self.data.device.poll(wgpu::PollType::Poll);
513
514                        self.pre_frame_update();
515
516                        // Check everything the game implementation can send to us
517                        if self.data.exit_flag.get() {
518                            window_target.exit();
519                        }
520
521                        let res = self.render();
522                        match res {
523                            Ok(_) => {}
524                            Err(wgpu::SurfaceError::Lost) => self.resize(self.data.size),
525                            Err(wgpu::SurfaceError::OutOfMemory) => {
526                                window_target.exit();
527                            }
528                            // All other errors (Outdated, Timeout) should be resolved by the next frame
529                            Err(e) => eprintln!("{:?}", e),
530                        }
531                    }
532                    _ => {}
533                }
534            }
535            Event::DeviceEvent { device_id, event } => {
536                log::debug!("device event: {device_id:?}::{event:?}");
537            }
538            Event::AboutToWait => {
539                self.window().request_redraw();
540            }
541            _ => {}
542        }
543    }
544
545    pub fn window(&self) -> &Window {
546        &self.data.window
547    }
548
549    fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
550        if new_size.width > 0 && new_size.height > 0 {
551            self.data.size = new_size;
552
553            self.surface.resize(new_size, &self.data.queue);
554
555            self.game.window_resize(&self.data, new_size)
556        }
557    }
558
559    fn process_linear_mouse_movement(&mut self, delta_x: f64, delta_y: f64) {
560        if delta_x.abs() > delta_y.abs() {
561            if delta_x > 0.0 {
562                self.linear_input(
563                    input::LinearInputType::Mouse(MouseInputType::MoveRight),
564                    input::LinearInputActivation::clamp(delta_x as f32 * self.mouse_sensitivity),
565                );
566            } else {
567                self.linear_input(
568                    input::LinearInputType::Mouse(MouseInputType::MoveLeft),
569                    input::LinearInputActivation::clamp(-delta_x as f32 * self.mouse_sensitivity),
570                );
571            }
572        } else {
573            if delta_y > 0.0 {
574                self.linear_input(
575                    input::LinearInputType::Mouse(MouseInputType::MoveUp),
576                    input::LinearInputActivation::clamp(delta_y as f32 * self.mouse_sensitivity),
577                );
578            } else {
579                self.linear_input(
580                    input::LinearInputType::Mouse(MouseInputType::MoveDown),
581                    input::LinearInputActivation::clamp(-delta_y as f32 * self.mouse_sensitivity),
582                );
583            }
584        }
585    }
586
587    fn linear_input(
588        &mut self,
589        inputted: input::LinearInputType,
590        activation: input::LinearInputActivation,
591    ) {
592        if !self.input_mode.should_handle_input() {
593            return;
594        }
595        let input_value = self.input_map.get_linear(inputted);
596        if let Some(input_value) = input_value {
597            self.game
598                .handle_linear_input(&self.data, input_value, activation)
599        }
600    }
601
602    fn vector_input(
603        &mut self,
604        inputted: input::VectorInputType,
605        activation: input::VectorInputActivation,
606    ) {
607        if !self.input_mode.should_handle_input() {
608            return;
609        }
610        let input_value = self.input_map.get_vector(inputted);
611        if let Some(input_value) = input_value {
612            self.game
613                .handle_vector_input(&self.data, input_value, activation)
614        }
615    }
616
617    fn request_exit(&mut self) {
618        self.data.exit_flag.set();
619        self.game.user_exit_requested(&self.data);
620    }
621
622    fn pre_frame_update(&mut self) {
623        // Get all the things the game wants to do before the next frame
624        while let Ok(cmd) = self.command_receiver.try_recv() {
625            match cmd {
626                GameCommand::Exit => self.data.exit_flag.set(),
627                GameCommand::SetInputMode(input_mode) => {
628                    self.input_mode = input_mode;
629
630                    let should_show_cursor = !input_mode.should_hide_cursor();
631                    self.data.window.set_cursor_visible(should_show_cursor);
632                }
633                GameCommand::SetMouseSensitivity(new_sensitivity) => {
634                    self.mouse_sensitivity = new_sensitivity;
635                }
636            }
637        }
638    }
639
640    fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
641        // If we are in the process of resizing, don't do anything
642        if let Some(surface) = self.surface.get(&self.data.device) {
643            let was_suboptimal = {
644                let output = surface.get_current_texture()?;
645                let view = output
646                    .texture
647                    .create_view(&wgpu::TextureViewDescriptor::default());
648
649                self.game.render_to(&self.data, view);
650
651                let was_suboptimal = output.suboptimal;
652
653                output.present();
654
655                was_suboptimal
656            };
657
658            if was_suboptimal {
659                // Force recreation
660                return Err(wgpu::SurfaceError::Lost);
661            }
662        }
663        Ok(())
664    }
665
666    fn finished(self) {
667        self.game.finished(self.data)
668    }
669}