Skip to content

Commit

Permalink
Precompute light propagation data at build time instead of in Space
Browse files Browse the repository at this point in the history
… initialization.

Instead of making a chart for the exact right size, we make the biggest
one we can, then do a distance check while iterating. This avoids
redundant run-time work and, in particular, should make the tests we
run under Miri that happen to allocate `Space`s much faster.

Size of the data is a concern; it's currently 645 KB. In the future, I
plan to replace this data structure with one which combines
nearly-parallel rays that traverse the same cubes, until they diverge,
instead of redundantly storing and iterating over each ray's sequence of
cubes. That will allow more rays for the same data and compute costs.

One unused possibility for further compression is storing *only* the
face/step directions instead of both the faces and the cube coordinates.
  • Loading branch information
kpreid committed May 12, 2024
1 parent 4bd005a commit 18caabe
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 123 deletions.
138 changes: 121 additions & 17 deletions all-is-cubes/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@
//! Does not do any native compilation; this is just precomputation and code-generation
//! more convenient than a proc macro.
use std::path::PathBuf;
extern crate alloc;

use std::path::{Path, PathBuf};
use std::{env, fs};

use all_is_cubes_base::math::{self, Face6, FaceMap, FreePoint, FreeVector};
use all_is_cubes_base::math::{self, Face6, FaceMap, FreePoint, FreeVector, VectorOps};
use all_is_cubes_base::raycast::Ray;

fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/space/light/chart_schema_shared.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");
let chart = generate_light_propagation_chart(&rays);
write_light_propagation_chart(chart);
}

const RAY_DIRECTION_STEP: isize = 5;

// TODO: Make multiple ray patterns that suit the maximum_distance parameter.
// TODO: Use morerays once we have a more efficient chart format that
// deduplicates work of near-parallel rays.
fn generate_light_ray_pattern() -> Vec<OneRay> {
let origin = FreePoint::new(0.5, 0.5, 0.5);

Expand All @@ -46,7 +46,7 @@ fn generate_light_ray_pattern() -> Vec<OneRay> {
cosines[face] = cosine;
}

rays.push(OneRay::new(origin, direction, cosines))
rays.push(OneRay::new(Ray::new(origin, direction), cosines))
}
}
}
Expand All @@ -55,23 +55,92 @@ fn generate_light_ray_pattern() -> Vec<OneRay> {
rays
}

use chart_schema::OneRay;
/// Convert rays into their steps (sequence of cube intersections).
fn generate_light_propagation_chart(rays: &[OneRay]) -> Vec<chart_schema::Steps> {
let maximum_distance = 127.0;
rays.iter()
.map(|&info| {
let ray: Ray = info.ray.into();
chart_schema::Steps {
info,
relative_cube_sequence: ray
.cast()
.take_while(|step| step.t_distance() <= maximum_distance)
.map(|step| chart_schema::Step {
relative_cube: step
.cube_ahead()
.lower_bounds()
.map(|coord| {
TargetEndian::from(i8::try_from(coord).expect("coordinate too big"))
})
.into(),
face: step.face().into(),
distance: step.t_distance().ceil() as u8,
})
.collect(),
}
})
.collect()
}

fn write_light_propagation_chart(chart: Vec<chart_schema::Steps>) {
// Repack data into two vectors instead of a vector of vectors.
let mut offset = 0;
let info: Vec<chart_schema::IndirectSteps> = chart
.iter()
.map(|steps| {
let len = steps.relative_cube_sequence.len();
let start = offset;
let end = offset + len;
offset += len;
chart_schema::IndirectSteps {
info: steps.info,
relative_cube_sequence: [start, end],
}
})
.collect();
let all_steps_concat: Vec<chart_schema::Step> = chart
.into_iter()
.flat_map(|steps| steps.relative_cube_sequence)
.collect();

writemuck(Path::new("light_chart_info.bin"), info.as_slice());
writemuck(
Path::new("light_chart_steps.bin"),
all_steps_concat.as_slice(),
);
}

/// Write the bytes of the given data to the given path within `OUT_DIR`.
fn writemuck<T: bytemuck::NoUninit>(out_relative_path: &Path, data: &[T]) {
assert!(out_relative_path.is_relative());
let path = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join(out_relative_path);
if let Err(e) = fs::write(&path, bytemuck::cast_slice::<T, u8>(data)) {
panic!(
"failed to write generated data to {path}: {e}",
path = path.display()
)
}
}

use chart_schema::{OneRay, TargetEndian};

#[path = "src/space/light/"]
mod chart_schema {
use crate::math::{FaceMap, FreePoint, FreeVector, VectorOps as _};
use crate::math::{FaceMap, VectorOps as _};
use all_is_cubes_base::raycast::Ray;
use core::fmt;
use num_traits::ToBytes;
use num_traits::{FromBytes, ToBytes};
use std::env;

mod chart_schema_shared;
pub(crate) use chart_schema_shared::OneRay;
pub(crate) use chart_schema_shared::{IndirectSteps, OneRay, Step, Steps};

impl OneRay {
pub fn new(origin: FreePoint, direction: FreeVector, face_cosines: FaceMap<f32>) -> Self {
pub fn new(ray: Ray, 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(),
ray: ray.into(),
face_cosines: [
face_cosines.nx,
face_cosines.ny,
Expand All @@ -84,6 +153,15 @@ mod chart_schema {
}
}

impl From<Ray> for chart_schema_shared::Ray {
fn from(value: Ray) -> Self {
Self {
origin: value.origin.map(TargetEndian::from).into(),
direction: value.direction.map(TargetEndian::from).into(),
}
}
}

/// Used as `super::TargetEndian` by `shared`.
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C, packed)]
Expand All @@ -106,4 +184,30 @@ mod chart_schema {
)
}
}

