From 05135beb87bbf0edace544b80ba40e327d9a89ac Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Sat, 15 Aug 2026 20:44:10 -0700 Subject: factor code out of main, optimizations --- src/main.rs | 529 ++++-------------------------------------------------------- 1 file changed, 28 insertions(+), 501 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 420fb1b..92fe01e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,11 @@ mod camera; mod config; +mod renderer; mod sim; -mod ui; -use egui::Id; -use egui_wgpu::{RendererOptions, ScreenDescriptor}; -use egui_winit::egui::{self, Context}; use futures::executor; use rand::random_range; use std::{collections::VecDeque, sync::Arc, time::Instant}; -use wgpu::Origin3d; use winit::{ application::ApplicationHandler, event::{ @@ -23,16 +19,15 @@ use winit::{ use crate::{ camera::Camera, - config::{CELLS_IN_CHUNK, CHUNK_SIZE, WINDOW_TITLE}, + config::{SIM_FPS, WINDOW_TITLE}, + renderer::RendererState, 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 { - fps: u16, brush_radius: u8, brush_material: MaterialId, use_threading: bool, @@ -55,489 +50,6 @@ struct Diagnostics { fps: f32, } -struct RendererChunk { - texture: wgpu::Texture, - bind_group: wgpu::BindGroup, -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ChunkData { - origin: [i32; 2], -} - -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, - - // world pixels - renderer_chunks: Vec, - pixels_pipeline: wgpu::RenderPipeline, - camera_uniform_buffer: wgpu::Buffer, - camera_uniform_bind_group: wgpu::BindGroup, -} - -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 { - required_features: wgpu::Features::default() | wgpu::Features::IMMEDIATES, - required_limits: wgpu::Limits { - max_immediate_size: 16, - ..wgpu::Limits::defaults() - }, - ..wgpu::DeviceDescriptor::default() - }) - .await - .unwrap(); - - device.on_uncaptured_error(Arc::new(|err| panic!("{err}"))); - - 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::AutoNoVsync, - 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 camera_uniform_bind_group_layout = - device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: None, - entries: &[wgpu::BindGroupLayoutEntry { - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - binding: 0, - count: None, - visibility: wgpu::ShaderStages::VERTEX, - }], - }); - - let 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 shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader"), - source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), - }); - - let pixels_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - immediate_size: 16, - bind_group_layouts: &[ - Some(&camera_uniform_bind_group_layout), - Some(&bind_group_layout), - ], - }); - - 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::TriangleStrip, - 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, - }); - - let camera_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Camera uniform buffer"), - size: 16, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - - let camera_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &camera_uniform_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: camera_uniform_buffer.as_entire_binding(), - }], - }); - - let mut renderer_chunks: Vec = Vec::new(); - // match number of world chunks - // TODO refactor so that this implicit - for _ in -10..10 { - for _ in -10..10 { - 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 bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: None, - layout: &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() - }, - )), - }], - }); - - renderer_chunks.push(RendererChunk { - texture, - bind_group, - }) - } - } - - RendererState { - window, - surface, - device, - queue, - config, - is_surface_configured: false, - - egui_context, - egui_state, - egui_renderer, - - renderer_chunks, - pixels_pipeline, - camera_uniform_bind_group, - camera_uniform_buffer, - } - } - - 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, - world: &mut World, - camera: &mut Camera, - config: &mut Config, - diagnostics: &Diagnostics, - input: &Input, - ) { - puffin::profile_function!(); - - self.window.request_redraw(); - - if !self.is_surface_configured { - return; - } - - let output = { - puffin::profile_scope!("Get current surface texture"); - 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, - ); - - // write the camera buffer - self.queue.write_buffer( - &self.camera_uniform_buffer, - 0, - bytemuck::bytes_of(&camera.to_uniform()), - ); - - // write the chunk textures - let mut chunk_buffer: [u8; (CELLS_IN_CHUNK * 4) as usize] = - [0; (CELLS_IN_CHUNK * 4) as usize]; - - { - puffin::profile_scope!("Upload chunk textures"); - for &idx in world.chunk_position_to_chunk_idx.values() { - let chunk = &mut world.chunks[idx]; - if !chunk.needs_texture_update { - continue; - } - chunk.needs_texture_update = false; - - // TODO use material palette to improve bandwidth of upload - for i in 0..CELLS_IN_CHUNK { - let material = chunk.cells[i].material.def(); - chunk_buffer[i * 4] = material.color.0; - chunk_buffer[i * 4 + 1] = material.color.1; - chunk_buffer[i * 4 + 2] = material.color.2; - chunk_buffer[i * 4 + 3] = material.color.3; - } - - let render_chunk = &self.renderer_chunks[idx]; - self.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &render_chunk.texture, - aspect: wgpu::TextureAspect::All, - mip_level: 0, - origin: Origin3d::ZERO, - }, - &chunk_buffer, - wgpu::TexelCopyBufferLayout { - bytes_per_row: Some(CHUNK_SIZE as u32 * 4), - offset: 0, - rows_per_image: Some(CHUNK_SIZE as u32), - }, - wgpu::Extent3d { - width: CHUNK_SIZE as u32, - height: CHUNK_SIZE as u32, - depth_or_array_layers: 1, - }, - ); - } - } - - { - puffin::profile_scope!("Main render pass"); - let mut render_pass: wgpu::RenderPass<'_> = - 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.0, - g: 0.0, - b: 0.0, - 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.camera_uniform_bind_group, &[]); - - // TODO only visible chunks - for cx in -10..10 { - for cy in -10..10 { - if let Some(idx) = world.chunk_position_to_chunk_idx.get(&(cx, cy)) { - let render_chunk = &self.renderer_chunks[*idx]; - render_pass.set_bind_group(1, &render_chunk.bind_group, &[]); - render_pass - .set_immediates(0, bytemuck::bytes_of(&ChunkData { origin: [cx, cy] })); - render_pass.draw(0..4, 0..1); - } - } - } - } - - { - puffin::profile_scope!("Egui 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); - } - - { - puffin::profile_scope!("Submit queue and present"); - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); - } - } -} - struct App { window: Option>, renderer_state: Option, @@ -555,7 +67,9 @@ struct App { ignore_pause_next_tick: bool, // used to compute delta_time - last_frame_real: Instant, + last_sim_tick: Instant, + sim_ticks_due: f32, + last_render: Instant, config: Config, diagnostics: Diagnostics, @@ -586,10 +100,11 @@ impl Default for App { sim_paused: false, ignore_pause_next_tick: false, - last_frame_real: Instant::now(), + last_sim_tick: Instant::now(), + sim_ticks_due: 0., + last_render: Instant::now(), config: Config { - fps: 120, use_threading: true, brush_radius: 10, brush_material: MaterialId::Sand, @@ -694,11 +209,12 @@ impl ApplicationHandler for App { puffin::GlobalProfiler::lock().new_frame(); puffin::profile_scope!("redraw_requested"); - // compute frame delta + // compute FPS diagnostics let now = Instant::now(); - let secs_since_last_frame = (now - self.last_frame_real).as_secs_f32(); + let secs_since_last_frame = (now - self.last_render).as_secs_f32(); + self.last_render = now; + let delta_time = secs_since_last_frame / (1.0 / 60.0); - self.last_frame_real = now; self.diagnostics .frame_times @@ -746,12 +262,25 @@ impl ApplicationHandler for App { } } - // 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 { + let secs_since_last_tick = (now - self.last_sim_tick).as_secs_f32(); + let expected_secs_since_last_tick = 1.0 / SIM_FPS as f32; + + 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; + } else if !self.sim_paused { + self.sim_ticks_due += secs_since_last_tick / expected_secs_since_last_tick; + self.last_sim_tick = now; + let mut ticks_done = 0; + // don't ever tick more than 3 times per frame, or else we can get a pseudo deadlock + while self.sim_ticks_due >= 1.0 && ticks_done < 3 { + sim_tick(world, self.sim_seqno, self.config.use_threading); + self.sim_seqno += 1; + ticks_done += 1; + } + self.sim_ticks_due -= ticks_done as f32; } renderer_state.render( @@ -768,8 +297,6 @@ impl ApplicationHandler for App { } fn about_to_wait(&mut self, _: &ActiveEventLoop) { - // let frame_duration: Duration = Duration::from_micros(1_000_000 / self.config.fps as u64); - // limit our internal redraw requests to (fps) if let Some(window) = &self.window { window.request_redraw(); } else { -- cgit v1.3.1