1use std::collections::VecDeque;
4use std::env;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use steel_utils::{ChunkPos, SectionPos};
9use tokio::time::sleep;
10use tokio_util::sync::CancellationToken;
11
12use crate::chunk::chunk_pyramid::GENERATION_PYRAMID;
13use crate::chunk::chunk_request::{
14 ChunkRequest, ChunkRequestHandle, ChunkRequestState, ChunkTicketKind,
15};
16use crate::chunk::status::ChunkStatus;
17use crate::server::Server;
18use crate::world::World;
19
20#[cfg(feature = "slow_chunk_gen")]
21use crate::chunk::chunk_holder::SLOW_CHUNK_GEN;
22#[cfg(feature = "slow_chunk_gen")]
23use std::sync::atomic::Ordering;
24
25const PREGEN_SIZE_ENV: &str = "PREGEN_SIZE";
26const PREGEN_WINDOW_SIZE_ENV: &str = "PREGEN_WINDOW_SIZE";
27const VANILLA_PLAYER_SPAWN_SIZE_CHUNKS: i32 = 7;
28const DEFAULT_PREGEN_WINDOW_SIZE: i32 = 32;
29const PREGEN_ACTIVE_WINDOWS: usize = 2;
30const PREGEN_UNLOAD_BACKPRESSURE_HIGH: usize = 8192;
31const PREGEN_UNLOAD_BACKPRESSURE_LOW: usize = 4096;
32const FULL_DEPENDENCY_RADIUS: i32 = GENERATION_PYRAMID
33 .get_step_to(ChunkStatus::Full)
34 .accumulated_dependencies
35 .get_radius_of(ChunkStatus::Empty) as i32;
36
37#[derive(Clone, Copy, Debug)]
38struct PregenWindow {
39 min_x: i32,
40 max_x: i32,
41 min_z: i32,
42 max_z: i32,
43}
44
45impl PregenWindow {
46 fn positions(self) -> Vec<ChunkPos> {
47 let mut positions = Vec::with_capacity(self.chunk_count());
48 for z in self.min_z..=self.max_z {
49 for x in self.min_x..=self.max_x {
50 positions.push(ChunkPos::new(x, z));
51 }
52 }
53 positions
54 }
55
56 const fn chunk_count(self) -> usize {
57 (self.width() * self.height()) as usize
58 }
59
60 const fn width(self) -> i32 {
61 self.max_x - self.min_x + 1
62 }
63
64 const fn height(self) -> i32 {
65 self.max_z - self.min_z + 1
66 }
67
68 const fn protected_rect(self) -> PregenRect {
69 PregenRect {
70 min_x: self.min_x - FULL_DEPENDENCY_RADIUS,
71 max_x: self.max_x + FULL_DEPENDENCY_RADIUS,
72 min_z: self.min_z - FULL_DEPENDENCY_RADIUS,
73 max_z: self.max_z + FULL_DEPENDENCY_RADIUS,
74 }
75 }
76}
77
78#[derive(Clone, Copy)]
79struct PregenRect {
80 min_x: i32,
81 max_x: i32,
82 min_z: i32,
83 max_z: i32,
84}
85
86impl PregenRect {
87 const fn overlaps(self, other: Self) -> bool {
88 self.min_x <= other.max_x
89 && self.max_x >= other.min_x
90 && self.min_z <= other.max_z
91 && self.max_z >= other.min_z
92 }
93}
94
95struct ActivePregenWindow {
96 window: PregenWindow,
97 handle: ChunkRequestHandle,
98 ready_chunks: usize,
99 ready: bool,
100 counted: bool,
101}
102
103impl ActivePregenWindow {
104 fn new(world: &Arc<World>, window: PregenWindow) -> Self {
105 let handle = world.chunk_map.request_chunks(ChunkRequest {
106 status: ChunkStatus::Full,
107 positions: window.positions(),
108 ticket_kind: ChunkTicketKind::Pregen,
109 });
110 Self {
111 window,
112 handle,
113 ready_chunks: 0,
114 ready: false,
115 counted: false,
116 }
117 }
118
119 fn poll(&mut self, world: &Arc<World>) {
120 match self.handle.poll() {
121 ChunkRequestState::Ready => {
122 self.ready_chunks = self.window.chunk_count();
123 self.ready = true;
124 }
125 ChunkRequestState::Pending { ready, .. } => {
126 self.ready_chunks = ready;
127 }
128 ChunkRequestState::Cancelled => {
129 self.handle = world.chunk_map.request_chunks(ChunkRequest {
130 status: ChunkStatus::Full,
131 positions: self.window.positions(),
132 ticket_kind: ChunkTicketKind::Pregen,
133 });
134 self.ready_chunks = 0;
135 self.ready = false;
136 self.counted = false;
137 }
138 }
139 }
140}
141
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143struct PregenSize {
144 side_length: i32,
145 radius: i32,
146}
147
148impl PregenSize {
149 fn from_side_length(side_length: i32) -> Result<Option<Self>, String> {
150 if side_length == 0 {
151 return Ok(None);
152 }
153 if side_length < 0 {
154 return Err(format!(
155 "{PREGEN_SIZE_ENV} must be 0 or a positive odd integer"
156 ));
157 }
158 if side_length % 2 == 0 {
159 return Err(format!(
160 "{PREGEN_SIZE_ENV} must be odd so the area has a single center chunk"
161 ));
162 }
163
164 Ok(Some(Self {
165 side_length,
166 radius: side_length / 2,
167 }))
168 }
169}
170
171impl Server {
172 pub async fn prepare_spawn_area(&self) -> bool {
178 let overworld = self.overworld();
179 let pregen_size = match get_pregen_size() {
180 Ok(Some(size)) => size,
181 Ok(None) => {
182 log::info!("Skipping custom startup spawn-area pregeneration");
183 return true;
184 }
185 Err(error) => {
186 log::error!("{error}");
187 return false;
188 }
189 };
190 let window_size = match get_pregen_window_size() {
191 Ok(window_size) => window_size,
192 Err(error) => {
193 log::error!("{error}");
194 return false;
195 }
196 };
197 let center_chunk = if pregen_size.side_length > VANILLA_PLAYER_SPAWN_SIZE_CHUNKS {
198 ChunkPos::new(0, 0)
199 } else {
200 let spawn_pos = overworld.level_data.read().data().spawn_pos();
201 ChunkPos::new(
202 SectionPos::block_to_section_coord(spawn_pos.0.x),
203 SectionPos::block_to_section_coord(spawn_pos.0.z),
204 )
205 };
206
207 pregen_overworld(
208 overworld,
209 center_chunk,
210 pregen_size,
211 window_size,
212 &self.cancel_token,
213 )
214 .await
215 }
216}
217
218fn get_pregen_size() -> Result<Option<PregenSize>, String> {
219 let side_length = match env::var(PREGEN_SIZE_ENV) {
220 Ok(value) => value
221 .parse::<i32>()
222 .map_err(|e| format!("{PREGEN_SIZE_ENV} must be 0 or a positive odd integer: {e}"))?,
223 Err(env::VarError::NotPresent) => return Ok(None),
224 Err(env::VarError::NotUnicode(_)) => {
225 return Err(format!("{PREGEN_SIZE_ENV} must be valid unicode"));
226 }
227 };
228
229 PregenSize::from_side_length(side_length)
230}
231
232fn get_pregen_window_size() -> Result<i32, String> {
233 match env::var(PREGEN_WINDOW_SIZE_ENV) {
234 Ok(value) => parse_pregen_window_size(&value),
235 Err(env::VarError::NotPresent) => Ok(DEFAULT_PREGEN_WINDOW_SIZE),
236 Err(env::VarError::NotUnicode(_)) => {
237 Err(format!("{PREGEN_WINDOW_SIZE_ENV} must be valid unicode"))
238 }
239 }
240}
241
242fn parse_pregen_window_size(value: &str) -> Result<i32, String> {
243 let window_size = value
244 .parse::<i32>()
245 .map_err(|error| format!("{PREGEN_WINDOW_SIZE_ENV} must be a positive integer: {error}"))?;
246 if window_size <= 0 {
247 return Err(format!(
248 "{PREGEN_WINDOW_SIZE_ENV} must be a positive integer"
249 ));
250 }
251
252 let window_size_as_usize = window_size as usize;
253 let Some(active_target_chunks) = window_size_as_usize
254 .checked_mul(window_size_as_usize)
255 .and_then(|chunk_count| chunk_count.checked_mul(PREGEN_ACTIVE_WINDOWS))
256 else {
257 return Err(format!(
258 "{PREGEN_WINDOW_SIZE_ENV} is too large for the pregeneration window budget"
259 ));
260 };
261 if active_target_chunks > PREGEN_UNLOAD_BACKPRESSURE_HIGH {
262 return Err(format!(
263 "{PREGEN_WINDOW_SIZE_ENV} must keep {PREGEN_ACTIVE_WINDOWS} active windows within the {PREGEN_UNLOAD_BACKPRESSURE_HIGH}-chunk unload-backpressure budget"
264 ));
265 }
266
267 Ok(window_size)
268}
269
270async fn pregen_overworld(
271 world: &Arc<World>,
272 center_chunk: ChunkPos,
273 pregen_size: PregenSize,
274 window_size: i32,
275 cancel_token: &CancellationToken,
276) -> bool {
277 let total_chunks = total_chunks(pregen_size.side_length);
278
279 log::info!(
280 "Preparing spawn area: {} chunks ({}x{}) around chunk ({}, {})",
281 total_chunks,
282 pregen_size.side_length,
283 pregen_size.side_length,
284 center_chunk.0.x,
285 center_chunk.0.y,
286 );
287
288 #[cfg(feature = "slow_chunk_gen")]
289 SLOW_CHUNK_GEN.store(true, Ordering::Relaxed);
290
291 let elapsed = {
292 let start = Instant::now();
293 let completed =
294 generate_pregen(world, center_chunk, pregen_size, window_size, cancel_token).await;
295 (start.elapsed(), completed)
296 };
297
298 #[cfg(feature = "slow_chunk_gen")]
299 SLOW_CHUNK_GEN.store(false, Ordering::Relaxed);
300
301 let elapsed_secs = elapsed.0.as_secs_f64();
302 let chunks_per_second = if elapsed_secs > 0.0 {
303 total_chunks as f64 / elapsed_secs
304 } else {
305 0.0
306 };
307 if elapsed.1 {
308 log::info!(
309 "Spawn area prepared: {total_chunks} chunks in {elapsed_secs:.2}s ({chunks_per_second:.1} chunks/s)",
310 );
311 } else {
312 log::info!("Spawn area preparation cancelled after {elapsed_secs:.2}s");
313 }
314 elapsed.1
315}
316
317fn build_pregen_windows(
318 center_chunk: ChunkPos,
319 radius: i32,
320 window_size: i32,
321) -> VecDeque<PregenWindow> {
322 let min_x = center_chunk.0.x - radius;
323 let max_x = center_chunk.0.x + radius;
324 let min_z = center_chunk.0.y - radius;
325 let max_z = center_chunk.0.y + radius;
326 let x_ranges = pregen_window_ranges(min_x, max_x, window_size);
327 let z_ranges = pregen_window_ranges(min_z, max_z, window_size);
328 let mut windows = VecDeque::new();
329
330 for (strip_index, z_pair) in z_ranges.chunks(2).enumerate() {
333 let mut push_column = |&(window_min_x, window_max_x): &(i32, i32)| {
334 for &(window_min_z, window_max_z) in z_pair {
335 windows.push_back(PregenWindow {
336 min_x: window_min_x,
337 max_x: window_max_x,
338 min_z: window_min_z,
339 max_z: window_max_z,
340 });
341 }
342 };
343
344 if strip_index % 2 == 0 {
345 for x_range in &x_ranges {
346 push_column(x_range);
347 }
348 } else {
349 for x_range in x_ranges.iter().rev() {
350 push_column(x_range);
351 }
352 }
353 }
354
355 windows
356}
357
358fn pregen_window_ranges(min: i32, max: i32, window_size: i32) -> Vec<(i32, i32)> {
359 let mut ranges = Vec::new();
360 let mut start = min;
361 while start <= max {
362 let end = start.saturating_add(window_size - 1).min(max);
363 ranges.push((start, end));
364 start = end + 1;
365 }
366 ranges
367}
368
369async fn generate_pregen(
370 world: &Arc<World>,
371 center_chunk: ChunkPos,
372 pregen_size: PregenSize,
373 window_size: i32,
374 cancel_token: &CancellationToken,
375) -> bool {
376 let total_chunks = total_chunks(pregen_size.side_length);
377 let mut pending_windows = build_pregen_windows(center_chunk, pregen_size.radius, window_size);
378 let mut active_windows = Vec::with_capacity(PREGEN_ACTIVE_WINDOWS + 1);
379 let mut last_report = Instant::now();
380 let mut last_completed = 0usize;
381 let mut completed = 0usize;
382 let mut unload_backpressure = false;
383 let mut peak_unloading_chunks = 0usize;
384 let start = Instant::now();
385
386 log::info!(
387 "Pregeneration windowing: {window_size}x{window_size} target chunks, {PREGEN_ACTIVE_WINDOWS} active windows, dependency halo {FULL_DEPENDENCY_RADIUS} chunks",
388 );
389
390 fill_active_windows(world, &mut pending_windows, &mut active_windows);
391
392 while completed < total_chunks {
393 if cancel_token.is_cancelled() {
394 release_all_windows(world, &mut active_windows);
395 return false;
396 }
397
398 drain_pregen_broadcasts(world);
399 world.chunk_map.advance_scheduling();
400 peak_unloading_chunks = peak_unloading_chunks.max(world.chunk_map.unloading_chunks.len());
401 update_unload_backpressure(world, &mut unload_backpressure);
402
403 for active in &mut active_windows {
404 active.poll(world);
405 }
406
407 if !unload_backpressure {
408 let newly_ready_count = active_windows
409 .iter()
410 .filter(|active| active.ready && !active.counted)
411 .count();
412 for _ in 0..newly_ready_count {
413 activate_next_window(world, &mut pending_windows, &mut active_windows);
414 }
415 }
416
417 for active in &mut active_windows {
418 if active.ready && !active.counted {
419 completed += active.window.chunk_count();
420 active.counted = true;
421 }
422 }
423 if !unload_backpressure {
424 fill_active_windows(world, &mut pending_windows, &mut active_windows);
425 }
426 drain_pregen_broadcasts(world);
427 world.chunk_map.advance_scheduling();
428 release_unneeded_completed_windows(world, &mut active_windows);
429 peak_unloading_chunks = peak_unloading_chunks.max(world.chunk_map.unloading_chunks.len());
430 update_unload_backpressure(world, &mut unload_backpressure);
431
432 if completed == total_chunks {
433 break;
434 }
435
436 if pregen_size.side_length > VANILLA_PLAYER_SPAWN_SIZE_CHUNKS
437 && last_report.elapsed() >= Duration::from_secs(5)
438 {
439 let report_elapsed = last_report.elapsed().as_secs_f64();
440 let ready_in_active = active_windows
441 .iter()
442 .filter(|active| !active.counted)
443 .map(|active| active.ready_chunks)
444 .sum::<usize>();
445 let current_completed = (completed + ready_in_active).min(total_chunks);
446 let elapsed = start.elapsed().as_secs_f64();
447 let chunks_per_sec = if elapsed > 0.0 {
448 (current_completed.saturating_sub(last_completed)) as f64 / report_elapsed
449 } else {
450 0.0
451 };
452 let percent = (current_completed as f64 / total_chunks as f64) * 100.0;
453 let remaining = total_chunks.saturating_sub(current_completed);
454 let eta = if chunks_per_sec > 0.0 && remaining > 0 {
455 remaining as f64 / chunks_per_sec
456 } else {
457 0.0
458 };
459 log::info!(
460 "Progress: {current_completed}/{total_chunks} ({percent:.1}%), {chunks_per_sec:.1} chunks/s, ETA: {eta:.0}s",
461 );
462 last_report = Instant::now();
463 last_completed = current_completed;
464 }
465
466 tokio::select! {
467 () = cancel_token.cancelled() => {
468 release_all_windows(world, &mut active_windows);
469 return false;
470 }
471 () = sleep(Duration::from_millis(10)) => {}
472 }
473 }
474
475 release_all_windows(world, &mut active_windows);
476 peak_unloading_chunks = peak_unloading_chunks.max(world.chunk_map.unloading_chunks.len());
477 log::info!("Pregeneration peak unload backlog: {peak_unloading_chunks} chunks");
478 true
479}
480
481fn update_unload_backpressure(world: &Arc<World>, unload_backpressure: &mut bool) {
482 let unloading_chunks = world.chunk_map.unloading_chunks.len();
483 if *unload_backpressure {
484 if unloading_chunks <= PREGEN_UNLOAD_BACKPRESSURE_LOW {
485 *unload_backpressure = false;
486 log::info!(
487 "Pregen unload backpressure released: unloading_chunks={unloading_chunks}, low_watermark={PREGEN_UNLOAD_BACKPRESSURE_LOW}",
488 );
489 }
490 return;
491 }
492
493 if unloading_chunks >= PREGEN_UNLOAD_BACKPRESSURE_HIGH {
494 *unload_backpressure = true;
495 log::info!(
496 "Pregen unload backpressure active: unloading_chunks={unloading_chunks}, high_watermark={PREGEN_UNLOAD_BACKPRESSURE_HIGH}, low_watermark={PREGEN_UNLOAD_BACKPRESSURE_LOW}",
497 );
498 }
499}
500
501fn drain_pregen_broadcasts(world: &Arc<World>) {
502 world.chunk_map.broadcast_changed_chunks();
503}
504
505fn total_chunks(side_length: i32) -> usize {
506 let side_length = i64::from(side_length);
507 (side_length * side_length) as usize
508}
509
510fn fill_active_windows(
511 world: &Arc<World>,
512 pending_windows: &mut VecDeque<PregenWindow>,
513 active_windows: &mut Vec<ActivePregenWindow>,
514) {
515 while active_windows.iter().filter(|active| !active.ready).count() < PREGEN_ACTIVE_WINDOWS {
516 if !activate_next_window(world, pending_windows, active_windows) {
517 break;
518 }
519 }
520}
521
522fn activate_next_window(
523 world: &Arc<World>,
524 pending_windows: &mut VecDeque<PregenWindow>,
525 active_windows: &mut Vec<ActivePregenWindow>,
526) -> bool {
527 let Some(window) = pending_windows.pop_front() else {
528 return false;
529 };
530
531 active_windows.push(ActivePregenWindow::new(world, window));
532 true
533}
534
535fn release_unneeded_completed_windows(
536 world: &Arc<World>,
537 active_windows: &mut Vec<ActivePregenWindow>,
538) {
539 let incomplete_windows = active_windows
540 .iter()
541 .filter(|active| !active.ready)
542 .map(|active| active.window)
543 .collect::<Vec<_>>();
544
545 active_windows.retain(|active| {
546 if !active.ready {
547 return true;
548 }
549
550 let protected = active.window.protected_rect();
551
552 incomplete_windows
553 .iter()
554 .any(|window| protected.overlaps(window.protected_rect()))
555 });
556
557 world.chunk_map.advance_scheduling();
558}
559
560fn release_all_windows(world: &Arc<World>, active_windows: &mut Vec<ActivePregenWindow>) {
561 active_windows.clear();
562 world.chunk_map.advance_scheduling();
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn pregen_size_accepts_zero_as_disabled() {
571 assert_eq!(PregenSize::from_side_length(0), Ok(None));
572 }
573
574 #[test]
575 fn pregen_size_accepts_odd_side_lengths() {
576 assert_eq!(
577 PregenSize::from_side_length(7),
578 Ok(Some(PregenSize {
579 side_length: 7,
580 radius: 3,
581 }))
582 );
583 }
584
585 #[test]
586 fn pregen_size_rejects_even_side_lengths() {
587 assert!(PregenSize::from_side_length(2).is_err());
588 }
589
590 #[test]
591 fn pregen_size_rejects_negative_side_lengths() {
592 assert!(PregenSize::from_side_length(-1).is_err());
593 }
594
595 #[test]
596 fn pregen_window_order_keeps_consecutive_dependency_areas_local() {
597 let radius = DEFAULT_PREGEN_WINDOW_SIZE * 2;
598 let windows = build_pregen_windows(ChunkPos::new(0, 0), radius, DEFAULT_PREGEN_WINDOW_SIZE);
599
600 assert_eq!(windows.len(), 25);
601 assert_eq!(
602 windows
603 .iter()
604 .map(|window| window.chunk_count())
605 .sum::<usize>(),
606 total_chunks(radius * 2 + 1)
607 );
608 assert!(
609 windows
610 .iter()
611 .zip(windows.iter().skip(1))
612 .all(|(current, next)| current.protected_rect().overlaps(next.protected_rect()))
613 );
614 }
615
616 #[test]
617 fn pregen_window_size_requires_a_positive_integer() {
618 assert_eq!(parse_pregen_window_size("1"), Ok(1));
619 assert_eq!(parse_pregen_window_size("64"), Ok(64));
620 assert!(parse_pregen_window_size("0").is_err());
621 assert!(parse_pregen_window_size("-1").is_err());
622 assert!(parse_pregen_window_size("wide").is_err());
623 }
624
625 #[test]
626 fn pregen_window_size_must_fit_unload_backpressure_budget() {
627 assert!(parse_pregen_window_size("65").is_err());
628 assert!(parse_pregen_window_size(&i32::MAX.to_string()).is_err());
629 }
630}