From fb61ca0250dfd7a2cef8c0565075462035444293 Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Fri, 3 May 2024 11:33:53 -0700 Subject: [PATCH] Generate light ray structs as binary rather than text. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm planning to switch to generating the entire “propagation table” at compile time, at which point we'll have a lot more data, which I want to not have to pass through all of rustc's processing for source code. So, this commit establishes the new data encoding technique before we actually change the data. --- all-is-cubes/Cargo.toml | 4 +- all-is-cubes/build.rs | 111 ++++++++++++------ all-is-cubes/src/space/light.rs | 2 + all-is-cubes/src/space/light/chart_schema.rs | 26 ++++ .../src/space/light/chart_schema_shared.rs | 26 ++++ all-is-cubes/src/space/light/rays.rs | 65 ++++++---- 6 files changed, 168 insertions(+), 66 deletions(-) create mode 100644 all-is-cubes/src/space/light/chart_schema.rs create mode 100644 all-is-cubes/src/space/light/chart_schema_shared.rs diff --git a/all-is-cubes/Cargo.toml b/all-is-cubes/Cargo.toml index da210fd07..38ae7cc26 100644 --- a/all-is-cubes/Cargo.toml +++ b/all-is-cubes/Cargo.toml @@ -131,8 +131,10 @@ unicode-segmentation = { workspace = true } yield-progress = { workspace = true } [build-dependencies] -# for calculation in build script +all-is-cubes-base = { workspace = true } +bytemuck = { workspace = true, features = ["derive"] } euclid = { version = "0.22.9", default-features = false, features = ["libm", "mint"] } +num-traits = { workspace = true } [dev-dependencies] allocation-counter = { workspace = true } diff --git a/all-is-cubes/build.rs b/all-is-cubes/build.rs index d9ae542df..a6fee662b 100644 --- a/all-is-cubes/build.rs +++ b/all-is-cubes/build.rs @@ -3,30 +3,31 @@ //! Does not do any native compilation; this is just precomputation and code-generation //! more convenient than a proc macro. -use std::io::Write as _; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::{env, fs}; -use euclid::default::{Point3D, Vector3D}; +use all_is_cubes_base::math::{self, Face6, FaceMap, FreePoint, FreeVector}; fn main() { println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/space/light/chart_data.rs"); - generate_light_ray_pattern( - &PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("light_ray_pattern.rs"), - ); + let rays = generate_light_ray_pattern(); + + fs::write( + PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("light_ray_pattern.bin"), + bytemuck::cast_slice::(rays.as_slice()), + ) + .expect("failed to write light_ray_pattern"); } const RAY_DIRECTION_STEP: isize = 5; // TODO: Make multiple ray patterns that suit the maximum_distance parameter. -// TODO: Consider replacing this manual formatting with https://docs.rs/uneval/latest -fn generate_light_ray_pattern(path: &Path) { - let mut file = fs::File::create(path).expect("failed to create light ray file"); - - let origin = Point3D::new(0.5, 0.5, 0.5); +fn generate_light_ray_pattern() -> Vec { + let origin = FreePoint::new(0.5, 0.5, 0.5); - writeln!(file, "static LIGHT_RAYS: &[LightRayData] = &[").unwrap(); + let mut rays = Vec::new(); // TODO: octahedron instead of cube for x in -RAY_DIRECTION_STEP..=RAY_DIRECTION_STEP { @@ -36,41 +37,73 @@ fn generate_light_ray_pattern(path: &Path) { || y.abs() == RAY_DIRECTION_STEP || z.abs() == RAY_DIRECTION_STEP { - let direction = Vector3D::new(x as f64, y as f64, z as f64).normalize(); - - writeln!(file, "LightRayData {{").unwrap(); - writeln!(file, - " ray: Ray {{ origin: Point3D::new({origin}), direction: Vector3D::new({direction}) }},\n face_cosines: FaceMap {{", - origin = vecfields(origin), - direction = vecfields(direction), - ).unwrap(); - - for (name, unit_vector) in [ - ("nx", Vector3D::new(-1, 0, 0)), - ("ny", Vector3D::new(0, -1, 0)), - ("nz", Vector3D::new(0, 0, -1)), - ("px", Vector3D::new(1, 0, 0)), - ("py", Vector3D::new(0, 1, 0)), - ("pz", Vector3D::new(0, 0, 1)), - ] { + let direction = FreeVector::new(x as f64, y as f64, z as f64).normalize(); + + let mut cosines = FaceMap::repeat(0.0f32); + for face in Face6::ALL { + let unit_vector: FreeVector = face.normal_vector(); let cosine = unit_vector.to_f32().dot(direction.to_f32()).max(0.0); - writeln!(file, " {name}: {cosine:?},").unwrap(); + cosines[face] = cosine; } - // close braces for `FaceMap` and `LightRayData` structs - writeln!(file, "}} }},").unwrap(); + rays.push(OneRay::new(origin, direction, cosines)) } } } } - // end of LIGHT_RAYS - writeln!(file, "];").unwrap(); - - file.flush().unwrap(); + rays } -fn vecfields(value: impl Into<[f64; 3]>) -> String { - let [x, y, z] = value.into(); - format!("{x:?}, {y:?}, {z:?},") +use chart_schema::OneRay; +#[path = "src/space/light/"] +mod chart_schema { + use crate::math::{FaceMap, FreePoint, FreeVector, VectorOps as _}; + use core::fmt; + use num_traits::ToBytes; + use std::env; + + mod chart_schema_shared; + pub(crate) use chart_schema_shared::OneRay; + + impl OneRay { + pub fn new(origin: FreePoint, direction: FreeVector, face_cosines: FaceMap) -> Self { + let face_cosines = face_cosines.map(|_, c| TargetEndian::from(c)); + Self { + origin: origin.map(TargetEndian::from).into(), + direction: direction.map(TargetEndian::from).into(), + face_cosines: [ + face_cosines.nx, + face_cosines.ny, + face_cosines.nz, + face_cosines.px, + face_cosines.py, + face_cosines.pz, + ], + } + } + } + + /// Used as `super::TargetEndian` by `shared`. + #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] + #[repr(C, packed)] + pub(crate) struct TargetEndian(::Bytes) + where + T: ToBytes, + ::Bytes: Copy + Clone + fmt::Debug + bytemuck::Pod + bytemuck::Zeroable; + + impl From for TargetEndian + where + ::Bytes: Copy + Clone + fmt::Debug + bytemuck::Pod + bytemuck::Zeroable, + { + fn from(value: T) -> Self { + Self( + match env::var("CARGO_CFG_TARGET_ENDIAN").unwrap().as_str() { + "big" => T::to_be_bytes(&value), + "little" => T::to_le_bytes(&value), + e => panic!("unknown endianness: {e}"), + }, + ) + } + } } diff --git a/all-is-cubes/src/space/light.rs b/all-is-cubes/src/space/light.rs index a3dbef233..cf1bf10e1 100644 --- a/all-is-cubes/src/space/light.rs +++ b/all-is-cubes/src/space/light.rs @@ -9,6 +9,8 @@ pub use debug::{LightComputeOutput, LightUpdateCubeInfo, LightUpdateRayInfo}; mod queue; pub(crate) use queue::{LightUpdateQueue, LightUpdateRequest, Priority}; +mod chart_schema; + mod rays; mod updater; diff --git a/all-is-cubes/src/space/light/chart_schema.rs b/all-is-cubes/src/space/light/chart_schema.rs new file mode 100644 index 000000000..c3d19c83e --- /dev/null +++ b/all-is-cubes/src/space/light/chart_schema.rs @@ -0,0 +1,26 @@ +use crate::raycast::Ray; + +#[path = "chart_schema_shared.rs"] +mod chart_schema_shared; +pub(crate) use chart_schema_shared::OneRay; + +impl OneRay { + pub fn ray(&self) -> Ray { + Ray::new(self.origin, self.direction) + } + + pub fn face_cosines(&self) -> crate::math::FaceMap { + let [nx, ny, nz, px, py, pz] = self.face_cosines; + crate::math::FaceMap { + nx, + ny, + nz, + px, + py, + pz, + } + } +} + +/// Used by `chart_data` type declarations to have compatible behavior when cross-compiling. +type TargetEndian = T; diff --git a/all-is-cubes/src/space/light/chart_schema_shared.rs b/all-is-cubes/src/space/light/chart_schema_shared.rs new file mode 100644 index 000000000..0dd5436d1 --- /dev/null +++ b/all-is-cubes/src/space/light/chart_schema_shared.rs @@ -0,0 +1,26 @@ +//! The single purpose of this file is to be used by both the regular crate code +//! and the build script to share some binary structure layouts. +//! +//! This is the most efficient way I could think of to store and transfer the +//! pre-computed light ray chart data. +//! +//! Currently, host and target endianness must be the same. +//! In the future, this may be handled by using number types wrapped to be explicitly +//! target endianness. + +// conditionally defined to be equal to f32 except in the build script +use super::TargetEndian; + +#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] +#[repr(C)] +pub(crate) struct OneRay { + // This can't be a `Ray` because `FaceMap` is not `repr(C)` + pub origin: [TargetEndian; 3], + pub direction: [TargetEndian; 3], + // This can't be a `FaceMap` because `FaceMap` is not `repr(C)` + pub face_cosines: [TargetEndian; 6], +} + +// Note: All of the methods are either only used for reading or only used for writing, +// so they're defined in the respective crates to reduce complications like what they depend on, +// how `TargetEndian` is defined, and dead code warnings. diff --git a/all-is-cubes/src/space/light/rays.rs b/all-is-cubes/src/space/light/rays.rs index 191248918..3700c6bfc 100644 --- a/all-is-cubes/src/space/light/rays.rs +++ b/all-is-cubes/src/space/light/rays.rs @@ -3,18 +3,11 @@ use alloc::vec::Vec; -use euclid::{Point3D, Vector3D}; - use crate::math::{CubeFace, FaceMap}; use crate::raycast::Ray; +use crate::space::light::chart_schema::OneRay; use crate::space::LightPhysics; -#[derive(Debug)] -struct LightRayData { - ray: Ray, - face_cosines: FaceMap, -} - /// Derived from [`LightRayData`], but with a pre-calculated sequence of cubes instead of a ray /// for maximum performance in the lighting calculation. #[derive(Debug)] @@ -35,9 +28,26 @@ pub(in crate::space) struct LightRayStep { pub relative_ray_to_here: Ray, } -// Build script generates the declaration: -// static LIGHT_RAYS: &[LightRayData] = &[... -include!(concat!(env!("OUT_DIR"), "/light_ray_pattern.rs")); +/// `bytemuck::cast_slice()` can't be const, so we have to write a function, +/// but this should all compile to a noop. +fn light_rays_data() -> &'static [OneRay] { + const LIGHT_RAYS_BYTES_LEN: usize = + include_bytes!(concat!(env!("OUT_DIR"), "/light_ray_pattern.bin")).len(); + + // Ensure the data is sufficiently aligned + #[repr(C)] + struct Align { + _aligner: [OneRay; 0], + data: [u8; LIGHT_RAYS_BYTES_LEN], + } + + static LIGHT_RAYS_BYTES: Align = Align { + _aligner: [], + data: *include_bytes!(concat!(env!("OUT_DIR"), "/light_ray_pattern.bin")), + }; + + bytemuck::cast_slice::(&LIGHT_RAYS_BYTES.data) +} /// Convert [`LIGHT_RAYS`] containing [`LightRayData`] into [`LightRayCubes`]. #[inline(never)] // cold code shouldn't be duplicated @@ -50,22 +60,25 @@ pub(in crate::space) fn calculate_propagation_table(physics: &LightPhysics) -> V // maximum_distance. LightPhysics::Rays { maximum_distance } => { let maximum_distance = f64::from(maximum_distance); - LIGHT_RAYS + light_rays_data() .iter() - .map(|&LightRayData { ray, face_cosines }| LightRayCubes { - relative_cube_sequence: ray - .cast() - .take_while(|step| step.t_distance() <= maximum_distance) - .map(|step| LightRayStep { - relative_cube_face: step.cube_face(), - relative_ray_to_here: Ray { - origin: ray.origin, - direction: step.intersection_point(ray) - ray.origin, - }, - }) - .collect(), - ray, - face_cosines, + .map(|&ray_data| { + let ray = ray_data.ray(); + LightRayCubes { + relative_cube_sequence: ray + .cast() + .take_while(|step| step.t_distance() <= maximum_distance) + .map(|step| LightRayStep { + relative_cube_face: step.cube_face(), + relative_ray_to_here: Ray { + origin: ray.origin, + direction: step.intersection_point(ray) - ray.origin, + }, + }) + .collect(), + ray, + face_cosines: ray_data.face_cosines(), + } }) .collect() }