Skip to main content

steel_core/chunk/light/
workset.rs

1use std::{mem, sync::Arc};
2
3use parking_lot::{RwLockReadGuard, RwLockWriteGuard};
4use steel_registry::{REGISTRY, vanilla_blocks};
5use steel_utils::{BlockStateId, ChunkPos, SectionPos};
6
7use crate::chunk::{Chunk, chunk_holder::ChunkHolder, section::ChunkSection, status::ChunkStatus};
8
9use super::{
10    CachedLightBlock, CachedLightChunk, ChunkLightData, ChunkLightLayerStorage,
11    LightCacheChunkScope, LightCacheLayout, LightCacheSetupRadius, LightChunkSlotArray, LightLayer,
12    LightSection, LightSectionData, LightSectionSlotArray, LightUpdateNotificationCache,
13};
14
15/// Error returned when a scoped light workset cannot acquire required chunks.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum LightWorksetSetupError {
18    /// A chunk inside `ScalableLux`'s required 1-radius cache was unavailable.
19    MissingRequiredChunk {
20        /// Missing chunk position.
21        chunk_pos: ChunkPos,
22    },
23}
24
25/// Scoped chunk admission for one light operation.
26///
27/// This keeps the `ScalableLux` cache-window admission rules without storing
28/// long-lived borrows into chunk internals. The workset pins admitted chunk
29/// holders, then builds short-lived read caches with locks acquired in stable
30/// cache-slot order.
31pub struct LightWorkset {
32    layout: LightCacheLayout,
33    chunks: LightChunkSlotArray<LightWorksetChunk>,
34}
35
36struct LightWorksetChunk {
37    holder: Arc<ChunkHolder>,
38    section_readable: bool,
39    light_writable: bool,
40}
41
42impl LightWorkset {
43    /// Creates a scoped cache window by scanning chunks in `ScalableLux` setup order.
44    pub fn setup(
45        layout: LightCacheLayout,
46        radius: LightCacheSetupRadius,
47        relaxed: bool,
48        mut chunk_for_lighting: impl FnMut(ChunkPos) -> Option<Arc<ChunkHolder>>,
49        mut can_use_chunk: impl FnMut(&Chunk) -> bool,
50    ) -> Result<Self, LightWorksetSetupError> {
51        Self::setup_with_scopes(
52            layout,
53            radius,
54            relaxed,
55            &mut chunk_for_lighting,
56            |_, _, chunk| {
57                let usable = can_use_chunk(chunk);
58                (usable, usable)
59            },
60        )
61    }
62
63    /// Creates a scoped cache window with separate section-read and light-write admission.
64    pub fn setup_with_scopes(
65        layout: LightCacheLayout,
66        radius: LightCacheSetupRadius,
67        relaxed: bool,
68        mut chunk_for_lighting: impl FnMut(ChunkPos) -> Option<Arc<ChunkHolder>>,
69        mut can_use_chunk: impl FnMut(CachedLightChunk, &ChunkHolder, &Chunk) -> (bool, bool),
70    ) -> Result<Self, LightWorksetSetupError> {
71        let mut chunks = LightChunkSlotArray::new();
72
73        for cached_chunk in layout.setup_chunks(radius) {
74            let Some(holder) =
75                Self::try_get_holder(cached_chunk, relaxed, &mut chunk_for_lighting)?
76            else {
77                continue;
78            };
79
80            let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
81                continue;
82            };
83            let (section_readable, light_writable) = can_use_chunk(cached_chunk, &holder, chunk);
84            if !section_readable && !light_writable {
85                continue;
86            }
87            chunks.insert(
88                cached_chunk,
89                LightWorksetChunk {
90                    holder,
91                    section_readable,
92                    light_writable,
93                },
94            );
95        }
96
97        Ok(Self { layout, chunks })
98    }
99
100    /// Returns this workset's cache layout.
101    #[must_use]
102    pub const fn layout(&self) -> LightCacheLayout {
103        self.layout
104    }
105
106    /// Returns the holder for a cached chunk slot.
107    #[must_use]
108    pub fn chunk_holder(&self, cached_chunk: CachedLightChunk) -> Option<&Arc<ChunkHolder>> {
109        self.chunks.get(cached_chunk).map(|chunk| &chunk.holder)
110    }
111
112    /// Returns whether a cached chunk was admitted for section reads.
113    #[must_use]
114    pub fn can_read_sections(&self, cached_chunk: CachedLightChunk) -> bool {
115        self.chunks
116            .get(cached_chunk)
117            .is_some_and(|chunk| chunk.section_readable)
118    }
119
120    /// Returns whether a cached chunk was admitted for light writes.
121    #[must_use]
122    pub fn can_write_light(&self, cached_chunk: CachedLightChunk) -> bool {
123        self.chunks
124            .get(cached_chunk)
125            .is_some_and(|chunk| chunk.light_writable)
126    }
127
128    /// Builds a chunk-read cache for the duration of `f`.
129    ///
130    /// The workset keeps holder `Arc`s alive, while this cache borrows the chunks
131    /// installed in their stable `OnceLock` storage for the scoped operation.
132    pub fn with_chunk_read_cache<R>(&self, f: impl FnOnce(&LightChunkReadCache<'_>) -> R) -> R {
133        let mut chunks = LightChunkSlotArray::new();
134        let mut light_chunks = LightChunkSlotArray::new();
135
136        for chunk_slot in 0..self.chunks.slot_count() {
137            let Some(workset_chunk) = self.chunks.get_slot(chunk_slot) else {
138                continue;
139            };
140            if workset_chunk.section_readable
141                && let Some(chunk) = workset_chunk.holder.try_chunk(ChunkStatus::Empty)
142            {
143                chunks.insert_slot(chunk_slot, chunk);
144            }
145            if workset_chunk.light_writable
146                && let Some(chunk) = workset_chunk.holder.try_chunk(ChunkStatus::Empty)
147            {
148                light_chunks.insert_slot(chunk_slot, chunk);
149            }
150        }
151
152        let cache = LightChunkReadCache {
153            layout: self.layout,
154            chunks,
155            light_chunks,
156        };
157        f(&cache)
158    }
159
160    fn try_get_holder(
161        cached_chunk: CachedLightChunk,
162        relaxed: bool,
163        chunk_for_lighting: &mut impl FnMut(ChunkPos) -> Option<Arc<ChunkHolder>>,
164    ) -> Result<Option<Arc<ChunkHolder>>, LightWorksetSetupError> {
165        let required = !relaxed && cached_chunk.scope == LightCacheChunkScope::Inner;
166        let holder = chunk_for_lighting(cached_chunk.chunk_pos)
167            .filter(|holder| holder.try_chunk(ChunkStatus::Empty).is_some());
168
169        if holder.is_none() && required {
170            return Err(LightWorksetSetupError::MissingRequiredChunk {
171                chunk_pos: cached_chunk.chunk_pos,
172            });
173        }
174
175        Ok(holder)
176    }
177}
178
179/// Flat cached chunk reads for one scoped lighting operation.
180pub struct LightChunkReadCache<'a> {
181    layout: LightCacheLayout,
182    chunks: LightChunkSlotArray<&'a Chunk>,
183    light_chunks: LightChunkSlotArray<&'a Chunk>,
184}
185
186impl LightChunkReadCache<'_> {
187    /// Returns this read cache's layout.
188    #[must_use]
189    pub const fn layout(&self) -> LightCacheLayout {
190        self.layout
191    }
192
193    /// Returns the cached chunk for a chunk slot.
194    #[must_use]
195    pub fn chunk(&self, cached_chunk: CachedLightChunk) -> Option<&Chunk> {
196        self.chunks.get(cached_chunk).copied()
197    }
198
199    /// Builds a section-read cache for the duration of `f`.
200    ///
201    /// Section locks are acquired in cache-slot order and released before this
202    /// method returns. Emptiness maps are copied into the cache so propagation
203    /// can query known section emptiness without keeping additional borrows.
204    pub fn with_section_read_cache<R>(&self, f: impl FnOnce(&LightSectionReadCache<'_>) -> R) -> R {
205        let mut sections = LightSectionSlotArray::new(self.layout);
206        let mut emptiness_maps = LightChunkSlotArray::new();
207
208        for chunk_slot in 0..self.chunks.slot_count() {
209            let Some(chunk_guard) = self.chunks.get_slot(chunk_slot) else {
210                continue;
211            };
212            let Some(chunk_pos) = self.layout.chunk_pos_for_slot(chunk_slot) else {
213                continue;
214            };
215
216            let chunk_sections = chunk_guard.sections();
217            emptiness_maps.insert_slot(chunk_slot, chunk_sections.section_emptiness_map());
218
219            let Some(section_slots) = self.layout.inner_light_section_slots_for_chunk(chunk_pos)
220            else {
221                continue;
222            };
223
224            for cached_section in section_slots {
225                let Some(section_index) = self
226                    .layout
227                    .range()
228                    .chunk_section_index(cached_section.section_pos.y())
229                else {
230                    continue;
231                };
232                let Some(section) = chunk_sections.sections.get(section_index) else {
233                    continue;
234                };
235                sections.insert(cached_section, section.read());
236            }
237        }
238
239        let cache = LightSectionReadCache {
240            layout: self.layout,
241            sections,
242            emptiness_maps,
243        };
244        f(&cache)
245    }
246
247    /// Builds a layer-specific light edit cache for the duration of `f`.
248    ///
249    /// Committed chunk light storage is copied into the edit cache before
250    /// propagation mutates it. The edit writes back only through
251    /// [`LightLayerEdit::commit`], so chunk-owned light does not regain the old
252    /// persistent visible/updating split.
253    pub fn with_light_edit<R>(
254        &self,
255        layer: LightLayer,
256        f: impl FnOnce(LightLayerEdit<'_>) -> R,
257    ) -> R {
258        let mut chunks = LightChunkSlotArray::new();
259
260        for chunk_slot in 0..self.light_chunks.slot_count() {
261            let Some(chunk_guard) = self.light_chunks.get_slot(chunk_slot) else {
262                continue;
263            };
264            chunks.insert_slot(chunk_slot, chunk_guard.light_mut());
265        }
266
267        let mut edits = Vec::new();
268        let sections = LightLayerEdit::build_section_edits(self.layout, layer, &chunks, &mut edits);
269        let edit = LightLayerEdit {
270            layout: self.layout,
271            layer,
272            chunks,
273            sections,
274            removed_missing_sections: LightSectionSlotArray::new(self.layout),
275            edits,
276        };
277        f(edit)
278    }
279}
280
281/// Flat cached chunk-section reads for block-state access during lighting.
282pub struct LightSectionReadCache<'a> {
283    layout: LightCacheLayout,
284    sections: LightSectionSlotArray<RwLockReadGuard<'a, ChunkSection>>,
285    emptiness_maps: LightChunkSlotArray<Box<[bool]>>,
286}
287
288impl LightSectionReadCache<'_> {
289    /// Returns this read cache's layout.
290    #[must_use]
291    pub const fn layout(&self) -> LightCacheLayout {
292        self.layout
293    }
294
295    /// Returns the block state for a cached light block, or air for missing sections.
296    #[must_use]
297    pub fn get_block_state(&self, cached_block: CachedLightBlock) -> BlockStateId {
298        let Some(section) = self.sections.get_slot(cached_block.section_slot) else {
299            return Self::air();
300        };
301
302        if section.is_empty() {
303            return Self::air();
304        }
305
306        let (local_x, local_y, local_z) = local_block_coords(cached_block.local_index);
307        section.states.get(local_x, local_y, local_z)
308    }
309
310    /// Returns whether a cached section exists and is non-empty.
311    #[must_use]
312    pub fn has_non_empty_section(&self, section_pos: SectionPos) -> bool {
313        let Some(cached_section) = self.layout.cached_section(section_pos) else {
314            return false;
315        };
316        self.sections
317            .get_slot(cached_section.section_slot)
318            .is_some_and(|section| !section.is_empty())
319    }
320
321    /// Returns whether a cached section was admitted into the section-read cache.
322    #[must_use]
323    pub fn has_cached_section(&self, section_pos: SectionPos) -> bool {
324        let Some(cached_section) = self.layout.cached_section(section_pos) else {
325            return false;
326        };
327        self.sections
328            .get_slot(cached_section.section_slot)
329            .is_some()
330    }
331
332    /// Returns known real-section emptiness for a readable cached chunk column.
333    #[must_use]
334    pub fn section_empty(&self, section_pos: SectionPos) -> Option<bool> {
335        let chunk_pos = ChunkPos::new(section_pos.x(), section_pos.z());
336        let cached_chunk = self.layout.cached_chunk(chunk_pos)?;
337        let emptiness_map = self.emptiness_maps.get_slot(cached_chunk.chunk_slot)?;
338        let section_index = self.layout.range().chunk_section_index(section_pos.y())?;
339        emptiness_map.get(section_index).copied()
340    }
341
342    fn air() -> BlockStateId {
343        REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR)
344    }
345}
346
347const fn local_block_coords(local_index: usize) -> (usize, usize, usize) {
348    let local_x = local_index & 15;
349    let local_z = (local_index >> 4) & 15;
350    let local_y = (local_index >> 8) & 15;
351    (local_x, local_y, local_z)
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
355struct StoredLightSectionEdit {
356    chunk_slot: usize,
357    section_index: usize,
358}
359
360#[derive(Debug, PartialEq, Eq)]
361struct LightSectionEdit {
362    section: LightSection,
363    dirty: bool,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
367enum LightSectionEditEntry {
368    Stored {
369        target: StoredLightSectionEdit,
370        edit_index: usize,
371    },
372    Transient {
373        edit_index: usize,
374    },
375}
376
377/// Scoped mutable light edits for one layer and one workset.
378pub struct LightLayerEdit<'a> {
379    layout: LightCacheLayout,
380    layer: LightLayer,
381    chunks: LightChunkSlotArray<RwLockWriteGuard<'a, ChunkLightData>>,
382    sections: LightSectionSlotArray<LightSectionEditEntry>,
383    removed_missing_sections: LightSectionSlotArray<StoredLightSectionEdit>,
384    edits: Vec<LightSectionEdit>,
385}
386
387impl LightLayerEdit<'_> {
388    /// Returns this edit cache's layout.
389    #[must_use]
390    pub const fn layout(&self) -> LightCacheLayout {
391        self.layout
392    }
393
394    /// Returns this edit cache's light layer.
395    #[must_use]
396    pub const fn layer(&self) -> LightLayer {
397        self.layer
398    }
399
400    /// Returns an edited light value for a cached light block.
401    #[must_use]
402    pub fn get(&self, cached_block: CachedLightBlock) -> u8 {
403        self.get_at_section_index(cached_block.section_slot, cached_block.local_index)
404    }
405
406    /// Returns whether a cached block has a non-missing edited section.
407    #[must_use]
408    pub fn has_non_missing(&self, cached_block: CachedLightBlock) -> bool {
409        self.section_edit(cached_block.section_slot)
410            .is_some_and(|section| !matches!(section.section, LightSection::Missing))
411    }
412
413    /// Returns whether a cached section has a non-missing edited section.
414    #[must_use]
415    pub fn has_non_missing_section(&self, section_pos: SectionPos) -> bool {
416        let Some(section_slot) = self.layout.section_slot(section_pos) else {
417            return false;
418        };
419        self.section_edit(section_slot)
420            .is_some_and(|section| !matches!(section.section, LightSection::Missing))
421    }
422
423    /// Returns whether a cached section has an edited missing section.
424    #[must_use]
425    pub fn is_section_missing(&self, section_pos: SectionPos) -> bool {
426        let Some(section_slot) = self.layout.section_slot(section_pos) else {
427            return false;
428        };
429        self.section_edit(section_slot)
430            .is_some_and(|section| matches!(section.section, LightSection::Missing))
431    }
432
433    /// Returns whether a cached section was admitted into the edit cache.
434    #[must_use]
435    pub fn has_cached_section(&self, section_pos: SectionPos) -> bool {
436        let Some(section_slot) = self.layout.section_slot(section_pos) else {
437            return false;
438        };
439        self.sections.get_slot(section_slot).is_some()
440    }
441
442    /// Returns true when a cached section has edited light data.
443    #[must_use]
444    pub fn has_light_data_section(&self, section_pos: SectionPos) -> bool {
445        let Some(section_slot) = self.layout.section_slot(section_pos) else {
446            return false;
447        };
448        self.section_edit(section_slot)
449            .is_some_and(|section| section_has_light_data(&section.section))
450    }
451
452    /// Returns known real-section emptiness for a writable cached chunk column.
453    #[must_use]
454    pub fn section_empty(&self, section_pos: SectionPos) -> Option<bool> {
455        let chunk_pos = ChunkPos::new(section_pos.x(), section_pos.z());
456        let cached_chunk = self.layout.cached_chunk(chunk_pos)?;
457        let light_data = self.chunks.get_slot(cached_chunk.chunk_slot)?;
458
459        Self::layer_storage(light_data, self.layer).section_empty(section_pos.y())
460    }
461
462    /// Updates the cached light layer's real-section emptiness map.
463    ///
464    /// Returns the previous value when the target layer and section are writable.
465    pub fn set_section_empty(&mut self, section_pos: SectionPos, empty: bool) -> Option<bool> {
466        let chunk_pos = ChunkPos::new(section_pos.x(), section_pos.z());
467        let cached_chunk = self.layout.cached_chunk(chunk_pos)?;
468        let layer = self.layer;
469        let light_data = self.chunks.get_mut_slot(cached_chunk.chunk_slot)?;
470
471        Self::layer_storage_mut(light_data, layer).set_section_empty(section_pos.y(), empty)
472    }
473
474    /// Marks a cached light section non-missing without allocating packed bytes.
475    ///
476    /// Returns false when the section has no writable cached edit entry.
477    pub fn set_section_non_missing(&mut self, section_pos: SectionPos) -> bool {
478        let Some(section_slot) = self.layout.section_slot(section_pos) else {
479            return false;
480        };
481        self.set_section_slot_non_missing(section_slot)
482    }
483
484    /// Marks a cached light section missing and drops edited bytes.
485    ///
486    /// Returns false when the section has no writable cached edit entry.
487    pub fn set_section_missing(&mut self, section_pos: SectionPos) -> bool {
488        let Some(section_slot) = self.layout.section_slot(section_pos) else {
489            return false;
490        };
491        let Some(section) = self.section_edit_mut(section_slot) else {
492            return false;
493        };
494        let was_present = !matches!(section.section, LightSection::Missing);
495        section.section = LightSection::missing();
496        section.dirty |= was_present;
497        was_present
498    }
499
500    /// Hides a cached section from external packet/save conversion.
501    ///
502    /// Missing and visible zero sections become missing, matching old
503    /// `Uninitialized -> Null` hidden-state behavior.
504    pub fn set_section_internal(&mut self, section_pos: SectionPos) -> bool {
505        let Some(section_slot) = self.layout.section_slot(section_pos) else {
506            return false;
507        };
508        let Some(section) = self.section_edit_mut(section_slot) else {
509            return false;
510        };
511        let was_present = !matches!(section.section, LightSection::Missing);
512        section.section = take_internal_section(&mut section.section);
513        section.dirty |= was_present;
514        was_present
515    }
516
517    /// Replaces one cached chunk column's layer sections with fresh missing sections.
518    ///
519    /// Initial chunk lighting lights into a fresh center layer, so previous
520    /// neighbor-written data cannot become the center chunk's canonical light.
521    pub fn reset_chunk_sections_to_missing(&mut self, chunk_pos: ChunkPos) -> bool {
522        let Some(cached_chunk) = self.layout.cached_chunk(chunk_pos) else {
523            return false;
524        };
525        if self.chunks.get_slot(cached_chunk.chunk_slot).is_none() {
526            return false;
527        }
528
529        let mut reset_any = false;
530        for section_y in
531            self.layout.range().min_section_y()..self.layout.range().max_section_y_exclusive()
532        {
533            let section_pos = SectionPos::new(chunk_pos.0.x, section_y, chunk_pos.0.y);
534            if self.set_section_missing(section_pos) {
535                reset_any = true;
536            }
537        }
538        reset_any
539    }
540
541    /// Removes missing sky sections from the temporary edit cache.
542    ///
543    /// Later materialization can create transient sections for propagation; those
544    /// transient sections notify on commit but do not write into chunk storage.
545    pub fn rewrite_missing_sections_for_skylight(&mut self) {
546        debug_assert_eq!(self.layer, LightLayer::Sky);
547
548        for section_slot in 0..self.sections.slot_count() {
549            let Some(LightSectionEditEntry::Stored { target, edit_index }) =
550                self.sections.get_slot(section_slot).copied()
551            else {
552                continue;
553            };
554            if !matches!(
555                self.edits.get(edit_index).map(|edit| &edit.section),
556                Some(LightSection::Missing)
557            ) {
558                continue;
559            }
560
561            self.sections.take_slot(section_slot);
562            self.removed_missing_sections
563                .insert_slot(section_slot, target);
564        }
565    }
566
567    /// Materializes a sky section that was removed from the temporary edit cache.
568    ///
569    /// Returns false when the section was not part of the writable cache or was
570    /// not removed by [`Self::rewrite_missing_sections_for_skylight`].
571    pub fn materialize_removed_missing_section(&mut self, section_pos: SectionPos) -> bool {
572        let Some(section_slot) = self.layout.section_slot(section_pos) else {
573            return false;
574        };
575        if self.sections.get_slot(section_slot).is_some() {
576            return true;
577        }
578        if self
579            .removed_missing_sections
580            .get_slot(section_slot)
581            .is_none()
582        {
583            return false;
584        }
585
586        let edit_index = self.edits.len();
587        self.edits.push(LightSectionEdit {
588            section: LightSection::missing(),
589            dirty: false,
590        });
591        self.sections.insert_slot(
592            section_slot,
593            LightSectionEditEntry::Transient { edit_index },
594        );
595        true
596    }
597
598    /// Fills a cached section with one edited light value.
599    ///
600    /// Returns false when the section has no writable cached edit entry.
601    pub fn fill_section(&mut self, section_pos: SectionPos, value: u8) -> bool {
602        let Some(section_slot) = self.layout.section_slot(section_pos) else {
603            return false;
604        };
605        let Some(section) = self.section_edit_mut(section_slot) else {
606            return false;
607        };
608
609        fill_section(&mut section.section, value);
610        section.dirty = true;
611        true
612    }
613
614    /// Extrudes the lower row from the first non-missing cached section above.
615    ///
616    /// Returns false when the target section or source section is unavailable.
617    pub fn extrude_lower_from_first_section_above(&mut self, section_pos: SectionPos) -> bool {
618        let Some(target_slot) = self.layout.section_slot(section_pos) else {
619            return false;
620        };
621
622        let mut source_row = None;
623        for source_y in (section_pos.y() + 1)..self.layout.range().max_section_y_exclusive() {
624            let source_pos = SectionPos::new(section_pos.x(), source_y, section_pos.z());
625            let Some(source_slot) = self.layout.section_slot(source_pos) else {
626                continue;
627            };
628            let Some(source) = self.section_edit(source_slot) else {
629                continue;
630            };
631            if matches!(source.section, LightSection::Missing) {
632                continue;
633            }
634            source_row = Some(lower_row(&source.section));
635            break;
636        }
637
638        let Some(source_row) = source_row else {
639            return false;
640        };
641        let Some(target) = self.section_edit_mut(target_slot) else {
642            return false;
643        };
644        extrude_lower_row(&mut target.section, source_row.as_ref());
645        target.dirty = true;
646        true
647    }
648
649    /// Returns an edited light value for a section slot and local light index.
650    #[must_use]
651    pub fn get_at_section_index(&self, section_slot: usize, local_index: usize) -> u8 {
652        let Some(section) = self.section_edit(section_slot) else {
653            return 0;
654        };
655        get_section_value(&section.section, local_index)
656    }
657
658    /// Sets an edited light value for a cached light block.
659    ///
660    /// Returns false when no writable non-missing section was cached for the block.
661    pub fn set(&mut self, cached_block: CachedLightBlock, level: u8) -> bool {
662        self.set_at_section_index(cached_block.section_slot, cached_block.local_index, level)
663    }
664
665    /// Sets an edited light value for a section slot and local light index.
666    ///
667    /// Returns false when no writable non-missing section was cached for the slot.
668    pub fn set_at_section_index(
669        &mut self,
670        section_slot: usize,
671        local_index: usize,
672        level: u8,
673    ) -> bool {
674        let Some(section) = self.section_edit_mut(section_slot) else {
675            return false;
676        };
677        if matches!(section.section, LightSection::Missing) {
678            return false;
679        }
680
681        set_section_value(&mut section.section, local_index, level);
682        section.dirty = true;
683        true
684    }
685
686    /// Commits edited stored sections and publishes changed or notified sections.
687    pub fn commit(
688        mut self,
689        notifications: Option<&LightUpdateNotificationCache>,
690        mut on_update: impl FnMut(SectionPos),
691    ) -> usize {
692        debug_assert!(notifications.is_none_or(|cache| cache.layout() == self.layout));
693        let mut updated = 0;
694
695        for section_slot in 0..self.sections.slot_count() {
696            let marked =
697                notifications.is_some_and(|cache| cache.is_marked_section_slot(section_slot));
698            let Some(entry) = self.sections.take_slot(section_slot) else {
699                continue;
700            };
701
702            let changed = match entry {
703                LightSectionEditEntry::Stored { target, edit_index } => {
704                    self.commit_stored_section(target, edit_index)
705                }
706                LightSectionEditEntry::Transient { edit_index } => self
707                    .edits
708                    .get(edit_index)
709                    .is_some_and(|section| section.dirty),
710            };
711
712            if (changed || marked)
713                && let Some(section_pos) = self.layout.section_pos_for_slot(section_slot)
714            {
715                on_update(section_pos);
716                updated += 1;
717            }
718        }
719
720        updated
721    }
722
723    fn build_section_edits(
724        layout: LightCacheLayout,
725        layer: LightLayer,
726        chunks: &LightChunkSlotArray<RwLockWriteGuard<'_, ChunkLightData>>,
727        edits: &mut Vec<LightSectionEdit>,
728    ) -> LightSectionSlotArray<LightSectionEditEntry> {
729        let mut sections = LightSectionSlotArray::new(layout);
730
731        for chunk_slot in 0..chunks.slot_count() {
732            let Some(light_data) = chunks.get_slot(chunk_slot) else {
733                continue;
734            };
735            let Some(chunk_pos) = layout.chunk_pos_for_slot(chunk_slot) else {
736                continue;
737            };
738            let Some(section_slots) = layout.inner_light_section_slots_for_chunk(chunk_pos) else {
739                continue;
740            };
741
742            let layer_storage = Self::layer_storage(light_data, layer);
743            for cached_section in section_slots {
744                let Some(section_index) = layer_storage
745                    .range()
746                    .section_index(cached_section.section_pos.y())
747                else {
748                    continue;
749                };
750                let Some(section) = layer_storage.sections().get(section_index) else {
751                    continue;
752                };
753
754                let edit_index = edits.len();
755                edits.push(LightSectionEdit {
756                    section: copy_light_section(section),
757                    dirty: false,
758                });
759                sections.insert(
760                    cached_section,
761                    LightSectionEditEntry::Stored {
762                        target: StoredLightSectionEdit {
763                            chunk_slot,
764                            section_index,
765                        },
766                        edit_index,
767                    },
768                );
769            }
770        }
771
772        sections
773    }
774
775    fn commit_stored_section(&mut self, target: StoredLightSectionEdit, edit_index: usize) -> bool {
776        let Some(edit) = self.edits.get_mut(edit_index) else {
777            return false;
778        };
779        let edited = mem::replace(&mut edit.section, LightSection::missing());
780        let layer = self.layer;
781        let Some(light_data) = self.chunks.get_mut_slot(target.chunk_slot) else {
782            return false;
783        };
784        let Some(target_section) = Self::layer_storage_mut(light_data, layer)
785            .sections_mut()
786            .get_mut(target.section_index)
787        else {
788            return false;
789        };
790
791        if *target_section == edited {
792            return false;
793        }
794
795        *target_section = edited;
796        true
797    }
798
799    fn set_section_slot_non_missing(&mut self, section_slot: usize) -> bool {
800        let Some(section) = self.section_edit_mut(section_slot) else {
801            return false;
802        };
803
804        let was_missing = matches!(section.section, LightSection::Missing);
805        set_section_non_missing(&mut section.section);
806        section.dirty |= was_missing;
807        was_missing
808    }
809
810    fn section_edit(&self, section_slot: usize) -> Option<&LightSectionEdit> {
811        let entry = self.sections.get_slot(section_slot)?;
812        match entry {
813            LightSectionEditEntry::Stored { edit_index, .. }
814            | LightSectionEditEntry::Transient { edit_index } => self.edits.get(*edit_index),
815        }
816    }
817
818    fn section_edit_mut(&mut self, section_slot: usize) -> Option<&mut LightSectionEdit> {
819        let entry = self.sections.get_slot(section_slot)?;
820        match entry {
821            LightSectionEditEntry::Stored { edit_index, .. }
822            | LightSectionEditEntry::Transient { edit_index } => self.edits.get_mut(*edit_index),
823        }
824    }
825
826    const fn layer_storage(
827        light_data: &ChunkLightData,
828        layer: LightLayer,
829    ) -> &ChunkLightLayerStorage {
830        match layer {
831            LightLayer::Sky => &light_data.sky,
832            LightLayer::Block => &light_data.block,
833        }
834    }
835
836    const fn layer_storage_mut(
837        light_data: &mut ChunkLightData,
838        layer: LightLayer,
839    ) -> &mut ChunkLightLayerStorage {
840        match layer {
841            LightLayer::Sky => &mut light_data.sky,
842            LightLayer::Block => &mut light_data.block,
843        }
844    }
845}
846
847fn copy_light_section(section: &LightSection) -> LightSection {
848    match section {
849        LightSection::Missing => LightSection::missing(),
850        LightSection::Visible(data) => LightSection::visible(copy_light_section_data(data)),
851        LightSection::Internal(data) => LightSection::internal(copy_light_section_data(data)),
852    }
853}
854
855fn copy_light_section_data(data: &LightSectionData) -> LightSectionData {
856    match data {
857        LightSectionData::Homogeneous(value) => LightSectionData::homogeneous(*value),
858        LightSectionData::Packed(data) => LightSectionData::Packed(Box::new(**data)),
859    }
860}
861
862fn set_section_non_missing(section: &mut LightSection) {
863    match section {
864        LightSection::Missing => {
865            *section = LightSection::visible(LightSectionData::homogeneous(0));
866        }
867        LightSection::Visible(_) => {}
868        LightSection::Internal(data) => {
869            *section = LightSection::visible(mem::replace(data, LightSectionData::homogeneous(0)));
870        }
871    }
872}
873
874fn take_internal_section(section: &mut LightSection) -> LightSection {
875    match mem::replace(section, LightSection::missing()) {
876        LightSection::Missing | LightSection::Visible(LightSectionData::Homogeneous(0)) => {
877            LightSection::missing()
878        }
879        LightSection::Visible(data) | LightSection::Internal(data) => LightSection::internal(data),
880    }
881}
882
883fn fill_section(section: &mut LightSection, value: u8) {
884    match section {
885        LightSection::Missing => {
886            *section = LightSection::visible(LightSectionData::homogeneous(value));
887        }
888        LightSection::Visible(data) | LightSection::Internal(data) => data.fill(value),
889    }
890}
891
892fn get_section_value(section: &LightSection, local_index: usize) -> u8 {
893    let data = match section {
894        LightSection::Missing => return 0,
895        LightSection::Visible(data) | LightSection::Internal(data) => data,
896    };
897    let (local_x, local_y, local_z) = local_block_coords(local_index);
898    data.get(local_x, local_y, local_z)
899}
900
901fn set_section_value(section: &mut LightSection, local_index: usize, level: u8) {
902    let data = match section {
903        LightSection::Missing => return,
904        LightSection::Visible(data) | LightSection::Internal(data) => data,
905    };
906    let (local_x, local_y, local_z) = local_block_coords(local_index);
907    data.set(local_x, local_y, local_z, level);
908}
909
910const fn section_has_light_data(section: &LightSection) -> bool {
911    match section {
912        LightSection::Missing | LightSection::Visible(LightSectionData::Homogeneous(0)) => false,
913        LightSection::Visible(_) | LightSection::Internal(_) => true,
914    }
915}
916
917fn lower_row(section: &LightSection) -> Option<[u8; 16 * 16]> {
918    let data = match section {
919        LightSection::Missing => return None,
920        LightSection::Visible(data) | LightSection::Internal(data) => data,
921    };
922
923    if let LightSectionData::Homogeneous(0) = data {
924        return None;
925    }
926
927    let mut row = [0; 16 * 16];
928    for z in 0..16 {
929        for x in 0..16 {
930            row[z * 16 + x] = data.get(x, 0, z);
931        }
932    }
933    Some(row)
934}
935
936fn extrude_lower_row(section: &mut LightSection, row: Option<&[u8; 16 * 16]>) {
937    let Some(row) = row else {
938        *section = LightSection::visible(LightSectionData::homogeneous(0));
939        return;
940    };
941
942    if matches!(section, LightSection::Missing) {
943        *section = LightSection::visible(LightSectionData::homogeneous(0));
944    }
945
946    for y in 0..16 {
947        for z in 0..16 {
948            for x in 0..16 {
949                set_section_value(section, x | (z << 4) | (y << 8), row[z * 16 + x]);
950            }
951        }
952    }
953}
954
955#[cfg(test)]
956mod tests {
957    use std::sync::{Arc, Weak};
958
959    use steel_registry::{init_vanilla_registry, vanilla_blocks};
960    use steel_utils::{BlockPos, SectionPos};
961
962    use super::*;
963    use crate::behavior::init_behaviors;
964    use crate::chunk::{
965        Chunk,
966        chunk_ticket_manager::ChunkTicketLevel,
967        section::{ChunkSection, Sections},
968    };
969
970    fn init_tests() {
971        init_vanilla_registry();
972        init_behaviors();
973    }
974
975    fn range() -> super::super::LightSectionRange {
976        let Ok(range) = super::super::LightSectionRange::from_world_height(0, 16) else {
977            panic!("test height should create a valid light range");
978        };
979        range
980    }
981
982    fn holder_with_section(pos: ChunkPos, section: ChunkSection) -> Arc<ChunkHolder> {
983        let sections = Sections::from_owned(vec![section].into_boxed_slice());
984        let proto = Chunk::new(sections, pos, 0, 16, Weak::new());
985        let holder = Arc::new(ChunkHolder::new(
986            pos,
987            ChunkTicketLevel::FULL_CHUNK,
988            Some(ChunkTicketLevel::FULL_CHUNK),
989            0,
990            16,
991        ));
992        holder.insert_chunk(proto, ChunkStatus::Light);
993        holder
994    }
995
996    fn set_light_section(
997        holder: &ChunkHolder,
998        layer: LightLayer,
999        section_y: i32,
1000        section: LightSection,
1001    ) {
1002        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1003            panic!("test chunk should be available");
1004        };
1005        let mut light = chunk.light_mut();
1006        let storage = match layer {
1007            LightLayer::Sky => &mut light.sky,
1008            LightLayer::Block => &mut light.block,
1009        };
1010        let Some(target) = storage.section_mut(section_y) else {
1011            panic!("test section should be inside light range");
1012        };
1013        *target = section;
1014    }
1015
1016    #[test]
1017    fn workset_pins_cached_chunk_holder_until_dropped() {
1018        init_tests();
1019        let center = ChunkPos::new(0, 0);
1020        let holder = holder_with_section(center, ChunkSection::new_empty());
1021        let layout = LightCacheLayout::new(center, range());
1022
1023        let Ok(workset) = LightWorkset::setup(
1024            layout,
1025            LightCacheSetupRadius::Full,
1026            true,
1027            |pos| (pos == center).then(|| Arc::clone(&holder)),
1028            |_| true,
1029        ) else {
1030            panic!("relaxed setup should accept missing optional chunks");
1031        };
1032
1033        let Some(cached_center) = layout.cached_chunk(center) else {
1034            panic!("center chunk should be inside the cache");
1035        };
1036        assert_eq!(workset.layout(), layout);
1037        assert!(workset.chunk_holder(cached_center).is_some());
1038        assert!(workset.can_read_sections(cached_center));
1039        assert!(workset.can_write_light(cached_center));
1040        assert_eq!(Arc::strong_count(&holder), 2);
1041
1042        drop(workset);
1043        assert_eq!(Arc::strong_count(&holder), 1);
1044    }
1045
1046    #[test]
1047    fn workset_reports_missing_required_inner_chunk() {
1048        init_tests();
1049        let layout = LightCacheLayout::new(ChunkPos::new(0, 0), range());
1050
1051        let result = LightWorkset::setup(
1052            layout,
1053            LightCacheSetupRadius::Inner,
1054            false,
1055            |_| None,
1056            |_| true,
1057        );
1058
1059        assert_eq!(
1060            result.err(),
1061            Some(LightWorksetSetupError::MissingRequiredChunk {
1062                chunk_pos: ChunkPos::new(-1, -1),
1063            })
1064        );
1065    }
1066
1067    #[test]
1068    fn chunk_read_cache_exposes_admitted_chunks() {
1069        init_tests();
1070        let center = ChunkPos::new(0, 0);
1071        let holder = holder_with_section(center, ChunkSection::new_empty());
1072        let layout = LightCacheLayout::new(center, range());
1073        let Ok(workset) = LightWorkset::setup(
1074            layout,
1075            LightCacheSetupRadius::Inner,
1076            true,
1077            |pos| (pos == center).then(|| Arc::clone(&holder)),
1078            |_| true,
1079        ) else {
1080            panic!("relaxed setup should accept missing neighbors");
1081        };
1082        let Some(cached_center) = layout.cached_chunk(center) else {
1083            panic!("center chunk should be inside the cache");
1084        };
1085
1086        workset.with_chunk_read_cache(|chunk_cache| {
1087            assert_eq!(chunk_cache.layout(), layout);
1088            assert!(chunk_cache.chunk(cached_center).is_some());
1089        });
1090    }
1091
1092    #[test]
1093    fn section_read_cache_uses_scalable_lux_local_indices() {
1094        init_tests();
1095        let center = ChunkPos::new(0, 0);
1096        let mut section = ChunkSection::new_empty();
1097        let stone = vanilla_blocks::STONE.default_state();
1098        section.set_block_state(1, 2, 3, stone);
1099        let holder = holder_with_section(center, section);
1100        let layout = LightCacheLayout::new(center, range());
1101        let Ok(workset) = LightWorkset::setup(
1102            layout,
1103            LightCacheSetupRadius::Inner,
1104            true,
1105            |pos| (pos == center).then(|| Arc::clone(&holder)),
1106            |_| true,
1107        ) else {
1108            panic!("relaxed setup should accept missing neighbors");
1109        };
1110
1111        let Some(cached_block) = layout.cached_block(BlockPos::new(1, 2, 3)) else {
1112            panic!("test block should be inside light cache");
1113        };
1114        let read_state = workset.with_chunk_read_cache(|chunk_cache| {
1115            chunk_cache.with_section_read_cache(|section_cache| {
1116                assert_eq!(section_cache.layout(), layout);
1117                section_cache.get_block_state(cached_block)
1118            })
1119        });
1120
1121        assert_eq!(read_state, stone);
1122    }
1123
1124    #[test]
1125    fn section_read_cache_reports_non_empty_sections() {
1126        init_tests();
1127        let center = ChunkPos::new(0, 0);
1128        let mut section = ChunkSection::new_empty();
1129        section.set_block_state(1, 2, 3, vanilla_blocks::STONE.default_state());
1130        let holder = holder_with_section(center, section);
1131        let layout = LightCacheLayout::new(center, range());
1132        let Ok(workset) = LightWorkset::setup(
1133            layout,
1134            LightCacheSetupRadius::Inner,
1135            true,
1136            |pos| (pos == center).then(|| Arc::clone(&holder)),
1137            |_| true,
1138        ) else {
1139            panic!("relaxed setup should accept missing neighbors");
1140        };
1141
1142        workset.with_chunk_read_cache(|chunk_cache| {
1143            chunk_cache.with_section_read_cache(|section_cache| {
1144                assert!(section_cache.has_cached_section(SectionPos::new(0, 0, 0)));
1145                assert!(section_cache.has_non_empty_section(SectionPos::new(0, 0, 0)));
1146                assert!(!section_cache.has_non_empty_section(SectionPos::new(0, 1, 0)));
1147                assert!(!section_cache.has_non_empty_section(SectionPos::new(1, 0, 0)));
1148            });
1149        });
1150    }
1151
1152    #[test]
1153    fn section_read_cache_reports_outer_chunk_emptiness_maps() {
1154        init_tests();
1155        let center = ChunkPos::new(0, 0);
1156        let outer = ChunkPos::new(2, 0);
1157        let center_holder = holder_with_section(center, ChunkSection::new_empty());
1158        let mut outer_section = ChunkSection::new_empty();
1159        outer_section.set_block_state(1, 2, 3, vanilla_blocks::STONE.default_state());
1160        let outer_holder = holder_with_section(outer, outer_section);
1161        let layout = LightCacheLayout::new(center, range());
1162        let Ok(workset) = LightWorkset::setup(
1163            layout,
1164            LightCacheSetupRadius::Full,
1165            true,
1166            |pos| {
1167                if pos == center {
1168                    Some(Arc::clone(&center_holder))
1169                } else if pos == outer {
1170                    Some(Arc::clone(&outer_holder))
1171                } else {
1172                    None
1173                }
1174            },
1175            |_| true,
1176        ) else {
1177            panic!("relaxed setup should accept cached test chunks");
1178        };
1179
1180        workset.with_chunk_read_cache(|chunk_cache| {
1181            chunk_cache.with_section_read_cache(|section_cache| {
1182                assert_eq!(
1183                    section_cache.section_empty(SectionPos::new(outer.0.x, 0, outer.0.y)),
1184                    Some(false)
1185                );
1186                assert!(
1187                    !section_cache.has_non_empty_section(SectionPos::new(outer.0.x, 0, outer.0.y))
1188                );
1189            });
1190        });
1191    }
1192
1193    #[test]
1194    fn workset_can_read_sections_without_writable_light_scope() {
1195        init_tests();
1196        let center = ChunkPos::new(0, 0);
1197        let east = ChunkPos::new(1, 0);
1198        let center_holder = holder_with_section(center, ChunkSection::new_empty());
1199        let mut east_section = ChunkSection::new_empty();
1200        east_section.set_block_state(0, 0, 0, vanilla_blocks::STONE.default_state());
1201        let east_holder = holder_with_section(east, east_section);
1202        let layout = LightCacheLayout::new(center, range());
1203
1204        let Ok(workset) = LightWorkset::setup_with_scopes(
1205            layout,
1206            LightCacheSetupRadius::Inner,
1207            true,
1208            |pos| {
1209                if pos == center {
1210                    Some(Arc::clone(&center_holder))
1211                } else if pos == east {
1212                    Some(Arc::clone(&east_holder))
1213                } else {
1214                    None
1215                }
1216            },
1217            |cached_chunk, _, _| (true, cached_chunk.chunk_pos == center),
1218        ) else {
1219            panic!("relaxed setup should accept missing neighbors");
1220        };
1221
1222        let Some(cached_center) = layout.cached_chunk(center) else {
1223            panic!("center chunk should be cached");
1224        };
1225        let Some(cached_east) = layout.cached_chunk(east) else {
1226            panic!("east chunk should be cached");
1227        };
1228        assert!(workset.can_read_sections(cached_center));
1229        assert!(workset.can_write_light(cached_center));
1230        assert!(workset.can_read_sections(cached_east));
1231        assert!(!workset.can_write_light(cached_east));
1232
1233        workset.with_chunk_read_cache(|chunk_cache| {
1234            chunk_cache.with_section_read_cache(|section_cache| {
1235                assert!(section_cache.has_non_empty_section(SectionPos::new(1, 0, 0)));
1236            });
1237        });
1238    }
1239
1240    #[test]
1241    fn light_edit_reads_writes_and_commits_sections() {
1242        init_tests();
1243        let center = ChunkPos::new(0, 0);
1244        let holder = holder_with_section(center, ChunkSection::new_empty());
1245        let layout = LightCacheLayout::new(center, range());
1246        let Ok(workset) = LightWorkset::setup(
1247            layout,
1248            LightCacheSetupRadius::Inner,
1249            true,
1250            |pos| (pos == center).then(|| Arc::clone(&holder)),
1251            |_| true,
1252        ) else {
1253            panic!("relaxed setup should accept missing neighbors");
1254        };
1255        let Some(cached_block) = layout.cached_block(BlockPos::new(1, 2, 3)) else {
1256            panic!("test block should be inside light cache");
1257        };
1258
1259        let updated = workset.with_chunk_read_cache(|chunk_cache| {
1260            chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
1261                assert_eq!(light_edit.layout(), layout);
1262                assert_eq!(light_edit.layer(), LightLayer::Block);
1263                assert_eq!(light_edit.get(cached_block), 0);
1264                assert!(!light_edit.set(cached_block, 12));
1265                assert!(light_edit.is_section_missing(SectionPos::new(0, 0, 0)));
1266
1267                assert!(light_edit.set_section_non_missing(SectionPos::new(0, 0, 0)));
1268                assert!(light_edit.has_non_missing_section(SectionPos::new(0, 0, 0)));
1269                assert!(!light_edit.is_section_missing(SectionPos::new(0, 0, 0)));
1270                assert!(light_edit.has_non_missing(cached_block));
1271                assert!(!light_edit.has_light_data_section(SectionPos::new(0, 0, 0)));
1272                assert!(light_edit.set(cached_block, 12));
1273                assert_eq!(light_edit.get(cached_block), 12);
1274                assert!(light_edit.has_light_data_section(SectionPos::new(0, 0, 0)));
1275
1276                let mut updated = Vec::new();
1277                assert_eq!(
1278                    light_edit.commit(None, |section_pos| updated.push(section_pos)),
1279                    1
1280                );
1281                updated
1282            })
1283        });
1284
1285        assert_eq!(updated, vec![SectionPos::new(0, 0, 0)]);
1286        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1287            panic!("test chunk should still be available");
1288        };
1289        let light = chunk.light();
1290        assert_eq!(
1291            light.get_light_value(LightLayer::Block, BlockPos::new(1, 2, 3)),
1292            12
1293        );
1294    }
1295
1296    #[test]
1297    fn light_edit_drops_without_commit() {
1298        init_tests();
1299        let center = ChunkPos::new(0, 0);
1300        let holder = holder_with_section(center, ChunkSection::new_empty());
1301        let layout = LightCacheLayout::new(center, range());
1302        let Ok(workset) = LightWorkset::setup(
1303            layout,
1304            LightCacheSetupRadius::Inner,
1305            true,
1306            |pos| (pos == center).then(|| Arc::clone(&holder)),
1307            |_| true,
1308        ) else {
1309            panic!("relaxed setup should accept missing neighbors");
1310        };
1311        let Some(cached_block) = layout.cached_block(BlockPos::new(1, 2, 3)) else {
1312            panic!("test block should be inside light cache");
1313        };
1314
1315        workset.with_chunk_read_cache(|chunk_cache| {
1316            chunk_cache.with_light_edit(LightLayer::Block, |mut light_edit| {
1317                assert!(light_edit.set_section_non_missing(SectionPos::new(0, 0, 0)));
1318                assert!(light_edit.set(cached_block, 12));
1319            });
1320        });
1321
1322        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1323            panic!("test chunk should still be available");
1324        };
1325        let light = chunk.light();
1326        assert_eq!(
1327            light.get_light_value(LightLayer::Block, BlockPos::new(1, 2, 3)),
1328            0
1329        );
1330    }
1331
1332    #[test]
1333    fn light_edit_publishes_explicit_notifications() {
1334        init_tests();
1335        let center = ChunkPos::new(0, 0);
1336        let holder = holder_with_section(center, ChunkSection::new_empty());
1337        set_light_section(
1338            &holder,
1339            LightLayer::Block,
1340            0,
1341            LightSection::visible(LightSectionData::homogeneous(0)),
1342        );
1343        let layout = LightCacheLayout::new(center, range());
1344        let Ok(workset) = LightWorkset::setup(
1345            layout,
1346            LightCacheSetupRadius::Inner,
1347            true,
1348            |pos| (pos == center).then(|| Arc::clone(&holder)),
1349            |_| true,
1350        ) else {
1351            panic!("relaxed setup should accept missing neighbors");
1352        };
1353        let mut notifications = LightUpdateNotificationCache::new(layout);
1354        assert!(notifications.mark_section(SectionPos::new(0, 0, 0)));
1355
1356        let updated = workset.with_chunk_read_cache(|chunk_cache| {
1357            chunk_cache.with_light_edit(LightLayer::Block, |light_edit| {
1358                let mut updated = Vec::new();
1359                assert_eq!(
1360                    light_edit.commit(Some(&notifications), |section_pos| {
1361                        updated.push(section_pos);
1362                    }),
1363                    1
1364                );
1365                updated
1366            })
1367        });
1368
1369        assert_eq!(updated, vec![SectionPos::new(0, 0, 0)]);
1370    }
1371
1372    #[test]
1373    fn sky_edit_materializes_removed_missing_sections_transiently() {
1374        init_tests();
1375        let center = ChunkPos::new(0, 0);
1376        let holder = holder_with_section(center, ChunkSection::new_empty());
1377        let layout = LightCacheLayout::new(center, range());
1378        let Ok(workset) = LightWorkset::setup(
1379            layout,
1380            LightCacheSetupRadius::Inner,
1381            true,
1382            |pos| (pos == center).then(|| Arc::clone(&holder)),
1383            |_| true,
1384        ) else {
1385            panic!("relaxed setup should accept missing neighbors");
1386        };
1387        let section_pos = SectionPos::new(0, 0, 0);
1388        let Some(cached_block) = layout.cached_block(BlockPos::new(1, 2, 3)) else {
1389            panic!("test block should be inside light cache");
1390        };
1391
1392        let updated = workset.with_chunk_read_cache(|chunk_cache| {
1393            chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
1394                light_edit.rewrite_missing_sections_for_skylight();
1395                assert!(!light_edit.has_cached_section(section_pos));
1396                assert!(!light_edit.set(cached_block, 12));
1397
1398                assert!(light_edit.materialize_removed_missing_section(section_pos));
1399                assert!(light_edit.has_cached_section(section_pos));
1400                assert!(light_edit.set_section_non_missing(section_pos));
1401                assert!(light_edit.set(cached_block, 12));
1402
1403                let mut updated = Vec::new();
1404                assert_eq!(
1405                    light_edit.commit(None, |section_pos| updated.push(section_pos)),
1406                    1
1407                );
1408                updated
1409            })
1410        });
1411
1412        assert_eq!(updated, vec![section_pos]);
1413        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1414            panic!("test chunk should still be available");
1415        };
1416        let light = chunk.light();
1417        assert_eq!(light.sky.section(0), Some(&LightSection::missing()));
1418    }
1419
1420    #[test]
1421    fn light_edit_extrudes_lower_row_from_source_above() {
1422        init_tests();
1423        let center = ChunkPos::new(0, 0);
1424        let holder = holder_with_section(center, ChunkSection::new_empty());
1425        set_light_section(
1426            &holder,
1427            LightLayer::Sky,
1428            1,
1429            LightSection::visible(LightSectionData::homogeneous(9)),
1430        );
1431        let layout = LightCacheLayout::new(center, range());
1432        let Ok(workset) = LightWorkset::setup(
1433            layout,
1434            LightCacheSetupRadius::Inner,
1435            true,
1436            |pos| (pos == center).then(|| Arc::clone(&holder)),
1437            |_| true,
1438        ) else {
1439            panic!("relaxed setup should accept missing neighbors");
1440        };
1441        let Some(cached_block) = layout.cached_block(BlockPos::new(1, 15, 3)) else {
1442            panic!("test block should be inside light cache");
1443        };
1444
1445        workset.with_chunk_read_cache(|chunk_cache| {
1446            chunk_cache.with_light_edit(LightLayer::Sky, |mut light_edit| {
1447                assert!(
1448                    light_edit.extrude_lower_from_first_section_above(SectionPos::new(0, 0, 0))
1449                );
1450                assert_eq!(light_edit.get(cached_block), 9);
1451                assert_eq!(light_edit.commit(None, |_| {}), 1);
1452            });
1453        });
1454
1455        let Some(chunk) = holder.try_chunk(ChunkStatus::Empty) else {
1456            panic!("test chunk should still be available");
1457        };
1458        let light = chunk.light();
1459        assert_eq!(
1460            light.get_light_value(LightLayer::Sky, BlockPos::new(1, 15, 3)),
1461            9
1462        );
1463    }
1464}