diff --git a/Cargo.lock b/Cargo.lock index b725a937..a5c80e6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,7 +293,7 @@ dependencies = [ ] [[package]] -name = "kdtree-tester" +name = "kdtree-tester-cli" version = "0.1.0" dependencies = [ "clap", diff --git a/Cargo.toml b/Cargo.toml index e5129680..864e1e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["geometry", "kdtree", "kdtree-cli", "kdtree-tester", "pathtracer-cli", "pathtracer-gui", "raytracer-cli", "scene", "tracing", "wavefront", "wavefront-cli"] +members = ["geometry", "kdtree", "kdtree-cli", "kdtree-tester-cli", "pathtracer-cli", "pathtracer-gui", "raytracer-cli", "scene", "tracing", "wavefront", "wavefront-cli"] resolver = "2" [profile.release] diff --git a/kdtree-tester/Cargo.toml b/kdtree-tester-cli/Cargo.toml similarity index 95% rename from kdtree-tester/Cargo.toml rename to kdtree-tester-cli/Cargo.toml index 0670afac..54428ad6 100644 --- a/kdtree-tester/Cargo.toml +++ b/kdtree-tester-cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "kdtree-tester" +name = "kdtree-tester-cli" version = "0.1.0" edition = "2021" diff --git a/kdtree-tester/src/checked_intersection.rs b/kdtree-tester-cli/src/checked_intersection.rs similarity index 100% rename from kdtree-tester/src/checked_intersection.rs rename to kdtree-tester-cli/src/checked_intersection.rs diff --git a/kdtree-tester/src/lib.rs b/kdtree-tester-cli/src/lib.rs similarity index 100% rename from kdtree-tester/src/lib.rs rename to kdtree-tester-cli/src/lib.rs diff --git a/kdtree-tester-cli/src/main.rs b/kdtree-tester-cli/src/main.rs new file mode 100644 index 00000000..eb1b4f19 --- /dev/null +++ b/kdtree-tester-cli/src/main.rs @@ -0,0 +1,102 @@ +use clap::{Parser, Subcommand}; +use kdtree::{sah::SahCost, MAX_DEPTH}; +use ray_tester::kdtree_ray_tester; +use reducer::kdtree_reduce; +use size::Size; + +mod checked_intersection; +mod ray_bouncer; +mod ray_tester; +mod reducer; +mod size; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Compare kdtree intersection with naive intersection + Test { + /// Wavefront OBJ input path + #[arg(short = 'i', long, required = true)] + input: std::path::PathBuf, + + /// Output ray fail binary data path + #[arg(short = 'o', long)] + output: Option, + /// Image size in pixels + #[arg(short, long, default_value_t = Size::new(512, 512))] + size: Size, + /// Max number of bounces to test + #[arg(short, long, default_value_t = 10)] + bounces: u32, + + /// Maximum kd-tree depth + #[arg(long, default_value_t = MAX_DEPTH as u32)] + max_depth: u32, + /// SAH kd-tree traverse cost + #[arg(long, default_value_t = SahCost::default().traverse_cost)] + traverse_cost: f32, + /// SAH kd-tree intersect cost + #[arg(long, default_value_t = SahCost::default().intersect_cost)] + intersect_cost: f32, + /// SAH kd-tree empty factor + #[arg(long, default_value_t = SahCost::default().empty_factor)] + empty_factor: f32, + }, + /// Reduce tree size for a specific intersection error + Reduce { + /// Wavefront OBJ input path + #[arg(short = 'i', long, required = true)] + input: std::path::PathBuf, + + /// Output reduced kd-tree JSON data path + #[arg(short = 'o', long, required = true)] + output: std::path::PathBuf, + + /// Output ray fail binary data path + #[arg(short = 'f', long)] + fail: Option, + + /// Seed for random generator used to shuffle input geometry + #[arg(short = 's', long, required = true)] + seed: u64, + }, +} + +fn main() { + let args = Cli::parse(); + match args.command { + Commands::Test { + input, + output, + size, + bounces, + max_depth, + traverse_cost, + intersect_cost, + empty_factor, + } => kdtree_ray_tester( + input, + output, + size, + bounces, + max_depth, + SahCost { + traverse_cost, + intersect_cost, + empty_factor, + }, + ), + Commands::Reduce { + input, + output, + fail, + seed, + } => kdtree_reduce(input, output, fail, seed), + } +} diff --git a/kdtree-tester/src/ray_bouncer.rs b/kdtree-tester-cli/src/ray_bouncer.rs similarity index 100% rename from kdtree-tester/src/ray_bouncer.rs rename to kdtree-tester-cli/src/ray_bouncer.rs diff --git a/kdtree-tester-cli/src/ray_tester.rs b/kdtree-tester-cli/src/ray_tester.rs new file mode 100644 index 00000000..c36725e3 --- /dev/null +++ b/kdtree-tester-cli/src/ray_tester.rs @@ -0,0 +1,67 @@ +use kdtree::{build::build_kdtree, sah::SahCost}; +use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; +use scene::{camera::Pinhole, Scene}; +use std::{ + fs::File, + io::{BufWriter, Write}, + path::PathBuf, +}; + +use crate::{ray_bouncer::RayBouncer, size::Size}; + +pub(crate) fn kdtree_ray_tester( + input: PathBuf, + output: Option, + size: Size, + bounces: u32, + max_depth: u32, + sah: SahCost, +) { + let scene = Scene::read_obj_file_with_print_logging(&input); + + println!("Building kdtree..."); + let kdtree = build_kdtree(&scene.geometries, max_depth, &sah); + + println!("Testing up to {} rays...", size.x * size.y * bounces); + let camera = Pinhole::new(scene.cameras[0].clone(), size.as_uvec2()); + let bouncer = RayBouncer { + scene, + kdtree, + camera, + size: size.as_uvec2(), + bounces, + }; + + let xs = 0..size.x; + let ys = 0..size.y; + let pixels = ys + .flat_map(|y| xs.clone().map(move |x| (x, y))) + .collect::>(); + let pixel_count = pixels.len(); + let fails = pixels + .into_par_iter() + .enumerate() + .filter_map(|(i, pixel)| { + let result = bouncer.bounce_pixel(pixel); + if let Some(fail) = &result { + eprintln!( + "Fail on pixel {} x {} ({} / {})", + pixel.0, pixel.1, i, pixel_count + ); + eprintln!(" {:?}", fail.ray); + eprintln!(" Expected: {:?}", fail.reference); + eprintln!(" Actual: {:?}", fail.kdtree); + } + result + }) + .collect::>(); + println!("Found {} fails", fails.len()); + + if let Some(path) = output { + println!("Writing failed rays to {:?}...", path); + let mut logger = BufWriter::new(File::create(path).unwrap()); + fails.iter().enumerate().for_each(|(i, fail)| { + logger.write_all(&fail.as_bytes(i as u16)).unwrap(); + }); + } +} diff --git a/kdtree-tester/src/bin/kdtree-reducer-cli.rs b/kdtree-tester-cli/src/reducer.rs similarity index 83% rename from kdtree-tester/src/bin/kdtree-reducer-cli.rs rename to kdtree-tester-cli/src/reducer.rs index 48aae323..6a2727e8 100644 --- a/kdtree-tester/src/bin/kdtree-reducer-cli.rs +++ b/kdtree-tester-cli/src/reducer.rs @@ -1,11 +1,10 @@ use std::{ fs::File, io::{BufReader, BufWriter, Write}, + path::PathBuf, time::Instant, }; -use clap::Parser; -use kdtree_tester::checked_intersection::CheckedIntersection; use rand::{rngs::SmallRng, seq::SliceRandom, SeedableRng}; use geometry::{geometry::Geometry, intersection::RayIntersection, ray::Ray, triangle::Triangle}; @@ -15,6 +14,8 @@ use kdtree::{ }; use wavefront::obj; +use crate::checked_intersection::CheckedIntersection; + fn build_test_tree(geometries: &[Geometry]) -> KdNode { build_kdtree(geometries, MAX_DEPTH as u32, &SahCost::default()) } @@ -101,29 +102,7 @@ fn reduce_tree( (geometries, tree) } -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct Args { - /// Wavefront OBJ input path - #[arg(short = 'i', long, required = true)] - input: std::path::PathBuf, - - /// Output reduced kd-tree JSON data path - #[arg(short = 'o', long, required = true)] - output: std::path::PathBuf, - - /// Output ray fail binary data path - #[arg(short = 'f', long)] - fail: Option, - - /// Seed for random generator used to shuffle input geometry - #[arg(short = 's', long, required = true)] - seed: u64, -} - -fn main() { - let args = Args::parse(); - +pub(crate) fn kdtree_reduce(input: PathBuf, output: PathBuf, fail: Option, seed: u64) { let intersection = CheckedIntersection { ray: Ray::new( [3.897963, 0.24242611, -4.203691].into(), @@ -146,14 +125,14 @@ fn main() { }, )), }; - eprintln!("Seed: {}", args.seed); + eprintln!("Seed: {}", seed); eprintln!("Testing with failed intersection:"); eprintln!(" {:?}", &intersection.ray); eprintln!(" Expected: {:?}", &intersection.reference); eprintln!(" Actual: {:?}", &intersection.kdtree); - eprintln!("Loading {}...", args.input.display()); - let obj = obj::obj(&mut BufReader::new(File::open(&args.input).unwrap())); + eprintln!("Loading {}...", input.display()); + let obj = obj::obj(&mut BufReader::new(File::open(&input).unwrap())); eprintln!(" Chunks: {}", obj.chunks.len()); eprintln!(" Vertices: {}", obj.vertices.len()); eprintln!(" Normals: {}", obj.normals.len()); @@ -182,7 +161,7 @@ fn main() { .collect::>(); eprintln!(" Geometries: {}", geometries.len()); - if let Some(path) = args.fail { + if let Some(path) = fail { eprintln!("Writing test ray to {:?}...", path); let file = File::create(path).unwrap(); let mut buf = BufWriter::new(file); @@ -190,11 +169,11 @@ fn main() { } eprintln!("Reducing tree..."); - let (geometries, tree) = reduce_tree(args.seed, intersection, geometries); + let (geometries, tree) = reduce_tree(seed, intersection, geometries); - eprintln!("Writing reduced tree to {:?}...", args.output); + eprintln!("Writing reduced tree to {:?}...", output); write_tree_json( - &mut BufWriter::new(File::create(args.output).unwrap()), + &mut BufWriter::new(File::create(output).unwrap()), &geometries, &tree, ) diff --git a/kdtree-tester-cli/src/size.rs b/kdtree-tester-cli/src/size.rs new file mode 100644 index 00000000..11c99f1b --- /dev/null +++ b/kdtree-tester-cli/src/size.rs @@ -0,0 +1,37 @@ +use std::{fmt::Display, str::FromStr}; + +use glam::UVec2; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct Size { + pub(crate) x: u32, + pub(crate) y: u32, +} + +impl Size { + pub(crate) fn new(x: u32, y: u32) -> Self { + Size { x, y } + } + + pub(crate) fn as_uvec2(self) -> UVec2 { + UVec2::new(self.x, self.y) + } +} + +impl FromStr for Size { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + let pos = s.find('x').expect("Could not parse"); + Ok(Size { + x: s[0..pos].parse()?, + y: s[pos + 1..].parse()?, + }) + } +} + +impl Display for Size { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}x{}", self.x, self.y) + } +} diff --git a/kdtree-tester/src/bin/kdtree-tester-cli.rs b/kdtree-tester/src/bin/kdtree-tester-cli.rs deleted file mode 100644 index b8ffffa2..00000000 --- a/kdtree-tester/src/bin/kdtree-tester-cli.rs +++ /dev/null @@ -1,139 +0,0 @@ -use clap::Parser; -use glam::UVec2; -use kdtree::{build::build_kdtree, sah::SahCost, MAX_DEPTH}; -use kdtree_tester::ray_bouncer::RayBouncer; -use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; -use scene::{camera::Pinhole, Scene}; -use std::{ - fmt::Display, - fs::File, - io::{BufWriter, Write}, - str::{self, FromStr}, -}; - -#[derive(Clone, Copy, Debug)] -struct Size { - x: u32, - y: u32, -} - -impl Size { - fn new(x: u32, y: u32) -> Self { - Size { x, y } - } - - fn as_uvec2(self) -> UVec2 { - UVec2::new(self.x, self.y) - } -} - -impl FromStr for Size { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - let pos = s.find('x').expect("Could not parse"); - Ok(Size { - x: s[0..pos].parse()?, - y: s[pos + 1..].parse()?, - }) - } -} - -impl Display for Size { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}x{}", self.x, self.y) - } -} - -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct Args { - /// Wavefront OBJ input path - #[arg(short = 'i', long, required = true)] - input: std::path::PathBuf, - - /// Output ray fail binary data path - #[arg(short = 'o', long)] - output: Option, - /// Image size in pixels - #[arg(short, long, default_value_t = Size::new(512, 512))] - size: Size, - /// Max number of bounces to test - #[arg(short, long, default_value_t = 10)] - bounces: u32, - - /// Maximum kd-tree depth - #[arg(long, default_value_t = MAX_DEPTH as u32)] - max_depth: u32, - /// SAH kd-tree traverse cost - #[arg(long, default_value_t = SahCost::default().traverse_cost)] - traverse_cost: f32, - /// SAH kd-tree intersect cost - #[arg(long, default_value_t = SahCost::default().intersect_cost)] - intersect_cost: f32, - /// SAH kd-tree empty factor - #[arg(long, default_value_t = SahCost::default().empty_factor)] - empty_factor: f32, -} - -fn main() { - let args = Args::parse(); - let scene = Scene::read_obj_file_with_print_logging(&args.input); - - println!("Building kdtree..."); - let kdtree = build_kdtree( - &scene.geometries, - args.max_depth, - &SahCost { - traverse_cost: args.traverse_cost, - intersect_cost: args.intersect_cost, - empty_factor: args.empty_factor, - }, - ); - - println!( - "Testing up to {} rays...", - args.size.x * args.size.y * args.bounces - ); - let camera = Pinhole::new(scene.cameras[0].clone(), args.size.as_uvec2()); - let bouncer = RayBouncer { - scene, - kdtree, - camera, - size: args.size.as_uvec2(), - bounces: args.bounces, - }; - - let xs = 0..args.size.x; - let ys = 0..args.size.y; - let pixels = ys - .flat_map(|y| xs.clone().map(move |x| (x, y))) - .collect::>(); - let pixel_count = pixels.len(); - let fails = pixels - .into_par_iter() - .enumerate() - .filter_map(|(i, pixel)| { - let result = bouncer.bounce_pixel(pixel); - if let Some(fail) = &result { - eprintln!( - "Fail on pixel {} x {} ({} / {})", - pixel.0, pixel.1, i, pixel_count - ); - eprintln!(" {:?}", fail.ray); - eprintln!(" Expected: {:?}", fail.reference); - eprintln!(" Actual: {:?}", fail.kdtree); - } - result - }) - .collect::>(); - println!("Found {} fails", fails.len()); - - if let Some(path) = args.output { - println!("Writing failed rays to {:?}...", path); - let mut logger = BufWriter::new(File::create(path).unwrap()); - fails.iter().enumerate().for_each(|(i, fail)| { - logger.write_all(&fail.as_bytes(i as u16)).unwrap(); - }); - } -}