use anyhow::Context;
use anyhow::Result;
use glam::UVec2;
use glam::Vec2;
use image::{DynamicImage, GenericImageView};
use std::{collections::HashMap, sync::Arc};
use wgpu::Extent3d;
use wgpu::PresentMode;
use wgpu::StoreOp;
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;
use winit::window::{Fullscreen, WindowId};
use winit::{
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::WindowAttributes,
};
use crate::input::KeyCode;
use crate::input::KeyState;
use crate::render::BackendState;
use crate::render::{CreateTextureCommand, RemoveTextureCommand};
use crate::{app::App, engine::Engine, handle::HandleId};
use super::clock::InstantClock;
use super::key_code::to_key_code;
use super::sprite_render::SpriteRender;
use super::types::TextureResource;
pub struct WGPUPlatform {
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration,
textures: HashMap<HandleId, TextureResource>,
window_size: UVec2,
sprite_renderer: SpriteRender,
ui_renderer: SpriteRender,
window: Arc<winit::window::Window>,
}
impl WGPUPlatform {
async fn new(window: Arc<winit::window::Window>, present_mode: PresentMode) -> Result<Self> {
let window_size = UVec2::new(window.inner_size().width, window.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,
})
.await?;
let cap = surface.get_capabilities(&adapter);
let surface_format = cap
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.unwrap_or(cap.formats[0]);
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: 2,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![surface_format],
};
surface.configure(&device, &config);
let sprite_renderer = SpriteRender::setup(device.clone(), queue.clone(), config.clone());
let ui_renderer = SpriteRender::setup(device.clone(), queue.clone(), config.clone());
Ok(Self {
window,
surface,
device,
queue,
config,
textures: HashMap::new(),
window_size,
sprite_renderer,
ui_renderer,
})
}
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);
}
fn create_texture_inner(&mut self, data: DynamicImage) -> TextureResource {
let dimentions = data.dimensions();
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Texture"),
size: wgpu::Extent3d {
width: dimentions.0,
height: dimentions.1,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
&data.to_rgba8(),
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * dimentions.0),
rows_per_image: Some(dimentions.1),
},
texture.size(),
);
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Texture Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
TextureResource {
texture,
view,
sampler,
}
}
fn get_render_target(&self, g: &Engine, frame: &wgpu::SurfaceTexture) -> wgpu::Texture {
let mut size = Extent3d::default();
let dpi_scale_factor = g.render.dpi_scale_factor;
let logical_size = (g.render.logical_size() * dpi_scale_factor as f32).as_uvec2();
size.width = logical_size.x;
size.height = logical_size.y;
let format = frame.texture.format();
let dimension = frame.texture.dimension();
self.device.create_texture(&wgpu::TextureDescriptor {
label: Some("Render Texture"),
size,
mip_level_count: 1,
sample_count: 1,
dimension,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
})
}
fn handle_draw_cmds(&mut self, cmds: Vec<crate::render::Command>, render: bool) {
use crate::render::*;
for cmd in cmds {
match cmd {
Command::Draw(cmd) if render => {
let texture = match self.textures.get(&cmd.handle.id()) {
Some(tex) => tex,
None => {
log::warn!("No texture");
continue;
}
};
self.sprite_renderer.draw(cmd, texture.to_owned());
}
Command::DrawUi(cmd) if render => {
let texture = match self.textures.get(&cmd.handle.id()) {
Some(tex) => tex,
None => {
log::warn!("No texture");
continue;
}
};
self.ui_renderer.draw(cmd, texture.to_owned());
}
Command::Draw(..) | Command::DrawUi(..) => {
}
Command::CreateTexture(cmd) => {
self.create_texture(cmd);
}
Command::RemoveTexture(cmd) => {
self.remove_texture(cmd);
}
}
}
}
fn render_frame(&mut self, g: &Engine, draw_commands: Vec<crate::render::Command>) {
let frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(e) => {
log::error!("Failed to acquire next swap chain texture: {e:?}");
return;
}
};
let render_target = self.get_render_target(g, &frame);
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &render_target.create_view(&wgpu::TextureViewDescriptor::default()),
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
{
let view_proj = g.camera().build_view_projection_matrix();
self.sprite_renderer.frame_pass(view_proj);
}
{
let view_proj = g.ui_camera().build_view_projection_matrix();
self.ui_renderer.frame_pass(view_proj);
}
g.render.post_renderer.frame_pass(g, &mut render_pass);
self.handle_draw_cmds(draw_commands, true);
self.sprite_renderer.frame_end(&mut render_pass);
self.ui_renderer.frame_end(&mut render_pass);
}
let renderer = &g.render.post_renderer;
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Post Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default()),
resolve_target: None,
ops: Default::default(),
depth_slice: None,
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
let view = render_target.create_view(&wgpu::TextureViewDescriptor::default());
let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("Render texture Sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let texture = TextureResource {
texture: render_target,
view,
sampler,
};
renderer.draw(&texture, &mut render_pass);
renderer.frame_end(&mut render_pass);
}
self.queue.submit(Some(encoder.finish()));
self.window.pre_present_notify();
frame.present();
self.window.request_redraw();
}
fn create_texture(&mut self, cmd: CreateTextureCommand) {
let tex = self.create_texture_inner(cmd.data);
self.textures.insert(cmd.handle.id(), tex);
}
fn remove_texture(&mut self, cmd: RemoveTextureCommand) {
self.textures.remove(&cmd.0);
}
pub fn run(app: App) -> Result<()>
where
Self: Sized,
{
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Poll);
let mut app = AppContext {
config: app,
engine: None,
state: None,
modifiers: None,
};
event_loop.run_app(&mut app).unwrap();
Ok(())
}
}
struct AppContext {
config: App,
engine: Option<Engine>,
state: Option<WGPUPlatform>,
modifiers: Option<ModifiersState>,
}
impl AppContext {
fn resize(&mut self, size: PhysicalSize<u32>) {
let state = self.state.as_mut().unwrap();
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) {
let state = self.state.as_mut().unwrap();
let Some(engine) = self.engine.as_mut() else {
return;
};
if let Err(err) = pollster::block_on(engine.handle_assets()) {
log::error!("Handle assets error {err:?}");
}
engine.update();
if engine.exit && !event_loop.exiting() {
event_loop.exit();
return;
}
let draw_commands = engine.render.take_commands();
if render {
state.render_frame(engine, draw_commands);
} else {
state.handle_draw_cmds(draw_commands, false);
}
}
}
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);
let present_mode = if *vsync {
PresentMode::AutoVsync
} else {
PresentMode::AutoNoVsync
};
let state = pollster::block_on(WGPUPlatform::new(window.clone(), present_mode)).unwrap();
self.state = Some(state);
if self.engine.is_none() {
let state = self.state.as_ref().unwrap();
let backend_state = BackendState {
device: state.device.clone(),
queue: state.queue.clone(),
surface_config: state.config.clone(),
};
let mut engine = Engine::new(InstantClock::default(), backend_state);
if let Some(setup) = self.config.setup.take() {
engine.init(setup);
}
self.engine = Some(engine);
}
self.resize(window.inner_size());
window.request_redraw();
}
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
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) {
let state = self.state.as_mut().unwrap();
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
{
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 MouseScrollDelta::LineDelta(_x, y) = delta else {
return;
};
let code = if y > 0.0 {
KeyCode::MouseWheelUp
} else {
KeyCode::MouseWheelDown
};
engine.input.set_input_state(code, KeyState::down());
}
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);
}
_ => (),
}
}
}