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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum LightWorksetSetupError {
18 MissingRequiredChunk {
20 chunk_pos: ChunkPos,
22 },
23}
24
25pub 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 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 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 #[must_use]
102 pub const fn layout(&self) -> LightCacheLayout {
103 self.layout
104 }
105
106 #[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 #[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 #[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 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
179pub struct LightChunkReadCache<'a> {
181 layout: LightCacheLayout,
182 chunks: LightChunkSlotArray<&'a Chunk>,
183 light_chunks: LightChunkSlotArray<&'a Chunk>,
184}
185
186impl LightChunkReadCache<'_> {
187 #[must_use]
189 pub const fn layout(&self) -> LightCacheLayout {
190 self.layout
191 }
192
193 #[must_use]
195 pub fn chunk(&self, cached_chunk: CachedLightChunk) -> Option<&Chunk> {
196 self.chunks.get(cached_chunk).copied()
197 }
198
199 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 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
281pub struct LightSectionReadCache<'a> {
283 layout: LightCacheLayout,
284 sections: LightSectionSlotArray<RwLockReadGuard<'a, ChunkSection>>,
285 emptiness_maps: LightChunkSlotArray<Box<[bool]>>,
286}
287
288impl LightSectionReadCache<'_> {
289 #[must_use]
291 pub const fn layout(&self) -> LightCacheLayout {
292 self.layout
293 }
294
295 #[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 #[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 #[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 #[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
377pub 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 #[must_use]
390 pub const fn layout(&self) -> LightCacheLayout {
391 self.layout
392 }
393
394 #[must_use]
396 pub const fn layer(&self) -> LightLayer {
397 self.layer
398 }
399
400 #[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 #[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 #[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 #[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 #[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 #[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(§ion.section))
450 }
451
452 #[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 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 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 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 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 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 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 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 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 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 #[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(§ion.section, local_index)
656 }
657
658 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 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 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(¢er_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(¢er_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(¬ifications), |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}