HandlingContextLost

From WebGL Public Wiki
Jump to navigation Jump to search

Handling Lost Context in WebGL

WebGL makes the amazing power of your GPU available to JavaScript to do all kinds of fancy and wonderful things. Unfortunately the GPU is a shared resource and as such there are times when it might be taken away from your program. Examples: Another page does something that takes the GPU too long and the browser or the OS decides to reset the GPU to get control back. 2 or more pages use too many resources and the browser decides to tell all the pages they lost the context and then restore it only to the front page for now. The user switches graphics cards (Turns on/off one or more in the control panel) or updates their driver (no reboot required on Windows7)

In all these cases and more your program may lose its WebGL context. By default when a WebGL program loses the context it never gets it back. To recover from a lost context you must take the following steps

1) add a lost context handler and tell it to prevent the default behavior

var canvas = document.getElementById("myCanvas");
canvas.addEventListener("webglcontextlost", function(event) {
    event.preventDefault();
}, false);

2) re-setup all your WebGL state and re-create all your WebGL resources when the context is restored.

canvas.addEventListener(
    "webglcontextrestored", setupWebGLStateAndResources, false);

At the point that setupWebGLStateAndResources is called the browser has reset all state to the default WebGL state and all previously allocated resources are invalid. So, you need to re-create textures, buffers, framebuffers, renderbuffers, shaders, programs, and setup your state (clearColor, blendFunc, depthFunc, etc...)

For many programs this is a simple matter of calling your init or setup function again.

We went through the demos in the Khronos WebGL SDK and made them handle lost context. Below are some of the issues we ran into and the solutions we used.

Handling getError

If you are checking for

var error = gl.getError();
if (error != gl.NO_ERROR) {  
  alert("fail");
}

change your code to

var error = gl.getError();
if (error != gl.NO_ERROR && error != gl.CONTEXT_LOST_WEBGL) {
  alert("fail");
}

Otherwise your program will exit or alert during lost context.

Handling Shaders and Programs

When checking for shader compilation and program linking success check that the context is not lost.

var shader = gl.createShader(type);
gl.shaderSource(shader, shaderSrc);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) &&
    !gl.isContextLost()) {
   var infoLog = gl.getShaderInfoLog(shader);
   alert("Error compiling shader:\n" + infoLog);   ...
}

Similarly for programs

gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked && !gl.isContextLost()) {
  var infoLog = gl.getProgramInfoLog(program);
  alert("Error linking program:\n" + infoLog);  
  ...
}

Don’t check for null on creation

var program = gl.createProgram();
if (program === null) // BAD!!!

You should really only get null from these functions if the context is lost and in that case there’s no reason to check. WebGL is designed so that for the most part everything just runs as normal even when a program or shader is null.

Don’t put attributes on WebGL resource objects

var tex = gl.createTexture();
tex.image = new Image();  // BAD!!!

‘tex’ will be null if the context is lost and you’ll get a JavaScript exception trying to add a field to null.

Turn off your rendering loop on lost context

var requestId;
function main() {
  canvas = document.getElementById("myCanvas");
  canvas.addEventListener(
      webglcontextlost, handleContextLost, false);
   ...
   init();
}

function init() {
  ...
  draw();
}

function draw() {
   ...
   draw webgl stuff
   ...  
   requestId = requestAnimationFrame(draw, canvas);
}

function handleContextLost(event) {
   event.preventDefault();
   cancelRequestAnimationFrame(requestId);
}

Deal with scoping issues

In one sample there was some code like this

