summaryrefslogtreecommitdiff
path: root/src/renderer
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/renderer
parent43cae9ac71f2d6933ba3ea25641585bb88ae60ad (diff)
factor code out of main, optimizations
Diffstat (limited to 'src/renderer')
-rw-r--r--src/renderer/mod.rs497
-rw-r--r--src/renderer/ui.rs64
2 files changed, 561 insertions, 0 deletions
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/renderer/ui.rs b/src/renderer/ui.rs
new file mode 100644
index 0000000..87ab878
--- /dev/null
+++ b/src/renderer/ui.rs
@@ -0,0 +1,64 @@
+use egui::{Color32, Stroke, Ui, epaint::CircleShape};
+
+use crate::{Camera, Config, Diagnostics, Input, sim::materials::MaterialId};
+
+pub fn draw_egui<'a>(
+ ui: &mut Ui,
+ config: &mut Config,
+ camera: &mut Camera,
+ diagnostics: &Diagnostics,
+ input: &Input,
+) {
+ puffin::profile_function!();
+ ui.heading("Config");
+ ui.add(egui::Slider::new(&mut config.brush_radius, 1..=100).text("Brush radius"));
+
+ let material = config.brush_material.def();
+
+ // material combobox
+ egui::ComboBox::from_label("Select a material")
+ .selected_text(format!(
+ "Material: [{}, d={}]",
+ material.name, material.density,
+ ))
+ .icon(move |ui, rect, _, _| {
+ ui.painter().add(egui::Shape::Circle(CircleShape {
+ center: rect.center(),
+ radius: rect.width() / 2.5,
+ stroke: Stroke::NONE,
+ fill: Color32::from_rgb(material.color.0, material.color.1, material.color.2),
+ }));
+ })
+ .show_ui(ui, |ui| {
+ for material_id in MaterialId::ALL {
+ ui.selectable_value(
+ &mut config.brush_material,
+ material_id,
+ material_id.def().name,
+ );
+ }
+ });
+
+ ui.checkbox(&mut config.use_threading, "Use multithreading");
+ ui.label(format!("Real FPS: {}", diagnostics.fps));
+ ui.heading("Camera");
+ ui.add(egui::Slider::new(&mut camera.zoom, 0.0..=10.0).text("Zoom"));
+ ui.heading("Input");
+
+ input.last_mouse_pos_on_screen.map(|p| {
+ ui.label(format!(
+ "Mouse: x,y=({x}, {y}), lmb_pressed={lmb}",
+ x = p.0,
+ y = p.1,
+ lmb = input.is_lmb_pressed
+ ))
+ });
+
+ input
+ .last_mouse_pos_on_board
+ .map(|p| ui.label(format!("Mouse (board): x,y=({x}, {y})", x = p.0, y = p.1,)));
+
+ input
+ .last_mouse_pos_on_board
+ .map(|p| ui.label(format!("Mouse (chunk): x,y=({x}, {y})", x = p.0, y = p.1,)));
+}