WebGL Off the Main Thread

We’re happy to announce WebGL in Web Workers in Firefox 44+! Using the new OffscreenCanvas API you can now create a WebGL context off of the main thread.

To follow along, you’ll need a copy of Firefox 44 or newer (currently Firefox Developer Edition or Firefox Nightly). You’ll have to enable this API by navigating to about:config in Firefox, searching for gfx.offscreencanvas.enabled, and setting it to true. You can grab the code examples from GitHub or preview it here in Firefox 44+ with gfx.offscreencanvas.enabled set to true. This functionality is not yet available on Windows pending ANGLE support. AlteredQualia points out things are running great on Windows/FF Nightly 46. Shame on me for not verifying!

Use Cases

This API is the first that allows a thread other than the main thread to change what is displayed to the user. This allows rendering to progress no matter what is going on in the main thread. You can see more use cases in the working group’s in-progress specification.

Code Changes

Let’s take a look at a basic example of WebGL animation from my Raw WebGL talk. We’ll port this code to run in a worker, rather than the main thread.

WebGL in Workers

The first step is moving all of the code from WebGL context creation to draw calls into a separate file.


<script src="gl-matrix.js"></script>
<script>
  // main thread
  var canvas = document.getElementById('myCanvas');
  ...
  gl.useProgram(program);
  ...

becomes:


// main thread
var canvas = document.getElementById('myCanvas');
if (!('transferControlToOffscreen' in canvas)) {
  throw new Error('webgl in worker unsupported');
}
var offscreen = canvas.transferControlToOffscreen();
var worker = new Worker('worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);
...

Recognize that we’re calling HTMLCanvasElement.prototype.transferControlToOffscreen, then transferring that to a newly constructed worker thread. transferControlToOffscreen returns a new object which is an instance of OffscreenCanvas, as opposed to HTMLCanvasElement. While similar, you can’t access properties like offscreen.clientWidth and offscreen.clientHeight, but you can access offscreen.width and offscreen.height. By passing it as the second argument to postMessage, we transfer ownership of the variable to the second thread.

Now in the worker thread, we’ll wait to receive the message from the main thread with the canvas element, before trying to get a WebGL context. The code for getting a WebGL context, creating and filling buffers, getting and setting attributes and uniforms, and drawing does not change.


// worker thread
importScripts('gl-matrix.js');

onmessage = function (e) {
  if (e.data.canvas) {
    createContext(e.data.canvas);
  }
};

function createContext (canvas) {
  var gl = canvas.getContext('webgl');
  ...

OffScreenCanvas simply adds one new method to WebGLRenderingContext.prototype called commit. The commit method will push the rendered image to the canvas element that created the OffscreenCanvas used by the WebGL context.

Animation Synchronization

Now to get the code animating, we can proxy requestAnimationFrame timings from the main thread to the worker with postMessage.


// main thread
(function tick (t) {
  worker.postMessage({ rAF: t });
  requestAnimationFrame(tick);
})(performance.now());

and onmessage in the worker becomes:


// worker thread
onmessage = function (e) {
  if (e.data.rAF && render) {
    render(e.data.rAF);
  } else if (e.data.canvas) {
    createContext(e.data.canvas);
  }
};

and our render function now has a final gl.commit(); statement rather than setting up another requestAnimationFrame loop.


// main thread
function render (dt) {
  // update
  ...
  // render
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
  requestAnimationFrame(render);
};

becomes:


// worker thread
function render (dt) {
  // update
  ...
  // render
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
  gl.commit(); // new for webgl in workers
};

Limitations with this approach

While in the example code, I’m not doing proper velocity based animation (by not using the value passed from requestAnimationFrame, I’m doing frame rate dependate animation, as opposed to the more correct velocity based animation which is frame rate independent), we still have an issue with this approach.

Assume we moved the rendering logic off of the main thread to avoid pauses from the JavaScript Virtual Machine’s Garbage Collector (GC pauses). GC pauses on the main thread will slow down invocations of requestAnimationFrame. Since calls to gl.drawArrays and gl.commit are asynchronously triggered in the worker thread by postMessages in a requestAnimationFrame loop on the main thread, GC pauses in the main thread will block rendering on the worker thread. Note: GC pauses in the main thread should not block progress in a worker thread (at least they don’t in Firefox’s SpiderMonkey Virtual Machine). GC pauses are per Worker in SpiderMonkey.

While we could try to do something clever in the worker to account for this, the solution will be to make requestAnimationFrame available in a Worker context. The bug tracking this work can be found here.

Summary

Developers will now be able to render to the screen without blocking on the main thread, thanks to the new OffscreenCanvas API. There’s still more work to do with getting requestAnimationFrame on Workers. I was able to port existing WebGL code to run in a worker in a few minutes. For comparison, see animation.html vs. animation-worker.html and worker.js.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)
Tagged on: , ,

Leave a Reply