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,