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 { 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 { 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::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>`, 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; } #[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::(); let mem = Uint8ClampedArray::new(&mem.buffer()).slice(base as u32, (base + len) as u32); ImageData::new(&mem, width as f64, height as f64).unwrap() }