summaryrefslogtreecommitdiff
path: root/src/renderer/instances.rs
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-09-05 17:23:46 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-09-05 17:23:46 -0700
commit0ee1423cb267363d0b315ae7572eb2e6f98bfa4a (patch)
tree8aa4686bab49adaae8bd03faecc120fbfa309b5e /src/renderer/instances.rs
parent67aee9ec7f3282cab20f6439f755588135a32af5 (diff)
refactor renderer to separate passes
Diffstat (limited to 'src/renderer/instances.rs')
-rw-r--r--src/renderer/instances.rs325
1 files changed, 325 insertions, 0 deletions
diff --git a/src/renderer/instances.rs b/src/renderer/instances.rs
new file mode 100644
index 0000000..1292b99
--- /dev/null
+++ b/src/renderer/instances.rs
@@ -0,0 +1,325 @@
+use fxhash::FxHashMap;
+
+use crate::{
+ camera::Camera,
+ config::{CELLS_IN_CHUNK, CHUNK_SIZE},
+ sim::{cell_manager::manager::CellManager, entity::EntityId, sim_manager::SimManager},
+};
+
+// TODO derive from shared chunk config
+const CHUNK_SLOTS: usize = 11 * 200;
+
+fn create_instance_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer {
+ device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Instance buffer"),
+ size: (capacity * size_of::<RendererInstance>()) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ })
+}
+
+fn create_cell_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer {
+ device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Cell buffer"),
+ size: (capacity * CELLS_IN_CHUNK) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ })
+}
+
+#[repr(C)]
+#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+struct RendererInstance {
+ centre: [f32; 2],
+ cos_sin: [f32; 2],
+ half_size: [f32; 2],
+ dims: [u32; 2],
+ cell_offset: u32,
+ _padding: u32,
+}
+
+pub struct InstancesRendererState {
+ instance_pipeline: wgpu::RenderPipeline,
+ instance_buffer: wgpu::Buffer,
+ cell_buffer: wgpu::Buffer,
+ instance_bind_group: wgpu::BindGroup,
+ entity_capacity: usize,
+
+ renderer_entities: FxHashMap<EntityId, usize>,
+ instances_count: usize,
+}
+
+impl InstancesRendererState {
+ pub fn update_buffers(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ sim: &mut SimManager,
+ camera: &Camera,
+ ) {
+ puffin::profile_function!();
+ let mut cell_buffer: [u8; CELLS_IN_CHUNK] = [0; CELLS_IN_CHUNK];
+ let mut instances: Vec<RendererInstance> = Vec::new();
+
+ for &idx in sim.cell_manager.chunk_position_to_chunk_idx.values() {
+ let chunk = &mut sim.cell_manager.chunks[idx];
+ if !chunk.needs_texture_update {
+ continue;
+ }
+ chunk.needs_texture_update = false;
+
+ for (byte, cell) in cell_buffer.iter_mut().zip(chunk.cells.iter()) {
+ *byte = cell.material as u8;
+ }
+
+ queue.write_buffer(
+ &self.cell_buffer,
+ (idx * CELLS_IN_CHUNK) as u64,
+ &cell_buffer,
+ );
+ }
+
+ // TODO optimize, this is very inefficient, just build it per frame with positions and skip the lookup?
+ self.renderer_entities.clear();
+ for entity in sim.entities.values() {
+ // TODO add "needs texture update"?
+ if let Some(cells) = &entity.data.cells {
+ for i in 0..cells.cells.len() {
+ cell_buffer[i] = cells.cells[i].material as u8;
+ }
+
+ let next_slot = CHUNK_SLOTS + self.renderer_entities.len();
+ let slot = *self
+ .renderer_entities
+ .entry(entity.data.id)
+ .or_insert(next_slot);
+
+ queue.write_buffer(
+ &self.cell_buffer,
+ (slot * CHUNK_SIZE as usize * CHUNK_SIZE as usize) as u64,
+ &cell_buffer,
+ );
+ }
+ }
+
+ let (lower, upper) = camera.viewport_bounds_world();
+ let ((cxl, cyl), _) =
+ CellManager::split_game_position(lower.x.floor() as i32, lower.y.floor() as i32);
+ let ((cxu, cyu), _) =
+ CellManager::split_game_position(upper.x.floor() as i32, upper.y.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) = sim.cell_manager.chunk_position_to_chunk_idx.get(&(cx, cy)) {
+ instances.push(RendererInstance {
+ 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,
+ });
+ }
+ }
+ }
+
+ let mut entities = 0;
+ for (id, slot) in &self.renderer_entities {
+ if let Some(entity) = sim.entities.get(id)
+ && let Some(cells) = &entity.data.cells
+ && let Some((pos, (cos, sin))) = entity.data._transform(&sim.rb_manager)
+ {
+ // TODO access this better
+ // let sleeping = sim
+ // .rb_manager
+ // .physics_manager
+ // .world
+ // .bodies
+ // .get(entity.data.rb_h.unwrap())
+ // .unwrap()
+ // .is_sleeping();
+ let sleeping = false;
+
+ entities += 1;
+ instances.push(RendererInstance {
+ centre: if sleeping {
+ pos.round().to_array()
+ } else {
+ pos.to_array()
+ },
+ cos_sin: [cos, sin],
+ half_size: (cells.size / 2).as_vec2().to_array(),
+ dims: cells.size.as_uvec2().to_array(),
+ cell_offset: (slot * CHUNK_SIZE as usize * CHUNK_SIZE as usize) as u32,
+ _padding: 0,
+ });
+ }
+ }
+
+ if entities > self.entity_capacity {
+ self.entity_capacity = entities.next_power_of_two();
+ self.instance_buffer =
+ create_instance_buffer(device, self.entity_capacity + CHUNK_SLOTS);
+ self.cell_buffer = create_cell_buffer(device, CHUNK_SLOTS + self.entity_capacity);
+ }
+
+ queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
+
+ self.instances_count = instances.len();
+ }
+
+ pub fn render(&self, render_pass: &mut wgpu::RenderPass) {
+ render_pass.set_pipeline(&self.instance_pipeline);
+ render_pass.set_bind_group(0, &self.instance_bind_group, &[]);
+ render_pass.draw(0..4, 0..self.instances_count as u32);
+ }
+
+ pub fn new(
+ device: &wgpu::Device,
+ surface_config: &wgpu::SurfaceConfiguration,
+ camera_uniform_buffer: &wgpu::Buffer,
+ palette_buffer: &wgpu::Buffer,
+ ) -> Self {
+ let instance_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Instance shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("../shader/instance.wgsl").into()),
+ });
+
+ let instance_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: 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,
+ },
+ // 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 instance_pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: None,
+ immediate_size: 0,
+ bind_group_layouts: &[Some(&instance_bind_group_layout)],
+ });
+
+ let instance_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: None,
+ layout: Some(&instance_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &instance_shader,
+ entry_point: Some("vs_main"),
+ buffers: &[],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &instance_shader,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: surface_config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ 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 entity_capacity = 2048;
+ let instance_buffer = create_instance_buffer(&device, CHUNK_SLOTS + entity_capacity);
+
+ let cell_buffer = create_cell_buffer(&device, entity_capacity + CHUNK_SLOTS);
+
+ let instance_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: None,
+ layout: &instance_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(),
+ },
+ ],
+ });
+
+ InstancesRendererState {
+ instance_pipeline,
+ instance_buffer,
+ cell_buffer,
+ instance_bind_group,
+ entity_capacity,
+
+ renderer_entities: FxHashMap::default(),
+ instances_count: 0,
+ }
+ }
+}