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#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum BlockLightPropagationContextError {
14 WrongLayer {
16 layer: LightLayer,
18 },
19 LayoutMismatch {
21 section_layout: Box<LightCacheLayout>,
23 light_layout: Box<LightCacheLayout>,
25 },
26 MissingCenterChunk {
28 chunk_pos: ChunkPos,
30 },
31}
32
33impl BlockLightPropagationContextError {
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#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct BlockLightUpdateResult {
45 pub updated_sections: Vec<SectionPos>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum BlockLightChunkEdgeChecks {
52 Required,
54 Skipped,
56}
57
58pub fn propagate_block_light_changes(
65 workset: &LightWorkset,
66 positions: impl IntoIterator<Item = BlockPos>,
67) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
68 propagate_block_light_changes_with_empty_sections(workset, positions, [])
69}
70
71pub fn propagate_block_light_changes_with_empty_sections(
73 workset: &LightWorkset,
74 positions: impl IntoIterator<Item = BlockPos>,
75 empty_sections: impl IntoIterator<Item = LightSectionEmptinessChange>,
76) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
77 let empty_sections = empty_sections.into_iter().collect::<Vec<_>>();
78
79 workset.with_chunk_read_cache(|chunk_cache| {
80 let layout = chunk_cache.layout();
81 let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
83 return Ok(BlockLightUpdateResult {
84 updated_sections: Vec::new(),
85 });
86 };
87 if chunk_cache.chunk(center_slot).is_none() {
88 return Ok(BlockLightUpdateResult {
89 updated_sections: Vec::new(),
90 });
91 }
92
93 chunk_cache.with_section_read_cache(|section_cache| {
94 chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
95 let mut queues = PackedLightPropagationQueues::new();
96
97 {
98 apply_block_empty_section_changes(
99 section_cache,
100 &mut light_edit,
101 &empty_sections,
102 );
103 let mut context = BlockLightPropagationContext::new(
104 section_cache,
105 &mut light_edit,
106 &mut queues,
107 )?;
108 for position in positions {
109 context.check_block(position);
110 }
111 context.perform_light_decrease();
112 }
113
114 let mut updated_sections = Vec::new();
115 light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
116 Ok(BlockLightUpdateResult { updated_sections })
117 })
118 })
119 })
120}
121
122fn apply_block_empty_section_changes(
123 sections: &LightSectionReadCache<'_>,
124 light: &mut LightLayerEdit<'_>,
125 empty_sections: &[LightSectionEmptinessChange],
126) -> usize {
127 let mut changed_chunks = Vec::new();
128 for change in empty_sections {
129 light.set_section_empty(change.section_pos, change.empty);
130 let chunk_pos = ChunkPos::new(change.section_pos.x(), change.section_pos.z());
131 if !changed_chunks.contains(&chunk_pos) {
132 changed_chunks.push(chunk_pos);
133 }
134 }
135
136 let mut initialized = 0;
137 for chunk_pos in changed_chunks {
138 initialized += sync_block_empty_light_sections(sections, light, chunk_pos);
139 }
140 initialized
141}
142
143pub fn propagate_block_light_chunk(
149 workset: &LightWorkset,
150 edge_checks: BlockLightChunkEdgeChecks,
151) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
152 workset.with_chunk_read_cache(|chunk_cache| {
153 let layout = chunk_cache.layout();
154 let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
155 return Err(BlockLightPropagationContextError::MissingCenterChunk {
156 chunk_pos: layout.center_chunk(),
157 });
158 };
159 let Some(center_chunk) = chunk_cache.chunk(center_slot) else {
160 return Err(BlockLightPropagationContextError::MissingCenterChunk {
161 chunk_pos: layout.center_chunk(),
162 });
163 };
164 let sources = center_chunk.block_light_sources();
165
166 chunk_cache.with_section_read_cache(|section_cache| {
167 chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
168 let mut queues = PackedLightPropagationQueues::new();
169
170 {
171 light_edit.reset_chunk_sections_to_missing(layout.center_chunk());
172 sync_block_empty_light_sections(
173 section_cache,
174 &mut light_edit,
175 layout.center_chunk(),
176 );
177 let mut context = BlockLightPropagationContext::new(
178 section_cache,
179 &mut light_edit,
180 &mut queues,
181 )?;
182 context.seed_block_light_sources(sources);
183 match edge_checks {
184 BlockLightChunkEdgeChecks::Required => {
185 context.perform_light_increase();
186 context.check_chunk_edges(layout.center_chunk());
187 }
188 BlockLightChunkEdgeChecks::Skipped => {
189 context.propagate_neighbor_levels(layout.center_chunk());
190 context.perform_light_increase();
191 }
192 }
193 }
194
195 let mut updated_sections = Vec::new();
196 light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
197 Ok(BlockLightUpdateResult { updated_sections })
198 })
199 })
200 })
201}
202
203pub fn force_load_block_light_chunk(
209 workset: &LightWorkset,
210) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
211 workset.with_chunk_read_cache(|chunk_cache| {
212 let layout = ensure_center_chunk(chunk_cache)?;
213
214 chunk_cache.with_section_read_cache(|section_cache| {
215 chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
216 sync_block_empty_light_sections(
217 section_cache,
218 &mut light_edit,
219 layout.center_chunk(),
220 );
221
222 let mut updated_sections = Vec::new();
223 light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
224 Ok(BlockLightUpdateResult { updated_sections })
225 })
226 })
227 })
228}
229
230pub fn check_block_light_chunk_edges(
236 workset: &LightWorkset,
237) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
238 workset.with_chunk_read_cache(|chunk_cache| {
239 let layout = ensure_center_chunk(chunk_cache)?;
240
241 chunk_cache.with_section_read_cache(|section_cache| {
242 chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
243 let mut queues = PackedLightPropagationQueues::new();
244
245 {
246 let mut context = BlockLightPropagationContext::new(
247 section_cache,
248 &mut light_edit,
249 &mut queues,
250 )?;
251 context.check_chunk_edges(layout.center_chunk());
252 }
253
254 let mut updated_sections = Vec::new();
255 light_edit.commit(None, |section_pos| updated_sections.push(section_pos));
256 Ok(BlockLightUpdateResult { updated_sections })
257 })
258 })
259 })
260}
261
262pub fn load_block_light_chunk(
267 workset: &LightWorkset,
268) -> Result<BlockLightUpdateResult, BlockLightPropagationContextError> {
269 let mut updated_sections = force_load_block_light_chunk(workset)?.updated_sections;
270 updated_sections.extend(check_block_light_chunk_edges(workset)?.updated_sections);
271 Ok(BlockLightUpdateResult { updated_sections })
272}
273
274fn ensure_center_chunk(
275 chunk_cache: &super::LightChunkReadCache<'_>,
276) -> Result<LightCacheLayout, BlockLightPropagationContextError> {
277 let layout = chunk_cache.layout();
278 let Some(center_slot) = layout.cached_chunk(layout.center_chunk()) else {
279 return Err(BlockLightPropagationContextError::MissingCenterChunk {
280 chunk_pos: layout.center_chunk(),
281 });
282 };
283 if chunk_cache.chunk(center_slot).is_none() {
284 return Err(BlockLightPropagationContextError::MissingCenterChunk {
285 chunk_pos: layout.center_chunk(),
286 });
287 }
288
289 Ok(layout)
290}
291
292fn sync_block_empty_light_sections(
293 sections: &LightSectionReadCache<'_>,
294 light: &mut LightLayerEdit<'_>,
295 chunk_pos: ChunkPos,
296) -> usize {
297 let layout = sections.layout();
298 let mut initialized = 0;
299
300 for section_y in
301 (layout.range().min_chunk_section_y()..layout.range().max_chunk_section_y_exclusive()).rev()
302 {
303 let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
304 if !section_is_non_empty(sections, light, section_pos) {
305 continue;
306 }
307
308 for offset_z in -1..=1 {
309 for offset_x in -1..=1 {
310 for offset_y in (-1..=1).rev() {
311 let target = SectionPos::new(
312 chunk_pos.0.x + offset_x,
313 section_y + offset_y,
314 chunk_pos.0.y + offset_z,
315 );
316 if light.set_section_non_missing(target) {
317 initialized += 1;
318 }
319 }
320 }
321 }
322 }
323
324 for offset_z in -1..=1 {
325 for offset_x in -1..=1 {
326 let target_chunk = ChunkPos::new(chunk_pos.0.x + offset_x, chunk_pos.0.y + offset_z);
327
328 for section_y in
329 (layout.range().min_section_y()..layout.range().max_section_y_exclusive()).rev()
330 {
331 let section_pos = SectionPos::new(target_chunk.0.x, section_y, target_chunk.0.y);
332 match section_neighborhood_all_empty_if_known(sections, target_chunk, section_y) {
333 Some(true) => {
334 light.set_section_internal(section_pos);
335 }
336 Some(false) => {
337 if light.set_section_non_missing(section_pos) {
338 initialized += 1;
339 }
340 }
341 None => {
342 if !section_neighborhood_all_empty(sections, light, target_chunk, section_y)
343 && light.set_section_non_missing(section_pos)
344 {
345 initialized += 1;
346 }
347 }
348 }
349 }
350 }
351 }
352
353 initialized
354}
355
356fn section_neighborhood_all_empty(
357 sections: &LightSectionReadCache<'_>,
358 light: &LightLayerEdit<'_>,
359 chunk_pos: ChunkPos,
360 section_y: i32,
361) -> bool {
362 for offset_y in -1..=1 {
363 let neighbor_y = section_y + offset_y;
364 if neighbor_y < sections.layout().range().min_chunk_section_y()
365 || neighbor_y >= sections.layout().range().max_chunk_section_y_exclusive()
366 {
367 continue;
368 }
369
370 for offset_z in -1..=1 {
371 for offset_x in -1..=1 {
372 let section_pos = SectionPos::new(
373 chunk_pos.0.x + offset_x,
374 neighbor_y,
375 chunk_pos.0.y + offset_z,
376 );
377 if section_is_non_empty(sections, light, section_pos) {
378 return false;
379 }
380 }
381 }
382 }
383
384 true
385}
386
387fn section_neighborhood_all_empty_if_known(
388 sections: &LightSectionReadCache<'_>,
389 chunk_pos: ChunkPos,
390 section_y: i32,
391) -> Option<bool> {
392 for offset_y in -1..=1 {
393 let neighbor_y = section_y + offset_y;
394 if neighbor_y < sections.layout().range().min_chunk_section_y()
395 || neighbor_y >= sections.layout().range().max_chunk_section_y_exclusive()
396 {
397 continue;
398 }
399
400 for offset_z in -1..=1 {
401 for offset_x in -1..=1 {
402 let section_pos = SectionPos::new(
403 chunk_pos.0.x + offset_x,
404 neighbor_y,
405 chunk_pos.0.y + offset_z,
406 );
407 let empty = sections.section_empty(section_pos)?;
408 if !empty {
409 return Some(false);
410 }
411 }
412 }
413 }
414
415 Some(true)
416}
417
418fn section_is_non_empty(
419 sections: &LightSectionReadCache<'_>,
420 light: &LightLayerEdit<'_>,
421 section_pos: SectionPos,
422) -> bool {
423 if let Some(empty) = sections.section_empty(section_pos) {
424 return !empty;
425 }
426
427 if let Some(empty) = light.section_empty(section_pos) {
428 return !empty;
429 }
430
431 sections.has_non_empty_section(section_pos)
432}
433
434pub struct BlockLightPropagationContext<'a, 'sections, 'light> {
440 layout: LightCacheLayout,
441 sections: &'a LightSectionReadCache<'sections>,
442 light: &'a mut LightLayerEdit<'light>,
443 queues: &'a mut PackedLightPropagationQueues,
444}
445
446impl<'a, 'sections, 'light> BlockLightPropagationContext<'a, 'sections, 'light> {
447 pub fn new(
449 sections: &'a LightSectionReadCache<'sections>,
450 light: &'a mut LightLayerEdit<'light>,
451 queues: &'a mut PackedLightPropagationQueues,
452 ) -> Result<Self, BlockLightPropagationContextError> {
453 if light.layer() != LightLayer::Block {
454 return Err(BlockLightPropagationContextError::WrongLayer {
455 layer: light.layer(),
456 });
457 }
458
459 if sections.layout() != light.layout() {
460 return Err(BlockLightPropagationContextError::layout_mismatch(
461 sections.layout(),
462 light.layout(),
463 ));
464 }
465
466 Ok(Self {
467 layout: light.layout(),
468 sections,
469 light,
470 queues,
471 })
472 }
473
474 pub fn check_block(&mut self, block_pos: BlockPos) -> bool {
478 let Some(cached_block) = self.layout.cached_block(block_pos) else {
479 return false;
480 };
481
482 let current_level = self.light.get(cached_block);
483 let block_state = self.sections.get_block_state(cached_block);
484 let emitted_level = block_state.get_light_emission() & MAX_LIGHT_LEVEL;
485
486 self.light.set(cached_block, emitted_level);
487 if emitted_level != 0 {
488 self.enqueue_increase(
489 block_pos,
490 emitted_level,
491 LightDirectionSet::all(),
492 Self::shape_flags(block_state),
493 );
494 }
495
496 self.enqueue_decrease(
497 block_pos,
498 current_level,
499 LightDirectionSet::all(),
500 LightQueueFlags::EMPTY,
501 );
502 true
503 }
504
505 pub fn seed_block_light_sources(&mut self, positions: impl IntoIterator<Item = BlockPos>) {
507 for position in positions {
508 self.seed_block_light_source(position);
509 }
510 }
511
512 pub fn propagate_neighbor_levels(&mut self, chunk_pos: ChunkPos) {
514 for section_y in (self.layout.range().min_section_y()
515 ..self.layout.range().max_section_y_exclusive())
516 .rev()
517 {
518 let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
519 if !self.light.has_non_missing_section(section_pos) {
520 continue;
521 }
522
523 for direction in LightAxisDirection::HORIZONTAL {
524 self.propagate_neighbor_level_section(chunk_pos, section_y, direction);
525 }
526 }
527 }
528
529 pub fn check_chunk_edges(&mut self, chunk_pos: ChunkPos) {
535 for section_y in (self.layout.range().min_section_y()
536 ..self.layout.range().max_section_y_exclusive())
537 .rev()
538 {
539 self.check_chunk_edge(chunk_pos, section_y);
540 }
541
542 self.perform_light_decrease();
543 }
544
545 #[must_use]
549 pub fn calculate_light_value(&self, block_pos: BlockPos, expect: u8) -> Option<u8> {
550 let cached_block = self.layout.cached_block(block_pos)?;
551 let center_state = self.sections.get_block_state(cached_block);
552 let mut level = center_state.get_light_emission() & MAX_LIGHT_LEVEL;
553
554 if level >= MAX_LIGHT_LEVEL - 1 || level > expect {
555 return Some(level);
556 }
557
558 let opacity = get_light_opacity(center_state);
559 if opacity >= MAX_LIGHT_LEVEL {
560 return Some(level);
561 }
562
563 for axis_direction in LightAxisDirection::ALL {
564 let neighbor_pos = Self::offset(block_pos, axis_direction);
565 let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
566 continue;
567 };
568 let neighbor_level = self.light.get(neighbor_block);
569 if neighbor_level.saturating_sub(1) <= level {
570 continue;
571 }
572
573 let neighbor_state = self.sections.get_block_state(neighbor_block);
574 let direction_from_neighbor = axis_direction.opposite().direction();
575 if get_light_block_into(
576 neighbor_state,
577 center_state,
578 direction_from_neighbor,
579 opacity,
580 ) == LIGHT_BLOCKED
581 {
582 continue;
583 }
584
585 level = level.max(neighbor_level.saturating_sub(opacity));
586 if level > expect {
587 return Some(level);
588 }
589 }
590
591 Some(level)
592 }
593
594 pub fn perform_light_decrease(&mut self) {
596 while let Some(entry) = self.queues.dequeue_decrease() {
597 let Some(source_block) = self.cached_block_from_entry(entry) else {
598 continue;
599 };
600 let source_state = if entry.has_sided_transparent_blocks() {
601 Some(self.sections.get_block_state(source_block))
602 } else {
603 None
604 };
605
606 for axis_direction in entry.directions().directions() {
607 let neighbor_pos = Self::offset(source_block.block_pos, axis_direction);
608 let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
609 continue;
610 };
611 if !self.light.has_non_missing(neighbor_block) {
612 continue;
613 }
614 let current_level = self.light.get(neighbor_block);
615 if current_level == 0 {
616 continue;
617 }
618
619 let neighbor_state = self.sections.get_block_state(neighbor_block);
620 let Some((target_level, flags)) = Self::target_level(
621 entry.level(),
622 source_state,
623 neighbor_state,
624 axis_direction.direction(),
625 true,
626 ) else {
627 continue;
628 };
629
630 if current_level > target_level {
631 self.enqueue_increase(
632 neighbor_pos,
633 current_level,
634 LightDirectionSet::all(),
635 flags.with(LightQueueFlags::RECHECK_LEVEL),
636 );
637 continue;
638 }
639
640 let emitted_light = neighbor_state.get_light_emission() & MAX_LIGHT_LEVEL;
641 if emitted_light != 0 {
642 self.enqueue_increase(
643 neighbor_pos,
644 emitted_light,
645 LightDirectionSet::all(),
646 flags.with(LightQueueFlags::WRITE_LEVEL),
647 );
648 }
649
650 self.light.set(neighbor_block, 0);
651 if target_level > 0 {
652 self.enqueue_decrease(
653 neighbor_pos,
654 target_level,
655 LightDirectionSet::all_except_opposite(axis_direction),
656 flags,
657 );
658 }
659 }
660 }
661
662 self.perform_light_increase();
663 }
664
665 pub fn perform_light_increase(&mut self) {
667 while let Some(entry) = self.queues.dequeue_increase() {
668 let Some(source_block) = self.cached_block_from_entry(entry) else {
669 continue;
670 };
671 if entry.should_recheck_level() {
672 if self.light.get(source_block) != entry.level() {
673 continue;
674 }
675 } else if entry.should_write_level() {
676 self.light.set(source_block, entry.level());
677 }
678
679 let source_state = if entry.has_sided_transparent_blocks() {
680 Some(self.sections.get_block_state(source_block))
681 } else {
682 None
683 };
684
685 for axis_direction in entry.directions().directions() {
686 let neighbor_pos = Self::offset(source_block.block_pos, axis_direction);
687 let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
688 continue;
689 };
690 if !self.light.has_non_missing(neighbor_block) {
691 continue;
692 }
693 let current_level = self.light.get(neighbor_block);
694 if current_level >= entry.level().saturating_sub(1) {
695 continue;
696 }
697
698 let neighbor_state = self.sections.get_block_state(neighbor_block);
699 let Some((target_level, flags)) = Self::target_level(
700 entry.level(),
701 source_state,
702 neighbor_state,
703 axis_direction.direction(),
704 false,
705 ) else {
706 continue;
707 };
708 if target_level <= current_level {
709 continue;
710 }
711
712 self.light.set(neighbor_block, target_level);
713 if target_level > 1 {
714 self.enqueue_increase(
715 neighbor_pos,
716 target_level,
717 LightDirectionSet::all_except_opposite(axis_direction),
718 flags,
719 );
720 }
721 }
722 }
723 }
724
725 fn cached_block_from_entry(&self, entry: PackedLightQueueEntry) -> Option<CachedLightBlock> {
726 self.layout.cached_block_from_packed(entry.block_pos())
727 }
728
729 fn enqueue_decrease(
730 &mut self,
731 block_pos: BlockPos,
732 level: u8,
733 directions: LightDirectionSet,
734 flags: LightQueueFlags,
735 ) {
736 let Some(packed_pos) = self.layout.encode_block_pos(block_pos) else {
737 return;
738 };
739 self.queues
740 .enqueue_decrease(PackedLightQueueEntry::from_parts(
741 packed_pos, level, directions, flags,
742 ));
743 }
744
745 fn check_chunk_edge(&mut self, chunk_pos: ChunkPos, section_y: i32) {
746 let current_section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
747 if !self.light.has_cached_section(current_section_pos) {
748 return;
749 }
750
751 for direction in LightAxisDirection::HORIZONTAL {
752 let (neighbor_offset_x, _, neighbor_offset_z) = direction.offset();
753 let neighbor_chunk_pos = ChunkPos::new(
754 chunk_pos.0.x + neighbor_offset_x,
755 chunk_pos.0.y + neighbor_offset_z,
756 );
757 let neighbor_section_pos =
758 SectionPos::new(neighbor_chunk_pos.0.x, section_y, neighbor_chunk_pos.0.y);
759 if !self.light.has_cached_section(neighbor_section_pos) {
760 continue;
761 }
762 if !self.light.has_light_data_section(current_section_pos)
763 && !self.light.has_light_data_section(neighbor_section_pos)
764 {
765 continue;
766 }
767
768 self.check_chunk_edge_direction(chunk_pos, neighbor_chunk_pos, section_y, direction);
769 }
770 }
771
772 fn check_chunk_edge_direction(
773 &mut self,
774 chunk_pos: ChunkPos,
775 neighbor_chunk_pos: ChunkPos,
776 section_y: i32,
777 direction: LightAxisDirection,
778 ) {
779 let (neighbor_offset_x, _, neighbor_offset_z) = direction.offset();
780 let (increment_x, increment_z, start_x, start_z) =
781 Self::current_edge_scan(chunk_pos, direction);
782 let mut center_delayed_checks = [0usize; 16 * 16];
783 let mut neighbor_delayed_checks = [0usize; 16 * 16];
784 let mut center_delayed_check_count = 0;
785 let mut neighbor_delayed_check_count = 0;
786
787 let min_y = section_y << 4;
788 let max_y = min_y | 15;
789 for y in min_y..=max_y {
790 let mut x = start_x;
791 let mut z = start_z;
792 for _ in 0..16 {
793 let current_pos = BlockPos::new(x, y, z);
794 let neighbor_pos = BlockPos::new(x + neighbor_offset_x, y, z + neighbor_offset_z);
795 let Some(current_block) = self.layout.cached_block(current_pos) else {
796 x += increment_x;
797 z += increment_z;
798 continue;
799 };
800 let Some(neighbor_block) = self.layout.cached_block(neighbor_pos) else {
801 x += increment_x;
802 z += increment_z;
803 continue;
804 };
805
806 let current_level = self.light.get(current_block);
807 if self
808 .calculate_light_value(current_pos, current_level)
809 .is_some_and(|calculated| calculated != current_level)
810 {
811 center_delayed_checks[center_delayed_check_count] = current_block.local_index;
812 center_delayed_check_count += 1;
813 }
814
815 let neighbor_level = self.light.get(neighbor_block);
816 if self
817 .calculate_light_value(neighbor_pos, neighbor_level)
818 .is_some_and(|calculated| calculated != neighbor_level)
819 {
820 neighbor_delayed_checks[neighbor_delayed_check_count] =
821 neighbor_block.local_index;
822 neighbor_delayed_check_count += 1;
823 }
824
825 x += increment_x;
826 z += increment_z;
827 }
828 }
829
830 let current_chunk_offset_x = chunk_pos.0.x << 4;
831 let current_chunk_offset_z = chunk_pos.0.y << 4;
832 let neighbor_chunk_offset_x = neighbor_chunk_pos.0.x << 4;
833 let neighbor_chunk_offset_z = neighbor_chunk_pos.0.y << 4;
834 let chunk_offset_y = section_y << 4;
835 let delayed_check_count = center_delayed_check_count.max(neighbor_delayed_check_count);
836 for delayed_check_index in 0..delayed_check_count {
837 if delayed_check_index < center_delayed_check_count {
838 let local_index = center_delayed_checks[delayed_check_index];
839 self.check_block(Self::block_pos_from_local_index(
840 current_chunk_offset_x,
841 chunk_offset_y,
842 current_chunk_offset_z,
843 local_index,
844 ));
845 }
846 if delayed_check_index < neighbor_delayed_check_count {
847 let local_index = neighbor_delayed_checks[delayed_check_index];
848 self.check_block(Self::block_pos_from_local_index(
849 neighbor_chunk_offset_x,
850 chunk_offset_y,
851 neighbor_chunk_offset_z,
852 local_index,
853 ));
854 }
855 }
856 }
857
858 fn seed_block_light_source(&mut self, block_pos: BlockPos) -> bool {
859 let Some(cached_block) = self.layout.cached_block(block_pos) else {
860 return false;
861 };
862
863 let block_state = self.sections.get_block_state(cached_block);
864 let emitted_level = block_state.get_light_emission() & MAX_LIGHT_LEVEL;
865 if emitted_level <= self.light.get(cached_block) {
866 return false;
867 }
868
869 self.enqueue_increase(
870 block_pos,
871 emitted_level,
872 LightDirectionSet::all(),
873 Self::shape_flags(block_state),
874 );
875 self.light.set(cached_block, emitted_level);
876 true
877 }
878
879 fn propagate_neighbor_level_section(
880 &mut self,
881 chunk_pos: ChunkPos,
882 section_y: i32,
883 direction: LightAxisDirection,
884 ) {
885 let (neighbor_offset_x, _, neighbor_offset_z) = direction.offset();
886 let neighbor_section_pos = SectionPos::new(
887 chunk_pos.0.x + neighbor_offset_x,
888 section_y,
889 chunk_pos.0.y + neighbor_offset_z,
890 );
891 if !self.light.has_light_data_section(neighbor_section_pos) {
892 return;
893 }
894
895 let (increment_x, increment_z, start_x, start_z) =
896 Self::neighbor_edge_scan(chunk_pos, direction);
897 let directions = LightDirectionSet::only(direction.opposite());
898 let flags = LightQueueFlags::EMPTY.with(LightQueueFlags::HAS_SIDED_TRANSPARENT_BLOCKS);
899
900 let min_y = section_y << 4;
901 let max_y = min_y | 15;
902 for y in min_y..=max_y {
903 let mut x = start_x;
904 let mut z = start_z;
905 for _ in 0..16 {
906 let source_pos = BlockPos::new(x, y, z);
907 let Some(source_block) = self.layout.cached_block(source_pos) else {
908 x += increment_x;
909 z += increment_z;
910 continue;
911 };
912 let level = self.light.get(source_block);
913 if level > 1 {
914 self.enqueue_increase(source_pos, level, directions, flags);
915 }
916 x += increment_x;
917 z += increment_z;
918 }
919 }
920 }
921
922 const fn current_edge_scan(
923 chunk_pos: ChunkPos,
924 direction: LightAxisDirection,
925 ) -> (i32, i32, i32, i32) {
926 let (offset_x, _, offset_z) = direction.offset();
927 if offset_x != 0 {
928 let start_x = if offset_x < 0 {
929 chunk_pos.0.x << 4
930 } else {
931 (chunk_pos.0.x << 4) | 15
932 };
933 return (0, 1, start_x, chunk_pos.0.y << 4);
934 }
935
936 let start_z = if offset_z < 0 {
937 chunk_pos.0.y << 4
938 } else {
939 (chunk_pos.0.y << 4) | 15
940 };
941 (1, 0, chunk_pos.0.x << 4, start_z)
942 }
943
944 const fn neighbor_edge_scan(
945 chunk_pos: ChunkPos,
946 direction: LightAxisDirection,
947 ) -> (i32, i32, i32, i32) {
948 let (offset_x, _, offset_z) = direction.offset();
949 if offset_x != 0 {
950 let start_x = if offset_x < 0 {
951 (chunk_pos.0.x << 4) - 1
952 } else {
953 (chunk_pos.0.x << 4) + 16
954 };
955 return (0, 1, start_x, chunk_pos.0.y << 4);
956 }
957
958 let start_z = if offset_z < 0 {
959 (chunk_pos.0.y << 4) - 1
960 } else {
961 (chunk_pos.0.y << 4) + 16
962 };
963 (1, 0, chunk_pos.0.x << 4, start_z)
964 }
965
966 const fn block_pos_from_local_index(
967 chunk_offset_x: i32,
968 chunk_offset_y: i32,
969 chunk_offset_z: i32,
970 local_index: usize,
971 ) -> BlockPos {
972 BlockPos::new(
973 chunk_offset_x | (local_index & 15) as i32,
974 chunk_offset_y | (local_index >> 8) as i32,
975 chunk_offset_z | ((local_index >> 4) & 15) as i32,
976 )
977 }
978
979 fn enqueue_increase(
980 &mut self,
981 block_pos: BlockPos,
982 level: u8,
983 directions: LightDirectionSet,
984 flags: LightQueueFlags,
985 ) {
986 let Some(packed_pos) = self.layout.encode_block_pos(block_pos) else {
987 return;
988 };
989 self.queues
990 .enqueue_increase(PackedLightQueueEntry::from_parts(
991 packed_pos, level, directions, flags,
992 ));
993 }
994
995 fn target_level(
996 propagated_level: u8,
997 source_state: Option<BlockStateId>,
998 target_state: BlockStateId,
999 direction: Direction,
1000 saturating: bool,
1001 ) -> Option<(u8, LightQueueFlags)> {
1002 let source_state = match source_state {
1003 Some(source_state) => source_state,
1004 None => Self::air(),
1005 };
1006 let opacity = get_light_block_into(
1007 source_state,
1008 target_state,
1009 direction,
1010 get_light_opacity(target_state),
1011 );
1012 if opacity == LIGHT_BLOCKED {
1013 return None;
1014 }
1015
1016 let target_level = if saturating {
1017 propagated_level.saturating_sub(opacity)
1018 } else if opacity >= propagated_level {
1019 return None;
1020 } else {
1021 propagated_level - opacity
1022 };
1023
1024 Some((target_level, Self::shape_flags(target_state)))
1025 }
1026
1027 fn shape_flags(block_state: BlockStateId) -> LightQueueFlags {
1028 if light_occlusion_shape(block_state).is_empty() {
1029 LightQueueFlags::EMPTY
1030 } else {
1031 LightQueueFlags::EMPTY.with(LightQueueFlags::HAS_SIDED_TRANSPARENT_BLOCKS)
1032 }
1033 }
1034
1035 const fn offset(block_pos: BlockPos, direction: LightAxisDirection) -> BlockPos {
1036 let (dx, dy, dz) = direction.offset();
1037 block_pos.offset(dx, dy, dz)
1038 }
1039
1040 fn air() -> BlockStateId {
1041 vanilla_blocks::AIR.default_state()
1042 }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use std::sync::{Arc, Weak};
1048
1049 use steel_registry::{
1050 blocks::properties::{BlockStateProperties, SlabType},
1051 init_vanilla_registry, vanilla_blocks,
1052 };
1053 use steel_utils::{ChunkPos, types::UpdateFlags};
1054
1055 use super::*;
1056 use crate::behavior::init_behaviors;
1057 use crate::chunk::{
1058 Chunk,
1059 chunk_holder::ChunkHolder,
1060 chunk_ticket_manager::ChunkTicketLevel,
1061 light::{LightCacheSetupRadius, LightSection, LightSectionData, LightSectionRange},
1062 section::{ChunkSection, Sections},
1063 status::ChunkStatus,
1064 };
1065
1066 fn init_tests() {
1067 init_vanilla_registry();
1068 init_behaviors();
1069 }
1070
1071 fn range() -> LightSectionRange {
1072 let Ok(range) = LightSectionRange::from_world_height(0, 16) else {
1073 panic!("test height should create a valid light range");
1074 };
1075 range
1076 }
1077
1078 fn holder_with_section(pos: ChunkPos, section: ChunkSection) -> Arc<ChunkHolder> {
1079 let sections = Sections::from_owned(vec![section].into_boxed_slice());
1080 let proto = Chunk::new(sections, pos, 0, 16, Weak::new());
1081 let holder = Arc::new(ChunkHolder::new(
1082 pos,
1083 ChunkTicketLevel::FULL_CHUNK,
1084 Some(ChunkTicketLevel::FULL_CHUNK),
1085 0,
1086 16,
1087 ));
1088 holder.insert_chunk(proto, ChunkStatus::Light);
1089 holder
1090 }
1091
1092 fn initialize_holder_light(holder: &ChunkHolder) {
1093 let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1094 panic!("test chunk should be available");
1095 };
1096 chunk.initialize_light_sources();
1097 }
1098
1099 fn set_block_section_non_missing(holder: &ChunkHolder, section_y: i32) {
1100 set_block_light_section(
1101 holder,
1102 section_y,
1103 LightSection::visible(LightSectionData::homogeneous(0)),
1104 );
1105 }
1106
1107 fn set_visible_block_light(
1108 holder: &ChunkHolder,
1109 section_y: i32,
1110 x: usize,
1111 y: usize,
1112 z: usize,
1113 level: u8,
1114 ) {
1115 let mut data = LightSectionData::homogeneous(0);
1116 data.set(x, y, z, level);
1117 set_block_light_section(holder, section_y, LightSection::visible(data));
1118 }
1119
1120 fn set_block_light_section(holder: &ChunkHolder, section_y: i32, section: LightSection) {
1121 let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1122 panic!("test chunk should be available");
1123 };
1124 let mut light = chunk.light_mut();
1125 let Some(target) = light.block.section_mut(section_y) else {
1126 panic!("test section should be inside light range");
1127 };
1128 *target = section;
1129 }
1130
1131 fn block_light_at(holder: &ChunkHolder, pos: BlockPos) -> u8 {
1132 let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1133 panic!("test chunk should be available");
1134 };
1135 chunk.light().get_light_value(LightLayer::Block, pos)
1136 }
1137
1138 #[test]
1139 fn context_requires_block_layer() {
1140 init_tests();
1141 let center = ChunkPos::new(0, 0);
1142 let holder = holder_with_section(center, ChunkSection::new_empty());
1143 set_block_section_non_missing(&holder, 0);
1144 let layout = LightCacheLayout::new(center, range());
1145 let Ok(workset) = LightWorkset::setup(
1146 layout,
1147 LightCacheSetupRadius::Inner,
1148 true,
1149 |pos| (pos == center).then(|| Arc::clone(&holder)),
1150 |_| true,
1151 ) else {
1152 panic!("relaxed setup should accept missing neighbors");
1153 };
1154
1155 workset.with_chunk_read_cache(|chunk_cache| {
1156 chunk_cache.with_section_read_cache(|section_cache| {
1157 chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
1158 let mut queues = PackedLightPropagationQueues::new();
1159 let result = BlockLightPropagationContext::new(
1160 section_cache,
1161 &mut light_edit,
1162 &mut queues,
1163 );
1164
1165 assert_eq!(
1166 result.err(),
1167 Some(BlockLightPropagationContextError::WrongLayer {
1168 layer: LightLayer::Sky,
1169 })
1170 );
1171 });
1172 });
1173 });
1174 }
1175
1176 #[test]
1177 fn block_light_runner_publishes_visible_updates() {
1178 init_tests();
1179 let center = ChunkPos::new(0, 0);
1180 let source_pos = BlockPos::new(1, 1, 1);
1181 let mut section = ChunkSection::new_empty();
1182 section.set_block_state(1, 1, 1, vanilla_blocks::LIGHT.default_state());
1183 let holder = holder_with_section(center, section);
1184 set_block_section_non_missing(&holder, 0);
1185 let layout = LightCacheLayout::new(center, range());
1186 let Ok(workset) = LightWorkset::setup(
1187 layout,
1188 LightCacheSetupRadius::Inner,
1189 true,
1190 |pos| (pos == center).then(|| Arc::clone(&holder)),
1191 |_| true,
1192 ) else {
1193 panic!("relaxed setup should accept missing neighbors");
1194 };
1195
1196 let Ok(result) = propagate_block_light_changes(&workset, [source_pos]) else {
1197 panic!("matching block caches should run block light updates");
1198 };
1199
1200 assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1201 assert_eq!(block_light_at(&holder, source_pos), 15);
1202 assert_eq!(block_light_at(&holder, BlockPos::new(2, 1, 1)), 14);
1203 }
1204
1205 #[test]
1206 fn block_light_changes_apply_empty_section_transitions() {
1207 init_tests();
1208 let center = ChunkPos::new(0, 0);
1209 let removed_pos = BlockPos::new(1, 1, 1);
1210 let mut holders = Vec::new();
1211 let mut center_holder = None;
1212 for z in -2..=2 {
1213 for x in -2..=2 {
1214 let pos = ChunkPos::new(x, z);
1215 let mut section = ChunkSection::new_empty();
1216 if pos == center {
1217 section.set_block_state(1, 1, 1, vanilla_blocks::STONE.default_state());
1218 }
1219 let holder = holder_with_section(pos, section);
1220 initialize_holder_light(&holder);
1221 if pos == center {
1222 center_holder = Some(Arc::clone(&holder));
1223 }
1224 holders.push((pos, holder));
1225 }
1226 }
1227 let Some(center_holder) = center_holder else {
1228 panic!("center holder should be created");
1229 };
1230 set_visible_block_light(¢er_holder, 0, 1, 1, 1, 9);
1231
1232 let Some(chunk) = center_holder.try_chunk(ChunkStatus::Empty) else {
1233 panic!("center chunk should be available");
1234 };
1235 assert_eq!(
1236 chunk.set_block_state_for_generation(
1237 ChunkStatus::Light,
1238 removed_pos,
1239 vanilla_blocks::AIR.default_state(),
1240 UpdateFlags::UPDATE_NONE,
1241 ),
1242 Some(vanilla_blocks::STONE.default_state())
1243 );
1244
1245 let layout = LightCacheLayout::new(center, range());
1246 let Ok(workset) = LightWorkset::setup(
1247 layout,
1248 LightCacheSetupRadius::Full,
1249 true,
1250 |pos| {
1251 holders
1252 .iter()
1253 .find(|(holder_pos, _)| *holder_pos == pos)
1254 .map(|(_, holder)| Arc::clone(holder))
1255 },
1256 |_| true,
1257 ) else {
1258 panic!("relaxed setup should accept cached test chunks");
1259 };
1260
1261 let Ok(result) = propagate_block_light_changes_with_empty_sections(
1262 &workset,
1263 [removed_pos],
1264 [LightSectionEmptinessChange {
1265 section_pos: SectionPos::new(0, 0, 0),
1266 empty: true,
1267 }],
1268 ) else {
1269 panic!("matching block caches should run block light updates");
1270 };
1271
1272 assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1273 let Some(chunk) = center_holder.try_chunk(ChunkStatus::Empty) else {
1274 panic!("center chunk should be available");
1275 };
1276 let light = chunk.light();
1277 assert_eq!(light.block.section_empty(0), Some(true));
1278 assert_eq!(light.get_light_value(LightLayer::Block, removed_pos), 0);
1279 assert!(matches!(
1280 light.block.section(0),
1281 Some(LightSection::Missing | LightSection::Internal(_))
1282 ));
1283 }
1284
1285 #[test]
1286 fn block_light_chunk_seeds_center_sources() {
1287 init_tests();
1288 let center = ChunkPos::new(0, 0);
1289 let source_pos = BlockPos::new(1, 1, 1);
1290 let mut section = ChunkSection::new_empty();
1291 section.set_block_state(1, 1, 1, vanilla_blocks::LIGHT.default_state());
1292 let holder = holder_with_section(center, section);
1293 let layout = LightCacheLayout::new(center, range());
1294 let Ok(workset) = LightWorkset::setup(
1295 layout,
1296 LightCacheSetupRadius::Inner,
1297 true,
1298 |pos| (pos == center).then(|| Arc::clone(&holder)),
1299 |_| true,
1300 ) else {
1301 panic!("relaxed setup should accept missing neighbors");
1302 };
1303
1304 let Ok(result) = propagate_block_light_chunk(&workset, BlockLightChunkEdgeChecks::Skipped)
1305 else {
1306 panic!("matching block caches should run block chunk lighting");
1307 };
1308
1309 assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1310 assert_eq!(block_light_at(&holder, source_pos), 15);
1311 assert_eq!(block_light_at(&holder, BlockPos::new(2, 1, 1)), 14);
1312 }
1313
1314 #[test]
1315 fn block_light_chunk_pulls_neighbor_edge_levels() {
1316 init_tests();
1317 let center = ChunkPos::new(0, 0);
1318 let east_chunk = ChunkPos::new(1, 0);
1319 let center_holder = holder_with_section(center, ChunkSection::new_empty());
1320 let mut east_section = ChunkSection::new_empty();
1321 east_section.set_block_state(0, 1, 1, vanilla_blocks::LIGHT.default_state());
1322 let east_holder = holder_with_section(east_chunk, east_section);
1323 set_visible_block_light(&east_holder, 0, 0, 1, 1, 15);
1324 let layout = LightCacheLayout::new(center, range());
1325 let Ok(workset) = LightWorkset::setup(
1326 layout,
1327 LightCacheSetupRadius::Inner,
1328 true,
1329 |pos| {
1330 if pos == center {
1331 Some(Arc::clone(¢er_holder))
1332 } else if pos == east_chunk {
1333 Some(Arc::clone(&east_holder))
1334 } else {
1335 None
1336 }
1337 },
1338 |_| true,
1339 ) else {
1340 panic!("relaxed setup should accept missing neighbors");
1341 };
1342
1343 let Ok(result) = propagate_block_light_chunk(&workset, BlockLightChunkEdgeChecks::Skipped)
1344 else {
1345 panic!("matching block caches should run block chunk lighting");
1346 };
1347
1348 assert!(result.updated_sections.contains(&SectionPos::new(0, 0, 0)));
1349 assert_eq!(block_light_at(¢er_holder, BlockPos::new(15, 1, 1)), 14);
1350 assert_eq!(block_light_at(¢er_holder, BlockPos::new(14, 1, 1)), 13);
1351 }
1352
1353 #[test]
1354 fn block_light_chunk_requires_center_chunk() {
1355 init_tests();
1356 let center = ChunkPos::new(0, 0);
1357 let layout = LightCacheLayout::new(center, range());
1358 let Ok(workset) = LightWorkset::setup(
1359 layout,
1360 LightCacheSetupRadius::Inner,
1361 true,
1362 |_| None,
1363 |_| true,
1364 ) else {
1365 panic!("relaxed setup should accept missing chunks");
1366 };
1367
1368 assert_eq!(
1369 propagate_block_light_chunk(&workset, BlockLightChunkEdgeChecks::Skipped).err(),
1370 Some(BlockLightPropagationContextError::MissingCenterChunk { chunk_pos: center })
1371 );
1372 }
1373
1374 #[test]
1375 fn block_light_changes_skip_missing_center_chunk() {
1376 init_tests();
1377 let center = ChunkPos::new(0, 0);
1378 let layout = LightCacheLayout::new(center, range());
1379 let Ok(workset) = LightWorkset::setup(
1380 layout,
1381 LightCacheSetupRadius::Full,
1382 true,
1383 |_| None,
1384 |_| true,
1385 ) else {
1386 panic!("relaxed setup should accept missing chunks");
1387 };
1388
1389 let Ok(result) = propagate_block_light_changes_with_empty_sections(
1390 &workset,
1391 [BlockPos::new(1, 1, 1)],
1392 [LightSectionEmptinessChange {
1393 section_pos: SectionPos::new(0, 0, 0),
1394 empty: true,
1395 }],
1396 ) else {
1397 panic!("dynamic block changes should skip a missing center chunk");
1398 };
1399
1400 assert!(result.updated_sections.is_empty());
1401 }
1402
1403 #[test]
1404 fn block_light_calculation_respects_occluding_faces() {
1405 init_tests();
1406 let center = ChunkPos::new(0, 0);
1407 let mut section = ChunkSection::new_empty();
1408 let bottom_slab = vanilla_blocks::STONE_SLAB
1409 .default_state()
1410 .set_value(&BlockStateProperties::SLAB_TYPE, SlabType::Bottom);
1411 section.set_block_state(1, 1, 1, bottom_slab);
1412 let holder = holder_with_section(center, section);
1413 set_block_section_non_missing(&holder, 0);
1414 let layout = LightCacheLayout::new(center, range());
1415 let Ok(workset) = LightWorkset::setup(
1416 layout,
1417 LightCacheSetupRadius::Inner,
1418 true,
1419 |pos| (pos == center).then(|| Arc::clone(&holder)),
1420 |_| true,
1421 ) else {
1422 panic!("relaxed setup should accept missing neighbors");
1423 };
1424
1425 workset.with_chunk_read_cache(|chunk_cache| {
1426 chunk_cache.with_section_read_cache(|section_cache| {
1427 chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
1428 let mut queues = PackedLightPropagationQueues::new();
1429 let Ok(context) = BlockLightPropagationContext::new(
1430 section_cache,
1431 &mut light_edit,
1432 &mut queues,
1433 ) else {
1434 panic!("matching block caches should build a propagation context");
1435 };
1436 let Some(below) = layout.cached_block(BlockPos::new(1, 0, 1)) else {
1437 panic!("below neighbor should be cached");
1438 };
1439 assert!(context.light.set(below, 15));
1440
1441 assert_eq!(
1442 context.calculate_light_value(BlockPos::new(1, 1, 1), 0),
1443 Some(0)
1444 );
1445 });
1446 });
1447 });
1448 }
1449}