1#![allow(
4 elided_lifetimes_in_paths,
5 clippy::needless_pass_by_value,
6 reason = "Bevy systems"
7)]
8
9use alloc::sync::{Arc, Weak};
10use alloc::vec::Vec;
11use core::cell::Cell;
12use core::fmt;
13use core::sync::atomic::Ordering;
14
15use bevy_ecs::change_detection::DetectChangesMut as _;
16use bevy_ecs::prelude as ecs;
17use bevy_ecs::schedule::IntoScheduleConfigs as _;
18use bevy_platform::sync::Mutex;
19use hashbrown::HashMap as HbHashMap;
20use itertools::Itertools as _;
21
22use crate::block::{self, AIR, AIR_EVALUATED, Block, BlockChange, EvaluatedBlock};
23use crate::listen::{self, Listener as _};
24use crate::math::{self, OpacityCategory};
25use crate::space::{BlockIndex, ChangeBuffer, SetCubeError, SpaceChange};
26use crate::time::{self, TimeStats};
27use crate::universe::{self, ReadTicket};
28use crate::util::atomic_cell::{AtomicCell32 as AtomicCell, ZERO, ZERO3, Zero, Zero3};
29
30#[cfg(doc)]
31use crate::space;
32
33cfg_if::cfg_if! {
34 if #[cfg(feature = "std")] {
35 use std::collections::HashMap as BlockHashMap;
37 } else {
38 use hashbrown::HashMap as BlockHashMap;
40 }
41
42}
43
44#[derive(ecs::Component)]
48#[require(PaletteUpdates)]
49pub(crate) struct Palette {
51 entries: Vec<SpaceBlockData>,
53
54 block_to_index: BlockHashMap<Block, BlockIndex>,
58
59 todo: Arc<Mutex<PaletteTodo>>,
61}
62
63impl Palette {
64 pub(crate) fn new(read_ticket: ReadTicket<'_>, block: Block, count: usize) -> Self {
66 let todo = Default::default();
67
68 if count == 0 {
69 return Self {
70 entries: Vec::new(),
71 block_to_index: BlockHashMap::new(),
72 todo,
73 };
74 }
75
76 let gate = Arc::new(());
77 let mut block_data = SpaceBlockData::new(
78 read_ticket,
79 block.clone(),
80 Arc::new(BlockListener {
82 gate: Arc::downgrade(&gate),
83 todo: Arc::downgrade(&todo),
84 index: AtomicCell::new(ListenerIndexState::Set(ZERO, 0)),
85 }),
86 gate,
87 );
88
89 block_data.count = count;
90
91 Self {
92 entries: vec![block_data],
93 block_to_index: BlockHashMap::from([(block, 0)]),
94 todo,
95 }
96 }
97
98 pub(crate) fn from_blocks(
104 read_ticket: ReadTicket<'_>,
105 blocks: &mut dyn ExactSizeIterator<Item = Block>,
106 ) -> Result<(Self, hashbrown::HashMap<BlockIndex, BlockIndex>), PaletteError> {
107 let dummy_notifier = listen::Notifier::new();
108 let dummy_buffer = &mut dummy_notifier.buffer();
109
110 let len = blocks.len();
111 if len.saturating_sub(1) > (BlockIndex::MAX as usize) {
112 return Err(PaletteError::PaletteTooLarge { len });
113 }
114
115 let mut new_self = Self {
116 entries: Vec::with_capacity(blocks.len()),
117 block_to_index: BlockHashMap::with_capacity(blocks.len()),
118 todo: Default::default(),
119 };
120
121 let mut remapping = hashbrown::HashMap::new();
122 for (original_index, block) in (0..=BlockIndex::MAX).zip(blocks) {
123 let new_index = new_self
124 .ensure_index(
125 &mut EvaluationMethod::Ticket(read_ticket),
126 &block,
127 dummy_buffer,
128 false,
129 )
130 .expect("palette iterator lied about its length");
131 if new_index != original_index {
132 remapping.insert(original_index, new_index);
133 }
134 }
135
136 Ok((new_self, remapping))
137 }
138
139 pub(crate) fn entries(&self) -> &[SpaceBlockData] {
140 &self.entries
141 }
142
143 #[inline]
145 #[track_caller]
146 pub(crate) fn entry(&self, index: BlockIndex) -> &SpaceBlockData {
147 &self.entries[index as usize]
148 }
149
150 pub(crate) fn all_block_opacities_as_category(&self) -> OpacityCategory {
154 self.entries
155 .iter()
156 .map(|entry| entry.evaluated.opacity_as_category())
157 .all_equal_value()
158 .unwrap_or(OpacityCategory::Partial)
159 }
160
161 #[inline]
169 pub(super) fn ensure_index(
170 &mut self,
171 evaluation_method: &mut EvaluationMethod<'_>,
172 block: &Block,
173 change_buffer: &mut ChangeBuffer<'_>,
174 use_zeroed_entries: bool,
175 ) -> Result<BlockIndex, TooManyBlocks> {
176 if let Some(&old_index) = self.block_to_index.get(block) {
177 Ok(old_index)
178 } else {
179 let high_mark = self.entries.len();
182 if use_zeroed_entries {
183 for new_index in 0..high_mark {
184 if self.entries[new_index].count == 0 {
185 self.entries[new_index] = self.create_pending_entry(
186 evaluation_method,
187 block.clone(),
188 new_index as BlockIndex,
189 );
190 self.block_to_index.insert(block.clone(), new_index as BlockIndex);
191 change_buffer.push(SpaceChange::BlockIndex(new_index as BlockIndex));
192 return Ok(new_index as BlockIndex);
193 }
194 }
195 }
196 if high_mark > BlockIndex::MAX as usize {
197 return Err(TooManyBlocks);
198 }
199 let new_index = high_mark as BlockIndex;
200 let new_data = self.create_pending_entry(evaluation_method, block.clone(), new_index);
202 self.entries.push(new_data);
204 self.block_to_index.insert(block.clone(), new_index);
205 change_buffer.push(SpaceChange::BlockIndex(new_index));
206 Ok(new_index)
207 }
208 }
209
210 pub(super) fn try_replace_unique(
213 &mut self,
214 evaluation_method: &mut EvaluationMethod<'_>,
215 old_block_index: BlockIndex,
216 new_block: &Block,
217 change_buffer: &mut ChangeBuffer<'_>,
218 ) -> bool {
219 if self.entries[old_block_index as usize].count == 1
220 && !self.block_to_index.contains_key(new_block)
221 {
222 let old_block = {
224 let mut data = self.create_pending_entry(
225 evaluation_method,
226 new_block.clone(),
227 old_block_index,
228 );
229 data.count = 1;
230 core::mem::swap(&mut data, &mut self.entries[old_block_index as usize]);
231 data.block
232 };
233
234 self.block_to_index.remove(&old_block);
236 self.block_to_index.insert(new_block.clone(), old_block_index);
237
238 change_buffer.push(SpaceChange::BlockIndex(old_block_index));
239
240 true
241 } else {
242 false
243 }
244 }
245
246 pub(crate) fn increment(&mut self, index: u16) {
247 self.entries[index as usize].count += 1
248 }
249
250 pub(crate) fn decrement_maybe_free(&mut self, old_block_index: BlockIndex) {
251 let old_data: &mut SpaceBlockData = &mut self.entries[old_block_index as usize];
252 old_data.count -= 1;
253 if old_data.count == 0 {
254 self.block_to_index.remove(&old_data.block);
256 *old_data = SpaceBlockData::tombstone();
257 }
258 }
259
260 pub(crate) fn free_all_zero_counts(&mut self) {
261 for data in self.entries.iter_mut() {
262 if data.count == 0 {
263 self.block_to_index.remove(&data.block);
264 *data = SpaceBlockData::tombstone();
265 }
266 }
267 }
268
269 fn create_pending_entry(
272 &self,
273 evaluation_method: &mut EvaluationMethod<'_>,
274 block: Block,
275 index: BlockIndex,
276 ) -> SpaceBlockData {
277 match evaluation_method {
278 &mut EvaluationMethod::Ticket(read_ticket) => {
279 let (listener, gate) =
280 self.listener_for_block(ListenerIndexState::Set(ZERO, index));
281 SpaceBlockData::new(read_ticket, block, listener, gate)
282 }
283 EvaluationMethod::AlreadyEvaluated(data) => {
284 let PendingEvaluation { entry, listener } =
285 data.take().expect("shouldn’t happen: new entry already taken");
286 let already_changed = listener.set_index(index);
287 if already_changed {
288 self.todo.lock().unwrap().blocks.insert(index);
289 }
290 entry
291 }
292 }
293 }
294
295 fn listener_for_block(&self, index_state: ListenerIndexState) -> (Arc<BlockListener>, Arc<()>) {
301 let gate = Arc::new(());
302 let listener = Arc::new(BlockListener {
303 gate: Arc::downgrade(&gate),
304 todo: Arc::downgrade(&self.todo),
305 index: AtomicCell::new(index_state),
306 });
307 (listener, gate)
308 }
309
310 #[cfg(test)]
313 #[track_caller]
314 pub(crate) fn consistency_check(&self, contents: &[BlockIndex]) {
315 let mut problems = Vec::new();
316
317 let mut actual_counts: hashbrown::HashMap<BlockIndex, usize> = Default::default();
318 for index in contents.iter().copied() {
319 *actual_counts.entry(index).or_insert(0) += 1;
320 }
321
322 for (index, data) in self.entries.iter().enumerate() {
324 let index = index as BlockIndex;
325
326 let actual_count = actual_counts.remove(&index).unwrap_or(0);
327 if data.count != actual_count {
328 problems.push(format!(
329 "Index {} appears {} times but {:?}",
330 index, actual_count, &data
331 ));
332 }
333 }
334
335 if !actual_counts.is_empty() {
338 problems.push(format!(
339 "Block indexes were not indexed in palette: {:?}",
340 &actual_counts
341 ));
342 }
343
344 for (index, data) in self.entries.iter().enumerate() {
346 if data.count == 0 {
347 continue;
349 }
350 let bti_index = self.block_to_index.get(&data.block).copied();
351 if bti_index != Some(index as BlockIndex) {
352 problems.push(format!(
353 "block_to_index[{:?}] should have been {:?}={:?} but was {:?}={:?}",
354 &data.block,
355 index,
356 data,
357 bti_index,
358 bti_index.map(|i| self.entries.get(usize::from(i))),
359 ));
360 }
361 }
362 for (block, &index) in self.block_to_index.iter() {
364 let data = self.entries.get(usize::from(index));
365 if Some(block) != data.map(|data| &data.block) {
366 problems.push(format!(
367 "block_to_index[{block:?}] points to {index} : {data:?}"
368 ));
369 }
370 }
371
372 if !problems.is_empty() {
373 panic!(
374 "Palette consistency check failed:\n • {}\n",
375 problems.join("\n • ")
376 );
377 }
378 }
379
380 #[cfg(test)]
385 pub(crate) fn block_is_to_be_reevaluated(&self, block: &Block) -> bool {
386 let index = self.block_to_index[block];
387 self.todo.lock().unwrap().blocks.contains(&index)
388 }
389}
390
391impl fmt::Debug for Palette {
392 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
393 let Self {
394 entries,
395 block_to_index: _,
396 todo: _,
397 } = self;
398
399 write!(fmt, "Palette(")?;
402 fmt::Debug::fmt(entries, fmt)?;
403 write!(fmt, ")")?;
404 Ok(())
405 }
406}
407
408impl universe::VisitHandles for Palette {
409 fn visit_handles(&self, visitor: &mut dyn universe::HandleVisitor) {
410 for SpaceBlockData { block, .. } in self.entries.iter() {
411 block.visit_handles(visitor);
412 }
413 }
414}
415
416impl Clone for Palette {
417 fn clone(&self) -> Self {
421 let todo = Arc::new(Mutex::new(PaletteTodo {
425 blocks: hashbrown::HashSet::from_iter((0..self.entries.len()).map(|i| i as BlockIndex)),
426 }));
427
428 Self {
429 entries: self
430 .entries()
431 .iter()
432 .map(|e| SpaceBlockData {
433 block: e.block.clone(),
434 count: e.count,
435 evaluated: e.evaluated.clone(),
436 block_listen_gate: None,
437 })
438 .collect(),
439 block_to_index: self.block_to_index.clone(),
440 todo,
441 }
442 }
443}
444
445pub struct SpaceBlockData {
455 pub(super) block: Block,
457 count: usize,
459 pub(super) evaluated: EvaluatedBlock,
460 #[expect(dead_code, reason = "Used only for its `Drop`")]
461 block_listen_gate: Option<Arc<()>>,
462}
463
464impl fmt::Debug for SpaceBlockData {
465 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
466 fmt.debug_struct("SpaceBlockData")
469 .field("count", &self.count)
470 .field("block", &self.block)
471 .finish_non_exhaustive()
472 }
473}
474
475impl SpaceBlockData {
476 pub const NOTHING: Self = Self {
479 block: AIR,
480 count: 0,
481 evaluated: AIR_EVALUATED,
482 block_listen_gate: None,
483 };
484
485 fn tombstone() -> Self {
490 Self {
491 block: AIR,
492 count: 0,
493 evaluated: AIR_EVALUATED,
494 block_listen_gate: None,
495 }
496 }
497
498 fn new(
499 read_ticket: ReadTicket<'_>,
500 block: Block,
501 block_listener: Arc<BlockListener>,
502 block_listen_gate: Arc<()>,
503 ) -> Self {
504 let block_listener: listen::DynListener<BlockChange> = block_listener; let original_budget = block::Budget::default();
509 let filter = block::EvalFilter {
510 read_ticket,
511 skip_eval: false,
512 listener: Some(block_listener.clone()),
513 budget: Cell::new(original_budget),
514 };
515 let evaluated = match block.evaluate2(&filter) {
516 Ok(ev) => ev,
517 Err(err) => {
518 block_listener.receive(&[BlockChange::new()]);
520 err.to_placeholder()
522 }
523 };
524 Self {
525 block,
526 count: 0,
527 evaluated,
528 block_listen_gate: Some(block_listen_gate),
529 }
530 }
531
532 #[inline]
534 pub fn block(&self) -> &Block {
535 &self.block
536 }
537
538 #[inline]
542 pub fn evaluated(&self) -> &EvaluatedBlock {
543 &self.evaluated
544 }
545
546 #[inline]
547 pub(crate) fn count(&self) -> usize {
548 self.count
549 }
550
551 }
554
555#[allow(clippy::large_enum_variant)]
559pub(in crate::space) enum EvaluationMethod<'t> {
560 Ticket(ReadTicket<'t>),
562
563 AlreadyEvaluated(Option<PendingEvaluation>),
569}
570
571#[derive(Debug, Default)]
575struct PaletteTodo {
576 blocks: hashbrown::HashSet<BlockIndex>,
577}
578
579#[derive(Debug)]
584struct BlockListener {
585 gate: Weak<()>,
588
589 todo: Weak<Mutex<PaletteTodo>>,
591
592 index: AtomicCell<ListenerIndexState>,
594}
595
596#[repr(u8, align(4))]
600#[derive(Debug, Clone, Copy, bytemuck::NoUninit, bytemuck::CheckedBitPattern)]
601enum ListenerIndexState {
602 Unset(Zero3),
607
608 AlreadyChanged(Zero3),
613
614 Set(Zero, BlockIndex),
619}
620
621impl BlockListener {
622 #[must_use]
624 fn set_index(&self, index: BlockIndex) -> bool {
625 match self.index.swap(ListenerIndexState::Set(ZERO, index), Ordering::Release) {
626 ListenerIndexState::Unset(ZERO3) => false,
627 ListenerIndexState::AlreadyChanged(ZERO3) => true,
628 ListenerIndexState::Set(ZERO, _index) => panic!("index should only be set once"),
629 }
630 }
631}
632
633impl listen::Listener<BlockChange> for BlockListener {
634 fn receive(&self, messages: &[BlockChange]) -> bool {
635 if self.gate.strong_count() == 0 {
636 false
637 } else if let Some(todo_mutex) = self.todo.upgrade() {
638 if !messages.is_empty() {
639 let index = match self.index.compare_exchange(
642 ListenerIndexState::Unset(ZERO3),
643 ListenerIndexState::AlreadyChanged(ZERO3),
644 Ordering::AcqRel,
645 Ordering::Acquire,
646 ) {
647 Ok(_) | Err(ListenerIndexState::AlreadyChanged(ZERO3)) => {
648 return true;
651 }
652 Err(ListenerIndexState::Set(ZERO, index)) => index,
653 Err(ListenerIndexState::Unset(ZERO3)) => unreachable!(),
654 };
655
656 if let Ok(mut todo) = todo_mutex.lock() {
657 todo.blocks.insert(index);
658 } else {
659 return false;
661 }
662 }
663 true
664 } else {
665 false
666 }
667 }
668}
669
670#[derive(Debug)]
675pub(in crate::space) struct PendingEvaluation {
676 entry: SpaceBlockData,
677 listener: Arc<BlockListener>,
678}
679
680impl PendingEvaluation {
681 pub fn new(read_ticket: ReadTicket<'_>, palette: &Palette, block: Block) -> Self {
682 let (listener, gate) = palette.listener_for_block(ListenerIndexState::Unset(ZERO3));
683 Self {
684 entry: SpaceBlockData::new(read_ticket, block, listener.clone(), gate),
685 listener,
686 }
687 }
688}
689
690#[derive(Clone, Debug, PartialEq)]
696#[allow(missing_docs)]
697#[non_exhaustive]
698pub enum PaletteError {
699 PaletteTooLarge { len: usize },
701
702 Index {
704 index: BlockIndex,
705 cube: math::Cube,
706 palette_len: usize,
707 },
708
709 WrongDataBounds {
711 expected: math::GridAab,
712 actual: math::GridAab,
713 },
714
715 Duplicate {
719 index_1: BlockIndex,
720 index_2: BlockIndex,
721 block: Block,
722 },
723}
724
725impl core::error::Error for PaletteError {}
726
727impl fmt::Display for PaletteError {
728 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
729 match self {
730 PaletteError::PaletteTooLarge { len } => {
731 write!(f, "a palette of {len} blocks is too large")
732 }
733 PaletteError::Index {
734 index,
735 cube,
736 palette_len,
737 } => write!(
738 f,
739 "block index {index} for cube {cube:?} exceeds palette length {palette_len}",
740 cube = Into::<[i32; 3]>::into(*cube),
741 ),
742 PaletteError::WrongDataBounds { expected, actual } => write!(
743 f,
744 "data bounds {actual:?} is incorrect for space bounds {expected:?}",
745 ),
746 PaletteError::Duplicate {
747 index_1,
748 index_2,
749 block,
750 } => write!(
751 f,
752 "duplicate block at indices {index_1} and {index_2}: {block:?}",
753 ),
754 }
755 }
756}
757
758#[derive(Copy, Clone, Debug, Eq, PartialEq)]
764pub(crate) struct TooManyBlocks;
765
766impl From<TooManyBlocks> for SetCubeError {
767 fn from(TooManyBlocks: TooManyBlocks) -> Self {
768 SetCubeError::TooManyBlocks()
769 }
770}
771
772#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, bevy_ecs::schedule::SystemSet)]
776pub(crate) struct SpacePaletteUpdateSet;
777
778pub(crate) struct PaletteStatsTag;
780
781pub(super) fn add_palette_systems(world: &mut ecs::World) {
782 let mut schedules = world.resource_mut::<ecs::Schedules>();
783
784 schedules.add_systems(
785 time::schedule::Synchronize,
786 (update_palette_phase_1, update_palette_phase_2).in_set(SpacePaletteUpdateSet),
787 );
788}
789
790#[derive(bevy_ecs::component::Component, Default)]
795pub(in crate::space) struct PaletteUpdates(HbHashMap<BlockIndex, EvaluatedBlock>);
796
797pub(crate) fn update_palette_phase_1(
801 mut info_collector: ecs::ResMut<universe::InfoCollector<TimeStats, PaletteStatsTag>>,
802 mut spaces: ecs::Query<'_, '_, (&Palette, &mut PaletteUpdates)>,
803 data_sources: universe::QueryBlockDataSources<'_, '_>,
804) {
805 let data_sources = data_sources.get();
806 let read_ticket = ReadTicket::from_queries(&data_sources);
807
808 for (current_palette, mut next_palette) in spaces.iter_mut() {
811 debug_assert!(
812 next_palette.0.is_empty(),
813 "PaletteUpdates should have been cleared"
814 );
815
816 let mut last_start_time = time::Instant::now();
817 let mut evaluations = TimeStats::default();
818
819 let mut try_eval_again = hashbrown::HashSet::new();
820 let mut todo = current_palette.todo.lock().unwrap();
821 for block_index in todo.blocks.drain() {
822 let data: &SpaceBlockData = ¤t_palette.entries[usize::from(block_index)];
823
824 next_palette.0.insert(
830 block_index,
831 data.block.evaluate(read_ticket).unwrap_or_else(|e| {
832 try_eval_again.insert(block_index);
834
835 e.to_placeholder()
836 }),
837 );
838
839 evaluations.record_consecutive_interval(&mut last_start_time, time::Instant::now());
843 }
844 if !try_eval_again.is_empty() {
845 todo.blocks = try_eval_again;
846 }
847
848 info_collector.record(evaluations);
849 }
850}
851
852pub(crate) fn update_palette_phase_2(
858 mut spaces: ecs::Query<
859 (&mut Palette, &mut PaletteUpdates, &super::Notifiers),
860 ecs::Changed<PaletteUpdates>,
861 >,
862) {
863 spaces
864 .par_iter_mut()
865 .for_each(|(mut current_palette, mut next_palette, notifiers)| {
866 let change_buffer: &mut ChangeBuffer<'_> = &mut notifiers.change_notifier.buffer();
867
868 for (block_index, evaluation) in next_palette.bypass_change_detection().0.drain() {
872 current_palette.entries[usize::from(block_index)].evaluated = evaluation;
873 change_buffer.push(SpaceChange::BlockEvaluation(block_index));
874 }
875 });
876}
877
878#[cfg(test)]
881mod tests {
882 use super::*;
883 use crate::content::make_some_blocks;
884 use crate::math::{GridAab, Rgba, Vol};
885 use crate::space::Space;
886 use pretty_assertions::assert_eq;
887
888 #[test]
889 fn clone_palette() {
890 let blocks = make_some_blocks::<2>();
891 let bounds = GridAab::from_lower_size([0, 0, 0], [3, 1, 1]);
893 let space = Space::builder(bounds)
894 .palette_and_contents(
895 blocks.clone(),
896 Vol::from_elements(bounds, [0, 1, 0]).unwrap(),
897 None,
898 )
899 .unwrap()
900 .build();
901
902 let cloned = space.palette.clone();
903
904 cloned.consistency_check(space.contents.as_linear());
906
907 let extract = |p: &Palette| {
908 p.entries().iter().map(|e| (e.block.clone(), e.count)).collect::<Vec<_>>()
909 };
910 assert_eq!(extract(&cloned), extract(&space.palette));
911
912 }
914
915 #[test]
916 fn with_maximum_number_of_entries() {
917 let expected_len = usize::from(BlockIndex::MAX) + 1;
918 let mut blocks_iter = (0..=BlockIndex::MAX).map(|i| {
919 Block::builder()
921 .color(Rgba::WHITE)
922 .display_name(arcstr::format!("#{i}"))
923 .build()
924 });
925
926 let (palette, remap) = Palette::from_blocks(ReadTicket::stub(), &mut blocks_iter).unwrap();
927
928 assert_eq!(palette.entries().len(), expected_len);
929 assert_eq!(remap, hashbrown::HashMap::default());
930 }
931
932 }