diff --git a/all-is-cubes/src/math/grid_aab.rs b/all-is-cubes/src/math/grid_aab.rs index fdc645544..37060406b 100644 --- a/all-is-cubes/src/math/grid_aab.rs +++ b/all-is-cubes/src/math/grid_aab.rs @@ -2,6 +2,7 @@ //! volumes ([`Vol`]), and related. use alloc::string::String; +use core::cmp::Ordering; use core::fmt; use core::iter::FusedIterator; use core::ops::Range; @@ -767,6 +768,37 @@ impl GridIter { }, } } + + /// Returns the bounds which this iterator iterates over. + /// This may be larger than the union of produced cubes, but it will not be smaller. + pub(crate) fn bounds(&self) -> GridAab { + GridAab::from_ranges([ + self.x_range.clone(), + self.y_range.clone(), + self.z_range.clone(), + ]) + } + + // Returns whether the iterator will produce the given cube. + pub(crate) fn contains_cube(&self, cube: Cube) -> bool { + if !self.bounds().contains_cube(cube) { + return false; + } + match cube.x.cmp(&self.cube.x) { + Ordering::Greater => true, // in a plane not yet emitted + Ordering::Less => false, // in a plane already emitted + Ordering::Equal => { + match cube.y.cmp(&self.cube.y) { + Ordering::Greater => true, // in a row not yet emitted + Ordering::Less => false, // in a row already emitted + Ordering::Equal => { + // We have now reduced to the single-dimensional case. + cube.z >= self.cube.z + } + } + } + } + } } impl Iterator for GridIter { @@ -1047,6 +1079,26 @@ mod tests { } } + #[test] + fn grid_iter_contains_cube() { + let b = GridAab::from_lower_size([0, 0, 0], [3, 3, 3]); + let expected_sequence: Vec = b.interior_iter().collect(); + + let mut iter = b.interior_iter(); + for current in 0..expected_sequence.len() { + for &cube in &expected_sequence[..current] { + assert!(!iter.contains_cube(cube), "{cube:?} should be absent at {current}"); + } + for &cube in &expected_sequence[current..] { + assert!(iter.contains_cube(cube), "{cube:?} should be present at {current}"); + } + + let item = iter.next(); + + assert_eq!(item, Some(expected_sequence[current])); // sanity check, not what we're testing + } + } + #[cfg(feature = "arbitrary")] #[test] fn arbitrary_grid_aab_size_hint() { diff --git a/all-is-cubes/src/space.rs b/all-is-cubes/src/space.rs index 4fbd22ccc..ca856d883 100644 --- a/all-is-cubes/src/space.rs +++ b/all-is-cubes/src/space.rs @@ -286,7 +286,7 @@ impl Space { self.light.get(cube.into()) } - #[allow(unused)] // currently only used on feature=save + #[allow(unused)] // currently only used on feature=save and tests pub(crate) fn in_light_update_queue(&self, cube: Cube) -> bool { self.light.in_light_update_queue(cube) } diff --git a/all-is-cubes/src/space/light/queue.rs b/all-is-cubes/src/space/light/queue.rs index 3a7f9cd4f..d99148bcc 100644 --- a/all-is-cubes/src/space/light/queue.rs +++ b/all-is-cubes/src/space/light/queue.rs @@ -7,7 +7,7 @@ use euclid::Vector3D; use hashbrown::hash_map::Entry; use hashbrown::HashMap as HbHashMap; -use crate::math::{Cube, GridCoordinate, GridPoint, VectorOps as _}; +use crate::math::{Cube, GridAab, GridCoordinate, GridIter, GridPoint, VectorOps as _}; use crate::space::light::PackedLightScalar; /// An entry in a [`LightUpdateQueue`], specifying a cubes that needs its light updated. @@ -114,9 +114,20 @@ pub(crate) struct LightUpdateQueue { /// Sorted storage of queue elements. /// This is a BTreeSet rather than a BinaryHeap so that items can be removed. queue: BTreeSet, - /// Maps Cube to priority value. This allows deduplicating entries, including + + /// Maps [`Cube`] to priority value. This allows deduplicating entries, including /// removing low-priority entries in favor of high-priority ones table: HbHashMap, + + /// If not `None`, then we are performing an update of **every** cube of the space, + /// and this iterator returns the next cube to update at `sweep_priority`. + sweep: Option, + + /// Priority with which the `sweep` should be performed. + sweep_priority: Priority, + + /// Whether a new sweep is needed after the current one. + sweep_again: bool, } impl LightUpdateQueue { @@ -124,17 +135,24 @@ impl LightUpdateQueue { Self { queue: BTreeSet::new(), table: HbHashMap::new(), + sweep: None, + sweep_priority: Priority::MIN, + sweep_again: false, } } #[inline] pub fn contains(&self, cube: Cube) -> bool { self.table.contains_key(&cube) + || self + .sweep + .as_ref() + .is_some_and(|sweep| sweep.contains_cube(cube)) } /// Inserts a queue entry or increases the priority of an existing one. #[inline] - pub fn insert(&mut self, request: LightUpdateRequest) { + pub(crate) fn insert(&mut self, request: LightUpdateRequest) { match self.table.entry(request.cube) { Entry::Occupied(mut e) => { let existing_priority = *e.get(); @@ -155,7 +173,32 @@ impl LightUpdateQueue { } } + /// Requests that the queue should produce every cube in `bounds` at `priority`, + /// without the cost of designating each cube individually. + pub(crate) fn sweep(&mut self, bounds: GridAab, priority: Priority) { + if self + .sweep + .as_ref() + .is_some_and(|it| it.bounds().contains_box(bounds)) + && self.sweep_priority >= priority + { + self.sweep_again = true; + self.sweep_priority = Ord::max(self.sweep_priority, priority); + } else if self.sweep.is_some() { + // Ideally, if we have an existing higher priority sweep, we'd finish it first + // and remember the next one, but not bothering with that now. + self.sweep = Some(bounds.interior_iter()); + self.sweep_priority = Ord::max(self.sweep_priority, priority); + } else { + // No current sweep, so we can ignore existing priority. + self.sweep = Some(bounds.interior_iter()); + self.sweep_priority = priority; + } + } + /// Removes the specified queue entry and returns whether it was present. + /// + /// Sweeps do not count as present entries. pub fn remove(&mut self, cube: Cube) -> bool { if let Some(priority) = self.table.remove(&cube) { let q_removed = self.queue.remove(&LightUpdateRequest { cube, priority }); @@ -166,8 +209,24 @@ impl LightUpdateQueue { } } + /// Removes and returns the highest priority queue entry. #[inline] pub fn pop(&mut self) -> Option { + if let Some(sweep) = &mut self.sweep { + if peek_priority(&self.queue).map_or(true, |p| self.sweep_priority > p) { + if let Some(cube) = sweep.next() { + return Some(LightUpdateRequest { + cube, + priority: self.sweep_priority, + }); + } else { + // Sweep ended + self.sweep = None; + self.sweep_priority = Priority::MIN; + } + } + } + let result = self.queue.pop_last(); if let Some(request) = result { let removed = self.table.remove(&request.cube); @@ -176,24 +235,40 @@ impl LightUpdateQueue { result } + pub fn clear(&mut self) { + self.queue.clear(); + self.table.clear(); + self.sweep = None; + self.sweep_priority = Priority::MIN; + self.sweep_again = false; + } + + /// Returns the number of elements that will be produced by [`Self::pop()`]. #[inline] pub fn len(&self) -> usize { - self.queue.len() + let sweep_items = match &self.sweep { + Some(sweep) => { + sweep.len() + + if self.sweep_again { + sweep.bounds().volume() + } else { + 0 + } + } + None => 0, + }; + self.queue.len() + sweep_items } #[inline] pub fn peek_priority(&self) -> Priority { - self.queue - .last() - .copied() - .map(|r| r.priority) - .unwrap_or(Priority::MIN) + peek_priority(&self.queue).unwrap_or(Priority::MIN) } +} - pub fn clear(&mut self) { - self.queue.clear(); - self.table.clear(); - } +#[inline] +fn peek_priority(queue: &BTreeSet) -> Option { + queue.last().copied().map(|r| r.priority) } #[cfg(test)] @@ -201,6 +276,18 @@ mod tests { use super::*; use alloc::vec::Vec; + fn drain(queue: &mut LightUpdateQueue) -> Vec { + Vec::from_iter(std::iter::from_fn(|| queue.pop())) + } + + fn r(cube: [GridCoordinate; 3], priority: PackedLightScalar) -> LightUpdateRequest { + let priority = Priority(priority); + LightUpdateRequest { + cube: Cube::from(cube), + priority, + } + } + #[test] fn priority_relations() { let least_special_priority = [ @@ -218,14 +305,6 @@ mod tests { #[test] fn queue_ordering() { - fn r(cube: [GridCoordinate; 3], priority: PackedLightScalar) -> LightUpdateRequest { - let priority = Priority(priority); - LightUpdateRequest { - cube: Cube::from(cube), - priority, - } - } - let mut queue = LightUpdateQueue::new(); queue.insert(r([0, 0, 0], 1)); queue.insert(r([2, 0, 0], 1)); @@ -237,7 +316,7 @@ mod tests { queue.insert(r([0, 0, 1], 100)); assert_eq!( - Vec::from_iter(std::iter::from_fn(|| queue.pop())), + drain(&mut queue), vec![ // High priorities r([0, 0, 2], 200), @@ -252,5 +331,60 @@ mod tests { ); } - // TODO: Test of queue priority updates + #[test] + fn sweep_basic() { + let mut queue = LightUpdateQueue::new(); + + queue.insert(LightUpdateRequest { + priority: Priority(101), + cube: Cube::new(0, 101, 0), + }); + queue.insert(LightUpdateRequest { + priority: Priority(100), + cube: Cube::new(0, 100, 0), + }); + queue.insert(LightUpdateRequest { + priority: Priority(99), + cube: Cube::new(0, 99, 0), + }); + queue.sweep( + GridAab::from_lower_upper([0, 0, 0], [3, 1, 1]), + Priority(100), + ); + + assert_eq!(queue.len(), 6); + assert_eq!( + drain(&mut queue), + vec![ + // Higher priority than sweep + r([0, 101, 0], 101), + // Equal priority explicit elements win + r([0, 100, 0], 100), + // Sweep elements. + // Sweeps don't use the interleaved order, not because we don't want to, but + // because that is more complex and thus not implemented. + r([0, 0, 0], 100), + r([1, 0, 0], 100), + r([2, 0, 0], 100), + // Lower priority than sweep + r([0, 99, 0], 99), + ] + ) + } + + #[test] + fn sweep_then_clear() { + let mut queue = LightUpdateQueue::new(); + queue.sweep( + GridAab::from_lower_upper([0, 0, 0], [3, 1, 1]), + Priority(100), + ); + + queue.clear(); + + assert_eq!(queue.len(), 0); + assert_eq!(queue.pop(), None); + } + + // TODO: Test of changing the priority of existing queue entries } diff --git a/all-is-cubes/src/space/light/updater.rs b/all-is-cubes/src/space/light/updater.rs index 9e8f773eb..fd4abe896 100644 --- a/all-is-cubes/src/space/light/updater.rs +++ b/all-is-cubes/src/space/light/updater.rs @@ -170,7 +170,7 @@ impl LightStorage { } } - #[allow(unused)] // currently only used on feature=save + #[allow(unused)] // currently only used on feature=save and tests pub(crate) fn in_light_update_queue(&self, cube: Cube) -> bool { self.light_update_queue.contains(cube) }