From 18caabe05100ba0d81fe665965b79271953b718d Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Tue, 7 May 2024 13:35:45 -0700 Subject: [PATCH] Precompute light propagation data at build time instead of in `Space` 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. --- all-is-cubes/build.rs | 138 +++++++++++++++--- all-is-cubes/src/space/light/chart_schema.rs | 19 ++- .../src/space/light/chart_schema_shared.rs | 93 +++++++++++- all-is-cubes/src/space/light/rays.rs | 130 +++++++---------- all-is-cubes/src/space/light/updater.rs | 38 ++--- 5 files changed, 295 insertions(+), 123 deletions(-) diff --git a/all-is-cubes/build.rs b/all-is-cubes/build.rs index 04b7c2f7e..cae328036 100644 --- a/all-is-cubes/build.rs +++ b/all-is-cubes/build.rs @@ -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::(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 { let origin = FreePoint::new(0.5, 0.5, 0.5); @@ -46,7 +46,7 @@ fn generate_light_ray_pattern() -> Vec { cosines[face] = cosine; } - rays.push(OneRay::new(origin, direction, cosines)) + rays.push(OneRay::new(Ray::new(origin, direction), cosines)) } } } @@ -55,23 +55,92 @@ fn generate_light_ray_pattern() -> Vec { rays } -use chart_schema::OneRay; +/// Convert rays into their steps (sequence of cube intersections). +fn generate_light_propagation_chart(rays: &[OneRay]) -> Vec { + 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) { + // Repack data into two vectors instead of a vector of vectors. + let mut offset = 0; + let info: Vec = 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 + .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(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::(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) -> Self { + pub fn new(ray: Ray, 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(), + ray: ray.into(), face_cosines: [ face_cosines.nx, face_cosines.ny, @@ -84,6 +153,15 @@ mod chart_schema { } } + impl From 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)] @@ -106,4 +184,30 @@ mod chart_schema { ) } } + + // Orphan rules don't allow this as a generic impl directly + impl TargetEndian + where + ::Bytes: Copy + Clone + bytemuck::Pod + bytemuck::Zeroable, + T: FromBytes::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> for f32 { + fn from(value: TargetEndian) -> Self { + value.into_value() + } + } + impl From> for f64 { + fn from(value: TargetEndian) -> Self { + value.into_value() + } + } } diff --git a/all-is-cubes/src/space/light/chart_schema.rs b/all-is-cubes/src/space/light/chart_schema.rs index c3d19c83e..fffc5ad8a 100644 --- a/all-is-cubes/src/space/light/chart_schema.rs +++ b/all-is-cubes/src/space/light/chart_schema.rs @@ -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 { let [nx, ny, nz, px, py, pz] = self.face_cosines; crate::math::FaceMap { @@ -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; diff --git a/all-is-cubes/src/space/light/chart_schema_shared.rs b/all-is-cubes/src/space/light/chart_schema_shared.rs index 0dd5436d1..f2eef43ab 100644 --- a/all-is-cubes/src/space/light/chart_schema_shared.rs +++ b/all-is-cubes/src/space/light/chart_schema_shared.rs @@ -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; 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, +} + +/// 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; 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; 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, +impl From 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 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 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. diff --git a/all-is-cubes/src/space/light/rays.rs b/all-is-cubes/src/space/light/rays.rs index 3700c6bfc..9294a43dc 100644 --- a/all-is-cubes/src/space/light/rays.rs +++ b/all-is-cubes/src/space/light/rays.rs @@ -1,86 +1,68 @@ //! Types and data pertaining to the pattern of rays that are cast from a block to potential //! light sources. Used by the algorithms in [`crate::space::light::updater`]. -use alloc::vec::Vec; +use crate::space::light::chart_schema::{self, OneRay}; -use crate::math::{CubeFace, FaceMap}; -use crate::raycast::Ray; -use crate::space::light::chart_schema::OneRay; -use crate::space::LightPhysics; - -/// Derived from [`LightRayData`], but with a pre-calculated sequence of cubes instead of a ray -/// for maximum performance in the lighting calculation. -#[derive(Debug)] -pub(in crate::space) struct LightRayCubes { - /// For diagnostics only - pub ray: Ray, - pub relative_cube_sequence: Vec, - pub face_cosines: FaceMap, -} - -/// A raycast step pre-adapted. -#[derive(Debug)] -pub(in crate::space) struct LightRayStep { - /// Cube we just hit, relative to the origin of rays. - pub relative_cube_face: CubeFace, - /// Ray segment from the origin to the point where it struck the cube. - /// Used only for diagnostic purposes ("where did the rays go?"). - pub relative_ray_to_here: Ray, +/// Precalculated data about how light propagates through the cube grid, +/// used to traverse a `Space` to determine what light falls on a single block. +#[derive(Clone, Copy)] +pub(crate) struct LightChart { + info: &'static [chart_schema::IndirectSteps], + all_steps: &'static [chart_schema::Step], } -/// `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")), - }; +impl LightChart { + /// `bytemuck::cast_slice()` can't be const, so we have to write a function, + /// but this should all compile to a noop. + pub fn get() -> Self { + const INFO_BYTES_LEN: usize = + include_bytes!(concat!(env!("OUT_DIR"), "/light_chart_info.bin")).len(); + const STEPS_BYTES_LEN: usize = + include_bytes!(concat!(env!("OUT_DIR"), "/light_chart_steps.bin")).len(); - bytemuck::cast_slice::(&LIGHT_RAYS_BYTES.data) -} + // Ensure the data is sufficiently aligned + #[repr(C)] + struct AlignInfo { + _aligner: [chart_schema::IndirectSteps; 0], + data: [u8; INFO_BYTES_LEN], + } + #[repr(C)] + struct AlignStep { + _aligner: [chart_schema::Step; 0], + data: [u8; STEPS_BYTES_LEN], + } -/// Convert [`LIGHT_RAYS`] containing [`LightRayData`] into [`LightRayCubes`]. -#[inline(never)] // cold code shouldn't be duplicated -pub(in crate::space) fn calculate_propagation_table(physics: &LightPhysics) -> Vec { - // TODO: Save memory for the table by adding mirroring support, like ChunkChart does + static INFO_BYTES: AlignInfo = AlignInfo { + _aligner: [], + data: *include_bytes!(concat!(env!("OUT_DIR"), "/light_chart_info.bin")), + }; + static STEPS_BYTES: AlignStep = AlignStep { + _aligner: [], + data: *include_bytes!(concat!(env!("OUT_DIR"), "/light_chart_steps.bin")), + }; - match *physics { - LightPhysics::None => vec![], - // TODO: Instead of having a constant ray pattern, choose one that suits the - // maximum_distance. - LightPhysics::Rays { maximum_distance } => { - let maximum_distance = f64::from(maximum_distance); - light_rays_data() - .iter() - .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() + LightChart { + info: bytemuck::cast_slice(&INFO_BYTES.data), + all_steps: bytemuck::cast_slice(&STEPS_BYTES.data), } } + + pub fn rays( + self, + maximum_distance: u8, + ) -> impl Iterator)> { + self.info.iter().map(move |ist| { + let &chart_schema::IndirectSteps { + info, + relative_cube_sequence: [start, end], + } = ist; + ( + info, + self.all_steps[start..end] + .iter() + .filter(move |step| step.distance <= maximum_distance) + .copied(), + ) + }) + } } diff --git a/all-is-cubes/src/space/light/updater.rs b/all-is-cubes/src/space/light/updater.rs index 5c5b222bb..36151bc83 100644 --- a/all-is-cubes/src/space/light/updater.rs +++ b/all-is-cubes/src/space/light/updater.rs @@ -58,9 +58,6 @@ pub(crate) struct LightStorage { pub(in crate::space) sky: Sky, pub(in crate::space) block_sky: BlockSky, - - /// Pre-computed table of what adjacent blocks need to be consulted to update light. - propagation_table: Vec, } /// Methods on Space that specifically implement the lighting algorithm. @@ -78,7 +75,6 @@ impl LightStorage { physics: physics.light.clone(), sky: physics.sky.clone(), block_sky: physics.sky.for_blocks(), - propagation_table: rays::calculate_propagation_table(&physics.light), } } @@ -98,7 +94,6 @@ impl LightStorage { self.contents = self .physics .initialize_lighting(uc.contents.without_elements(), opacity); - self.propagation_table = rays::calculate_propagation_table(&self.physics); match self.physics { LightPhysics::None => { @@ -375,6 +370,11 @@ impl LightStorage { let mut cube_buffer = LightBuffer::new(); let mut info_rays = D::RayInfoBuffer::default(); + let maximum_distance = match self.physics { + LightPhysics::None => 0, + LightPhysics::Rays { maximum_distance } => maximum_distance, + }; + let ev_origin = uc.get_evaluated(cube); let origin_is_opaque = ev_origin.opaque == FaceMap::repeat(true); if origin_is_opaque { @@ -387,14 +387,11 @@ impl LightStorage { FaceMap::from_fn(|face| uc.get_evaluated(cube + face.normal_vector())); let direction_weights = directions_to_seek_light(ev_origin, ev_neighbors); - for &rays::LightRayCubes { - ray, - ref relative_cube_sequence, - face_cosines, - } in self.propagation_table.iter() + for (ray_info, relative_cube_sequence) in rays::LightChart::get().rays(maximum_distance) { // TODO: Theoretically we should weight light rays by the cosine but that has caused poor behavior in the past. - let ray_weight_by_faces = face_cosines + let ray_weight_by_faces = ray_info + .face_cosines() .zip(direction_weights, |_face, ray_cosine, reflects| { ray_cosine * reflects }) @@ -403,7 +400,8 @@ impl LightStorage { if ray_weight_by_faces <= 0.0 { continue; } - let mut ray_state = LightRayState::new(cube, ray, ray_weight_by_faces); + let mut ray_state = + LightRayState::new(cube, ray_info.ray.into(), ray_weight_by_faces); // Stores the light value that might have been fetched, if it was, from the previous // step's cube_ahead, which is the current step's cube_behind. @@ -411,7 +409,7 @@ impl LightStorage { 'raycast: for step in relative_cube_sequence { let cube_face = step - .relative_cube_face + .relative_cube_face() .translate(cube.lower_bounds().to_vector()); cube_buffer.cost += 1; @@ -425,9 +423,9 @@ impl LightStorage { &mut ray_state, &mut info_rays, self, - step.relative_cube_face + step.relative_cube_face() .translate(cube.lower_bounds().to_vector()), - step.relative_ray_to_here, + f64::from(step.distance), // TODO: this will be sloppy uc.get_evaluated(cube_face.cube), &mut light_ahead_cache, light_behind_cache, @@ -638,9 +636,7 @@ struct LightRayState { /// Weighting factor for how much this ray contributes to the total light. /// If zero, this will not be counted as a ray at all. ray_weight_by_faces: f32, - /// The cube we're lighting; remembered to check for loopbacks - origin_cube: Cube, - /// The ray we're casting; remembered for debugging only. (TODO: avoid this?) + /// The ray we're casting; remembered for debugging and sky light sampling. translated_ray: Ray, } @@ -659,7 +655,6 @@ impl LightRayState { LightRayState { alpha: 1.0, ray_weight_by_faces, - origin_cube, translated_ray, } } @@ -690,7 +685,7 @@ impl LightBuffer { info: &mut D::RayInfoBuffer, current_light: &LightStorage, hit: CubeFace, - relative_ray_to_here: Ray, + distance: f64, ev_hit: &EvaluatedBlock, light_ahead_cache: &mut Option, light_behind_cache: Option, @@ -764,8 +759,7 @@ impl LightBuffer { // Iff this is the hit that terminates the ray, record it. // TODO: Record transparency too. D::push_ray(info, || LightUpdateRayInfo { - ray: relative_ray_to_here - .translate(ray_state.origin_cube.lower_bounds().to_vector().to_f64()), + ray: ray_state.translated_ray.scale_direction(distance), trigger_cube: hit.cube, value_cube: light_cube, value: stored_light,