Release Notes for v0.8.1

It's 2020 Now

For the last release I forgot to change the copyright notice, that's fixed now:

$ ./target/release/rs_pbrt assets/scenes/cornell_box.pbrt 
pbrt version 0.8.1 [Detected 28 cores]
Copyright (c) 2016-2020 Jan Douglas Bert Walter.
Rust code based on C++ code by Matt Pharr, Greg Humphreys, and Wenzel Jakob.
...

Files Changed

36 files changed, 354 insertions(+), 393 deletions(-)
...
deleted    examples/shapes_sphere_intersect.rs
...

SurfaceInteraction

The biggest change of this release is about the struct SurfaceInteraction.

modified   src/core/interaction.rs
@@ -274,7 +274,7 @@ pub struct SurfaceInteraction<'a> {
     pub dvdx: Cell<Float>,
     pub dudy: Cell<Float>,
     pub dvdy: Cell<Float>,
-    pub primitive: Option<&'a Primitive>,
+    pub primitive: Option<*const Primitive>,
     pub shading: Shading,
     pub bsdf: Option<Bsdf>,
     pub bssrdf: Option<TabulatedBssrdf>,

The change is less about the struct itself (only the optional pointer to a Primitive changed), but rather when the memory for an instance of the struct is allocated and how it is passed to other parts of the code to be either modified or simply used.

modified   src/shapes/triangle.rs
@@ -148,7 +147,7 @@ impl Triangle {
             self.mesh.p[self.mesh.vertex_indices[(self.id * 3) as usize + 2] as usize];
         bnd3_union_pnt3(&Bounds3f::new(p0, p1), &p2)
     }
