From e4def9fe44370c1eb40153395514264cf42c99ad Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 26 Jul 2024 13:15:57 -0700 Subject: [PATCH] gpu: Make `Alloctree` use `u16` coordinates and typed outputs. This has some slightly messy consequences, because `euclid::Box3D` doesn't have the feature I added to `GridAab` where the `.size()` is unsigned, but overall I think it makes some improvement. --- Cargo.lock | 1 + all-is-cubes-base/src/math/grid_aab.rs | 10 + all-is-cubes-base/src/math/octant.rs | 36 +++- all-is-cubes-gpu/Cargo.toml | 1 + all-is-cubes-gpu/src/common/octree_alloc.rs | 203 +++++++++++------- all-is-cubes-gpu/src/in_wgpu/block_texture.rs | 24 ++- all-is-cubes-gpu/src/in_wgpu/glue.rs | 48 ++--- all-is-cubes-gpu/src/in_wgpu/light_texture.rs | 26 ++- all-is-cubes-wasm/Cargo.lock | 1 + fuzz/fuzz_targets/fuzz_octree.rs | 10 +- 10 files changed, 232 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11fd40c76..40731923d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,6 +195,7 @@ dependencies = [ "image 0.25.1", "itertools 0.12.1", "log", + "num-traits", "pollster", "rand", "rand_xoshiro", diff --git a/all-is-cubes-base/src/math/grid_aab.rs b/all-is-cubes-base/src/math/grid_aab.rs index 67d009726..cc85cac14 100644 --- a/all-is-cubes-base/src/math/grid_aab.rs +++ b/all-is-cubes-base/src/math/grid_aab.rs @@ -911,6 +911,16 @@ impl From for Aab { } } +impl From for euclid::Box3D { + #[inline] + fn from(aab: GridAab) -> Self { + Self { + min: aab.lower_bounds(), + max: aab.upper_bounds(), + } + } +} + #[cfg(feature = "arbitrary")] #[mutants::skip] impl<'a> arbitrary::Arbitrary<'a> for GridAab { diff --git a/all-is-cubes-base/src/math/octant.rs b/all-is-cubes-base/src/math/octant.rs index ad63bcdac..860c2f063 100644 --- a/all-is-cubes-base/src/math/octant.rs +++ b/all-is-cubes-base/src/math/octant.rs @@ -1,8 +1,8 @@ use core::{fmt, ops}; -use euclid::Vector3D; +use euclid::{vec3, Vector3D}; -use crate::math::{Cube, Face6, FreeVector, GridCoordinate}; +use crate::math::{Cube, Face6, FreeVector, GridCoordinate, GridPoint}; /// Identifies one of eight octants, or elements of a 2×2×2 cube. /// @@ -80,6 +80,24 @@ impl Octant { } } + /// Given the low corner of an octant in the volume (0..2)³, + /// return which octant it is. + /// + /// That is, each coordinate of the returned [`Vector3D`] is either 0 or 1. + /// This is equivalent to [`Self::try_from_positive_cube()`] but with more flexible input. + /// + /// TODO: better trait bounds would be `Zero + One` + #[inline] + pub fn try_from_01( + corner_or_translation: Vector3D, + ) -> Option { + let low_corner: GridPoint = corner_or_translation + .try_cast::()? + .cast_unit() + .to_point(); + Self::try_from_positive_cube(Cube::from(low_corner)) + } + const fn to_zmaj_index(self) -> u8 { self as u8 } @@ -102,8 +120,18 @@ impl Octant { #[inline] #[must_use] pub fn to_positive_cube(self) -> Cube { - let i = GridCoordinate::from(self.to_zmaj_index()); - Cube::new((i >> 2) & 1, (i >> 1) & 1, i & 1) + Cube::from(self.to_01::().map(GridCoordinate::from).to_point()) + } + + /// Returns this octant of the volume (0..2)³ expressed as a translation vector + /// from the origin. + /// + /// That is, each coordinate of the returned [`Vector3D`] is either 0 or 1. + #[inline] + #[must_use] + pub fn to_01(self) -> Vector3D { + let i = self.to_zmaj_index(); + vec3((i >> 2) & 1, (i >> 1) & 1, i & 1) } /// For each component of `vector`, negate it if `self` is on the negative side of that axis. diff --git a/all-is-cubes-gpu/Cargo.toml b/all-is-cubes-gpu/Cargo.toml index e12151a10..dc470e45b 100644 --- a/all-is-cubes-gpu/Cargo.toml +++ b/all-is-cubes-gpu/Cargo.toml @@ -67,6 +67,7 @@ futures-util = { workspace = true, features = [ half = { workspace = true, features = ["bytemuck"] } itertools = { workspace = true } log = { workspace = true } +num-traits = { workspace = true } # Used to implement ensure_polled on non-Wasm targets, and in the `rerun` support. pollster = { workspace = true } rand = { workspace = true } diff --git a/all-is-cubes-gpu/src/common/octree_alloc.rs b/all-is-cubes-gpu/src/common/octree_alloc.rs index e5cf05946..db95ba138 100644 --- a/all-is-cubes-gpu/src/common/octree_alloc.rs +++ b/all-is-cubes-gpu/src/common/octree_alloc.rs @@ -1,22 +1,45 @@ -use all_is_cubes::euclid::default::Translation3D; +use core::fmt; +use core::marker::PhantomData; + +use all_is_cubes::euclid::{Box3D, Point3D, Size3D, Translation3D}; use all_is_cubes::math::{ - self, Cube, GridAab, GridCoordinate, GridPoint, GridSizeCoord, Octant, OctantMap, + self, Cube, GridAab, GridCoordinate, GridSizeCoord, Octant, OctantMap, VectorOps as _, }; +type TreeCoord = u16; + /// An octree that knows how to allocate box regions of itself. It stores no other data. -#[derive(Clone, Debug)] -pub struct Alloctree { +/// +/// The maximum side length of the octree is `2.pow(Alloctree::MAX_SIZE_EXPONENT)`, +/// which is chosen so that the volume is not greater than `u32::MAX` and thus cannot overflow +/// `usize` on any but 16-bit platforms (which are not supported). +/// This also means that the side length is less than [`u16::MAX`], and so `u16` coordinates are +/// used, for compactness and to allow infallible conversion to both `u32` and `i32`. +/// +/// `A` is the coordinate system marker type for the coordinates within the volume that +/// the allocator manages (e.g. the bounds of a texture). +/// +/// TODO: Add coordinate system type for the input, to reflect that it's also some-kind-of-texels +/// that aren't necessarily cubes-for-blocks. If we can manage that without too much regression +/// in necessary i32/u32 conversions. +/// +/// Note: this struct is public (but hidden) for the `fuzz_octree` test. +pub struct Alloctree { /// log2 of the size of the region available to allocate. Lower bounds are always zero size_exponent: u8, + root: AlloctreeNode, + /// Occupied units, strictly in terms of request volume. /// TODO: Change this to account for known fragmentation that can't be allocated. occupied_volume: usize, + + _phantom: PhantomData A>, } -impl Alloctree { - /// Temporary node for swapping - const PLACEHOLDER: Alloctree = Self::new(0); +impl Alloctree { + /// Temporary instance for swapping + const PLACEHOLDER: Alloctree = Self::new(0); /// Largest allowed size of [`Alloctree`]. /// @@ -36,6 +59,7 @@ impl Alloctree { size_exponent, root: AlloctreeNode::Empty, occupied_volume: 0, + _phantom: PhantomData, } } @@ -44,14 +68,14 @@ impl Alloctree { /// The returned handle **does not deallocate on drop**, because this tree does not /// implement interior mutability; it is the caller's responsibility to provide such /// functionality if needed. - pub fn allocate(&mut self, request: GridAab) -> Option { + pub fn allocate(&mut self, request: GridAab) -> Option> { if !fits(request, self.size_exponent) { // Too big, can never fit. return None; } let handle = self .root - .allocate(self.size_exponent, GridPoint::origin(), request)?; + .allocate::(self.size_exponent, Point3D::origin(), request)?; self.occupied_volume += request.volume().unwrap(); Some(handle) } @@ -59,7 +83,7 @@ impl Alloctree { /// Allocates a region of the given size, growing the overall bounds if needed. /// /// Returns `None` if the tree cannot grow further. - pub fn allocate_with_growth(&mut self, request: GridAab) -> Option { + pub fn allocate_with_growth(&mut self, request: GridAab) -> Option> { if !fits(request, Self::MAX_SIZE_EXPONENT) { // Too big, can never fit even with growth. return None; @@ -80,7 +104,7 @@ impl Alloctree { .max(requested_size_exponent) .checked_add(1)?; - if new_size_exponent <= Alloctree::MAX_SIZE_EXPONENT { + if new_size_exponent <= Self::MAX_SIZE_EXPONENT { // Grow the allocatable region and try again. self.grow_to(new_size_exponent); @@ -109,10 +133,9 @@ impl Alloctree { /// If the handle does not exactly match a previous allocation from this allocator, /// may panic or deallocate something else. #[allow(clippy::needless_pass_by_value)] // deliberately taking handle ownership - pub fn free(&mut self, handle: AlloctreeHandle) { - self.root - .free(self.size_exponent, handle.allocation.lower_bounds()); - self.occupied_volume -= handle.allocation.volume().unwrap(); + pub fn free(&mut self, handle: AlloctreeHandle) { + self.root.free(self.size_exponent, handle.allocation.min); + self.occupied_volume -= handle.allocation.map(usize::from).volume(); } /// Enlarge the bounds to be as if this tree had been allocated with @@ -131,20 +154,14 @@ impl Alloctree { size_exponent: old.size_exponent + 1, root: old.root.wrap_in_oct(), occupied_volume: old.occupied_volume, // we're only adding unoccupied volume + _phantom: PhantomData, }; } } - pub fn size_exponent(&self) -> u8 { - self.size_exponent - } - /// Returns the region that could be allocated within. - pub fn bounds(&self) -> GridAab { - GridAab::from_lower_size( - [0, 0, 0], - math::GridSize::splat(expsize(self.size_exponent)), - ) + pub fn bounds(&self) -> Box3D { + Box3D::from_size(Size3D::splat(expsize(self.size_exponent))) } pub fn occupied_volume(&self) -> usize { @@ -152,6 +169,29 @@ impl Alloctree { } } +// Manual implementation to avoid trait bounds. +impl Clone for Alloctree { + fn clone(&self) -> Self { + Self { + size_exponent: self.size_exponent, + root: self.root.clone(), + occupied_volume: self.occupied_volume, + _phantom: PhantomData, + } + } +} + +// Manual implementation to avoid trait bounds. +impl fmt::Debug for Alloctree { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Alloctree") + .field("size_exponent", &self.size_exponent) + .field("occupied_volume", &self.occupied_volume) + .field("root", &self.root) + .finish() + } +} + /// Tree node making up an [`Alloctree`]. /// /// The nodes do not know their size or position; this is tracked by the traversal @@ -177,12 +217,12 @@ impl AlloctreeNode { AlloctreeNode::Oct(oct) } - fn allocate( + fn allocate( &mut self, size_exponent: u8, - low_corner: GridPoint, + low_corner: Point3D, request: GridAab, - ) -> Option { + ) -> Option> { // eprintln!( // "allocate(2^{} = {}, {:?})", // size_exponent, @@ -214,14 +254,18 @@ impl AlloctreeNode { // It's possible for the offset calculation to overflow if the request // bounds are near GridCoordinate::MIN. - let offset = Translation3D::new( + let low_corner = low_corner.map(GridCoordinate::from); + let offset = Translation3D::::new( low_corner.x.checked_sub(request.lower_bounds().x)?, low_corner.y.checked_sub(request.lower_bounds().y)?, low_corner.z.checked_sub(request.lower_bounds().z)?, ); *self = AlloctreeNode::Full; Some(AlloctreeHandle { - allocation: request.translate(offset.to_vector().cast_unit()), + allocation: offset + .transform_box3d(&Box3D::from(request)) + .try_cast() + .expect("can't happen: computing translation overflowed"), offset, }) } @@ -234,13 +278,12 @@ impl AlloctreeNode { // The tree is subdivided into parts too small to use. return None; } - let child_size = expisize(size_exponent - 1); + let child_size = expsize(size_exponent - 1); children.iter_mut().find_map(|(octant, child)| { child.allocate( size_exponent - 1, - low_corner - + octant.to_positive_cube().lower_bounds().to_vector() * child_size, + low_corner + octant.to_01().map(TreeCoord::from) * child_size, request, ) }) @@ -251,7 +294,7 @@ impl AlloctreeNode { /// `size_exponent` is the size of this node. /// `relative_low_corner` is the low corner of the allocation to be freed, /// *relative to the low corner of this node*. - fn free(&mut self, size_exponent: u8, relative_low_corner: GridPoint) { + fn free(&mut self, size_exponent: u8, relative_low_corner: Point3D) { match self { AlloctreeNode::Empty => panic!("Alloctree::free: node is empty"), AlloctreeNode::Full => { @@ -259,15 +302,16 @@ impl AlloctreeNode { } AlloctreeNode::Oct(children) => { debug_assert!(size_exponent > 0, "tree is deeper than size"); - let child_size = expisize(size_exponent - 1); - let octant = Octant::try_from_positive_cube(Cube::from( - relative_low_corner.map(|c| c.div_euclid(child_size)), - )) + let child_size = expsize(size_exponent - 1); + let octant = Octant::try_from_01( + relative_low_corner + .map(|c| c.div_euclid(child_size)) + .to_vector(), + ) .expect("Alloctree::free: out of bounds"); children[octant].free( size_exponent - 1, - relative_low_corner - - octant.to_positive_cube().lower_bounds().to_vector() * child_size, + relative_low_corner - octant.to_01().map(TreeCoord::from) * child_size, ); } } @@ -280,14 +324,30 @@ impl AlloctreeNode { /// mutability; it is the caller's responsibility to provide such functionality if needed. // // TODO(euclid migration): don't use Grid* units since these are not space cubes -#[derive(Debug, Eq, PartialEq)] #[non_exhaustive] -pub struct AlloctreeHandle { +pub struct AlloctreeHandle { /// Allocated region — this is the region to write into. - pub allocation: GridAab, + pub allocation: Box3D, /// Coordinate translation from the originally requested [`GridAab`] to the location /// allocated for it. - pub offset: Translation3D, + pub offset: Translation3D, +} + +impl Eq for AlloctreeHandle {} +impl PartialEq for AlloctreeHandle { + fn eq(&self, other: &Self) -> bool { + let &Self { allocation, offset } = self; + allocation == other.allocation && offset == other.offset + } +} + +impl fmt::Debug for AlloctreeHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("AlloctreeHandle") + .field("allocation", &self.allocation) + .field("offset", &self.offset) + .finish() + } } /// Test if the given [`GridAab`] fits in a cube of the given size. @@ -295,8 +355,12 @@ fn fits(request: GridAab, size_exponent: u8) -> bool { max_edge_length(request.size()) <= expsize(size_exponent) } -fn max_edge_length(size: math::GridSize) -> GridSizeCoord { - size.width.max(size.height).max(size.depth) +/// Find the largest the given size is on any axis. +/// If the answer overflows, it is clamped to [`TreeCoord::MAX`], which will always fail to fit. +fn max_edge_length(size: math::GridSize) -> TreeCoord { + let size = size.width.max(size.height).max(size.depth); + + size.clamp(0, const { TreeCoord::MAX as GridSizeCoord }) as TreeCoord } fn max_edge_length_exponent(size: math::GridSize) -> u8 { @@ -309,7 +373,7 @@ fn max_edge_length_exponent(size: math::GridSize) -> u8 { // Compute exponent. // unwrap cannot fail since the maximum possible value is ilog2(i32::MAX) + 1, // which is 32, which is < u8::MAX. - let mut exp = max_edge_length.ilog2().try_into().unwrap(); + let mut exp: u8 = max_edge_length.ilog2().try_into().unwrap(); if max_edge_length > expsize(exp) { // Round up instead of down exp += 1; @@ -323,24 +387,15 @@ fn max_edge_length_exponent(size: math::GridSize) -> u8 { } /// Bigger than the maximum allowed exponent, but smaller than would overflow. -const CLAMP_EXPONENT: u32 = Alloctree::MAX_SIZE_EXPONENT as u32 + 1; +const CLAMP_EXPONENT: u32 = Alloctree::<()>::MAX_SIZE_EXPONENT as u32 + 1; /// Convert `size_exponent` to actual size. /// /// Exponents greater than [`Alloctree::MAX_SIZE_EXPONENT`] are clamped -/// to an arbitrary larger value. -fn expsize(size_exponent: u8) -> GridSizeCoord { - // Using pow() instead of bit shift because it isn't defined to overflow to zero - 2u32.pow(u32::from(size_exponent).min(CLAMP_EXPONENT)) -} - -/// Convert `size_exponent` to `GridCoordinate`. -/// -/// Exponents greater than [`Alloctree::MAX_SIZE_EXPONENT`] are clamped -/// to an arbitrary larger value. -fn expisize(size_exponent: u8) -> GridCoordinate { +/// to an arbitrary larger value that will always fail to fit the tree. +fn expsize(size_exponent: u8) -> TreeCoord { // Using pow() instead of bit shift because it isn't defined to overflow to zero - 2i32.pow(u32::from(size_exponent).min(CLAMP_EXPONENT)) + 2u16.pow(u32::from(size_exponent).min(CLAMP_EXPONENT)) } #[cfg(test)] @@ -350,27 +405,25 @@ mod tests { #[track_caller] fn check_no_overlaps( - t: &mut Alloctree, + t: &mut Alloctree<()>, requests: impl IntoIterator, - ) -> Vec { - let mut handles: Vec = Vec::new(); + ) -> Vec> { + let mut handles: Vec> = Vec::new(); for request in requests { let Some(handle) = t.allocate(request) else { panic!("check_no_overlaps: allocation failure for {request:?}") }; assert_eq!( - request.size(), - handle.allocation.size(), + request.size().cast_unit::<()>(), + handle.allocation.size().to_u32(), "mismatch of requested {:?} and granted {:?}", request, handle.allocation ); for existing in &handles { - if let Some(intersection) = - handle.allocation.intersection_cubes(existing.allocation) - { + if let Some(intersection) = handle.allocation.intersection(&existing.allocation) { assert!( - intersection.volume() == Some(0), + intersection.volume() == 0, "intersection between\n{:?} and {:?}\n", existing.allocation, handle.allocation @@ -385,7 +438,7 @@ mod tests { #[test] fn basic_complete_fill() { let mut t = Alloctree::new(5); // side length 2^5 cube = eight side length 16 cubes - let _allocations: Vec = (0..8) + let _allocations: Vec> = (0..8) .map(|i| match t.allocate(GridAab::for_block(R16)) { Some(val) => val, None => panic!("basic_complete_fill allocation failure for #{i}"), @@ -398,7 +451,7 @@ mod tests { #[test] fn free_and_allocate_again() { let mut t = Alloctree::new(6); // side length 2^6 cube = 64 side length 16 cubes - let mut allocations: Vec> = (0..64) + let mut allocations: Vec>> = (0..64) .map(|i| match t.allocate(GridAab::for_block(R16)) { Some(val) => Some(val), None => panic!("free_and_allocate_again initial allocation failure for #{i}"), @@ -413,7 +466,7 @@ mod tests { #[test] fn no_overlap() { - let mut t = Alloctree::new(5); + let mut t = Alloctree::<()>::new(5); check_no_overlaps( &mut t, [ @@ -426,8 +479,8 @@ mod tests { #[test] fn growth() { - let mut t = Alloctree::new(3); - assert_eq!(t.bounds(), GridAab::for_block(R8)); + let mut t = Alloctree::::new(3); + assert_eq!(t.bounds().map(i32::from), GridAab::for_block(R8).into()); let _initial_allocation = t.allocate(GridAab::for_block(R8)).unwrap(); // Allocation without growth fails @@ -436,13 +489,13 @@ mod tests { // Allocation with growth succeeds t.allocate_with_growth(GridAab::ORIGIN_CUBE) .expect("second allocation should succeed"); - assert_eq!(t.bounds(), GridAab::for_block(R16)); + assert_eq!(t.bounds().map(i32::from), GridAab::for_block(R16).into()); } #[test] fn expsize_edge_cases() { assert_eq!( - Alloctree::MAX_SIZE_EXPONENT, + Alloctree::<()>::MAX_SIZE_EXPONENT, 10, "this test is hardcoded around 10" ); diff --git a/all-is-cubes-gpu/src/in_wgpu/block_texture.rs b/all-is-cubes-gpu/src/in_wgpu/block_texture.rs index acdd22573..089fe07b5 100644 --- a/all-is-cubes-gpu/src/in_wgpu/block_texture.rs +++ b/all-is-cubes-gpu/src/in_wgpu/block_texture.rs @@ -7,8 +7,8 @@ use std::sync::{Arc, Mutex, MutexGuard, Weak}; use all_is_cubes::block::Evoxel; use all_is_cubes::content::palette; -use all_is_cubes::euclid::Translation3D; -use all_is_cubes::math::{GridAab, GridCoordinate, Vol}; +use all_is_cubes::euclid::{Box3D, Translation3D}; +use all_is_cubes::math::{Cube, GridAab, GridCoordinate, VectorOps as _, Vol}; use all_is_cubes::time; use all_is_cubes_mesh::texture::{self, Channels}; @@ -65,7 +65,7 @@ pub(crate) struct BlockTextureViews { #[derive(Debug)] struct WeakTile { /// Bounds of the allocation in the atlas. - allocated_bounds: GridAab, + allocated_bounds: Box3D, backing: Weak>, } @@ -83,7 +83,7 @@ struct TileBacking { /// Allocator information, and the region of the atlas texture which this tile owns. /// /// Property: `self.handle.unwrap().allocation.volume() == self.data.len()`. - handle: Option, + handle: Option>, /// sRGB reflectance data (that might not be sent to the GPU yet, or reused upon resize). /// Is `Some` if `write()` has been called. @@ -106,7 +106,7 @@ struct TileBacking { #[derive(Debug)] struct AllocatorBacking { /// Tracks which regions of the texture are free or allocated. - alloctree: Alloctree, + alloctree: Alloctree, /// Whether flush needs to do anything. dirty: bool, @@ -202,7 +202,8 @@ impl texture::Allocator for AtlasAllocator { let result = AtlasTile { requested_bounds, channels, - offset: Translation3D::from_untyped(&handle.offset), + // TODO: generalize Alloctree so it doesn't use Cube here and the units match automatically + offset: Translation3D::<_, texture::TexelUnit, Cube>::identity() + handle.offset, backing: Arc::new(Mutex::new(TileBacking { handle: Some(handle), reflectance: None, @@ -384,11 +385,12 @@ impl AllocatorBacking { { let backing: &mut TileBacking = &mut strong_ref.lock().unwrap(); if backing.dirty || copy_everything_anyway { - let region: GridAab = backing + let region: Box3D = backing .handle .as_ref() .expect("can't happen: dead TileBacking") - .allocation; + .allocation + .map(u32::from); if let Some(data) = backing.reflectance.as_ref() { write_texture_by_aab( @@ -427,12 +429,12 @@ impl AllocatorBacking { // TODO: This is inefficient but we want to keep it at least until fixing // , at which point we // might reasonably disable it. - let region = weak_tile.allocated_bounds; + let region = weak_tile.allocated_bounds.map(u32::from); // TODO: keep a preallocated GPU buffer instead let data = vec![ palette::UNALLOCATED_TEXELS_ERROR.to_srgb8(); - region.volume().unwrap() + region.volume() as usize ]; write_texture_by_aab(queue, &textures.reflectance.texture, region, &data); @@ -458,7 +460,7 @@ impl AllocatorBacking { flush_time: time::Duration::ZERO, in_use_tiles: backing.in_use.len(), in_use_texels: backing.alloctree.occupied_volume(), - capacity_texels: backing.alloctree.bounds().volume().unwrap(), + capacity_texels: backing.alloctree.bounds().to_usize().volume(), }, ); diff --git a/all-is-cubes-gpu/src/in_wgpu/glue.rs b/all-is-cubes-gpu/src/in_wgpu/glue.rs index 47afe1c6b..af5ed4113 100644 --- a/all-is-cubes-gpu/src/in_wgpu/glue.rs +++ b/all-is-cubes-gpu/src/in_wgpu/glue.rs @@ -1,14 +1,14 @@ //! Miscellaneous conversion functions and trait impls for [`wgpu`]. -use core::num::TryFromIntError; use core::ops::Range; use bytemuck::Pod; use wgpu::util::DeviceExt as _; -use all_is_cubes::euclid::Point3D; -use all_is_cubes::math::{GridAab, GridCoordinate, GridSize, Rgba}; +use all_is_cubes::euclid::{Box3D, Point3D, Size3D}; +use all_is_cubes::math::{GridSize, Rgba}; use all_is_cubes_mesh::IndexSlice; +use num_traits::NumCast; pub fn to_wgpu_color(color: Rgba) -> wgpu::Color { // TODO: Check whether this is gamma-correct @@ -33,56 +33,56 @@ pub fn to_wgpu_index_range(range: Range) -> Range { range.start.try_into().unwrap()..range.end.try_into().unwrap() } -/// Write to a texture, with the region written specified by a [`GridAab`]. +/// Write to the specified region of a 3D texture. /// /// `T` must be a single texel of the appropriate format. /// -/// Panics if `region` has any negative coordinates. -pub fn write_texture_by_aab( +/// Panics if `region`’s volume does not match the data length. +pub fn write_texture_by_aab( queue: &wgpu::Queue, texture: &wgpu::Texture, - region: GridAab, + region: Box3D, data: &[T], ) { - let volume = region.volume().unwrap(); + let volume = usize::try_from(region.volume()).unwrap(); let len = data.len(); assert!( volume == len, "volume {volume} of texture region {region:?} does not match supplied data length {len}", ); + let size = region.size(); + queue.write_texture( wgpu::ImageCopyTexture { texture, mip_level: 0, - origin: point_to_origin(region.lower_bounds()), + origin: point_to_origin(region.min), aspect: wgpu::TextureAspect::All, }, bytemuck::cast_slice::(data), wgpu::ImageDataLayout { offset: 0, - bytes_per_row: Some(size_of::() as u32 * region.size().width), - rows_per_image: Some(region.size().height), + bytes_per_row: Some(size_of::() as u32 * size.width), + rows_per_image: Some(size.height), }, - size3d_to_extent(region.size()), + size3d_to_extent(size), ) } -/// Convert point to [`wgpu::Origin3d`]. Panics if the input is negative. +/// Convert point to [`wgpu::Origin3d`]. #[inline(never)] -pub fn point_to_origin(origin: Point3D) -> wgpu::Origin3d { - (|| -> Result<_, TryFromIntError> { - Ok(wgpu::Origin3d { - x: origin.x.try_into()?, - y: origin.y.try_into()?, - z: origin.z.try_into()?, - }) - })() - .expect("negative origin") +pub fn point_to_origin(origin: Point3D) -> wgpu::Origin3d { + wgpu::Origin3d { + x: origin.x, + y: origin.y, + z: origin.z, + } } -/// Convert [`GridSize`] to [`wgpu::Extent3d`]. -pub fn size3d_to_extent(size: GridSize) -> wgpu::Extent3d { +/// Convert [`GridSize`] or similar size types to [`wgpu::Extent3d`]. +pub fn size3d_to_extent(size: Size3D) -> wgpu::Extent3d { + let size = size.to_u32(); wgpu::Extent3d { width: size.width, height: size.height, diff --git a/all-is-cubes-gpu/src/in_wgpu/light_texture.rs b/all-is-cubes-gpu/src/in_wgpu/light_texture.rs index df45794bc..05fccddc1 100644 --- a/all-is-cubes-gpu/src/in_wgpu/light_texture.rs +++ b/all-is-cubes-gpu/src/in_wgpu/light_texture.rs @@ -5,7 +5,7 @@ use rayon::{ slice::ParallelSliceMut as _, }; -use all_is_cubes::euclid::{size3, Vector3D}; +use all_is_cubes::euclid::{Box3D, Vector3D}; use all_is_cubes::math::{ Aab, Axis, Cube, FaceMap, FreeCoordinate, GridAab, GridCoordinate, GridSize, GridSizeCoord, }; @@ -296,18 +296,20 @@ impl LightTexture { } } - let ts = extent_to_size3d(self.texture.size()); + let region = Box3D::from(region); + let texture_size = extent_to_size3d(self.texture.size()); write_texture_by_aab( queue, &self.texture, - GridAab::from_lower_size( + Box3D::from_origin_and_size( region - .lower_bounds() - .zip(ts.to_vector().to_point().to_i32(), |coord, size| { - coord.rem_euclid(size) + .min + .zip(texture_size.to_vector().to_point().cast(), |coord, size| { + // after rem_euclid it is guaranteed to be nonnegative + coord.rem_euclid(size) as u32 }) .to_point(), - region.size(), + region.size().cast(), ), buffer, ); @@ -362,10 +364,16 @@ impl LightTexture { wgpu::ImageCopyTexture { texture: &self.texture, mip_level: 0, - origin: point_to_origin(cube.lower_bounds().rem_euclid(&texture_size)), + origin: point_to_origin( + cube.lower_bounds().rem_euclid(&texture_size).to_u32(), + ), aspect: wgpu::TextureAspect::All, }, - size3d_to_extent(size3(1, 1, 1)), + wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, ); batch_count += 1; diff --git a/all-is-cubes-wasm/Cargo.lock b/all-is-cubes-wasm/Cargo.lock index 6f2a3f059..315b6e6ce 100644 --- a/all-is-cubes-wasm/Cargo.lock +++ b/all-is-cubes-wasm/Cargo.lock @@ -114,6 +114,7 @@ dependencies = [ "half", "itertools 0.12.1", "log", + "num-traits", "pollster", "rand", "rand_xoshiro", diff --git a/fuzz/fuzz_targets/fuzz_octree.rs b/fuzz/fuzz_targets/fuzz_octree.rs index 2034b3748..b667da4c9 100644 --- a/fuzz/fuzz_targets/fuzz_octree.rs +++ b/fuzz/fuzz_targets/fuzz_octree.rs @@ -48,19 +48,19 @@ fuzz_target!(|input: FuzzOctree| { } }); -fn validate(tree: &Alloctree, handles: &[AlloctreeHandle]) { +fn validate(tree: &Alloctree<()>, handles: &[AlloctreeHandle<()>]) { for (i, h1) in handles.iter().enumerate() { assert!( - tree.bounds().contains_box(h1.allocation), + tree.bounds().contains_box(&h1.allocation), "allocation was out of bounds" ); for (j, h2) in handles.iter().enumerate() { if i == j { continue; } - if let Some(intersection) = h1.allocation.intersection_cubes(h2.allocation) { + if let Some(intersection) = h1.allocation.intersection(&h2.allocation) { assert!( - intersection.volume() == Some(0), + intersection.volume() == 0, "intersection between\n{:?} and {:?}\n", h1.allocation, h2.allocation @@ -71,5 +71,5 @@ fn validate(tree: &Alloctree, handles: &[AlloctreeHandle]) { } fn clean_exponent(input: u8) -> u8 { - input.rem_euclid(Alloctree::MAX_SIZE_EXPONENT + 1) + input.rem_euclid(Alloctree::<()>::MAX_SIZE_EXPONENT + 1) }