Skip to main content

steel_core/chunk/light/
mod.rs

1//! Light storage primitives used by chunk and world lighting.
2
3use steel_registry::blocks::{block_state_ext::BlockStateExt, shapes::VoxelShape};
4use steel_utils::{BlockStateId, Direction, SectionPos};
5
6use crate::physics::shapes::{face_shape_occludes, merged_face_occludes};
7
8/// Maximum light value stored by vanilla lighting.
9pub const MAX_LIGHT_LEVEL: u8 = 15;
10/// Minimum opacity used while propagating vanilla light.
11pub const MIN_LIGHT_OPACITY: u8 = 1;
12/// Opacity returned when a block face fully blocks light.
13pub const LIGHT_BLOCKED: u8 = MAX_LIGHT_LEVEL + 1;
14/// Vanilla stores one extra light section below and above the build height.
15pub const LIGHT_SECTION_PADDING: i32 = 1;
16
17/// Number of blocks along one edge of a light section.
18pub const DATA_LAYER_EDGE: usize = 16;
19/// Number of blocks in a light section.
20pub const DATA_LAYER_BLOCK_COUNT: usize = DATA_LAYER_EDGE * DATA_LAYER_EDGE * DATA_LAYER_EDGE;
21/// Number of packed bytes in a light section.
22pub const DATA_LAYER_SIZE: usize = DATA_LAYER_BLOCK_COUNT / 2;
23const CHUNK_EDGE: usize = 16;
24const CHUNK_COLUMN_COUNT: usize = CHUNK_EDGE * CHUNK_EDGE;
25const NEGATIVE_INFINITY: i32 = i32::MIN;
26
27/// Vanilla light layer kind.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum LightLayer {
30    /// Sky light propagated from dimensions with skylight.
31    Sky,
32    /// Block light emitted by blocks.
33    Block,
34}
35
36/// Real chunk-section emptiness transition that must be applied before block checks.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct LightSectionEmptinessChange {
39    /// World section whose real block-section emptiness changed.
40    pub section_pos: SectionPos,
41    /// New emptiness value for the real block section.
42    pub empty: bool,
43}
44
45/// Returns whether vanilla must re-check lighting after a block-state change.
46#[must_use]
47pub fn has_different_light_properties(old_state: BlockStateId, new_state: BlockStateId) -> bool {
48    old_state != new_state
49        && (old_state.get_light_dampening() != new_state.get_light_dampening()
50            || old_state.get_light_emission() != new_state.get_light_emission()
51            || old_state.use_shape_for_light_occlusion()
52            || new_state.use_shape_for_light_occlusion())
53}
54
55/// Returns vanilla's simple opacity for light propagation.
56///
57/// Vanilla clamps block light dampening to at least one while propagating
58/// through neighbors.
59#[must_use]
60pub fn get_light_opacity(state: BlockStateId) -> u8 {
61    state.get_light_dampening().max(MIN_LIGHT_OPACITY)
62}
63
64/// Returns the occlusion shape vanilla lighting uses for a block state.
65#[must_use]
66pub fn light_occlusion_shape(state: BlockStateId) -> VoxelShape {
67    if !state.get_block().config.can_occlude || !state.use_shape_for_light_occlusion() {
68        return VoxelShape::EMPTY;
69    }
70
71    state.get_occlusion_shape()
72}
73
74/// Returns vanilla's `LightEngine.getLightDampeningInto` result.
75#[must_use]
76pub fn get_light_block_into(
77    from_state: BlockStateId,
78    to_state: BlockStateId,
79    direction: Direction,
80    simple_opacity: u8,
81) -> u8 {
82    let from_shape = light_occlusion_shape(from_state);
83    let to_shape = light_occlusion_shape(to_state);
84    if from_shape.is_empty() && to_shape.is_empty() {
85        return simple_opacity;
86    }
87
88    if merged_face_occludes(from_shape, to_shape, direction) {
89        LIGHT_BLOCKED
90    } else {
91        simple_opacity
92    }
93}
94
95/// Returns whether the selected state faces fully occlude light.
96#[must_use]
97pub fn light_face_occludes(
98    from_state: BlockStateId,
99    to_state: BlockStateId,
100    direction: Direction,
101) -> bool {
102    let from_shape = light_occlusion_shape(from_state);
103    let to_shape = light_occlusion_shape(to_state);
104    face_shape_occludes(from_shape, direction, to_shape, direction.opposite())
105}
106
107mod cache;
108mod data_layer;
109mod packet;
110mod propagation;
111mod queue;
112mod section_storage;
113mod sky_propagation;
114mod sky_sources;
115mod storage;
116mod work_gate;
117mod workset;
118
119pub use cache::{
120    CachedLightBlock, CachedLightChunk, CachedLightSection, LIGHT_CACHE_CHUNK_SLOTS,
121    LIGHT_CACHE_DIAMETER, LIGHT_CACHE_RADIUS, LIGHT_CACHE_SECTION_RADIUS, LightCacheChunkScope,
122    LightCacheLayout, LightCacheSetupChunks, LightCacheSetupRadius, LightChunkSectionSlots,
123    LightChunkSlotArray, LightSectionSlotArray, LightUpdateNotificationCache, PackedLightBlockPos,
124};
125pub use data_layer::{DataLayer, DataLayerLengthError};
126pub use packet::{build_chunk_light_update_packet, build_chunk_light_update_packet_for_sections};
127pub use propagation::{
128    BlockLightChunkEdgeChecks, BlockLightPropagationContext, BlockLightPropagationContextError,
129    BlockLightUpdateResult, check_block_light_chunk_edges, force_load_block_light_chunk,
130    load_block_light_chunk, propagate_block_light_changes,
131    propagate_block_light_changes_with_empty_sections, propagate_block_light_chunk,
132};
133pub use queue::{
134    LightAxisDirection, LightDirectionSet, LightDirectionSetIter, LightPropagationQueue,
135    LightPropagationQueues, LightQueueEntry, LightQueueFlags, PackedLightPropagationQueue,
136    PackedLightPropagationQueues, PackedLightQueueEntry, PooledPackedLightQueues,
137    QueuedLightUpdate,
138};
139pub use section_storage::{LightSectionRange, LightSectionRangeError};
140pub use sky_propagation::{
141    SkyLightChunkEdgeChecks, SkyLightPropagationContext, SkyLightPropagationContextError,
142    SkyLightUpdateResult, check_sky_light_chunk_edges, force_load_sky_light_chunk,
143    load_sky_light_chunk, propagate_sky_light_changes,
144    propagate_sky_light_changes_with_empty_sections, propagate_sky_light_chunk,
145    propagate_sky_light_chunk_without_edge_checks,
146};
147pub use sky_sources::ChunkSkyLightSources;
148pub use storage::{
149    ChunkLightData, ChunkLightEmptinessMapLengthError, ChunkLightLayerStorage, LightSection,
150    LightSectionData,
151};
152pub(crate) use work_gate::{LightWorkWindowGate, LightWorkWindowReservation};
153pub use workset::{
154    LightChunkReadCache, LightLayerEdit, LightSectionReadCache, LightWorkset,
155    LightWorksetSetupError,
156};
157
158#[cfg(test)]
159mod tests {
160    use steel_registry::{
161        blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
162        init_vanilla_registry, vanilla_blocks,
163    };
164    use steel_utils::BlockStateId;
165    use steel_utils::{BlockPos, ChunkPos, SectionPos};
166    use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
167
168    use crate::{
169        behavior::init_behaviors,
170        chunk::section::{ChunkSection, Sections},
171    };
172
173    use super::{
174        ChunkLightData, ChunkSkyLightSources, DATA_LAYER_SIZE, DataLayer, LightLayer, LightSection,
175        LightSectionData, LightSectionRange, MAX_LIGHT_LEVEL, build_chunk_light_update_packet,
176        build_chunk_light_update_packet_for_sections, get_light_opacity,
177        has_different_light_properties,
178    };
179
180    fn init_light_tests() {
181        init_vanilla_registry();
182        init_behaviors();
183    }
184
185    fn empty_sections(section_count: usize) -> Sections {
186        let sections: Vec<ChunkSection> = (0..section_count)
187            .map(|_| ChunkSection::new_empty())
188            .collect();
189        Sections::from_owned(sections.into_boxed_slice())
190    }
191
192    fn single_section_with_block(local_y: usize, state: BlockStateId) -> Sections {
193        let mut section = ChunkSection::new_empty();
194        section.set_block_state(0, local_y, 0, state);
195        Sections::from_owned(vec![section].into_boxed_slice())
196    }
197
198    fn new_test_sky_sources() -> ChunkSkyLightSources {
199        let Ok(sources) = ChunkSkyLightSources::new(0, 16) else {
200            panic!("valid single-section height rejected");
201        };
202        sources
203    }
204
205    fn mask_bit(mask: &[u64], index: usize) -> bool {
206        (mask[index / 64] & (1 << (index % 64))) != 0
207    }
208
209    #[test]
210    fn data_layer_uses_vanilla_low_nibble_first_order() {
211        let mut layer = DataLayer::new();
212
213        layer.set(0, 0, 0, 5);
214        layer.set(1, 0, 0, 12);
215        layer.set(1, 2, 3, 31);
216
217        assert_eq!(layer.get(0, 0, 0), 5);
218        assert_eq!(layer.get(1, 0, 0), 12);
219        assert_eq!(layer.get(1, 2, 3), MAX_LIGHT_LEVEL);
220        assert_eq!(layer.get(2, 2, 3), 0);
221
222        let bytes = layer.to_bytes();
223        assert_eq!(bytes[0], 0xC5);
224    }
225
226    #[test]
227    fn data_layer_preserves_homogeneous_non_zero_without_backing_bytes() {
228        let layer = DataLayer::filled(15);
229
230        assert!(layer.is_homogeneous());
231        assert!(!layer.is_empty());
232        assert_eq!(layer.homogeneous_value(), Some(15));
233        assert!(layer.to_bytes().iter().all(|byte| *byte == 0xFF));
234    }
235
236    #[test]
237    fn light_section_range_matches_vanilla_padded_section_range() {
238        let range = LightSectionRange::from_world_height(
239            OverworldNoiseSettings::MIN_Y,
240            OverworldNoiseSettings::HEIGHT,
241        )
242        .expect("vanilla overworld height should produce a light range");
243
244        assert_eq!(range.min_section_y(), -5);
245        assert_eq!(range.max_section_y_exclusive(), 21);
246        assert_eq!(range.section_count(), 26);
247        assert_eq!(range.chunk_section_count(), 24);
248        assert_eq!(range.section_index(-5), Some(0));
249        assert_eq!(range.section_y(25), Some(20));
250        assert_eq!(range.section_index(21), None);
251    }
252
253    #[test]
254    fn chunk_light_packet_omits_missing_and_internal_sections() {
255        let mut light = ChunkLightData::for_valid_world_height(0, 16);
256        *light.sky.section_mut(0).expect("real section in range") =
257            LightSection::internal(LightSectionData::homogeneous(15));
258        *light.block.section_mut(0).expect("real section in range") = LightSection::missing();
259
260        let packet = build_chunk_light_update_packet(&light, true);
261
262        assert!(!mask_bit(&packet.sky_y_mask.0, 1));
263        assert!(!mask_bit(&packet.empty_sky_y_mask.0, 1));
264        assert_eq!(packet.sky_updates.len(), 0);
265        assert!(!mask_bit(&packet.block_y_mask.0, 1));
266        assert!(!mask_bit(&packet.empty_block_y_mask.0, 1));
267        assert_eq!(packet.block_updates.len(), 0);
268    }
269
270    #[test]
271    fn chunk_light_packet_uses_empty_mask_for_visible_zero_sections() {
272        let mut light = ChunkLightData::for_valid_world_height(0, 16);
273        *light.block.section_mut(0).expect("real section in range") =
274            LightSection::visible(LightSectionData::homogeneous(0));
275
276        let packet = build_chunk_light_update_packet(&light, true);
277
278        assert!(!mask_bit(&packet.block_y_mask.0, 1));
279        assert!(mask_bit(&packet.empty_block_y_mask.0, 1));
280        assert_eq!(packet.block_updates.len(), 0);
281    }
282
283    #[test]
284    fn chunk_light_packet_expands_visible_homogeneous_non_zero_sections() {
285        let mut light = ChunkLightData::for_valid_world_height(0, 16);
286        *light.sky.section_mut(0).expect("real section in range") =
287            LightSection::visible(LightSectionData::homogeneous(15));
288
289        let packet = build_chunk_light_update_packet(&light, true);
290
291        assert!(mask_bit(&packet.sky_y_mask.0, 1));
292        assert!(!mask_bit(&packet.empty_sky_y_mask.0, 1));
293        assert_eq!(packet.sky_updates.len(), 1);
294        assert_eq!(packet.sky_updates[0].len(), DATA_LAYER_SIZE);
295        assert!(packet.sky_updates[0].iter().all(|byte| *byte == 0xFF));
296    }
297
298    #[test]
299    fn chunk_light_packet_omits_sky_layer_when_dimension_has_no_skylight() {
300        let mut light = ChunkLightData::for_valid_world_height(0, 16);
301        *light.sky.section_mut(0).expect("real section in range") =
302            LightSection::visible(LightSectionData::homogeneous(15));
303
304        let packet = build_chunk_light_update_packet(&light, false);
305
306        assert_eq!(packet.sky_updates.len(), 0);
307        assert!(!mask_bit(&packet.sky_y_mask.0, 1));
308        assert!(!mask_bit(&packet.empty_sky_y_mask.0, 1));
309    }
310
311    #[test]
312    fn changed_section_packet_preserves_ascending_light_section_order() {
313        let chunk_pos = ChunkPos::new(3, -2);
314        let mut light = ChunkLightData::for_valid_world_height(0, 48);
315        *light.block.section_mut(2).expect("upper section in range") =
316            LightSection::visible(LightSectionData::homogeneous(3));
317        *light.block.section_mut(0).expect("lower section in range") =
318            LightSection::visible(LightSectionData::homogeneous(7));
319
320        let packet = build_chunk_light_update_packet_for_sections(
321            chunk_pos,
322            &light,
323            true,
324            &[],
325            &[
326                SectionPos::new(chunk_pos.0.x, 2, chunk_pos.0.y),
327                SectionPos::new(chunk_pos.0.x, 0, chunk_pos.0.y),
328            ],
329        );
330
331        assert_eq!(packet.block_updates.len(), 2);
332        assert!(packet.block_updates[0].iter().all(|byte| *byte == 0x77));
333        assert!(packet.block_updates[1].iter().all(|byte| *byte == 0x33));
334    }
335
336    #[test]
337    fn chunk_light_data_reads_visible_block_and_sky_light() {
338        let mut light = ChunkLightData::for_valid_world_height(0, 16);
339        let pos = BlockPos::new(1, 2, 3);
340        let mut data = LightSectionData::homogeneous(0);
341        data.set(1, 2, 3, 12);
342        *light.block.section_mut(0).expect("real section in range") = LightSection::visible(data);
343
344        assert_eq!(light.get_light_value(LightLayer::Block, pos), 12);
345        assert_eq!(light.get_light_value(LightLayer::Sky, pos), 15);
346    }
347
348    #[test]
349    fn sections_collect_block_light_sources_in_scalable_lux_order() {
350        init_light_tests();
351
352        let torch = vanilla_blocks::TORCH.default_state();
353        let lantern = vanilla_blocks::SEA_LANTERN.default_state();
354        let mut lower = ChunkSection::new_empty();
355        lower.set_block_state(3, 4, 5, torch);
356        lower.set_block_state(1, 0, 0, lantern);
357        let mut upper = ChunkSection::new_empty();
358        upper.set_block_state(15, 15, 15, lantern);
359        let sections =
360            Sections::from_owned(vec![lower, ChunkSection::new_empty(), upper].into_boxed_slice());
361
362        assert_eq!(
363            sections.block_light_sources(ChunkPos::new(2, -3), -16),
364            vec![
365                BlockPos::new(33, -16, -48),
366                BlockPos::new(35, -12, -43),
367                BlockPos::new(47, 31, -33),
368            ]
369        );
370    }
371
372    #[test]
373    fn light_opacity_uses_vanilla_minimum_opacity() {
374        init_light_tests();
375        let air = vanilla_blocks::AIR.default_state();
376        let stone = vanilla_blocks::STONE.default_state();
377
378        assert_eq!(get_light_opacity(air), 1);
379        assert_eq!(get_light_opacity(stone), 15);
380    }
381
382    #[test]
383    fn different_light_properties_match_vanilla_conditions() {
384        init_light_tests();
385        let air = vanilla_blocks::AIR.default_state();
386        let stone = vanilla_blocks::STONE.default_state();
387
388        assert!(!has_different_light_properties(air, air));
389        assert!(has_different_light_properties(air, stone));
390
391        let light = vanilla_blocks::LIGHT.default_state();
392        let dim_light = light.set_value(&BlockStateProperties::LEVEL, 7);
393        assert!(has_different_light_properties(light, dim_light));
394    }
395
396    #[test]
397    fn sky_light_sources_empty_chunk_extends_below_world() {
398        init_light_tests();
399        let sections = empty_sections(1);
400        let mut sources = new_test_sky_sources();
401
402        sources.fill_from_sections(&sections);
403
404        assert_eq!(sources.get_lowest_source_y(0, 0), i32::MIN);
405        assert_eq!(sources.get_lowest_source_y(15, 15), i32::MIN);
406        assert_eq!(sources.get_highest_lowest_source_y(), i32::MIN);
407    }
408
409    #[test]
410    fn sky_light_sources_find_lowest_occluding_edge() {
411        init_light_tests();
412        let stone = vanilla_blocks::STONE.default_state();
413        let sections = single_section_with_block(4, stone);
414        let mut sources = new_test_sky_sources();
415
416        sources.fill_from_sections(&sections);
417
418        assert_eq!(sources.get_lowest_source_y(0, 0), 5);
419        assert_eq!(sources.get_lowest_source_y(1, 0), i32::MIN);
420        assert_eq!(sources.get_highest_lowest_source_y(), 5);
421    }
422
423    #[test]
424    fn sky_light_sources_update_adds_and_removes_occluding_edge() {
425        init_light_tests();
426        let air = vanilla_blocks::AIR.default_state();
427        let stone = vanilla_blocks::STONE.default_state();
428        let sections = empty_sections(1);
429        let mut sources = new_test_sky_sources();
430        sources.fill_from_sections(&sections);
431
432        let added = sources.update(0, 4, 0, |_x, y, _z| if y == 4 { stone } else { air });
433
434        assert!(added);
435        assert_eq!(sources.get_lowest_source_y(0, 0), 5);
436
437        let removed = sources.update(0, 4, 0, |_x, _y, _z| air);
438
439        assert!(removed);
440        assert_eq!(sources.get_lowest_source_y(0, 0), i32::MIN);
441    }
442
443    #[test]
444    fn sky_light_sources_update_ignores_changes_below_current_source_edge() {
445        init_light_tests();
446        let stone = vanilla_blocks::STONE.default_state();
447        let sections = single_section_with_block(10, stone);
448        let mut sources = new_test_sky_sources();
449        sources.fill_from_sections(&sections);
450
451        let changed = sources.update(0, 4, 0, |_x, _y, _z| stone);
452
453        assert!(!changed);
454        assert_eq!(sources.get_lowest_source_y(0, 0), 11);
455    }
456}