#![allow(clippy::float_cmp)] #![allow(clippy::inline_always)] #![allow(clippy::many_single_char_names)] #![allow(clippy::needless_lifetimes)] #![allow(clippy::needless_return)] #![allow(clippy::or_fun_call)] #![allow(clippy::too_many_arguments)] #![allow(clippy::redundant_field_names)] #![allow(clippy::enum_variant_names)] #![allow(clippy::cast_lossless)] #![allow(clippy::needless_range_loop)] #![allow(clippy::excessive_precision)] #![allow(clippy::transmute_ptr_to_ptr)] extern crate lazy_static; mod accel; mod algorithm; mod bbox; mod bbox4; mod boundable; mod camera; mod color; mod fp_utils; mod image; mod lerp; mod light; mod math; mod mis; mod parse; mod ray; mod renderer; mod sampling; mod scene; mod scramble; mod shading; mod space_fill; mod surface; mod timer; mod tracer; // mod transform_stack; use std::{fs::File, io, io::Read, mem, path::Path, str::FromStr}; use clap::{App, Arg}; use nom::bytes::complete::take_until; use kioku::Arena; use crate::{ accel::BVH4Node, bbox::BBox, parse::{parse_scene, DataTree}, surface::SurfaceIntersection, timer::Timer, }; const VERSION: &str = env!("CARGO_PKG_VERSION"); #[allow(clippy::cognitive_complexity)] fn main() { let mut t = Timer::new(); // Parse command line arguments. let args = App::new("Psychopath") .version(VERSION) .about("A slightly psychotic path tracer") .arg( Arg::with_name("input") .short("i") .long("input") .value_name("FILE") .help("Input .psy file") .takes_value(true) .required_unless_one(&["dev", "use_stdin"]), ) .arg( Arg::with_name("spp") .short("s") .long("spp") .value_name("N") .help("Number of samples per pixel") .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("bucket_size") .short("b") .long("bucket_size") .value_name("N") .help("Height and width of each render bucket in pixels.") .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("crop") .long("crop") .value_name("X1 Y1 X2 Y2") .help( "Only render the image between pixel coordinates (X1, Y1) \ and (X2, Y2). Coordinates are zero-indexed and inclusive.", ) .takes_value(true) .number_of_values(4) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be four integers".to_string())) }), ) .arg( Arg::with_name("threads") .short("t") .long("threads") .value_name("N") .help( "Number of threads to render with. Defaults to the number of logical \ cores on the system.", ) .takes_value(true) .validator(|s| { usize::from_str(&s) .and(Ok(())) .or(Err("must be an integer".to_string())) }), ) .arg( Arg::with_name("stats") .long("stats") .help("Print additional statistics about rendering"), ) .arg( Arg::with_name("dev") .long("dev") .help("Show useful dev/debug info."), ) .arg( Arg::with_name("serialized_output") .long("serialized_output") .help("Serialize and send render output to standard output.") .hidden(true), ) .arg( Arg::with_name("use_stdin") .long("use_stdin") .help("Take scene file in from stdin instead of a file path.") .hidden(true), ) .get_matches(); // Print some misc useful dev info. if args.is_present("dev") { println!( "SurfaceIntersection size: {} bytes", mem::size_of::() ); println!("BBox size: {} bytes", mem::size_of::()); // println!("BVHNode size: {} bytes", mem::size_of::()); println!("BVH4Node size: {} bytes", mem::size_of::()); return; } let crop = args.values_of("crop").map(|mut vals| { let coords = ( u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), u32::from_str(vals.next().unwrap()).unwrap(), ); if coords.0 > coords.2 { panic!("Argument '--crop': X1 must be less than or equal to X2"); } if coords.1 > coords.3 { panic!("Argument '--crop': Y1 must be less than or equal to Y2"); } coords }); // Parse data tree of scene file if !args.is_present("serialized_output") { println!("Parsing scene file...",); } t.tick(); let psy_contents = if args.is_present("use_stdin") { // Read from stdin let mut input = Vec::new(); let tmp = std::io::stdin(); let mut stdin = tmp.lock(); let mut buf = vec![0u8; 4096]; loop { let count = stdin .read(&mut buf) .expect("Unexpected end of scene input."); let start = if input.len() < 11 { 0 } else { input.len() - 11 }; let end = input.len() + count; input.extend(&buf[..count]); let mut done = false; let mut trunc_len = 0; if let nom::IResult::Ok((remaining, _)) = take_until::<&str, &[u8], ()>("__PSY_EOF__")(&input[start..end]) { done = true; trunc_len = input.len() - remaining.len(); } if done { input.truncate(trunc_len); break; } } String::from_utf8(input).unwrap() } else { // Read from file let mut input = String::new(); let fp = args.value_of("input").unwrap(); let mut f = io::BufReader::new(File::open(fp).unwrap()); let _ = f.read_to_string(&mut input); input }; let dt = DataTree::from_str(&psy_contents).unwrap(); if !args.is_present("serialized_output") { println!("\tParsed scene file in {:.3}s", t.tick()); } // Iterate through scenes and render them if let DataTree::Internal { ref children, .. } = dt { for child in children { t.tick(); if child.type_name() == "Scene" { if !args.is_present("serialized_output") { println!("Building scene..."); } let arena = Arena::new().with_block_size((1 << 20) * 4); let mut r = parse_scene(&arena, child).unwrap_or_else(|e| { e.print(&psy_contents); panic!("Parse error."); }); if let Some(spp) = args.value_of("spp") { if !args.is_present("serialized_output") { println!("\tOverriding scene spp: {}", spp); } r.spp = usize::from_str(spp).unwrap(); } let bucket_size = if let Some(bucket_size) = args.value_of("bucket_size") { u32::from_str(bucket_size).unwrap() } else { 32 }; let thread_count = if let Some(threads) = args.value_of("threads") { u32::from_str(threads).unwrap() } else { num_cpus::get() as u32 }; if !args.is_present("serialized_output") { println!("\tBuilt scene in {:.3}s", t.tick()); } if !args.is_present("serialized_output") { println!("Rendering scene with {} threads...", thread_count); } let (mut image, rstats) = r.render( bucket_size, crop, thread_count, args.is_present("serialized_output"), ); // Print render stats if !args.is_present("serialized_output") { let rtime = t.tick(); println!("\tRendered scene in {:.3}s", rtime); println!("\t\t\tRays traced: {}", rstats.ray_count); println!("\t\t\tRay/node tests: {}", rstats.accel_node_visits); } // Write to disk if !args.is_present("serialized_output") { println!("Writing image to disk into '{}'...", r.output_file); if r.output_file.ends_with(".png") { image .write_png(Path::new(&r.output_file)) .expect("Failed to write png..."); } else if r.output_file.ends_with(".exr") { image.write_exr(Path::new(&r.output_file)); } else { panic!("Unknown output file extension."); } println!("\tWrote image in {:.3}s", t.tick()); } // Print memory stats if stats are wanted. if args.is_present("stats") { // let arena_stats = arena.stats(); // let mib_occupied = arena_stats.0 as f64 / 1_048_576.0; // let mib_allocated = arena_stats.1 as f64 / 1_048_576.0; // println!("MemArena stats:"); // if mib_occupied >= 1.0 { // println!("\tOccupied: {:.1} MiB", mib_occupied); // } else { // println!("\tOccupied: {:.4} MiB", mib_occupied); // } // if mib_allocated >= 1.0 { // println!("\tUsed: {:.1} MiB", mib_allocated); // } else { // println!("\tUsed: {:.4} MiB", mib_allocated); // } // println!("\tTotal blocks: {}", arena_stats.2); } } } } // End with blank line println!(); }