diff --git a/README.md b/README.md index f537624..3f8364d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ If you want to use the original C++ implementation from Rust, check out the [mes ## Features -`meshoptimizer` v0.16 feature level is the current support target. Planned but currently missing features: +`meshoptimizer` v0.17 feature level is the current support target. Planned but currently missing features: - [SIMD support](https://github.com/yzsolt/meshopt-rs/issues/1) - [WASM support](https://github.com/yzsolt/meshopt-rs/issues/2) diff --git a/benches/with_input.rs b/benches/with_input.rs index 613500f..efff25c 100644 --- a/benches/with_input.rs +++ b/benches/with_input.rs @@ -1,17 +1,14 @@ use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main}; use meshopt_rs::Stream; -#[cfg(feature = "experimental")] use meshopt_rs::cluster::{Meshlet, build_meshlets, build_meshlets_bound, compute_meshlet_bounds}; use meshopt_rs::index::IndexEncodingVersion; use meshopt_rs::index::buffer::{decode_index_buffer, encode_index_buffer, encode_index_buffer_bound}; use meshopt_rs::index::generator::{ generate_shadow_index_buffer, generate_vertex_remap, remap_index_buffer, remap_vertex_buffer, }; -#[cfg(feature = "experimental")] use meshopt_rs::index::sequence::{decode_index_sequence, encode_index_sequence, encode_index_sequence_bound}; use meshopt_rs::overdraw::optimize_overdraw; -#[cfg(feature = "experimental")] use meshopt_rs::stripify::{stripify, stripify_bound}; use meshopt_rs::vertex::Position; use meshopt_rs::vertex::cache::{optimize_vertex_cache, optimize_vertex_cache_fifo, optimize_vertex_cache_strip}; @@ -180,9 +177,6 @@ fn with_input(c: &mut Criterion) { let copystrip_vertices = copystrip.vertices.clone(); optimize_vertex_fetch(&mut copystrip.vertices, &mut copystrip.indices, ©strip_vertices); - // stripify - - #[cfg(feature = "experimental")] c.bench_with_input(BenchmarkId::new("build_meshlets", input_name), ©, |b, mesh| { const MAX_VERTICES: usize = 64; const MAX_TRIANGLES: usize = 124; @@ -207,7 +201,6 @@ fn with_input(c: &mut Criterion) { }); }); - #[cfg(feature = "experimental")] c.bench_with_input( BenchmarkId::new("compute_meshlet_bounds", input_name), ©, @@ -286,7 +279,6 @@ fn with_input(c: &mut Criterion) { } group.finish(); - #[cfg(feature = "experimental")] { let mut group = c.benchmark_group("index-sequence-encoding"); diff --git a/bin/codec_bench.rs b/bin/codec_bench.rs index cb391b7..6d1dbfc 100644 --- a/bin/codec_bench.rs +++ b/bin/codec_bench.rs @@ -145,19 +145,19 @@ fn bench_filters( for _ in 0..10 { let t0 = Instant::now(); - decode_filter_oct_8(&mut d4); + decode_filter_oct_8(d4.iter_mut()); let t1 = Instant::now(); - decode_filter_oct_16(&mut d8); + decode_filter_oct_16(d8.iter_mut()); let t2 = Instant::now(); - decode_filter_quat(&mut d8); + decode_filter_quat(d8.iter_mut()); let t3 = Instant::now(); - decode_filter_exp(&mut d32); + decode_filter_exp(d32.iter_mut()); let t4 = Instant::now(); diff --git a/examples/demo.rs b/examples/demo.rs index 838b0de..9a33b14 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -1,7 +1,9 @@ #![allow(clippy::identity_op)] +use meshopt_rs::cluster::*; use meshopt_rs::index::buffer::*; use meshopt_rs::index::generator::*; +use meshopt_rs::index::sequence::*; use meshopt_rs::index::*; use meshopt_rs::overdraw::*; use meshopt_rs::quantize::*; @@ -13,7 +15,7 @@ use meshopt_rs::vertex::*; use meshopt_rs::{INVALID_INDEX, Stream}; #[cfg(feature = "experimental")] -use meshopt_rs::{cluster::*, index::sequence::*, simplify::*, spatial_order::*}; +use meshopt_rs::{simplify::*, spatial_order::*}; use std::env; use std::fmt::Debug; @@ -465,7 +467,6 @@ fn encode_index(mesh: &Mesh, desc: char) { ); } -#[cfg(feature = "experimental")] fn encode_index_sequence1(data: &[u32], vertex_count: usize, desc: char) { // allocate result outside of the timing loop to exclude memset() from decode timing let mut result = vec![0; data.len()]; @@ -885,7 +886,6 @@ fn shadow(mesh: &Mesh) { ); } -#[cfg(feature = "experimental")] fn meshlets(mesh: &Mesh, scan: bool) { const MAX_VERTICES: usize = 64; const MAX_TRIANGLES: usize = 124; // NVidia-recommended 126, rounded down to a multiple of 4 @@ -1222,7 +1222,6 @@ where Ok(()) } -#[cfg(feature = "experimental")] fn tessellation_adjacency(mesh: &Mesh) { let start = Instant::now(); @@ -1277,14 +1276,10 @@ fn process(mesh: &Mesh) { stripify_mesh(©, true, 'R'); stripify_mesh(©strip, true, 'S'); - #[cfg(feature = "experimental")] - { - meshlets(©, false); - meshlets(©, true); - } + meshlets(©, false); + meshlets(©, true); shadow(©); - #[cfg(feature = "experimental")] tessellation_adjacency(©); encode_index(©, ' '); @@ -1294,7 +1289,6 @@ fn process(mesh: &Mesh) { let size = stripify(&mut strip, ©strip.indices, copystrip.vertices.len(), 0); strip.resize(size, 0); - #[cfg(feature = "experimental")] encode_index_sequence1(&strip, copystrip.vertices.len(), 'D'); pack_vertex(©); @@ -1313,8 +1307,7 @@ fn process(mesh: &Mesh) { } } -fn process_dev(#[allow(unused)] mesh: &Mesh) { - #[cfg(feature = "experimental")] +fn process_dev(mesh: &Mesh) { tessellation_adjacency(mesh); } diff --git a/src/cluster.rs b/src/cluster.rs index 924d9f9..939ac47 100644 --- a/src/cluster.rs +++ b/src/cluster.rs @@ -1,4 +1,4 @@ -//! **Experimental** meshlet building and cluster bounds generation +//! Meshlet building and cluster bounds generation use crate::quantize::quantize_snorm; use crate::util::zero_inverse; diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..7e44414 --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,19 @@ +use std::hash::{BuildHasherDefault, Hasher}; + +#[derive(Default)] +pub struct NoopHasher { + state: u32, +} + +impl Hasher for NoopHasher { + fn write(&mut self, bytes: &[u8]) { + debug_assert_eq!(bytes.len(), 4); + self.state = u32::from_ne_bytes(bytes.try_into().unwrap()); + } + + fn finish(&self) -> u64 { + self.state as u64 + } +} + +pub type BuildNoopHasher = BuildHasherDefault; diff --git a/src/index/generator.rs b/src/index/generator.rs index cede2cd..c16ecb0 100644 --- a/src/index/generator.rs +++ b/src/index/generator.rs @@ -3,11 +3,12 @@ // This work is based on: // John McDonald, Mark Kilgard. Crack-Free Point-Normal Triangles using Adjacent Edge Normals. 2010 +use crate::hash::BuildNoopHasher; use crate::{INVALID_INDEX, Stream}; use std::collections::HashMap; use std::collections::hash_map::Entry; -use std::hash::{BuildHasherDefault, Hasher}; +use std::hash::{BuildHasherDefault, Hash, Hasher}; #[derive(Default)] struct VertexHasher { @@ -22,8 +23,8 @@ impl Hasher for VertexHasher { let mut h = self.state; - for k4 in bytes.chunks_exact(4) { - let mut k = u32::from_ne_bytes(k4.try_into().unwrap()); + for k4 in bytes.as_chunks().0 { + let mut k = u32::from_ne_bytes(*k4); k = k.wrapping_mul(M); k ^= k >> R; @@ -43,53 +44,29 @@ impl Hasher for VertexHasher { type BuildVertexHasher = BuildHasherDefault; -#[cfg(feature = "experimental")] -mod experimental { - use super::*; - use std::hash::Hash; - - #[derive(PartialEq, Eq)] - pub struct Edge(pub (u32, u32)); - - impl Hash for Edge { - fn hash(&self, state: &mut H) { - const M: u32 = 0x5bd1e995; - - let edge = self.0; - let mut h1 = edge.0; - let mut h2 = edge.1; - - // MurmurHash64B finalizer - h1 ^= h2 >> 18; - h1 = h1.wrapping_mul(M); - h2 ^= h1 >> 22; - h2 = h2.wrapping_mul(M); - h1 ^= h2 >> 17; - h1 = h1.wrapping_mul(M); - h2 ^= h1 >> 19; - h2 = h2.wrapping_mul(M); - - state.write_u32(h2); - } - } +#[derive(PartialEq, Eq)] +pub struct Edge(pub (u32, u32)); - #[derive(Default)] - pub struct NoopEdgeHasher { - state: u32, - } - - impl Hasher for NoopEdgeHasher { - fn write(&mut self, bytes: &[u8]) { - debug_assert_eq!(bytes.len(), 4); - self.state = u32::from_ne_bytes(bytes.try_into().unwrap()); - } +impl Hash for Edge { + fn hash(&self, state: &mut H) { + const M: u32 = 0x5bd1e995; - fn finish(&self) -> u64 { - self.state as u64 - } + let edge = self.0; + let mut h1 = edge.0; + let mut h2 = edge.1; + + // MurmurHash64B finalizer + h1 ^= h2 >> 18; + h1 = h1.wrapping_mul(M); + h2 ^= h1 >> 22; + h2 = h2.wrapping_mul(M); + h1 ^= h2 >> 17; + h1 = h1.wrapping_mul(M); + h2 ^= h1 >> 19; + h2 = h2.wrapping_mul(M); + + state.write_u32(h2); } - - pub type BuildNoopEdgeHasher = BuildHasherDefault; } fn generate_vertex_remap_inner( @@ -298,7 +275,6 @@ pub fn generate_shadow_index_buffer_multi(destination: &mut [u32], indices: &[u3 }) } -#[cfg(feature = "experimental")] fn build_position_remap(indices: &[u32], vertices: &Stream) -> Vec { let mut table = HashMap::with_capacity_and_hasher(vertices.len(), BuildVertexHasher::default()); let mut remap = vec![INVALID_INDEX; vertices.len()]; @@ -331,10 +307,7 @@ fn build_position_remap(indices: &[u32], vertices: &Stream) -> Vec { /// # Arguments /// /// * `destination`: must contain enough space for the resulting index buffer (`indices.len() * 4` elements) -#[cfg(feature = "experimental")] pub fn generate_tessellation_index_buffer(destination: &mut [u32], indices: &[u32], vertices: &Stream) { - use experimental::*; - assert_eq!(indices.len() % 3, 0); assert!(destination.len() >= indices.len() * 4); @@ -344,7 +317,7 @@ pub fn generate_tessellation_index_buffer(destination: &mut [u32], indices: &[u3 let remap = build_position_remap(indices, vertices); // build edge set; this stores all triangle edges but we can look these up by any other wedge - let mut edge_table = HashMap::with_capacity_and_hasher(indices.len(), BuildNoopEdgeHasher::default()); + let mut edge_table = HashMap::with_capacity_and_hasher(indices.len(), BuildNoopHasher::default()); for i in indices.chunks_exact(3) { for e in 0..3 { @@ -400,10 +373,7 @@ pub fn generate_tessellation_index_buffer(destination: &mut [u32], indices: &[u3 /// # Arguments /// /// * `destination`: must contain enough space for the resulting index buffer (`indices.len() * 2` elements) -#[cfg(feature = "experimental")] pub fn generate_adjacency_index_buffer(destination: &mut [u32], indices: &[u32], vertices: &Stream) { - use experimental::*; - assert_eq!(indices.len() % 3, 0); assert!(destination.len() >= indices.len() * 2); @@ -413,7 +383,7 @@ pub fn generate_adjacency_index_buffer(destination: &mut [u32], indices: &[u32], let remap = build_position_remap(indices, vertices); // build edge set; this stores all triangle edges but we can look these up by any other wedge - let mut edge_vertex_table = HashMap::with_capacity_and_hasher(indices.len(), BuildNoopEdgeHasher::default()); + let mut edge_vertex_table = HashMap::with_capacity_and_hasher(indices.len(), BuildNoopHasher::default()); for i in indices.chunks_exact(3) { for e in 0..3 { @@ -450,7 +420,7 @@ pub fn generate_adjacency_index_buffer(destination: &mut [u32], indices: &[u32], } } -#[cfg(all(test, feature = "experimental"))] +#[cfg(test)] mod test { use super::*; diff --git a/src/index/mod.rs b/src/index/mod.rs index 7eec840..478b434 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -1,6 +1,5 @@ pub mod buffer; pub mod generator; -#[cfg(feature = "experimental")] pub mod sequence; use crate::util::{read_byte, write_byte}; @@ -15,33 +14,19 @@ pub enum DecodeError { UnexpectedEof, } +#[derive(Default)] pub enum IndexEncodingVersion { /// Decodable by all versions V0, /// Decodable by 0.14+ - #[cfg(feature = "experimental")] + #[default] V1, } -#[allow(clippy::derivable_impls)] -impl Default for IndexEncodingVersion { - fn default() -> Self { - #[cfg(not(feature = "experimental"))] - { - Self::V0 - } - #[cfg(feature = "experimental")] - { - Self::V1 - } - } -} - impl From for u8 { fn from(value: IndexEncodingVersion) -> u8 { match value { IndexEncodingVersion::V0 => 0, - #[cfg(feature = "experimental")] IndexEncodingVersion::V1 => 1, } } diff --git a/src/index/sequence.rs b/src/index/sequence.rs index c7a11d7..83fd6cf 100644 --- a/src/index/sequence.rs +++ b/src/index/sequence.rs @@ -1,4 +1,4 @@ -//! **Experimental** index sequence encoding and decoding +//! Index sequence encoding and decoding use std::io::Write; diff --git a/src/lib.rs b/src/lib.rs index d553441..e384b53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,8 @@ #![allow(clippy::identity_op)] #![allow(clippy::erasing_op)] -#[cfg(feature = "experimental")] pub mod cluster; +mod hash; pub mod index; pub mod overdraw; pub mod quantize; diff --git a/src/quantize.rs b/src/quantize.rs index 8b518b9..c2504e7 100644 --- a/src/quantize.rs +++ b/src/quantize.rs @@ -17,7 +17,7 @@ pub fn quantize_unorm(mut v: f32, n: i32) -> i32 { /// Assumes reconstruction function `q / (2^(n-1)-1)`, which is the case for fixed-function normalized fixed point conversion (except early OpenGL versions). /// /// Maximum reconstruction error: `1/2^n` -pub fn quantize_snorm(mut v: f32, n: i32) -> i32 { +pub fn quantize_snorm(mut v: f32, n: u32) -> i32 { let scale = ((1 << (n - 1)) - 1) as f32; let round = if v >= 0.0 { 0.5 } else { -0.5 }; diff --git a/src/simplify.rs b/src/simplify.rs index ed51aa6..bebda83 100644 --- a/src/simplify.rs +++ b/src/simplify.rs @@ -2,6 +2,8 @@ use crate::INVALID_INDEX; use crate::Vector3; +use crate::hash::BuildNoopHasher; +use crate::simplify::hash::VertexId; use crate::util::zero_inverse; use crate::vertex::{Position, calc_pos_extents}; @@ -88,13 +90,13 @@ fn update_edge_adjacency(adjacency: &mut EdgeAdjacency, indices: &[u32], remap: } mod hash { - use std::hash::{BuildHasherDefault, Hash, Hasher}; + use std::hash::{Hash, Hasher}; #[derive(Clone)] pub struct VertexPosition(pub [f32; 3]); impl VertexPosition { - const BYTES: usize = 3 * std::mem::size_of::(); + const BYTES: usize = std::mem::size_of::(); fn as_bytes(&self) -> &[u8] { let bytes: &[u8; Self::BYTES] = unsafe { std::mem::transmute(&self.0) }; @@ -104,7 +106,16 @@ mod hash { impl Hash for VertexPosition { fn hash(&self, state: &mut H) { - state.write(self.as_bytes()); + let [x, y, z] = self.0; + let [x, y, z] = [f32::to_bits(x), f32::to_bits(y), f32::to_bits(z)]; + + // scramble bits to make sure that integer coordinates have entropy in lower bits + let x = x ^ (x >> 17); + let y = y ^ (y >> 17); + let z = z ^ (z >> 17); + + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + state.write_u32((x.wrapping_mul(73856093)) ^ (y.wrapping_mul(19349663)) ^ (z.wrapping_mul(83492791))); } } @@ -116,62 +127,28 @@ mod hash { impl Eq for VertexPosition {} - #[derive(Default)] - pub struct PositionHasher { - state: u64, - } - - impl Hasher for PositionHasher { - fn write(&mut self, bytes: &[u8]) { - assert!(bytes.len() == VertexPosition::BYTES); - - let a = u32::from_ne_bytes((&bytes[0..4]).try_into().unwrap()); - let b = u32::from_ne_bytes((&bytes[4..8]).try_into().unwrap()); - let c = u32::from_ne_bytes((&bytes[8..12]).try_into().unwrap()); - - // Optimized Spatial Hashing for Collision Detection of Deformable Objects - self.state = ((a.wrapping_mul(73856093)) ^ (b.wrapping_mul(19349663)) ^ (c.wrapping_mul(83492791))) as u64; - } - - fn finish(&self) -> u64 { - self.state - } - } - - pub type BuildPositionHasher = BuildHasherDefault; - - #[derive(Default)] - pub struct IdHasher { - state: u64, - } + #[derive(PartialEq, Eq, Clone, Copy, Default)] + pub struct VertexId(pub u32); - impl Hasher for IdHasher { - fn write(&mut self, bytes: &[u8]) { - assert!(bytes.len() == std::mem::size_of::()); - - let mut h = u32::from_ne_bytes((&bytes[0..4]).try_into().unwrap()); + impl Hash for VertexId { + fn hash(&self, state: &mut H) { + let mut h = self.0; // MurmurHash2 finalizer h ^= h >> 13; h = h.wrapping_mul(0x5bd1e995); h ^= h >> 15; - self.state = h as u64; - } - - fn finish(&self) -> u64 { - self.state + state.write_u32(h); } } - - pub type BuildIdHasher = BuildHasherDefault; } fn build_position_remap(remap: &mut [u32], wedge: &mut [u32], vertices: &[Vertex]) where Vertex: Position, { - let mut table = HashMap::with_capacity_and_hasher(vertices.len(), hash::BuildPositionHasher::default()); + let mut table = HashMap::with_capacity_and_hasher(vertices.len(), BuildNoopHasher::default()); // build forward remap: for each vertex, which other (canonical) vertex does it map to? // we use position equivalence for this, and remap vertices to other existing vertices @@ -1063,7 +1040,7 @@ fn remap_edge_loops(loop_: &mut [u32], collapse_remap: &[u32]) { } } -fn compute_vertex_ids(vertex_ids: &mut [u32], vertex_positions: &[Vector3], grid_size: i32) { +fn compute_vertex_ids(vertex_ids: &mut [VertexId], vertex_positions: &[Vector3], grid_size: i32) { assert!((1..=1024).contains(&grid_size)); let cell_scale = (grid_size - 1) as f32; @@ -1072,11 +1049,11 @@ fn compute_vertex_ids(vertex_ids: &mut [u32], vertex_positions: &[Vector3], grid let yi = (pos.y * cell_scale + 0.5) as i32; let zi = (pos.z * cell_scale + 0.5) as i32; - *id = ((xi << 20) | (yi << 10) | zi) as u32; + *id = VertexId(((xi << 20) | (yi << 10) | zi) as u32); } } -fn count_triangles(vertex_ids: &[u32], indices: &[u32]) -> usize { +fn count_triangles(vertex_ids: &[VertexId], indices: &[u32]) -> usize { let mut result = 0; for abc in indices.chunks_exact(3) { @@ -1091,9 +1068,9 @@ fn count_triangles(vertex_ids: &[u32], indices: &[u32]) -> usize { } fn fill_vertex_cells( - table: &mut HashMap, + table: &mut HashMap, vertex_cells: &mut [u32], - vertex_ids: &[u32], + vertex_ids: &[VertexId], ) -> usize { let mut result: usize = 0; @@ -1111,7 +1088,7 @@ fn fill_vertex_cells( result } -fn count_vertex_cells(table: &mut HashMap, vertex_ids: &[u32]) -> usize { +fn count_vertex_cells(table: &mut HashMap, vertex_ids: &[VertexId]) -> usize { table.clear(); let mut result = 0; @@ -1119,11 +1096,11 @@ fn count_vertex_cells(table: &mut HashMap, vertex for id in vertex_ids { result += match table.entry(*id) { Entry::Occupied(mut entry) => { - *entry.get_mut() = *id; + *entry.get_mut() = id.0; 0 } Entry::Vacant(entry) => { - entry.insert(*id); + entry.insert(id.0); 1 } }; @@ -1194,7 +1171,7 @@ fn fill_cell_remap( fn filter_triangles( destination: &mut [u32], - tritable: &mut HashMap, + tritable: &mut HashMap, indices: &[u32], vertex_cells: &[u32], cell_remap: &[u32], @@ -1450,7 +1427,7 @@ where result_count } -/// Reduces the number of triangles in the mesh, sacrificing mesh apperance for simplification performance. +/// Reduces the number of triangles in the mesh, sacrificing mesh appearance for simplification performance. /// /// The algorithm doesn't preserve mesh topology but can stop short of the target goal based on target error. /// @@ -1493,7 +1470,7 @@ where ); } - let mut vertex_ids = vec![0; vertices.len()]; + let mut vertex_ids = vec![VertexId::default(); vertices.len()]; const INTERPOLATION_PASSES: i32 = 5; @@ -1581,7 +1558,7 @@ where } // build vertex->cell association by mapping all vertices with the same quantized position to the same cell - let mut table = HashMap::with_capacity_and_hasher(vertices.len(), hash::BuildIdHasher::default()); + let mut table = HashMap::with_capacity_and_hasher(vertices.len(), BuildNoopHasher::default()); let mut vertex_cells = vec![0; vertices.len()]; @@ -1607,7 +1584,7 @@ where // collapse triangles! // note that we need to filter out triangles that we've already output because we very frequently generate redundant triangles between cells :( - let mut tritable = HashMap::with_capacity_and_hasher(min_triangles, hash::BuildPositionHasher::default()); + let mut tritable = HashMap::with_capacity_and_hasher(min_triangles, BuildNoopHasher::default()); let write = filter_triangles(destination, &mut tritable, indices, &vertex_cells, &cell_remap); assert!(write <= target_index_count); @@ -1656,9 +1633,9 @@ where println!("target: {target_cell_count} cells"); } - let mut vertex_ids = vec![0; vertices.len()]; + let mut vertex_ids = vec![VertexId::default(); vertices.len()]; - let mut table = HashMap::with_capacity_and_hasher(vertices.len(), hash::BuildIdHasher::default()); + let mut table = HashMap::with_capacity_and_hasher(vertices.len(), BuildNoopHasher::default()); const INTERPOLATION_PASSES: i32 = 5; diff --git a/src/vertex/filter.rs b/src/vertex/filter.rs index ef20a65..51031d3 100644 --- a/src/vertex/filter.rs +++ b/src/vertex/filter.rs @@ -4,78 +4,45 @@ //! //! Mainly useful in combination with the `EXT_meshopt_compression` glTF extension. -use crate::util::as_mut_bytes; - -macro_rules! from_bytes_oct { - ($e:ident, 4) => { - ( - i8::from_ne_bytes([$e[0]]) as f32, - i8::from_ne_bytes([$e[1]]) as f32, - i8::from_ne_bytes([$e[2]]) as f32, - ) - }; - ($e:ident, 8) => { - ( - i16::from_ne_bytes([$e[0], $e[1]]) as f32, - i16::from_ne_bytes([$e[2], $e[3]]) as f32, - i16::from_ne_bytes([$e[4], $e[5]]) as f32, - ) - }; -} +use crate::{quantize::quantize_snorm, util::zero_inverse}; -macro_rules! to_bytes_oct { - ($e:ident, $x:ident, $y:ident, $z:ident, 4) => {{ - $e[0] = $x.to_ne_bytes()[0]; - $e[1] = $y.to_ne_bytes()[0]; - $e[2] = $z.to_ne_bytes()[0]; - }}; - ($e:ident, $x:ident, $y:ident, $z:ident, 8) => {{ - $e[0] = $x.to_ne_bytes()[0]; - $e[1] = $x.to_ne_bytes()[1]; - $e[2] = $y.to_ne_bytes()[0]; - $e[3] = $y.to_ne_bytes()[1]; - $e[4] = $z.to_ne_bytes()[0]; - $e[5] = $z.to_ne_bytes()[1]; - }}; -} +macro_rules! decode_filter_oct_scalar_impl { + ($dest:ty, $signed_dest:ty, $data:expr) => { + const MAX: f32 = ((1 << (std::mem::size_of::<$dest>() * 8 - 1)) - 1) as f32; -fn decode_filter_oct_scalar(data: &mut [u8], stride: usize) { - let max = ((1 << (stride / 4 * 8 - 1)) - 1) as f32; + for v in $data { + // convert x and y to floats and reconstruct z; this assumes zf encodes 1.0 at the same bit count + let (mut x, mut y, mut z) = ( + v[0] as $signed_dest as f32, + v[1] as $signed_dest as f32, + v[2] as $signed_dest as f32, + ); - for bytes in data.chunks_exact_mut(stride) { - // convert x and y to floats and reconstruct z; this assumes zf encodes 1.0 at the same bit count - let (mut x, mut y, mut z) = match stride { - 4 => from_bytes_oct!(bytes, 4), - 8 => from_bytes_oct!(bytes, 8), - _ => unreachable!(""), - }; + z = z - x.abs() - y.abs(); - z = z - x.abs() - y.abs(); + // fixup octahedral coordinates for z<0 + let t = z.min(0.0); - // fixup octahedral coordinates for z<0 - let t = z.min(0.0); + x += if x >= 0.0 { t } else { -t }; + y += if y >= 0.0 { t } else { -t }; - x += if x >= 0.0 { t } else { -t }; - y += if y >= 0.0 { t } else { -t }; + // compute normal length & scale + let l = (x * x + y * y + z * z).sqrt(); + let s = MAX / l; - // compute normal length & scale - let l = (x * x + y * y + z * z).sqrt(); - let s = max / l; + // rounded signed float->int + let xf = (x * s + if x >= 0.0 { 0.5 } else { -0.5 }) as i32; + let yf = (y * s + if y >= 0.0 { 0.5 } else { -0.5 }) as i32; + let zf = (z * s + if z >= 0.0 { 0.5 } else { -0.5 }) as i32; - // rounded signed float->int - let xf = (x * s + if x >= 0.0 { 0.5 } else { -0.5 }) as i32; - let yf = (y * s + if y >= 0.0 { 0.5 } else { -0.5 }) as i32; - let zf = (z * s + if z >= 0.0 { 0.5 } else { -0.5 }) as i32; - - match stride { - 4 => to_bytes_oct!(bytes, xf, yf, zf, 4), - 8 => to_bytes_oct!(bytes, xf, yf, zf, 8), - _ => unreachable!(""), + v[0] = xf as $dest; + v[1] = yf as $dest; + v[2] = zf as $dest; } - } + }; } -fn decode_filter_quat_scalar(data: &mut [[u16; 4]]) { +fn decode_filter_quat_scalar<'a>(data: impl Iterator) { let scale = 1.0 / 2.0f32.sqrt(); for q in data { @@ -108,7 +75,7 @@ fn decode_filter_quat_scalar(data: &mut [[u16; 4]]) { } } -fn decode_filter_exp_scalar(data: &mut [u32]) { +fn decode_filter_exp_scalar<'a>(data: impl Iterator) { for v in data { // decode mantissa and exponent let m = (*v << 8) as i32 >> 8; @@ -131,29 +98,174 @@ fn decode_filter_exp_scalar(data: &mut [u32]) { /// Decodes octahedral encoding of a unit vector with K-bit (K <= 16) signed X/Y as an input; Z must store 1.0. /// /// Each component is stored as an 8-bit normalized integer. W is preserved as is. -pub fn decode_filter_oct_8(data: &mut [[u8; 4]]) { - decode_filter_oct_scalar(as_mut_bytes(data), 4); +pub fn decode_filter_oct_8<'a>(data: impl Iterator) { + decode_filter_oct_scalar_impl!(u8, i8, data); } /// Decodes octahedral encoding of a unit vector with K-bit (K <= 16) signed X/Y as an input; Z must store 1.0. /// /// Each component is stored as a 16-bit normalized integer. W is preserved as is. -pub fn decode_filter_oct_16(data: &mut [[u16; 4]]) { - decode_filter_oct_scalar(as_mut_bytes(data), 8); +pub fn decode_filter_oct_16<'a>(data: impl Iterator) { + decode_filter_oct_scalar_impl!(u16, i16, data); } /// Decodes 3-component quaternion encoding with K-bit (4 <= K <= 16) component encoding and a 2-bit component index indicating which component to reconstruct. -pub fn decode_filter_quat(data: &mut [[u16; 4]]) { +pub fn decode_filter_quat<'a>(data: impl Iterator) { decode_filter_quat_scalar(data); } /// Decodes exponential encoding of floating-point data with 8-bit exponent and 24-bit integer mantissa as 2^E*M. /// /// Each 32-bit component is decoded in isolation. -pub fn decode_filter_exp(data: &mut [u32]) { +pub fn decode_filter_exp<'a>(data: impl Iterator) { decode_filter_exp_scalar(data); } +macro_rules! encode_filter_oct_scalar_impl { + ($data_type:ty, $signed_data_type:ty, $bits: expr, $data:expr, $dest:expr) => { + assert!((1..=16).contains(&$bits)); + + const STRIDE: usize = std::mem::size_of::<$data_type>() * 4; + const BYTE_BITS: u32 = (STRIDE * 2) as u32; + + for (n, dest) in $data.zip($dest) { + // octahedral encoding of a unit vector + let mut nx = n[0]; + let mut ny = n[1]; + let nz = n[2]; + let nw = n[3]; + let nl = nx.abs() + ny.abs() + nz.abs(); + let ns = zero_inverse(nl); + + nx *= ns; + ny *= ns; + + let u = if nz >= 0.0 { + nx + } else { + (1.0 - ny.abs()) * if nx >= 0.0 { 1.0 } else { -1.0 } + }; + let v = if nz >= 0.0 { + ny + } else { + (1.0 - nx.abs()) * if ny >= 0.0 { 1.0 } else { -1.0 } + }; + + let fu = quantize_snorm(u, $bits); + let fv = quantize_snorm(v, $bits); + let fo = quantize_snorm(1.0, $bits); + let fw = quantize_snorm(nw, BYTE_BITS); + + dest[0] = (fu as $signed_data_type) as $data_type; + dest[1] = (fv as $signed_data_type) as $data_type; + dest[2] = (fo as $signed_data_type) as $data_type; + dest[3] = (fw as $signed_data_type) as $data_type; + } + }; +} + +/// Encodes unit vectors with K-bit (K <= 16) signed X/Y as an output. +/// +/// Each component is stored as an 8-bit normalized integer. W is preserved as is. +pub fn encode_filter_oct_8<'a>( + destination: impl Iterator, + bits: u32, + data: impl Iterator, +) { + encode_filter_oct_scalar_impl!(u8, i8, bits, data, destination); +} + +/// Encodes unit vectors with K-bit (K <= 16) signed X/Y as an output. +/// +/// Each component is stored as a 16-bit normalized integer. W is preserved as is. +pub fn encode_filter_oct_16<'a>( + destination: impl Iterator, + bits: u32, + data: impl Iterator, +) { + encode_filter_oct_scalar_impl!(u16, i16, bits, data, destination); +} + +/// Encodes unit quaternions with K-bit (4 <= K <= 16) component encoding. +/// +/// Each component is stored as an 16-bit integer. +pub fn encode_filter_quat<'a>( + destination: impl Iterator, + bits: u32, + data: impl Iterator, +) { + assert!((4..=16).contains(&bits)); + + let scaler = 2.0f32.sqrt(); + + for (q, d) in data.zip(destination) { + // establish maximum quaternion component + let mut qc = 0; + qc = if q[1].abs() > q[qc].abs() { 1 } else { qc }; + qc = if q[2].abs() > q[qc].abs() { 2 } else { qc }; + qc = if q[3].abs() > q[qc].abs() { 3 } else { qc }; + + // we use double-cover properties to discard the sign + let sign = if q[qc] < 0.0 { -1.0 } else { 1.0 }; + + // note: we always encode a cyclical swizzle to be able to recover the order via rotation + d[0] = quantize_snorm(q[(qc + 1) & 3] * scaler * sign, bits) as u16; + d[1] = quantize_snorm(q[(qc + 2) & 3] * scaler * sign, bits) as u16; + d[2] = quantize_snorm(q[(qc + 3) & 3] * scaler * sign, bits) as u16; + d[3] = ((quantize_snorm(1.0, bits) & !3) | (qc as i32)) as u16; + } +} + +fn frexp(s: f32) -> (f32, i32) { + if 0.0 == s { + (s, 0) + } else { + let lg = s.abs().log2(); + let x = (lg.fract() - 1.).exp2(); + let exp = lg.floor() + 1.0; + (s.signum() * x, exp as i32) + } +} + +fn ldexp(a: f32, exp: i32) -> f32 { + let f = exp as f32; + a * f.exp2() +} + +/// Encodes arbitrary (finite) floating-point data with 8-bit exponent and K-bit integer mantissa (1 <= K <= 24). +/// +/// Mantissa is shared between all components of a given vector as defined by `N`. +/// When individual (scalar) encoding is desired, simply pass N=1 and adjust iterators accordingly ([core::array::from_ref], [core::array::from_mut]). +pub fn encode_filter_exp<'a, const N: usize>( + destination: impl Iterator, + bits: u32, + data: impl Iterator, +) { + assert!((1..=24).contains(&bits)); + + for (v, d) in data.zip(destination) { + // use maximum exponent to encode values; this guarantees that mantissa is [-1, 1] + let mut exp = -100; + + for c in v { + let (_, e) = frexp(*c); + exp = exp.max(e); + } + + // note that we additionally scale the mantissa to make it a K-bit signed integer (K-1 bits for magnitude) + exp -= (bits - 1) as i32; + + // compute renormalized rounded mantissa for each component + let mmask = (1 << 24) - 1; + + for (vc, dc) in v.iter().zip(d.iter_mut()) { + let m = (ldexp(*vc, -exp) + if *vc >= 0.0 { 0.5 } else { -0.5 }) as i32; + + *dc = ((m & mmask) as u32) | ((exp as u32) << 24); + } + } +} + #[cfg(test)] mod test { use super::*; @@ -177,13 +289,13 @@ mod test { // Aligned by 4 let mut full = [[0; 4]; 4]; full.copy_from_slice(&DATA); - decode_filter_oct_8(&mut full); + decode_filter_oct_8(full.iter_mut()); assert_eq!(full, EXPECTED); // Tail processing for unaligned data let mut tail = [[0; 4]; 3]; tail.copy_from_slice(&DATA[0..3]); - decode_filter_oct_8(&mut tail); + decode_filter_oct_8(tail.iter_mut()); assert_eq!(tail, &EXPECTED[0..tail.len()]); } @@ -206,18 +318,18 @@ mod test { // Aligned by 4 let mut full = [[0; 4]; 4]; full.copy_from_slice(&DATA); - decode_filter_oct_16(&mut full); + decode_filter_oct_16(full.iter_mut()); assert_eq!(full, EXPECTED); // Tail processing for unaligned data let mut tail = [[0; 4]; 3]; tail.copy_from_slice(&DATA[0..3]); - decode_filter_oct_16(&mut tail); + decode_filter_oct_16(tail.iter_mut()); assert_eq!(tail, &EXPECTED[0..tail.len()]); } #[test] - fn decode_filter_quat_12() { + fn test_decode_filter_quat_12() { const DATA: [[u16; 4]; 4] = [ [0, 1, 0, 0x7fc], [0, 1870, 0, 0x7fd], @@ -234,31 +346,142 @@ mod test { // Aligned by 4 let mut full = DATA; - decode_filter_quat(&mut full); + decode_filter_quat(full.iter_mut()); assert_eq!(full, EXPECTED); // Tail processing for unaligned data let mut tail = [[0; 4]; 3]; tail.copy_from_slice(&DATA[0..3]); - decode_filter_quat(&mut tail); + decode_filter_quat(tail.iter_mut()); assert_eq!(tail, &EXPECTED[0..tail.len()]); } #[test] fn test_decode_filter_exp() { const DATA: [u32; 4] = [0, 0xff000003, 0x02fffff7, 0xfe7fffff]; - const EXPECTED: [u32; 4] = [0, 0x3fc00000, 0xc2100000, 0x49fffffe]; // Aligned by 4 let mut full = DATA; - decode_filter_exp(&mut full); + decode_filter_exp(full.iter_mut()); assert_eq!(full, EXPECTED); // Tail processing for unaligned data let mut tail = [0; 3]; tail.copy_from_slice(&DATA[0..3]); - decode_filter_exp(&mut tail); + decode_filter_exp(tail.iter_mut()); assert_eq!(tail, &EXPECTED[0..tail.len()]); } + + #[test] + fn test_encode_filter_oct_8() { + const DATA: [[f32; 4]; 4] = [ + [1.0, 0.0, 0.0, 0.0], + [0.0, -1.0, 0.0, 0.0], + [0.7071068, 0.0, 0.707168, 1.0], + [-0.7071068, 0.0, -0.707168, 1.0], + ]; + + const EXPECTED: [[u8; 4]; 4] = [ + [0x7f, 0, 0x7f, 0], + [0, 0x81, 0x7f, 0], + [0x3f, 0, 0x7f, 0x7f], + [0x81, 0x40, 0x7f, 0x7f], + ]; + + let mut encoded = [0u8; 4 * 4]; + encode_filter_oct_8(encoded.as_chunks_mut::<4>().0.iter_mut(), 8, DATA.iter()); + + assert_eq!(encoded, *EXPECTED.iter().flatten().copied().collect::>()); + + let mut decoded = encoded.clone(); + decode_filter_oct_8(decoded.as_chunks_mut::<4>().0.iter_mut()); + + for (dat, dec) in DATA.iter().flatten().zip(decoded) { + assert!((dec as i8 as f32 / 127.0 - dat).abs() < 1e-2); + } + } + + #[test] + fn test_encode_filter_oct_12() { + const DATA: [[f32; 4]; 4] = [ + [1.0, 0.0, 0.0, 0.0], + [0.0, -1.0, 0.0, 0.0], + [0.7071068, 0.0, 0.707168, 1.0], + [-0.7071068, 0.0, -0.707168, 1.0], + ]; + + const EXPECTED: [[u16; 4]; 4] = [ + [0x7ff, 0, 0x7ff, 0], + [0, 0xf801, 0x7ff, 0], + [0x3ff, 0, 0x7ff, 0x7fff], + [0xf801, 0x400, 0x7ff, 0x7fff], + ]; + + let mut encoded = [0u16; 4 * 4]; + encode_filter_oct_16(encoded.as_chunks_mut::<4>().0.iter_mut(), 12, DATA.iter()); + + assert_eq!(encoded, *EXPECTED.iter().flatten().copied().collect::>()); + + let mut decoded = encoded.clone(); + decode_filter_oct_16(decoded.as_chunks_mut::<4>().0.iter_mut()); + + for (dat, dec) in DATA.iter().flatten().zip(decoded) { + assert!((dec as i16 as f32 / 32767.0 - dat).abs() < 1e-3); + } + } + + #[test] + fn test_encode_filter_quat_12() { + const DATA: [[f32; 4]; 4] = [ + [1.0, 0.0, 0.0, 0.0], + [0.0, -1.0, 0.0, 0.0], + [0.7071068, 0.0, 0.0, 0.707168], + [-0.7071068, 0.0, 0.0, -0.707168], + ]; + + const EXPECTED: [[u16; 4]; 4] = [ + [0, 0, 0, 0x7fc], + [0, 0, 0, 0x7fd], + [0x7ff, 0, 0, 0x7ff], + [0x7ff, 0, 0, 0x7ff], + ]; + + let mut encoded = [0u16; 4 * 4]; + encode_filter_quat(encoded.as_chunks_mut::<4>().0.iter_mut(), 12, DATA.iter()); + + assert_eq!(encoded, *EXPECTED.iter().flatten().copied().collect::>()); + + let mut decoded = encoded.clone(); + decode_filter_quat(decoded.as_chunks_mut::<4>().0.iter_mut()); + + for (dat, dec) in DATA.iter().zip(decoded.as_chunks::<4>().0) { + let dx = dec[0] as f32 / 32767.0; + let dy = dec[1] as f32 / 32767.0; + let dz = dec[2] as f32 / 32767.0; + let dw = dec[3] as f32 / 32767.0; + + let dp = dat[0] * dx + dat[1] * dy + dat[2] * dz + dat[3] * dw; + + assert!((dp.abs() - 1.0).abs() < 1e-4); + } + } + + #[test] + fn test_encode_filter_exp() { + const DATA: [[f32; 3]; 1] = [[1.0, -23.4, -0.1]]; + const EXPECTED: [[u32; 3]; 1] = [[0xf7000200, 0xf7ffd133, 0xf7ffffcd]]; + + let mut encoded = [[0u32; 3]; 1]; + encode_filter_exp::<3>(encoded.iter_mut(), 15, DATA.iter()); + + assert_eq!(encoded, EXPECTED); + + let mut decoded = encoded.clone(); + decode_filter_exp(decoded.iter_mut().flatten()); + + for (dat, dec) in DATA.iter().flatten().zip(decoded.iter().flatten()) { + assert!((f32::from_bits(*dec) - *dat).abs() < 1e-3); + } + } } diff --git a/src/vertex/mod.rs b/src/vertex/mod.rs index 5c62e15..d757b52 100644 --- a/src/vertex/mod.rs +++ b/src/vertex/mod.rs @@ -1,6 +1,7 @@ pub mod buffer; pub mod cache; pub mod fetch; +#[cfg(feature = "experimental")] pub mod filter; #[derive(Debug)]