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
This commit is contained in:
Wito Wiala 2026-09-03 12:39:56 +01:00
parent 1b1344c393
commit 5038ed8270
4 changed files with 34 additions and 10 deletions

4
.cargo/config.toml Normal file
View file

@ -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"]

View file

@ -63,14 +63,22 @@ impl DiskShape {
if Float::from(di.z()) == 0. { if Float::from(di.z()) == 0. {
return None; return None;
} }
let t_shape_hit: Interval = (self.height - oi.z()) / di.z(); // pbrt computes the plane hit as a SCALAR:
if t_shape_hit.high <= 0. || t_shape_hit.low >= t_max { // 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; return None;
} }
let oi_f = Point3f::from(oi); let oi_f = Point3f::from(oi);
let di_f = Vector3f::from(di); 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 p_hit: Point3f = oi_f + di_f * t;
let dist2 = square(p_hit.x()) + square(p_hit.y()); let dist2 = square(p_hit.x()) + square(p_hit.y());

View file

@ -1,3 +1,4 @@
use crate::wavefront::integrator::RAYON_MIN_LEN;
use crate::globals::get_options; use crate::globals::get_options;
use log::debug; use log::debug;
use std::sync::atomic::{AtomicU32, Ordering}; 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); let n_rays = ray_q.size().min(max_rays as u32);
// Intersect ray with the scene and enqueue resulting work // 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 r = unsafe { ray_q.get(i) };
let Some(si) = self.aggregate.intersect(&r.ray, None) else { 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); 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 work = unsafe { shadow_ray_q.get(i) };
let ray = Ray::new(work.ray.o, work.ray.d, Some(work.ray.time), Ptr::null()); let ray = Ray::new(work.ray.o, work.ray.d, Some(work.ray.time), Ptr::null());

View file

@ -37,6 +37,10 @@ use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering}; 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_EVAL_ENTER: AtomicU32 = AtomicU32::new(0);
static DIAG_BSDF_EMPTY: AtomicU32 = AtomicU32::new(0); static DIAG_BSDF_EMPTY: AtomicU32 = AtomicU32::new(0);
static DIAG_NON_SPECULAR_SKIP: 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 filter = Ptr::from(&film.base().filter);
let light_sampler = create_light_sampler("power", &lights, arena); let light_sampler = create_light_sampler("power", &lights, arena);
let res_x = pixel_bounds.diagonal().x() as u32; 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 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 max_queue_size = res_x * scanlines_per_pass;
let mut infinite_lights: GVec<LightIdx> = gvec(); let mut infinite_lights: GVec<LightIdx> = gvec();
@ -323,6 +332,7 @@ impl CpuWavefrontRenderer {
// and every later kernel addresses state by this same absolute index. // and every later kernel addresses state by this same absolute index.
(0..self.max_queue_size as usize) (0..self.max_queue_size as usize)
.into_par_iter() .into_par_iter()
.with_min_len(RAYON_MIN_LEN)
.for_each(|pixel_index| { .for_each(|pixel_index| {
let p_pixel = Point2i::new( let p_pixel = Point2i::new(
pixel_bounds.p_min.x() + (pixel_index as i32 % x_resolution), 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 pixel_sample_state = &self.pixel_sample_state;
let escaped_ray_queue = &self.escaped_ray_queue; 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 w = unsafe { escaped_ray_queue.storage.get(i) };
let mut l_contrib = SampledSpectrum::new(0.0); let mut l_contrib = SampledSpectrum::new(0.0);
@ -425,7 +435,7 @@ impl CpuWavefrontRenderer {
let pixel_sample_state = &self.pixel_sample_state; let pixel_sample_state = &self.pixel_sample_state;
let hit_area_light_queue = &self.hit_area_light_queue; 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) }; let w = unsafe { hit_area_light_queue.get(i) };
if w.area_light.is_none() { if w.area_light.is_none() {
return; return;
@ -483,7 +493,7 @@ impl CpuWavefrontRenderer {
let next_ray_queue = &self.ray_queues[next]; let next_ray_queue = &self.ray_queues[next];
let regularize = self.regularize; 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) }; let w = unsafe { queue.storage.get(i) };
// Fires on every material-queue pass (reset each depth/batch/sample), so it // Fires on every material-queue pass (reset each depth/batch/sample), so it
// ran continuously and dominated render time. Gated behind cpu_debug. // 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) { fn update_film(&self, _y0: i32, _y1: i32, pixel_bounds: &Bounds2i) {
(0..self.max_queue_size as usize) (0..self.max_queue_size as usize)
.into_par_iter() .into_par_iter()
.with_min_len(RAYON_MIN_LEN)
.for_each(|pixel_index| { .for_each(|pixel_index| {
let p_pixel = self.pixel_sample_state.pixel.get(pixel_index); let p_pixel = self.pixel_sample_state.pixel.get(pixel_index);
if !pixel_bounds.contains_exclusive(p_pixel) { if !pixel_bounds.contains_exclusive(p_pixel) {
@ -749,7 +760,7 @@ impl CpuWavefrontRenderer {
let pixel_sample_state = &self.pixel_sample_state; let pixel_sample_state = &self.pixel_sample_state;
let sampler_proto = &self.sampler; 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 w = unsafe { ray_queue.storage.get(i) };
let dimension = 6 + 7 * w.depth; let dimension = 6 + 7 * w.depth;
let pi = w.pixel_index as usize; let pi = w.pixel_index as usize;