function init() {
   ...
   var mouseUniformLocation = gl.getUniformLocation(program, "uMouse");
   ...
   if (!once) {
       once = true;     
       canvas.addEventListener("mousemove", function(event) {
           gl.uniform2f(mouseUniformLocation, event.x, event.y);
       }, false);
   ...

Why didn’t this work with context lost/restored? Because the anonymous function in the event listener was using the object bound to the variable mouseUniformLocation that was created the first time through init. When the context is restored, the second time through init, mouseUniformLocation is pointing to a new object but the anonymous function assigned to the listener was still looking at the object that was there AT THE TIME IT WAS ORIGINALLY BOUND.

There are lots of solutions to this issue. The one I chose was to take make the function named instead of anonymous, then I could remove them like this

function init() {
  canvas.removeEventListener("mousemove", mouseMoveHandler, false);
  ...
  var mouseUniformLocation = gl.getUniformLocation(program, "uMouse");
  ...
  function mouseMoveHandler(event) {
    gl.uniform2f(mouseUniformLocation, event.x, event.y);
  }

  canvas.addEventListener("mousemove", mouseMoveHandler, false);
  ...

Deal with outstanding asynchronous requests

You ask the browser to load 5 images, before they finish loading you lose the context. Two cases come to mind. One, they finish loading before the context is restored. Two, they finish loading after the context has been restored. In either case you probably need to do something.

The simplest solution might just be to make sure those requests are ignored when they complete.

// Array of outstanding image requests.
var loadingImages = [];
// load an image asynchronously
function loadImage(url) {
  var img = new Image();
  img.src = url;
  img.onload = function() {
    // remove from loadingLmages
    loadingImages.splice(loadingImages.indexOf(img), 1);
    makeTextureFromImage(img);
  }  loadingImages.push(img);
}

function handleContextLost(event) {
   ...
   // ignore all pending loading images by removing
   // their onload handler
   for (var ii = 0; ii < loadingImages.length; ++ii) {
     loadingImages[ii].onload = undefined;
   }
   loadingImages = [];
}

Use the context lost simulator to help find issues

You can lose the context AT ANY TIME! In otherwords, you can lose the context part way through initialization. You can also lose the context immediately after calling canvas.getContext. You can lose the context between any 2 WebGL function calls.

You need to check your code for subtle bugs that, for example, only happen if the context is lost between the Nth and Nth+1 calls in your program.

<script src="webgl-debug.js"></script>
var canvas = document.getElementById("canvas");
canvas = WebGLDebugUtils.makeLostContextSimulatingCanvas(canvas);
canvas.loseContextInNCalls(5);

canvas.addEventListener("webglcontextlost", handleContextLost, false);
canvas.addEventListener("webglcontextrestored", handleContextRestored, false);

var gl = canvas.getContext("webgl");

The simulator has a few other functions. loseContext lets you manually lose the context. For example

// lose the context when I press click the mouse.
window.addEventListener("mousedown", function() {
  canvas.loseContext();
}

getNumCalls lets you get the number of WebGL calls so far so you find out a good number to pass to loseContextInNCalls

  var canvas = document.getElementById("canvas");

  canvas = WebGLDebugUtils.makeLostContextSimulatingCanvas(canvas);
  ...
  var gl = canvas.getContext("webgl");
  gl.clearColor(1,0,0,1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  console.log("num calls: " + canvas.getNumCalls());  // should print 2
  ...

By default when the context is lost it will immediately be restored. If you'd like to make it restore later you can either set the recovery time to some number of milliseconds

canvas.setRestoreTimeout(5000);  // recover in 5 seconds

Or you can restore it manually


// Turn off automatic recovery
canvas.setRestoreTimeout(-1);

// Restore the context when the mouse is clicked.
window.addEventListener("mousedown", function() {
  canvas.restoreContext();
}

The simulator is part of the webgl debug utilities found here https://www.khronos.org/registry/webgl/sdk/debug/webgl-debug.js

How Lost Context Works

It’s important to understand that although you can get lose the context at any time the actual context lost event will not get delivered until your code yields to the browser.

In other words imagine code like this

var requestId;
var tex;
var gl;
var canvas;

function main() {
  canvas = document.getElementById(myCanvas);
  canvas.addEventListener(
      "webglcontextlost", handleContextLost, false);
  canvas.addEventListener(
      "webglcontextrestored", handleContextRestored, false);
  gl = canvas.getContext("webgl");
  init();
}

function init() {
  tex = gl.createTexture();                     // gl cmd #1
  gl.bindTexture(gl.TEXTURE_2D, tex);           // gl cmd #2
  gl.clearColor(1,0,0,1);                       // gl cmd #3

  draw();
}

function draw() {
  gl.clear(gl.COLOR_BUFFER_BIT);                // gl cmd #4
  requestId = requestAnimationFrame(draw);
}

function handleContextLost(event) {
  event.preventDefault();
  cancelRequestAnimationFrame(requestId);
}

funciton handleContextRestored(event) {
  init();
}

Now, Assume that when you run this program, for some reason (user action, system update, other program) the context gets lost at gl cmd #3 above. This is the order of execution

  1. cmd #1 is executed
  2. cmd #2 is executed
  3. the context is lost
  4. gl.isContextLost() starts reporting true
  5. gl.getError() returns gl.CONTEXT_LOST_WEBGL exactly 1 time
  6. tex is now an invalid resource as it belongs to a lost context
  7. cmd #3 is ignored
  8. draw is called
  9. cmd #4 is ignored
  10. requestAnimationFrame is called
  11. draw returns
  12. init returns
  13. javascript has now yielded to the browser
  14. handleContextLost is now called by the browser
  15. event.preventDefault is called
  16. our requestAnimationFrame is cancelled
  17. javascript returns control to the browser
  18. when the browser can restore the context handleContextRestored is called

    At this point in time tex is still left over from the lost context and must be re-created. Also, the clear color has been set back to the default, 0, 0, 0, 0

  19. init is called.

    init wiil set up our resource (tex) and our state (clearColor) again.