diff options
| author | Kai Stevenson <kai@kaistevenson.com> | 2026-08-16 18:37:24 -0700 |
|---|---|---|
| committer | Kai Stevenson <kai@kaistevenson.com> | 2026-08-16 18:37:24 -0700 |
| commit | e937ac372fa92cd971d344748978a08316a55db4 (patch) | |
| tree | 32f12bbc3d9167d1192473883ce7001253819084 /src/renderer | |
| parent | 1759cdce2845d24183f34febd8319cb4ff1ac673 (diff) | |
instance buffer, rotation
Diffstat (limited to 'src/renderer')
| -rw-r--r-- | src/renderer/mod.rs | 537 |
1 files changed, 242 insertions, 295 deletions
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 1cf39e2..39a507b 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -13,20 +13,29 @@ use crate::{ sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::RbSimManager}, }; -struct RendererChunk { - texture: wgpu::Texture, - bind_group: wgpu::BindGroup, -} - -struct RendererRbEntity { - texture: wgpu::Texture, - bind_group: wgpu::BindGroup, -} +// TODO derive from shared chunk config +const CHUNK_SLOTS: usize = 11 * 200; +const RB_ENTITY_SLOTS: usize = 64; +const CELL_SLOTS: usize = CHUNK_SLOTS + RB_ENTITY_SLOTS; #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct ChunkData { - origin: [i32; 2], +struct Instance { + centre: [f32; 2], + cos_sin: [f32; 2], + half_size: [f32; 2], + dims: [u32; 2], + cell_offset: u32, + _padding: u32, +} + +fn srgb_to_linear(channel: u8) -> f32 { + let channel = channel as f32 / 255.0; + if channel <= 0.04045 { + channel / 12.92 + } else { + ((channel + 0.055) / 1.055).powf(2.4) + } } pub struct RendererState { @@ -44,12 +53,13 @@ pub struct RendererState { 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, + instance_buffer: wgpu::Buffer, + cell_buffer: wgpu::Buffer, + pixels_bind_group: wgpu::BindGroup, - renderer_rb_entities: FxHashMap<u32, RendererRbEntity>, + renderer_rb_entities: FxHashMap<u32, usize>, } impl RendererState { @@ -123,35 +133,57 @@ impl RendererState { } }); - let camera_uniform_bind_group_layout = + let pixels_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, + entries: &[ + // camera uniform + 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, }, - binding: 0, - count: None, - visibility: wgpu::ShaderStages::VERTEX, - }], + // palette + wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + binding: 1, + count: None, + visibility: wgpu::ShaderStages::FRAGMENT, + }, + // instance buffer + wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + binding: 2, + count: None, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + }, + // cells + wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + binding: 3, + count: None, + visibility: wgpu::ShaderStages::FRAGMENT, + }, + ], }); - 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()), @@ -160,11 +192,8 @@ impl RendererState { 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), - ], + immediate_size: 0, + bind_group_layouts: &[Some(&pixels_bind_group_layout)], }); let pixels_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { @@ -181,7 +210,7 @@ impl RendererState { entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format: config.format, - blend: Some(wgpu::BlendState::REPLACE), + blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), @@ -208,59 +237,61 @@ impl RendererState { 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 palette = [0.0f32; MaterialId::ALL.len() * 4]; + for material in MaterialId::ALL { + let def = material.def(); + let i = material as usize * 4; + palette[i] = srgb_to_linear(def.color.0); + palette[i + 1] = srgb_to_linear(def.color.1); + palette[i + 2] = srgb_to_linear(def.color.2); + palette[i + 3] = def.color.3 as f32 / 255.0; + } + + let palette_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Palette buffer"), + size: size_of_val(&palette) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, }); - let mut renderer_chunks: Vec<RendererChunk> = Vec::new(); - // match number of world chunks - // TODO refactor so that this implicit - for _ in -10..1 { - for _ in -100..100 { - 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: &[], - }); + queue.write_buffer(&palette_buffer, 0, bytemuck::cast_slice(&palette)); - 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() - }, - )), - }], - }); + let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Instance buffer"), + size: (CELL_SLOTS * size_of::<Instance>()) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); - renderer_chunks.push(RendererChunk { - texture, - bind_group, - }) - } - } + let cell_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Cell buffer"), + size: (CELL_SLOTS * CELLS_IN_CHUNK) as u64, + usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let pixels_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &pixels_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: camera_uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: palette_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: instance_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: cell_buffer.as_entire_binding(), + }, + ], + }); RendererState { window, @@ -274,10 +305,11 @@ impl RendererState { egui_state, egui_renderer, - renderer_chunks, pixels_pipeline, - camera_uniform_bind_group, camera_uniform_buffer, + instance_buffer, + cell_buffer, + pixels_bind_group, renderer_rb_entities: FxHashMap::default(), } @@ -386,11 +418,11 @@ impl RendererState { bytemuck::bytes_of(&camera.to_uniform()), ); - // write the chunk textures - let mut chunk_buffer: [u8; CELLS_IN_CHUNK * 4] = [0; (CELLS_IN_CHUNK * 4)]; + let mut cell_buffer: [u8; CELLS_IN_CHUNK] = [0; CELLS_IN_CHUNK]; + let mut instances: Vec<Instance> = Vec::new(); { - puffin::profile_scope!("Upload chunk textures"); + puffin::profile_scope!("Upload chunk cells"); for &idx in world.chunk_position_to_chunk_idx.values() { let chunk = &mut world.chunks[idx]; if !chunk.needs_texture_update { @@ -398,227 +430,142 @@ impl RendererState { } 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; + for (byte, cell) in cell_buffer.iter_mut().zip(chunk.cells.iter()) { + *byte = cell.material as u8; } - 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, - }, + self.queue.write_buffer( + &self.cell_buffer, + (idx * CELLS_IN_CHUNK) as u64, + &cell_buffer, ); } } { - puffin::profile_scope!("Upload entity textures"); + puffin::profile_scope!("Upload entity cells"); for entity in rb_sim_manager.rb_entities.values() { // TODO add "needs texture update"? - - // TODO use material palette to improve bandwidth of upload - for i in 0..CELLS_IN_CHUNK { - let material = entity.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; + for (byte, cell) in cell_buffer.iter_mut().zip(entity.cells.iter()) { + *byte = cell.material as u8; } - let render_entity = &self.renderer_rb_entities.entry(entity.id).or_insert({ - let texture = self.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_layout = - self.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 next_slot = CHUNK_SLOTS + self.renderer_rb_entities.len(); + let slot = *self + .renderer_rb_entities + .entry(entity.id) + .or_insert(next_slot); - let bind_group = self.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() - }, - )), - }], - }); - - RendererRbEntity { - texture, - bind_group, - } - }); - - self.queue.write_texture( - wgpu::TexelCopyTextureInfo { - texture: &render_entity.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, - }, + self.queue.write_buffer( + &self.cell_buffer, + (slot * CELLS_IN_CHUNK) as u64, + &cell_buffer, ); } + } - { - 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, &[]); + { + puffin::profile_scope!("Build instances"); + let (xl, xu, yl, yu) = camera.viewport_bounds_world(); + let ((cxl, cyl), _) = World::split_game_position(xl.floor() as i32, yl.floor() as i32); + let ((cxu, cyu), _) = World::split_game_position(xu.floor() as i32, yu.floor() as i32); - let (xl, xu, yl, yu) = camera.viewport_bounds_world(); - let ((cxl, cyl), _) = - World::split_game_position(xl.floor() as i32, yl.floor() as i32); - let ((cxu, cyu), _) = - World::split_game_position(xu.floor() as i32, yu.floor() as i32); + let half_size = [CHUNK_SIZE as f32 / 2.0, CHUNK_SIZE as f32 / 2.0]; + let dims = [CHUNK_SIZE as u32, CHUNK_SIZE as u32]; - for cx in cxl..=cxu { - for cy in cyl..=cyu { - 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 * CHUNK_SIZE, cy * CHUNK_SIZE], - }), - ); - render_pass.draw(0..4, 0..1); - } + for cx in cxl..=cxu { + for cy in cyl..=cyu { + if let Some(idx) = world.chunk_position_to_chunk_idx.get(&(cx, cy)) { + instances.push(Instance { + centre: [ + (cx * CHUNK_SIZE) as f32 + half_size[0], + (cy * CHUNK_SIZE) as f32 + half_size[1], + ], + cos_sin: [1.0, 0.0], + half_size, + dims, + cell_offset: (idx * CELLS_IN_CHUNK) as u32, + _padding: 0, + }); } } + } - // TODO this is inefficient - for rb_entity in rb_sim_manager.rb_entities.keys() { - let renderer_rb_entity = self.renderer_rb_entities.get(rb_entity).unwrap(); - let (x, y) = rb_sim_manager.get_rb_entity_position(*rb_entity).unwrap(); - render_pass.set_bind_group(1, &renderer_rb_entity.bind_group, &[]); - render_pass.set_immediates( - 0, - bytemuck::bytes_of(&ChunkData { - origin: [x.round() as i32, y.round() as i32], - }), - ); - render_pass.draw(0..4, 0..1); - } + for (id, slot) in &self.renderer_rb_entities { + let (x, y, cos, sin) = rb_sim_manager.get_rb_entity_transform(*id).unwrap(); + instances.push(Instance { + centre: [x, y], + cos_sin: [cos, sin], + half_size, + dims, + cell_offset: (slot * CELLS_IN_CHUNK) as u32, + _padding: 0, + }); } - { - 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.queue + .write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances)); + } - self.egui_renderer - .render(&mut egui_pass, &clipped_primitives, &screen_descriptor); - } + { + 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, + }); - { - puffin::profile_scope!("Submit queue and present"); - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); - } + render_pass.set_pipeline(&self.pixels_pipeline); + render_pass.set_bind_group(0, &self.pixels_bind_group, &[]); + render_pass.draw(0..4, 0..instances.len() as u32); + } + + { + 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(); } } } |
