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