Skip to main content

steel_core/chunk/light/
cache.rs

1use std::iter::FusedIterator;
2
3use steel_utils::{BlockPos, ChunkPos, Direction, SectionPos};
4
5use super::LightSectionRange;
6
7/// Horizontal cache radius used by `ScalableLux` light propagation.
8pub const LIGHT_CACHE_RADIUS: i32 = 2;
9/// Horizontal radius where `ScalableLux` populates section and nibble cache data.
10pub const LIGHT_CACHE_SECTION_RADIUS: i32 = 1;
11/// Horizontal cache width and depth used by `ScalableLux` light propagation.
12pub const LIGHT_CACHE_DIAMETER: usize = LIGHT_CACHE_RADIUS as usize * 2 + 1;
13/// Number of chunk columns in one light-engine cache window.
14pub 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/// `ScalableLux` packed block position used in light propagation queue entries.
32///
33/// The lower 28 bits store `x | (z << 6) | (y << 12)`. X and Z are encoded in
34/// a 64-block window around the active chunk; Y is encoded relative to the
35/// section below the vanilla light-section range.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub struct PackedLightBlockPos(u32);
38
39impl PackedLightBlockPos {
40    /// Creates a packed light block position from raw queue bits.
41    #[must_use]
42    pub const fn from_raw(raw: u32) -> Self {
43        Self(raw & LIGHT_ENCODED_POSITION_MASK)
44    }
45
46    /// Returns the raw lower 28 queue-position bits.
47    #[must_use]
48    pub const fn raw(self) -> u32 {
49        self.0
50    }
51
52    /// Returns the encoded 6-bit X coordinate.
53    #[must_use]
54    pub const fn encoded_x(self) -> u8 {
55        (self.0 & LIGHT_ENCODED_HORIZONTAL_MASK as u32) as u8
56    }
57
58    /// Returns the encoded 6-bit Z coordinate.
59    #[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    /// Returns the encoded 16-bit Y coordinate.
65    #[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/// `ScalableLux` cache role for a cached chunk column.
72///
73/// During two-radius setup `ScalableLux` may cache chunk and emptiness-map data
74/// for the full 5x5 window, but section and nibble arrays are normally
75/// populated only for the inner 3x3 chunk window.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub enum LightCacheChunkScope {
78    /// Chunk is within the inner 3x3 window and may populate sections/nibbles.
79    Inner,
80    /// Chunk is in the outer ring and is cached for chunk/emptiness lookups only.
81    Outer,
82}
83
84/// `ScalableLux` setup-cache radius.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum LightCacheSetupRadius {
87    /// Scan the inner 3x3 chunk window.
88    Inner,
89    /// Scan the full 5x5 chunk window.
90    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/// Cached chunk slot plus its `ScalableLux` cache role.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct CachedLightChunk {
110    /// World chunk position for this cached chunk.
111    pub chunk_pos: ChunkPos,
112    /// Slot into `ScalableLux`'s 5x5 chunk cache arrays.
113    pub chunk_slot: usize,
114    /// Whether this chunk is in the inner section/nibble radius or outer ring.
115    pub scope: LightCacheChunkScope,
116}
117
118/// Cached section slot for one section position.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120pub struct CachedLightSection {
121    /// World section position for this cached section.
122    pub section_pos: SectionPos,
123    /// Slot into `ScalableLux`'s section/nibble cache arrays.
124    pub section_slot: usize,
125}
126
127/// Cached section slot and local nibble index for one block.
128///
129/// `ScalableLux` propagation uses `sectionIndex` plus local index
130/// `x | (z << 4) | (y << 8)` instead of repeatedly materializing section
131/// positions and local coordinates inside the hot propagation loops.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct CachedLightBlock {
134    /// World block position for this cached block.
135    pub block_pos: BlockPos,
136    /// Slot into `ScalableLux`'s section/nibble cache arrays.
137    pub section_slot: usize,
138    /// Local block index inside the 16x16x16 light section.
139    pub local_index: usize,
140}
141
142/// Iterator over chunks scanned by `ScalableLux` cache setup.
143#[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/// Optional value slots for `ScalableLux`'s 5x5 chunk cache arrays.
189#[derive(Debug, Clone)]
190pub struct LightChunkSlotArray<T> {
191    values: Box<[Option<T>]>,
192}
193
194impl<T> LightChunkSlotArray<T> {
195    /// Creates an empty 5x5 chunk slot array.
196    #[must_use]
197    pub fn new() -> Self {
198        Self {
199            values: empty_option_slots(LIGHT_CACHE_CHUNK_SLOTS),
200        }
201    }
202
203    /// Returns the number of chunk slots.
204    #[must_use]
205    pub fn slot_count(&self) -> usize {
206        self.values.len()
207    }
208
209    /// Returns true when no chunk slots hold values.
210    #[must_use]
211    pub fn is_clear(&self) -> bool {
212        self.values.iter().all(Option::is_none)
213    }
214
215    /// Clears every chunk slot.
216    pub fn clear(&mut self) {
217        for value in &mut self.values {
218            *value = None;
219        }
220    }
221
222    /// Inserts a value at a cached chunk slot.
223    pub fn insert(&mut self, chunk: CachedLightChunk, value: T) -> Option<T> {
224        self.insert_slot(chunk.chunk_slot, value)
225    }
226
227    /// Inserts a value at a raw chunk slot.
228    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    /// Returns the value at a cached chunk slot.
235    #[must_use]
236    pub fn get(&self, chunk: CachedLightChunk) -> Option<&T> {
237        self.get_slot(chunk.chunk_slot)
238    }
239
240    /// Returns the mutable value at a cached chunk slot.
241    pub fn get_mut(&mut self, chunk: CachedLightChunk) -> Option<&mut T> {
242        self.get_mut_slot(chunk.chunk_slot)
243    }
244
245    /// Returns the value at a raw chunk slot.
246    #[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    /// Returns the mutable value at a raw chunk slot.
252    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/// Iterator over one inner cached chunk's vanilla light-section slots.
264#[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/// Optional value slots for `ScalableLux`'s section/nibble cache arrays.
304#[derive(Debug, Clone)]
305pub struct LightSectionSlotArray<T> {
306    layout: LightCacheLayout,
307    values: Box<[Option<T>]>,
308}
309
310impl<T> LightSectionSlotArray<T> {
311    /// Creates an empty section slot array sized for `layout`.
312    #[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    /// Returns the layout this array is indexed by.
321    #[must_use]
322    pub const fn layout(&self) -> LightCacheLayout {
323        self.layout
324    }
325
326    /// Returns the number of section slots.
327    #[must_use]
328    pub fn slot_count(&self) -> usize {
329        self.values.len()
330    }
331
332    /// Returns true when no section slots hold values.
333    #[must_use]
334    pub fn is_clear(&self) -> bool {
335        self.values.iter().all(Option::is_none)
336    }
337
338    /// Clears every section slot.
339    pub fn clear(&mut self) {
340        for value in &mut self.values {
341            *value = None;
342        }
343    }
344
345    /// Inserts a value at a cached section slot.
346    pub fn insert(&mut self, section: CachedLightSection, value: T) -> Option<T> {
347        self.insert_slot(section.section_slot, value)
348    }
349
350    /// Inserts a value at a raw section slot.
351    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    /// Removes and returns the value at a raw section slot.
358    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    /// Returns the value at a cached section slot.
363    #[must_use]
364    pub fn get(&self, section: CachedLightSection) -> Option<&T> {
365        self.get_slot(section.section_slot)
366    }
367
368    /// Returns the mutable value at a cached section slot.
369    pub fn get_mut(&mut self, section: CachedLightSection) -> Option<&mut T> {
370        self.get_mut_slot(section.section_slot)
371    }
372
373    /// Returns the value at a raw section slot.
374    #[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    /// Returns the mutable value at a raw section slot.
380    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/// Section-slot notification flags used while publishing visible light updates.
386///
387/// `ScalableLux` keeps `notifyUpdateCache` beside its nibble cache and marks the
388/// cached sections touched by a block's one-block lighting neighborhood. The
389/// light engine later scans the same section slots while publishing dirty
390/// nibbles and notifying clients.
391#[derive(Debug, Clone)]
392pub struct LightUpdateNotificationCache {
393    layout: LightCacheLayout,
394    marked: Box<[bool]>,
395}
396
397impl LightUpdateNotificationCache {
398    /// Creates an empty notification cache for a light-engine cache window.
399    #[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    /// Returns the layout this notification cache is indexed by.
408    #[must_use]
409    pub const fn layout(&self) -> LightCacheLayout {
410        self.layout
411    }
412
413    /// Returns true when no section slots are marked for notification.
414    #[must_use]
415    pub fn is_empty(&self) -> bool {
416        self.marked.iter().all(|marked| !marked)
417    }
418
419    /// Removes every pending section notification.
420    pub fn clear(&mut self) {
421        self.marked.fill(false);
422    }
423
424    /// Marks a cached section, returning true only when it was newly marked.
425    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    /// Marks every cached section touched by a block's lighting neighborhood.
434    ///
435    /// Returns `None` if the full one-block neighborhood is not inside this
436    /// cache window, so callers do not accidentally publish a partial update.
437    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    /// Returns whether a cached section is marked for notification.
456    #[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    /// Returns whether a section slot is marked for notification.
466    #[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    /// Iterates marked section positions in cache-slot order.
472    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/// `ScalableLux` cache-window layout for chunk, section, and nibble arrays.
497///
498/// `ScalableLux` keeps a 5x5 chunk window around the active chunk and stores
499/// light sections in flat arrays with one extra cached section below and above
500/// the vanilla light-section range. This type owns that index math so the
501/// light engine can share the same slots for chunk sections, nibbles, and
502/// update notifications without repeating coordinate transforms.
503#[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    /// Creates a cache layout centered on one chunk.
520    #[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    /// Returns the chunk at the center of this cache window.
551    #[must_use]
552    pub const fn center_chunk(self) -> ChunkPos {
553        self.center_chunk
554    }
555
556    /// Returns the vanilla padded light-section range.
557    #[must_use]
558    pub const fn range(self) -> LightSectionRange {
559        self.range
560    }
561
562    /// Returns the first cached section Y coordinate, including the lower buffer.
563    #[must_use]
564    pub const fn cached_min_section_y(self) -> i32 {
565        self.cached_min_section_y
566    }
567
568    /// Returns the section Y coordinate one past the last cached section.
569    #[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    /// Returns the number of cached vertical sections, including both buffers.
575    #[must_use]
576    pub const fn cached_section_count(self) -> usize {
577        self.cached_section_count
578    }
579
580    /// Returns the number of section/nibble slots in this cache window.
581    #[must_use]
582    pub const fn section_slot_count(self) -> usize {
583        LIGHT_CACHE_CHUNK_SLOTS * self.cached_section_count
584    }
585
586    /// Iterates chunks in `ScalableLux` `setupCaches` scan order.
587    #[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    /// Returns cached chunk slot data for a chunk column.
601    #[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    /// Returns cached chunk slot data for chunk coordinates.
607    #[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    /// Returns the slot for a cached chunk column.
630    #[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    /// Returns the slot for a cached chunk column by chunk coordinates.
636    #[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    /// Converts a chunk slot back to its cached chunk position.
649    #[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    /// Returns the section/nibble slot for a section position.
662    #[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    /// Returns cached section slot data for a section position.
668    #[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    /// Converts a section/nibble slot back to its cached section position.
677    #[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    /// Iterates the vanilla light-section slots for an inner cached chunk.
693    ///
694    /// Returns `None` for the outer radius-2 ring because `ScalableLux` keeps
695    /// those chunks available for chunk/emptiness lookups but does not
696    /// populate section or nibble cache data for them during normal setup.
697    #[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    /// Returns cache slot data for a block position.
715    #[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    /// Returns cache slot data for block coordinates.
721    #[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    /// Returns cache slot data for a cached block's neighboring block.
742    #[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    /// Decodes a packed queue position and returns its cache slot data.
757    #[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    /// Returns the local light-section index for a block position.
763    #[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    /// Returns the local light-section index for block coordinates.
769    #[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    /// Returns the first block X coordinate that can be packed into queue entries.
777    #[must_use]
778    pub const fn encoded_min_block_x(self) -> i32 {
779        self.encoded_min_block_x as i32
780    }
781
782    /// Returns the block X coordinate one past the packed queue window.
783    #[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    /// Returns the first block Z coordinate that can be packed into queue entries.
789    #[must_use]
790    pub const fn encoded_min_block_z(self) -> i32 {
791        self.encoded_min_block_z as i32
792    }
793
794    /// Returns the block Z coordinate one past the packed queue window.
795    #[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    /// Packs a block position for `ScalableLux` queue storage.
801    #[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    /// Decodes `ScalableLux` queue-position bits back to a world block position.
820    #[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    /// Returns true if a block position is inside the packed queue-coordinate window.
834    #[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    /// Returns the section/nibble slot for section coordinates.
846    #[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    /// Returns the cached vertical index for a section Y coordinate.
866    #[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    /// Converts a cached vertical index back to section Y.
876    #[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    /// Returns true if a chunk coordinate is inside the 5x5 cache window.
886    #[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    /// Returns true if a chunk coordinate is inside the inner section/nibble cache radius.
894    #[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    /// Returns true if a section Y is inside vanilla's padded light-section range.
902    #[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    /// Returns true if a section Y coordinate is inside the cached vertical range.
908    #[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}