Skip to main content

steel_core/chunk/light/sky_propagation/
mod.rs

1use steel_registry::{blocks::block_state_ext::BlockStateExt, vanilla_blocks};
2use steel_utils::{BlockPos, BlockStateId, ChunkPos, Direction, SectionPos};
3
4use super::{
5    CachedLightBlock, LIGHT_BLOCKED, LightAxisDirection, LightCacheLayout, LightDirectionSet,
6    LightLayer, LightLayerEdit, LightQueueFlags, LightSectionEmptinessChange,
7    LightSectionReadCache, LightWorkset, MAX_LIGHT_LEVEL, PackedLightPropagationQueues,
8    PackedLightQueueEntry, PooledPackedLightQueues, get_light_block_into, get_light_opacity,
9    light_occlusion_shape,
10};
11
12/// Error returned when a sky-light propagation context is built from mismatched caches.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum SkyLightPropagationContextError {
15    /// Sky-light propagation requires a sky light edit cache.
16    WrongLayer {
17        /// Layer supplied by the edit cache.
18        layer: LightLayer,
19    },
20    /// Section and light caches were built from different cache layouts.
21    LayoutMismatch {
22        /// Layout used by the section cache.
23        section_layout: Box<LightCacheLayout>,
24        /// Layout used by the light cache.
25        light_layout: Box<LightCacheLayout>,
26    },
27    /// The workset does not contain its center chunk.
28    MissingCenterChunk {
29        /// Missing center chunk position.
30        chunk_pos: ChunkPos,
31    },
32}
33
34impl SkyLightPropagationContextError {
35    fn layout_mismatch(section_layout: LightCacheLayout, light_layout: LightCacheLayout) -> Self {
36        Self::LayoutMismatch {
37            section_layout: Box::new(section_layout),
38            light_layout: Box::new(light_layout),
39        }
40    }
41}
42
43/// Sections whose visible sky-light data changed during a scoped update.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SkyLightUpdateResult {
46    /// Light sections that should be reported to the world/chunk update layer.
47    pub updated_sections: Vec<SectionPos>,
48}
49
50/// Whether chunk sky-light generation must validate edge consistency.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum SkyLightChunkEdgeChecks {
53    /// Seed skylight and validate this chunk's horizontal edges against neighbors.
54    Required,
55    /// Trust existing neighboring light and pull initialized edge levels inward.
56    Skipped,
57}
58
59/// Seeds and propagates sky light for the center chunk without edge checks.
60pub fn propagate_sky_light_chunk_without_edge_checks(
61    workset: &LightWorkset,
62) -> Result<SkyLightUpdateResult, SkyLightPropagationContextError> {
63    propagate_sky_light_chunk(workset, SkyLightChunkEdgeChecks::Skipped)
64}
65
66/// Seeds and propagates sky light for the center chunk of a scoped workset.
67///
68/// This matches `ScalableLux` `SkyStarLightEngine.lightChunk`: sky sections
69/// around non-empty sections are initialized, full skylight is propagated
70/// downward, then the caller chooses between validating edge consistency or
71/// pulling already-initialized neighbor levels inward.
72pub fn propagate_sky_light_chunk(
73    workset: &LightWorkset,
74    edge_checks: SkyLightChunkEdgeChecks,
75) -> Result<SkyLightUpdateResult, SkyLightPropagationContextError> {
76    workset.with_chunk_read_cache(|chunk_cache| {
77        let layout = chunk_cache.layout();
78        let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
79            return Err(SkyLightPropagationContextError::MissingCenterChunk {
80                chunk_pos: layout.center_chunk(),
81            });
82        };
83        if chunk_cache.chunk(center_slot).is_none() {
84            return Err(SkyLightPropagationContextError::MissingCenterChunk {
85                chunk_pos: layout.center_chunk(),
86            });
87        }
88
89        chunk_cache.with_section_read_cache(|section_cache| {
90            chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
91                let mut queues = PooledPackedLightQueues::take();
92
93                {
94                    let mut context = SkyLightPropagationContext::new(
95                        section_cache,
96                        &mut light_edit,
97                        &mut queues,
98                    )?;
99                    context.reset_center_chunk_sections();
100                    context.handle_unlit_empty_section_changes(layout.center_chunk());
101                    context.light_chunk(layout.center_chunk(), edge_checks);
102                    if edge_checks == SkyLightChunkEdgeChecks::Required {
103                        context.deinit_and_lazy_init_empty_sections(layout.center_chunk(), true);
104                    }
105                }
106
107                let mut updated_sections = Vec::new();
108                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
109                Ok(SkyLightUpdateResult { updated_sections })
110            })
111        })
112    })
113}
114
115/// Force-synchronizes sky-light sections for an already-lit loaded chunk.
116pub fn force_load_sky_light_chunk(
117    workset: &LightWorkset,
118) -> Result<SkyLightUpdateResult, SkyLightPropagationContextError> {
119    workset.with_chunk_read_cache(|chunk_cache| {
120        let layout = ensure_center_chunk(chunk_cache)?;
121
122        chunk_cache.with_section_read_cache(|section_cache| {
123            chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
124                let mut queues = PooledPackedLightQueues::take();
125
126                {
127                    let mut context = SkyLightPropagationContext::new(
128                        section_cache,
129                        &mut light_edit,
130                        &mut queues,
131                    )?;
132                    context.handle_loaded_empty_section_changes(layout.center_chunk());
133                }
134
135                let mut updated_sections = Vec::new();
136                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
137                Ok(SkyLightUpdateResult { updated_sections })
138            })
139        })
140    })
141}
142
143/// Validates already-loaded sky-light chunk edges without resetting sections.
144pub fn check_sky_light_chunk_edges(
145    workset: &LightWorkset,
146) -> Result<SkyLightUpdateResult, SkyLightPropagationContextError> {
147    workset.with_chunk_read_cache(|chunk_cache| {
148        let layout = ensure_center_chunk(chunk_cache)?;
149
150        chunk_cache.with_section_read_cache(|section_cache| {
151            chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
152                let mut queues = PooledPackedLightQueues::take();
153
154                {
155                    let mut context = SkyLightPropagationContext::new(
156                        section_cache,
157                        &mut light_edit,
158                        &mut queues,
159                    )?;
160                    context.light.rewrite_missing_sections_for_skylight();
161                    for section_y in (layout.range().min_section_y()
162                        ..layout.range().max_section_y_exclusive())
163                        .rev()
164                    {
165                        context.check_missing_section(layout.center_chunk(), section_y, true);
166                    }
167                    context.check_chunk_edges(
168                        layout.center_chunk(),
169                        layout.range().min_section_y(),
170                        layout.range().max_section_y_exclusive() - 1,
171                    );
172                }
173
174                let mut updated_sections = Vec::new();
175                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
176                Ok(SkyLightUpdateResult { updated_sections })
177            })
178        })
179    })
180}
181
182/// Loads already-persisted sky light and validates chunk edges without resetting sections.
183pub fn load_sky_light_chunk(
184    workset: &LightWorkset,
185) -> Result<SkyLightUpdateResult, SkyLightPropagationContextError> {
186    let mut updated_sections = force_load_sky_light_chunk(workset)?.updated_sections;
187    updated_sections.extend(check_sky_light_chunk_edges(workset)?.updated_sections);
188    Ok(SkyLightUpdateResult { updated_sections })
189}
190
191fn ensure_center_chunk(
192    chunk_cache: &super::LightChunkReadCache<'_>,
193) -> Result<LightCacheLayout, SkyLightPropagationContextError> {
194    let layout = chunk_cache.layout();
195    let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
196        return Err(SkyLightPropagationContextError::MissingCenterChunk {
197            chunk_pos: layout.center_chunk(),
198        });
199    };
200    if chunk_cache.chunk(center_slot).is_none() {
201        return Err(SkyLightPropagationContextError::MissingCenterChunk {
202            chunk_pos: layout.center_chunk(),
203        });
204    }
205
206    Ok(layout)
207}
208
209/// Runs ScalableLux-style sky-light propagation for changed blocks in a scoped workset.
210pub fn propagate_sky_light_changes(
211    workset: &LightWorkset,
212    positions: impl IntoIterator<Item = BlockPos>,
213) -> Result<SkyLightUpdateResult, SkyLightPropagationContextError> {
214    propagate_sky_light_changes_with_empty_sections(workset, positions, [])
215}
216
217/// Runs sky-light propagation after applying real section emptiness transitions.
218pub fn propagate_sky_light_changes_with_empty_sections(
219    workset: &LightWorkset,
220    positions: impl IntoIterator<Item = BlockPos>,
221    empty_sections: impl IntoIterator<Item = LightSectionEmptinessChange>,
222) -> Result<SkyLightUpdateResult, SkyLightPropagationContextError> {
223    let positions = positions.into_iter().collect::<Vec<_>>();
224    let empty_sections = empty_sections.into_iter().collect::<Vec<_>>();
225
226    workset.with_chunk_read_cache(|chunk_cache| {
227        let layout = chunk_cache.layout();
228        // ScalableLux drops queued dynamic changes once the center chunk leaves the light cache.
229        let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
230            return Ok(SkyLightUpdateResult {
231                updated_sections: Vec::new(),
232            });
233        };
234        if chunk_cache.chunk(center_slot).is_none() {
235            return Ok(SkyLightUpdateResult {
236                updated_sections: Vec::new(),
237            });
238        }
239
240        chunk_cache.with_section_read_cache(|section_cache| {
241            chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
242                let mut queues = PooledPackedLightQueues::take();
243
244                {
245                    let mut context = SkyLightPropagationContext::new(
246                        section_cache,
247                        &mut light_edit,
248                        &mut queues,
249                    )?;
250                    let mut changed_chunks = Vec::new();
251                    for change in &empty_sections {
252                        let chunk_pos =
253                            ChunkPos::new(change.section_pos.x(), change.section_pos.z());
254                        context
255                            .light
256                            .set_section_empty(change.section_pos, change.empty);
257                        if !changed_chunks.contains(&chunk_pos) {
258                            changed_chunks.push(chunk_pos);
259                        }
260                    }
261                    for chunk_pos in changed_chunks {
262                        context.deinit_and_lazy_init_empty_sections(chunk_pos, false);
263                    }
264                    context.propagate_block_changes(&positions);
265                }
266
267                let mut updated_sections = Vec::new();
268                light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
269                Ok(SkyLightUpdateResult { updated_sections })
270            })
271        })
272    })
273}
274
275mod context;
276
277pub use context::SkyLightPropagationContext;
278
279mod algorithms;
280mod queue_engine;
281
282#[cfg(test)]
283mod tests;