1pub(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#[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 Exclusive,
50 UI,
53 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
88pub 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
106pub trait Game: Sized {
110 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 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 fn render_to(&mut self, data: &GameData, view: wgpu::TextureView);
151
152 fn user_exit_requested(&mut self, data: &GameData) {
156 let _ = data.command_sender.send(GameCommand::Exit);
157 }
158
159 fn finished(self, _: GameData) {}
161}
162
163pub(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 input_mode: InputMode,
175 last_cursor_position: PhysicalPosition<f64>,
177 mouse_sensitivity: f32,
179}
180
181impl<T: Game + 'static> GameState<T> {
182 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 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 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 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 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 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 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 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 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 event = match event {
414 Event::WindowEvent { window_id, .. } if window_id != self.window().id() => return,
415 event => event,
416 };
417
418 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 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 }
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 if delta_x.abs() > 2.0 || delta_y.abs() > 2.0 {
479 self.process_linear_mouse_movement(delta_x, delta_y);
480 }
481
482 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 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 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 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 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 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 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 return Err(wgpu::SurfaceError::Lost);
661 }
662 }
663 Ok(())
664 }
665
666 fn finished(self) {
667 self.game.finished(self.data)
668 }
669}