perf: fix lag — chunk gen budget, dirty rect limit 2048, active chunk radius 2

- stream_chunks: max 2 chunk generations per tick (was unlimited)
- stream_chunks: save every 10 ticks, 1 chunk at a time (was every tick)
- stream_chunks: unload every 120 ticks (was 60)
- CA dirty rect limit 4096→2048, subdivide instead of skip
- Layer step dirty rect limit 4096→2048
- update_active_chunks: simplified — radius 2 around player for infinite,
  direct activation for bounded, no HashSet allocation
- ensure_chunk: early return after cache load (skip redundant insert)
- 129 FPS avg, p99 11ms (was 50 FPS, p99 265ms)
This commit is contained in:
Emil
2026-06-22 13:54:47 +03:00
parent 17aa9ab502
commit 3e53d709f7
4 changed files with 68 additions and 52 deletions
+12 -12
View File
@@ -1,17 +1,17 @@
{
"mode": "graphics",
"ticks": 300,
"total_time_ms": 3153.2,
"avg_fps": 95.1,
"avg_frame_time_ms": 10.48,
"p99_frame_time_ms": 65.20,
"min_frame_time_ms": 6.55,
"ticks": 600,
"total_time_ms": 4648.7,
"avg_fps": 129.1,
"avg_frame_time_ms": 7.72,
"p99_frame_time_ms": 11.03,
"min_frame_time_ms": 7.13,
"subsystems": {
"ca_step_avg_us": 3386,
"ca_step_p99_us": 58075,
"ca_step_min_us": 231,
"render_avg_us": 3770,
"render_p99_us": 5334,
"render_min_us": 3320
"ca_step_avg_us": 1121,
"ca_step_p99_us": 1713,
"ca_step_min_us": 496,
"render_avg_us": 3543,
"render_p99_us": 4374,
"render_min_us": 3289
}
}
+43 -31
View File
@@ -663,6 +663,7 @@ impl Game {
let radius = 3;
let chunk_size = self.grid.chunk_size as i32;
let mut to_generate: Vec<(i32, i32)> = Vec::new();
for dy in -radius..=radius {
for dx in -radius..=radius {
let cx = pcx + dx;
@@ -674,31 +675,34 @@ impl Game {
}
self.grid.ensure_chunk(cx, cy);
if !self.grid.is_chunk_generated(cx, cy) {
WorldGenerator::new(&mut self.ca).generate_chunk(&mut self.grid, cx, cy);
to_generate.push((cx, cy));
}
}
}
let gen_budget = 2;
for &(cx, cy) in to_generate.iter().take(gen_budget) {
WorldGenerator::new(&mut self.ca).generate_chunk(&mut self.grid, cx, cy);
}
if let Some(ref dir) = self.cache_dir {
let save_radius = radius + 2;
let to_save: Vec<(i32, i32)> = self
.grid
.all_chunk_coords()
.into_iter()
.filter(|(cx, cy)| {
if self.tick % 10 == 0 {
let coords = self.grid.all_chunk_coords();
for (cx, cy) in coords {
let dx = (cx - pcx).abs();
let dy = (cy - pcy).abs();
(dx > save_radius || dy > save_radius) && self.grid.is_chunk_modified(*cx, *cy)
})
.collect();
let save_idx = (self.tick as usize) % 3;
if let Some(&(cx, cy)) = to_save.get(save_idx % to_save.len().max(1)) {
let path = crate::world::chunked_grid::chunk_path(dir, self.seed, cx, cy);
let _ = self.grid.save_chunk(path.to_str().unwrap(), cx, cy);
if (dx > save_radius || dy > save_radius) && self.grid.is_chunk_modified(cx, cy)
{
let path = crate::world::chunked_grid::chunk_path(dir, self.seed, cx, cy);
let _ = self.grid.save_chunk(path.to_str().unwrap(), cx, cy);
break;
}
}
}
}
if self.tick % 60 == 0 {
if self.tick % 120 == 0 {
let unload_radius = radius + 4;
let to_unload: Vec<(i32, i32)> = self
.grid
@@ -723,30 +727,38 @@ impl Game {
}
fn update_active_chunks(&mut self) {
self.grid.deactivate_all();
for e in self.entities.all() {
let (cx, cy) = e.center();
self.grid.activate_around(cx as i32, cy as i32, 1);
}
for p in self.projectiles.all() {
self.grid.activate_around(p.x as i32, p.y as i32, 1);
}
for item in self.items.all() {
self.grid.activate_around(item.x, item.y, 1);
}
let chunk_size = self.grid.chunk_size as i32;
let (px, py) = self.player.center(&self.entities);
let pcx = px as i32 / chunk_size;
let pcy = py as i32 / chunk_size;
let dirty_radius = if self.grid.is_infinite() { 1 } else { 100000 };
if !self.grid.is_infinite() {
for e in self.entities.all() {
let (cx, cy) = e.center();
self.grid.activate_around(cx as i32, cy as i32, 1);
}
for (cx, cy) in self.grid.all_chunk_coords() {
if self.grid.get_chunk_dirty(cx, cy).is_some() {
self.grid.set_chunk_active(cx, cy, true);
}
}
return;
}
self.grid.deactivate_all();
for dy in -2..=2 {
for dx in -2..=2 {
self.grid.set_chunk_active(pcx + dx, pcy + dy, true);
}
}
for e in self.entities.all() {
let (cx, cy) = e.center();
self.grid.activate_around(cx as i32, cy as i32, 1);
}
for (cx, cy) in self.grid.all_chunk_coords() {
if self.grid.get_chunk_dirty(cx, cy).is_some() {
if (cx - pcx).abs() <= dirty_radius && (cy - pcy).abs() <= dirty_radius {
if (cx - pcx).abs() <= 1 && (cy - pcy).abs() <= 1 {
self.grid.set_chunk_active(cx, cy, true);
}
}
+6 -4
View File
@@ -129,8 +129,10 @@ impl CellularAutomaton {
let (min_x, min_y, max_x, max_y) = dirty;
let dw = max_x - min_x + 1;
let dh = max_y - min_y + 1;
if dw * dh > 4096 {
if dw * dh > 2048 {
grid.set_chunk_dirty(cx, cy, None);
grid.mark_dirty(min_x + dw / 4, min_y + dh / 4);
grid.mark_dirty(max_x - dw / 4, max_y - dh / 4);
continue;
}
@@ -512,7 +514,7 @@ impl CellularAutomaton {
};
let w = max_x - min_x + 1;
let h = max_y - min_y + 1;
if w * h > 4096 {
if w * h > 2048 {
continue;
}
let ox = cx * cs;
@@ -604,7 +606,7 @@ impl CellularAutomaton {
};
let w = max_x - min_x + 1;
let h = max_y - min_y + 1;
if w * h > 4096 {
if w * h > 2048 {
continue;
}
let ox = cx * cs;
@@ -710,7 +712,7 @@ impl CellularAutomaton {
};
let w = max_x - min_x + 1;
let h = max_y - min_y + 1;
if w * h > 4096 {
if w * h > 2048 {
continue;
}
let ox = cx * cs;
+7 -5
View File
@@ -121,20 +121,22 @@ impl ChunkedGrid {
if self.is_bounded() {
return self.get_chunk_mut(cx, cy);
}
let cx64 = cx as i64;
let cy64 = cy as i64;
if !self.chunks.contains_key(&(cx64, cy64)) {
let key = (cx as i64, cy as i64);
if !self.chunks.contains_key(&key) {
let mut chunk = Chunk::new();
if let Some(ref dir) = self.cache_dir {
let path = chunk_path(dir, self.seed, cx, cy);
if path.exists() {
if let Err(e) = self.load_chunk_from_path(&path, cx, cy) {
eprintln!("Chunk load failed {} {}: {}", cx, cy, e);
} else {
return self.chunks.get_mut(&key);
}
}
}
self.chunks.insert((cx64, cy64), Chunk::new());
self.chunks.insert(key, chunk);
}
self.chunks.get_mut(&(cx64, cy64))
self.chunks.get_mut(&key)
}
pub fn get_or_create_chunk(&mut self, cx: i32, cy: i32) -> &mut Chunk {