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