Skip to content

Commit

Permalink
Generate light ray structs as binary rather than text.
Browse files Browse the repository at this point in the history
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.
kpreid committed May 4, 2024
1 parent 45a772d commit fb61ca0
Showing 6 changed files with 168 additions and 66 deletions.
4 changes: 3 additions & 1 deletion all-is-cubes/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
111 changes: 72 additions & 39 deletions all-is-cubes/build.rs
Original file line number Diff line number Diff line change
@@ -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::<OneRay, u8>(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<OneRay> {
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<f32>) -> 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<T>(<T as ToBytes>::Bytes)
where
T: ToBytes,
<T as ToBytes>::Bytes: Copy + Clone + fmt::Debug + bytemuck::Pod + bytemuck::Zeroable;

impl<T: ToBytes> From<T> for TargetEndian<T>
where
<T as ToBytes>::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}"),
},
)
}
}
}
2 changes: 2 additions & 0 deletions all-is-cubes/src/space/light.rs
Original file line number Diff line number Diff line change
@@ -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;
26 changes: 26 additions & 0 deletions all-is-cubes/src/space/light/chart_schema.rs
Original file line number Diff line number Diff line change
@@ -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<f32> {
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> = T;
26 changes: 26 additions & 0 deletions all-is-cubes/src/space/light/chart_schema_shared.rs
Original file line number Diff line number Diff line change
@@ -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<f64>; 3],
pub direction: [TargetEndian<f64>; 3],
// This can't be a `FaceMap` because `FaceMap` is not `repr(C)`
pub face_cosines: [TargetEndian<f32>; 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.
65 changes: 39 additions & 26 deletions all-is-cubes/src/space/light/rays.rs
Original file line number Diff line number Diff line change
@@ -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<f32>,
}

/// 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::<u8, OneRay>(&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()
}

0 comments on commit fb61ca0

Please sign in to comment.