mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-06-23 01:31:34 +00:00
Implement support for WebAssembly threads
... and add a parallel raytracing demo! This commit adds enough support to `wasm-bindgen` to produce a workable wasm binary *today* with the experimental WebAssembly threads support implemented in Firefox Nightly. I've tried to comment what's going on in the commits and such, but at a high level the changes made here are: * A new transformation, living in a new `wasm-bindgen-threads-xform` crate, prepares a wasm module for parallel execution. This performs a number of mundane tasks which I hope to detail in a blog post later on. * The `--no-modules` output is enhanced with more support for when shared memory is enabled, allowing passing in the module/memory to initialize the wasm instance on multiple threads (sharing both module and memory). * The `wasm-bindgen` crate now offers the ability, in `--no-modules` mode, to get a handle on the `WebAssembly.Module` instance. * The example itself requires Xargo to recompile the standard library with atomics and an experimental feature enabled. Afterwards it experimentally also enables threading support in wasm-bindgen. I've also added hopefully enough CI support to compile this example in a builder so we can upload it and poke around live online. I hope to detail more about the technical details here in a blog post soon as well!
This commit is contained in:
3
examples/raytrace-parallel/.gitignore
vendored
Normal file
3
examples/raytrace-parallel/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
package-lock.json
|
||||
raytrace_parallel.js
|
||||
raytrace_parallel_bg.wasm
|
29
examples/raytrace-parallel/Cargo.toml
Normal file
29
examples/raytrace-parallel/Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "raytrace-parallel"
|
||||
version = "0.1.0"
|
||||
authors = ["The wasm-bindgen Developers"]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
console_error_panic_hook = "0.1"
|
||||
futures = "0.1"
|
||||
js-sys = { path = '../../crates/js-sys' }
|
||||
raytracer = { git = 'https://github.com/alexcrichton/raytracer', branch = 'update-deps' }
|
||||
wasm-bindgen = { path = "../..", features = ['serde-serialize'] }
|
||||
wasm-bindgen-futures = { path = '../../crates/futures' }
|
||||
|
||||
[dependencies.web-sys]
|
||||
path = '../../crates/web-sys'
|
||||
features = [
|
||||
'CanvasRenderingContext2d',
|
||||
'ErrorEvent',
|
||||
'Event',
|
||||
'ImageData',
|
||||
'Navigator',
|
||||
'Window',
|
||||
'Worker',
|
||||
'DedicatedWorkerGlobalScope',
|
||||
'MessageEvent',
|
||||
]
|
17
examples/raytrace-parallel/README.md
Normal file
17
examples/raytrace-parallel/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Parallel Raytracing
|
||||
|
||||
[View documentation for this example online][dox] or [View compiled example
|
||||
online][compiled]
|
||||
|
||||
[dox]: https://rustwasm.github.io/wasm-bindgen/examples/raytrace.html
|
||||
[compiled]: https://rustwasm.github.io/wasm-bindgen/exbuild/raytrace/
|
||||
|
||||
You can build the example locally with:
|
||||
|
||||
```
|
||||
$ ./build.sh
|
||||
```
|
||||
|
||||
(or running the commands on Windows manually)
|
||||
|
||||
and then visiting http://localhost:8080 in a browser should run the example!
|
3
examples/raytrace-parallel/Xargo.toml
Normal file
3
examples/raytrace-parallel/Xargo.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[dependencies.std]
|
||||
stage = 0
|
||||
features = ['wasm-bindgen-threads']
|
25
examples/raytrace-parallel/build.sh
Executable file
25
examples/raytrace-parallel/build.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -ex
|
||||
|
||||
# Two critical steps are required here to get this working:
|
||||
#
|
||||
# * First, the Rust standard library needs to be compiled. The default version
|
||||
# is not compatible with atomics so we need to compile a version, with xargo,
|
||||
# that is compatible.
|
||||
#
|
||||
# * Next we need to compile everything with the `atomics` feature enabled,
|
||||
# ensuring that LLVM will generate atomic instructions and such.
|
||||
RUSTFLAGS='-C target-feature=+atomics' \
|
||||
rustup run nightly xargo build --target wasm32-unknown-unknown --release
|
||||
|
||||
# Threading support is disabled by default in wasm-bindgen, so use an env var
|
||||
# here to turn it on for our bindings generation. Also note that webpack isn't
|
||||
# currently compatible with atomics, so we go with the --no-modules output.
|
||||
WASM_BINDGEN_THREADS=1 \
|
||||
cargo +nightly run --manifest-path ../../crates/cli/Cargo.toml \
|
||||
--bin wasm-bindgen -- \
|
||||
../../target/wasm32-unknown-unknown/release/raytrace_parallel.wasm --out-dir . \
|
||||
--no-modules
|
||||
|
||||
python3 -m http.server
|
226
examples/raytrace-parallel/index.html
Normal file
226
examples/raytrace-parallel/index.html
Normal file
@ -0,0 +1,226 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"/>
|
||||
<style>
|
||||
#scene {
|
||||
height: 100%;
|
||||
width: 500px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#render, .concurrency, .timing {
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
float: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id='scene'>
|
||||
{
|
||||
"width": 800,
|
||||
"height": 800,
|
||||
"fov": 90.0,
|
||||
"shadow_bias": 1e-13,
|
||||
"max_recursion_depth": 20,
|
||||
"elements": [
|
||||
{
|
||||
"Sphere" : {
|
||||
"center": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": -5.0
|
||||
},
|
||||
"radius": 1.0,
|
||||
"material": {
|
||||
"coloration" : {
|
||||
"Color": {
|
||||
"red": 0.2,
|
||||
"green": 1.0,
|
||||
"blue": 0.2
|
||||
}
|
||||
},
|
||||
"albedo": 0.18,
|
||||
"surface": {
|
||||
"Reflective": {
|
||||
"reflectivity": 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sphere" : {
|
||||
"center": {
|
||||
"x": -3.0,
|
||||
"y": 1.0,
|
||||
"z": -6.0
|
||||
},
|
||||
"radius": 2.0,
|
||||
"material": {
|
||||
"coloration": {
|
||||
"Color": {
|
||||
"red": 1.0,
|
||||
"green": 1.0,
|
||||
"blue": 1.0
|
||||
}
|
||||
},
|
||||
"albedo": 0.58,
|
||||
"surface": "Diffuse"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sphere": {
|
||||
"center": {
|
||||
"x": 2.0,
|
||||
"y": 1.0,
|
||||
"z": -4.0
|
||||
},
|
||||
"radius": 1.5,
|
||||
"material": {
|
||||
"coloration": {
|
||||
"Color": {
|
||||
"red": 1.0,
|
||||
"green": 1.0,
|
||||
"blue": 1.0
|
||||
}
|
||||
},
|
||||
"albedo": 0.18,
|
||||
"surface": {
|
||||
"Refractive": {
|
||||
"index": 1.5,
|
||||
"transparency": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Plane": {
|
||||
"origin": {
|
||||
"x": 0.0,
|
||||
"y": -2.0,
|
||||
"z": -5.0
|
||||
},
|
||||
"normal": {
|
||||
"x": 0.0,
|
||||
"y": -1.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"material": {
|
||||
"coloration": {
|
||||
"Color": {
|
||||
"red": 1.0,
|
||||
"green": 1.0,
|
||||
"blue": 1.0
|
||||
}
|
||||
},
|
||||
"albedo": 0.18,
|
||||
"surface": {
|
||||
"Reflective": {
|
||||
"reflectivity": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Plane": {
|
||||
"origin": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": -20.0
|
||||
},
|
||||
"normal": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": -1.0
|
||||
},
|
||||
"material": {
|
||||
"coloration": {
|
||||
"Color": {
|
||||
"red": 0.2,
|
||||
"green": 0.3,
|
||||
"blue": 1.0
|
||||
}
|
||||
},
|
||||
"albedo": 0.38,
|
||||
"surface": "Diffuse"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"lights": [
|
||||
{
|
||||
"Spherical": {
|
||||
"position": {
|
||||
"x": -2.0,
|
||||
"y": 10.0,
|
||||
"z": -3.0
|
||||
},
|
||||
"color": {
|
||||
"red": 0.3,
|
||||
"green": 0.8,
|
||||
"blue": 0.3
|
||||
},
|
||||
"intensity": 10000.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Spherical": {
|
||||
"position": {
|
||||
"x": 0.25,
|
||||
"y": 0.0,
|
||||
"z": -2.0
|
||||
},
|
||||
"color": {
|
||||
"red": 0.8,
|
||||
"green": 0.3,
|
||||
"blue": 0.3
|
||||
},
|
||||
"intensity": 250.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"Directional": {
|
||||
"direction": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": -1.0
|
||||
},
|
||||
"color": {
|
||||
"red": 1.0,
|
||||
"green": 1.0,
|
||||
"blue": 1.0
|
||||
},
|
||||
"intensity": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</textarea>
|
||||
|
||||
<button disabled id='render'>Loading wasm...</button>
|
||||
<div class='concurrency'>
|
||||
<p id='concurrency-amt'>Concurrency: 1</p>
|
||||
<br/>
|
||||
<input disabled type="range" id="concurrency" min="0" max="1" />
|
||||
</div>
|
||||
<div id='timing'>
|
||||
Render duration:
|
||||
<p id='timing-val'></p>
|
||||
</div>
|
||||
|
||||
|
||||
<canvas id='canvas'></canvas>
|
||||
|
||||
<script>
|
||||
delete WebAssembly.instantiateStreaming;
|
||||
document.getElementById('render').disabled = true;
|
||||
document.getElementById('concurrency').disabled = true;
|
||||
</script>
|
||||
<script src='raytrace_parallel.js'></script>
|
||||
<script src='index.js'></script>
|
||||
</body>
|
||||
</html>
|
119
examples/raytrace-parallel/index.js
Normal file
119
examples/raytrace-parallel/index.js
Normal file
@ -0,0 +1,119 @@
|
||||
const button = document.getElementById('render');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const scene = document.getElementById('scene');
|
||||
const concurrency = document.getElementById('concurrency');
|
||||
const concurrencyAmt = document.getElementById('concurrency-amt');
|
||||
const timing = document.getElementById('timing');
|
||||
const timingVal = document.getElementById('timing-val');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
button.disabled = true;
|
||||
concurrency.disabled = true;
|
||||
|
||||
// First up, but try to do feature detection to provide better error messages
|
||||
function loadWasm() {
|
||||
if (typeof SharedArrayBuffer !== 'function') {
|
||||
alert('this browser does not have SharedArrayBuffer support enabled');
|
||||
return
|
||||
}
|
||||
// Test for bulk memory operations with passive data segments
|
||||
// (module (memory 1) (data passive ""))
|
||||
const buf = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||
0x05, 0x03, 0x01, 0x00, 0x01, 0x0b, 0x03, 0x01, 0x01, 0x00]);
|
||||
if (!WebAssembly.validate(buf)) {
|
||||
alert('this browser does not support passive wasm memory, demo does not work');
|
||||
return
|
||||
}
|
||||
|
||||
wasm_bindgen('./raytrace_parallel_bg.wasm')
|
||||
.then(run)
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
loadWasm();
|
||||
|
||||
const { Scene, WorkerPool } = wasm_bindgen;
|
||||
|
||||
function run() {
|
||||
// The maximal concurrency of our web worker pool is `hardwareConcurrency`,
|
||||
// so set that up here and this ideally is the only location we create web
|
||||
// workers.
|
||||
pool = new WorkerPool(navigator.hardwareConcurrency);
|
||||
|
||||
// Configure various buttons and such.
|
||||
button.onclick = function() {
|
||||
console.time('render');
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(scene.value);
|
||||
} catch(e) {
|
||||
alert(`invalid json: ${e}`);
|
||||
return
|
||||
}
|
||||
canvas.width = json.width;
|
||||
canvas.height = json.height;
|
||||
render(new Scene(json));
|
||||
};
|
||||
button.innerText = 'Render!';
|
||||
button.disabled = false;
|
||||
|
||||
concurrency.oninput = function() {
|
||||
concurrencyAmt.innerText = 'Concurrency: ' + concurrency.value;
|
||||
};
|
||||
concurrency.min = 1;
|
||||
concurrency.step = 1;
|
||||
concurrency.max = navigator.hardwareConcurrency;
|
||||
concurrency.value = concurrency.max;
|
||||
concurrency.oninput();
|
||||
concurrency.disabled = false;
|
||||
}
|
||||
|
||||
let rendering = null;
|
||||
let start = null;
|
||||
let interval = null;
|
||||
let pool = null;
|
||||
|
||||
class State {
|
||||
constructor(wasm) {
|
||||
this.start = performance.now();
|
||||
this.wasm = wasm;
|
||||
this.running = true;
|
||||
this.counter = 1;
|
||||
|
||||
this.interval = setInterval(() => this.updateTimer(), 100);
|
||||
|
||||
wasm.promise()
|
||||
.then(() => {
|
||||
this.updateTimer();
|
||||
this.stop();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
updateTimer() {
|
||||
const dur = performance.now() - this.start;
|
||||
timingVal.innerText = `${dur}ms`;
|
||||
this.counter += 1;
|
||||
if (this.wasm && this.counter % 3 == 0)
|
||||
this.wasm.requestUpdate();
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.running)
|
||||
return;
|
||||
console.timeEnd('render');
|
||||
this.running = false;
|
||||
pool = this.wasm.cancel(); // this frees `wasm`, returning the worker pool
|
||||
this.wasm = null;
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
function render(scene) {
|
||||
if (rendering) {
|
||||
rendering.stop();
|
||||
rendering = null;
|
||||
}
|
||||
rendering = new State(scene.render(concurrency.value, pool, ctx));
|
||||
pool = null; // previous call took ownership of `pool`, zero it out here too
|
||||
}
|
364
examples/raytrace-parallel/src/lib.rs
Normal file
364
examples/raytrace-parallel/src/lib.rs
Normal file
@ -0,0 +1,364 @@
|
||||
extern crate futures;
|
||||
extern crate js_sys;
|
||||
extern crate raytracer;
|
||||
extern crate wasm_bindgen;
|
||||
extern crate web_sys;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::cmp;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicUsize, AtomicBool, Ordering::SeqCst};
|
||||
use std::sync::atomic::ATOMIC_USIZE_INIT;
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use futures::Future;
|
||||
use futures::sync::oneshot;
|
||||
use js_sys::{Promise, Error, WebAssembly, Uint8ClampedArray, Array};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{CanvasRenderingContext2d, Worker, Event, ErrorEvent};
|
||||
use web_sys::{DedicatedWorkerGlobalScope, MessageEvent};
|
||||
|
||||
macro_rules! console_log {
|
||||
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
fn log(s: &str);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Scene {
|
||||
inner: raytracer::scene::Scene,
|
||||
}
|
||||
|
||||
static NEXT_ID: AtomicUsize = ATOMIC_USIZE_INIT;
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl 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()))?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
self,
|
||||
concurrency: usize,
|
||||
pool: WorkerPool,
|
||||
ctx: CanvasRenderingContext2d,
|
||||
) -> Result<RenderingScene, JsValue> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let rx = rx.then(|_| Ok(JsValue::undefined()));
|
||||
|
||||
let data = Rc::new(RefCell::new(None::<Render>));
|
||||
|
||||
let pixels = (self.inner.width * self.inner.height) as usize;
|
||||
let mut r = Render {
|
||||
tx: Some(tx),
|
||||
callback: None,
|
||||
shared: Arc::new(Shared {
|
||||
id: NEXT_ID.fetch_add(1, SeqCst),
|
||||
need_update: AtomicBool::new(false),
|
||||
scene: self.inner,
|
||||
next_pixel: AtomicUsize::new(0),
|
||||
remaining: AtomicUsize::new(concurrency),
|
||||
rgb_data: Mutex::new(vec![0; 4 * pixels]),
|
||||
}),
|
||||
ctx,
|
||||
};
|
||||
|
||||
let data2 = data.clone();
|
||||
let callback = Closure::wrap(Box::new(move |msg: Event| -> Result<(), JsValue> {
|
||||
let mut slot = data2.borrow_mut();
|
||||
if let Some(mut data) = slot.take() {
|
||||
match data.event(&msg) {
|
||||
Ok(true) => {}
|
||||
Ok(false) => *slot = Some(data),
|
||||
Err(e) => {
|
||||
*slot = Some(data);
|
||||
return Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}) as Box<FnMut(_) -> Result<(), JsValue>>);
|
||||
|
||||
for worker in &pool.workers[..concurrency] {
|
||||
let ptr_to_send = Arc::into_raw(r.shared.clone()) as u32;
|
||||
let ptr_to_send = JsValue::from(ptr_to_send);
|
||||
worker.post_message(&ptr_to_send)?;
|
||||
worker.set_onmessage(Some(callback.as_ref().unchecked_ref()));
|
||||
worker.set_onerror(Some(callback.as_ref().unchecked_ref()));
|
||||
}
|
||||
|
||||
r.callback = Some(callback);
|
||||
*data.borrow_mut() = Some(r);
|
||||
|
||||
Ok(RenderingScene {
|
||||
inner: data,
|
||||
promise: wasm_bindgen_futures::future_to_promise(rx),
|
||||
pool,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct WorkerPool {
|
||||
workers: Vec<Worker>,
|
||||
callback: Closure<FnMut(Event)>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WorkerPool {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(max: u32) -> Result<WorkerPool, JsValue> {
|
||||
let callback = Closure::wrap(Box::new(|event: Event| {
|
||||
console_log!("unhandled event: {}", event.type_());
|
||||
}) as Box<FnMut(Event)>);
|
||||
let mut workers = Vec::new();
|
||||
for _ in 0..max {
|
||||
// TODO: what do do about `./worker.js`:
|
||||
//
|
||||
// * the path is only known by the bundler. How can we, as a
|
||||
// library, know what's going on?
|
||||
// * How do we not fetch a script N times? It internally then
|
||||
// causes another script to get fetched N times...
|
||||
let worker = Worker::new("./worker.js")?;
|
||||
let array = js_sys::Array::new();
|
||||
array.push(&wasm_bindgen::module());
|
||||
|
||||
// TODO: memory allocation error handling here is hard:
|
||||
//
|
||||
// * How to we make sure that our strong ref made it to a client
|
||||
// thread?
|
||||
// * Need to handle the `?` on `post_message` as well.
|
||||
array.push(&wasm_bindgen::memory());
|
||||
worker.post_message(array.as_ref())?;
|
||||
worker.set_onmessage(Some(callback.as_ref().unchecked_ref()));
|
||||
worker.set_onerror(Some(callback.as_ref().unchecked_ref()));
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
Ok(WorkerPool {
|
||||
workers,
|
||||
callback,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WorkerPool {
|
||||
fn drop(&mut self) {
|
||||
for worker in self.workers.iter() {
|
||||
worker.terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct RenderingScene {
|
||||
inner: Rc<RefCell<Option<Render>>>,
|
||||
promise: Promise,
|
||||
pool: WorkerPool,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl RenderingScene {
|
||||
pub fn promise(&self) -> Promise {
|
||||
self.promise.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = requestUpdate)]
|
||||
pub fn request_update(&self) {
|
||||
if let Some(render) = self.inner.borrow().as_ref() {
|
||||
render.shared.need_update.store(true, SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel(self) -> WorkerPool {
|
||||
if let Some(render) = self.inner.borrow_mut().take() {
|
||||
// drain the rest of the pixels to cause all workers to cancel ASAP.
|
||||
let pixels = render.shared.scene.width * render.shared.scene.height;
|
||||
render.shared.next_pixel.fetch_add(pixels as usize, SeqCst);
|
||||
}
|
||||
for worker in self.pool.workers.iter() {
|
||||
worker.set_onmessage(Some(&self.pool.callback.as_ref().unchecked_ref()));
|
||||
worker.set_onerror(Some(&self.pool.callback.as_ref().unchecked_ref()));
|
||||
}
|
||||
self.pool
|
||||
}
|
||||
}
|
||||
|
||||
struct Render {
|
||||
callback: Option<Closure<FnMut(Event) -> Result<(), JsValue>>>,
|
||||
tx: Option<oneshot::Sender<()>>,
|
||||
shared: Arc<Shared>,
|
||||
ctx: CanvasRenderingContext2d,
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
id: usize,
|
||||
need_update: AtomicBool,
|
||||
scene: raytracer::scene::Scene,
|
||||
next_pixel: AtomicUsize,
|
||||
remaining: AtomicUsize,
|
||||
rgb_data: Mutex<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern {
|
||||
type ImageData;
|
||||
|
||||
#[wasm_bindgen(constructor, catch)]
|
||||
fn new(data: &Uint8ClampedArray, width: f64, height: f64) -> Result<ImageData, JsValue>;
|
||||
}
|
||||
|
||||
impl Render {
|
||||
fn event(&mut self, event: &Event) -> Result<bool, JsValue> {
|
||||
if let Some(error) = event.dyn_ref::<ErrorEvent>() {
|
||||
let msg = format!("error in worker: {}", error.message());
|
||||
return Err(Error::new(&msg).into());
|
||||
}
|
||||
|
||||
if let Some(msg) = event.dyn_ref::<MessageEvent>() {
|
||||
let data = msg.data();
|
||||
if let Some(data) = data.dyn_ref::<Array>() {
|
||||
let id = data.pop();
|
||||
let done = data.pop();
|
||||
let image = data.pop();
|
||||
if let Some(id) = id.as_f64() {
|
||||
if id == self.shared.id as f64 {
|
||||
self.ctx.put_image_data(image.unchecked_ref(), 0.0, 0.0)?;
|
||||
return Ok(done.as_bool() == Some(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
console_log!("unhandled message: {:?}", data);
|
||||
return Ok(false)
|
||||
}
|
||||
|
||||
console_log!("unhandled event: {}", event.type_());
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn child_entry_point(ptr: u32) -> Result<(), JsValue> {
|
||||
let ptr = unsafe {
|
||||
Arc::from_raw(ptr as *const Shared)
|
||||
};
|
||||
assert_send(&ptr);
|
||||
|
||||
let global = js_sys::global()
|
||||
.unchecked_into::<DedicatedWorkerGlobalScope>();
|
||||
ptr.work(&global)?;
|
||||
|
||||
return Ok(());
|
||||
|
||||
fn assert_send<T: Send + 'static>(_: &T) {}
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
fn work(&self, global: &DedicatedWorkerGlobalScope) -> Result<(), JsValue> {
|
||||
// Once we're done raytracing a pixel we need to actually write its rgb
|
||||
// value into the shared memory buffer for our image. This, however,
|
||||
// requires synchronization with other threads (as currently
|
||||
// implemented). To help amortize the cost of synchronization each
|
||||
// thread processes a chunk of pixels at a time, and this number is how
|
||||
// many pixes will be rendered synchronously before committing them back
|
||||
// to memory.
|
||||
const BLOCK: usize = 1024;
|
||||
|
||||
let width = self.scene.width as usize;
|
||||
let height = self.scene.height as usize;
|
||||
let end = width * height;
|
||||
|
||||
// Thread-local storage for our RGB data, commited back in one batch to
|
||||
// the main image memory.
|
||||
let mut local_rgb = [0; BLOCK * 4];
|
||||
|
||||
loop {
|
||||
// First up, grab a block of pixels to render using an atomic add.
|
||||
// If we're beyond the end then we're done!
|
||||
let start = self.next_pixel.fetch_add(BLOCK, SeqCst);
|
||||
if start >= end {
|
||||
break
|
||||
}
|
||||
|
||||
// Raytrace all our pixels synchronously, writing all the results
|
||||
// into our local memory buffer.
|
||||
let len = cmp::min(end, start + BLOCK) - start;
|
||||
for (i, dst) in local_rgb.chunks_mut(4).enumerate().take(len) {
|
||||
let x = (start + i) % width;
|
||||
let y = (start + i) / width;
|
||||
let ray = raytracer::Ray::create_prime(x as u32, y as u32, &self.scene);
|
||||
let result = raytracer::cast_ray(&self.scene, &ray, 0).to_rgba();
|
||||
dst[0] = result.data[0];
|
||||
dst[1] = result.data[1];
|
||||
dst[2] = result.data[2];
|
||||
dst[3] = result.data[3];
|
||||
}
|
||||
|
||||
// Ok, time to synchronize and commit this data back into the main
|
||||
// image buffer for other threads and the main thread to see.
|
||||
let mut data = self.rgb_data.lock().unwrap();
|
||||
data[start * 4..(start + len) * 4]
|
||||
.copy_from_slice(&mut local_rgb[..len * 4]);
|
||||
|
||||
// As a "nifty feature" we try to have a live progressive rendering.
|
||||
// That means that we need to periodically send an `ImageData` to
|
||||
// the main thread. Do so whenever the main thread requests it.
|
||||
if self.need_update.swap(false, SeqCst) {
|
||||
self.update_image(false, data, global)?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we're the last thread out, be sure to update the main thread's
|
||||
// image as this is the last chance we'll get!
|
||||
if self.remaining.fetch_sub(1, SeqCst) == 1 {
|
||||
let data = self.rgb_data.lock().unwrap();
|
||||
self.update_image(true, data, global)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_image(
|
||||
&self,
|
||||
done: bool,
|
||||
data: MutexGuard<Vec<u8>>,
|
||||
global: &DedicatedWorkerGlobalScope,
|
||||
) -> Result<(), JsValue> {
|
||||
// This is pretty icky. We can't create an `ImageData` backed by
|
||||
// `SharedArrayBuffer`, so we need to copy the memory into a local
|
||||
// JS array using `slice`. This means we can't use
|
||||
// `web_sys::ImageData` right now but rather we have to use our own
|
||||
// binding.
|
||||
let mem = wasm_bindgen::memory()
|
||||
.unchecked_into::<WebAssembly::Memory>();
|
||||
let mem = Uint8ClampedArray::new(&mem.buffer())
|
||||
.slice(
|
||||
data.as_ptr() as u32,
|
||||
data.as_ptr() as u32 + data.len() as u32,
|
||||
);
|
||||
drop(data); // unlock the lock, we've copied the data now
|
||||
let data = ImageData::new(
|
||||
&mem,
|
||||
self.scene.width as f64,
|
||||
self.scene.height as f64,
|
||||
)?;
|
||||
let arr = Array::new();
|
||||
arr.push(data.as_ref());
|
||||
arr.push(&JsValue::from(done));
|
||||
arr.push(&JsValue::from(self.id as f64));
|
||||
global.post_message(arr.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
32
examples/raytrace-parallel/worker.js
Normal file
32
examples/raytrace-parallel/worker.js
Normal file
@ -0,0 +1,32 @@
|
||||
// synchronously, using the browser, import out shim JS scripts
|
||||
importScripts('raytrace_parallel.js');
|
||||
|
||||
let booted = false;
|
||||
let lastPtr = null;
|
||||
|
||||
// Wait for the main thread to send us the shared module/memory. Once we've got
|
||||
// it, initialize it all with the `wasm_bindgen` global we imported via
|
||||
// `importScripts`.
|
||||
//
|
||||
// After our first message all subsequent messages are an entry point to run,
|
||||
// so we just do that.
|
||||
self.onmessage = function(args) {
|
||||
self.onmessage = event => run(event.data);
|
||||
const [module, memory] = args.data;
|
||||
wasm_bindgen(module, memory)
|
||||
.then(() => {
|
||||
booted = true;
|
||||
if (lastPtr)
|
||||
run(lastPtr);
|
||||
})
|
||||
.catch(e => setTimeout(() => { throw e; })); // propagate to main `onerror`
|
||||
};
|
||||
|
||||
function run(ptr) {
|
||||
if (!booted) {
|
||||
lastPtr = ptr;
|
||||
return;
|
||||
}
|
||||
lastPtr = null;
|
||||
wasm_bindgen.child_entry_point(ptr);
|
||||
}
|
Reference in New Issue
Block a user