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", "mode": "graphics",
"ticks": 300, "ticks": 600,
"total_time_ms": 3153.2, "total_time_ms": 4648.7,
"avg_fps": 95.1, "avg_fps": 129.1,
"avg_frame_time_ms": 10.48, "avg_frame_time_ms": 7.72,
"p99_frame_time_ms": 65.20, "p99_frame_time_ms": 11.03,
"min_frame_time_ms": 6.55, "min_frame_time_ms": 7.13,
"subsystems": { "subsystems": {
"ca_step_avg_us": 3386, "ca_step_avg_us": 1121,
"ca_step_p99_us": 58075, "ca_step_p99_us": 1713,
"ca_step_min_us": 231, "ca_step_min_us": 496,
"render_avg_us": 3770, "render_avg_us": 3543,
"render_p99_us": 5334, "render_p99_us": 4374,
"render_min_us": 3320 "render_min_us": 3289
} }
} }
+43 -31
View File
@@ -663,6 +663,7 @@ impl Game {
let radius = 3; let radius = 3;
let chunk_size = self.grid.chunk_size as i32; let chunk_size = self.grid.chunk_size as i32;
let mut to_generate: Vec<(i32, i32)> = Vec::new();
for dy in -radius..=radius { for dy in -radius..=radius {
for dx in -radius..=radius { for dx in -radius..=radius {
let cx = pcx + dx; let cx = pcx + dx;
@@ -674,31 +675,34 @@ impl Game {
} }
self.grid.ensure_chunk(cx, cy); self.grid.ensure_chunk(cx, cy);
if !self.grid.is_chunk_generated(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 { if let Some(ref dir) = self.cache_dir {
let save_radius = radius + 2; let save_radius = radius + 2;
let to_save: Vec<(i32, i32)> = self if self.tick % 10 == 0 {
.grid let coords = self.grid.all_chunk_coords();
.all_chunk_coords() for (cx, cy) in coords {
.into_iter()
.filter(|(cx, cy)| {
let dx = (cx - pcx).abs(); let dx = (cx - pcx).abs();
let dy = (cy - pcy).abs(); let dy = (cy - pcy).abs();
(dx > save_radius || dy > save_radius) && self.grid.is_chunk_modified(*cx, *cy) if (dx > save_radius || dy > save_radius) && self.grid.is_chunk_modified(cx, cy)
}) {
.collect(); let path = crate::world::chunked_grid::chunk_path(dir, self.seed, cx, cy);
let save_idx = (self.tick as usize) % 3; let _ = self.grid.save_chunk(path.to_str().unwrap(), cx, cy);
if let Some(&(cx, cy)) = to_save.get(save_idx % to_save.len().max(1)) { break;
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 self.tick % 60 == 0 { if self.tick % 120 == 0 {
let unload_radius = radius + 4; let unload_radius = radius + 4;
let to_unload: Vec<(i32, i32)> = self let to_unload: Vec<(i32, i32)> = self
.grid .grid
@@ -723,30 +727,38 @@ impl Game {
} }
fn update_active_chunks(&mut self) { 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 chunk_size = self.grid.chunk_size as i32;
let (px, py) = self.player.center(&self.entities); let (px, py) = self.player.center(&self.entities);
let pcx = px as i32 / chunk_size; let pcx = px as i32 / chunk_size;
let pcy = py 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() { for (cx, cy) in self.grid.all_chunk_coords() {
if self.grid.get_chunk_dirty(cx, cy).is_some() { 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); 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 (min_x, min_y, max_x, max_y) = dirty;
let dw = max_x - min_x + 1; let dw = max_x - min_x + 1;
let dh = max_y - min_y + 1; let dh = max_y - min_y + 1;
if dw * dh > 4096 { if dw * dh > 2048 {
grid.set_chunk_dirty(cx, cy, None); 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; continue;
} }
@@ -512,7 +514,7 @@ impl CellularAutomaton {
}; };
let w = max_x - min_x + 1; let w = max_x - min_x + 1;
let h = max_y - min_y + 1; let h = max_y - min_y + 1;
if w * h > 4096 { if w * h > 2048 {
continue; continue;
} }
let ox = cx * cs; let ox = cx * cs;
@@ -604,7 +606,7 @@ impl CellularAutomaton {
}; };
let w = max_x - min_x + 1; let w = max_x - min_x + 1;
let h = max_y - min_y + 1; let h = max_y - min_y + 1;
if w * h > 4096 { if w * h > 2048 {
continue; continue;
} }
let ox = cx * cs; let ox = cx * cs;
@@ -710,7 +712,7 @@ impl CellularAutomaton {
}; };
let w = max_x - min_x + 1; let w = max_x - min_x + 1;
let h = max_y - min_y + 1; let h = max_y - min_y + 1;
if w * h > 4096 { if w * h > 2048 {
continue; continue;
} }
let ox = cx * cs; let ox = cx * cs;
+7 -5
View File
@@ -121,20 +121,22 @@ impl ChunkedGrid {
if self.is_bounded() { if self.is_bounded() {
return self.get_chunk_mut(cx, cy); return self.get_chunk_mut(cx, cy);
} }
let cx64 = cx as i64; let key = (cx as i64, cy as i64);
let cy64 = cy as i64; if !self.chunks.contains_key(&key) {
if !self.chunks.contains_key(&(cx64, cy64)) { let mut chunk = Chunk::new();
if let Some(ref dir) = self.cache_dir { if let Some(ref dir) = self.cache_dir {
let path = chunk_path(dir, self.seed, cx, cy); let path = chunk_path(dir, self.seed, cx, cy);
if path.exists() { if path.exists() {
if let Err(e) = self.load_chunk_from_path(&path, cx, cy) { if let Err(e) = self.load_chunk_from_path(&path, cx, cy) {
eprintln!("Chunk load failed {} {}: {}", cx, cy, e); 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 { pub fn get_or_create_chunk(&mut self, cx: i32, cy: i32) -> &mut Chunk {