Release Notes for v0.7.2

Barber Shop

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:

src/integrators/whitted.rs : The Rust code in this file implements the WhittedIntegrator as described in issue #115. 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:

Implement CreateMaxMinDistSampler. 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".

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):

I didn't pay much attention to the difference between std::vec::Vec, the primitive types array and slice, but I am confident that after scene creation we can replace the growable vectors by either shared or mutable slices , which might improve performance.

, which might improve performance. Read (again) the appendix A.4 about Memory Management of the Physically Based Rendering book and see if we can find Rust counterparts to the described concepts.

about Memory Management of the Physically Based Rendering book and see if we can find to the described concepts. Replace the remaining traits by enums ? As I said above it's not proven (yet) that this will improve performance, but lets at least try.

by ? As I said above it's not proven (yet) that this will improve performance, but lets at least try. Something which gives me headache is the creation of SurfaceInteraction . See below for the difference between the C++ code and the current Rust implementation. Basically C++ allocates an instance of SurfaceInteraction per bounce and passes it on until it arrives e.g. at a triangle, which fills in it's information calculated from a triangle intersection. Passing on a pointer basically allows everyone inbetween to potentially modify that piece of memory. On the Rust side the triangle allocates the memory and returns potential intersection information as an Option<T> .

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:

Rust

Lets first look at the UML Sequence diagram:

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.