
Writing a small ray tracer in Rust and Zig - cyber1
https://nelari.us/post/raytracer_with_rust_and_zig/
======
skrebbel
This is an awesome post.

At the risk of shedding it to bikes, one point that the author makes is that
Zig's lack of operator overloading makes him write vector math like this:

    
    
        if (discriminant > 0.0) {
            // I stared at this monster for a while to ensure I got it right
            return uv.sub(n.mul(dt)).mul(ni_over_nt).sub(n.mul(math.sqrt(discriminant)));
        }
    

He signs off with:

> _How do C programmers manage?_

The answer is simple: we assign names to intermediate results. Now, I have
absolutely no idea what that expression computes, because I suck at graphics
programming and math in general. Please pretend that these are proper mathy
terms:

    
    
        if (discriminant > 0.0) {
            const banana = uv.sub(n.mul(dt))
            const apple = banana.mul(ni_over_nt)
            const pear = n.mul(math.sqrt(discriminant)
            return apple.sub(pear)
        }
    

I'm convinced that there's a proper mathy/lighting-y word for each component
in that expression. Of course this approach totally breaks down if you're
copying expressions from papers or articles without understanding why they are
correct (which is how I do all my graphics programming). I do find that naming
variables is often a great way to force myself to grok what's going on.

~~~
geokon
In my experience writing math code the intermediary values get quite goofy Ex:
orthogonal-vector-to-plane-bisecting-input-vector-and-first-column-vector

But I rather use crazy descriptive names than hiding it away. Otherwise it
gets quite incomprehensible when you reread it 3 months later

Anyone else hit this problem? I suspect most people just reference a paper or
book and use the letters to match the source ( x/y/n/m/etc. )

~~~
socialdemocrat
I think people's brains must work different, because for me this would be a
terrible way to do it. I simply cannot read and comprehend math with very long
descriptive variable names.

Whenever I see people do that, I have to write down the equation with single
letters, and then look at it.

I am more of a literate programming type of person. I prefer writing longer
explanations of code. But usually as a header. I like keeping the core of the
code as clean and noise free as possible.

So I write code more like a math or physics book I guess. The equations are
kept simple and clutter free, and then there is a body of text above or below
explaining how to think about it. I tend to prefer using a lot of unicode,
because following conventions helps me a lot. If I see a t₀, t and Δt variable
e.g. I immediately get a sense of what sort of variables this is and how they
are related. If instead it said start_time_of_incident, current_time and
time_difference_between_events I could not quickly parse and internalize that.

~~~
geokon
Yeah, I feel you on that and I started out writing in a similar style b/c it's
terse and easy to digest. To be clear, this is mostly a hobby things for me so
I'm not speaking from a position of authority, but in my limited experience
once the problem gets bigger and you worry about
performance/caching/recursion/etc. the math and equation start to diverge from
the code substantially.

For instance over the last few months I've been playing with BLAS and the BLAS
operators just don't really map cleanly to equations - and the way you
structure efficient solutions just ends up being more nuanced and complicated
than the simple clean "theoretical" on-paper solution. All the nuances need to
be captured in the variable names to have any chance of making the code
understandable

~~~
Fronzie
the distance between BLAS and mathematics is quite big. Have you had a look
at, for example, the Eigen library?

Although it's still more verbose than math notation, it stays much closer and,
in my experience, does allow for nearly all the optimization-wiggle-room one
could want.

------
oconnor663
> But rendering in separate threads turned out to be (unsurprisingly) harder
> than the way I would do it in C++...It was a bit frustrating to figure out
> how to accomplish this. Googling yielded a few stack overflow posts with
> similar questions, and were answered by people basically saying use my
> crate!

Based on some discussion in r/rust
([https://www.reddit.com/r/rust/comments/c7t5za/writing_a_smal...](https://www.reddit.com/r/rust/comments/c7t5za/writing_a_small_ray_tracer_in_rust_and_zig/))
I went ahead and added a Rayon-based answer to that SO question
([https://stackoverflow.com/a/56840441/823869](https://stackoverflow.com/a/56840441/823869)).
That's been the de facto standard for data parallelism in Rust for the last
few years. But the article highlights that discovering the de facto standards
is still a challenge for new Rust users -- does anyone know of a well-
maintained list of the 10-20 most critical crates that new users should
familiarize themselves with after reading The Book? Things like Rayon and
lazy_static. The ranked search results at
[https://crates.io/crates?sort=recent-
downloads](https://crates.io/crates?sort=recent-downloads) are _almost_ good
enough, but they include a lot of transitive dependencies that new users
shouldn't care about. (I.e. `regex` is a very important crate, but `aho-
corasick` is usually only downloaded as a dependency of `regex`.)

~~~
steveklabnik
[https://rust-lang-nursery.github.io/rust-cookbook/](https://rust-lang-
nursery.github.io/rust-cookbook/) is sorta kinda this, sorta

~~~
OskarS
I will say, the one example they have there which is sort-of analogous to
"render each pixel of this image in parallel" is the "draw a julia set" one
[0], and it's a very bad way of convincing a C/C++ programmer that Rust is
good at this sort of thing. Even if the "loop over all rows in the main
thread, adding to the pool a lambda that loops over each column" is somehow
optimized in a good data-parallel way (I doubt it compares favorably
performance-wise with "#pragma omp parallel for"), the lambdas then push each
finished pixel into a channel along with their coordinates. The main thread
then has to literally loop through every pixel and read from the channel for
each and every one.

The natural way to do that in C/C++ is to just write the pixel to the bitmap
in each thread. There are no race conditions here (everything is embarassingly
parallel), just write the resulting pixel to the bitmap and be done with it.
The only reason to have that channel with all that overhead (and that final
synchronization on the main thread) is to satisfy the borrow checker, which is
just silly in this case. It adds a tremendous amount of overhead just to make
it idiomatic Rust.

It's true that you can do it the "C++ way" in Rust using unsafe and raw
pointers (and there's probably crates that can do the "parallel for" in a way
that compares well with OpenMP), but as a graphics programmer who's done a lot
of this sort of thing, that piece of code made a very bad first impression of
Rust as a high-performance language.

EDIT: also, the description is wrong. It says: "ThreadPool::execute receives
each pixel as a separate job". No it doesn't, it receives each scanline as a
separate job. It might be better if the pool had each pixel as a separate job,
but that's not what the code is doing.

[0]: [https://rust-lang-nursery.github.io/rust-
cookbook/concurrenc...](https://rust-lang-nursery.github.io/rust-
cookbook/concurrency/threads.html#draw-fractal-dispatching-work-to-a-thread-
pool)

~~~
gmueckl
To me, this somehow says that most truely interesting problems can only be
solved efficiently in Rust using unsafe. Granted, my definition of interesting
may be a bit limited, but my initial takeaway here is that Rusts safety story
is much more limited than its proponents claim.

~~~
pcwalton
> To me, this somehow says that most truely interesting problems can only be
> solved efficiently in Rust using unsafe.

Even Vec is implemented with unsafe code under the hood. Rust isn't about not
using unsafe code _anywhere_ : it's about encapsulating that unsafe code in
reusable abstractions that can be easily audited. This is how essentially all
languages work to begin with: unless you're using CompCert, the compiler
itself is not proven correct, and so the compiler's code generator is
"unsafe".

Here's an example of how rav1e, an AV1 encoder in Rust, builds safe
abstractions out of unsafe primitives in order to solve a similar problem:
[https://blog.rom1v.com/2019/04/implementing-tile-encoding-
in...](https://blog.rom1v.com/2019/04/implementing-tile-encoding-in-rav1e/)

~~~
gmueckl
Wait, so how exactly is Rust better than C++ again? If I need to bring my own
unsafe code to the party, where is the gain? Especially, if - in your words -
"This is how essentially all languages work to begin with". I can build "safe"
abstractions in any other language, too, can I not?

~~~
pcwalton
No, you can't build safe abstractions in C++. The language lacks the ability
to enforce memory safety. Every abstraction you can come up with will have
some way to subvert it. In Rust, the abstractions can't be broken without
using unsafe, which application logic should never use.

This isn't just a theoretical distinction. Empirically, Rust programs have far
fewer memory safety problems than C++ programs do.

------
forrestthewoods
What a delightful post. Author wrote a very nice description of things they
learned from a little weekend project. No preaching or ranting or
opinionating. Just “I did a thing and here’s what I learned”. That’s easily my
favorite type of blog post.

Thanks for sharing! ️

------
tntn
> I wrapped my objects in atomic reference counters, and wrapped my pixel
> buffer in a mutex

Rust people, is there a way to tell the compiler that each thread gets its own
elements? Do you really have to either (unnecessarily) add a lock or reach for
unsafe?

~~~
saagarjha
I wonder if there’s a way to borrow noncontiguous slices.

~~~
sitkack
unsafe is the tool u are looking for.

~~~
Tuna-Fish
No, there are plenty of safe ways to achieve this in the standard library. The
chunks and split families of functions on slices are all designed to do pretty
much exactly this.

~~~
cyphar
In fairness to GP, they are implemented using unsafe (which is unsurprising
since they take one &mut and return two to the same borrowed data).

~~~
setr
If you go by that definition, I think you’ll eventually find out that
everything depends on unsafe, and thus nothing is actually safe

Which isn’t a very useful distinction

------
jorangreef
Zig is worth supporting on Patreon:
[https://www.patreon.com/andrewrk](https://www.patreon.com/andrewrk)

------
mrec
I didn't quite grok this bit:

> _The ability to return values from if expressions and blocks is awesome and
> I don’t know how I’ve managed to live up until now without it. Instead of
> conditionally assigning to a bunch of variables it is better to return them
> from an if expression instead._

The example shown (aside from the fact it's assigning a tuple, which is a
different point) would naturally be a ternary in C/C++. Does the awesomeness
kick in for more complicated examples where you want intermediate vars etc in
the two branches?

~~~
monocasa
Yeah, exactly. YOu know how it becomes a pain to make a const variable that
needs some setup in C and C++? This lets you const all the things.

~~~
Fronzie
You could assign it from a lambda function.

~~~
monocasa
Which is still more of a pain than "everything's an expression".

------
MrGilbert
Really cool post. I always dreamed of writing a small realtime, raytraced
game, with lights etc. Basically a roguelike in 3D. I never managed to finish
it, and this project reminds me of it.

------
olodus
I really don't think these languages should be set opposite to one another as
much as they do. I mean, I get why they are. They take up almost the same
position and have quite different ways to view security and lang design. And
they both try to grow and compete. But still. I think there are some space for
them both. I would really think it would be cool to spend my working hours
programming Rust for safety and switching to Zig whenever I have to code an
unsafe block (all of it compiling to webasm :o )

~~~
flohofwoe
In my mind/opinion, Rust is a potential replacement for C++, while Zig is a
potential replacement for C, and both have their place in the world.

Rust feels more restrictive, but that may be the right approach for building
large software projects with big, "diverse" (in terms of skill level) teams.

Zig is smaller and feels more nimble, and might be better suited for smaller
teams working on smaller projects, and for getting results faster, while
avoiding most of C's darker corners.

------
keyle
That was a really fun read... Now if the author could do it in nim and
v-lang...

