diff --git a/config.toml b/config.toml index 9e6f89b..fbb1c42 100644 --- a/config.toml +++ b/config.toml @@ -1,4 +1,4 @@ -initial_chunk_radius = 5 +initial_chunk_radius = 15 [spawn_counts] dorfs = 5 diff --git a/src/world/mod.rs b/src/world/mod.rs index 9698168..f597c34 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -27,6 +27,7 @@ impl Plugin for WorldPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .add_message::() .add_message::() .add_message::() diff --git a/src/world/tiles/benchmark.rs b/src/world/tiles/benchmark.rs index bac3570..8541a71 100644 --- a/src/world/tiles/benchmark.rs +++ b/src/world/tiles/benchmark.rs @@ -37,6 +37,11 @@ pub struct TilemapBenchmark { pub dirty_keys_total: usize, /// CPU memory estimate for tile data in MB. pub tile_data_mb: f64, + + pub last_z_change_dirty_ms: f64, + pub last_z_change_dirty_count: usize, + pub last_z_change_populate_ms: f64, + pub z_change_history_ms: Vec, } impl Default for TilemapBenchmark { @@ -53,6 +58,10 @@ impl Default for TilemapBenchmark { dirty_keys_last: 0, dirty_keys_total: 0, tile_data_mb: 0.0, + last_z_change_dirty_ms: 0.0, + last_z_change_dirty_count: 0, + last_z_change_populate_ms: 0.0, + z_change_history_ms: Vec::with_capacity(100), } } } @@ -69,7 +78,7 @@ impl TilemapBenchmark { let p50_idx = (self.frame_times.len() as f32 * 0.50) as usize; let p99_idx = (self.frame_times.len() as f32 * 0.99) as usize; let mut sorted = self.frame_times.clone(); - sorted.sort(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); let p50 = sorted .get(p50_idx.min(sorted.len().saturating_sub(1))) .copied() @@ -107,6 +116,35 @@ impl TilemapBenchmark { ); println!("║ Tile data mem: {:.2} MB", self.tile_data_mb); println!("║ Cumulative: {:.2}s", self.populate_ms / 1000.0); + println!("╠══════════════════════════════════════════════════════════════╣"); + let z_p50 = if !self.z_change_history_ms.is_empty() { + let mut sorted = self.z_change_history_ms.clone(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); + sorted[sorted.len() / 2] + } else { + 0.0 + }; + let z_p95 = if !self.z_change_history_ms.is_empty() { + let mut sorted = self.z_change_history_ms.clone(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); + let idx = ((sorted.len() as f32) * 0.95) as usize; + sorted[idx.min(sorted.len().saturating_sub(1))] + } else { + 0.0 + }; + println!("║ Z-scroll spike:"); + println!( + "║ Last dirty marking: {:.2}ms ({} keys)", + self.last_z_change_dirty_ms, self.last_z_change_dirty_count + ); + println!( + "║ Last z-change populate: {:.2}ms", + self.last_z_change_populate_ms + ); + println!( + "║ Z-change populate p50: {:.2}ms p95: {:.2}ms", + z_p50, z_p95 + ); println!("╚══════════════════════════════════════════════════════════════╝"); println!(); } diff --git a/src/world/tiles/tilemap_chunk.rs b/src/world/tiles/tilemap_chunk.rs index 6074dc5..bf8f459 100644 --- a/src/world/tiles/tilemap_chunk.rs +++ b/src/world/tiles/tilemap_chunk.rs @@ -50,6 +50,17 @@ impl Default for TilemapChunkSpawner { } } +pub const MAX_POPULATE_PER_FRAME: usize = 512; + +#[derive(Resource)] +pub struct PreviousZIndex(pub i32); + +impl Default for PreviousZIndex { + fn default() -> Self { + Self(0) + } +} + impl TilemapChunkSpawner { pub fn queue_chunk(&mut self, chunk_pos: IVec2) { for z in 0..=(Z_TOTAL as usize) { @@ -284,18 +295,27 @@ pub fn populate_tilemap_chunk_data( if registry.dirty_keys.is_empty() { return; } + + let was_z_change = bench.last_z_change_dirty_count > 0; let now = Instant::now(); let camera_z = z_index.0 as i32; - let current_z = (camera_z as f32 + Z_BELOW) as usize; + let current_z_index = (camera_z as f32 + Z_BELOW) as usize; + + let mut to_process: Vec = registry.dirty_keys.iter().cloned().collect(); + to_process.sort_by_key(|k| (k.z_index as i32 - current_z_index as i32).unsigned_abs()); + to_process.truncate(MAX_POPULATE_PER_FRAME); + + for key in &to_process { + registry.dirty_keys.remove(key); + } - let dirty: Vec = registry.dirty_keys.drain().collect(); let mut processed = 0usize; - for key in dirty { + for key in to_process { let Some(&entity) = registry.entities.get(&key) else { continue; }; - let Ok((mut tile_data, chunk_key)) = chunk_data_query.get_mut(entity) else { + let Ok((mut tile_data, _)) = chunk_data_query.get_mut(entity) else { continue; }; @@ -320,15 +340,48 @@ pub fn populate_tilemap_chunk_data( bench.dirty_keys_total += processed; bench.tile_data_mb = (registry.entities.len() as f64 * (CHUNK_SIZE * CHUNK_SIZE) as f64 * 4.0) / (1024.0 * 1024.0); + + if was_z_change { + bench.last_z_change_populate_ms = elapsed; + bench.z_change_history_ms.push(elapsed); + if bench.z_change_history_ms.len() > 100 { + bench.z_change_history_ms.remove(0); + } + } } -pub fn on_camera_z_changed(z_index: Res, mut registry: ResMut) { - if z_index.is_changed() { - let keys: Vec = registry.entities.keys().cloned().collect(); - for key in keys { +pub fn on_camera_z_changed( + z_index: Res, + mut registry: ResMut, + mut prev_z: ResMut, + mut bench: ResMut, +) { + if !z_index.is_changed() { + return; + } + + let old_camera_z = prev_z.0; + let new_camera_z = z_index.0 as i32; + prev_z.0 = new_camera_z; + + let now = Instant::now(); + let mut count = 0; + + let keys: Vec = registry.entities.keys().cloned().collect(); + for key in keys { + let tile_z = camera_z_for_z_index(key.z_index); + let old_diff = old_camera_z - tile_z; + let new_diff = new_camera_z - tile_z; + let was_in_range = old_diff >= 0 && old_diff <= 8; + let is_in_range = new_diff >= 0 && new_diff <= 8; + if was_in_range || is_in_range { registry.dirty_keys.insert(key); + count += 1; } } + + bench.last_z_change_dirty_ms = now.elapsed().as_secs_f64() * 1000.0; + bench.last_z_change_dirty_count = count; } pub fn despawn_tilemap_chunks(