155 lines
5.5 KiB
Rust

use futures::Future;
use js_sys::{Promise, Uint8ClampedArray, WebAssembly};
use rayon::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
macro_rules! console_log {
($($t:tt)*) => (crate::log(&format_args!($($t)*).to_string()))
}
mod pool;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = console, js_name = log)]
fn logv(x: &JsValue);
}
#[wasm_bindgen]
pub struct Scene {
inner: raytracer::scene::Scene,
}
#[wasm_bindgen]
impl Scene {
/// Creates a new scene from the JSON description in `object`, which we
/// deserialize here into an actual scene.
#[wasm_bindgen(constructor)]
pub fn new(object: &JsValue) -> Result<Scene, JsValue> {
console_error_panic_hook::set_once();
Ok(Scene {
inner: object
.into_serde()
.map_err(|e| JsValue::from(e.to_string()))?,
})
}
/// Renders this scene with the provided concurrency and worker pool.
///
/// This will spawn up to `concurrency` workers which are loaded from or
/// spawned into `pool`. The `RenderingScene` state contains information to
/// get notifications when the render has completed.
pub fn render(
self,
concurrency: usize,
pool: &pool::WorkerPool,
) -> Result<RenderingScene, JsValue> {
let scene = self.inner;
let height = scene.height;
let width = scene.width;
// Allocate the pixel data which our threads will be writing into.
let pixels = (width * height) as usize;
let mut rgb_data = vec![0; 4 * pixels];
let base = rgb_data.as_ptr() as usize;
let len = rgb_data.len();
// Configure a rayon thread pool which will pull web workers from
// `pool`.
let thread_pool = rayon::ThreadPoolBuilder::new()
.num_threads(concurrency - 1)
.spawn_handler(|thread| Ok(pool.run(|| thread.run()).unwrap()))
.build()
.unwrap();
// And now execute the render! The entire render happens on our worker
// threads so we don't lock up the main thread, so we ship off a thread
// which actually does the whole rayon business. When our returned
// future is resolved we can pull out the final version of the image.
let done = pool
.run_notify(move || {
thread_pool.install(|| {
rgb_data
.par_chunks_mut(4)
.enumerate()
.for_each(|(i, chunk)| {
let i = i as u32;
let x = i % width;
let y = i / width;
let ray = raytracer::Ray::create_prime(x, y, &scene);
let result = raytracer::cast_ray(&scene, &ray, 0).to_rgba();
chunk[0] = result.data[0];
chunk[1] = result.data[1];
chunk[2] = result.data[2];
chunk[3] = result.data[3];
});
});
rgb_data
})?
.map(move |_data| image_data(base, len, width, height).into());
Ok(RenderingScene {
promise: wasm_bindgen_futures::atomics::future_to_promise(done),
base,
len,
height,
width,
})
}
}
#[wasm_bindgen]
pub struct RenderingScene {
base: usize,
len: usize,
promise: Promise,
width: u32,
height: u32,
}
// Inline the definition of `ImageData` here because `web_sys` uses
// `&Clamped<Vec<u8>>`, whereas we want to pass in a JS object here.
#[wasm_bindgen]
extern "C" {
pub type ImageData;
#[wasm_bindgen(constructor, catch)]
fn new(data: &Uint8ClampedArray, width: f64, height: f64) -> Result<ImageData, JsValue>;
}
#[wasm_bindgen]
impl RenderingScene {
/// Returns the JS promise object which resolves when the render is complete
pub fn promise(&self) -> Promise {
self.promise.clone()
}
/// Return a progressive rendering of the image so far
#[wasm_bindgen(js_name = imageSoFar)]
pub fn image_so_far(&self) -> ImageData {
image_data(self.base, self.len, self.width, self.height)
}
}
fn image_data(base: usize, len: usize, width: u32, height: u32) -> ImageData {
// Use the raw access available through `memory.buffer`, but be sure to
// use `slice` instead of `subarray` to create a copy that isn't backed
// by `SharedArrayBuffer`. Currently `ImageData` rejects a view of
// `Uint8ClampedArray` that's backed by a shared buffer.
//
// FIXME: that this may or may not be UB based on Rust's rules. For example
// threads may be doing unsynchronized writes to pixel data as we read it
// off here. In the context of wasm this may or may not be UB, we're
// unclear! In any case for now it seems to work and produces a nifty
// progressive rendering. A more production-ready application may prefer to
// instead use some form of signaling here to request an update from the
// workers instead of synchronously acquiring an update, and that way we
// could ensure that even on the Rust side of things it's not UB.
let mem = wasm_bindgen::memory().unchecked_into::<WebAssembly::Memory>();
let mem = Uint8ClampedArray::new(&mem.buffer()).slice(base as u32, (base + len) as u32);
ImageData::new(&mem, width as f64, height as f64).unwrap()
}