mod camera; mod config; mod sim; mod ui; use egui::Id; use egui_wgpu::{RendererOptions, ScreenDescriptor}; use egui_winit::egui::{self, Context}; use pixels::{Pixels, ScalingMode, SurfaceTexture}; use std::time::{Duration, Instant}; 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::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH, WINDOW_TITLE}, sim::{cell::Cell, materials::MaterialId, sim::sim_tick, world::World}, ui::draw_egui, }; pub type Error = Box; pub type Result = std::result::Result; struct Config { show_ticks: bool, fps: u16, brush_radius: u8, brush_material: MaterialId, } 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 App { // core egui_renderer: Option, egui_state: Option, egui_context: Option, window: Option<&'static Window>, pixels: 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 { egui_renderer: None, egui_state: None, egui_context: None, window: None, pixels: 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, show_ticks: false, 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 = event_loop .create_window(Window::default_attributes().with_title(WINDOW_TITLE)) .unwrap(); let size = window.inner_size(); let window_ref: &'static Window = Box::leak(Box::new(window)); 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); let egui_context = Context::default(); let egui_state = egui_winit::State::new( egui_context.clone(), egui_context.viewport_id(), window_ref, None, None, None, ); let egui_renderer = egui_wgpu::Renderer::new(pixels.device(), pixels.render_texture_format(), { RendererOptions { msaa_samples: 1, ..RendererOptions::default() } }); self.egui_context = Some(egui_context); self.egui_state = Some(egui_state); self.egui_renderer = Some(egui_renderer); self.window = Some(window_ref); self.pixels = Some(pixels); } fn window_event( &mut self, event_loop: &ActiveEventLoop, _: winit::window::WindowId, event: WindowEvent, ) { if let Some(pixels) = &mut self.pixels && let Some(window) = self.window && let Some(egui_context) = &self.egui_context && let Some(egui_state) = &mut self.egui_state && let Some(egui_renderer) = &mut self.egui_renderer && let Some(world) = &mut self.world && let Some(camera) = &mut self.camera { let egui_response = 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 pixels.resize_surface(size.width, size.height).unwrap(); } 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); // pixels/camera logic let frame = pixels.frame_mut(); frame.fill(0); // --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 { // brush/selection if ((x - lm.0).pow(2) + (y - lm.1).pow(2)) < (self.config.brush_radius as i32).pow(2) { world.set_cell_from_game_position( x, y, Cell::from_material(self.config.brush_material), ); } } } } // TODO check if we need to run another sim tick given the sim speed // SIM logic if !self.sim_paused || self.ignore_pause_next_tick { sim_tick(world, self.sim_seqno); self.sim_seqno += 1; self.ignore_pause_next_tick = false; } // let get_overlay = // create_compute_combined_overlay_offset(&world, &self.config, &self.input); camera.write_frame_view(frame, &world); // egui logic let raw_input = egui_state.take_egui_input(window); let full_output = 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, &mut self.config, camera, &self.diagnostics, &self.input, ) }); }); egui_state.handle_platform_output(window, full_output.platform_output); let clipped_primitives = egui_context.tessellate(full_output.shapes, full_output.pixels_per_point); let pixels_per_point = full_output.pixels_per_point; let size = window.inner_size(); let screen_descriptor = ScreenDescriptor { size_in_pixels: [size.width, size.height], pixels_per_point, }; let queue = pixels.queue(); let device = pixels.device(); for (id, delta) in &full_output.textures_delta.set { egui_renderer.update_texture(&device, &queue, *id, delta); } let _ = pixels.render_with(|encoder, render_target, context| { context.scaling_renderer.render(encoder, render_target); egui_renderer.update_buffers( device, &queue, encoder, &clipped_primitives, &screen_descriptor, ); let mut egui_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("egui pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: render_target, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }) .forget_lifetime(); egui_renderer.render( &mut egui_pass, &clipped_primitives, &screen_descriptor, ); drop(egui_pass); Ok(()) }); } _ => {} } } } 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; self.window .expect("Bug - Window should exist") .request_redraw(); } } } #[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(()) }