Skip to main content

steel_core/chunk/
chunk_pyramid.rs

1//! This module contains the `ChunkPyramid`, which is used to check chunk dependencies.
2//! All structures are const-compatible and computed at compile time.
3
4use std::sync::Arc;
5
6use crate::chunk::{
7    chunk_generation_task::StaticCache2D, chunk_holder::ChunkHolder,
8    chunk_status_tasks::ChunkStatusTasks, status::ChunkStatus,
9};
10use crate::worldgen::context::WorldGenContext;
11
12/// Number of `ChunkStatus` variants.
13const STATUS_COUNT: usize = 12;
14/// Maximum dependency radius supported.
15const MAX_RADIUS: usize = 16;
16
17/// A collection of chunk dependencies (const-compatible).
18#[derive(Debug, Clone, Copy)]
19pub struct ChunkDependencies {
20    dependency_by_radius: [Option<ChunkStatus>; MAX_RADIUS],
21    len: usize,
22    radius_by_dependency: [usize; STATUS_COUNT],
23}
24
25impl ChunkDependencies {
26    /// Empty dependencies constant.
27    pub const EMPTY: Self = Self {
28        dependency_by_radius: [None; MAX_RADIUS],
29        len: 0,
30        radius_by_dependency: [0; STATUS_COUNT],
31    };
32
33    /// Creates dependencies from requirements and optional parent status.
34    #[must_use]
35    const fn from_requirements(
36        reqs: &[(ChunkStatus, usize)],
37        parent_status: Option<ChunkStatus>,
38    ) -> Self {
39        let mut dependency_by_radius = [None; MAX_RADIUS];
40        let mut len = 0;
41
42        // If we have a parent, start with parent at radius 0
43        if let Some(parent) = parent_status {
44            dependency_by_radius[0] = Some(parent);
45            len = 1;
46        }
47
48        // Process requirements
49        let mut i = 0;
50        while i < reqs.len() {
51            let (status, radius) = reqs[i];
52            let new_len = radius + 1;
53
54            // Extend if needed, filling with this status
55            if new_len > len {
56                let mut j = len;
57                while j < new_len {
58                    dependency_by_radius[j] = Some(status);
59                    j += 1;
60                }
61                len = new_len;
62            }
63
64            // Update existing entries if this status is higher
65            let limit = const_min(len, new_len);
66            let mut j = 0;
67            while j < limit {
68                if let Some(existing) = dependency_by_radius[j]
69                    && status.get_index() > existing.get_index()
70                {
71                    dependency_by_radius[j] = Some(status);
72                }
73                j += 1;
74            }
75
76            i += 1;
77        }
78
79        // Build radius_by_dependency
80        let radius_by_dependency = Self::build_radius_lookup(&dependency_by_radius, len);
81
82        Self {
83            dependency_by_radius,
84            len,
85            radius_by_dependency,
86        }
87    }
88
89    /// Builds the radius lookup table from dependency array.
90    const fn build_radius_lookup(
91        deps: &[Option<ChunkStatus>; MAX_RADIUS],
92        len: usize,
93    ) -> [usize; STATUS_COUNT] {
94        let mut radius_by_dependency = [0usize; STATUS_COUNT];
95        let mut radius = 0;
96        while radius < len {
97            if let Some(dep) = deps[radius] {
98                let index = dep.get_index();
99                let mut j = 0;
100                while j <= index && j < STATUS_COUNT {
101                    radius_by_dependency[j] = radius;
102                    j += 1;
103                }
104            }
105            radius += 1;
106        }
107        radius_by_dependency
108    }
109
110    /// Computes accumulated dependencies by merging with parent's accumulated dependencies.
111    const fn accumulate(&self, parent_accumulated: &Self, parent_status: ChunkStatus) -> Self {
112        // Find the last radius where we reference the parent status or higher
113        let mut radius_of_parent = 0;
114        let mut i = 0;
115        while i < self.len {
116            if let Some(s) = self.dependency_by_radius[i]
117                && s.get_index() >= parent_status.get_index()
118            {
119                radius_of_parent = i;
120            }
121            i += 1;
122        }
123
124        let parent_len = parent_accumulated.len;
125        let new_len = const_max(radius_of_parent + parent_len, self.len);
126        let capped_len = const_min(new_len, MAX_RADIUS);
127
128        let mut accumulated = [None; MAX_RADIUS];
129
130        let mut dist = 0;
131        while dist < capped_len {
132            let dist_in_parent = dist.saturating_sub(radius_of_parent);
133
134            let parent_dep = if dist_in_parent < parent_accumulated.len {
135                parent_accumulated.dependency_by_radius[dist_in_parent]
136            } else {
137                None
138            };
139
140            let direct_dep = if dist < self.len {
141                self.dependency_by_radius[dist]
142            } else {
143                None
144            };
145
146            accumulated[dist] = const_max_status(direct_dep, parent_dep);
147            dist += 1;
148        }
149
150        let radius_by_dependency = Self::build_radius_lookup(&accumulated, capped_len);
151
152        Self {
153            dependency_by_radius: accumulated,
154            len: capped_len,
155            radius_by_dependency,
156        }
157    }
158
159    /// Gets the radius of the dependencies for the given status.
160    ///
161    /// # Panics
162    /// Panics if the status index is out of bounds.
163    #[must_use]
164    pub const fn get_radius_of(&self, status: ChunkStatus) -> usize {
165        self.radius_by_dependency[status.get_index()]
166    }
167
168    /// Gets the radius of the dependencies.
169    #[must_use]
170    pub const fn get_radius(&self) -> usize {
171        self.len.saturating_sub(1)
172    }
173
174    /// Gets the dependency status at the given distance.
175    #[must_use]
176    pub const fn get(&self, distance: usize) -> Option<ChunkStatus> {
177        if distance < self.len {
178            self.dependency_by_radius[distance]
179        } else {
180            None
181        }
182    }
183}
184
185const fn const_max(a: usize, b: usize) -> usize {
186    if a > b { a } else { b }
187}
188
189const fn const_min(a: usize, b: usize) -> usize {
190    if a < b { a } else { b }
191}
192
193const fn const_max_status(a: Option<ChunkStatus>, b: Option<ChunkStatus>) -> Option<ChunkStatus> {
194    match (a, b) {
195        (Some(sa), Some(sb)) => {
196            if sa.get_index() > sb.get_index() {
197                Some(sa)
198            } else {
199                Some(sb)
200            }
201        }
202        (Some(s), None) | (None, Some(s)) => Some(s),
203        (None, None) => None,
204    }
205}
206
207/// A task that generates a chunk.
208pub type ChunkStatusTask =
209    fn(Arc<WorldGenContext>, &ChunkStep, &Arc<StaticCache2D<Arc<ChunkHolder>>>, Arc<ChunkHolder>);
210
211/// A chunk step (const-compatible).
212#[derive(Clone, Copy)]
213pub struct ChunkStep {
214    /// The target status of the step.
215    pub target_status: ChunkStatus,
216    /// The direct dependencies of the step.
217    pub direct_dependencies: ChunkDependencies,
218    /// The accumulated dependencies of the step.
219    pub accumulated_dependencies: ChunkDependencies,
220    /// The block state write radius of the step.
221    pub block_state_write_radius: i32,
222    /// The task of the step.
223    pub task: ChunkStatusTask,
224}
225
226impl ChunkStep {
227    /// A placeholder step used for array initialization.
228    const PLACEHOLDER: Self = Self {
229        target_status: ChunkStatus::Empty,
230        direct_dependencies: ChunkDependencies::EMPTY,
231        accumulated_dependencies: ChunkDependencies::EMPTY,
232        block_state_write_radius: -1,
233        task: noop_task,
234    };
235
236    /// Gets the accumulated radius of the dependencies for the given status.
237    #[must_use]
238    pub const fn get_accumulated_radius_of(&self, status: ChunkStatus) -> usize {
239        if status.get_index() == self.target_status.get_index() {
240            0
241        } else {
242            self.accumulated_dependencies.get_radius_of(status)
243        }
244    }
245}
246
247fn noop_task(
248    _context: Arc<WorldGenContext>,
249    _step: &ChunkStep,
250    _cache: &Arc<StaticCache2D<Arc<ChunkHolder>>>,
251    _holder: Arc<ChunkHolder>,
252) {
253}
254
255/// Represents the hierarchy and dependencies for chunk generation or loading.
256pub struct ChunkPyramid {
257    steps: [ChunkStep; STATUS_COUNT],
258}
259
260impl ChunkPyramid {
261    /// Gets the step for the given status.
262    #[must_use]
263    pub const fn get_step_to(&self, status: ChunkStatus) -> &ChunkStep {
264        &self.steps[status.get_index()]
265    }
266}
267
268/// Const-time pyramid builder.
269struct ConstPyramidBuilder {
270    steps: [ChunkStep; STATUS_COUNT],
271    count: usize,
272}
273
274impl ConstPyramidBuilder {
275    const fn new() -> Self {
276        Self {
277            steps: [ChunkStep::PLACEHOLDER; STATUS_COUNT],
278            count: 0,
279        }
280    }
281
282    const fn step(
283        mut self,
284        status: ChunkStatus,
285        requirements: &[(ChunkStatus, usize)],
286        block_state_write_radius: i32,
287        task: ChunkStatusTask,
288    ) -> Self {
289        // Get parent info if we have previous steps
290        let (parent_status, parent_accumulated) = if self.count > 0 {
291            let parent = &self.steps[self.count - 1];
292            (
293                Some(parent.target_status),
294                Some(parent.accumulated_dependencies),
295            )
296        } else {
297            (None, None)
298        };
299
300        // Compute direct dependencies
301        let direct = ChunkDependencies::from_requirements(requirements, parent_status);
302
303        // Compute accumulated dependencies
304        let accumulated = match (parent_status, parent_accumulated) {
305            (Some(ps), Some(pa)) => direct.accumulate(&pa, ps),
306            _ => direct,
307        };
308
309        self.steps[self.count] = ChunkStep {
310            target_status: status,
311            direct_dependencies: direct,
312            accumulated_dependencies: accumulated,
313            block_state_write_radius,
314            task,
315        };
316        self.count += 1;
317        self
318    }
319
320    const fn build(self) -> ChunkPyramid {
321        ChunkPyramid { steps: self.steps }
322    }
323}
324
325/// Macro for defining chunk pyramids with nice syntax.
326///
327/// # Example
328/// ```ignore
329/// define_pyramid! {
330///     pub static MY_PYRAMID = {
331///         Empty => { task: my_task },
332///         StructureStarts => {
333///             requirements: [(StructureStarts, 8)],
334///             task: other_task,
335///         },
336///     };
337/// }
338/// ```
339macro_rules! define_pyramid {
340    (
341        $vis:vis const $name:ident = {
342            $($status:ident => {
343                $(requirements: [$( ($req_status:ident, $req_radius:expr) ),* $(,)?] ,)?
344                $(block_state_write_radius: $bswr:expr ,)?
345                task: $task:expr $(,)?
346            }),* $(,)?
347        };
348    ) => {
349        #[expect(missing_docs, reason = "generated pyramid constant; name is self-documenting")]
350        $vis const $name: &'static ChunkPyramid = &{
351            ConstPyramidBuilder::new()
352            $(
353                .step(
354                    ChunkStatus::$status,
355                    &[ $( $( (ChunkStatus::$req_status, $req_radius) ),* )? ],
356                    define_pyramid!(@bswr $($bswr)?),
357                    $task,
358                )
359            )*
360            .build()
361        };
362    };
363
364    // Default block_state_write_radius
365    (@bswr) => { -1 };
366    (@bswr $bswr:expr) => { $bswr };
367}
368
369define_pyramid! {
370    pub const GENERATION_PYRAMID = {
371        Empty => {
372            task: ChunkStatusTasks::empty,
373        },
374        StructureStarts => {
375            task: ChunkStatusTasks::generate_structure_starts,
376        },
377        StructureReferences => {
378            requirements: [(StructureStarts, 8)],
379            task: ChunkStatusTasks::generate_structure_references,
380        },
381        Biomes => {
382            requirements: [(StructureStarts, 8)],
383            task: ChunkStatusTasks::generate_biomes,
384        },
385        Noise => {
386            requirements: [(StructureStarts, 8), (Biomes, 1)],
387            block_state_write_radius: 0,
388            task: ChunkStatusTasks::generate_noise,
389        },
390        Surface => {
391            requirements: [(StructureStarts, 8), (Biomes, 1)],
392            block_state_write_radius: 0,
393            task: ChunkStatusTasks::generate_surface,
394        },
395        Carvers => {
396            requirements: [(StructureStarts, 8)],
397            block_state_write_radius: 0,
398            task: ChunkStatusTasks::generate_carvers,
399        },
400        Features => {
401            requirements: [(StructureStarts, 8), (Carvers, 1)],
402            block_state_write_radius: 1,
403            task: ChunkStatusTasks::generate_features,
404        },
405        InitializeLight => {
406            task: ChunkStatusTasks::initialize_light,
407        },
408        Light => {
409            requirements: [(InitializeLight, 1)],
410            block_state_write_radius: 0,
411            task: ChunkStatusTasks::light,
412        },
413        Spawn => {
414            requirements: [(Biomes, 1)],
415            task: ChunkStatusTasks::generate_spawn,
416        },
417        Full => {
418            task: ChunkStatusTasks::full,
419        },
420    };
421}
422
423define_pyramid! {
424    pub const LOADING_PYRAMID = {
425        Empty => {
426            task: noop_task,
427        },
428        StructureStarts => {
429            task: ChunkStatusTasks::load_structure_starts,
430        },
431        StructureReferences => {
432            task: noop_task,
433        },
434        Biomes => {
435            task: noop_task,
436        },
437        Noise => {
438            task: noop_task,
439        },
440        Surface => {
441            task: noop_task,
442        },
443        Carvers => {
444            task: noop_task,
445        },
446        Features => {
447            task: noop_task,
448        },
449        InitializeLight => {
450            task: ChunkStatusTasks::initialize_light,
451        },
452        Light => {
453            requirements: [(InitializeLight, 1)],
454            task: ChunkStatusTasks::load_light,
455        },
456        Spawn => {
457            task: noop_task,
458        },
459        Full => {
460            task: ChunkStatusTasks::full,
461        },
462    };
463}