pub(crate) mod input;
mod surface;
pub(crate) mod window;
use std::sync::{atomic::AtomicBool, Arc};
use log::info;
use winit::{
dpi::PhysicalPosition,
event::{DeviceEvent, Event, WindowEvent},
event_loop::{ActiveEventLoop, EventLoop},
keyboard::PhysicalKey,
window::Window,
};
use crate::{game::window::GameWindow, LfLimitsExt};
use self::input::{InputMap, MouseInputType, VectorInputActivation, VectorInputType};
#[derive(Clone)]
pub struct ExitFlag {
inner: Arc<AtomicBool>,
}
impl ExitFlag {
fn new() -> Self {
Self {
inner: Arc::new(AtomicBool::new(false)),
}
}
pub fn get(&self) -> bool {
self.inner.load(std::sync::atomic::Ordering::SeqCst)
}
fn set(&self) {
self.inner.store(true, std::sync::atomic::Ordering::SeqCst)
}
}
#[derive(Debug, Clone, Copy)]
pub enum InputMode {
Exclusive,
UI,
Unified,
}
impl InputMode {
fn should_hide_cursor(self) -> bool {
match self {
InputMode::Exclusive => true,
InputMode::UI => false,
InputMode::Unified => false,
}
}
fn should_handle_input(self) -> bool {
match self {
InputMode::Exclusive => true,
InputMode::UI => false,
InputMode::Unified => true,
}
}
fn should_propogate_raw_input(self) -> bool {
match self {
InputMode::Exclusive => false,
InputMode::UI => true,
InputMode::Unified => true,
}
}
fn should_lock_cursor(self) -> bool {
match self {
InputMode::Exclusive => true,
InputMode::UI => false,
InputMode::Unified => false,
}
}
}
pub enum GameCommand {
Exit,
SetInputMode(InputMode),
SetMouseSensitivity(f32),
}
pub struct GameData {
pub command_sender: flume::Sender<GameCommand>,
pub surface_format: wgpu::TextureFormat,
pub limits: wgpu::Limits,
pub size: winit::dpi::PhysicalSize<u32>,
pub window: GameWindow,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub exit_flag: ExitFlag,
}
pub trait Game: Sized {
type InitData;
type LinearInputType;
type VectorInputType;
fn title() -> impl Into<String>;
fn target_limits() -> wgpu::Limits {
wgpu::Limits::downlevel_webgl2_defaults()
}
fn default_inputs(&self) -> InputMap<Self::LinearInputType, Self::VectorInputType>;
fn init(data: &GameData, init: Self::InitData) -> anyhow::Result<Self>;
fn process_raw_event<'a, T>(&mut self, _: &GameData, event: Event<T>) -> Option<Event<T>> {
Some(event)
}
fn window_resize(&mut self, data: &GameData, new_size: winit::dpi::PhysicalSize<u32>);
fn handle_linear_input(
&mut self,
data: &GameData,
input: &Self::LinearInputType,
activation: input::LinearInputActivation,
);
fn handle_vector_input(
&mut self,
data: &GameData,
input: &Self::VectorInputType,
activation: input::VectorInputActivation,
);
fn render_to(&mut self, data: &GameData, view: wgpu::TextureView);
fn user_exit_requested(&mut self, data: &GameData) {
let _ = data.command_sender.send(GameCommand::Exit);
}
fn finished(self, _: GameData) {}
}
pub(crate) struct GameState<T: Game> {
data: GameData,
game: T,
input_map: input::InputMap<T::LinearInputType, T::VectorInputType>,
command_receiver: flume::Receiver<GameCommand>,
surface: surface::ResizableSurface<'static>,
input_mode: InputMode,
last_cursor_position: PhysicalPosition<f64>,
mouse_sensitivity: f32,
}
impl<T: Game + 'static> GameState<T> {
async fn new(init: T::InitData, window: GameWindow) -> anyhow::Result<Self> {
let size = (&window).inner_size();
#[cfg(debug_assertions)]
let flags = wgpu::InstanceFlags::DEBUG | wgpu::InstanceFlags::VALIDATION;
#[cfg(not(debug_assertions))]
let flags = wgpu::InstanceFlags::DISCARD_HAL_LABELS;
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::from_env().unwrap_or_default(),
flags,
backend_options: wgpu::BackendOptions {
gl: wgpu::GlBackendOptions {
gles_minor_version: wgpu::Gles3MinorVersion::Automatic,
fence_behavior: wgpu::GlFenceBehavior::Normal,
},
dx12: wgpu::Dx12BackendOptions::from_env_or_default(),
noop: wgpu::NoopBackendOptions { enable: true },
},
memory_budget_thresholds: wgpu::MemoryBudgetThresholds {
for_resource_creation: None,
for_device_loss: None,
},
});
let surface = window.create_surface(&instance)?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: Some(&surface),
})
.await?;
let available_limits = if cfg!(target_arch = "wasm32") {
wgpu::Limits::downlevel_webgl2_defaults()
} else {
adapter.limits()
};
let target_limits = T::target_limits();
let required_limits = available_limits.intersection(&target_limits);
let mut required_features = wgpu::Features::empty();
if adapter
.features()
.contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS)
&& matches!(
adapter.get_info().device_type,
wgpu::DeviceType::IntegratedGpu
| wgpu::DeviceType::Cpu
| wgpu::DeviceType::VirtualGpu
)
{
required_features |= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
}
required_features |= adapter.features().intersection(
wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES,
);
info!("info: {:#?}", adapter.get_info());
info!("limits: {:#?}", adapter.limits());
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features,
required_limits: required_limits.clone(),
label: None,
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
trace: wgpu::Trace::Off,
})
.await?;
let mut surface_config = surface
.get_default_config(&adapter, size.width, size.height)
.ok_or(anyhow::Error::msg("failed to get surface configuration"))?;
surface_config.present_mode = wgpu::PresentMode::AutoVsync;
surface.configure(&device, &surface_config);
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.filter(|f| f.is_srgb())
.next()
.unwrap_or(surface_caps.formats[0]);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width,
height: size.height,
present_mode: surface_caps.present_modes[0],
alpha_mode: surface_caps.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
let surface = surface::ResizableSurface::new(surface, &device, config);
let (command_sender, command_receiver) = flume::unbounded();
command_sender
.try_send(GameCommand::SetInputMode(InputMode::Unified))
.expect("unbounded queue held by this thread should send immediately");
let data = GameData {
command_sender,
surface_format,
limits: required_limits,
size,
window,
device,
queue,
exit_flag: ExitFlag::new(),
};
let game = T::init(&data, init)?;
let input_map = game.default_inputs();
Ok(Self {
data,
game,
surface,
command_receiver,
input_map,
input_mode: InputMode::Unified,
last_cursor_position: PhysicalPosition { x: 0.0, y: 0.0 },
mouse_sensitivity: 0.01,
})
}
pub(crate) fn run(init: T::InitData) {
let event_loop = EventLoop::new().expect("could not create game loop");
let mut state: Option<Self> = None;
let (state_transmission, state_reception) = flume::bounded(1);
let mut init = Some((init, state_transmission));
event_loop
.run(move |event, window_target| {
if event == Event::LoopExiting {
state.take().expect("loop is destroyed once").finished();
return;
}
if state.is_none() && event == Event::Resumed {
if let Some((init, state_transmission)) = init.take() {
async fn build_state<T: Game + 'static>(
init: T::InitData,
window: GameWindow,
state_transmission: flume::Sender<GameState<T>>,
) {
let state = GameState::<T>::new(init, window).await;
let state = match state {
Ok(state) => state,
Err(err) => {
crate::alert_dialogue(&format!(
"Initialisation failure:\n{err}"
));
panic!("{err}");
}
};
state_transmission.try_send(state).unwrap();
}
let window = GameWindow::new::<T>(window_target);
crate::block_on(build_state::<T>(init, window, state_transmission));
}
}
let state = match state.as_mut() {
None => {
if let Ok(new_state) = state_reception.try_recv() {
state = Some(new_state);
state.as_mut().unwrap()
} else {
return;
}
}
Some(state) => state,
};
state.receive_event(event, window_target);
})
.expect("run err");
}
fn is_input_event(event: &Event<()>) -> bool {
match event {
winit::event::Event::WindowEvent { event, .. } => match event {
WindowEvent::CursorMoved { .. }
| WindowEvent::CursorEntered { .. }
| WindowEvent::CursorLeft { .. }
| WindowEvent::MouseWheel { .. }
| WindowEvent::MouseInput { .. }
| WindowEvent::TouchpadPressure { .. }
| WindowEvent::AxisMotion { .. }
| WindowEvent::Touch(_)
| WindowEvent::KeyboardInput { .. }
| WindowEvent::ModifiersChanged(_)
| WindowEvent::Ime(_) => true,
_ => false,
},
winit::event::Event::DeviceEvent { event, .. } => match event {
DeviceEvent::MouseMotion { .. }
| DeviceEvent::MouseWheel { .. }
| DeviceEvent::Motion { .. }
| DeviceEvent::Button { .. }
| DeviceEvent::Key(_) => true,
_ => false,
},
_ => false,
}
}
fn receive_event(&mut self, mut event: Event<()>, window_target: &ActiveEventLoop) {
event = match event {
Event::WindowEvent { window_id, .. } if window_id != self.window().id() => return,
event => event,
};
let should_send_input = self.input_mode.should_propogate_raw_input();
if should_send_input || !Self::is_input_event(&event) {
event = match self.game.process_raw_event(&self.data, event) {
None => return,
Some(event) => event,
};
}
self.process_event(event, window_target)
}
fn process_event(&mut self, event: Event<()>, window_target: &ActiveEventLoop) {
match event {
Event::WindowEvent { event, window_id } if window_id == self.window().id() => {
match event {
WindowEvent::CloseRequested | WindowEvent::Destroyed => self.request_exit(),
WindowEvent::Resized(winit::dpi::PhysicalSize {
width: 0,
height: 0,
}) => {}
WindowEvent::Resized(physical_size) => {
log::debug!("Resized: {:?}", physical_size);
self.resize(physical_size);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
log::debug!("Scale Factor Changed: {:?}", scale_factor);
}
WindowEvent::KeyboardInput {
device_id: _device_id,
event,
is_synthetic,
} if !is_synthetic && !event.repeat => {
if let PhysicalKey::Code(key) = event.physical_key {
let activation = match event.state {
winit::event::ElementState::Pressed => 1.0,
winit::event::ElementState::Released => 0.0,
};
let activation = input::LinearInputActivation::try_from(activation)
.expect("from const");
self.linear_input(
input::LinearInputType::KnownKeyboard(key.into()),
activation,
);
} else {
eprintln!("unknown key code, scan code: {:?}", event.physical_key)
}
}
WindowEvent::CursorMoved {
device_id: _device_id,
position,
..
} => {
let delta_x = position.x - self.last_cursor_position.x;
let delta_y = position.y - self.last_cursor_position.y;
if delta_x.abs() > 2.0 || delta_y.abs() > 2.0 {
self.process_linear_mouse_movement(delta_x, delta_y);
}
self.vector_input(
VectorInputType::MouseMove,
VectorInputActivation::clamp(
delta_x as f32 * self.mouse_sensitivity,
delta_y as f32 * self.mouse_sensitivity,
),
);
self.last_cursor_position = position.cast();
let should_lock_cursor = self.input_mode.should_lock_cursor();
if should_lock_cursor {
let mut center = self.data.window.inner_size();
center.width /= 2;
center.height /= 2;
let old_pos = position.cast::<u32>();
let new_pos = PhysicalPosition::new(center.width, center.height);
if old_pos != new_pos {
let _ = self.data.window.set_cursor_position(new_pos);
}
self.last_cursor_position = new_pos.cast();
}
}
WindowEvent::RedrawRequested => {
let _ = self.data.device.poll(wgpu::PollType::Poll);
self.pre_frame_update();
if self.data.exit_flag.get() {
window_target.exit();
}
let res = self.render();
match res {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => self.resize(self.data.size),
Err(wgpu::SurfaceError::OutOfMemory) => {
window_target.exit();
}
Err(e) => eprintln!("{:?}", e),
}
}
_ => {}
}
}
Event::DeviceEvent { device_id, event } => {
log::debug!("device event: {device_id:?}::{event:?}");
}
Event::AboutToWait => {
self.window().request_redraw();
}
_ => {}
}
}
pub fn window(&self) -> &Window {
&self.data.window
}
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.data.size = new_size;
self.surface.resize(new_size, &self.data.queue);
self.game.window_resize(&self.data, new_size)
}
}
fn process_linear_mouse_movement(&mut self, delta_x: f64, delta_y: f64) {
if delta_x.abs() > delta_y.abs() {
if delta_x > 0.0 {
self.linear_input(
input::LinearInputType::Mouse(MouseInputType::MoveRight),
input::LinearInputActivation::clamp(delta_x as f32 * self.mouse_sensitivity),
);
} else {
self.linear_input(
input::LinearInputType::Mouse(MouseInputType::MoveLeft),
input::LinearInputActivation::clamp(-delta_x as f32 * self.mouse_sensitivity),
);
}
} else {
if delta_y > 0.0 {
self.linear_input(
input::LinearInputType::Mouse(MouseInputType::MoveUp),
input::LinearInputActivation::clamp(delta_y as f32 * self.mouse_sensitivity),
);
} else {
self.linear_input(
input::LinearInputType::Mouse(MouseInputType::MoveDown),
input::LinearInputActivation::clamp(-delta_y as f32 * self.mouse_sensitivity),
);
}
}
}
fn linear_input(
&mut self,
inputted: input::LinearInputType,
activation: input::LinearInputActivation,
) {
if !self.input_mode.should_handle_input() {
return;
}
let input_value = self.input_map.get_linear(inputted);
if let Some(input_value) = input_value {
self.game
.handle_linear_input(&self.data, input_value, activation)
}
}
fn vector_input(
&mut self,
inputted: input::VectorInputType,
activation: input::VectorInputActivation,
) {
if !self.input_mode.should_handle_input() {
return;
}
let input_value = self.input_map.get_vector(inputted);
if let Some(input_value) = input_value {
self.game
.handle_vector_input(&self.data, input_value, activation)
}
}
fn request_exit(&mut self) {
self.data.exit_flag.set();
self.game.user_exit_requested(&self.data);
}
fn pre_frame_update(&mut self) {
while let Ok(cmd) = self.command_receiver.try_recv() {
match cmd {
GameCommand::Exit => self.data.exit_flag.set(),
GameCommand::SetInputMode(input_mode) => {
self.input_mode = input_mode;
let should_show_cursor = !input_mode.should_hide_cursor();
self.data.window.set_cursor_visible(should_show_cursor);
}
GameCommand::SetMouseSensitivity(new_sensitivity) => {
self.mouse_sensitivity = new_sensitivity;
}
}
}
}
fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
if let Some(surface) = self.surface.get(&self.data.device) {
let was_suboptimal = {
let output = surface.get_current_texture()?;
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
self.game.render_to(&self.data, view);
let was_suboptimal = output.suboptimal;
output.present();
was_suboptimal
};
if was_suboptimal {
return Err(wgpu::SurfaceError::Lost);
}
}
Ok(())
}
fn finished(self) {
self.game.finished(self.data)
}
}