top | item 20325638

Writing a small ray tracer in Rust and Zig

260 points| cyber1 | 6 years ago |nelari.us | reply

96 comments

order
[+] skrebbel|6 years ago|reply
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.
[+] dahart|6 years ago|reply
> I’m convinced that there’s a proper mathy / lighting-y word for each component in that expression.

Sometimes yes, but often no. Frequently this kind of expression is the result of solving an equation, so it’s just an expression.

Graphics people often use two approaches for sub-expressions:

- You can name them with the same letters that are in the expression, just with the punctuation & operators removed, for example:

    const uvMinusNTimesDt = uv.sub(n.mul(dt))
- Alternatively, just like with equations, math people often freely assign single letter names to variables without worrying about semantic meaning.

    const q = uv.sub(n.mul(dt))
Nothing really wrong with naming sub-expressions after fruits, or single letters, or spelling them out explicitly.

It might be worth reflecting on what the goals are with your naming, and whether it matters what they’re named. As software engineers, our biases lean toward making choices that improve readability and maintainability. But for a specific equation that will never change once it works correctly, our preconceived notions about good software design and best practices might not actually apply to this situation. It might be more important to document the source of the equation than to make the implementation readable.

[+] geokon|6 years ago|reply
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. )

[+] tjelen|6 years ago|reply
So why not just write the original formula into the comment above the code? Maybe it's not the purest approach, but it sure would help parsing the code in this case.
[+] 0815test|6 years ago|reply
The most principled approach there, I think, would be to build a little Expression data-structure, and then feed it to an evaluation routine, trusting the optimizer to compile the whole thing down to something efficient. If Rust didn't have operator overloading anyway, you could do the job with procedural macros.

In practice, if I had to write that quasi-monstrosity in something like C, I'd probably just comment it as clearly and liberally as possible, to the point where I manage to reassure the reader that the final expression is correct; and then add a // See above: Please DO NOT edit this expression directly!// comment as an extra caution.

[+] mhh__|6 years ago|reply
The fruity method should be preferred: Any compiler worth using will immediately eliminate/register-allocate the variables and now you don't have undebuggable spaghetti code
[+] drcongo|6 years ago|reply
This is how I've been helping my daughter understand some maths problems at school. Break it down and name everything, then write the sum with the names in and it makes sense.
[+] oconnor663|6 years ago|reply
> 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...) I went ahead and added a Rayon-based answer to that SO question (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 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`.)

[+] gbmor|6 years ago|reply
> 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?

I came across this exact thing recently: https://github.com/brson/stdx

[+] xiphias2|6 years ago|reply
I still don't understand why Rayon is not part of the Rust standard library.

Rust was created to have easy and safe multithreading on the CPU, and Rayon is the clear winner in this space, for me it feels like something that should be part of Rust.

[+] forrestthewoods|6 years ago|reply
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|6 years ago|reply
> 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?

[+] Direct|6 years ago|reply
It's a library, so only half an answer to your question, but there's a fantastic library called rayon[1] created by one of the core contributors the the Rust language itself, Niko Matsakis. It lets you use Rust's iterator API to do extremely easy parallelism:

  list.iter().map(<some_fn>)
becomes:

  list.par_iter().map(<some_fn>)
Seeing as in the original example code, the final copies into the minifb have to be sequential due to the lock anyway, all the usage of synchronization primitives and in fact the whole loop could be replaced with something like:

  let rendered = buffers.par_iter().map(<rendering>).collect();  
  for buffer in rendered.iter() {  
    // The copy from the article  
  }
I've not written much Rust in a while, so maybe the state of the art is different now, but there are a lot of ways to avoid having to reach specifically for synchronization primitives.
[+] Twisol|6 years ago|reply
If you want to use completely safe Rust, you could probably get the Vec<u32> as a `&mut [u32]`, then use `.split_at()` on the slice to chop up the buffer into multiple contiguous sub-pieces for each thread. Collect up those pieces behind a struct for easier usage. It would cost you an extra pointer + length for each subpiece, but that's the price for guaranteeing that no thread reaches outside the contiguous intervals assigned to it.

EDIT: As mentioned by a sibling, `chunks_mut` is probably closer to what you want in this instance. If you have to get chunks of various sizes -- for instance, if the number of threads doesn't evenly divide the buffer into nice uniform tiles -- you'd need to drop down to the `split_at` level anyway.

[+] MaulingMonkey|6 years ago|reply
> Rust people, is there a way to tell the compiler that each thread gets its own elements?

That's what `local_pixels` does in the post. Where things get trickier is when you want to share write access to a single shared buffer in a non-overlapping way (e.g. `buffer` in the post.) To do this you need to either resort to unsafe, or to prove to the compiler that the writes aren't overlapping. One way to do the latter this is to get a slice (which Vec is convertable into), and then split up that slice (which the standard library has plenty of methods for: https://doc.rust-lang.org/std/slice/index.html ), and then give each thread those non-overlapping slices.

[+] db48x|6 years ago|reply
Yes. Instead of having a single slice of pixels, split it into n slices, one for each thread.
[+] saagarjha|6 years ago|reply
I wonder if there’s a way to borrow noncontiguous slices.
[+] mrec|6 years ago|reply
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|6 years ago|reply
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.
[+] gameswithgo|6 years ago|reply
There are lots of little nice things:

* you can more easily handle more than 2 options

* you can more easily have a bunch of code in each case

* you don't have a new syntax for the compiler or programmer to have to know about, everything is just consistently an expression

It is nice, and those of us who say that are usually aware of various alternative workarounds with ternaries etc.

[+] MrGilbert|6 years ago|reply
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|6 years ago|reply
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|6 years ago|reply
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|6 years ago|reply
That was a really fun read... Now if the author could do it in nim and v-lang...