mod camera; mod config; mod sim; mod ui; use egui::Id; use egui_wgpu::{RendererOptions, ScreenDescriptor}; use egui_winit::egui::{self, Context}; use futures::executor; use std::{ sync::Arc, time::{Duration, Instant}, }; use wgpu::{Origin3d, TextureUsages}; use winit::{ application::ApplicationHandler, event::{ ElementState, KeyEvent, MouseButton, WindowEvent::{self}, }, event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, keyboard::{KeyCode, PhysicalKey}, window::Window, }; use crate::{ camera::Camera, config::WINDOW_TITLE, sim::{materials::MaterialId, sim::sim_tick, world::World}, ui::draw_egui, }; pub type Error = Box; pub type Result = std::result::Result; struct Config { fps: u16, brush_radius: u8, brush_material: MaterialId, use_threading: bool, } struct Input { last_mouse_pos_on_screen: Option<(f64, f64)>, last_mouse_pos_on_board: Option<(i32, i32)>, is_lmb_pressed: bool, // keybindings is_up_pressed: bool, is_left_pressed: bool, is_down_pressed: bool, is_right_pressed: bool, } struct Diagnostics { fps: f32, } struct RendererState { window: Arc, surface: wgpu::Surface<'static>, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration, is_surface_configured: bool, // egui egui_context: egui::Context, egui_state: egui_winit::State, egui_renderer: egui_wgpu::Renderer, pixels_pipeline: wgpu::RenderPipeline, pixels_bind_group: wgpu::BindGroup, texture: wgpu::Texture, // TODO remove me frame_view: Vec, } impl RendererState { // https://sotrh.github.io/learn-wgpu/beginner/tutorial1-window pub async fn new(window: Arc) -> Self { let size = window.inner_size(); println!("Got window! ({}x{})", size.width, size.height); let instance = wgpu::Instance::default(); let surface = instance.create_surface(window.clone()).unwrap(); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { compatible_surface: Some(&surface), ..wgpu::RequestAdapterOptions::default() }) .await .unwrap(); println!( "Initialized adapter, using GPU '{}'", adapter.get_info().name ); let (device, queue) = adapter .request_device(&wgpu::DeviceDescriptor::default()) .await .unwrap(); let surface_caps = surface.get_capabilities(&adapter); let surface_format = surface_caps .formats .iter() .find(|f| f.is_srgb()) .copied() .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: wgpu::PresentMode::AutoVsync, alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], desired_maximum_frame_latency: 2, }; let egui_context = Context::default(); let egui_state = egui_winit::State::new( egui_context.clone(), egui_context.viewport_id(), &window, None, None, None, ); let egui_renderer = egui_wgpu::Renderer::new(&device, surface_format, { RendererOptions { msaa_samples: 1, ..RendererOptions::default() } }); let texture = device.create_texture(&wgpu::TextureDescriptor { label: None, mip_level_count: 1, sample_count: 1, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, format: wgpu_types::TextureFormat::Rgba8UnormSrgb, size: wgpu::Extent3d { width: size.width, height: size.height, depth_or_array_layers: 1, }, dimension: wgpu::TextureDimension::D2, view_formats: &[], }); let pixels_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, entries: &[wgpu::BindGroupLayoutEntry { ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, binding: 0, count: None, visibility: wgpu::ShaderStages::FRAGMENT, }], }); let pixels_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, immediate_size: 0, bind_group_layouts: &[Some(&pixels_bind_group_layout)], }); let pixels_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &pixels_bind_group_layout, entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&texture.create_view( &wgpu::TextureViewDescriptor { dimension: Some(wgpu::TextureViewDimension::D2), usage: Some( wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, ), ..wgpu::TextureViewDescriptor::default() }, )), }], }); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Shader"), source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), }); let pixels_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: None, layout: Some(&pixels_pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format: config.format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, cull_mode: None, polygon_mode: wgpu::PolygonMode::Fill, unclipped_depth: false, conservative: false, }, depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }); RendererState { window, surface, device, queue, config, is_surface_configured: false, egui_context, egui_state, egui_renderer, texture, frame_view: vec![0; size.width as usize * size.height as usize * 4], pixels_pipeline, pixels_bind_group, } } pub fn resize(&mut self, width: u32, height: u32) { if width > 0 && height > 0 { self.config.width = width; self.config.height = height; self.surface.configure(&self.device, &self.config); self.is_surface_configured = true; } } pub fn render( &mut self, config: &mut Config, camera: &mut Camera, diagnostics: &Diagnostics, input: &Input, ) { puffin::profile_function!(); self.window.request_redraw(); if !self.is_surface_configured { return; } let output = match self.surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(surface_texture) => surface_texture, wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => surface_texture, wgpu::CurrentSurfaceTexture::Timeout | wgpu::CurrentSurfaceTexture::Occluded | wgpu::CurrentSurfaceTexture::Validation => { // Skip this frame return; } wgpu::CurrentSurfaceTexture::Outdated => { self.surface.configure(&self.device, &self.config); return; } wgpu::CurrentSurfaceTexture::Lost => { panic!("Lost device?"); } }; let view = output .texture .create_view(&wgpu::TextureViewDescriptor::default()); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("Render Encoder"), }); let raw_input = self.egui_state.take_egui_input(&self.window); let full_output = self.egui_context.run_ui(raw_input, |ui| { let right_panel = egui::Panel::right(Id::new("right_panel")); right_panel .resizable(false) // TODO collapse button .show_collapsible(ui, &mut true, |panel_ui| { draw_egui(panel_ui, config, camera, diagnostics, input) }); }); self.egui_state .handle_platform_output(&self.window, full_output.platform_output); let clipped_primitives = self .egui_context .tessellate(full_output.shapes, full_output.pixels_per_point); let pixels_per_point = full_output.pixels_per_point; let size = self.window.inner_size(); let screen_descriptor = ScreenDescriptor { size_in_pixels: [size.width, size.height], pixels_per_point, }; for (id, delta) in &full_output.textures_delta.set { self.egui_renderer .update_texture(&self.device, &self.queue, *id, delta); } self.egui_renderer.update_buffers( &self.device, &self.queue, &mut encoder, &clipped_primitives, &screen_descriptor, ); // let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { // label: Some("Render Pass"), // color_attachments: &[Some(wgpu::RenderPassColorAttachment { // view: &view, // resolve_target: None, // depth_slice: None, // ops: wgpu::Operations { // load: wgpu::LoadOp::Clear(wgpu::Color { // r: 0.1, // g: 0.2, // b: 0.3, // a: 1.0, // }), // store: wgpu::StoreOp::Store, // }, // })], // depth_stencil_attachment: None, // occlusion_query_set: None, // timestamp_writes: None, // multiview_mask: None, // }); // drop(render_pass); self.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &self.texture, aspect: wgpu::TextureAspect::All, mip_level: 0, origin: Origin3d::ZERO, }, &self.frame_view, wgpu::TexelCopyBufferLayout { bytes_per_row: Some(size.width * 4), offset: 0, rows_per_image: Some(size.height), }, wgpu::Extent3d { width: size.width, height: size.height, depth_or_array_layers: 1, }, ); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, depth_slice: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.1, g: 0.2, b: 0.3, a: 1.0, }), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, occlusion_query_set: None, timestamp_writes: None, multiview_mask: None, }); render_pass.set_pipeline(&self.pixels_pipeline); render_pass.set_bind_group(0, &self.pixels_bind_group, &[]); render_pass.draw(0..3, 0..1); drop(render_pass); let mut egui_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("egui pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, resolve_target: None, depth_slice: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }) .forget_lifetime(); self.egui_renderer .render(&mut egui_pass, &clipped_primitives, &screen_descriptor); drop(egui_pass); self.queue.submit(std::iter::once(encoder.finish())); output.present(); } } struct App { window: Option>, renderer_state: Option, input: Input, camera: Option, world: Option, // sim state // the last/current (not yet completed) seqno sim_seqno: u64, sim_paused: bool, ignore_pause_next_tick: bool, // used to wait for drawing last_frame_requested: Instant, // used to compute delta_time last_frame_real: Instant, config: Config, diagnostics: Diagnostics, } impl Default for App { fn default() -> Self { Self { window: None, renderer_state: None, input: Input { last_mouse_pos_on_screen: None, last_mouse_pos_on_board: None, is_lmb_pressed: false, is_up_pressed: false, is_left_pressed: false, is_down_pressed: false, is_right_pressed: false, }, camera: Some(Camera { x: 0.0, y: 0.0, zoom: 1.0, }), world: Some(World::from_default_size()), sim_seqno: 0, sim_paused: false, ignore_pause_next_tick: false, last_frame_requested: Instant::now(), last_frame_real: Instant::now(), config: Config { fps: 120, use_threading: true, brush_radius: 10, brush_material: MaterialId::Sand, }, diagnostics: Diagnostics { fps: 0.0 }, } } } impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let window = Arc::new( event_loop .create_window(Window::default_attributes().with_title(WINDOW_TITLE)) .unwrap(), ); self.window = Some(window.clone()); self.renderer_state = Some(executor::block_on(RendererState::new(window.clone()))); // let surface = SurfaceTexture::new(size.width, size.height, window_ref); // let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap(); // pixels.set_scaling_mode(ScalingMode::Fill); } fn window_event( &mut self, event_loop: &ActiveEventLoop, _: winit::window::WindowId, event: WindowEvent, ) { if let Some(window) = &self.window && let Some(renderer_state) = &mut self.renderer_state && let Some(world) = &mut self.world && let Some(camera) = &mut self.camera { let egui_response = renderer_state.egui_state.on_window_event(window, &event); // if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks if egui_response.consumed { return; } match event { WindowEvent::KeyboardInput { event: KeyEvent { physical_key: PhysicalKey::Code(code), state, .. }, .. } => { let pressed = state.is_pressed(); match code { KeyCode::KeyW => self.input.is_up_pressed = pressed, KeyCode::KeyA => self.input.is_left_pressed = pressed, KeyCode::KeyS => self.input.is_down_pressed = pressed, KeyCode::KeyD => self.input.is_right_pressed = pressed, KeyCode::KeyC => self.world = Some(World::from_default_size()), KeyCode::Space => { if pressed { self.sim_paused = !self.sim_paused } } KeyCode::KeyX => { if pressed { self.ignore_pause_next_tick = true } } _ => {} } } WindowEvent::CursorMoved { position, .. } => { self.input.last_mouse_pos_on_screen = Some((position.x, position.y)); // self.input.last_mouse_pos_on_board = pixels // .window_pos_to_pixel((position.x as f32, position.y as f32)) // .map(|v| camera.screen_position_to_world(v.0 as f64, v.1 as f64)) // .map(|v| (v.0 as i32, v.1 as i32)) // .ok(); } WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left { self.input.is_lmb_pressed = state == ElementState::Pressed } } WindowEvent::Resized(size) => { // Important: resize the surface when the window's size change renderer_state.resize(size.width, size.height); } WindowEvent::CloseRequested => { event_loop.exit(); } WindowEvent::RedrawRequested => { #[cfg(feature = "profiler")] puffin::GlobalProfiler::lock().new_frame(); puffin::profile_scope!("redraw_requested"); // compute frame delta let now = Instant::now(); let secs_since_last_frame = (now - self.last_frame_real).as_secs_f32(); let delta_time = secs_since_last_frame / (1.0 / 60.0); self.last_frame_real = now; let instantaneous_fps = 1.0 / secs_since_last_frame; // TODO: can smooth and round this self.diagnostics.fps = instantaneous_fps; // apply inputs camera.handle_camera_input(&self.input, delta_time); // // --TEST DRAWING-- // if self.input.is_lmb_pressed // && let Some(lm) = self.input.last_mouse_pos_on_board // { // // start with the bounding box of the drawing brush circle + some margin // // clamp the bounding box to the board sie // // TODO better way to do this without unwrap? // let bb_xl = lm.0 - self.config.brush_radius as i32; // let bb_xu = lm.0 + self.config.brush_radius as i32; // let bb_yl = lm.1 - self.config.brush_radius as i32; // let bb_yu = lm.1 + self.config.brush_radius as i32; // // for each point, check if the distance is less than the brush size and write the pixel // for x in bb_xl..bb_xu { // for y in bb_yl..bb_yu { // let r = random_range(0.0..1.0); // if ((x - lm.0).pow(2) + (y - lm.1).pow(2)) // < (self.config.brush_radius as i32).pow(2) // && r > 0.9 // { // let mut cell = Cell::from_material(self.config.brush_material); // cell.flags = (self.sim_seqno as u8) & 0b1; // world.set_cell_from_game_position( // x, y, cell, // wake the chunk // false, // ); // } // } // } // } // TODO check if we need to run another sim tick given the sim speed + delta_time // SIM logic if !self.sim_paused || self.ignore_pause_next_tick { sim_tick(world, self.sim_seqno, self.config.use_threading); self.sim_seqno += 1; self.ignore_pause_next_tick = false; } camera.write_frame_view(&mut renderer_state.frame_view, world); renderer_state.render( &mut self.config, camera, &self.diagnostics, &mut self.input, ); } _ => {} } } } fn about_to_wait(&mut self, _: &ActiveEventLoop) { let now = Instant::now(); let frame_duration: Duration = Duration::from_micros(1_000_000 / self.config.fps as u64); // limit our internal redraw requests to (fps) if now - self.last_frame_requested >= frame_duration { self.last_frame_requested = now; if let Some(window) = &self.window { window.request_redraw(); } else { panic!("No window!") } } } } #[cfg(feature = "profiler")] fn start_profiler() { let _server = puffin_http::Server::new("127.0.0.1:8585").unwrap(); puffin::set_scopes_on(true); std::mem::forget(_server); // keep serving for the process lifetime std::process::Command::new("puffin_viewer") .args(["--url", "127.0.0.1:8585"]) .spawn() .ok(); // don't die if it isn't installed } fn main() -> Result<()> { #[cfg(feature = "profiler")] start_profiler(); let event_loop = EventLoop::new()?; event_loop.set_control_flow(ControlFlow::Poll); let mut app = App::default(); event_loop.run_app(&mut app)?; Ok(()) }