all_is_cubes/block/eval/
voxel_storage.rs

1use alloc::sync::Arc;
2use core::hash::Hash as _;
3use core::ops;
4
5// Things mentioned in doc comments only
6#[cfg(doc)]
7use crate::block::{AIR, Atom, Primitive};
8
9use crate::block::{
10    BlockAttributes, BlockCollision, EvaluatedBlock,
11    Resolution::{self, R1},
12};
13use crate::math::{Cube, GridAab, OpacityCategory, Rgb, Rgba, Vol};
14
15/// Properties of an individual voxel within [`EvaluatedBlock`].
16///
17/// This is essentially a subset of the information in a full [`EvaluatedBlock`] and
18/// its [`BlockAttributes`].
19#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
20#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
21#[non_exhaustive]
22pub struct Evoxel {
23    // Note: documentation wording here should match [`BlockAttributes`]
24    /// Diffuse reflection color.
25    ///
26    /// See [`Atom::color`] for details on the meaning of this value.
27    // TODO: Maybe we should convert to a smaller color format at this point?
28    // These are frequently going to be copied into 32-bit texture color anyway.
29    pub color: Rgba,
30
31    /// Light emitted (not reflected) by the voxel.
32    ///
33    /// This field has the same meaning as [`Atom::emission`].
34    pub emission: Rgb,
35
36    /// Whether players' [cursors](crate::character::Cursor) target this voxel's containing
37    /// block or pass through it.
38    pub selectable: bool,
39
40    /// The effect on a [`Body`](crate::physics::Body) of colliding with this voxel.
41    pub collision: BlockCollision,
42}
43
44impl Evoxel {
45    /// The `Evoxel` value that is contained in [`AIR`].
46    ///
47    /// This also represents the behavior of the empty space outside the bounds of
48    /// an [`EvaluatedBlock::voxels`] that is smaller than the full unit cube.
49    pub const AIR: Self = Self {
50        color: Rgba::TRANSPARENT,
51        emission: Rgb::ZERO,
52        selectable: false,
53        collision: BlockCollision::None,
54    };
55
56    /// Construct an [`Evoxel`] which represents the given evaluated block.
57    ///
58    /// This is the same operation as is used for each block/voxel in a [`Primitive::Recur`].
59    pub fn from_block(block: &EvaluatedBlock) -> Self {
60        Self {
61            color: block.color(),
62            emission: block.light_emission(),
63            selectable: block.attributes().selectable,
64            // TODO: This won't generalize properly to having more than 2 states of
65            // BlockCollision. We need uniform_collision to carry more info.
66            collision: block.uniform_collision().unwrap_or(BlockCollision::Hard),
67        }
68    }
69
70    /// Construct the [`Evoxel`] that would have resulted from evaluating a voxel block
71    /// with the given color and default attributes.
72    pub const fn from_color(color: Rgba) -> Self {
73        Self {
74            color,
75            emission: Rgb::ZERO,
76            // Use the selectable value from BlockAttributes's default for consistency.
77            // with the result of a default atom block.
78            selectable: BlockAttributes::DEFAULT_REF.selectable,
79            collision: BlockCollision::DEFAULT_FOR_FROM_COLOR,
80        }
81    }
82
83    /// Reports whether this voxel is invisible, fully opaque, or neither.
84    ///
85    /// This differs from `self.color.opacity_category()` in that it accounts for emission
86    /// making a voxel visible.
87    //---
88    // Inlining is beneficial to performance of all-is-cubes-mesh's analyze() phase.
89    // TODO: Further optimizations? Tweak the data layout and even increase density?
90    #[inline]
91    pub fn opacity_category(&self) -> OpacityCategory {
92        let &Self {
93            color,
94            emission,
95            selectable: _,
96            collision: _,
97        } = self;
98        let mut category = color.opacity_category();
99        if emission != Rgb::ZERO && category == OpacityCategory::Invisible {
100            category = OpacityCategory::Partial;
101        }
102        category
103    }
104}
105
106/// Storage of an [`EvaluatedBlock`]'s shape — its _evaluated voxels._
107///
108/// This voxel data may be smaller than the dimensions implied by [`Self::resolution`],
109/// in which case the out-of-bounds space should be treated as [`Evoxel::AIR`].
110/// The logical bounds are always the cube computed by [`GridAab::for_block`].
111///
112/// This improves on a `Vol<Arc<[Evoxel]>>` by avoiding heap allocation and indirection
113/// for the case of a single element, returning voxels by value rather than
114/// reference, automatically returning [`Evoxel::AIR`] when appropriate, and
115/// enforcing that the `Vol` has no wasted data outside the block bounds.
116#[derive(Clone, Debug, Eq, Hash, PartialEq)]
117#[non_exhaustive]
118pub struct Evoxels(EvoxelsInner);
119
120// TODO: implement Eq and Hash with by-value rather than by-structure comparisons
121// TODO: Consider switching to struct-of-arrays layout that allows uniform properties
122// (e.g. all zero light emission) to not be stored.
123#[derive(Clone, Debug, Eq, Hash, PartialEq)]
124enum EvoxelsInner {
125    /// Compact representation of exactly one voxel. The resolution is implicitly 1.
126    One(Evoxel),
127    /// The [`Vol`] should not have any data outside of the expected bounds
128    /// `GridAab::for_block(resolution)`, but may have less.
129    Many(Resolution, Vol<Arc<[Evoxel]>>),
130}
131
132impl Evoxels {
133    /// Construct an [`Evoxels`] of resolution 1, completely filled with the given voxel.
134    #[inline]
135    pub const fn from_one(voxel: Evoxel) -> Self {
136        Evoxels(EvoxelsInner::One(voxel))
137    }
138
139    /// Construct an [`Evoxels`] storing the given voxels.
140    ///
141    /// Panics if `voxels` contains any data outside the block bounds.
142    /// Such data would at best be ignored, and at worst would confuse block
143    /// processing algorithms, and is therefore rejected.
144    pub fn from_many(resolution: Resolution, voxels: Vol<Arc<[Evoxel]>>) -> Self {
145        let bounds = voxels.bounds();
146        assert!(
147            GridAab::for_block(resolution).contains_box(bounds),
148            "Evoxels data bounds {bounds:?} exceeds specified resolution {resolution}"
149        );
150
151        // TODO: Should this check if the resolution is 1 and switch to `Evoxels::One`,
152        // or is sharing of the `Arc` desirable since it already exists?
153        Evoxels(EvoxelsInner::Many(resolution, voxels))
154    }
155
156    /// Returns the resolution (scale factor) of this set of voxels.
157    /// See [`Resolution`] for more information.
158    #[inline]
159    pub fn resolution(&self) -> Resolution {
160        match self.0 {
161            EvoxelsInner::One(_) => R1,
162            EvoxelsInner::Many(resolution, _) => resolution,
163        }
164    }
165
166    /// Returns the count of voxels, aka [`Vol::volume()`] at the resolution.
167    pub fn count(&self) -> usize {
168        match self.0 {
169            EvoxelsInner::One(_) => 1,
170            EvoxelsInner::Many(_, ref voxels) => voxels.volume(),
171        }
172    }
173
174    /// If this has a resolution of 1, then return that single voxel.
175    #[inline]
176    pub fn single_voxel(&self) -> Option<Evoxel> {
177        match self.0 {
178            EvoxelsInner::One(v) => Some(v),
179            EvoxelsInner::Many(R1, ref voxels) => {
180                Some(voxels.get([0, 0, 0]).copied().unwrap_or(Evoxel::AIR))
181            }
182            EvoxelsInner::Many(_, _) => None,
183        }
184    }
185
186    /// Returns a [`Vol`] borrowing these voxels.
187    pub fn as_vol_ref(&self) -> Vol<&[Evoxel]> {
188        match self.0 {
189            EvoxelsInner::One(ref voxel) => Vol::from_element_ref(voxel),
190            EvoxelsInner::Many(_, ref voxels) => voxels.as_ref(),
191        }
192    }
193
194    /// Returns a [`Vol`] mutably borrowing these voxels.
195    pub fn as_vol_mut(&mut self) -> Vol<&mut [Evoxel]> {
196        match self.0 {
197            EvoxelsInner::One(ref mut voxel) => Vol::from_element_mut(voxel),
198            EvoxelsInner::Many(_, ref mut voxels) => {
199                Vol::from_elements(voxels.bounds(), voxels.make_linear_mut()).unwrap()
200            }
201        }
202    }
203
204    /// Get the single voxel at the specified position, or [`None`] if the position is
205    /// out of bounds of the data (which is not necessarily out of bounds of the block;
206    /// missing data should be taken as [`Evoxel::AIR`]).
207    ///
208    /// Generally behaves like [`Vol::get()`].
209    ///
210    /// TODO: Should we inherently return AIR instead of None?
211    #[inline]
212    pub fn get(&self, position: Cube) -> Option<Evoxel> {
213        match (&self.0, position) {
214            (&EvoxelsInner::One(voxel), Cube::ORIGIN) => Some(voxel),
215            (EvoxelsInner::One(_), _) => None,
216            (EvoxelsInner::Many(_, voxels), position) => voxels.get(position).copied(),
217        }
218    }
219
220    /// Returns the bounds of the voxel data.
221    #[inline]
222    pub fn bounds(&self) -> GridAab {
223        match self.0 {
224            EvoxelsInner::One(_) => GridAab::ORIGIN_CUBE,
225            EvoxelsInner::Many(_, ref voxels) => voxels.bounds(),
226        }
227    }
228
229    /// Check if `self` is equal to `other` without checking every voxel.
230    ///
231    /// May return a false negative if the two are large and do not have equal
232    /// storage pointers.
233    pub(crate) fn cheap_or_ptr_eq(&self, other: &Self) -> bool {
234        // TODO: We could probably afford to compare the voxels if there are sufficiently few.
235        // Need a benchmark to judge the threshold.
236        match (self.single_voxel(), other.single_voxel()) {
237            (Some(v1), Some(v2)) => v1 == v2,
238            (None, Some(_)) | (Some(_), None) => false, // Must be unequal!
239            (None, None) => {
240                self.resolution() == other.resolution()
241                    && self.bounds() == other.bounds()
242                    && core::ptr::eq(
243                        self.as_vol_ref().as_linear(),
244                        other.as_vol_ref().as_linear(),
245                    )
246            }
247        }
248    }
249
250    /// Hash function that matches [`Self::cheap_or_ptr_eq()`].
251    pub(crate) fn cheap_or_ptr_hash<H: core::hash::Hasher>(&self, state: &mut H) {
252        match self.single_voxel() {
253            Some(v) => {
254                0u8.hash(state);
255                v.hash(state);
256            }
257            None => {
258                1u8.hash(state);
259                self.resolution().hash(state);
260                self.as_vol_ref().as_linear().as_ptr().hash(state);
261            }
262        }
263    }
264
265    #[cfg(debug_assertions)]
266    pub(crate) fn consistency_check(&self) {
267        let allowed_bounds = GridAab::for_block(self.resolution());
268        let actual_bounds = self.bounds();
269        assert!(
270            allowed_bounds.contains_box(actual_bounds),
271            "Evoxels contains out of bounds voxels {actual_bounds:?} \
272                within allowed {allowed_bounds:?}"
273        );
274    }
275}
276
277impl ops::Index<Cube> for Evoxels {
278    type Output = Evoxel;
279
280    #[inline]
281    #[track_caller]
282    fn index(&self, position: Cube) -> &Self::Output {
283        match (&self.0, position) {
284            (EvoxelsInner::One(voxel), Cube::ORIGIN) => voxel,
285            (EvoxelsInner::One(_), _) => panic!("out of bounds of Evoxels::One"),
286            (EvoxelsInner::Many(_, voxels), position) => &voxels[position],
287        }
288    }
289}
290
291#[cfg(feature = "arbitrary")]
292#[mutants::skip]
293impl<'a> arbitrary::Arbitrary<'a> for Evoxels {
294    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
295        use crate::math::GridCoordinate;
296        use euclid::point3;
297
298        let resolution = Resolution::arbitrary(u)?;
299        Ok(if resolution == R1 {
300            Evoxels::from_one(u.arbitrary()?)
301        } else {
302            let limit = GridCoordinate::from(resolution) - 1;
303            let lower_bounds = point3(
304                u.int_in_range(0..=limit)?,
305                u.int_in_range(0..=limit)?,
306                u.int_in_range(0..=limit)?,
307            );
308            let upper_bounds = point3(
309                u.int_in_range(lower_bounds.x..=limit)?,
310                u.int_in_range(lower_bounds.y..=limit)?,
311                u.int_in_range(lower_bounds.z..=limit)?,
312            );
313            let bounds = GridAab::from_lower_upper(lower_bounds, upper_bounds).to_vol().unwrap();
314            let contents = u
315                .arbitrary_iter()?
316                .take(bounds.volume())
317                .collect::<Result<Arc<[Evoxel]>, _>>()?;
318            Evoxels::from_many(
319                resolution,
320                bounds
321                    .with_elements(contents)
322                    .map_err(|_wrong_length| arbitrary::Error::NotEnoughData)?,
323            )
324        })
325    }
326
327    fn size_hint(depth: usize) -> (usize, Option<usize>) {
328        use crate::math::GridCoordinate;
329
330        let max_data_size =
331            GridAab::for_block(Resolution::MAX).volume().unwrap() * Evoxel::size_hint(0).1.unwrap();
332
333        arbitrary::size_hint::and(
334            Resolution::size_hint(depth),
335            arbitrary::size_hint::or(
336                Evoxel::size_hint(depth), // single-voxel case
337                arbitrary::size_hint::and(
338                    (3, Some(size_of::<GridCoordinate>() * 6)), // variable size of bounds choice
339                    (0, Some(max_data_size)),                   // data
340                ),
341            ),
342        )
343    }
344}
345
346impl crate::universe::VisitHandles for Evoxels {
347    fn visit_handles(&self, visitor: &mut dyn crate::universe::HandleVisitor) {
348        // Currently there are never any handles in voxels, but that could someday change,
349        // so we offer this implementation to be prepared.
350        let _ = visitor;
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use crate::block::Resolution::*;
358    use core::hash::{BuildHasher as _, Hasher};
359
360    #[test]
361    fn cheap_eq_hash() {
362        // Some distinct voxels to compare
363        let vox1 = Evoxel::AIR;
364        let vox2 = Evoxel {
365            emission: Rgb::new(1.0, 2.0, 3.0),
366            ..Evoxel::AIR
367        };
368
369        let vol_of_vox1 = Vol::from_element(vox1);
370        let samples = [
371            // We need multiple Evoxels::One to prove we're not incorrectly using the address
372            // of the direct value.
373            (0, Evoxels::from_one(vox1)),
374            (0, Evoxels::from_one(vox1)),
375            (1, Evoxels::from_one(vox2)),
376            // these clones should ve equal
377            (2, Evoxels::from_many(R2, vol_of_vox1.clone())),
378            (2, Evoxels::from_many(R2, vol_of_vox1.clone())),
379            // these two will be unequal since they are separate allocations for the same values,
380            (3, Evoxels::from_many(R2, Vol::from_element(vox2))),
381            (4, Evoxels::from_many(R2, Vol::from_element(vox2))),
382            // Different resolution makes this different from group 2
383            (5, Evoxels::from_many(R4, vol_of_vox1.clone())),
384            // Different bounds makes this different from vol_of_vox1
385            (6, Evoxels::from_many(R2, vol_of_vox1.translate([1, 0, 0]))),
386        ];
387
388        let hasher = std::hash::BuildHasherDefault::<std::hash::DefaultHasher>::default();
389        let cheap_hash_one = |value: &Evoxels| {
390            let mut state = hasher.build_hasher();
391            value.cheap_or_ptr_hash(&mut state);
392            state.finish()
393        };
394
395        let mut equals = 0;
396        let mut collisions = 0;
397        let mut checks = 0;
398        for all1 @ (_index1, (class1, value1)) in samples.iter().enumerate() {
399            for all2 @ (_index2, (class2, value2)) in samples.iter().enumerate() {
400                eprintln!("checking {value1:?} == {value2:?}");
401
402                let equal12 = value1.cheap_or_ptr_eq(value2);
403                let hash1 = cheap_hash_one(value1);
404                let hash2 = cheap_hash_one(value2);
405
406                assert_eq!(
407                    equal12,
408                    class1 == class2,
409                    "equality not as expected for:\nleft: {all1:?}\nright: {all2:?}"
410                );
411
412                if equal12 {
413                    assert_eq!(hash1, hash2);
414                    equals += 1;
415                } else if hash1 == hash2 {
416                    collisions += 1;
417                }
418                checks += 1;
419            }
420        }
421
422        dbg!(equals, collisions, checks);
423
424        // This is unfortunately probabilistic and might be broken by a change in implementation,
425        // but we do want to be assured that the hash is okayish.
426        assert!(collisions < checks / 10);
427    }
428
429    #[cfg(feature = "arbitrary")]
430    #[test]
431    fn arbitrary_size_hints() {
432        use arbitrary::Arbitrary as _;
433
434        // Check that the size hints are bounded.
435        Evoxel::size_hint(0).1.unwrap();
436        Evoxels::size_hint(0).1.unwrap();
437    }
438}