Fallback to 2d on context lost

When webgl contexts are lost, fallback to 2d rendering context.

Because it's not possible for a canvas to generate a different
context after one has already been created, the canvas elements
must be recreated from scratch, and event handlers must also be
updated.

This resolves https://github.com/nasa/openmct/issues/1976
This commit is contained in:
Pete Richards 2018-04-13 16:28:28 -07:00
parent 8ad07fa8de
commit 1f86a5a131
6 changed files with 102 additions and 6 deletions

View File

@ -197,9 +197,29 @@ function (
this.canvas = canvas; this.canvas = canvas;
this.overlay = overlay; this.overlay = overlay;
this.drawAPI = DrawLoader.getDrawAPI(canvas, overlay); this.drawAPI = DrawLoader.getDrawAPI(canvas, overlay);
if (this.drawAPI) {
this.listenTo(this.drawAPI, 'error', this.fallbackToCanvas, this);
}
return !!this.drawAPI; return !!this.drawAPI;
}; };
MCTChartController.prototype.fallbackToCanvas = function () {
this.stopListening(this.drawAPI);
DrawLoader.releaseDrawAPI(this.drawAPI);
// Have to throw away the old canvas elements and replace with new
// canvas elements in order to get new drawing contexts.
var div = document.createElement('div');
div.innerHTML = this.TEMPLATE;
var mainCanvas = div.querySelectorAll("canvas")[1];
var overlayCanvas = div.querySelectorAll("canvas")[0];
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
this.canvas = mainCanvas;
this.overlay.parentNode.replaceChild(overlayCanvas, this.overlay);
this.overlay = overlayCanvas;
this.drawAPI = DrawLoader.getFallbackDrawAPI(this.canvas, this.overlay);
this.$scope.$emit('plot:reinitializeCanvas');
};
MCTChartController.prototype.removeChartElement = function (series) { MCTChartController.prototype.removeChartElement = function (series) {
var elements = this.seriesElements.get(series); var elements = this.seriesElements.get(series);

View File

@ -43,6 +43,7 @@ define([
restrict: "E", restrict: "E",
template: TEMPLATE, template: TEMPLATE,
link: function ($scope, $element, attrs, ctrl) { link: function ($scope, $element, attrs, ctrl) {
ctrl.TEMPLATE = TEMPLATE;
var mainCanvas = $element.find("canvas")[1]; var mainCanvas = $element.find("canvas")[1];
var overlayCanvas = $element.find("canvas")[0]; var overlayCanvas = $element.find("canvas")[0];

View File

@ -22,9 +22,13 @@
define([ define([
'lodash',
'EventEmitter',
'../lib/eventHelpers'
], function ( ], function (
_,
EventEmitter,
eventHelpers
) { ) {
/** /**
@ -47,6 +51,9 @@ define([
} }
} }
_.extend(Draw2D.prototype, EventEmitter.prototype);
eventHelpers.extend(Draw2D.prototype);
// Convert from logical to physical x coordinates // Convert from logical to physical x coordinates
Draw2D.prototype.x = function (v) { Draw2D.prototype.x = function (v) {
return ((v - this.origin[0]) / this.dimensions[0]) * this.width; return ((v - this.origin[0]) / this.dimensions[0]) * this.width;

View File

@ -35,7 +35,7 @@ define(
ALLOCATIONS: [] ALLOCATIONS: []
}, },
{ {
MAX_INSTANCES: Number.MAX_INFINITY, MAX_INSTANCES: Number.POSITIVE_INFINITY,
API: Draw2D, API: Draw2D,
ALLOCATIONS: [] ALLOCATIONS: []
} }
@ -83,12 +83,24 @@ define(
return api; return api;
}, },
/**
* Returns a fallback draw api.
*/
getFallbackDrawAPI: function (canvas, overlay) {
var api = new CHARTS[1].API(canvas, overlay);
CHARTS[1].ALLOCATIONS.push(api);
return api;
},
releaseDrawAPI: function (api) { releaseDrawAPI: function (api) {
CHARTS.forEach(function (CHART_TYPE) { CHARTS.forEach(function (CHART_TYPE) {
if (api instanceof CHART_TYPE.API) { if (api instanceof CHART_TYPE.API) {
CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1); CHART_TYPE.ALLOCATIONS.splice(CHART_TYPE.ALLOCATIONS.indexOf(api), 1);
} }
}); });
if (api.destroy) {
api.destroy();
}
} }
}; };
} }

View File

@ -22,9 +22,13 @@
define([ define([
'lodash',
'EventEmitter',
'../lib/eventHelpers'
], function ( ], function (
_,
EventEmitter,
eventHelpers
) { ) {
// WebGL shader sources (for drawing plain colors) // WebGL shader sources (for drawing plain colors)
@ -69,6 +73,21 @@ define([
throw new Error("WebGL unavailable."); throw new Error("WebGL unavailable.");
} }
this.initContext();
this.listenTo(this.canvas, "webglcontextlost", this.onContextLost, this);
}
_.extend(DrawWebGL.prototype, EventEmitter.prototype);
eventHelpers.extend(DrawWebGL.prototype);
DrawWebGL.prototype.onContextLost = function (event) {
this.emit('error');
this.isContextLost = true;
this.destroy();
};
DrawWebGL.prototype.initContext = function () {
// Initialize shaders // Initialize shaders
this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER); this.vertexShader = this.gl.createShader(this.gl.VERTEX_SHADER);
this.gl.shaderSource(this.vertexShader, VERTEX_SHADER); this.gl.shaderSource(this.vertexShader, VERTEX_SHADER);
@ -103,7 +122,12 @@ define([
// Enable blending, for smoothness // Enable blending, for smoothness
this.gl.enable(this.gl.BLEND); this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA); this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
}
};
DrawWebGL.prototype.destroy = function () {
this.stopListening();
};
// Convert from logical to physical x coordinates // Convert from logical to physical x coordinates
DrawWebGL.prototype.x = function (v) { DrawWebGL.prototype.x = function (v) {
@ -117,6 +141,9 @@ define([
}; };
DrawWebGL.prototype.doDraw = function (drawType, buf, color, points) { DrawWebGL.prototype.doDraw = function (drawType, buf, color, points) {
if (this.isContextLost) {
return;
}
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW); this.gl.bufferData(this.gl.ARRAY_BUFFER, buf, this.gl.DYNAMIC_DRAW);
this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0); this.gl.vertexAttribPointer(this.aVertexPosition, 2, this.gl.FLOAT, false, 0, 0);
@ -125,6 +152,9 @@ define([
}; };
DrawWebGL.prototype.clear = function () { DrawWebGL.prototype.clear = function () {
if (this.isContextLost) {
return;
}
this.height = this.canvas.height = this.canvas.offsetHeight; this.height = this.canvas.height = this.canvas.offsetHeight;
this.width = this.canvas.width = this.canvas.offsetWidth; this.width = this.canvas.width = this.canvas.offsetWidth;
this.overlay.height = this.overlay.offsetHeight; this.overlay.height = this.overlay.offsetHeight;
@ -151,6 +181,9 @@ define([
DrawWebGL.prototype.setDimensions = function (dimensions, origin) { DrawWebGL.prototype.setDimensions = function (dimensions, origin) {
this.dimensions = dimensions; this.dimensions = dimensions;
this.origin = origin; this.origin = origin;
if (this.isContextLost) {
return;
}
if (dimensions && dimensions.length > 0 && if (dimensions && dimensions.length > 0 &&
origin && origin.length > 0) { origin && origin.length > 0) {
this.gl.uniform2fv(this.uDimensions, dimensions); this.gl.uniform2fv(this.uDimensions, dimensions);
@ -169,6 +202,9 @@ define([
* @param {number} points the number of points to draw * @param {number} points the number of points to draw
*/ */
DrawWebGL.prototype.drawLine = function (buf, color, points) { DrawWebGL.prototype.drawLine = function (buf, color, points) {
if (this.isContextLost) {
return;
}
this.doDraw(this.gl.LINE_STRIP, buf, color, points); this.doDraw(this.gl.LINE_STRIP, buf, color, points);
}; };
@ -177,6 +213,9 @@ define([
* *
*/ */
DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize) { DrawWebGL.prototype.drawPoints = function (buf, color, points, pointSize) {
if (this.isContextLost) {
return;
}
this.gl.uniform1f(this.uPointSize, pointSize); this.gl.uniform1f(this.uPointSize, pointSize);
this.doDraw(this.gl.POINTS, buf, color, points); this.doDraw(this.gl.POINTS, buf, color, points);
}; };
@ -191,6 +230,9 @@ define([
* is in the range of 0.0-1.0 * is in the range of 0.0-1.0
*/ */
DrawWebGL.prototype.drawSquare = function (min, max, color) { DrawWebGL.prototype.drawSquare = function (min, max, color) {
if (this.isContextLost) {
return;
}
this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array( this.doDraw(this.gl.TRIANGLE_FAN, new Float32Array(
min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]]) min.concat([min[0], max[1]]).concat(max).concat([max[0], min[1]])
), color, 4); ), color, 4);

View File

@ -59,6 +59,19 @@ define([
eventHelpers.extend(MCTPlotController.prototype); eventHelpers.extend(MCTPlotController.prototype);
MCTPlotController.prototype.initCanvas = function () {
if (this.$canvas) {
this.stopListening(this.$canvas);
}
this.$canvas = this.$element.find('canvas');
this.listenTo(this.$canvas, 'mousemove', this.trackMousePosition, this);
this.listenTo(this.$canvas, 'mouseleave', this.untrackMousePosition, this);
this.listenTo(this.$canvas, 'mousedown', this.onMouseDown, this);
this.watchForMarquee();
}
MCTPlotController.prototype.initialize = function () { MCTPlotController.prototype.initialize = function () {
this.$canvas = this.$element.find('canvas'); this.$canvas = this.$element.find('canvas');
@ -82,6 +95,7 @@ define([
this.listenTo(this.$scope, '$destroy', this.destroy, this); this.listenTo(this.$scope, '$destroy', this.destroy, this);
this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this); this.listenTo(this.$scope, 'plot:tickWidth', this.onTickWidthChange, this);
this.listenTo(this.$scope, 'plot:highlight:set', this.onPlotHighlightSet, this); this.listenTo(this.$scope, 'plot:highlight:set', this.onPlotHighlightSet, this);
this.listenTo(this.$scope, 'plot:reinitializeCanvas', this.initCanvas, this);
this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this); this.listenTo(this.config.xAxis, 'change:displayRange', this.onXAxisChange, this);
this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this); this.listenTo(this.config.yAxis, 'change:displayRange', this.onYAxisChange, this);