Raytracer wasm support
18 November 2020
Lately, i got interested in ray tracing and i owe that to all the posts i see everyday on Hacker News about how easy to build a simple ray tracer from scratch. So to get a better understanding of the subject, i hacked a toy version in a couple days.
It was nothing fancy just a camera and a spheres populated environment but i implemented it in rust so it was a great opportunity to make it run in a browser. I also decided to write it about since the process wasn't as straightforward as i initially thought.
Concurrency
Concurrency is achieved by simply dividing the image into sections and let each processor handle the pixels assigned to it.
This is done using Rayon with code as simple as this:
let bands: Vec<(usize, &mut [Color<u8>])> =
pixels.chunks_mut(image_width).enumerate().collect();
bands.into_par_iter().for_each(|(index, band)| {
render(band, (image_width,1), index, data, world);
});
Normally Rayon will spawn N system threads (typically N equals the number of logical cores) in a ThreadPool to use, but in a browser environment you don't have the luxury of accessing low level APIs such as pthreads or Windows Threads. Instead browsers support parallelism via Web Workers which are objects that runs a named JavaScript file in the background and communicates through a messaging system (no shared memory between them).
Combining Web Workers with SharedArrayBuffer allows WebAssembly threads to share mutable data that leads to a threaded environment much similar to native platforms.
Thanks to Alex Crichton module, i didn't have to deal with creating the Web Workers from rust myself and it made executing rayon as simple as feeding his web worker pool implementation to the rayon pool builder.
let thread_pool = rayon::ThreadPoolBuilder::new()
.num_threads(workers_num)
.spawn_handler(|thread| Ok(pool.run(|| thread.run()).unwrap()))
.build()
.unwrap();
Random Number Generation
I heavily used the rand crate for RNG and while
it supports wasm by enabling the stdweb
feature in your cargo.toml
, it didn't
work as expected in a multi-threaded environment. The rand crate uses the
getrandom crate internally which obviously
is responsible of retrieving random data. getrandom had a function with this signature:
fn get_random_values(me: &BrowserCrypto, buf: &mut [u8]) -> Result<(), JsValue>
The buf
parameter is the output buffer and apparently the &mut [u8]
type did not act well
with the browser's SharedArrayBuffer, well since it's supposed to be shared between threads.
So it had to be changed to a memory region allocated by Javascript instead, it got finally fixed by this
PR later.
Fortunately, compiling the rand crate with the master branch of getrandom didn't break anything
and nothing more than the cargo.toml
file needed to change (Fork).
Building the project
Rust standard library needs to be recompiled with these flags
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory'
to ship with precompiled threading support enabled and it's also
recommended to run cargo with the std
Aware Cargo feature to build the the standard library with these custom settings:
RUSTFLAGS='-C target-feature=+atomics,+bulk-memory' cargo build --target wasm32-unknown-unknown --release -Z build-std=panic_abort,std
And finally running wasm-bindgen
with the --target no-modules
flag is mandatory to run code
with multi-threading.
wasm-bindgen wasm_client.wasm --target no-modules
You could also run the compiled wasm file through wasm-opt
to optimize even more (for size or speed):
wasm-opt wasm_client_bg.wasm -o wasm_client_bg.wasm -O3 --enable-mutable-globals --detect-features