Skip to content

await ctx.commit() is an illusion #1

@juj

Description

@juj

In 5f969dd#diff-fe83e208ade4a4d8de4dc610010e1271R91, there is an example to illustrate how to set up a synchronously blocking main loop:

"Another possibility is to use the async/await syntax:"

async function animationLoop() {
  var promise;
  do {
    //draw stuff
    (...)
    promise = ctx.commit()
    // do post commit work
    (...)
  } while (await promise);
}

However this does not actually do what readers expect - and especially - it does not solve what @OleksandrChekhovskyi was asking for in https://discourse.wicg.io/t/offscreencanvas-animations-in-workers/1989/15. That is, the comment

"Okay, I think we've got it. Basically, the syntax for getting a commit that throttles by blocking (as Oleksadr suggests) would simply be "await context.commit();". And of course, this is much better than hard-blocking the entire thread."

unfortunately does not solve the use case for a synchronous main loop.

The subtle issue is that an async function is a function that returns a Promise, and calling await immediately returns from the calling function (chain of async functions) and continues execution from the first non-async function. This effect breaks the computation model that is required for WebAssembly/Emscripten-based applications that set up their own main loops.

Here is a more concrete example based on OffscreenCanvas prototypes, which attempts to set up a blocking main loop, but fails due to the subtlety:

webgl_modal_loop.html:

<html><body><canvas id='canvas'>
<script>
var htmlCanvas = document.getElementById("canvas");
var offscreen = htmlCanvas.transferControlToOffscreen();
var worker = new Worker("webgl_worker.js"); 
worker.postMessage({canvas: offscreen}, [offscreen]);
</script>
</body></html>

webgl_worker.js:

/*
// This is a simulation of a blocking GL.commit(): (can try as alternative to real GL.commit() if OffscreenCanvas not yet implemented)
function commit() {
  var displayRefreshRate = 1; // Simulate a 1Hz display for easy observing
  return new Promise(function (resolve, reject) {
    setTimeout(function() {
      resolve();
    }, 1000/displayRefreshRate);
  });
}
*/

var gl = null;

async function renderLoop() {
  var frameNumber = 0;
  // Start our modal rendering loop
  for(;;) {
    gl.clearColor(performance.now(), performance.now(), performance.now(), performance.now());
    gl.clear(gl.COLOR_BUFFER_BIT);
    await gl.commit();
//    await commit(); // Alternatively to try out simulated gl.commit() in the absence of real thing

    console.log('rendered frame ' + frameNumber++);
    if (frameNumber > 10) break; // Stop test after 10 frames to not flood the browser
  }
}

function init(evt) {
  console.log('init');
  // Startup initialization for the application
  var canvas = evt.data.canvas;
  gl = canvas.getContext("webgl");
}

function runGame() {
  console.log('runGame');
  renderLoop();
}

function deinit() {
  console.log('deinit');
  gl = null; // tear down
}

onmessage = function(evt) {
  // Game "main": init, run loop, and deinit
  init(evt);
  runGame();
  deinit();
};

The expectation from a synchronously blocking execution is that the above application should print out

init
runGame
rendered frame 0
rendered frame 1
rendered frame 2
rendered frame 3
rendered frame 4
rendered frame 5
rendered frame 6
rendered frame 7
rendered frame 8
rendered frame 9
rendered frame 10
deinit

but instead, running the page will print out

init
runGame
deinit
rendered frame 0
<throw since gl is null>

This is because the onmessage function will continue executing immediately after the the await gl.commit(); is called, and deinit() will be called, breaking down the synchronous programming model.

I am currently implementing OffscreenCanvas support to Emscripten, and trying to figure out how to implement vsync synchronization when a Worker is rendering via OffscreenCanvas. In the absence of a GL.commit(blockUntilVsyncFinished=true) type of API or similar help from the OffscreenCanvas spec, my current thinking is to set up a requestAnimationFrame loop in the main browser thread, and use that to ping "vsync finished" events to a Worker, via SharedArrayBuffer. This will work if OffscreenCanvas based rendering is guaranteed to still allow observing "proper" requestAnimationFrame synchronization on the main browser thread, though I am not sure if OffscreenCanvas currently says anything about this?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions