use std::collections::HashSet;
use super::super::types::{
BUFFER_GROWTH_FACTOR, DrawIndexedIndirect, MAX_INSTANCES, ModelMatrix, ObjectData,
compute_normal_matrix,
};
use super::super::world_state::BatchRange;
use super::MeshPass;
impl MeshPass {
pub(in super::super) fn update_dirty_transforms(
&mut self,
world: &crate::ecs::world::World,
queue: &wgpu::Queue,
) -> bool {
let dirty_entities = self
.frame_dirty
.as_mut()
.map(|fd| std::mem::take(&mut fd.transform_dirty))
.unwrap_or_default();
let dirty_count = dirty_entities.len();
let local_matrices_dirty = self
.frame_dirty
.as_mut()
.map(|fd| std::mem::take(&mut fd.instanced_local_matrices_dirty))
.unwrap_or_default();
let world_state = self.world_states.get_mut(&self.current_world_id).unwrap();
if !local_matrices_dirty.is_empty() {
for &entity in &local_matrices_dirty {
if let Some(&(start, count)) = world_state.instanced_transform_ranges.get(&entity)
&& let Some(instanced_mesh) = world.core.get_instanced_mesh(entity)
{
let local_matrices = instanced_mesh.cached_local_matrices();
let upload_count = local_matrices.len().min(count as usize);
if upload_count > 0 {
let raw: Vec<[[f32; 4]; 4]> = local_matrices[..upload_count]
.iter()
.map(|mat| (*mat).into())
.collect();
if let Some(gpu) = world_state.gpu_buffers.as_ref() {
let offset_bytes =
(start as u64) * std::mem::size_of::<[[f32; 4]; 4]>() as u64;
queue.write_buffer(
&gpu.instanced_local_matrix_buffer,
offset_bytes,
bytemuck::cast_slice(&raw),
);
}
world_state.instanced_compute_dirty = true;
}
}
}
}
let total = world_state.gpu_registry.len();
if dirty_count > 0 && total > 0 {
let threshold = (total as f32 * 0.25).ceil() as usize;
if dirty_count > threshold {
let regular_count = world_state.regular_object_count as usize;
let slot_limit = regular_count.min(world_state.cached_transforms.len());
let all_dirty = dirty_count >= regular_count;
let dirty_set: Option<&HashSet<crate::ecs::world::Entity>> = if all_dirty {
None
} else {
Some(&dirty_entities)
};
let mut any_mesh_dirty = false;
for slot in 0..slot_limit {
if let Some(entity) = world_state
.gpu_registry
.slot_to_entity
.get(slot)
.copied()
.flatten()
&& (all_dirty || dirty_set.is_some_and(|s| s.contains(&entity)))
&& let Some(transform) = world.core.get_global_transform(entity)
{
any_mesh_dirty = true;
world_state.cached_transforms[slot] = ModelMatrix {
model: transform.0.into(),
normal_matrix: compute_normal_matrix(&transform.0),
};
}
}
for (&entity, &(start, count)) in &world_state.instanced_transform_ranges {
if !dirty_entities.contains(&entity) {
continue;
}
world_state.instanced_compute_dirty = true;
let Some(instanced_mesh) = world.core.get_instanced_mesh(entity) else {
continue;
};
let custom_data_slice = instanced_mesh.custom_data_slice();
let start_idx = start as usize;
let end_idx = start_idx + count as usize;
if end_idx <= world_state.cached_custom_data.len() {
for (offset, data) in custom_data_slice.iter().enumerate() {
world_state.cached_custom_data[start_idx + offset] = data.tint;
}
}
}
let gpu = world_state.gpu_buffers.as_ref().unwrap();
let regular_count_for_upload = world_state.regular_object_count as usize;
if any_mesh_dirty
&& regular_count_for_upload > 0
&& regular_count_for_upload <= world_state.cached_transforms.len()
{
queue.write_buffer(
&gpu.transform_buffer,
0,
bytemuck::cast_slice(
&world_state.cached_transforms[..regular_count_for_upload],
),
);
}
let used_count = world_state.object_count as usize;
if used_count > 0 && used_count <= world_state.cached_custom_data.len() {
queue.write_buffer(
&gpu.custom_data_buffer,
0,
bytemuck::cast_slice(&world_state.cached_custom_data[..used_count]),
);
}
} else {
let mut mesh_updates: Vec<(u32, ModelMatrix)> = Vec::new();
let mut instanced_dirty: Vec<crate::ecs::world::Entity> = Vec::new();
for &entity in &dirty_entities {
if let Some(&slot) = world_state.gpu_registry.entity_to_slot.get(&entity)
&& let Some(transform) = world.core.get_global_transform(entity)
{
let model_matrix = ModelMatrix {
model: transform.0.into(),
normal_matrix: compute_normal_matrix(&transform.0),
};
if (slot as usize) < world_state.cached_transforms.len() {
world_state.cached_transforms[slot as usize] = model_matrix;
}
mesh_updates.push((slot, model_matrix));
} else if world_state.instanced_transform_ranges.contains_key(&entity) {
instanced_dirty.push(entity);
} else if world
.core
.entity_has_components(entity, crate::ecs::world::INSTANCED_MESH)
{
return false;
}
}
let gpu = world_state.gpu_buffers.as_ref().unwrap();
if !mesh_updates.is_empty() {
mesh_updates.sort_by_key(|(slot, _)| *slot);
let element_size = std::mem::size_of::<ModelMatrix>() as u64;
let mut range_start = mesh_updates[0].0;
let mut range_data: Vec<ModelMatrix> = vec![mesh_updates[0].1];
for &(slot, matrix) in mesh_updates.iter().skip(1) {
if slot == range_start + range_data.len() as u32 {
range_data.push(matrix);
} else {
queue.write_buffer(
&gpu.transform_buffer,
(range_start as u64) * element_size,
bytemuck::cast_slice(&range_data),
);
range_start = slot;
range_data.clear();
range_data.push(matrix);
}
}
queue.write_buffer(
&gpu.transform_buffer,
(range_start as u64) * element_size,
bytemuck::cast_slice(&range_data),
);
}
if !instanced_dirty.is_empty() {
world_state.instanced_compute_dirty = true;
for entity in instanced_dirty {
let &(start, count) =
world_state.instanced_transform_ranges.get(&entity).unwrap();
let Some(instanced_mesh) = world.core.get_instanced_mesh(entity) else {
continue;
};
let custom_data_slice = instanced_mesh.custom_data_slice();
let updated_custom: Vec<[f32; 4]> =
custom_data_slice.iter().map(|data| data.tint).collect();
let start_idx = start as usize;
let end_idx = start_idx + count as usize;
if end_idx <= world_state.cached_custom_data.len() {
world_state.cached_custom_data[start_idx..end_idx]
.copy_from_slice(&updated_custom);
}
let custom_element_size = std::mem::size_of::<[f32; 4]>() as u64;
queue.write_buffer(
&gpu.custom_data_buffer,
(start as u64) * custom_element_size,
bytemuck::cast_slice(&updated_custom),
);
}
}
}
} else if dirty_count > 0 {
for &entity in &dirty_entities {
if !world_state.instanced_transform_ranges.contains_key(&entity)
&& world
.core
.entity_has_components(entity, crate::ecs::world::INSTANCED_MESH)
{
return false;
}
}
world_state.instanced_compute_dirty = true;
if let Some(gpu) = world_state.gpu_buffers.as_ref() {
for &entity in &dirty_entities {
let Some(&(start, count)) = world_state.instanced_transform_ranges.get(&entity)
else {
continue;
};
let Some(instanced_mesh) = world.core.get_instanced_mesh(entity) else {
continue;
};
let custom_data_slice = instanced_mesh.custom_data_slice();
let updated_custom: Vec<[f32; 4]> =
custom_data_slice.iter().map(|data| data.tint).collect();
let start_idx = start as usize;
let end_idx = start_idx + count as usize;
if end_idx <= world_state.cached_custom_data.len() {
world_state.cached_custom_data[start_idx..end_idx]
.copy_from_slice(&updated_custom);
}
let custom_element_size = std::mem::size_of::<[f32; 4]>() as u64;
queue.write_buffer(
&gpu.custom_data_buffer,
(start as u64) * custom_element_size,
bytemuck::cast_slice(&updated_custom),
);
}
}
}
true
}
pub(in super::super) fn can_do_incremental_update(&self) -> bool {
let Some(fd) = self.frame_dirty.as_ref() else {
return false;
};
fd.frame_initialized
&& !fd.full_rebuild_needed
&& !fd.batches_invalidated
&& !fd.instanced_meshes_changed
&& fd.entities_added.is_empty()
&& fd.entities_removed.is_empty()
&& fd.material_dirty.is_empty()
&& !fd.transform_dirty.is_empty()
}
pub(in super::super) fn can_do_incremental_entity_update(
&self,
world: &crate::ecs::world::World,
) -> bool {
let Some(fd) = self.frame_dirty.as_ref() else {
return false;
};
if !fd.frame_initialized
|| fd.full_rebuild_needed
|| fd.instanced_meshes_changed
|| !fd.material_dirty.is_empty()
{
return false;
}
let has_adds = !fd.entities_added.is_empty();
let has_removes = !fd.entities_removed.is_empty();
if !has_adds && !has_removes {
return false;
}
let world_state = match self.world_states.get(&self.current_world_id) {
Some(state) => state,
None => return false,
};
if world_state.gpu_buffers.is_none() {
return false;
}
if has_adds {
if world_state.cached_name_to_material_id.is_empty() {
return false;
}
for entity in &fd.entities_added {
let has_mesh = world
.core
.get_render_mesh(*entity)
.is_some_and(|mesh| self.meshes.contains_key(&mesh.name));
if !has_mesh {
return false;
}
let has_cached_material =
world.core.get_material_ref(*entity).is_some_and(|mat_ref| {
world_state
.cached_name_to_material_id
.contains_key(&mat_ref.name)
});
if !has_cached_material {
return false;
}
}
}
true
}
pub(in super::super) fn incremental_update_entities(
&mut self,
world: &crate::ecs::world::World,
device: &wgpu::Device,
queue: &wgpu::Queue,
) {
use crate::ecs::generational_registry::{registry_entry, registry_entry_by_name};
let fd = self.frame_dirty.as_mut().unwrap();
let entities_added = std::mem::take(&mut fd.entities_added);
let entities_removed = std::mem::take(&mut fd.entities_removed);
let dirty_transforms = std::mem::take(&mut fd.transform_dirty);
let world_state = self.world_states.get_mut(&self.current_world_id).unwrap();
if !entities_removed.is_empty() {
let gpu = world_state.gpu_buffers.as_ref().unwrap();
let far_away_transform = ModelMatrix {
model: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[1e10, 1e10, 1e10, 1.0],
],
normal_matrix: [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
};
for &entity in &entities_removed {
if let Some(slot) = world_state.gpu_registry.entity_to_slot.remove(&entity) {
if (slot as usize) < world_state.gpu_registry.slot_to_entity.len() {
world_state.gpu_registry.slot_to_entity[slot as usize] = None;
}
world_state.entity_to_transform_index.remove(&entity);
world_state.cached_material_map.remove(&entity);
if (slot as usize) < world_state.cached_objects.len() {
let mesh_id = world_state.cached_objects[slot as usize].mesh_id;
let material_id = world_state.cached_objects[slot as usize].material_id;
world_state
.free_slots_by_group
.entry((mesh_id, material_id))
.or_default()
.push(slot);
}
let transform_offset =
(slot as u64) * std::mem::size_of::<ModelMatrix>() as u64;
queue.write_buffer(
&gpu.transform_buffer,
transform_offset,
bytemuck::cast_slice(&[far_away_transform]),
);
if (slot as usize) < world_state.cached_transforms.len() {
world_state.cached_transforms[slot as usize] = far_away_transform;
}
if (slot as usize) < world_state.cached_objects.len() {
world_state.cached_objects[slot as usize].batch_id = u32::MAX;
let object_offset =
(slot as u64) * std::mem::size_of::<ObjectData>() as u64;
queue.write_buffer(
&gpu.object_buffer,
object_offset,
bytemuck::cast_slice(&[world_state.cached_objects[slot as usize]]),
);
}
if (slot as usize) < world_state.cached_custom_data.len() {
world_state.cached_custom_data[slot as usize] = [0.0, 0.0, 0.0, 0.0];
let custom_data_offset =
(slot as u64) * std::mem::size_of::<[f32; 4]>() as u64;
queue.write_buffer(
&gpu.custom_data_buffer,
custom_data_offset,
bytemuck::cast_slice(&[[0.0f32, 0.0, 0.0, 0.0]]),
);
}
}
}
}
let current_object_count = world_state.object_count as usize;
struct ClassifiedEntity {
entity: crate::ecs::world::Entity,
mesh_id: u32,
material_id: u32,
is_transparent: bool,
transform: nalgebra_glm::Mat4,
custom_data: [f32; 4],
}
let mut classified = Vec::with_capacity(entities_added.len());
for &entity in &entities_added {
if world
.sprite2d
.entity_has_components(entity, crate::ecs::world::SPRITE)
{
continue;
}
let Some(mesh) = world.core.get_render_mesh(entity) else {
continue;
};
let Some(&mesh_id) = self.meshes.get(&mesh.name) else {
continue;
};
let material_id = if let Some(mat_ref) = world.core.get_material_ref(entity) {
if let Some(&cached_id) = world_state.cached_name_to_material_id.get(&mat_ref.name)
{
cached_id
} else {
0
}
} else {
0
};
let Some(transform) = world.core.get_global_transform(entity) else {
continue;
};
let material = world.core.get_material_ref(entity).and_then(|mat_ref| {
if let Some(id) = mat_ref.id {
registry_entry(
&world.resources.material_registry.registry,
id.index,
id.generation,
)
} else {
registry_entry_by_name(
&world.resources.material_registry.registry,
&mat_ref.name,
)
}
});
let material_is_transparent = material.is_some_and(|m| {
m.alpha_mode == crate::ecs::material::components::AlphaMode::Blend
|| m.transmission_factor > 0.0
});
let custom_alpha = 1.0_f32;
let is_transparent = material_is_transparent || custom_alpha < 1.0;
classified.push(ClassifiedEntity {
entity,
mesh_id,
material_id,
is_transparent,
transform: transform.0,
custom_data: [1.0, 1.0, 1.0, 1.0],
});
world_state.cached_material_map.insert(entity, material_id);
}
classified.sort_by(|a, b| {
a.is_transparent
.cmp(&b.is_transparent)
.then(a.mesh_id.cmp(&b.mesh_id))
.then(a.material_id.cmp(&b.material_id))
});
let gpu = world_state.gpu_buffers.as_ref().unwrap();
let mut reused_indices: Vec<(usize, u32)> = Vec::new();
let mut remaining_indices: Vec<usize> = Vec::new();
for (index, info) in classified.iter().enumerate() {
let group_key = (info.mesh_id, info.material_id);
if let Some(free_slot) = world_state
.free_slots_by_group
.get_mut(&group_key)
.and_then(|slots| slots.pop())
{
reused_indices.push((index, free_slot));
} else {
remaining_indices.push(index);
}
}
for &(classified_index, slot) in &reused_indices {
let info = &classified[classified_index];
let old_batch_id = world_state.cached_objects[slot as usize].batch_id;
let mesh_data = &self.mesh_data[info.mesh_id as usize];
let model_matrix = ModelMatrix {
model: info.transform.into(),
normal_matrix: compute_normal_matrix(&info.transform),
};
let object_data = ObjectData {
transform_index: slot,
mesh_id: info.mesh_id,
material_id: info.material_id,
batch_id: old_batch_id,
morph_weights: [0.0f32; 8],
morph_target_count: mesh_data.morph_target_count,
morph_displacement_offset: mesh_data.morph_displacement_offset,
mesh_vertex_offset: mesh_data.vertex_offset,
mesh_vertex_count: mesh_data.vertex_count,
entity_id: info.entity.id,
is_overlay: 0,
skip_occlusion: u32::from(info.is_transparent),
_padding0: 0,
};
let transform_offset = (slot as u64) * std::mem::size_of::<ModelMatrix>() as u64;
queue.write_buffer(
&gpu.transform_buffer,
transform_offset,
bytemuck::cast_slice(&[model_matrix]),
);
let object_offset = (slot as u64) * std::mem::size_of::<ObjectData>() as u64;
queue.write_buffer(
&gpu.object_buffer,
object_offset,
bytemuck::cast_slice(&[object_data]),
);
let custom_offset = (slot as u64) * std::mem::size_of::<[f32; 4]>() as u64;
queue.write_buffer(
&gpu.custom_data_buffer,
custom_offset,
bytemuck::cast_slice(&[info.custom_data]),
);
world_state.cached_transforms[slot as usize] = model_matrix;
world_state.cached_objects[slot as usize] = object_data;
world_state.cached_custom_data[slot as usize] = info.custom_data;
world_state.cached_entities[slot as usize] = info.entity;
world_state
.gpu_registry
.entity_to_slot
.insert(info.entity, slot);
world_state.gpu_registry.slot_to_entity[slot as usize] = Some(info.entity);
world_state
.entity_to_transform_index
.insert(info.entity, slot);
world_state
.cached_material_map
.insert(info.entity, info.material_id);
}
let mut generic_free_slots: Vec<u32> = Vec::new();
for slots in world_state.free_slots_by_group.values_mut() {
generic_free_slots.append(slots);
}
world_state.free_slots_by_group.clear();
let mut generic_reused_indices: Vec<(usize, u32)> = Vec::new();
let mut truly_appended_indices: Vec<usize> = Vec::new();
for &index in &remaining_indices {
if let Some(slot) = generic_free_slots.pop() {
generic_reused_indices.push((index, slot));
} else {
truly_appended_indices.push(index);
}
}
for slot in generic_free_slots {
if (slot as usize) < world_state.cached_objects.len() {
let obj = &world_state.cached_objects[slot as usize];
world_state
.free_slots_by_group
.entry((obj.mesh_id, obj.material_id))
.or_default()
.push(slot);
}
}
for &(classified_index, slot) in &generic_reused_indices {
let info = &classified[classified_index];
let mesh_data = &self.mesh_data[info.mesh_id as usize];
let model_matrix = ModelMatrix {
model: info.transform.into(),
normal_matrix: compute_normal_matrix(&info.transform),
};
let object_data = ObjectData {
transform_index: slot,
mesh_id: info.mesh_id,
material_id: info.material_id,
batch_id: 0,
morph_weights: [0.0f32; 8],
morph_target_count: mesh_data.morph_target_count,
morph_displacement_offset: mesh_data.morph_displacement_offset,
mesh_vertex_offset: mesh_data.vertex_offset,
mesh_vertex_count: mesh_data.vertex_count,
entity_id: info.entity.id,
is_overlay: 0,
skip_occlusion: u32::from(info.is_transparent),
_padding0: 0,
};
let transform_offset = (slot as u64) * std::mem::size_of::<ModelMatrix>() as u64;
queue.write_buffer(
&gpu.transform_buffer,
transform_offset,
bytemuck::cast_slice(&[model_matrix]),
);
let object_offset = (slot as u64) * std::mem::size_of::<ObjectData>() as u64;
queue.write_buffer(
&gpu.object_buffer,
object_offset,
bytemuck::cast_slice(&[object_data]),
);
let custom_offset = (slot as u64) * std::mem::size_of::<[f32; 4]>() as u64;
queue.write_buffer(
&gpu.custom_data_buffer,
custom_offset,
bytemuck::cast_slice(&[info.custom_data]),
);
world_state.cached_transforms[slot as usize] = model_matrix;
world_state.cached_objects[slot as usize] = object_data;
world_state.cached_custom_data[slot as usize] = info.custom_data;
world_state.cached_entities[slot as usize] = info.entity;
world_state
.gpu_registry
.entity_to_slot
.insert(info.entity, slot);
world_state.gpu_registry.slot_to_entity[slot as usize] = Some(info.entity);
world_state
.entity_to_transform_index
.insert(info.entity, slot);
world_state
.cached_material_map
.insert(info.entity, info.material_id);
}
if !dirty_transforms.is_empty() {
let dirty_count = dirty_transforms.len();
let total = world_state.gpu_registry.len();
let threshold = (total as f32 * 0.25).ceil() as usize;
if dirty_count > threshold {
let regular_count = world_state.regular_object_count as usize;
let slot_limit = regular_count.min(world_state.cached_transforms.len());
let all_dirty = dirty_count >= regular_count;
let dirty_set: Option<&HashSet<crate::ecs::world::Entity>> = if all_dirty {
None
} else {
Some(&dirty_transforms)
};
let mut any_mesh_dirty = false;
for slot in 0..slot_limit {
if let Some(entity) = world_state
.gpu_registry
.slot_to_entity
.get(slot)
.copied()
.flatten()
&& (all_dirty || dirty_set.is_some_and(|s| s.contains(&entity)))
&& let Some(transform) = world.core.get_global_transform(entity)
{
any_mesh_dirty = true;
world_state.cached_transforms[slot] = ModelMatrix {
model: transform.0.into(),
normal_matrix: compute_normal_matrix(&transform.0),
};
}
}
let used_count = world_state.object_count as usize;
if any_mesh_dirty
&& used_count > 0
&& used_count <= world_state.cached_transforms.len()
{
queue.write_buffer(
&gpu.transform_buffer,
0,
bytemuck::cast_slice(&world_state.cached_transforms[..used_count]),
);
}
} else {
let mut mesh_updates: Vec<(u32, ModelMatrix)> = Vec::new();
for entity in dirty_transforms {
if let Some(&slot) = world_state.gpu_registry.entity_to_slot.get(&entity)
&& let Some(transform) = world.core.get_global_transform(entity)
{
let model_matrix = ModelMatrix {
model: transform.0.into(),
normal_matrix: compute_normal_matrix(&transform.0),
};
if (slot as usize) < world_state.cached_transforms.len() {
world_state.cached_transforms[slot as usize] = model_matrix;
}
mesh_updates.push((slot, model_matrix));
}
}
if !mesh_updates.is_empty() {
mesh_updates.sort_by_key(|(slot, _)| *slot);
let element_size = std::mem::size_of::<ModelMatrix>() as u64;
let mut range_start = mesh_updates[0].0;
let mut range_data: Vec<ModelMatrix> = vec![mesh_updates[0].1];
for &(slot, matrix) in mesh_updates.iter().skip(1) {
if slot == range_start + range_data.len() as u32 {
range_data.push(matrix);
} else {
queue.write_buffer(
&gpu.transform_buffer,
(range_start as u64) * element_size,
bytemuck::cast_slice(&range_data),
);
range_start = slot;
range_data.clear();
range_data.push(matrix);
}
}
queue.write_buffer(
&gpu.transform_buffer,
(range_start as u64) * element_size,
bytemuck::cast_slice(&range_data),
);
}
}
}
if !truly_appended_indices.is_empty() {
let new_total = current_object_count + truly_appended_indices.len();
let needs_grow = new_total
> world_state
.gpu_buffers
.as_ref()
.unwrap()
.transform_buffer_size
|| new_total > world_state.gpu_buffers.as_ref().unwrap().object_buffer_size
|| new_total
> world_state
.gpu_buffers
.as_ref()
.unwrap()
.custom_data_buffer_size;
if needs_grow {
let new_size = std::cmp::min(
(new_total as f32 * BUFFER_GROWTH_FACTOR).ceil() as usize,
MAX_INSTANCES,
);
if new_size
> world_state
.gpu_buffers
.as_ref()
.unwrap()
.transform_buffer_size
{
let new_transform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Mesh Transform Buffer (Incremental Resize)"),
size: (std::mem::size_of::<ModelMatrix>() * new_size) as u64,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
if !world_state.cached_transforms.is_empty() {
queue.write_buffer(
&new_transform,
0,
bytemuck::cast_slice(&world_state.cached_transforms),
);
}
let new_custom = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Mesh Custom Data Buffer (Incremental Resize)"),
size: (std::mem::size_of::<[f32; 4]>() * new_size) as u64,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
if !world_state.cached_custom_data.is_empty() {
queue.write_buffer(
&new_custom,
0,
bytemuck::cast_slice(&world_state.cached_custom_data),
);
}
let gpu_mut = world_state.gpu_buffers.as_mut().unwrap();
gpu_mut.transform_buffer = new_transform;
gpu_mut.transform_buffer_size = new_size;
gpu_mut.custom_data_buffer = new_custom;
gpu_mut.custom_data_buffer_size = new_size;
}
if new_size > world_state.gpu_buffers.as_ref().unwrap().object_buffer_size {
let new_object = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Mesh Object Buffer (Incremental Resize)"),
size: (std::mem::size_of::<ObjectData>() * new_size) as u64,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST
| wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
if !world_state.cached_objects.is_empty() {
queue.write_buffer(
&new_object,
0,
bytemuck::cast_slice(&world_state.cached_objects),
);
}
let gpu_mut = world_state.gpu_buffers.as_mut().unwrap();
gpu_mut.object_buffer = new_object;
gpu_mut.object_buffer_size = new_size;
}
let visible_size = (world_state
.gpu_buffers
.as_ref()
.unwrap()
.visible_indices_buffer
.size()
/ std::mem::size_of::<u32>() as u64)
as usize;
if new_size > visible_size {
let gpu_mut = world_state.gpu_buffers.as_mut().unwrap();
gpu_mut.visible_indices_buffer =
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Visible Indices Buffer (Incremental Resize)"),
size: (std::mem::size_of::<u32>() * new_size) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
gpu_mut.phase1_visible_indices_buffer =
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Phase 1 Visible Indices Buffer (Incremental Resize)"),
size: (std::mem::size_of::<u32>() * new_size) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
{
let gpu_mut = world_state.gpu_buffers.as_mut().unwrap();
gpu_mut.instance_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Mesh Instance Bind Group"),
layout: &self.instance_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: gpu_mut.transform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: gpu_mut.materials_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: gpu_mut.object_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: gpu_mut.lights_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: gpu_mut.visible_indices_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: gpu_mut.custom_data_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: self.morph_displacement_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: gpu_mut.light_grid_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 8,
resource: gpu_mut.light_indices_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 9,
resource: gpu_mut.cluster_uniforms_buffer.as_entire_binding(),
},
],
});
gpu_mut.phase1_instance_bind_group =
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Phase 1 Instance Bind Group"),
layout: &self.instance_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: gpu_mut.transform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: gpu_mut.materials_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: gpu_mut.object_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: gpu_mut.lights_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 4,
resource: gpu_mut
.phase1_visible_indices_buffer
.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 5,
resource: gpu_mut.custom_data_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 6,
resource: self.morph_displacement_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 7,
resource: gpu_mut.light_grid_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 8,
resource: gpu_mut.light_indices_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 9,
resource: gpu_mut.cluster_uniforms_buffer.as_entire_binding(),
},
],
});
gpu_mut.culling_bind_group = None;
gpu_mut.phase1_culling_bind_group = None;
gpu_mut.transform_compute_bind_group = None;
gpu_mut.instanced_compute_bind_group = None;
}
}
}
let gpu = world_state.gpu_buffers.as_ref().unwrap();
let batches_invalidated = self
.frame_dirty
.as_ref()
.is_some_and(|fd| fd.batches_invalidated);
let needs_rebatch = batches_invalidated
|| !reused_indices.is_empty()
|| !generic_reused_indices.is_empty()
|| !truly_appended_indices.is_empty();
if !truly_appended_indices.is_empty() {
let insert_at = world_state.regular_object_count as usize;
let instanced_slot_count = current_object_count.saturating_sub(insert_at);
let appended_count = truly_appended_indices.len();
let new_total = current_object_count + appended_count;
if new_total > world_state.cached_transforms.len() {
world_state
.cached_transforms
.resize(new_total, bytemuck::Zeroable::zeroed());
world_state
.cached_objects
.resize(new_total, bytemuck::Zeroable::zeroed());
world_state
.cached_custom_data
.resize(new_total, [1.0, 1.0, 1.0, 1.0]);
world_state.cached_entities.resize(
new_total,
crate::ecs::world::Entity {
id: 0,
generation: 0,
},
);
}
if world_state.gpu_registry.slot_to_entity.len() < new_total {
world_state
.gpu_registry
.slot_to_entity
.resize(new_total, None);
}
if instanced_slot_count > 0 {
world_state.cached_transforms.copy_within(
insert_at..insert_at + instanced_slot_count,
insert_at + appended_count,
);
world_state.cached_objects.copy_within(
insert_at..insert_at + instanced_slot_count,
insert_at + appended_count,
);
world_state.cached_custom_data.copy_within(
insert_at..insert_at + instanced_slot_count,
insert_at + appended_count,
);
for slot_index in (insert_at + appended_count)
..(insert_at + appended_count + instanced_slot_count)
{
world_state.cached_objects[slot_index].transform_index = slot_index as u32;
}
let shift = appended_count as u32;
for batch in &mut world_state.instanced_opaque_batches {
batch.2 += shift;
batch.3 += shift;
}
for batch in &mut world_state.instanced_opaque_double_sided_batches {
batch.2 += shift;
batch.3 += shift;
}
for batch in &mut world_state.instanced_transparent_batches {
batch.2 += shift;
batch.3 += shift;
}
}
for (local_index, &classified_index) in truly_appended_indices.iter().enumerate() {
let info = &classified[classified_index];
let slot = (insert_at + local_index) as u32;
let mesh_data = &self.mesh_data[info.mesh_id as usize];
let model_matrix = ModelMatrix {
model: info.transform.into(),
normal_matrix: compute_normal_matrix(&info.transform),
};
let object_data = ObjectData {
transform_index: slot,
mesh_id: info.mesh_id,
material_id: info.material_id,
batch_id: 0,
morph_weights: [0.0f32; 8],
morph_target_count: mesh_data.morph_target_count,
morph_displacement_offset: mesh_data.morph_displacement_offset,
mesh_vertex_offset: mesh_data.vertex_offset,
mesh_vertex_count: mesh_data.vertex_count,
entity_id: info.entity.id,
is_overlay: 0,
skip_occlusion: u32::from(info.is_transparent),
_padding0: 0,
};
world_state.cached_transforms[slot as usize] = model_matrix;
world_state.cached_objects[slot as usize] = object_data;
world_state.cached_custom_data[slot as usize] = info.custom_data;
world_state.cached_entities[slot as usize] = info.entity;
world_state
.gpu_registry
.entity_to_slot
.insert(info.entity, slot);
world_state.gpu_registry.slot_to_entity[slot as usize] = Some(info.entity);
world_state.gpu_registry.slot_count =
world_state.gpu_registry.slot_count.max(slot + 1);
world_state
.entity_to_transform_index
.insert(info.entity, slot);
world_state
.cached_material_map
.insert(info.entity, info.material_id);
}
{
let append_end = insert_at + appended_count;
let transform_offset =
(insert_at as u64) * std::mem::size_of::<ModelMatrix>() as u64;
queue.write_buffer(
&gpu.transform_buffer,
transform_offset,
bytemuck::cast_slice(&world_state.cached_transforms[insert_at..append_end]),
);
let object_offset = (insert_at as u64) * std::mem::size_of::<ObjectData>() as u64;
queue.write_buffer(
&gpu.object_buffer,
object_offset,
bytemuck::cast_slice(&world_state.cached_objects[insert_at..append_end]),
);
let custom_offset = (insert_at as u64) * std::mem::size_of::<[f32; 4]>() as u64;
queue.write_buffer(
&gpu.custom_data_buffer,
custom_offset,
bytemuck::cast_slice(&world_state.cached_custom_data[insert_at..append_end]),
);
}
if instanced_slot_count > 0 {
let shifted_start = insert_at + appended_count;
let shifted_end = shifted_start + instanced_slot_count;
let transform_offset =
(shifted_start as u64) * std::mem::size_of::<ModelMatrix>() as u64;
queue.write_buffer(
&gpu.transform_buffer,
transform_offset,
bytemuck::cast_slice(
&world_state.cached_transforms[shifted_start..shifted_end],
),
);
let custom_offset = (shifted_start as u64) * std::mem::size_of::<[f32; 4]>() as u64;
queue.write_buffer(
&gpu.custom_data_buffer,
custom_offset,
bytemuck::cast_slice(
&world_state.cached_custom_data[shifted_start..shifted_end],
),
);
}
world_state.object_count += appended_count as u32;
world_state.regular_object_count += appended_count as u32;
}
if needs_rebatch {
let regular_count = world_state.regular_object_count as usize;
let overlay_opaque_instances =
std::mem::take(&mut world_state.overlay_opaque_instances);
let overlay_opaque_double_sided_instances =
std::mem::take(&mut world_state.overlay_opaque_double_sided_instances);
let overlay_transparent_instances =
std::mem::take(&mut world_state.overlay_transparent_instances);
world_state.opaque_instances.clear();
world_state.opaque_double_sided_instances.clear();
world_state.transparent_instances.clear();
let transparent_mat_ids = &world_state.cached_transparent_material_ids;
let double_sided_mat_ids = &world_state.cached_double_sided_material_ids;
for index in 0..regular_count.min(world_state.cached_objects.len()) {
if world_state
.gpu_registry
.slot_to_entity
.get(index)
.copied()
.flatten()
.is_none()
{
continue;
}
let material_id = world_state.cached_objects[index].material_id;
let is_transparent = transparent_mat_ids.contains(&material_id);
let is_double_sided = double_sided_mat_ids.contains(&material_id);
world_state.cached_objects[index].skip_occlusion = u32::from(is_transparent);
let object_index = index as u32;
let mesh_id = world_state.cached_objects[index].mesh_id;
let batch_list = if is_transparent {
&mut world_state.transparent_instances
} else if is_double_sided {
&mut world_state.opaque_double_sided_instances
} else {
&mut world_state.opaque_instances
};
if let Some(last) = batch_list.last_mut() {
if last.0 == mesh_id && last.1 == material_id && last.3 == object_index {
last.3 = object_index + 1;
} else {
batch_list.push((mesh_id, material_id, object_index, object_index + 1));
}
} else {
batch_list.push((mesh_id, material_id, object_index, object_index + 1));
}
}
world_state.overlay_opaque_instances = overlay_opaque_instances;
world_state.overlay_opaque_double_sided_instances =
overlay_opaque_double_sided_instances;
world_state.overlay_transparent_instances = overlay_transparent_instances;
let all_regular_batches: Vec<BatchRange> = world_state
.opaque_instances
.iter()
.chain(world_state.opaque_double_sided_instances.iter())
.chain(world_state.transparent_instances.iter())
.cloned()
.collect();
for (batch_index, &(_, _, start, end)) in all_regular_batches.iter().enumerate() {
let batch_id = batch_index as u32;
for slot in start..end {
if (slot as usize) < world_state.cached_objects.len() {
world_state.cached_objects[slot as usize].batch_id = batch_id;
}
}
}
let overlay_batch_start = all_regular_batches.len() as u32;
let all_overlay_batches: Vec<BatchRange> = world_state
.overlay_opaque_instances
.iter()
.chain(world_state.overlay_opaque_double_sided_instances.iter())
.chain(world_state.overlay_transparent_instances.iter())
.cloned()
.collect();
for (batch_index, &(_, _, start, end)) in all_overlay_batches.iter().enumerate() {
let batch_id = overlay_batch_start + batch_index as u32;
for slot in start..end {
if (slot as usize) < world_state.cached_objects.len() {
world_state.cached_objects[slot as usize].batch_id = batch_id;
}
}
}
let instanced_batch_start = overlay_batch_start + all_overlay_batches.len() as u32;
let all_instanced_batches: Vec<BatchRange> = world_state
.instanced_opaque_batches
.iter()
.chain(world_state.instanced_opaque_double_sided_batches.iter())
.chain(world_state.instanced_transparent_batches.iter())
.cloned()
.collect();
for (batch_index, &(_, _, start, end)) in all_instanced_batches.iter().enumerate() {
let batch_id = instanced_batch_start + batch_index as u32;
for slot in start..end {
if (slot as usize) < world_state.cached_objects.len() {
world_state.cached_objects[slot as usize].batch_id = batch_id;
}
}
}
{
let gpu = world_state.gpu_buffers.as_ref().unwrap();
queue.write_buffer(
&gpu.object_buffer,
0,
bytemuck::cast_slice(&world_state.cached_objects),
);
}
let all_instances: Vec<_> = world_state
.opaque_instances
.iter()
.chain(world_state.opaque_double_sided_instances.iter())
.chain(world_state.transparent_instances.iter())
.chain(world_state.overlay_opaque_instances.iter())
.chain(world_state.overlay_opaque_double_sided_instances.iter())
.chain(world_state.overlay_transparent_instances.iter())
.chain(world_state.instanced_opaque_batches.iter())
.chain(world_state.instanced_opaque_double_sided_batches.iter())
.chain(world_state.instanced_transparent_batches.iter())
.cloned()
.collect();
let total_batch_count = all_instances.len();
let total_object_count = world_state.object_count as usize;
if total_batch_count
> world_state
.gpu_buffers
.as_ref()
.unwrap()
.indirect_buffer_size
{
let new_size = std::cmp::min(
(total_batch_count as f32 * BUFFER_GROWTH_FACTOR).ceil() as usize,
MAX_INSTANCES,
);
let gpu_mut = world_state.gpu_buffers.as_mut().unwrap();
gpu_mut.indirect_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Indirect Draw Buffer (Resized)"),
size: (std::mem::size_of::<DrawIndexedIndirect>() * new_size) as u64,
usage: wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
gpu_mut.indirect_buffer_size = new_size;
gpu_mut.phase1_indirect_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Phase 1 Indirect Buffer (Resized)"),
size: (std::mem::size_of::<DrawIndexedIndirect>() * new_size) as u64,
usage: wgpu::BufferUsages::INDIRECT
| wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
gpu_mut.indirect_reset_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Indirect Reset Buffer (Resized)"),
size: (std::mem::size_of::<DrawIndexedIndirect>() * new_size) as u64,
usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
gpu_mut.phase1_indirect_reset_buffer =
device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Phase 1 Indirect Reset Buffer (Resized)"),
size: (std::mem::size_of::<DrawIndexedIndirect>() * new_size) as u64,
usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
gpu_mut.culling_bind_group = None;
gpu_mut.phase1_culling_bind_group = None;
}
let gpu = world_state.gpu_buffers.as_ref().unwrap();
let mut indirect_commands = Vec::with_capacity(total_batch_count);
let mut indirect_reset_commands = Vec::with_capacity(total_batch_count);
let mut visible_indices = Vec::with_capacity(total_object_count);
for &(mesh_id, _material_id, start, end) in all_instances.iter() {
let mesh_data = &self.mesh_data[mesh_id as usize];
let instance_count = end - start;
let first_instance = visible_indices.len() as u32;
indirect_commands.push(DrawIndexedIndirect {
index_count: mesh_data.index_count,
instance_count,
first_index: mesh_data.index_offset,
base_vertex: mesh_data.vertex_offset as i32,
first_instance,
});
indirect_reset_commands.push(DrawIndexedIndirect {
index_count: mesh_data.index_count,
instance_count: 0,
first_index: mesh_data.index_offset,
base_vertex: mesh_data.vertex_offset as i32,
first_instance,
});
for object_index in start..end {
visible_indices.push(object_index);
}
}
queue.write_buffer(
&gpu.indirect_buffer,
0,
bytemuck::cast_slice(&indirect_commands),
);
queue.write_buffer(
&gpu.visible_indices_buffer,
0,
bytemuck::cast_slice(&visible_indices),
);
queue.write_buffer(
&gpu.phase1_visible_indices_buffer,
0,
bytemuck::cast_slice(&visible_indices),
);
queue.write_buffer(
&gpu.indirect_reset_buffer,
0,
bytemuck::cast_slice(&indirect_reset_commands),
);
queue.write_buffer(
&gpu.phase1_indirect_reset_buffer,
0,
bytemuck::cast_slice(&indirect_reset_commands),
);
world_state.indirect_reset_count = total_batch_count;
}
world_state.frames_since_full_rebuild += 1;
let _ = world_state;
}
}