[whatwg] Canvas in Workers

Over the years, there have been lots of requests for asynchronous image 
manipulation APIs, and each time we discuss it we end up concluding that 
what we should really do is just put canvas on workers.

I've now specced a concrete proposal for this.

Since we can't literally put <canvas> in a worker since workers don't have 
DOMs, I was faced with two options:

Option A: Provide an API for off-screen graphics in workers, requiring 
that every frame you package the whole thing up, send it over to the main 
thread, and blt it there.

Option B: Provide a mechanism by which a worker can actually paint 
directly onto a main thread <canvas> element.

I ended up specifying something that lets you do both. You can instantiate 
a CanvasRenderingContext2D object anywhere, including a worker; when a 
rendering context is not bound to a specific canvas, it can be used as an 
off-screen drawing surface. You can then bind such a rendering context to 
a canvas; when you do this, there is a commit() method that you can use 
which tells the user agent to push the bitmap drawn on the 
CanvasRenderingContext2D object to the canvas bitmap / screen.

A quick example of how this can work:

   // main.html
   <canvas></canvas>
   <script>
    var canvas = document.getElementsByTagName('canvas')[0];
    var worker = new Worker('clock.js');
    var proxy = canvas.transferControlToProxy());
    worker.postMessage(proxy, [proxy]);
   </script>

   // clock.js worker
   onmessage = function (event) {
     var context = new CanvasRenderingContext2d();
     event.data.setContext(context);
     setInterval(function () {
       context.clearRect(0, 0, context.width, context.height);
       context.fillText(0, 100, new Date());
       context.commit();
     }, 1000);
   };

There are several difficulties in a canvas-in-worker solution that I'd 
like to mention here, with a brief pointer to the solutions I used:

1. Workers had no way to represent images (e.g. sprites) before, which is 
rather limiting for a bitmap canvas API. I have introduced a new object 
called ImageBitmap that can be constructed from a Blob obtained from XHR 
in a worker, or that can be created from <img>, <video>, and <canvas> 
elements and transfered over to a worker. This can then be used to render 
images in workers.

2. The 2D rendering context API has several features that actually 
interact with the page, e.g. scrollIntoView(). I've tried to make these 
work too; there's a mechanism by which these actions are queued up and 
then when you call commit() they're run as a task on the main thread.

3. Since the main thread paints on its own schedule, and the worker might 
be half-way through its drawing when the main thread wants to paint, I've 
had to introduce an explicit commit() that tells the UA to push the data 
from the worker to the rendering.

4. Since you can read the image from the main thread (for example, using 
the context.drawImage(canvas, ...) method, or even, now that there's the 
ImageBitmap object, using "new ImageBitmap(canvas)"), as well as read the 
image from the worker (using the same mechanisms, but using the context as 
the source instead of the canvas -- we have to support this, because it's 
common to do things like fade out the current scene, or shift it a bit, or 
otherwise manipulate the image itself), and given that the rendered bitmap 
is not the same as the currently drawing one (since we have to commit, see 
above), I've ended up having to specify that when you're on a worker, a 
canvas really has two bitmaps, the scratch bitmap and the output bitmap. 
This does, if the author actually tries to read the scratch bitmap, mean 
that there's a greater memory requirement when working on a worker, but I 
really don't see a way around it.

5. We don't yet have a way to access Web Fonts in a worker. I'm relying on 
the CSS Fonts module's FontLoader proposal for this.

At the same time, the WebGL community has been looking for a way to bind a 
single rendering context to a several rendering contexts in sequence, 
allowing the same scene to be painted from different angles. I also tried 
to support this model; it will require some additions to WebGL to use the 
hooks that have been provided. Specifically:

 - define the WebGLRenderingContext constructor.

 - define the "binding steps" and the "unbinding steps".

 - define a way to specify the settings for how to render to bitmaps, for 
   example whether to anti-alias or not. If you want to have this 
   information be on the canvas elements themselves, please let me know; 
   you'll need to come up with a mechanism for how to do this cross-thread
   without race conditions.

 - define when pixels are pushed to "the canvas's bitmap". In particular, 
   this needs to handle how to do it when the canvas is in a different 
   thread, such that it is well-defined what bitmap an author gets if they
   use the HTMLCanvasElement itself in a drawImage() call. (See the 
   definition of commit() and the "commit the scratch bitmap" algorithm 
   for how the 2D case does it now.)

 - manage the canvas bitmap's "clean-origin" flag.

If WebGLRenderingContext is supposed to react to the <canvas> element's 
"width" and "height" attributes being mutated, please let me know so that 
I can update the spec accordingly.

I've tried to leave the old getContext() mechanism mostly untouched, in 
terms of normative requirements. The new model is alongside it. So for 
example, the new model involves a scratch bitmap that you commit() to the 
output bitmap, whereas if you use getContext() you end up with a rendering 
context where the scratch bitmap andhhte output bitmap _are the same 
bitmap_ so anything you do immediately gets rendered (and commit() is not 
useful -- indeed it throws in this case).

As a result of this rewrite, and the way that the canvas spec now has to 
have much more intimate knowledge of rendering contexts, I've dropped the 
attempt to use a registry for rendering contexts. If a new one comes up, 
I'll just update the spec. (Proprietary extensions have a hook in the 
spec, in case anyone wants to spec them.)

Another result of this rewrite is that things like drawImage() are now 
specced much more precisely. I've also moved the 'origin-clean' stuff into 
the parts of the spec that actually affect it, rather than off in a 
separate section.

I haven't done anything to make requestAnimationFrame() be available on a 
worker, but we should do that. This will require changes to the rAF() 
spec, however.


Obviously all of this is just a proposal; if it turns out I took a huge 
wrong turn early on here then I'll happily throw it all out and try again!

Feedback is very welcome! :-)

-- 
Ian Hickson               U+1047E                )\._.,--....,'``.    fL
http://ln.hixie.ch/       U+263A                /,   _.. \   _\  ;`._ ,.
Things that are impossible just take longer.   `._.-(,_..'--(,_..'`-.;.'

Received on Friday, 16 November 2012 20:49:34 UTC