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/full.patch b/full.patch new file mode 100644 index 0000000..4f39466 --- /dev/null +++ b/full.patch @@ -0,0 +1,201 @@ +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( diff --git a/src/world/mod.rs b/src/world/mod.rs index f597c34..2238158 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -26,6 +26,7 @@ impl Plugin for WorldPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() .init_resource::() .init_resource::() .add_message::() diff --git a/src/world/tiles/tilemap_chunk.rs b/src/world/tiles/tilemap_chunk.rs index bf8f459..261cf4c 100644 --- a/src/world/tiles/tilemap_chunk.rs +++ b/src/world/tiles/tilemap_chunk.rs @@ -24,6 +24,7 @@ pub struct ChunkLayerKey { pub struct TilemapChunkRegistry { pub entities: HashMap, pub dirty_keys: HashSet, + pub z_change_keys: HashSet, } impl Default for TilemapChunkRegistry { @@ -31,10 +32,17 @@ impl Default for TilemapChunkRegistry { Self { entities: Default::default(), dirty_keys: Default::default(), + z_change_keys: Default::default(), } } } +#[derive(Resource, Default)] +pub struct TilemapChunkStates { + pub solid_bits: HashMap, + pub underground_bits: HashMap, +} + #[derive(Resource)] pub struct TilemapChunkSpawner { pub pending: Vec, @@ -146,15 +154,14 @@ fn populate_chunk_tiles( chunk_pos: IVec2, z_index: usize, camera_z: i32, -) -> Vec> { +) -> (Vec>, u64) { let z_diff = i32::max(camera_z - camera_z_for_z_index(z_index), 0); - if z_diff > 8 { - return vec![None; (CHUNK_SIZE * CHUNK_SIZE) as usize]; - } - let mut tiles = Vec::with_capacity((CHUNK_SIZE * CHUNK_SIZE) as usize); + let mut underground_bits: u64 = 0; + for local_y in 0..CHUNK_SIZE { for local_x in 0..CHUNK_SIZE { + let slot_idx = local_y * CHUNK_SIZE + local_x; let world_pos = IVec3::new( chunk_pos.x * CHUNK_SIZE_TILE + local_x * ITILE_SIZE, chunk_pos.y * CHUNK_SIZE_TILE + local_y * ITILE_SIZE, @@ -166,6 +173,10 @@ fn populate_chunk_tiles( let idx = tile_id_to_tileset_index(t.id as u32); if let Some(tileset_idx) = idx { let is_visible = is_tile_visible_at_z(t, z_index); + let is_underground = !is_visible && t.id != 0; + if is_underground { + underground_bits |= 1u64 << slot_idx; + } let tile_data = if is_visible { tile_for_depth(tileset_idx, z_diff) } else if t.id == 0 { @@ -186,7 +197,42 @@ fn populate_chunk_tiles( } } } - tiles + (tiles, underground_bits) +} + +fn recolor_chunk_for_depth( + tile_data: &mut TilemapChunkTileData, + z_diff: i32, + solid_bits: u64, + underground_bits: u64, +) { + for (i, slot) in tile_data.0.iter_mut().enumerate() { + let Some(td) = slot else { continue }; + let is_underground = (underground_bits >> i) & 1 == 1; + td.color = if is_underground { + Color::BLACK + } else { + tile_fade_color(td.tileset_index, z_diff) + }; + } +} + +fn tile_fade_color(tileset_idx: u16, z_diff: i32) -> Color { + if z_diff <= 0 { + return Color::WHITE; + } + let n = (z_diff as f32).min(8_f32); + let t = 1_f32 - (1_f32 - LAYER_ALPHA).powf(n); + let sky_r = 133_f32 / 255_f32; + let sky_g = 167_f32 / 255_f32; + let sky_b = 178_f32 / 255_f32; + if t >= 0.98 || tileset_idx == 0 { + return Color::srgb(sky_r, sky_g, sky_b); + } + let r = (1_f32 - t) + sky_r * t; + let g = (1_f32 - t) + sky_g * t; + let b = (1_f32 - t) + sky_b * t; + Color::srgb(r, g, b) } pub fn spawn_tilemap_chunks( @@ -194,6 +240,7 @@ pub fn spawn_tilemap_chunks( tilemap: Res, tileset: Res, mut registry: ResMut, + mut states: ResMut, mut spawner: ResMut, z_index: Res, mut bench: ResMut, @@ -202,6 +249,9 @@ pub fn spawn_tilemap_chunks( if !spawner.started && !registry.entities.is_empty() { registry.entities.clear(); registry.dirty_keys.clear(); + registry.z_change_keys.clear(); + states.solid_bits.clear(); + states.underground_bits.clear(); spawner.started = true; } return; @@ -217,13 +267,16 @@ pub fn spawn_tilemap_chunks( for key in spawner.pending.drain(..) { let z_diff = i32::max(camera_z - camera_z_for_z_index(key.z_index), 0); - let tile_data = if key.layer == LAYER_FLOOR { - populate_chunk_tiles(&tilemap, key.chunk_pos, key.z_index, camera_z) + let (tile_data, underground_bits) = if key.layer == LAYER_FLOOR { + let (tiles, ub) = populate_chunk_tiles(&tilemap, key.chunk_pos, key.z_index, camera_z); + let solid_bits = compute_solid_bits(&tiles); + states.solid_bits.insert(key, solid_bits); + states.underground_bits.insert(key, ub); + (tiles, ub) } else { - vec![None; (CHUNK_SIZE * CHUNK_SIZE) as usize] + (vec![None; (CHUNK_SIZE * CHUNK_SIZE) as usize], 0u64) }; - // Fixed let half_chunk = (CHUNK_SIZE_TILE as f32) / 2.0 - TILE_SIZE / 2.0; let world_x = (key.chunk_pos.x as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk; let world_y = (key.chunk_pos.y as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk; @@ -264,6 +317,18 @@ pub fn spawn_tilemap_chunks( bench.populate_ms += now.elapsed().as_secs_f64() * 1000.0; } +fn compute_solid_bits(tiles: &[Option]) -> u64 { + let mut bits: u64 = 0; + for (i, slot) in tiles.iter().enumerate() { + if let Some(td) = slot { + if td.tileset_index != 0 { + bits |= 1u64 << i; + } + } + } + bits +} + pub fn update_tilemap_chunk_visibility( z_index: Res, mut query: Query<(&ChunkLayerKey, &mut Visibility)>, @@ -287,50 +352,92 @@ pub fn update_tilemap_chunk_visibility( pub fn populate_tilemap_chunk_data( mut registry: ResMut, + mut states: ResMut, tilemap: Res, z_index: Res, mut chunk_data_query: Query<(&mut TilemapChunkTileData, &ChunkLayerKey)>, mut bench: ResMut, ) { - 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_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 mut processed = 0usize; - for key in to_process { - let Some(&entity) = registry.entities.get(&key) else { - continue; - }; - let Ok((mut tile_data, _)) = chunk_data_query.get_mut(entity) else { - continue; - }; + if !registry.z_change_keys.is_empty() { + let current_z_index = (camera_z as f32 + Z_BELOW) as usize; + let mut z_keys: Vec = registry.z_change_keys.drain().collect(); + z_keys.sort_by_key(|k| (k.z_index as i32 - current_z_index as i32).unsigned_abs()); + z_keys.truncate(MAX_POPULATE_PER_FRAME); - if key.layer != LAYER_FLOOR { - continue; + for key in &z_keys { + registry.z_change_keys.remove(key); } - let z_diff = i32::max(camera_z - camera_z_for_z_index(key.z_index), 0); - if z_diff > 8 { - continue; + bench.last_z_change_dirty_count = registry.entities.len(); + + for key in z_keys { + let Some(&entity) = registry.entities.get(&key) else { + continue; + }; + let Ok((mut tile_data, _)) = chunk_data_query.get_mut(entity) else { + continue; + }; + if key.layer != LAYER_FLOOR { + continue; + } + + let z_diff = i32::max(camera_z - camera_z_for_z_index(key.z_index), 0); + if z_diff > 8 { + continue; + } + + let solid_bits = states.solid_bits.get(&key).copied().unwrap_or(0); + let underground_bits = states.underground_bits.get(&key).copied().unwrap_or(0); + recolor_chunk_for_depth(&mut tile_data, z_diff, solid_bits, underground_bits); + processed += 1; } - let new_tiles = populate_chunk_tiles(&tilemap, key.chunk_pos, key.z_index, camera_z); - tile_data.0 = new_tiles; - processed += 1; + let elapsed = now.elapsed().as_secs_f64() * 1000.0; + 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); + } + } + + if !registry.dirty_keys.is_empty() { + 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); + } + + for key in to_process { + let Some(&entity) = registry.entities.get(&key) else { + continue; + }; + let Ok((mut tile_data, _)) = chunk_data_query.get_mut(entity) else { + continue; + }; + if key.layer != LAYER_FLOOR { + continue; + } + + let z_diff = i32::max(camera_z - camera_z_for_z_index(key.z_index), 0); + if z_diff > 8 { + continue; + } + + let (new_tiles, underground_bits) = + populate_chunk_tiles(&tilemap, key.chunk_pos, key.z_index, camera_z); + let solid_bits = compute_solid_bits(&new_tiles); + states.solid_bits.insert(key, solid_bits); + states.underground_bits.insert(key, underground_bits); + tile_data.0 = new_tiles; + processed += 1; + } } let elapsed = now.elapsed().as_secs_f64() * 1000.0; @@ -340,14 +447,6 @@ 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( @@ -365,21 +464,34 @@ pub fn on_camera_z_changed( prev_z.0 = new_camera_z; let now = Instant::now(); + + let old_entered_idx = (old_camera_z as f32 + Z_BELOW) as usize; + let old_exited_idx = ((old_camera_z - 8) as f32 + Z_BELOW) as usize; + let new_entered_idx = (new_camera_z as f32 + Z_BELOW) as usize; + let new_exited_idx = ((new_camera_z - 8) as f32 + Z_BELOW) as usize; + 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); + let mut new_keys: Vec = Vec::new(); + + for key in registry.entities.keys() { + if key.z_index == old_entered_idx + || key.z_index == new_entered_idx + || key.z_index == old_exited_idx + || key.z_index == new_exited_idx + { + new_keys.push(*key); count += 1; } } + if !new_keys.is_empty() { + registry.z_change_keys.clear(); + for key in new_keys { + registry.z_change_keys.insert(key); + } + } + bench.last_z_change_dirty_ms = now.elapsed().as_secs_f64() * 1000.0; bench.last_z_change_dirty_count = count; } @@ -388,6 +500,7 @@ pub fn despawn_tilemap_chunks( mut commands: Commands, chunk_map: Res, mut registry: ResMut, + mut states: ResMut, query: Query<(Entity, &ChunkLayerKey)>, ) { let loaded: HashSet = chunk_map.loaded_chunks.keys().cloned().collect(); @@ -396,6 +509,10 @@ pub fn despawn_tilemap_chunks( if !loaded.contains(&key.chunk_pos) { commands.entity(entity).despawn(); registry.entities.remove(key); + registry.dirty_keys.remove(key); + registry.z_change_keys.remove(key); + states.solid_bits.remove(key); + states.underground_bits.remove(key); } } }