// Orphan rules don't allow this as a generic impl directly
impl<T> TargetEndian<T>
where
<T as ToBytes>::Bytes: Copy + Clone + bytemuck::Pod + bytemuck::Zeroable,
T: FromBytes<Bytes = <T as ToBytes>::Bytes> + ToBytes + bytemuck::Pod + bytemuck::Zeroable,
{
fn into_value(self) -> T {
let bytes = self.0;
match env::var("CARGO_CFG_TARGET_ENDIAN").unwrap().as_str() {
"big" => T::from_be_bytes(&bytes),
"little" => T::from_le_bytes(&bytes),
e => panic!("unknown endianness: {e}"),
}
}
}
impl From<TargetEndian<f32>> for f32 {
fn from(value: TargetEndian<f32>) -> Self {
value.into_value()
}
}
impl From<TargetEndian<f64>> for f64 {
fn from(value: TargetEndian<f64>) -> Self {
value.into_value()
}
}
}
19 changes: 13 additions & 6 deletions all-is-cubes/src/space/light/chart_schema.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
use crate::raycast::Ray;
use euclid::Point3D;

use all_is_cubes_base::math::{Cube, CubeFace};

#[path = "chart_schema_shared.rs"]
mod chart_schema_shared;
pub(crate) use chart_schema_shared::OneRay;
pub(crate) use chart_schema_shared::*;

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 {
Expand All @@ -22,5 +20,14 @@ impl OneRay {
}
}

impl Step {
pub fn relative_cube_face(self) -> CubeFace {
CubeFace {
cube: Cube::from(Point3D::from(self.relative_cube).to_i32()),
face: self.face.into(),
}
}
}

/// Used by `chart_data` type declarations to have compatible behavior when cross-compiling.
type TargetEndian<T> = T;
93 changes: 89 additions & 4 deletions all-is-cubes/src/space/light/chart_schema_shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,104 @@
//! In the future, this may be handled by using number types wrapped to be explicitly
//! target endianness.
use all_is_cubes_base::math::Face7;

// conditionally defined to be equal to f32 except in the build script
use super::TargetEndian;

/// Information about a single one of the bundle of light rays that we follow
/// from a struck block towards light sources.
#[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)`
/// Ray whose origin is within the [0,0,0]..[1,1,1] cube and direction
/// is a unit vector in the direction of this light ray.
pub ray: Ray,
/// `FaceMap` data which stores the cosine between each face normal and this ray.
pub face_cosines: [TargetEndian<f32>; 6],
}

/// The pre-computed sequence of cubes which is traversed by a [`OneRay`].
/// This format is used only while computing them.
#[derive(Clone, Debug)]
#[allow(dead_code)]
#[repr(C)]
pub(crate) struct Steps {
pub info: OneRay,
pub relative_cube_sequence: ::alloc::vec::Vec<Step>,
}

/// The pre-computed sequence of cubes which is traversed by a [`OneRay`].
/// This format is used in the static pre-computed data.
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub(crate) struct IndirectSteps {
pub info: OneRay,
/// Sequence of steps to take, specified by an inclusive-exclusive range of a slice of
/// separately stored [`Step`]s.
pub relative_cube_sequence: [usize; 2],
}

#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub(crate) struct Step {
/// Cube we just hit, relative to the origin of rays
/// (the block we're computing light for).
pub relative_cube: [TargetEndian<i8>; 3],

/// Distance from ray origin that has been traversed so far to strike this cube,
/// rounded up.
pub distance: u8,

/// Face struck.
pub face: Face7Safe,
}

#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub(crate) struct Ray {
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,
impl From<Ray> for all_is_cubes_base::raycast::Ray {
fn from(value: Ray) -> Self {
Self::new(value.origin.map(f64::from), value.direction.map(f64::from))
}
}

/// [`Face6`] but without an enum's validity invariant.
/// Panics on unsuccessful conversion.
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(transparent)]
pub(crate) struct Face7Safe(u8);
impl From<Face7Safe> for Face7 {
fn from(value: Face7Safe) -> Self {
match value.0 {
0 => Face7::Within,
1 => Face7::NX,
2 => Face7::NY,
3 => Face7::NZ,
4 => Face7::PX,
5 => Face7::PY,
6 => Face7::PZ,
_ => {
if cfg!(debug_assertions) {
panic!("invalid {value:?}");
} else {
// avoid generating a panic branch
Face7::Within
}
}
}
}
}
impl From<Face7> for Face7Safe {
fn from(value: Face7) -> Self {
Self(value as u8)
}
}

// Note: Most 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.
Loading

0 comments on commit 18caabe

Please sign in to comment.