From 43cae9ac71f2d6933ba3ea25641585bb88ae60ad Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Sat, 15 Aug 2026 17:59:17 -0700 Subject: working, performance is bad --- src/main.rs | 435 ++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 234 insertions(+), 201 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index d248a75..420fb1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,11 +8,8 @@ use egui_wgpu::{RendererOptions, ScreenDescriptor}; use egui_winit::egui::{self, Context}; use futures::executor; use rand::random_range; -use std::{ - sync::Arc, - time::{Duration, Instant}, -}; -use wgpu::{Origin3d, TextureUsages}; +use std::{collections::VecDeque, sync::Arc, time::Instant}; +use wgpu::Origin3d; use winit::{ application::ApplicationHandler, event::{ @@ -26,7 +23,7 @@ use winit::{ use crate::{ camera::Camera, - config::WINDOW_TITLE, + config::{CELLS_IN_CHUNK, CHUNK_SIZE, WINDOW_TITLE}, sim::{cell::Cell, materials::MaterialId, sim::sim_tick, world::World}, ui::draw_egui, }; @@ -54,6 +51,7 @@ struct Input { } struct Diagnostics { + frame_times: VecDeque, fps: f32, } @@ -62,6 +60,12 @@ struct RendererChunk { 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>, @@ -78,10 +82,8 @@ struct RendererState { // world pixels renderer_chunks: Vec, pixels_pipeline: wgpu::RenderPipeline, - pixels_bind_group: wgpu::BindGroup, - texture: wgpu::Texture, - // TODO remove me - frame_view: Vec, + camera_uniform_buffer: wgpu::Buffer, + camera_uniform_bind_group: wgpu::BindGroup, } impl RendererState { @@ -107,10 +109,19 @@ impl RendererState { ); let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor::default()) + .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 @@ -124,7 +135,7 @@ impl RendererState { format: surface_format, width: size.width, height: size.height, - present_mode: wgpu::PresentMode::AutoVsync, + present_mode: wgpu::PresentMode::AutoNoVsync, alpha_mode: surface_caps.alpha_modes[0], view_formats: vec![], desired_maximum_frame_latency: 2, @@ -146,50 +157,32 @@ impl RendererState { } }); - 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 = + let camera_uniform_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, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, }, binding: 0, count: None, - visibility: wgpu::ShaderStages::FRAGMENT, + visibility: wgpu::ShaderStages::VERTEX, }], }); - let pixels_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: None, - layout: &pixels_bind_group_layout, - entries: &[wgpu::BindGroupEntry { + entries: &[wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, 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() - }, - )), + count: None, + visibility: wgpu::ShaderStages::FRAGMENT, }], }); @@ -201,8 +194,11 @@ impl RendererState { let pixels_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: None, - immediate_size: 0, - bind_group_layouts: &[Some(&pixels_bind_group_layout)], + 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 { @@ -225,7 +221,7 @@ impl RendererState { compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { - topology: wgpu::PrimitiveTopology::TriangleList, + topology: wgpu::PrimitiveTopology::TriangleStrip, strip_index_format: None, front_face: wgpu::FrontFace::Ccw, cull_mode: None, @@ -239,8 +235,23 @@ impl RendererState { cache: None, }); - let mut renderer_chunks: Vec = Vec::new(); + 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 { @@ -260,21 +271,6 @@ impl RendererState { view_formats: &[], }); - 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 bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: None, layout: &bind_group_layout, @@ -312,11 +308,10 @@ impl RendererState { egui_state, egui_renderer, - renderer_chunks: Vec::new(), - texture, - frame_view: vec![0; size.width as usize * size.height as usize * 4], + renderer_chunks, pixels_pipeline, - pixels_bind_group, + camera_uniform_bind_group, + camera_uniform_buffer, } } @@ -331,8 +326,9 @@ impl RendererState { pub fn render( &mut self, - config: &mut Config, + world: &mut World, camera: &mut Camera, + config: &mut Config, diagnostics: &Diagnostics, input: &Input, ) { @@ -344,21 +340,24 @@ impl RendererState { 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 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?"); + } } }; @@ -411,103 +410,131 @@ impl RendererState { &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, - }, + // write the camera buffer + self.queue.write_buffer( + &self.camera_uniform_buffer, + 0, + bytemuck::bytes_of(&camera.to_uniform()), ); - 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, - }); + // 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; + } - 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, + 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, }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - multiview_mask: None, - }) - .forget_lifetime(); + &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); - drop(egui_pass); + self.egui_renderer + .render(&mut egui_pass, &clipped_primitives, &screen_descriptor); + } - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); + { + puffin::profile_scope!("Submit queue and present"); + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + } } } @@ -527,8 +554,6 @@ struct App { 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, @@ -553,11 +578,7 @@ impl Default for App { is_right_pressed: false, }, - camera: Some(Camera { - x: 0.0, - y: 0.0, - zoom: 1.0, - }), + camera: None, world: Some(World::from_default_size()), @@ -565,7 +586,6 @@ impl Default for App { sim_paused: false, ignore_pause_next_tick: false, - last_frame_requested: Instant::now(), last_frame_real: Instant::now(), config: Config { @@ -574,7 +594,10 @@ impl Default for App { brush_radius: 10, brush_material: MaterialId::Sand, }, - diagnostics: Diagnostics { fps: 0.0 }, + diagnostics: Diagnostics { + fps: 0.0, + frame_times: VecDeque::new(), + }, } } } @@ -590,6 +613,9 @@ impl ApplicationHandler for App { self.window = Some(window.clone()); self.renderer_state = Some(executor::block_on(RendererState::new(window.clone()))); + let size = window.inner_size(); + self.camera = Some(Camera::new((size.width as i32, size.height as i32))); + // let surface = SurfaceTexture::new(size.width, size.height, window_ref); // let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap(); @@ -646,7 +672,8 @@ impl ApplicationHandler for App { } WindowEvent::CursorMoved { position, .. } => { self.input.last_mouse_pos_on_screen = Some((position.x, position.y)); - let world_pos = camera.screen_position_to_world(position.x, position.y); + let world_pos = + camera.screen_position_to_world(position.x as f32, position.y as f32); self.input.last_mouse_pos_on_board = Some((world_pos.0 as i32, world_pos.1 as i32)) } @@ -656,8 +683,8 @@ impl ApplicationHandler for App { } } WindowEvent::Resized(size) => { - // Important: resize the surface when the window's size change renderer_state.resize(size.width, size.height); + camera.resize((size.width as i32, size.height as i32)); } WindowEvent::CloseRequested => { event_loop.exit(); @@ -665,15 +692,26 @@ impl ApplicationHandler for App { 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; + + self.diagnostics + .frame_times + .push_back(secs_since_last_frame); + + if self.diagnostics.frame_times.len() > 30 { + self.diagnostics.frame_times.pop_front(); + } + + let average_frame_time = self.diagnostics.frame_times.iter().sum::() + / self.diagnostics.frame_times.len() as f32; + + self.diagnostics.fps = 1.0 / average_frame_time; // apply inputs camera.handle_camera_input(&self.input, delta_time); @@ -716,11 +754,10 @@ impl ApplicationHandler for App { self.ignore_pause_next_tick = false; } - camera.write_frame_view(&mut renderer_state.frame_view, world); - renderer_state.render( - &mut self.config, + world, camera, + &mut self.config, &self.diagnostics, &mut self.input, ); @@ -731,16 +768,12 @@ impl ApplicationHandler for App { } 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); + // 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!") - } + if let Some(window) = &self.window { + window.request_redraw(); + } else { + panic!("No window!") } } } -- cgit v1.3.1