-    pub fn intersect(&self, ray: &Ray) -> Option<(Rc<SurfaceInteraction>, Float)> {
+    pub fn intersect(&self, ray: &Ray, t_hit: &mut Float, isect: &mut SurfaceInteraction) -> bool {
         // get triangle vertices in _p0_, _p1_, and _p2_
         let p0: &Point3f = &self.mesh.p[self.mesh.vertex_indices[(self.id * 3) as usize] as usize];
         let p1: &Point3f =

In the past the memory was allocated e.g. during a Triangle intersection with a Ray, now we follow more closely the C++ code, where the memory is allocated once e.g. before we ask for an intersection with the Scene and possibly re-used many times instead of re-allocating another SurfaceInteraction struct.

You can see the difference between v0.8.0 and v0.8.1 easily by running heaptrack.

Ambient Occlusion

modified   src/integrators/ao.rs
@@ -65,9 +64,10 @@ impl AOIntegrator {
             differential: r.differential,
             medium: r.medium.clone(),
         };
-        if let Some(mut isect) = scene.intersect(&mut ray) {
+        let mut isect: SurfaceInteraction = SurfaceInteraction::default();
+        if scene.intersect(&mut ray, &mut isect) {
             let mode: TransportMode = TransportMode::Radiance;
-            Rc::get_mut(&mut isect).unwrap().compute_scattering_functions(&ray, true, mode);
+            isect.compute_scattering_functions(&ray, true, mode);
             // if (!isect.bsdf) {
             //     VLOG(2) << "Skipping intersection due to null bsdf";
             //     ray = isect.SpawnRay(ray.d);

For Ambient Occlusion you can see the effect between version 0.8.0 (left side) and 0.8.1 (right side) basically on the right column. That column is reduced a lot. The other columns are more or less the same, except that through scaling you see more details on the right side.

Ambient Occlusion sizes

For the allocated memory you can see that the lowest part (orange color) was removed on the left side, and more details of everything above it are visible on the right side.

Ambient Occlusion allocated

Direct Lighting

modified   src/integrators/directlighting.rs
@@ -79,12 +78,11 @@ impl DirectLightingIntegrator {
         // TODO: ProfilePhase p(Prof::SamplerIntegratorLi);
         let mut l: Spectrum = Spectrum::new(0.0 as Float);
         // find closest ray intersection or return background radiance
-        if let Some(mut isect) = scene.intersect(ray) {
+        let mut isect: SurfaceInteraction = SurfaceInteraction::default();
+        if scene.intersect(ray, &mut isect) {
             // compute scattering functions for surface interaction
             let mode: TransportMode = TransportMode::Radiance;
-            Rc::get_mut(&mut isect)
-                .unwrap()
-                .compute_scattering_functions(ray, false, mode);
+            isect.compute_scattering_functions(ray, false, mode);
             // if (!isect.bsdf)
             //     return Li(isect.SpawnRay(ray.d), scene, sampler, arena, depth);
             let wo: Vector3f = isect.wo;

For Direct Lighting you can see as well that the new release almost entirely removes the right column.

Direct Lighting sizes

The graph for the allocated memory shows as well that far less memory is allocated (because it can be re-used).

Direct Lighting allocated

Whitted Ray Tracing

modified   src/integrators/whitted.rs
@@ -50,7 +49,8 @@ impl WhittedIntegrator {
     ) -> Spectrum {
         let mut l: Spectrum = Spectrum::default();
         // find closest ray intersection or return background radiance
-        if let Some(mut isect) = scene.intersect(ray) {
+        let mut isect: SurfaceInteraction = SurfaceInteraction::default();
+        if scene.intersect(ray, &mut isect) {
             // compute emitted and reflected light at ray intersection point
 
             // initialize common variables for Whitted integrator
@@ -59,9 +59,7 @@ impl WhittedIntegrator {
 
             // compute scattering functions for surface interaction
             let mode: TransportMode = TransportMode::Radiance;
-            Rc::get_mut(&mut isect)
-                .unwrap()
-                .compute_scattering_functions(ray, false, mode);
+            isect.compute_scattering_functions(ray, false, mode);
             // if (!isect.bsdf)
             if let Some(ref _bsdf) = isect.bsdf {
             } else {

For Whitted Ray Tracing the effect is similar to the one we saw for Ambient Occlusion:

Whitted Ray Tracing sizes

Whitted Ray Tracing allocated

Path Tracing

modified   src/integrators/path.rs
@@ -91,7 +90,8 @@ impl PathIntegrator {
             // println!("Path tracer bounce {:?}, current L = {:?}, beta = {:?}",
             //          bounces, l, beta);
             // intersect _ray_ with scene and store intersection in _isect_
-            if let Some(mut isect) = scene.intersect(&mut ray) {
+            let mut isect: SurfaceInteraction = SurfaceInteraction::default();
+            if scene.intersect(&mut ray, &mut isect) {
                 // possibly add emitted light at intersection
                 if bounces == 0 || specular_bounce {
                     // add emitted light at path vertex
@@ -104,7 +104,7 @@ impl PathIntegrator {
                 }
                 // compute scattering functions and skip over medium boundaries
                 let mode: TransportMode = TransportMode::Radiance;
-                Rc::get_mut(&mut isect).unwrap().compute_scattering_functions(&ray, true, mode);
+                isect.compute_scattering_functions(&ray, true, mode);
                 if let Some(ref _bsdf) = isect.bsdf {
                     // we are fine (for below)
                 } else {

Path Tracing sizes

Path Tracing allocated

Bi-Directional Path Tracing

modified   src/integrators/bdpt.rs
@@ -1218,11 +1221,10 @@ pub fn random_walk<'a>(
         //     bounces, beta, pdf_fwd, pdf_rev
         // );
         let mut mi_opt: Option<MediumInteraction> = None;
-        let mut si_opt: Option<Rc<SurfaceInteraction>> = None;
         // trace a ray and sample the medium, if any
         let found_intersection: bool;
-        if let Some(isect) = scene.intersect(&mut ray) {
-            si_opt = Some(isect);
+        let mut isect: SurfaceInteraction = SurfaceInteraction::default();
+        if scene.intersect(&mut ray, &mut isect) {
             found_intersection = true;
         } else {
             found_intersection = false;
@@ -1286,13 +1288,10 @@ pub fn random_walk<'a>(
                     bounces += 1;
                 }
                 break;
-            }
-            if let Some(mut isect) = si_opt {
+            } else {
                 // compute scattering functions for _mode_ and skip over medium
                 // boundaries
-                Rc::get_mut(&mut isect)
-                    .unwrap()
-                    .compute_scattering_functions(&ray, true, mode);
+                isect.compute_scattering_functions(&ray, true, mode);
                 let isect_wo: Vector3f = isect.wo;
                 let isect_shading_n: Normal3f = isect.shading.n;
                 if isect.bsdf.is_none() {

Bi-Directional Path Tracing sizes

Bi-Directional Path Tracing allocated

Metropolis Light Transport

modified   src/integrators/mlt.rs
@@ -437,7 +437,9 @@ impl MLTIntegrator {
         ) * (n_strategies as Float)
     }
     pub fn render(&self, scene: &Scene, num_threads: u8) {
-        let num_cores = if num_threads == 0_u8 {
+        let mut num_cores: usize; // TMP
+        // let num_cores = if num_threads == 0_u8 {
+        let num_cores_init = if num_threads == 0_u8 { // TMP
             num_cpus::get()
         } else {
             num_threads as usize
@@ -445,6 +447,7 @@ impl MLTIntegrator {
         if let Some(light_distr) = compute_light_power_distribution(scene) {
             println!("Generating bootstrap paths ...");
             // generate bootstrap samples and compute normalization constant $b$
+            num_cores = 1; // TMP: disable multi-threading
             let n_bootstrap_samples: u32 = self.n_bootstrap * (self.max_depth + 1);
             let mut bootstrap_weights: Vec<Float> =
                 vec![0.0 as Float; n_bootstrap_samples as usize];
@@ -477,7 +480,13 @@ impl MLTIntegrator {
                                         )));
                                     let mut p_raster: Point2f = Point2f::default();
                                     *weight = integrator
-                                        .l(scene, &light_distr, &mut sampler, depth, &mut p_raster)
+                                        .l(
+                                            scene,
+                                            light_distr.clone(),
+                                            &mut sampler,
+                                            depth,
+                                            &mut p_raster,
+                                        )
                                         .y();
                                 }
                             });
@@ -499,6 +508,7 @@ impl MLTIntegrator {
             let bootstrap: Distribution1D = Distribution1D::new(bootstrap_weights);
             let b: Float = bootstrap.func_int * (self.max_depth + 1) as Float;
             // run _n_chains_ Markov chains in parallel
+            num_cores = num_cores_init; // TMP: re-enable multi-threading
             let film: Arc<Film> = self.get_camera().get_film();
             let n_total_mutations: u64 =
                 self.mutations_per_pixel as u64 * film.get_sample_bounds().area() as u64;

For the Metropolis Light Transport algorithm most of the changes were done in the file responsible for Bi-Directional Path Tracing because they both share some functionality. But what's important is that I had to (temporarily) disable multi-threading for a phase where bootstrap samples are generated and a normalization constant is computed. I might be able to activate multi-threading (for this particular phase) again in later releases, but for now it was more important to save some time for almost all algorithms by re-using the struct SurfaceInteraction where possible instead of allocating and dropping the memory all the time.

Metropolis Light Transport sizes

Metropolis Light Transport allocated

Unsafe Code

If you look at the change to the struct SurfaceInteraction again:

modified   src/core/interaction.rs
@@ -274,7 +274,7 @@ pub struct SurfaceInteraction<'a> {
     pub dvdx: Cell<Float>,
     pub dudy: Cell<Float>,
     pub dvdy: Cell<Float>,
-    pub primitive: Option<&'a Primitive>,
+    pub primitive: Option<*const Primitive>,
     pub shading: Shading,
     pub bsdf: Option<Bsdf>,
     pub bssrdf: Option<TabulatedBssrdf>,

The new release allocates the memory for the struct SurfaceInteraction when a ray hits a shape, but the pointer to a Primitive will be set later (or better: somewhere else). Therefore we have to use an Option because we can't use a NULL pointer (like in C++).

$ rg -trust "\.primitive = None"
src/shapes/triangle.rs
453:        isect.primitive = None;
...

So here are some places where that missing information is filled in:

$ rg -trust "\.primitive = Some"
...
src/core/primitive.rs
43:                    isect.primitive = Some(self);
...
src/integrators/bdpt.rs
1326:                    si_eval.primitive = Some(primitive);

As you can read about here:

Recall that we can create raw pointers in safe code, but we can’t 
dereference raw pointers and read the data being pointed to.

Therefore we need unsafe code to access the Primitive pointed to:

$ rg -trust "primitive = unsafe"
src/core/scene.rs
99:		let primitive = unsafe { &*primitive_raw };

src/core/interaction.rs
426:	    let primitive = unsafe { &*primitive_raw };
516:	    let primitive = unsafe { &*primitive_raw };

src/core/integrator.rs
543:                        let primitive = unsafe { &*primitive_raw };
556:                    let primitive = unsafe { &*primitive_raw };

src/core/light.rs
214:		    let primitive = unsafe { &*primitive_raw };

src/integrators/bdpt.rs
472:                    let primitive = unsafe { &*primitive_raw };
538:                let primitive = unsafe { &*primitive_raw };
657:                    let primitive = unsafe { &*primitive_raw };
746:                        let primitive = unsafe { &*primitive_raw };

Here is a piece of Rust code how to get access to the Primitive from the SurfaceInteraction and call a member function:

if hit_surface {
    found_surface_interaction = true;
    if let Some(primitive_raw) = light_isect.primitive {
        let primitive = unsafe { &*primitive_raw };
        if let Some(area_light) = primitive.get_area_light() {
            ...
        }
    }
}

The End

I hope I didn't forget anything important. Have fun and enjoy the v0.8.1 release.