[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Public WebGL] Issues with sharing resources across contexts



I agree that when dealing with two contexts on the same page it feels incredibly cumbersome to always be acquiring and releasing resources. I'm not sure what it would take to make that happen cleanly on the backend. In any case, I personally find the worker scenario more interesting/useful so I'll focus on that.

Since we're in a worker that is most likely explicitly created for the purposes of managing these shared resources having a callback-centric API feels like overkill. I'd rather have it be a blocking call, since it lends to cleaner code and less garbage creation. As a blocking call I imagine it would return a success/fail code of some sort (boolean is probably sufficient), which the user can detect and query gl.getError for more information (did it timeout, is the resource invalid, etc?) So your worker code from above now looks like this:

function render(data) {
  if(!gl.acquireResource(texture, gl.WRITE)) {
    // Query error info and do something with it
    self.postMessage({cmd: "Failed"});
    return;
  }

  gl.bindTexture(gl.TEXTURE_2D, texture);
  // And so on...  
};

That does feel slightly non-GL-ish to me, however. A more natural feeling API would be one where acquireResource actually returns the resource in question on success or null on failure, allowing it to mimic the behavior of createTexture/createBuffer/createFramebuffer/etc. This has the nice side effect of unhandled errors producing behavior that is already well defined. (We all know what gl.bindTexture(gl.TEXTURE_2D, null) should do.) Of course, for such an API to make sense the data passed between workers would no longer be the resource itself but instead some sort of shared resource id. That could play nicely into your concept of explicitly flagging shared resources, however:

//-- Main Thread --

function requestFrame() {
  var textureShareId = gl.shareResource(texture);
  worker.postMessage({cmd: 'render', resourceId: textureShareId});
}

//-- Worker --
function render(data) {
  texture = gl.acquireResource(data.resourceId, gl.WRITE);
  // Obviously I can check for null and query gl.getError here if I care...

  gl.bindTexture(gl.TEXTURE_2D, texture);
  // And so on...  
};

Now you are required to flag resources as shared in order to expose them (good for internal optimization) to other workers, and the API on the other end has more familiar behavior.

Just a thought!

--Brandon

On Thu, Aug 23, 2012 at 6:18 PM, Gregg Tavares (社用) <gman@google.com> wrote:
Actually, hmmm, I take this back.

It sounds like workers require a different solution than 2+ contexts in the same page. Ideally, if you have 2 contexts in the same page you shouldn't have to call acquire/release at all. Otherwise, the simple case of a 3D editor with multiple views, each view having a different canvas, would really suck as you'd have to acquire/release hundreds of resources.

Well, more food for thought.


On Thu, Aug 23, 2012 at 6:15 PM, Gregg Tavares (社用) <gman@google.com> wrote:
So I've been taking a look at getting something working. My first thought was doing a kind of auto sync. 

If you bind a resource and it has been modified by a different context since you last modified it I'd do a glFinish or glFlush/glWaitSync
If you try to use a resource and it's been modified by another context and you haven't bound it since then you'd get INVALID_OPERATION

I was hoping that would be the simplest way to make it work but unfortunately, to get that to work across workers requires too much locking. Every function that touches a shared resource has to lock the resource the entire time it's dealing with it.

So, that suggests the acquire/release method is probably a better way to go. Then locks are only needed during acquire and release. I'd still track modifications and generate INVALID_OPERATION if you haven't bound the resources in your context if has been modified by another context. Similarly I can auto insert the appropriate glFinish or glFlush/glWaitSync or similar synchronization.

Some questions come up about acquireResource though. 

How should it fail (gl ERROR? Exception?)
Should it fail at all, maybe it should be async as in gl.acquireResource(resource, flags, callback);

You then have the issue though of multiple callbacks, on multiple threads (workers and the main thread).

It's one of those things that seems like I need to implement it before I know if it sucks or not but I suppose we can write some pseudo code.

--main-page--
gl = canvas.getContext("webgl");

// make a texture to share.
texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, ....., width, height);

worker = new Worker("renderscene.js");

// Write a dispatch function for messages received from the worker.
worker. {
  switch (oEvent.data.cmd) {
    case 'initialized':
      requestFrame();
    case 'rendered':
      render();
  }
};

// Tell the worker to startup. Give it our shared
// texture and the shareGroup.
worker.postMessage({
  cmd: 'start',
  data: {
    texture:texture, 
    shareGroup: gl.getContextAttributes().shareGrouo
  }
});

// Ask the worker to render a frame.
function requestFrame() {
  worker.postMessage({cmd: 'render'});
}

// Called by the worker when a frame is ready.
function render()
   // Draw to canvas with texture that worker rendered to
   gl.acquireResource(texture, gl.READ_ONLY);

   // must bind to see the changes.
   gl.bindTexture(gl.TEXTURE_2D, texture); 

   gl.useProgram(drawQuadProgram);
   gl.drawArrays(gl.TRIANGLES, 0, 4);

   gl.releaseResource(texture);

   // Request a new frame using RAF.
   requestAnimationFrame(requestFrame);
}

--renderscene.js--

self. (oEvent) {
  switch (oEvent.data.cmd) {
    case 'start':
      init(oEvent.data.data);
      break;
    case 'render':
      render(oEvent.data.data);
      break;
   }
};

function init(data) {
  gl = createWebGLContextInWorkerMagic({
     shareGroup: data.shareGroup;
  });
  
  texture = data.texture;

  // Even though framebufferTexture2D does not modify or read
  // the texture to make sure it's not deleted when we try 
  // to attach it to or our FBO we need to acquire it.
  gl.acquireResource(texture, gl.READ_ONLY);

  // Make an FBO to use the texture.
  fbo = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.TEXTURE_2D, texture, 0);

  gl.releaseResource(texture);

  // Load all our scene data.
  ...

  self.postMessage({cmd: "initialized"});  
};

function render(data) {
  gl.acquireResource(texture, gl.WRITE);

  // This is not needed if we never write to this texture from
  // the main thread.
  gl.bindTexture(gl.TEXTURE_2D, texture);

  renderSceneToTextureThoughFBO(fbo);
  
  gl.releaseResource(texture);

  self.postMessage({cmd: "rendered"});  
};

Note: I'm not saying the code above is how you'd actually write this. At a minimum you'd probably want to double buffer 'texture' and figure out a more performant way of getting parallelization.

But, after writing this it seems like a non-async acquireResource that throws an exception if it can't acquire would be best? It would be easiest to use and you'd get an error immediately if you make a mistake.

One interesting question this brings up is when do you need to start using acquireResource/releaseResource. Meaning, right now, with 1 context you don't need to call it at all. It almost seems like we'd need a 'shareResource(resource)' function to mark those resources that are shared and therefore require calling acquireResource before being used vs those resources that don't. And if you don't call shareResource on an object you'd get an error if you tried to use it in another context.

Thoughts? I'm thinking out loud here.