Improve chunk queue performance

This commit is contained in:
Elias Stepanik 2025-06-09 11:13:23 +02:00
parent 366381286a
commit d56b2857d6
5 changed files with 39 additions and 32 deletions

View File

@ -6,7 +6,7 @@ use crate::plugins::environment::systems::voxels::queue_systems;
use crate::plugins::environment::systems::voxels::queue_systems::{enqueue_visible_chunks, process_chunk_queue}; use crate::plugins::environment::systems::voxels::queue_systems::{enqueue_visible_chunks, process_chunk_queue};
use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks; use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks;
use crate::plugins::environment::systems::voxels::lod::update_chunk_lods; use crate::plugins::environment::systems::voxels::lod::update_chunk_lods;
use crate::plugins::environment::systems::voxels::structure::{ChunkBudget, ChunkCullingCfg, ChunkQueue, SparseVoxelOctree, SpawnedChunks, ChunkOffsets, PrevCameraChunk}; use crate::plugins::environment::systems::voxels::structure::{ChunkBudget, ChunkCullingCfg, ChunkQueue, SparseVoxelOctree, SpawnedChunks, PrevCameraChunk};
pub struct EnvironmentPlugin; pub struct EnvironmentPlugin;
impl Plugin for EnvironmentPlugin { impl Plugin for EnvironmentPlugin {
@ -25,7 +25,6 @@ impl Plugin for EnvironmentPlugin {
let view_distance_chunks = 100; let view_distance_chunks = 100;
app.insert_resource(ChunkCullingCfg { view_distance_chunks }); app.insert_resource(ChunkCullingCfg { view_distance_chunks });
app.insert_resource(ChunkBudget { per_frame: 20 }); app.insert_resource(ChunkBudget { per_frame: 20 });
app.insert_resource(ChunkOffsets::new(view_distance_chunks));
app.init_resource::<PrevCameraChunk>(); app.init_resource::<PrevCameraChunk>();
app.add_systems(Update, log_mesh_count); app.add_systems(Update, log_mesh_count);
app app

View File

@ -11,31 +11,28 @@ pub fn update_chunk_lods(
cfg: Res<ChunkCullingCfg>, cfg: Res<ChunkCullingCfg>,
) { ) {
let cam_pos = cam_q.single().translation(); let cam_pos = cam_q.single().translation();
let (max_depth, range_step, chunk_size);
{ // Borrow the octree only once to avoid repeated query lookups
let tree = tree_q.single(); let mut tree = tree_q.single_mut();
max_depth = tree.max_depth; let max_depth = tree.max_depth;
range_step = cfg.view_distance_chunks as f32 / max_depth as f32; let range_step = cfg.view_distance_chunks as f32 / max_depth as f32;
chunk_size = CHUNK_SIZE as f32 * tree.get_spacing_at_depth(max_depth); let chunk_size = CHUNK_SIZE as f32 * tree.get_spacing_at_depth(max_depth);
}
let mut changed = Vec::new(); let mut changed = Vec::new();
for (chunk, mut lod) in chunks.iter_mut() { for (chunk, mut lod) in chunks.iter_mut() {
let tree = tree_q.single();
let center = chunk_center_world(&tree, chunk.key); let center = chunk_center_world(&tree, chunk.key);
let dist_chunks = cam_pos.distance(center) / chunk_size; let dist_chunks = cam_pos.distance(center) / chunk_size;
let mut level = (dist_chunks / range_step).floor() as u32; let mut level = (dist_chunks / range_step).floor() as u32;
if level > max_depth { level = max_depth; } if level > max_depth {
level = max_depth;
}
if lod.0 != level { if lod.0 != level {
lod.0 = level; lod.0 = level;
changed.push(chunk.key); changed.push(chunk.key);
} }
} }
if !changed.is_empty() { for key in changed {
let mut tree = tree_q.single_mut(); tree.dirty_chunks.insert(key);
for key in changed {
tree.dirty_chunks.insert(key);
}
} }
} }

View File

@ -310,6 +310,7 @@ pub(crate) fn mesh_chunk(
// ──────────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────────
const N: usize = CHUNK_SIZE as usize; const N: usize = CHUNK_SIZE as usize;
const MASK_LEN: usize = N * N;
// Safe voxel query that falls back to the octree for outofchunk requests. // Safe voxel query that falls back to the octree for outofchunk requests.
let filled = |x: i32, y: i32, z: i32| -> bool { let filled = |x: i32, y: i32, z: i32| -> bool {
@ -327,10 +328,12 @@ pub(crate) fn mesh_chunk(
// Push a single quad (4 vertices, 6 indices). `base` is the lowerleft // Push a single quad (4 vertices, 6 indices). `base` is the lowerleft
// corner in world space; `u`/`v` are the tangent vectors (length 1); `size` // corner in world space; `u`/`v` are the tangent vectors (length 1); `size`
// is expressed in world units along those axes; `n` is the face normal. // is expressed in world units along those axes; `n` is the face normal.
let mut positions = Vec::<[f32; 3]>::new(); // Preallocate vertex buffers for better performance
let mut normals = Vec::<[f32; 3]>::new(); let voxel_count = N * N * N;
let mut uvs = Vec::<[f32; 2]>::new(); let mut positions = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
let mut indices = Vec::<u32>::new(); let mut normals = Vec::<[f32; 3]>::with_capacity(voxel_count * 4);
let mut uvs = Vec::<[f32; 2]>::with_capacity(voxel_count * 4);
let mut indices = Vec::<u32>::with_capacity(voxel_count * 6);
let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3| { let mut push_quad = |base: Vec3, size: Vec2, n: Vec3, u: Vec3, v: Vec3| {
let i0 = positions.len() as u32; let i0 = positions.len() as u32;
@ -371,8 +374,10 @@ pub(crate) fn mesh_chunk(
// the 0…N grid lines (inclusive) because the positiveside faces of the // the 0…N grid lines (inclusive) because the positiveside faces of the
// last voxel sit at slice N. // last voxel sit at slice N.
for slice in 0..=N { for slice in 0..=N {
// Build the face mask for this slice. // Build the face mask for this slice using a fixed-size array to
let mut mask = vec![false; N * N]; // avoid heap allocations.
let mut mask = [false; MASK_LEN];
let mut visited = [false; MASK_LEN];
let idx = |u: usize, v: usize| -> usize { u * N + v }; let idx = |u: usize, v: usize| -> usize { u * N + v };
for u in 0..N { for u in 0..N {
@ -396,7 +401,6 @@ pub(crate) fn mesh_chunk(
} }
// Greedy merge the mask into maximal rectangles. // Greedy merge the mask into maximal rectangles.
let mut visited = vec![false; N * N];
for u0 in 0..N { for u0 in 0..N {
for v0 in 0..N { for v0 in 0..N {
if !mask[idx(u0, v0)] || visited[idx(u0, v0)] { if !mask[idx(u0, v0)] || visited[idx(u0, v0)] {

View File

@ -10,7 +10,6 @@ pub fn enqueue_visible_chunks(
mut queue : ResMut<ChunkQueue>, mut queue : ResMut<ChunkQueue>,
spawned : Res<SpawnedChunks>, spawned : Res<SpawnedChunks>,
mut prev_cam : ResMut<PrevCameraChunk>, mut prev_cam : ResMut<PrevCameraChunk>,
offsets : Res<ChunkOffsets>,
cfg : Res<ChunkCullingCfg>, cfg : Res<ChunkCullingCfg>,
cam_q : Query<&GlobalTransform, With<Camera>>, cam_q : Query<&GlobalTransform, With<Camera>>,
tree_q : Query<&SparseVoxelOctree>, tree_q : Query<&SparseVoxelOctree>,
@ -24,12 +23,16 @@ pub fn enqueue_visible_chunks(
} }
prev_cam.0 = Some(centre); prev_cam.0 = Some(centre);
for offset in &offsets.0 { let r = cfg.view_distance_chunks;
let key = ChunkKey(centre.0 + offset.x, centre.1 + offset.y, centre.2 + offset.z); for key in &tree.occupied_chunks {
if spawned.0.contains_key(&key) { continue; } let dx = key.0 - centre.0;
if queue.0.contains(&key) { continue; } let dy = key.1 - centre.1;
if !tree.occupied_chunks.contains(&key) { continue; } let dz = key.2 - centre.2;
queue.0.push_back(key); if dx.abs() > r || dy.abs() > r || dz.abs() > r { continue; }
if spawned.0.contains_key(key) { continue; }
if queue.set.contains(key) { continue; }
queue.keys.push_back(*key);
queue.set.insert(*key);
} }
} }
@ -41,7 +44,8 @@ pub fn process_chunk_queue(
) { ) {
let mut tree = tree_q.single_mut(); let mut tree = tree_q.single_mut();
for _ in 0..budget.per_frame { for _ in 0..budget.per_frame {
if let Some(key) = queue.0.pop_front() { if let Some(key) = queue.keys.pop_front() {
queue.set.remove(&key);
tree.dirty_chunks.insert(key); tree.dirty_chunks.insert(key);
} else { break; } } else { break; }
} }

View File

@ -118,7 +118,10 @@ impl Default for ChunkBudget {
/// FIFO queue with chunk keys that still need meshing /// FIFO queue with chunk keys that still need meshing
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct ChunkQueue(pub VecDeque<ChunkKey>); pub struct ChunkQueue {
pub keys: VecDeque<ChunkKey>,
pub set: HashSet<ChunkKey>,
}
/// map “which chunk key already has an entity in the world?” /// map “which chunk key already has an entity in the world?”
#[derive(Resource, Default)] #[derive(Resource, Default)]