1use std::iter::FusedIterator;
2
3use steel_utils::{BlockPos, ChunkPos, Direction, SectionPos};
4
5use super::LightSectionRange;
6
7pub const LIGHT_CACHE_RADIUS: i32 = 2;
9pub const LIGHT_CACHE_SECTION_RADIUS: i32 = 1;
11pub const LIGHT_CACHE_DIAMETER: usize = LIGHT_CACHE_RADIUS as usize * 2 + 1;
13pub const LIGHT_CACHE_CHUNK_SLOTS: usize = LIGHT_CACHE_DIAMETER * LIGHT_CACHE_DIAMETER;
15
16const LIGHT_CACHE_DIAMETER_I64: i64 = LIGHT_CACHE_DIAMETER as i64;
17const LIGHT_CACHE_CHUNK_SLOTS_I64: i64 = LIGHT_CACHE_CHUNK_SLOTS as i64;
18const LIGHT_CACHE_SECTION_RADIUS_I64: i64 = LIGHT_CACHE_SECTION_RADIUS as i64;
19const LIGHT_LOCAL_BLOCK_MASK: usize = 15;
20const LIGHT_LOCAL_BLOCK_Z_SHIFT: usize = 4;
21const LIGHT_LOCAL_BLOCK_Y_SHIFT: usize = 8;
22const LIGHT_ENCODED_HORIZONTAL_BITS: i64 = 6;
23const LIGHT_ENCODED_VERTICAL_BITS: i64 = 16;
24const LIGHT_ENCODED_HORIZONTAL_MASK: i64 = (1 << LIGHT_ENCODED_HORIZONTAL_BITS) - 1;
25const LIGHT_ENCODED_VERTICAL_MASK: i64 = (1 << LIGHT_ENCODED_VERTICAL_BITS) - 1;
26const LIGHT_ENCODED_POSITION_MASK: u32 =
27 (1 << (LIGHT_ENCODED_HORIZONTAL_BITS * 2 + LIGHT_ENCODED_VERTICAL_BITS)) - 1;
28const LIGHT_ENCODED_Z_SHIFT: u32 = LIGHT_ENCODED_HORIZONTAL_BITS as u32;
29const LIGHT_ENCODED_Y_SHIFT: u32 = (LIGHT_ENCODED_HORIZONTAL_BITS * 2) as u32;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct PackedLightBlockPos(u32);
38
39impl PackedLightBlockPos {
40 #[must_use]
42 pub const fn from_raw(raw: u32) -> Self {
43 Self(raw & LIGHT_ENCODED_POSITION_MASK)
44 }
45
46 #[must_use]
48 pub const fn raw(self) -> u32 {
49 self.0
50 }
51
52 #[must_use]
54 pub const fn encoded_x(self) -> u8 {
55 (self.0 & LIGHT_ENCODED_HORIZONTAL_MASK as u32) as u8
56 }
57
58 #[must_use]
60 pub const fn encoded_z(self) -> u8 {
61 ((self.0 >> LIGHT_ENCODED_Z_SHIFT) & LIGHT_ENCODED_HORIZONTAL_MASK as u32) as u8
62 }
63
64 #[must_use]
66 pub const fn encoded_y(self) -> u16 {
67 ((self.0 >> LIGHT_ENCODED_Y_SHIFT) & LIGHT_ENCODED_VERTICAL_MASK as u32) as u16
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum LightCacheChunkScope {
78 Inner,
80 Outer,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum LightCacheSetupRadius {
87 Inner,
89 Full,
91}
92
93impl LightCacheSetupRadius {
94 const fn chunk_radius(self) -> i32 {
95 match self {
96 Self::Inner => LIGHT_CACHE_SECTION_RADIUS,
97 Self::Full => LIGHT_CACHE_RADIUS,
98 }
99 }
100
101 const fn chunk_count(self) -> usize {
102 let diameter = self.chunk_radius() as usize * 2 + 1;
103 diameter * diameter
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct CachedLightChunk {
110 pub chunk_pos: ChunkPos,
112 pub chunk_slot: usize,
114 pub scope: LightCacheChunkScope,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct CachedLightSection {
121 pub section_pos: SectionPos,
123 pub section_slot: usize,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct CachedLightBlock {
134 pub block_pos: BlockPos,
136 pub section_slot: usize,
138 pub local_index: usize,
140}
141
142#[derive(Debug, Clone)]
144pub struct LightCacheSetupChunks {
145 layout: LightCacheLayout,
146 radius: i32,
147 next_dx: i32,
148 next_dz: i32,
149 remaining: usize,
150}
151
152impl Iterator for LightCacheSetupChunks {
153 type Item = CachedLightChunk;
154
155 fn next(&mut self) -> Option<Self::Item> {
156 if self.remaining == 0 {
157 return None;
158 }
159
160 let dx = self.next_dx;
161 let dz = self.next_dz;
162 self.remaining -= 1;
163
164 self.next_dx += 1;
165 if self.next_dx > self.radius {
166 self.next_dx = -self.radius;
167 self.next_dz += 1;
168 }
169
170 let chunk_x = self.layout.center_chunk.0.x + dx;
171 let chunk_z = self.layout.center_chunk.0.y + dz;
172 let chunk = self.layout.cached_chunk_by_coords(chunk_x, chunk_z);
173 if chunk.is_none() {
174 self.remaining = 0;
175 }
176 chunk
177 }
178
179 fn size_hint(&self) -> (usize, Option<usize>) {
180 (self.remaining, Some(self.remaining))
181 }
182}
183
184impl ExactSizeIterator for LightCacheSetupChunks {}
185
186impl FusedIterator for LightCacheSetupChunks {}
187
188#[derive(Debug, Clone)]
190pub struct LightChunkSlotArray<T> {
191 values: Box<[Option<T>]>,
192}
193
194impl<T> LightChunkSlotArray<T> {
195 #[must_use]
197 pub fn new() -> Self {
198 Self {
199 values: empty_option_slots(LIGHT_CACHE_CHUNK_SLOTS),
200 }
201 }
202
203 #[must_use]
205 pub fn slot_count(&self) -> usize {
206 self.values.len()
207 }
208
209 #[must_use]
211 pub fn is_clear(&self) -> bool {
212 self.values.iter().all(Option::is_none)
213 }
214
215 pub fn clear(&mut self) {
217 for value in &mut self.values {
218 *value = None;
219 }
220 }
221
222 pub fn insert(&mut self, chunk: CachedLightChunk, value: T) -> Option<T> {
224 self.insert_slot(chunk.chunk_slot, value)
225 }
226
227 pub fn insert_slot(&mut self, chunk_slot: usize, value: T) -> Option<T> {
229 self.values
230 .get_mut(chunk_slot)
231 .and_then(|slot| slot.replace(value))
232 }
233
234 #[must_use]
236 pub fn get(&self, chunk: CachedLightChunk) -> Option<&T> {
237 self.get_slot(chunk.chunk_slot)
238 }
239
240 pub fn get_mut(&mut self, chunk: CachedLightChunk) -> Option<&mut T> {
242 self.get_mut_slot(chunk.chunk_slot)
243 }
244
245 #[must_use]
247 pub fn get_slot(&self, chunk_slot: usize) -> Option<&T> {
248 self.values.get(chunk_slot).and_then(Option::as_ref)
249 }
250
251 pub fn get_mut_slot(&mut self, chunk_slot: usize) -> Option<&mut T> {
253 self.values.get_mut(chunk_slot).and_then(Option::as_mut)
254 }
255}
256
257impl<T> Default for LightChunkSlotArray<T> {
258 fn default() -> Self {
259 Self::new()
260 }
261}
262
263#[derive(Debug, Clone)]
265pub struct LightChunkSectionSlots {
266 layout: LightCacheLayout,
267 chunk_pos: ChunkPos,
268 next_section_y: i32,
269 end_section_y: i32,
270}
271
272impl Iterator for LightChunkSectionSlots {
273 type Item = CachedLightSection;
274
275 fn next(&mut self) -> Option<Self::Item> {
276 if self.next_section_y >= self.end_section_y {
277 return None;
278 }
279
280 let section_y = self.next_section_y;
281 self.next_section_y += 1;
282 let section = self.layout.cached_section(SectionPos::new(
283 self.chunk_pos.0.x,
284 section_y,
285 self.chunk_pos.0.y,
286 ));
287 if section.is_none() {
288 self.next_section_y = self.end_section_y;
289 }
290 section
291 }
292
293 fn size_hint(&self) -> (usize, Option<usize>) {
294 let remaining = (self.end_section_y - self.next_section_y).max(0) as usize;
295 (remaining, Some(remaining))
296 }
297}
298
299impl ExactSizeIterator for LightChunkSectionSlots {}
300
301impl FusedIterator for LightChunkSectionSlots {}
302
303#[derive(Debug, Clone)]
305pub struct LightSectionSlotArray<T> {
306 layout: LightCacheLayout,
307 values: Box<[Option<T>]>,
308}
309
310impl<T> LightSectionSlotArray<T> {
311 #[must_use]
313 pub fn new(layout: LightCacheLayout) -> Self {
314 Self {
315 layout,
316 values: empty_option_slots(layout.section_slot_count()),
317 }
318 }
319
320 #[must_use]
322 pub const fn layout(&self) -> LightCacheLayout {
323 self.layout
324 }
325
326 #[must_use]
328 pub fn slot_count(&self) -> usize {
329 self.values.len()
330 }
331
332 #[must_use]
334 pub fn is_clear(&self) -> bool {
335 self.values.iter().all(Option::is_none)
336 }
337
338 pub fn clear(&mut self) {
340 for value in &mut self.values {
341 *value = None;
342 }
343 }
344
345 pub fn insert(&mut self, section: CachedLightSection, value: T) -> Option<T> {
347 self.insert_slot(section.section_slot, value)
348 }
349
350 pub fn insert_slot(&mut self, section_slot: usize, value: T) -> Option<T> {
352 self.values
353 .get_mut(section_slot)
354 .and_then(|slot| slot.replace(value))
355 }
356
357 pub fn take_slot(&mut self, section_slot: usize) -> Option<T> {
359 self.values.get_mut(section_slot).and_then(Option::take)
360 }
361
362 #[must_use]
364 pub fn get(&self, section: CachedLightSection) -> Option<&T> {
365 self.get_slot(section.section_slot)
366 }
367
368 pub fn get_mut(&mut self, section: CachedLightSection) -> Option<&mut T> {
370 self.get_mut_slot(section.section_slot)
371 }
372
373 #[must_use]
375 pub fn get_slot(&self, section_slot: usize) -> Option<&T> {
376 self.values.get(section_slot).and_then(Option::as_ref)
377 }
378
379 pub fn get_mut_slot(&mut self, section_slot: usize) -> Option<&mut T> {
381 self.values.get_mut(section_slot).and_then(Option::as_mut)
382 }
383}
384
385#[derive(Debug, Clone)]
392pub struct LightUpdateNotificationCache {
393 layout: LightCacheLayout,
394 marked: Box<[bool]>,
395}
396
397impl LightUpdateNotificationCache {
398 #[must_use]
400 pub fn new(layout: LightCacheLayout) -> Self {
401 Self {
402 layout,
403 marked: vec![false; layout.section_slot_count()].into_boxed_slice(),
404 }
405 }
406
407 #[must_use]
409 pub const fn layout(&self) -> LightCacheLayout {
410 self.layout
411 }
412
413 #[must_use]
415 pub fn is_empty(&self) -> bool {
416 self.marked.iter().all(|marked| !marked)
417 }
418
419 pub fn clear(&mut self) {
421 self.marked.fill(false);
422 }
423
424 pub fn mark_section(&mut self, section_pos: SectionPos) -> bool {
426 let Some(section_slot) = self.layout.section_slot(section_pos) else {
427 return false;
428 };
429
430 self.mark_section_slot(section_slot)
431 }
432
433 pub fn mark_block_neighborhood(&mut self, block_pos: BlockPos) -> Option<usize> {
438 let mut contained = true;
439 sections_around_and_at_block_pos(block_pos, |section_pos| {
440 contained &= self.layout.section_slot(section_pos).is_some();
441 });
442 if !contained {
443 return None;
444 }
445
446 let mut newly_marked = 0;
447 sections_around_and_at_block_pos(block_pos, |section_pos| {
448 if self.mark_section(section_pos) {
449 newly_marked += 1;
450 }
451 });
452 Some(newly_marked)
453 }
454
455 #[must_use]
457 pub fn is_marked_section(&self, section_pos: SectionPos) -> bool {
458 let Some(section_slot) = self.layout.section_slot(section_pos) else {
459 return false;
460 };
461
462 self.is_marked_section_slot(section_slot)
463 }
464
465 #[must_use]
467 pub fn is_marked_section_slot(&self, section_slot: usize) -> bool {
468 self.marked.get(section_slot).copied().unwrap_or(false)
469 }
470
471 pub fn marked_section_positions(&self) -> impl Iterator<Item = SectionPos> + '_ {
473 self.marked
474 .iter()
475 .enumerate()
476 .filter_map(move |(section_slot, marked)| {
477 if *marked {
478 self.layout.section_pos_for_slot(section_slot)
479 } else {
480 None
481 }
482 })
483 }
484
485 fn mark_section_slot(&mut self, section_slot: usize) -> bool {
486 let Some(marked) = self.marked.get_mut(section_slot) else {
487 return false;
488 };
489
490 let newly_marked = !*marked;
491 *marked = true;
492 newly_marked
493 }
494}
495
496#[derive(Debug, Clone, Copy, PartialEq, Eq)]
504pub struct LightCacheLayout {
505 center_chunk: ChunkPos,
506 range: LightSectionRange,
507 cached_min_section_y: i32,
508 cached_section_count: usize,
509 chunk_index_offset: i64,
510 chunk_section_index_offset: i64,
511 encode_offset_x: i64,
512 encode_offset_y: i64,
513 encode_offset_z: i64,
514 encoded_min_block_x: i64,
515 encoded_min_block_z: i64,
516}
517
518impl LightCacheLayout {
519 #[must_use]
521 pub fn new(center_chunk: ChunkPos, range: LightSectionRange) -> Self {
522 let cached_min_section_y = range.min_section_y() - 1;
523 let chunk_offset_x = i64::from(LIGHT_CACHE_RADIUS) - i64::from(center_chunk.0.x);
524 let chunk_offset_z = i64::from(LIGHT_CACHE_RADIUS) - i64::from(center_chunk.0.y);
525 let chunk_index_offset = chunk_offset_x + LIGHT_CACHE_DIAMETER_I64 * chunk_offset_z;
526 let chunk_offset_y = -i64::from(cached_min_section_y);
527 let chunk_section_index_offset =
528 chunk_index_offset + LIGHT_CACHE_CHUNK_SLOTS_I64 * chunk_offset_y;
529 let center_block_x = i64::from(center_chunk.0.x) * 16 + 7;
530 let center_block_z = i64::from(center_chunk.0.y) * 16 + 7;
531 let encode_offset_x = 31 - center_block_x;
532 let encode_offset_y = -(i64::from(cached_min_section_y) * 16);
533 let encode_offset_z = 31 - center_block_z;
534
535 Self {
536 center_chunk,
537 range,
538 cached_min_section_y,
539 cached_section_count: range.section_count() + 2,
540 chunk_index_offset,
541 chunk_section_index_offset,
542 encode_offset_x,
543 encode_offset_y,
544 encode_offset_z,
545 encoded_min_block_x: center_block_x - 31,
546 encoded_min_block_z: center_block_z - 31,
547 }
548 }
549
550 #[must_use]
552 pub const fn center_chunk(self) -> ChunkPos {
553 self.center_chunk
554 }
555
556 #[must_use]
558 pub const fn range(self) -> LightSectionRange {
559 self.range
560 }
561
562 #[must_use]
564 pub const fn cached_min_section_y(self) -> i32 {
565 self.cached_min_section_y
566 }
567
568 #[must_use]
570 pub const fn cached_max_section_y_exclusive(self) -> i32 {
571 self.cached_min_section_y + self.cached_section_count as i32
572 }
573
574 #[must_use]
576 pub const fn cached_section_count(self) -> usize {
577 self.cached_section_count
578 }
579
580 #[must_use]
582 pub const fn section_slot_count(self) -> usize {
583 LIGHT_CACHE_CHUNK_SLOTS * self.cached_section_count
584 }
585
586 #[must_use]
588 pub const fn setup_chunks(self, radius: LightCacheSetupRadius) -> LightCacheSetupChunks {
589 let remaining = radius.chunk_count();
590 let radius = radius.chunk_radius();
591 LightCacheSetupChunks {
592 layout: self,
593 radius,
594 next_dx: -radius,
595 next_dz: -radius,
596 remaining,
597 }
598 }
599
600 #[must_use]
602 pub fn cached_chunk(self, chunk_pos: ChunkPos) -> Option<CachedLightChunk> {
603 self.cached_chunk_by_coords(chunk_pos.0.x, chunk_pos.0.y)
604 }
605
606 #[must_use]
608 pub fn cached_chunk_by_coords(self, chunk_x: i32, chunk_z: i32) -> Option<CachedLightChunk> {
609 let dx = i64::from(chunk_x) - i64::from(self.center_chunk.0.x);
610 let dz = i64::from(chunk_z) - i64::from(self.center_chunk.0.y);
611 let distance = dx.abs().max(dz.abs());
612 if distance > i64::from(LIGHT_CACHE_RADIUS) {
613 return None;
614 }
615
616 let scope = if distance <= LIGHT_CACHE_SECTION_RADIUS_I64 {
617 LightCacheChunkScope::Inner
618 } else {
619 LightCacheChunkScope::Outer
620 };
621
622 Some(CachedLightChunk {
623 chunk_pos: ChunkPos::new(chunk_x, chunk_z),
624 chunk_slot: self.chunk_slot_by_coords(chunk_x, chunk_z)?,
625 scope,
626 })
627 }
628
629 #[must_use]
631 pub fn chunk_slot(self, chunk_pos: ChunkPos) -> Option<usize> {
632 self.chunk_slot_by_coords(chunk_pos.0.x, chunk_pos.0.y)
633 }
634
635 #[must_use]
637 pub fn chunk_slot_by_coords(self, chunk_x: i32, chunk_z: i32) -> Option<usize> {
638 if !self.contains_chunk_coords(chunk_x, chunk_z) {
639 return None;
640 }
641
642 let slot = i64::from(chunk_x)
643 + LIGHT_CACHE_DIAMETER_I64 * i64::from(chunk_z)
644 + self.chunk_index_offset;
645 usize::try_from(slot).ok()
646 }
647
648 #[must_use]
650 pub const fn chunk_pos_for_slot(self, chunk_slot: usize) -> Option<ChunkPos> {
651 if chunk_slot >= LIGHT_CACHE_CHUNK_SLOTS {
652 return None;
653 }
654
655 Some(ChunkPos::new(
656 self.center_chunk.0.x - LIGHT_CACHE_RADIUS + (chunk_slot % LIGHT_CACHE_DIAMETER) as i32,
657 self.center_chunk.0.y - LIGHT_CACHE_RADIUS + (chunk_slot / LIGHT_CACHE_DIAMETER) as i32,
658 ))
659 }
660
661 #[must_use]
663 pub fn section_slot(self, section_pos: SectionPos) -> Option<usize> {
664 self.section_slot_by_coords(section_pos.x(), section_pos.y(), section_pos.z())
665 }
666
667 #[must_use]
669 pub fn cached_section(self, section_pos: SectionPos) -> Option<CachedLightSection> {
670 Some(CachedLightSection {
671 section_pos,
672 section_slot: self.section_slot(section_pos)?,
673 })
674 }
675
676 #[must_use]
678 pub const fn section_pos_for_slot(self, section_slot: usize) -> Option<SectionPos> {
679 if section_slot >= self.section_slot_count() {
680 return None;
681 }
682
683 let section_x = self.center_chunk.0.x - LIGHT_CACHE_RADIUS
684 + (section_slot % LIGHT_CACHE_DIAMETER) as i32;
685 let section_z = self.center_chunk.0.y - LIGHT_CACHE_RADIUS
686 + ((section_slot / LIGHT_CACHE_DIAMETER) % LIGHT_CACHE_DIAMETER) as i32;
687 let section_y = self.cached_min_section_y + (section_slot / LIGHT_CACHE_CHUNK_SLOTS) as i32;
688
689 Some(SectionPos::new(section_x, section_y, section_z))
690 }
691
692 #[must_use]
698 pub fn inner_light_section_slots_for_chunk(
699 self,
700 chunk_pos: ChunkPos,
701 ) -> Option<LightChunkSectionSlots> {
702 if self.cached_chunk(chunk_pos)?.scope != LightCacheChunkScope::Inner {
703 return None;
704 }
705
706 Some(LightChunkSectionSlots {
707 layout: self,
708 chunk_pos,
709 next_section_y: self.range.min_section_y(),
710 end_section_y: self.range.max_section_y_exclusive(),
711 })
712 }
713
714 #[must_use]
716 pub fn cached_block(self, block_pos: BlockPos) -> Option<CachedLightBlock> {
717 self.cached_block_by_coords(block_pos.x(), block_pos.y(), block_pos.z())
718 }
719
720 #[must_use]
722 pub fn cached_block_by_coords(
723 self,
724 block_x: i32,
725 block_y: i32,
726 block_z: i32,
727 ) -> Option<CachedLightBlock> {
728 let section_slot = self.section_slot_by_coords(
729 SectionPos::block_to_section_coord(block_x),
730 SectionPos::block_to_section_coord(block_y),
731 SectionPos::block_to_section_coord(block_z),
732 )?;
733
734 Some(CachedLightBlock {
735 block_pos: BlockPos::new(block_x, block_y, block_z),
736 section_slot,
737 local_index: Self::local_block_index_by_coords(block_x, block_y, block_z),
738 })
739 }
740
741 #[must_use]
743 pub fn cached_neighbor(
744 self,
745 cached_block: CachedLightBlock,
746 direction: Direction,
747 ) -> Option<CachedLightBlock> {
748 let (dx, dy, dz) = direction.offset();
749 self.cached_block_by_coords(
750 cached_block.block_pos.x().checked_add(dx)?,
751 cached_block.block_pos.y().checked_add(dy)?,
752 cached_block.block_pos.z().checked_add(dz)?,
753 )
754 }
755
756 #[must_use]
758 pub fn cached_block_from_packed(self, packed: PackedLightBlockPos) -> Option<CachedLightBlock> {
759 self.cached_block(self.decode_block_pos(packed)?)
760 }
761
762 #[must_use]
764 pub const fn local_block_index(block_pos: BlockPos) -> usize {
765 Self::local_block_index_by_coords(block_pos.x(), block_pos.y(), block_pos.z())
766 }
767
768 #[must_use]
770 pub const fn local_block_index_by_coords(block_x: i32, block_y: i32, block_z: i32) -> usize {
771 (block_x as usize & LIGHT_LOCAL_BLOCK_MASK)
772 | ((block_z as usize & LIGHT_LOCAL_BLOCK_MASK) << LIGHT_LOCAL_BLOCK_Z_SHIFT)
773 | ((block_y as usize & LIGHT_LOCAL_BLOCK_MASK) << LIGHT_LOCAL_BLOCK_Y_SHIFT)
774 }
775
776 #[must_use]
778 pub const fn encoded_min_block_x(self) -> i32 {
779 self.encoded_min_block_x as i32
780 }
781
782 #[must_use]
784 pub const fn encoded_max_block_x_exclusive(self) -> i32 {
785 (self.encoded_min_block_x + LIGHT_ENCODED_HORIZONTAL_MASK + 1) as i32
786 }
787
788 #[must_use]
790 pub const fn encoded_min_block_z(self) -> i32 {
791 self.encoded_min_block_z as i32
792 }
793
794 #[must_use]
796 pub const fn encoded_max_block_z_exclusive(self) -> i32 {
797 (self.encoded_min_block_z + LIGHT_ENCODED_HORIZONTAL_MASK + 1) as i32
798 }
799
800 #[must_use]
802 pub fn encode_block_pos(self, block_pos: BlockPos) -> Option<PackedLightBlockPos> {
803 if !self.contains_encoded_block_pos(block_pos) {
804 return None;
805 }
806
807 let encoded_x =
808 (i64::from(block_pos.x()) + self.encode_offset_x) & LIGHT_ENCODED_HORIZONTAL_MASK;
809 let encoded_y =
810 (i64::from(block_pos.y()) + self.encode_offset_y) & LIGHT_ENCODED_VERTICAL_MASK;
811 let encoded_z =
812 (i64::from(block_pos.z()) + self.encode_offset_z) & LIGHT_ENCODED_HORIZONTAL_MASK;
813
814 Some(PackedLightBlockPos::from_raw(
815 encoded_x as u32 | (encoded_z as u32) << 6 | (encoded_y as u32) << 12,
816 ))
817 }
818
819 #[must_use]
821 pub fn decode_block_pos(self, packed: PackedLightBlockPos) -> Option<BlockPos> {
822 let x = i64::from(packed.encoded_x()) - self.encode_offset_x;
823 let y = i64::from(packed.encoded_y()) - self.encode_offset_y;
824 let z = i64::from(packed.encoded_z()) - self.encode_offset_z;
825
826 Some(BlockPos::new(
827 i32::try_from(x).ok()?,
828 i32::try_from(y).ok()?,
829 i32::try_from(z).ok()?,
830 ))
831 }
832
833 #[must_use]
835 pub fn contains_encoded_block_pos(self, block_pos: BlockPos) -> bool {
836 let x = i64::from(block_pos.x());
837 let z = i64::from(block_pos.z());
838 x >= self.encoded_min_block_x
839 && x <= self.encoded_min_block_x + LIGHT_ENCODED_HORIZONTAL_MASK
840 && z >= self.encoded_min_block_z
841 && z <= self.encoded_min_block_z + LIGHT_ENCODED_HORIZONTAL_MASK
842 && self.contains_section_y(SectionPos::block_to_section_coord(block_pos.y()))
843 }
844
845 #[must_use]
847 pub fn section_slot_by_coords(
848 self,
849 section_x: i32,
850 section_y: i32,
851 section_z: i32,
852 ) -> Option<usize> {
853 if !self.contains_chunk_coords(section_x, section_z) || !self.contains_section_y(section_y)
854 {
855 return None;
856 }
857
858 let slot = i64::from(section_x)
859 + LIGHT_CACHE_DIAMETER_I64 * i64::from(section_z)
860 + LIGHT_CACHE_CHUNK_SLOTS_I64 * i64::from(section_y)
861 + self.chunk_section_index_offset;
862 usize::try_from(slot).ok()
863 }
864
865 #[must_use]
867 pub fn cached_section_index(self, section_y: i32) -> Option<usize> {
868 if !self.contains_section_y(section_y) {
869 return None;
870 }
871
872 usize::try_from(section_y - self.cached_min_section_y).ok()
873 }
874
875 #[must_use]
877 pub const fn cached_section_y(self, index: usize) -> Option<i32> {
878 if index >= self.cached_section_count {
879 return None;
880 }
881
882 Some(self.cached_min_section_y + index as i32)
883 }
884
885 #[must_use]
887 pub fn contains_chunk_coords(self, chunk_x: i32, chunk_z: i32) -> bool {
888 let dx = i64::from(chunk_x) - i64::from(self.center_chunk.0.x);
889 let dz = i64::from(chunk_z) - i64::from(self.center_chunk.0.y);
890 dx.abs().max(dz.abs()) <= i64::from(LIGHT_CACHE_RADIUS)
891 }
892
893 #[must_use]
895 pub fn contains_inner_chunk_coords(self, chunk_x: i32, chunk_z: i32) -> bool {
896 let dx = i64::from(chunk_x) - i64::from(self.center_chunk.0.x);
897 let dz = i64::from(chunk_z) - i64::from(self.center_chunk.0.y);
898 dx.abs().max(dz.abs()) <= LIGHT_CACHE_SECTION_RADIUS_I64
899 }
900
901 #[must_use]
903 pub const fn contains_light_section_y(self, section_y: i32) -> bool {
904 self.range.section_index(section_y).is_some()
905 }
906
907 #[must_use]
909 pub const fn contains_section_y(self, section_y: i32) -> bool {
910 section_y >= self.cached_min_section_y && section_y < self.cached_max_section_y_exclusive()
911 }
912}
913
914fn sections_around_and_at_block_pos(
915 block_pos: BlockPos,
916 mut section_consumer: impl FnMut(SectionPos),
917) {
918 let min_section_x = SectionPos::block_to_section_coord(block_pos.x().wrapping_sub(1));
919 let max_section_x = SectionPos::block_to_section_coord(block_pos.x().wrapping_add(1));
920 let min_section_y = SectionPos::block_to_section_coord(block_pos.y().wrapping_sub(1));
921 let max_section_y = SectionPos::block_to_section_coord(block_pos.y().wrapping_add(1));
922 let min_section_z = SectionPos::block_to_section_coord(block_pos.z().wrapping_sub(1));
923 let max_section_z = SectionPos::block_to_section_coord(block_pos.z().wrapping_add(1));
924
925 if min_section_x == max_section_x
926 && min_section_y == max_section_y
927 && min_section_z == max_section_z
928 {
929 section_consumer(SectionPos::new(min_section_x, min_section_y, min_section_z));
930 return;
931 }
932
933 for section_x in min_section_x..=max_section_x {
934 for section_y in min_section_y..=max_section_y {
935 for section_z in min_section_z..=max_section_z {
936 section_consumer(SectionPos::new(section_x, section_y, section_z));
937 }
938 }
939 }
940}
941
942fn empty_option_slots<T>(len: usize) -> Box<[Option<T>]> {
943 let mut values = Vec::with_capacity(len);
944 values.resize_with(len, || None);
945 values.into_boxed_slice()
946}
947
948#[cfg(test)]
949mod tests {
950 use super::*;
951
952 fn range(min_y: i32, height: i32) -> LightSectionRange {
953 let Ok(range) = LightSectionRange::from_world_height(min_y, height) else {
954 panic!("test world height should create a light range");
955 };
956 range
957 }
958
959 #[test]
960 fn cache_layout_matches_scalable_lux_chunk_indexing() {
961 let layout = LightCacheLayout::new(ChunkPos::new(10, -20), range(0, 16));
962
963 assert_eq!(layout.center_chunk(), ChunkPos::new(10, -20));
964 assert_eq!(layout.range(), range(0, 16));
965 assert_eq!(layout.chunk_slot(ChunkPos::new(10, -20)), Some(12));
966 assert_eq!(layout.chunk_slot(ChunkPos::new(8, -22)), Some(0));
967 assert_eq!(layout.chunk_slot(ChunkPos::new(12, -18)), Some(24));
968 assert_eq!(layout.chunk_slot(ChunkPos::new(9, -20)), Some(11));
969 assert_eq!(layout.chunk_slot(ChunkPos::new(13, -20)), None);
970 assert_eq!(layout.chunk_slot(ChunkPos::new(10, -23)), None);
971 }
972
973 #[test]
974 fn cache_layout_classifies_inner_and_outer_cached_chunks() {
975 let layout = LightCacheLayout::new(ChunkPos::new(10, -20), range(0, 16));
976
977 assert_eq!(
978 layout.cached_chunk(ChunkPos::new(10, -20)),
979 Some(CachedLightChunk {
980 chunk_pos: ChunkPos::new(10, -20),
981 chunk_slot: 12,
982 scope: LightCacheChunkScope::Inner,
983 })
984 );
985 assert_eq!(
986 layout.cached_chunk(ChunkPos::new(9, -21)),
987 Some(CachedLightChunk {
988 chunk_pos: ChunkPos::new(9, -21),
989 chunk_slot: 6,
990 scope: LightCacheChunkScope::Inner,
991 })
992 );
993 assert_eq!(
994 layout.cached_chunk(ChunkPos::new(8, -22)),
995 Some(CachedLightChunk {
996 chunk_pos: ChunkPos::new(8, -22),
997 chunk_slot: 0,
998 scope: LightCacheChunkScope::Outer,
999 })
1000 );
1001 assert_eq!(layout.cached_chunk(ChunkPos::new(13, -20)), None);
1002
1003 assert!(layout.contains_inner_chunk_coords(11, -19));
1004 assert!(!layout.contains_inner_chunk_coords(12, -20));
1005 assert!(layout.contains_chunk_coords(12, -20));
1006 }
1007
1008 #[test]
1009 fn cache_layout_decodes_chunk_slots() {
1010 let layout = LightCacheLayout::new(ChunkPos::new(10, -20), range(0, 16));
1011
1012 assert_eq!(layout.chunk_pos_for_slot(0), Some(ChunkPos::new(8, -22)));
1013 assert_eq!(layout.chunk_pos_for_slot(12), Some(ChunkPos::new(10, -20)));
1014 assert_eq!(layout.chunk_pos_for_slot(24), Some(ChunkPos::new(12, -18)));
1015 assert_eq!(layout.chunk_pos_for_slot(25), None);
1016 }
1017
1018 #[test]
1019 fn cache_layout_iterates_full_setup_chunks_in_scalable_lux_order() {
1020 let layout = LightCacheLayout::new(ChunkPos::new(10, -20), range(0, 16));
1021 let chunks = layout
1022 .setup_chunks(LightCacheSetupRadius::Full)
1023 .collect::<Vec<_>>();
1024
1025 assert_eq!(chunks.len(), LIGHT_CACHE_CHUNK_SLOTS);
1026 assert_eq!(
1027 chunks.first(),
1028 Some(&CachedLightChunk {
1029 chunk_pos: ChunkPos::new(8, -22),
1030 chunk_slot: 0,
1031 scope: LightCacheChunkScope::Outer,
1032 })
1033 );
1034 assert_eq!(
1035 chunks.get(12),
1036 Some(&CachedLightChunk {
1037 chunk_pos: ChunkPos::new(10, -20),
1038 chunk_slot: 12,
1039 scope: LightCacheChunkScope::Inner,
1040 })
1041 );
1042 assert_eq!(
1043 chunks.last(),
1044 Some(&CachedLightChunk {
1045 chunk_pos: ChunkPos::new(12, -18),
1046 chunk_slot: 24,
1047 scope: LightCacheChunkScope::Outer,
1048 })
1049 );
1050 }
1051
1052 #[test]
1053 fn cache_layout_adds_vertical_buffer_around_light_range() {
1054 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1055
1056 assert_eq!(layout.cached_min_section_y(), -2);
1057 assert_eq!(layout.cached_max_section_y_exclusive(), 3);
1058 assert_eq!(layout.cached_section_count(), 5);
1059 assert_eq!(layout.section_slot_count(), 125);
1060
1061 assert_eq!(layout.cached_section_index(-2), Some(0));
1062 assert_eq!(layout.cached_section_index(-1), Some(1));
1063 assert_eq!(layout.cached_section_index(2), Some(4));
1064 assert_eq!(layout.cached_section_index(3), None);
1065
1066 assert_eq!(layout.cached_section_y(0), Some(-2));
1067 assert_eq!(layout.cached_section_y(4), Some(2));
1068 assert_eq!(layout.cached_section_y(5), None);
1069 }
1070
1071 #[test]
1072 fn cache_layout_matches_scalable_lux_section_indexing() {
1073 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1074
1075 assert_eq!(layout.section_slot_by_coords(0, -2, 0), Some(12));
1076 assert_eq!(layout.section_slot_by_coords(0, -1, 0), Some(37));
1077 assert_eq!(layout.section_slot_by_coords(0, 1, 0), Some(87));
1078 assert_eq!(layout.section_slot_by_coords(0, 2, 0), Some(112));
1079 assert_eq!(layout.section_slot_by_coords(0, 3, 0), None);
1080 }
1081
1082 #[test]
1083 fn cache_layout_iterates_inner_chunk_light_section_slots() {
1084 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1085
1086 let Some(slots) = layout.inner_light_section_slots_for_chunk(ChunkPos::new(1, 0)) else {
1087 panic!("inner chunk should have light section slots");
1088 };
1089 assert_eq!(slots.len(), 3);
1090 assert_eq!(
1091 slots.collect::<Vec<_>>(),
1092 vec![
1093 CachedLightSection {
1094 section_pos: SectionPos::new(1, -1, 0),
1095 section_slot: 38,
1096 },
1097 CachedLightSection {
1098 section_pos: SectionPos::new(1, 0, 0),
1099 section_slot: 63,
1100 },
1101 CachedLightSection {
1102 section_pos: SectionPos::new(1, 1, 0),
1103 section_slot: 88,
1104 },
1105 ]
1106 );
1107
1108 assert!(layout.contains_light_section_y(-1));
1109 assert!(layout.contains_light_section_y(1));
1110 assert!(!layout.contains_light_section_y(-2));
1111 assert!(!layout.contains_light_section_y(2));
1112 }
1113
1114 #[test]
1115 fn cache_layout_uses_scalable_lux_local_block_indices() {
1116 assert_eq!(
1117 LightCacheLayout::local_block_index(BlockPos::new(0, 0, 0)),
1118 0
1119 );
1120 assert_eq!(
1121 LightCacheLayout::local_block_index(BlockPos::new(15, 15, 15)),
1122 15 | (15 << 4) | (15 << 8)
1123 );
1124 assert_eq!(
1125 LightCacheLayout::local_block_index(BlockPos::new(-1, -1, -1)),
1126 15 | (15 << 4) | (15 << 8)
1127 );
1128 assert_eq!(
1129 LightCacheLayout::local_block_index(BlockPos::new(16, 16, 16)),
1130 0
1131 );
1132 }
1133
1134 #[test]
1135 fn cache_layout_maps_block_positions_to_cached_blocks() {
1136 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1137
1138 assert_eq!(
1139 layout.cached_block(BlockPos::new(31, 0, -32)),
1140 Some(CachedLightBlock {
1141 block_pos: BlockPos::new(31, 0, -32),
1142 section_slot: 53,
1143 local_index: 15,
1144 })
1145 );
1146 assert_eq!(
1147 layout.cached_block(BlockPos::new(-1, -1, -1)),
1148 Some(CachedLightBlock {
1149 block_pos: BlockPos::new(-1, -1, -1),
1150 section_slot: 31,
1151 local_index: 4095,
1152 })
1153 );
1154 assert_eq!(layout.cached_block(BlockPos::new(48, 0, 0)), None);
1155 }
1156
1157 #[test]
1158 fn cache_layout_maps_cached_neighbors_across_section_edges() {
1159 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1160 let Some(block) = layout.cached_block(BlockPos::new(15, 15, 15)) else {
1161 panic!("test block should be cached");
1162 };
1163
1164 assert_eq!(
1165 layout.cached_neighbor(block, Direction::East),
1166 Some(CachedLightBlock {
1167 block_pos: BlockPos::new(16, 15, 15),
1168 section_slot: 63,
1169 local_index: (15 << 4) | (15 << 8),
1170 })
1171 );
1172 assert_eq!(
1173 layout.cached_neighbor(block, Direction::Up),
1174 Some(CachedLightBlock {
1175 block_pos: BlockPos::new(15, 16, 15),
1176 section_slot: 87,
1177 local_index: 15 | (15 << 4),
1178 })
1179 );
1180 }
1181
1182 #[test]
1183 fn packed_light_block_pos_masks_to_scalable_lux_position_bits() {
1184 let packed = PackedLightBlockPos::from_raw(u32::MAX);
1185
1186 assert_eq!(packed.raw(), (1 << 28) - 1);
1187 assert_eq!(packed.encoded_x(), 63);
1188 assert_eq!(packed.encoded_z(), 63);
1189 assert_eq!(packed.encoded_y(), u16::MAX);
1190 }
1191
1192 #[test]
1193 fn cache_layout_encodes_scalable_lux_queue_position_window() {
1194 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1195
1196 assert_eq!(layout.encoded_min_block_x(), -24);
1197 assert_eq!(layout.encoded_max_block_x_exclusive(), 40);
1198 assert_eq!(layout.encoded_min_block_z(), -24);
1199 assert_eq!(layout.encoded_max_block_z_exclusive(), 40);
1200
1201 let Some(center) = layout.encode_block_pos(BlockPos::new(7, 0, 7)) else {
1202 panic!("center chunk block should encode");
1203 };
1204 assert_eq!(center.encoded_x(), 31);
1205 assert_eq!(center.encoded_z(), 31);
1206 assert_eq!(center.encoded_y(), 32);
1207 assert_eq!(
1208 layout.decode_block_pos(center),
1209 Some(BlockPos::new(7, 0, 7))
1210 );
1211
1212 let Some(min) = layout.encode_block_pos(BlockPos::new(-24, -32, -24)) else {
1213 panic!("minimum queue block should encode");
1214 };
1215 assert_eq!(min.raw(), 0);
1216 assert_eq!(
1217 layout.decode_block_pos(min),
1218 Some(BlockPos::new(-24, -32, -24))
1219 );
1220
1221 let Some(max) = layout.encode_block_pos(BlockPos::new(39, 47, 39)) else {
1222 panic!("maximum queue block should encode");
1223 };
1224 assert_eq!(max.encoded_x(), 63);
1225 assert_eq!(max.encoded_z(), 63);
1226 assert_eq!(max.encoded_y(), 79);
1227 assert_eq!(
1228 layout.decode_block_pos(max),
1229 Some(BlockPos::new(39, 47, 39))
1230 );
1231
1232 assert_eq!(layout.encode_block_pos(BlockPos::new(40, 0, 0)), None);
1233 assert_eq!(layout.encode_block_pos(BlockPos::new(0, 0, 40)), None);
1234 assert_eq!(layout.encode_block_pos(BlockPos::new(0, 48, 0)), None);
1235 }
1236
1237 #[test]
1238 fn cache_layout_maps_packed_positions_to_cached_blocks() {
1239 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1240 let Some(packed) = layout.encode_block_pos(BlockPos::new(7, 0, 7)) else {
1241 panic!("center block should encode");
1242 };
1243
1244 assert_eq!(
1245 layout.cached_block_from_packed(packed),
1246 Some(CachedLightBlock {
1247 block_pos: BlockPos::new(7, 0, 7),
1248 section_slot: 62,
1249 local_index: 7 | (7 << 4),
1250 })
1251 );
1252 assert_eq!(
1253 layout.cached_block_from_packed(PackedLightBlockPos::from_raw(u32::MAX)),
1254 None
1255 );
1256 }
1257
1258 #[test]
1259 fn slot_arrays_store_values_by_cached_slots() {
1260 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1261 let Some(chunk) = layout.cached_chunk(ChunkPos::new(0, 0)) else {
1262 panic!("center chunk should be cached");
1263 };
1264 let Some(section) = layout.cached_section(SectionPos::new(0, 0, 0)) else {
1265 panic!("section should be cached");
1266 };
1267 let mut chunks = LightChunkSlotArray::new();
1268 let mut sections = LightSectionSlotArray::new(layout);
1269
1270 assert_eq!(chunks.slot_count(), LIGHT_CACHE_CHUNK_SLOTS);
1271 assert!(chunks.is_clear());
1272 assert_eq!(chunks.insert(chunk, 5), None);
1273 assert_eq!(chunks.get(chunk), Some(&5));
1274 assert_eq!(chunks.insert_slot(chunk.chunk_slot, 11), Some(5));
1275 assert_eq!(chunks.get_mut(chunk), Some(&mut 11));
1276 assert_eq!(chunks.insert_slot(LIGHT_CACHE_CHUNK_SLOTS, 4), None);
1277
1278 assert_eq!(sections.layout(), layout);
1279 assert_eq!(sections.slot_count(), layout.section_slot_count());
1280 assert!(sections.is_clear());
1281 assert_eq!(sections.insert(section, "sky"), None);
1282 assert_eq!(sections.get(section), Some(&"sky"));
1283 assert_eq!(
1284 sections.insert_slot(section.section_slot, "block"),
1285 Some("sky")
1286 );
1287 assert_eq!(sections.take_slot(section.section_slot), Some("block"));
1288 assert_eq!(sections.get_slot(section.section_slot), None);
1289
1290 chunks.clear();
1291 sections.clear();
1292 assert!(chunks.is_clear());
1293 assert!(sections.is_clear());
1294 }
1295
1296 #[test]
1297 fn notification_cache_marks_one_block_light_neighborhood() {
1298 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1299 let mut notifications = LightUpdateNotificationCache::new(layout);
1300
1301 assert_eq!(notifications.layout(), layout);
1302 assert!(notifications.is_empty());
1303 assert_eq!(
1304 notifications.mark_block_neighborhood(BlockPos::new(8, 8, 8)),
1305 Some(1)
1306 );
1307 assert_eq!(
1308 notifications.marked_section_positions().collect::<Vec<_>>(),
1309 vec![SectionPos::new(0, 0, 0)]
1310 );
1311 assert!(notifications.is_marked_section(SectionPos::new(0, 0, 0)));
1312 assert!(notifications.is_marked_section_slot(62));
1313
1314 notifications.clear();
1315 assert_eq!(
1316 notifications.mark_block_neighborhood(BlockPos::new(16, 16, 16)),
1317 Some(8)
1318 );
1319
1320 let marked = notifications.marked_section_positions().collect::<Vec<_>>();
1321 assert_eq!(marked.len(), 8);
1322 assert!(marked.contains(&SectionPos::new(0, 0, 0)));
1323 assert!(marked.contains(&SectionPos::new(1, 0, 0)));
1324 assert!(marked.contains(&SectionPos::new(0, 1, 0)));
1325 assert!(marked.contains(&SectionPos::new(1, 1, 1)));
1326 }
1327
1328 #[test]
1329 fn notification_cache_rejects_partial_block_neighborhoods() {
1330 let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range(0, 16));
1331 let mut notifications = LightUpdateNotificationCache::new(layout);
1332
1333 assert_eq!(
1334 notifications.mark_block_neighborhood(BlockPos::new(48, 8, 8)),
1335 None
1336 );
1337 assert!(notifications.is_empty());
1338 }
1339}