Release Notes for v0.7.2

Barber Shop

Barber Shop work in progress

I will not go into all the details, but the parse_blend_file was improved to read e.g. vertex colors. The work in progress (WIP) for the Barber Shop scene is documented here.

New files

Currently we have 172 Rust files in total, and 111 of them in the src directory (or in subdirectories of src):

> find . -name "*.rs" | wc -l
172
> find src/ -name "*.rs" | wc -l
111

In the examples folder most of the Rust files just test some functionality, but two of them can be used for rendering, examples/parse_ass_file.rs and examples/parse_blend_file.rs. This release changed a lot (89) of the existing files, but added only two new files:

  1. src/integrators/whitted.rs: The Rust code in this file implements the WhittedIntegrator as described in issue #115.
  2. src/lights/goniometric.rs: Another missing feature (or in this case light) was the GonioPhotometricLight and the matching issue is #117.

Still on my TODO list:

  1. Implement CreateMaxMinDistSampler.
  2. Implement CreateStratifiedSampler.

Branches (or traits vs. enums)

While investigating the performance differences between the current Rust and the original C++ code I first looked at arena based allocation and which options I would have on the Rust side. While asking questions on the Rust programming language forum @matklad suggested to "replace dynamic dispatch with an enum".

Converting traits into enums

As you can see in the screenshot above the difference between the v0.7.1 and the v0.7.2 release is that a lot of the traits (left side is from v0.7.1) moved into the enums section (right side is from v0.7.2).

Performance

While I was not able to improve the performance of the Rust code dramatically within this release the whole discussion made me think about the stack vs. the heap in general and I decided that it's time to diverge from the original C++ code, now that the Rust code is more or less feature complete. In the past I thought a trait would be the closest counterpart to a C++ class and I also like the way it looks like in the documentation, but Rust's enums go beyond what C++ and C# enums can do. I recommend reading the chapter 10 of the Programming Rust book.

New Branch(es)

Anyway, until I really decide which way to go I have created a new branch called traits and the master branch is basically following the enums route and tries to replace most or all traits by enums. It should be easy to switch between both branches and measure performance as described in Perf and Heaptrack blog.

The Future

Some ideas for future releases (and how to improve the performance finally):

C++

The C++ version works like this:

// from e.g. PathIntegrator ...
Spectrum PathIntegrator::Li(const RayDifferential &r, const Scene &scene,
                            Sampler &sampler, MemoryArena &arena,
                            int depth) const {
...
    for (bounces = 0;; ++bounces) {
...
        // ... create SurfaceInteraction ...
        SurfaceInteraction isect;
        // ... and pass it on ...
        bool foundIntersection = scene.Intersect(ray, &isect);
...
    }
    ReportValue(pathLength, bounces);
    return L;
}
bool Scene::Intersect(const Ray &ray, SurfaceInteraction *isect) const {
    // ... and on ...
    return aggregate->Intersect(ray, isect);
}
bool BVHAccel::Intersect(const Ray &ray, SurfaceInteraction *isect) const {
...
    while (true) {
...
                for (int i = 0; i < node->nPrimitives; ++i)
                    // ... and on ...
                    if (primitives[node->primitivesOffset + i]->Intersect(
                            ray, isect))
                        hit = true;
                if (toVisitOffset == 0) break;
...
    }
    return hit;
}
bool GeometricPrimitive::Intersect(const Ray &r,
                                   SurfaceInteraction *isect) const {
...
    // ... and on ...
    if (!shape->Intersect(r, &tHit, isect)) return false;
...
    return true;
}
bool Triangle::Intersect(const Ray &ray, Float *tHit, SurfaceInteraction *isect,
                         bool testAlphaTexture) const {
...
    // Fill in _SurfaceInteraction_ from triangle hit
    *isect = SurfaceInteraction(pHit, pError, uvHit, -ray.d, dpdu, dpdv,
                                Normal3f(0, 0, 0), Normal3f(0, 0, 0), ray.time,
                                this, faceIndex);
...
    return true;
}

Or as a UML Sequence diagram:

UML Sequence C++

Rust

Lets first look at the UML Sequence diagram:

UML Sequence Rust

And here the relevant Rust code:

// the shape (Triangle is one of them) creates the SurfaceInteraction
impl Triangle {
...
    pub fn intersect(&self, ray: &Ray) -> Option<(SurfaceInteraction, Float)> {
...
        // create SurfaceInteraction here
        let mut si: SurfaceInteraction = SurfaceInteraction::new(
            &p_hit,
            &p_error,
            &uv_hit,
            &wo,
            &dpdu,
            &dpdv,
            &dndu,
            &dndv,
            ray.time,
            None,
        );
...
        // return it
        Some((si, t as Float))
    }
...
}
impl Shape {
...
    // arriving at the caller
    pub fn intersect(&self, r: &Ray) -> Option<(SurfaceInteraction, Float)> {
        match self {
            Shape::Crv(shape) => shape.intersect(r),
            Shape::Clndr(shape) => shape.intersect(r),
            Shape::Dsk(shape) => shape.intersect(r),
            Shape::Sphr(shape) => shape.intersect(r),
            Shape::Trngl(shape) => shape.intersect(r),
        }
    }
...
}
impl GeometricPrimitive {
...
    // arriving at the caller
    pub fn intersect(&self, ray: &mut Ray) -> Option<SurfaceInteraction> {
        if let Some((mut isect, t_hit)) = self.shape.intersect(ray) {
...
            Some(isect)
        } else {
            None
        }
    }
...
}
impl Primitive {
...
    // arriving at the caller
    pub fn intersect(&self, ray: &mut Ray) -> Option<SurfaceInteraction> {
        match self {
            Primitive::Geometric(primitive) => {
                let isect_opt = primitive.intersect(ray);
                if let Some(mut isect) = isect_opt {
                    isect.primitive = Some(self);
                    Some(isect)
                } else {
                    isect_opt
                }
            }
            Primitive::Transformed(primitive) => primitive.intersect(ray),
            Primitive::BVH(primitive) => primitive.intersect(ray),
            Primitive::KdTree(primitive) => primitive.intersect(ray),
        }
    }
...
}
impl BVHAccel {
...
    // arriving at the caller
    pub fn intersect(&self, ray: &mut Ray) -> Option<SurfaceInteraction> {
...
        loop {
...
                        if let Some(isect) = self.primitives[node.offset + i].intersect(ray) {
                            si = isect;
                            hit = true;
                        }
...
        }
        if hit {
            Some(si)
        } else {
            None
        }
    }
...
}
impl Scene {
...
    // arriving at the caller
    pub fn intersect(&self, ray: &mut Ray) -> Option<SurfaceInteraction> {
...
        self.aggregate.intersect(ray)
    }
...
}
impl SamplerIntegrator for PathIntegrator {
...
    fn li(
        &self,
        r: &mut Ray,
        scene: &Scene,
        sampler: &mut Box<dyn Sampler + Send + Sync>,
        _depth: i32,
    ) -> Spectrum {
...
        loop {
...
            // arriving at the caller
            if let Some(mut isect) = scene.intersect(&mut ray) {
...
            } else {
...
            }
            bounces += 1_u32;
        }
        l
    }
...
}

The End

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