Skip to content

Commit

Permalink
Add “sweep” updates to LightUpdateQueue.
Browse files Browse the repository at this point in the history
Not used for anything yet, but it will be for `fill_uniform()`.
`set_physics()` doesn't need it because it has `fast_evaluate_light`.
  • Loading branch information
kpreid committed Nov 29, 2023
1 parent a544010 commit 51a9478
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 25 deletions.
52 changes: 52 additions & 0 deletions all-is-cubes/src/math/grid_aab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Cube> = 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() {
Expand Down
2 changes: 1 addition & 1 deletion all-is-cubes/src/space.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
180 changes: 157 additions & 23 deletions all-is-cubes/src/space/light/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -114,27 +114,45 @@ 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<LightUpdateRequest>,
/// 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<Cube, Priority>,

/// 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<GridIter>,

/// 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 {
pub fn new() -> Self {
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();
Expand All @@ -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 });
Expand All @@ -166,8 +209,24 @@ impl LightUpdateQueue {
}
}

/// Removes and returns the highest priority queue entry.
#[inline]
pub fn pop(&mut self) -> Option<LightUpdateRequest> {
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);
Expand All @@ -176,31 +235,59 @@ 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<LightUpdateRequest>) -> Option<Priority> {
queue.last().copied().map(|r| r.priority)
}

#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;

fn drain(queue: &mut LightUpdateQueue) -> Vec<LightUpdateRequest> {
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 = [
Expand All @@ -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));
Expand All @@ -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),
Expand All @@ -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
}
2 changes: 1 addition & 1 deletion all-is-cubes/src/space/light/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down

0 comments on commit 51a9478

Please sign in to comment.