From 5038ed8270d3adab9ce5fb978a580286a538ff4c Mon Sep 17 00:00:00 2001 From: Wito Wiala Date: Thu, 3 Sep 2026 12:39:56 +0100 Subject: [PATCH] basic_intersect for DiskShape computed the plane hit with interval arithmetic where pbrt uses a scalar (shapes.h: Float tShapeHit = (height - Float(oi.z)) / Float(di.z)). It produced NaN, and since every NaN comparison is false neither the t range test nor the subsequent dist2 > radius^2 test could reject it. The disk reported a hit on every ray tested against it, and the NaN reached dpdu/ dpdv, the shading normal, and the area-light MIS pdf. NaN is absorbing in the film, so one bad sample poisoned pixel RgbSum permantenly. Wavefront now works --- .cargo/config.toml | 4 ++++ shared/src/shapes/disk.rs | 14 +++++++++++--- src/wavefront/aggregate.rs | 5 +++-- src/wavefront/integrator.rs | 21 ++++++++++++++++----- 4 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..e7ccc98 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[build] +# Enable the host's AVX2/FMA. Measured ~12% on the CPU wavefront (13700H). +# NOTE: pins binaries to this CPU family. Use "x86-64-v3" if you ship them. +rustflags = ["-C", "target-cpu=native"] diff --git a/shared/src/shapes/disk.rs b/shared/src/shapes/disk.rs index 034f803..5c6b97b 100644 --- a/shared/src/shapes/disk.rs +++ b/shared/src/shapes/disk.rs @@ -63,14 +63,22 @@ impl DiskShape { if Float::from(di.z()) == 0. { return None; } - let t_shape_hit: Interval = (self.height - oi.z()) / di.z(); - if t_shape_hit.high <= 0. || t_shape_hit.low >= t_max { + // pbrt computes the plane hit as a SCALAR: + // Float tShapeHit = (height - Float(oi.z)) / Float(di.z); + // if (tShapeHit <= 0 || tShapeHit >= tMax) return {}; + // Interval arithmetic here produced NaN, and neither `high <= 0` nor + // `low >= t_max` rejects a NaN (every NaN comparison is false). The NaN then + // flowed into p_hit -- so the `dist2 > radius^2` test could not reject it either, + // and the disk swallowed every ray it was tested against -- and on into dpdu/dpdv + // and the shading normal, poisoning the area-light MIS pdf. + let t_shape_hit: Float = (self.height - Float::from(oi.z())) / Float::from(di.z()); + if t_shape_hit <= 0. || t_shape_hit >= t_max { return None; } let oi_f = Point3f::from(oi); let di_f = Vector3f::from(di); - let t = Float::from(t_shape_hit); + let t = t_shape_hit; let p_hit: Point3f = oi_f + di_f * t; let dist2 = square(p_hit.x()) + square(p_hit.y()); diff --git a/src/wavefront/aggregate.rs b/src/wavefront/aggregate.rs index 31366d7..bbc3f0c 100644 --- a/src/wavefront/aggregate.rs +++ b/src/wavefront/aggregate.rs @@ -1,3 +1,4 @@ +use crate::wavefront::integrator::RAYON_MIN_LEN; use crate::globals::get_options; use log::debug; use std::sync::atomic::{AtomicU32, Ordering}; @@ -51,7 +52,7 @@ impl WavefrontAggregate for CpuAggregate { let n_rays = ray_q.size().min(max_rays as u32); // Intersect ray with the scene and enqueue resulting work - (0..n_rays as usize).into_par_iter().for_each(|i| { + (0..n_rays as usize).into_par_iter().with_min_len(RAYON_MIN_LEN).for_each(|i| { let r = unsafe { ray_q.get(i) }; let Some(si) = self.aggregate.intersect(&r.ray, None) else { @@ -172,7 +173,7 @@ impl WavefrontAggregate for CpuAggregate { ) { let n_rays = shadow_ray_q.size().min(max_rays as u32); - (0..n_rays as usize).into_par_iter().for_each(|i| { + (0..n_rays as usize).into_par_iter().with_min_len(RAYON_MIN_LEN).for_each(|i| { let work = unsafe { shadow_ray_q.get(i) }; let ray = Ray::new(work.ray.o, work.ray.d, Some(work.ray.time), Ptr::null()); diff --git a/src/wavefront/integrator.rs b/src/wavefront/integrator.rs index 099c40e..c880ba9 100644 --- a/src/wavefront/integrator.rs +++ b/src/wavefront/integrator.rs @@ -37,6 +37,10 @@ use std::ops::{Deref, DerefMut}; use std::sync::Arc; use std::sync::atomic::{AtomicU32, Ordering}; +/// Minimum rayon chunk. The per-item bodies are short enough that splitting all the way +/// down costs more in work-stealing than it recovers in balance (~3x on this scene). +pub(crate) const RAYON_MIN_LEN: usize = 64; + static DIAG_EVAL_ENTER: AtomicU32 = AtomicU32::new(0); static DIAG_BSDF_EMPTY: AtomicU32 = AtomicU32::new(0); static DIAG_NON_SPECULAR_SKIP: AtomicU32 = AtomicU32::new(0); @@ -90,8 +94,13 @@ where let filter = Ptr::from(&film.base().filter); let light_sampler = create_light_sampler("power", &lights, arena); let res_x = pixel_bounds.diagonal().x() as u32; + // pbrt (wavefront/integrator.cpp:231) rebalances so the last pass isn't mostly + // empty; without it every queue is oversized. + let res_y = pixel_bounds.diagonal().y() as u32; let max_samples = 1024u32 * 1024; - let scanlines_per_pass = (max_samples / res_x).max(1); + let mut scanlines_per_pass = (max_samples / res_x).max(1); + let n_passes = res_y.div_ceil(scanlines_per_pass).max(1); + scanlines_per_pass = res_y.div_ceil(n_passes).max(1); let max_queue_size = res_x * scanlines_per_pass; let mut infinite_lights: GVec = gvec(); @@ -323,6 +332,7 @@ impl CpuWavefrontRenderer { // and every later kernel addresses state by this same absolute index. (0..self.max_queue_size as usize) .into_par_iter() + .with_min_len(RAYON_MIN_LEN) .for_each(|pixel_index| { let p_pixel = Point2i::new( pixel_bounds.p_min.x() + (pixel_index as i32 % x_resolution), @@ -386,7 +396,7 @@ impl CpuWavefrontRenderer { let pixel_sample_state = &self.pixel_sample_state; let escaped_ray_queue = &self.escaped_ray_queue; - (0..n as usize).into_par_iter().for_each(|i| { + (0..n as usize).into_par_iter().with_min_len(RAYON_MIN_LEN).for_each(|i| { let w = unsafe { escaped_ray_queue.storage.get(i) }; let mut l_contrib = SampledSpectrum::new(0.0); @@ -425,7 +435,7 @@ impl CpuWavefrontRenderer { let pixel_sample_state = &self.pixel_sample_state; let hit_area_light_queue = &self.hit_area_light_queue; - (0..n as usize).into_par_iter().for_each(|i| { + (0..n as usize).into_par_iter().with_min_len(RAYON_MIN_LEN).for_each(|i| { let w = unsafe { hit_area_light_queue.get(i) }; if w.area_light.is_none() { return; @@ -483,7 +493,7 @@ impl CpuWavefrontRenderer { let next_ray_queue = &self.ray_queues[next]; let regularize = self.regularize; - (0..n as usize).into_par_iter().for_each(|i| { + (0..n as usize).into_par_iter().with_min_len(RAYON_MIN_LEN).for_each(|i| { let w = unsafe { queue.storage.get(i) }; // Fires on every material-queue pass (reset each depth/batch/sample), so it // ran continuously and dominated render time. Gated behind cpu_debug. @@ -726,6 +736,7 @@ impl CpuWavefrontRenderer { fn update_film(&self, _y0: i32, _y1: i32, pixel_bounds: &Bounds2i) { (0..self.max_queue_size as usize) .into_par_iter() + .with_min_len(RAYON_MIN_LEN) .for_each(|pixel_index| { let p_pixel = self.pixel_sample_state.pixel.get(pixel_index); if !pixel_bounds.contains_exclusive(p_pixel) { @@ -749,7 +760,7 @@ impl CpuWavefrontRenderer { let pixel_sample_state = &self.pixel_sample_state; let sampler_proto = &self.sampler; - (0..n as usize).into_par_iter().for_each(|i| { + (0..n as usize).into_par_iter().with_min_len(RAYON_MIN_LEN).for_each(|i| { let w = unsafe { ray_queue.storage.get(i) }; let dimension = 6 + 7 * w.depth; let pi = w.pixel_index as usize;