use anyhow::Context;
use anyhow::Result;
use glam::{UVec2, Vec2};
use std::{cell::RefCell, rc::Rc, sync::Arc};
use wgpu::PresentMode;
use winit::application::ApplicationHandler;
use winit::dpi::PhysicalPosition;
use winit::dpi::{LogicalSize, PhysicalSize};
use winit::event::MouseButton;
use winit::event::MouseScrollDelta;
use winit::keyboard::ModifiersState;
use winit::keyboard::PhysicalKey;
#[cfg(target_arch = "wasm32")]
use winit::platform::web::EventLoopExtWebSys;
use winit::window::{Fullscreen, WindowId};
use winit::{
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::WindowAttributes,
};
use crate::commands::*;
use crate::input::KeyCode;
use crate::input::KeyState;
use crate::platform::window::setup_window;
use crate::render::BackendState;
use crate::{app::App, engine::Engine, tasks};
use super::clock::InstantClock;
use super::key_code::to_key_code;
use crate::commands::ResourceCommand;
#[cfg(feature = "egui")]
use crate::egui::system::EguiSystem;
use crate::renderer::driver::RenderDriver;
pub struct WGPUPlatform {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
window_size: UVec2,
window: Arc<winit::window::Window>,
driver: RenderDriver,
surface_view_format: wgpu::TextureFormat,
#[cfg(feature = "egui")]
egui: Option<EguiSystem>,
}
impl WGPUPlatform {
async fn new(
window: Arc<winit::window::Window>,
present_mode: PresentMode,
desired: UVec2,
) -> Result<Self> {
let mut inner_size = window.inner_size();
if inner_size.width == 0 || inner_size.height == 0 {
inner_size.width = inner_size.width.max(desired.x);
inner_size.height = inner_size.height.max(desired.y);
let _ = window.request_inner_size(inner_size);
}
let window_size = UVec2::new(inner_size.width, inner_size.height);
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default().with_env());
let surface = instance.create_surface(window.clone())?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.context("Failed to find an appropriate adapter")?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
label: None,
memory_hints: Default::default(),
trace: wgpu::Trace::Off,
experimental_features: Default::default(),
})
.await?;
let capabilities = surface.get_capabilities(&adapter);
let surface_format = capabilities
.formats
.iter()
.copied()
.find(|fmt| fmt.is_srgb())
.unwrap_or(capabilities.formats[0]);
let mut surface_view_format = surface_format;
if !surface_view_format.is_srgb() {
let srgb_candidate = surface_format.add_srgb_suffix();
if srgb_candidate != surface_format {
let features = adapter.get_texture_format_features(srgb_candidate);
let supports_attachment = features
.allowed_usages
.contains(wgpu::TextureUsages::RENDER_ATTACHMENT);
let supports_sampling = features
.allowed_usages
.contains(wgpu::TextureUsages::TEXTURE_BINDING);
if supports_attachment && supports_sampling {
surface_view_format = srgb_candidate;
}
}
}
let mut view_formats = Vec::new();
if surface_view_format != surface_format {
view_formats.push(surface_view_format);
}
let desired_maximum_frame_latency = if present_mode == PresentMode::AutoNoVsync {
1
} else {
2
};
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: window_size.x,
height: window_size.y,
present_mode,
desired_maximum_frame_latency,
alpha_mode: capabilities.alpha_modes[0],
view_formats,
};
surface.configure(&device, &config);
let backend_state = BackendState {
device: device.clone(),
queue: queue.clone(),
surface_config: config.clone(),
surface_view_format,
};
let driver = RenderDriver::new(&backend_state);
#[cfg(feature = "egui")]
let egui = Some(EguiSystem::new(&window, &backend_state));
Ok(Self {
window,
surface,
device,
queue,
config,
window_size,
driver,
surface_view_format,
#[cfg(feature = "egui")]
egui,
})
}
fn is_window_visible(&self) -> bool {
self.window.as_ref().is_visible().unwrap_or_default()
}
fn resize(&mut self, size: PhysicalSize<u32>) {
self.window_size = UVec2::new(size.width, size.height);
self.config.width = size.width;
self.config.height = size.height;
self.surface.configure(&self.device, &self.config);
self.driver.resize();
}
fn handle_resource_cmds(&mut self, cmds: Vec<ResourceCommand>) {
self.driver.apply_resource_cmds(cmds);
}
fn render_frame(
&mut self,
g: &Engine,
draw_commands: Vec<Command>,
draw3d: Vec<crate::renderer::shader3d::draw3d::Draw3D>,
) {
if self.config.width == 0 || self.config.height == 0 {
return;
}
let frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(wgpu::SurfaceError::Outdated) => {
self.surface.configure(&self.device, &self.config);
match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(e) => {
log::error!(
"Failed to acquire next swap chain texture after reconfiguration: {e:?}"
);
return;
}
}
}
Err(e) => {
log::error!("Failed to acquire next swap chain texture: {e:?}");
return;
}
};
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
self.driver
.record_frame(g, draw_commands, draw3d, &mut encoder, &frame);
#[cfg(feature = "egui")]
if let Some(egui) = self.egui.as_mut() {
let frame_view = frame.texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("Swapchain sRGB View (egui)"),
format: Some(self.surface_view_format),
..Default::default()
});
egui.paint(
&self.window,
&self.device,
&self.queue,
&mut encoder,
&frame_view,
self.window_size,
);
}
self.queue.submit(Some(encoder.finish()));
#[cfg(feature = "egui")]
if let Some(egui) = self.egui.as_mut() {
egui.after_submit();
}
self.window.pre_present_notify();
frame.present();
self.window.request_redraw();
}
pub fn run(app: App) -> Result<()>
where
Self: Sized,
{
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let app = AppContext {
config: app,
engine: None,
state: None,
modifiers: None,
pending_state: None,
};
#[cfg(target_arch = "wasm32")]
{
event_loop.spawn_app(app);
}
#[cfg(not(target_arch = "wasm32"))]
{
let mut app = app;
event_loop.run_app(&mut app).unwrap();
}
Ok(())
}
}
struct AppContext {
config: App,
engine: Option<Engine>,
state: Option<WGPUPlatform>,
modifiers: Option<ModifiersState>,
pending_state: Option<Rc<RefCell<Option<Result<WGPUPlatform>>>>>,
}
impl AppContext {
fn install_state(&mut self, state: WGPUPlatform) {
self.state = Some(state);
let desired = self.config.window;
let window_size = if let Some(state) = self.state.as_ref() {
let _ = state
.window
.request_inner_size(LogicalSize::new(desired.x, desired.y));
state.window.inner_size()
} else {
PhysicalSize::new(desired.x, desired.y)
};
if self.engine.is_none() {
let state = self.state.as_ref().expect("state set before engine init");
let backend_state = BackendState {
device: state.device.clone(),
queue: state.queue.clone(),
surface_config: state.config.clone(),
surface_view_format: state.surface_view_format,
};
let mut engine = Engine::new(InstantClock::default(), backend_state);
if let Some(scale_mode) = self.config.scale_mode.take() {
engine.set_scale_mode(scale_mode);
}
if let Some(scene) = self.config.scene.take() {
engine.set_scene_boxed(scene);
}
if let Some(setup) = self.config.setup.take() {
engine.init(setup);
}
self.engine = Some(engine);
}
self.resize(window_size);
if let Some(state) = self.state.as_ref() {
state.window.request_redraw();
}
}
fn poll_pending_state(&mut self) {
let result = self.pending_state.as_ref().and_then(|pending| {
let mut pending_ref = pending.borrow_mut();
pending_ref.take()
});
if let Some(result) = result {
self.pending_state = None;
match result {
Ok(state) => self.install_state(state),
Err(err) => log::error!("Failed to initialize WGPU platform: {err:?}"),
}
}
}
fn resize(&mut self, size: PhysicalSize<u32>) {
let Some(state) = self.state.as_mut() else {
return;
};
if size.width > 0 && size.height > 0 {
let scale_factor = state.window.scale_factor();
state.resize(size);
let Some(engine) = self.engine.as_mut() else {
return;
};
engine.on_resize(UVec2::new(size.width, size.height), scale_factor);
}
}
fn update(&mut self, event_loop: &ActiveEventLoop, render: bool) {
self.poll_pending_state();
if self.state.is_none() || self.engine.is_none() {
return;
}
let (draw_commands, res_commands, draw3d, exit_requested) = {
let engine = self.engine.as_mut().unwrap();
#[cfg(feature = "egui")]
{
let state = self.state.as_mut().unwrap();
if let Some(egui) = state.egui.as_mut() {
let ctx = egui.begin_frame(&state.window).clone();
engine.update();
engine.run_egui_layers(&ctx);
egui.end_frame(&state.window);
}
}
#[cfg(not(feature = "egui"))]
engine.update();
(
engine.take_draw_commands(),
engine.take_resource_commands(),
engine.take_3d_draws(),
engine.exit,
)
};
if exit_requested && !event_loop.exiting() {
event_loop.exit();
return;
}
let state = self.state.as_mut().unwrap();
if render {
state.handle_resource_cmds(res_commands);
let engine = self.engine.as_ref().unwrap();
state.render_frame(engine, draw_commands, draw3d);
} else {
state.handle_resource_cmds(res_commands);
}
}
}
impl ApplicationHandler for AppContext {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let App {
title,
window,
vsync,
resizable,
fullscreen,
cursor_visible,
..
} = &self.config;
let mut window_attrs = WindowAttributes::default()
.with_title(title.to_owned())
.with_resizable(*resizable)
.with_inner_size(LogicalSize::new(window.x, window.y));
if *fullscreen {
window_attrs = window_attrs.with_fullscreen(Some(Fullscreen::Borderless(None)));
}
let window = Arc::new(event_loop.create_window(window_attrs).unwrap());
window.set_cursor_visible(*cursor_visible);
setup_window(&window, self.config.window);
let present_mode = if *vsync {
PresentMode::AutoVsync
} else {
PresentMode::AutoNoVsync
};
let pending = Rc::new(RefCell::new(None));
self.pending_state = Some(pending.clone());
let window_clone = window.clone();
let desired = self.config.window;
tasks::spawn_local({
let pending = pending.clone();
async move {
let result = WGPUPlatform::new(window_clone.clone(), present_mode, desired).await;
*pending.borrow_mut() = Some(result);
window_clone.request_redraw();
}
});
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
self.poll_pending_state();
if self.state.is_none() || self.engine.is_none() {
return;
}
let is_visible = self.state.as_ref().unwrap().is_window_visible();
if !self.config.vsync && !is_visible {
self.update(event_loop, false);
}
}
fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
self.poll_pending_state();
#[cfg(feature = "egui")]
if let Some(state) = self.state.as_mut()
&& let Some(egui) = state.egui.as_mut()
{
let response = egui.on_window_event(&state.window, &event);
if response.consumed {
return;
}
}
match event {
WindowEvent::CloseRequested => {
log::debug!("The close button was pressed; stopping");
event_loop.exit();
}
WindowEvent::RedrawRequested => {
self.update(event_loop, true);
}
WindowEvent::Resized(size) => {
self.resize(size);
}
WindowEvent::KeyboardInput { event, .. } => {
let Some(engine) = self.engine.as_mut() else {
return;
};
let key_state = if event.state.is_pressed() {
KeyState::down()
} else {
KeyState::up()
};
let PhysicalKey::Code(code) = event.physical_key else {
return;
};
if self.modifiers.is_some_and(|m| m.alt_key())
&& key_state.is_down()
&& code == winit::keyboard::KeyCode::Enter
{
if let Some(state) = self.state.as_mut() {
match state.window.fullscreen() {
Some(_) => {
state.window.set_fullscreen(None);
}
None => {
state
.window
.set_fullscreen(Some(Fullscreen::Borderless(None)));
}
}
}
return;
}
let code = to_key_code(code);
if code != KeyCode::Invalid {
engine.input.set_input_state(code, key_state);
}
}
WindowEvent::ModifiersChanged(m) => {
self.modifiers.replace(m.state());
}
WindowEvent::MouseInput { state, button, .. } => {
let Some(engine) = self.engine.as_mut() else {
return;
};
let code = match button {
MouseButton::Left => KeyCode::MouseLeft,
MouseButton::Middle => KeyCode::MouseMiddle,
MouseButton::Right => KeyCode::MouseRight,
_ => return,
};
let state = if state.is_pressed() {
KeyState::down()
} else {
KeyState::up()
};
engine.input.set_input_state(code, state);
}
WindowEvent::MouseWheel { delta, .. } => {
let Some(engine) = self.engine.as_mut() else {
return;
};
let scroll_delta = match delta {
MouseScrollDelta::LineDelta(x, y) => {
crate::input::MouseScrollDelta::LineDelta(Vec2::new(x, y))
}
MouseScrollDelta::PixelDelta(pos) => {
crate::input::MouseScrollDelta::PixelDelta(Vec2::new(
pos.x as f32,
pos.y as f32,
))
}
};
engine.input.add_mouse_scroll_delta(scroll_delta);
}
WindowEvent::CursorMoved {
position: PhysicalPosition { x, y },
..
} => {
let Some(engine) = self.engine.as_mut() else {
return;
};
let mut mp = Vec2::new(x as f32, y as f32) * engine.render.inv_screen_scale;
let logical_size = engine.render.logical_size();
mp.y = logical_size.y - mp.y;
mp -= logical_size * 0.5;
engine.input.set_mouse_pos(mp);
}
_ => (),
}
}
}