diff --git a/.gitignore b/.gitignore index 6a58583c..5ebd9b58 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ Cargo.lock # some scene files are not checked into version control local_scenes + +# profiling +profile.json diff --git a/Cargo.toml b/Cargo.toml index 578be356..e6a98435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,7 @@ members = ["clovers", "clovers-cli"] [profile.release] codegen-units = 1 lto = "fat" + +[profile.profiling] +inherits = "release" +debug = true diff --git a/Justfile b/Justfile index 0fa31ec7..43b44fc2 100644 --- a/Justfile +++ b/Justfile @@ -21,3 +21,8 @@ all-scenes: for scene in $(ls scenes/ |grep json); \ do just cli -s 1 --input scenes/$scene --output renders/$DATE/${scene%.json}.png; \ done; + +# Profiling helper +profile *ARGS: + cargo build --profile profiling; \ + samply record -- ./target/profiling/clovers-cli {{ARGS}} diff --git a/clovers-cli/Cargo.toml b/clovers-cli/Cargo.toml index c8240161..04546764 100644 --- a/clovers-cli/Cargo.toml +++ b/clovers-cli/Cargo.toml @@ -18,12 +18,12 @@ clovers = { path = "../clovers", features = [ ], default-features = false } # External -clap = { version = "4.4.1", features = ["std", "derive"] } +clap = { version = "4.4.7", features = ["std", "derive"] } human_format = "1.0.3" humantime = "2.1.0" image = { version = "0.24.7", features = ["png"], default-features = false } img-parts = "0.3.0" -indicatif = { version = "0.17.6", features = [ +indicatif = { version = "0.17.7", features = [ "rayon", ], default-features = false } palette = { version = "0.7.3", features = ["serializing"] } @@ -31,9 +31,9 @@ rand = { version = "0.8.5", features = [ "small_rng", "getrandom", ], default-features = false } -rayon = "1.7.0" -serde = { version = "1.0.188", features = ["derive"], default-features = false } +rayon = "1.8.0" +serde = { version = "1.0.190", features = ["derive"], default-features = false } serde_json = { version = "1.0", features = ["alloc"], default-features = false } -time = { version = "0.3.28", default-features = false } -tracing = "0.1.37" +time = { version = "0.3.30", default-features = false } +tracing = "0.1.40" tracing-subscriber = { version = "0.3.17", features = ["time"] } diff --git a/clovers-cli/src/draw_cpu.rs b/clovers-cli/src/draw_cpu.rs index 389ab6d1..7b5af8cb 100644 --- a/clovers-cli/src/draw_cpu.rs +++ b/clovers-cli/src/draw_cpu.rs @@ -9,72 +9,80 @@ use rand::rngs::SmallRng; use rand::{Rng, SeedableRng}; use rayon::prelude::*; use scenes::Scene; -use std::time::Duration; /// The main drawing function, returns a `Vec` as a pixelbuffer. pub fn draw(opts: RenderOpts, scene: &Scene) -> Vec> { - // Progress bar - let pixels = (opts.width * opts.height) as u64; - let bar = ProgressBar::new(pixels); + let width = opts.width as usize; + let height = opts.height as usize; + let bar = progress_bar(&opts); - if opts.quiet { - bar.set_draw_target(ProgressDrawTarget::hidden()) - } else { - bar.set_style(ProgressStyle::default_bar().template( - "Elapsed: {elapsed_precise}\nPixels: {bar} {pos}/{len}\nETA: {eta_precise}", - ).unwrap()); - bar.enable_steady_tick(Duration::from_millis(100)); - } - - let black: Srgb = Srgb::new(0, 0, 0); - let mut pixelbuffer = vec![black; pixels as usize]; - - pixelbuffer - .par_iter_mut() - .enumerate() - .for_each(|(index, pixel)| { - // Enumerate gives us an usize, opts.width and opts.height are u32 - // Most internal functions expect a Float, perform conversions - let x = (index % (opts.width as usize)) as Float; - let y = (index / (opts.width as usize)) as Float; - let width = opts.width as Float; - let height = opts.height as Float; - - // Initialize a thread-local random number generator + let pixelbuffer: Vec> = (0..height) + .into_par_iter() + .map(|row_index| { let mut rng = SmallRng::from_entropy(); - - // Initialize a mutable base color for the pixel - let mut color: LinSrgb = LinSrgb::new(0.0, 0.0, 0.0); - - if opts.normalmap { - // If we are rendering just a normalmap, make it quick and early return - let u = x / width; - let v = y / height; - let ray: Ray = scene.camera.get_ray(u, v, &mut rng); - color = normal_map(&ray, scene, &mut rng); - let color: Srgb = color.into_color(); - *pixel = color.into_format(); - return; - } - // Otherwise, do a regular render - - // Multisampling for antialiasing - for _sample in 0..opts.samples { - if let Some(s) = sample(scene, x, y, width, height, &mut rng, opts.max_depth) { - color += s + let mut row = Vec::with_capacity(width); + for index in 0..width { + let index = index + row_index * width; + if opts.normalmap { + row.push(render_pixel_normalmap(scene, &opts, index, &mut rng)); + } else { + row.push(render_pixel(scene, &opts, index, &mut rng)); } } - color /= opts.samples as Float; - // Gamma / component transfer function - let color: Srgb = color.into_color(); - *pixel = color.into_format(); - bar.inc(1); - }); + row + }) + .flatten() + .collect(); pixelbuffer } +// Render a single pixel, including possible multisampling +fn render_pixel(scene: &Scene, opts: &RenderOpts, index: usize, rng: &mut SmallRng) -> Srgb { + let (x, y, width, height) = index_to_params(opts, index); + let mut color: LinSrgb = LinSrgb::new(0.0, 0.0, 0.0); + for _sample in 0..opts.samples { + if let Some(s) = sample(scene, x, y, width, height, rng, opts.max_depth) { + color += s + } + } + color /= opts.samples as Float; + let color: Srgb = color.into_color(); + let color: Srgb = color.into_format(); + color +} + +// Render a single pixel in normalmap mode +fn render_pixel_normalmap( + scene: &Scene, + opts: &RenderOpts, + index: usize, + rng: &mut SmallRng, +) -> Srgb { + let (x, y, width, height) = index_to_params(opts, index); + let color: LinSrgb = sample_normalmap(scene, x, y, width, height, rng); + let color: Srgb = color.into_color(); + let color: Srgb = color.into_format(); + color +} + +// Get a single sample for a single pixel in the scene, normalmap mode. +fn sample_normalmap( + scene: &Scene, + x: Float, + y: Float, + width: Float, + height: Float, + rng: &mut SmallRng, +) -> LinSrgb { + let u = x / width; + let v = y / height; + let ray: Ray = scene.camera.get_ray(u, v, rng); + let color = normal_map(&ray, scene, rng); + color.into_color() +} + /// Get a single sample for a single pixel in the scene. Has slight jitter for antialiasing when multisampling. fn sample( scene: &Scene, @@ -88,11 +96,31 @@ fn sample( let u = (x + rng.gen::()) / width; let v = (y + rng.gen::()) / height; let ray: Ray = scene.camera.get_ray(u, v, rng); - let new_color: Xyz = colorize(&ray, scene, 0, max_depth, rng); - let new_color: Xyz = new_color.adapt_into(); - let new_color: LinSrgb = new_color.into_color_unclamped(); - if new_color.red.is_finite() && new_color.green.is_finite() && new_color.blue.is_finite() { - return Some(new_color); + let color: Xyz = colorize(&ray, scene, 0, max_depth, rng); + let color: Xyz = color.adapt_into(); + let color: LinSrgb = color.into_color_unclamped(); + if color.red.is_finite() && color.green.is_finite() && color.blue.is_finite() { + return Some(color); } None } + +fn index_to_params(opts: &RenderOpts, index: usize) -> (Float, Float, Float, Float) { + let x = (index % (opts.width as usize)) as Float; + let y = (index / (opts.width as usize)) as Float; + let width = opts.width as Float; + let height = opts.height as Float; + (x, y, width, height) +} + +fn progress_bar(opts: &RenderOpts) -> ProgressBar { + let bar = ProgressBar::new(opts.height as u64); + if opts.quiet { + bar.set_draw_target(ProgressDrawTarget::hidden()) + } else { + bar.set_style(ProgressStyle::default_bar().template( + "Elapsed: {elapsed_precise}\nRows: {bar} {pos}/{len}\nRemaining: {eta_precise}", + ).unwrap()); + } + bar +} diff --git a/clovers/Cargo.toml b/clovers/Cargo.toml index 86ae4580..08caecbe 100644 --- a/clovers/Cargo.toml +++ b/clovers/Cargo.toml @@ -20,15 +20,15 @@ traces = ["tracing"] [dependencies] enum_dispatch = "0.3.12" gltf = { version = "1.3.0", optional = true } -nalgebra = { version = "0.32.3", features = ["libm"], default-features = false } +nalgebra = { version = "0.32.3" } palette = { version = "0.7.3", features = ["serializing"] } rand = { version = "0.8.5", features = ["small_rng"], default-features = false } rand_distr = "0.4.3" -serde = { version = "1.0.188", features = [ +serde = { version = "1.0.190", features = [ "derive", ], default-features = false, optional = true } stl_io = { version = "0.7.0", optional = true } -tracing = { version = "0.1.37", optional = true } +tracing = { version = "0.1.40", optional = true } [dev-dependencies] criterion = "0.4.0"