summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-15 20:44:10 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-15 20:44:10 -0700
commit05135beb87bbf0edace544b80ba40e327d9a89ac (patch)
tree1ec3fc0d3cf041c57273e92ecbe751756c616f67 /src
parent43cae9ac71f2d6933ba3ea25641585bb88ae60ad (diff)
factor code out of main, optimizations
Diffstat (limited to 'src')
-rw-r--r--src/config.rs4
-rw-r--r--src/main.rs529
-rw-r--r--src/renderer/mod.rs497
-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.rs14
-rw-r--r--src/sim/world.rs8
7 files changed, 544 insertions, 513 deletions
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 {