5

I'm having an unexpected issue with Chrome (current). I've got a web application that uses webGL canvas to do some basic image manipulation and later copies the webGl canvas to a 2d canvas in order to save off the image data. The problem is that the drawing context of the webgl canvas appears to get lost in Chrome after I switch tabs. This doesn't happen in any other browser that I've tried (FF, IE 11/Edge, Safari).

I would expect Chrome to also fire the webgl context lost event if this is indeed what is happening, but I never get that event.

Here's a very simple fiddle that demonstrates the issue.

https://jsfiddle.net/JoeBlow/m14evgzj/

1) Load page that draws to a webgl canvas

2) Switch to a new tab

3) Switch back to original tab

4) Click 'Copy Image'.

If I reinitialize the 3d canvas (reload everything and re-draw), then everything works as expected even after a tab switch, but I was trying to avoid capturing the blur/focus events on the window/viewport.

here's the simplified code:

window.canvasSource  = document.getElementById("source");
var canvasDestination = document.getElementById("destination");
var buttonCopy = document.getElementById("btnCopy");
var btnToggle = document.getElementById("btnToggle");
var btnClear = document.getElementById("btnClear");
var btnInit3d = document.getElementById('btnInit3d');

var canvasOptions = { preserveDrawingBuffer: true };

//var sourceContext = canvasSource.getContext("2d", options);
var destinationContext = canvasDestination.getContext("2d", canvasOptions);

window.canvasSource.addEventListener("webglcontextlost", function(event) {
console.log('context lost');
    event.preventDefault();
}, false);

window.canvasSource.addEventListener("webglcontextrestored", init3d, false);

btnToggle.addEventListener('click', function() {
    if (window.canvasSource.style.visibility != "hidden") {
      window.canvasSource.style['visibility']='hidden';
    }
  else {
   window.canvasSource.style['visibility'] = 'visible';
  }
});

btnCopy.addEventListener('click', function() {
    destinationContext.drawImage(window.canvasSource,0,0);
});

btnClear.addEventListener('click', function() {
    destinationContext.clearRect(0,0, 1500, 1500);
});

btnInit3d.addEventListener('click', function() {
    init3d();
});

function init3d() {
console.log('init3d()');
window.gl = window.canvasSource.getContext("webgl", canvasOptions);
var gl = window.gl;
if (gl==null)
 console.log("couldn't get webgl context");
else 
 console.log(gl);

gl.viewport(0, 0, window.canvasSource.width, window.canvasSource.height);
gl.clearColor(0, 0.5, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);

var v = document.getElementById("vertex").firstChild.nodeValue;
var f = document.getElementById("fragment").firstChild.nodeValue;

var vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, v);
gl.compileShader(vs);

var fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, f);
gl.compileShader(fs);

program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
}

init3d();
SDShooter
  • 103
  • 6
  • https://bugs.chromium.org/p/chromium/issues/detail?id=639105 For a workaround... I'm not sure but drawing the webgl context over a 2d one might do ([an related Q/A about recording a webgl stream](https://stackoverflow.com/questions/44156528/canvas-doesnt-repaint-when-tab-inactive-backgrounded-for-recording-webgl/44172801#44172801)) – Kaiido Jun 26 '17 at 23:25
  • I didn't notice a workaround in there, unless you're referring to using setTimeout rather than requestAnimationFrame (Which still wouldn't draw while the tab is not focused). That issue is probably related to what I'm experiencing, but the main symptom is that once you switch off then back to a tab with a webgl rendered canvas, the image shown in the canvas will still look correct, but until you draw a new frame (after reinitializing from scratch, you can't use it as a source for a copy operation because the underlying drawingBuffer that would facilitate the copy operation is gone. – SDShooter Jun 27 '17 at 00:58
  • There is no `setTimeout` in the linked Q/A *(well except for stopping the recording, but really it's only for the demo and not needed at all)*. The whole point of the answer their is to use an homemade timing loop based on the WebAudioAPI, which is not tied to tab focus. Also, it uses a 2Dcontext for the recording, which may also be a workaround for you copy operation. – Kaiido Jun 27 '17 at 01:04
  • I don't need anything to happen when my tab is not active. My app involves image editing/filtering. As such, I just need it to work after the user refocuses my tab and clicks a button that performs the copy operation. Unfortunately, so far it is looking like this is only going to be possible if I start from scratch with the WebGL operations (reload textures, possibly recompile shaders, execute shaders to recreate my single frame, etc). In other words, I'm probably going to need to re-run what is inside 'render_func();' in the example you kindly provided. – SDShooter Jun 27 '17 at 01:16
  • I see you did fill this [bug report](https://bugs.chromium.org/p/chromium/issues/detail?id=736969) ; that was the correct thing to do ;-) – Kaiido Jun 27 '17 at 02:23

1 Answers1

4

I guess this issue is related to this one.

Apparently, chrome does clear the drawingBuffer when the tab is blurred, even if you did tell it not to do so.

A possible workaround is to create yourself a preserveDrawingBuffer object.

2DContext is not affected by this bug, so instead of setting preserveDrawingBuffer at your wegbl initiation, you can create an offscreen 2D context, on which you will draw the webgl context at each new draw.

var canvasSource = document.getElementById("source");
var canvasDestination = document.getElementById("destination");
var buttonCopy = document.getElementById("btnCopy");
//var btnToggle = document.getElementById("btnToggle");
var btnClear = document.getElementById("btnClear");
var btnInit3d = document.getElementById('btnInit3d');
var gl;

// we don't need to preserve the drawing buffer anymore
// since we'll do it manually
//var canvasOptions = { preserveDrawingBuffer: true};
var bufferContext = canvasSource.cloneNode().getContext('2d');
// 2D context don't have an preserveDrawingBuffer option
var destinationContext = canvasDestination.getContext("2d");

btnCopy.addEventListener('click', function() {
  var error = gl.getError();
  if (error) {
    console.log('Gl Error: ' + error);
  }
  // we draw the buffer canvas instead of the webgl one
  destinationContext.drawImage(bufferContext.canvas, 0, 0);
});

btnClear.addEventListener('click', function() {
  destinationContext.clearRect(0, 0, 1500, 1500);
});

function init3d() {
  gl = canvasSource.getContext("webgl");
  if (gl == null)
    console.log("couldn't get webgl context");

  gl.viewport(0, 0, canvasSource.width, canvasSource.height);
  gl.clearColor(0, 0.5, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  // at each draw, save the webgl state
  bufferContext.drawImage(canvasSource, 0,0);
}

init3d();
<canvas id="source"> </canvas>
<canvas id="destination"> </canvas>
<div>
  <button id="btnCopy">Copy Image</button>
  <button id="btnClear">Clear Copy</button>
</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Why do you use cloneNode() ? Simple canvasSource.getContext('2d') works for me. – Inflight Oct 16 '19 at 11:29
  • @Inflight well... Seems like this bug has been fixed, so yeah, no real reason to use this workaround anymore. We were using cloneNode in order to have a copy of the original canvas, since the original is already assigned a webgl context it can't have also a 2d one, so we needed an other canvas element, the same size, so cloneNode was the straight-forward solution. But are you saying you still face the original issue? – Kaiido Oct 16 '19 at 12:57
  • @Kaiido Clearing buffer issue in Chrome is still here. But I use just 2 canvases, one with with webGl context, other with 2d. – Inflight Oct 17 '19 at 13:09