diff options
Diffstat (limited to 'src/renderer/mod.rs')
| -rw-r--r-- | src/renderer/mod.rs | 497 |
1 files changed, 497 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(); + } + } +} |
