diff options
| author | Kai Stevenson <kai@kaistevenson.com> | 2026-08-15 20:44:10 -0700 |
|---|---|---|
| committer | Kai Stevenson <kai@kaistevenson.com> | 2026-08-15 20:44:10 -0700 |
| commit | 05135beb87bbf0edace544b80ba40e327d9a89ac (patch) | |
| tree | 1ec3fc0d3cf041c57273e92ecbe751756c616f67 | |
| parent | 43cae9ac71f2d6933ba3ea25641585bb88ae60ad (diff) | |
factor code out of main, optimizations
| -rw-r--r-- | .cargo/config.toml | 3 | ||||
| -rw-r--r-- | Cargo.lock | 10 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/config.rs | 4 | ||||
| -rw-r--r-- | src/main.rs | 529 | ||||
| -rw-r--r-- | src/renderer/mod.rs | 497 | ||||
| -rw-r--r-- | src/renderer/ui.rs (renamed from src/ui.rs) | 1 | ||||
| -rw-r--r-- | src/shader/shader.wgsl (renamed from src/shader.wgsl) | 4 | ||||
| -rw-r--r-- | src/sim/sim.rs | 14 | ||||
| -rw-r--r-- | src/sim/world.rs | 8 |
10 files changed, 557 insertions, 514 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml index 32794cb..6687bf8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,3 @@ [alias] -profile = "run --features profiler" +profile = "run --release --features profiler" +release = "run --release" @@ -991,6 +991,15 @@ dependencies = [ ] [[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] name = "gethostname" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2242,6 +2251,7 @@ dependencies = [ "egui-winit", "env_logger", "futures", + "fxhash", "puffin", "puffin_http", "rand", @@ -17,6 +17,7 @@ rand = "0.10.2" rayon = "1.12.0" futures = "0.3.34" bytemuck = "1.25.2" +fxhash = "0.2.1" [features] profiler = [] diff --git a/src/config.rs b/src/config.rs index 54ec6b5..fc3e341 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,8 @@ pub const WINDOW_TITLE: &str = "pxs"; -pub const CHUNK_SIZE: i32 = 32; +pub const CHUNK_SIZE: i32 = 128; pub const CELLS_IN_CHUNK: usize = (CHUNK_SIZE * CHUNK_SIZE) as usize; pub const CAMERA_MOVEMENT_SPEED: f32 = 40.0; + +pub const SIM_FPS: u32 = 120; 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<dyn std::error::Error>; pub type Result<T> = std::result::Result<T, Error>; 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<Window>, - 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<RendererChunk>, - 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<Window>) -> 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<RendererChunk> = 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<Arc<Window>>, renderer_state: Option<RendererState>, @@ -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 { diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs new file mode 100644 index 0000000..fcc5d36 --- /dev/null +++ b/src/renderer/mod.rs @@ -0,0 +1,497 @@ +mod ui; + +use std::sync::Arc; + +use winit::window::Window; + +use crate::{ + Config, Diagnostics, Input, + camera::Camera, + config::{CELLS_IN_CHUNK, CHUNK_SIZE}, + renderer::ui::draw_egui, + sim::world::World, +}; + +struct RendererChunk { + texture: wgpu::Texture, + bind_group: wgpu::BindGroup, +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct ChunkData { + origin: [i32; 2], +} + +pub struct RendererState { + window: Arc<Window>, + surface: wgpu::Surface<'static>, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + is_surface_configured: bool, + + // egui + egui_context: egui::Context, + // TODO remove pub + pub egui_state: egui_winit::State, + egui_renderer: egui_wgpu::Renderer, + + // world pixels + renderer_chunks: Vec<RendererChunk>, + 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<Window>) -> 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 = egui::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, { + egui_wgpu::RendererOptions { + msaa_samples: 1, + ..egui_wgpu::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/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<RendererChunk> = 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: CHUNK_SIZE as u32, + height: CHUNK_SIZE as u32, + 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(egui::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 = egui_wgpu::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: wgpu::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(); + } + } +} diff --git a/src/ui.rs b/src/renderer/ui.rs index 8608447..87ab878 100644 --- a/src/ui.rs +++ b/src/renderer/ui.rs @@ -39,7 +39,6 @@ pub fn draw_egui<'a>( } }); - ui.add(egui::Slider::new(&mut config.fps, 1..=1000).text("Max FPS")); ui.checkbox(&mut config.use_threading, "Use multithreading"); ui.label(format!("Real FPS: {}", diagnostics.fps)); ui.heading("Camera"); diff --git a/src/shader.wgsl b/src/shader/shader.wgsl index 6adcda8..bd38cf0 100644 --- a/src/shader.wgsl +++ b/src/shader/shader.wgsl @@ -26,7 +26,7 @@ fn vs_main( let corner = vec2f(f32(i & 1u), f32(i >> 1u)); // CHUNK_SIZE - let world = (vec2f(chunk.origin) + corner) * 32.0; + let world = (vec2f(chunk.origin) + corner) * 128.0; let ndc = (world - camera.centre) * camera.scale; out.clip_position = vec4f(ndc, 0.0, 1.0); @@ -38,7 +38,7 @@ fn vs_main( @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { // CHUNK_SIZE - let dims = vec2f(32.0, 32.0); + let dims = vec2f(128.0, 128.0); return textureLoad(tex, vec2i(in.uv * dims), 0); // return vec4f(0.0, 1.0, 0.0, 1.0); }
\ No newline at end of file diff --git a/src/sim/sim.rs b/src/sim/sim.rs index ef2fc80..4c6d908 100644 --- a/src/sim/sim.rs +++ b/src/sim/sim.rs @@ -1,5 +1,6 @@ -use std::{collections::HashMap, marker::PhantomData}; +use std::marker::PhantomData; +use fxhash::FxHashMap; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::{ @@ -73,11 +74,13 @@ pub fn set_cell(chunks: &mut [Option<&mut Chunk>; 9], x: i32, y: i32, cell: Cell if let Some(chunk) = &mut chunks[(dcx + 1 + (dcy + 1) * 3) as usize] { chunk.set_cell_at_local_position(nc_x, nc_y, cell); chunk.needs_texture_update = true; + chunk.sleeping = false; } } else { if let Some(target) = &mut chunks[4] { target.set_cell_at_local_position(x as u8, y as u8, cell); target.needs_texture_update = true; + target.sleeping = false; } } } @@ -90,15 +93,18 @@ impl UpdateCtx<'_, '_, '_> { } fn set_cell(&mut self, dx: i32, dy: i32, cell: Cell) { + // cannot move out of the neighbourhood, but also cannot move to the edge of the neighbourhood + // as this would wake a chunk outside of the neighbourhood + debug_assert!(dx > -15 && dx < 15); + debug_assert!(dy > -15 && dy < 15); let x = self.x + dx; let y = self.y + dy; set_cell(self.chunks, x, y, cell); - // wake all the chunks self.chunks.iter_mut().for_each(|c| { if let Some(chunk) = c { chunk.sleeping = false; } - }); + }) } pub fn candidates_swap(&mut self, candidates: &[(i32, i32)]) -> bool { @@ -170,7 +176,7 @@ const NEIGHBORHOOD_OFFSETS: [(i32, i32); 9] = [ pub fn sim_tick(world: &mut World, seqno: u64, use_threading: bool) { puffin::profile_function!(); - let mut columns: HashMap<i32, Vec<i32>> = HashMap::new(); + let mut columns: FxHashMap<i32, Vec<i32>> = FxHashMap::default(); for &(cx, cy) in world.chunk_position_to_chunk_idx.keys() { columns.entry(cx).or_default().push(cy); } diff --git a/src/sim/world.rs b/src/sim/world.rs index bdb1286..6645d4e 100644 --- a/src/sim/world.rs +++ b/src/sim/world.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use fxhash::FxHashMap; use crate::{ config::CHUNK_SIZE, @@ -7,8 +7,8 @@ use crate::{ pub struct World { pub chunks: Vec<Chunk>, - // TODO FxHashMap? - pub chunk_position_to_chunk_idx: HashMap<(i32, i32), usize>, + // TODO FxFxHashMap? + pub chunk_position_to_chunk_idx: FxHashMap<(i32, i32), usize>, } impl World { @@ -60,7 +60,7 @@ impl World { pub fn from_default_size() -> Self { let mut world = World { chunks: Vec::new(), - chunk_position_to_chunk_idx: HashMap::new(), + chunk_position_to_chunk_idx: FxHashMap::default(), }; for y in -10..10 { |
