Writing a small ray tracer in Rust and Zig
nelari.us
nelari.us
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.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. )
If you do this (and I'll certainly admit that I do this as well) make sure you link to the paper in question somewhere in either comments or the documentation so that future developers can find the canonical reference to what the variables mean.
A long name can tell you _what_ a value is. But it’s of zero use in explaining _why_ it’s being used.
The only solution is a lengthy comment explaining the process. Here some ballistic trajectory code I wrote awhile back. Good luck making sense of any single term!
https://github.com/forrestthewoods/lib_fts/blob/26a0d115eb44...
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.
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
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.
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.
\vec r = \frac {n_{1}}{n_{2}} \cdot (\vec v - dot(\vec v, \vec n) \cdot \vec N) - \sqrt{1 - \frac {n_{1}^2}{n_{2}^2} \cdot (1 - dot(\vec v, \vec n))} \cdot \vec N
(This is partly a joke and partly a serious suggestion, there are definitely people out there who would find that easier to write, especially if the IDE rendered it properly. I stole it by searching for ni_over_nt and finding http://viclw17.github.io/2018/08/05/raytracing-dielectric-ma... )[1 2 3
4 5 6
7 8 9].
And you can transform Julia expressions to LaTeX:
https://github.com/korsbo/Latexify.jl
But even they didn't go as far as being able to parse full LaTeX expressions (though you technically could with reader macros plus editor support).
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.
Personally, I think that depends, so I like to challenge my own assumptions and examine things case by case. The implementation of a math equation, especially one that is the result of solving a different equation, and so went through a symbolic/algebraic transformation, that is one place where adding more names might not clarify the implementation.
In general, though, my personal experience has been one of not seeing things named enough. Specifically, feature creep in software tends to try to add code with a light touch without disturbing too much of the surrounding code, but with new concepts that deserve more naming than they get. This can and does lead to code that is difficult to maintain, and I think I’ve seen more of that mistake than of naming things too much, and I suspect it’s much easier to not just shoot yourself in the foot, but cut your leg off, by under-naming rather than by over-naming.
> It is very common for functional programmers to write functions as a composition of other functions, never mentioning the actual arguments they will be applied to.
https://wiki.haskell.org/Pointfree
> We are going to use named variables to keep track of the arguments, then write a definition without them.
Some time ago I’ve stopped solving equations, using software for that. Nowadays, when I need them solved, I sometimes don’t even write code, here’s an example: https://stackoverflow.com/questions/1351746/find-a-tangent-p...
Symbolic math software does more “optimizations” than even offline compilers. For example, I have never saw an optimizer which is aware that sin(x)^2 + cos(x)^2 = 1.
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`.)
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...
The right way is to use rayon, which is similar to OpenMP in this sense. Each thread would get a pointer to the chunk of the buffer it's operating on. You don't need to write any unsafe code to do this.
This is not actually the first time I've seen this example code. I was wondering how Rust solved this issue (parallel rendering into a single framebuffer) in a way that satisfied the borrow checker, and came across this example. It was a bit disheartening. Rust seems like an excellent language with boundless potential, but as I said, this particular example makes for a very bad first impression. It is also not obvious at all that this is part of the "nursery" and is not an official Rust document (the "Rust Cookbook" makes it seem more polished than maybe it is).
Yeah, I mean, the reddit thread way up this comment chain talks about how Rayon should have been used, but the discoverability issue is real.
> It is also not obvious at all that this is part of the "nursery" and is not an official Rust document (the "Rust Cookbook" makes it seem more polished than maybe it is).
Yep; it's in the URL but not everyone will see that. Like most things, this is basically an accident of history; there was a push to clean this project up and make it an official resource, but before that work was finished, all the contributors had life stuff happen and had to go do something else. So it's in this weird quasi-state where there's a ton of good in there, but also some bad. We'll get to it...
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...
And when you do decide you need to use `unsafe` for a non-FFI purpose, Rust provides the tooling to explicitly define the abstraction barrier such that clients of your code don't have to care (or even know) that you're using unsafe internally.
This isn't just a theoretical distinction. Empirically, Rust programs have far fewer memory safety problems than C++ programs do.
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.
What the Rust ecosystem is still lacking is a quasi-standard "Rust Platform" of best-practice library components where the community can freely deprecate something when a clearly better replacement comes along. There are a few "community" websites that try to provide guidance wrt. some parts of the Rust ecosystem, but nothing that feels even close to official or consensus-driven.
Sort of like VS Code has extension packs.
Basically have some metapackages for common development areas, that pull in community vetted, high quality libraries for certain areas.
See python: my workplace (and I'm sure many others) is stuck with what's in anaconda for better or worse. The situation is certainly better than pure std.
It is easy to end up with 200+ dependencies on more complex projects.
Some things should definitely be moved into std over the long term.
BUT: the time is not now. The language is still evolving rapidly. Upcoming features like specialization and a form of higher kinded types have the potential to impact API design a lot. I also really want named function arguments.
No-one wants to end up with a outdated and arcane standard library.
Go has the exact same problem, and has also historically made similar mistakes to Rust in their stdlib (the "syscall" module is strongly recommended against -- instead you should use "golang.org/x/sys"). And it should be noted that Go is even more bare-bones than Rust -- the lack of generics means you have to use "containers" which is very limited (I also have a feeling it's used by effectively nobody, because of the bad ergonomics).
I think the best solution for this problem might be some form of "meta-crate" concept which would allow you to pull in all of the well-vetted and stable libraries that most people want, without having to curate ~20 libraries yourself.
In certain ways Go is more bare bones, but Go also comes with common hashes, some crypto primitives, encodings like JSON, compression/archives, logging, date/time utilities, Regex, a templating engine, an http client and server, regex, .... All of which requires a dependency with Rust.
I do remember an effort of a meta package like you mentioned. I think it was driven by brson and on Github, but I can't find it now, and it was abandoned anyway.
* As with "syscall", users are strongly suggested to use "golang.org/x/crypto" for crypto primitives like AEAD. All of the crypto bits in the standard library are probably still "okay" but there are better interfaces and more efficient implementations in "golang.org/x/crypto".
* The "flags" package is basically useless for large projects (everyone uses "cobra" or "spf13/cli" to create CLI interfaces now). It also has the really weird Go-ism of "-flag" arguments.
* For logging, most people use "logrus" or similar. "log" isn't necessarily bad, but it's not full-featured enough that most people end up not using it.
* As for HTTP servers, at the very least you'll use "gorilla" to deal with routes -- if not completely switch to a different HTTP server implementation.
* Not to mention some of the other weird bits and bobs which are useful, but are strange to include in such a small stdlib, like "mime". Or some of the more fruity stuff like "database".
This is the downside of the batteries-included model. Especially when a lot of the bits in the Go stdlib were included early in the language's life and then quickly became a clearly bad idea but it was too late to remove them.
But some parts of the ecosystem end up in a lot of projects. Conservatively extending std would be good for the language, IMO. To set a standard, reduce compile times, and ease the burden for companies that have hard review requirements for each dependency.
Some things I'd like to see in std in a few years:
* `serde`
* `rand`
* `log`, but probably without a backend, or a simple default
* a subset of `chrono`
* some parts of `rayon`
Both failed to gain any traction.
This is totally understandable position, but it is still important to have sensible defaults for common tasks for end users.
One example that I can point to is Haskell, which I tried to learn from Haskell books, and it took me years to realize that it's not the language that's extremely inefficient, but the default libraries that use lists instead of arrays for most of the tasks.
I'm writing about Rayon because I see it as a reoccuring discoverability problem for people who are new to Rust.
There are many possible solutions, but one would be to have a default list of dependencies created in Cargo.toml that can of course be modified by the Rust users. Also that list can of course be changed over time by Rust developers.
Java had to change the collection API when it added generics, but it showed that it's possible to split the API contract while improving on the underlying implementation over time. The low level Rayon primitives of course shouldn't be in the standard library name space.
I came across this exact thing recently: https://github.com/brson/stdx
[0]: https://users.rust-lang.org/t/error-chain-is-no-longer-maint...
Thanks for sharing! ️
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?
Splitting a single slice into two mutable slices is done via https://doc.rust-lang.org/std/primitive.slice.html#method.sp... if you want more than that, you will need to roll your own.
I think it would be a great exercise to implement what you are asking for, the docs link directly to the source.
https://doc.rust-lang.org/src/core/slice/mod.rs.html#991-100...
Which isn’t a very useful distinction
unsafe is the mechanism that GP needs to use to get multiple contiguous mutable borrows.
There is nothing wrong with unsafe, it is used to build all of the safe abstractions in Rust.
The entire point of rust's safety system is that it is possible to build safe things on unsafe foundations because the unsafety can be encapsulated into functions and types that can only be used safely. The safety of these functions then depends on them being bug-free, and the best way this is achieved is by minimizing the total amount of unsafe code in the ecosystem, and sharing it in widely used libraries so that there are enough users and testing to find the bugs.
So no, unsafe is not the mechanism GP needs to, or should use, because the split\* and chunks\* families of functions already exist and do exactly what he wants.
[0]: https://doc.rust-lang.org/std/primitive.slice.html#method.ch...
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.
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.
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.> 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?
https://ziglang.org/documentation/master/#switch
https://doc.rust-lang.org/reference/expressions/match-expr.h...
* 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.
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.