Skip to content

Commit

Permalink
Fix: Catch loss of WebGL context in Box3D and reload preview
Browse files Browse the repository at this point in the history
  • Loading branch information
MiiBond authored Jun 8, 2017
1 parent 5760a12 commit 0de993e
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 24 deletions.
3 changes: 3 additions & 0 deletions src/lib/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,9 @@ const PREVIEW_LOCATION = findScriptLocation(PREVIEW_SCRIPT_NAME, document.curren
case 'load':
this.finishLoading(data.data);
break;
case 'progressstart':
this.startProgressBar();
break;
case 'progressend':
this.finishProgressBar();
break;
Expand Down
42 changes: 41 additions & 1 deletion src/lib/viewers/box3d/Box3DRenderer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
/* global Box3D */
/* eslint no-param-reassign:0 */
import EventEmitter from 'events';
import { EVENT_SHOW_VR_BUTTON, EVENT_SCENE_LOADED, EVENT_TRIGGER_RENDER } from './box3DConstants';
import {
EVENT_SHOW_VR_BUTTON,
EVENT_SCENE_LOADED,
EVENT_TRIGGER_RENDER,
EVENT_WEBGL_CONTEXT_RESTORED,
EVENT_WEBGL_CONTEXT_LOST
} from './box3DConstants';
import { MODEL3D_STATIC_ASSETS_VERSION } from '../../constants';

const PREVIEW_CAMERA_CONTROLLER_ID = 'orbit_camera';
Expand Down Expand Up @@ -49,6 +55,8 @@ class Box3DRenderer extends EventEmitter {
this.defaultCameraQuaternion = PREVIEW_CAMERA_QUATERNION;
this.vrGamepadLoadPromises = {};
this.vrCommonLoadPromise = null;
this.handleContextLost = this.handleContextLost.bind(this);
this.handleContextRestored = this.handleContextRestored.bind(this);
}

/**
Expand Down Expand Up @@ -80,6 +88,11 @@ class Box3DRenderer extends EventEmitter {
return;
}

if (this.box3d.canvas) {
this.box3d.canvas.removeEventListener('webglcontextlost', this.handleContextLost);
this.box3d.canvas.removeEventListener('webglcontextrestored', this.handleContextRestored);
}

this.disableVr();

this.box3d.destroy();
Expand Down Expand Up @@ -210,6 +223,10 @@ class Box3DRenderer extends EventEmitter {
engineName: 'Default',
resourceLoader
});
if (box3d.canvas) {
box3d.canvas.addEventListener('webglcontextlost', this.handleContextLost);
box3d.canvas.addEventListener('webglcontextrestored', this.handleContextRestored);
}
return new Promise((resolve) => {
box3d.addEntities(sceneEntities);
const app = box3d.getAssetById('APP_ASSET_ID');
Expand All @@ -219,6 +236,29 @@ class Box3DRenderer extends EventEmitter {
});
}

/**
* Catch loss of WebGL context and prevent browser default behavior
* of displaying an error message. This could happen for various reasons
* including the browser trying to recover from a driver error to another
* application requesting a context.
*
* @param {Event} event The event for the context loss.
* @return {void}
*/
handleContextLost(event) {
event.preventDefault();
this.emit(EVENT_WEBGL_CONTEXT_LOST);
}

/**
* When the WebGL context has been restored by the browser, reload the preview.
*
* @return {void}
*/
handleContextRestored() {
this.emit(EVENT_WEBGL_CONTEXT_RESTORED);
}

/**
* Enable VR and reset the scene, on scene load event fired from Box3DRuntime.
*
Expand Down
46 changes: 43 additions & 3 deletions src/lib/viewers/box3d/Box3DViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import fullscreen from '../../Fullscreen';
import Box3DControls from './Box3DControls';
import Box3DRenderer from './Box3DRenderer';
import Browser from '../../Browser';
import Notification from '../../Notification';
import { get } from '../../util';
import { showLoadingIndicator } from '../../ui';
import {
CSS_CLASS_BOX3D,
EVENT_ERROR,
Expand All @@ -13,7 +15,9 @@ import {
EVENT_SCENE_LOADED,
EVENT_SHOW_VR_BUTTON,
EVENT_TOGGLE_FULLSCREEN,
EVENT_TOGGLE_VR
EVENT_TOGGLE_VR,
EVENT_WEBGL_CONTEXT_RESTORED,
EVENT_WEBGL_CONTEXT_LOST
} from './box3DConstants';
import JS from './box3DAssets';
import './Box3D.scss';
Expand Down Expand Up @@ -42,6 +46,7 @@ const CLASS_VR_ENABLED = 'vr-enabled';

this.wrapperEl = this.containerEl.appendChild(document.createElement('div'));
this.wrapperEl.className = CSS_CLASS_BOX3D;
this.contextNotification = new Notification(this.wrapperEl);

this.loadTimeout = LOAD_TIMEOUT;
}
Expand Down Expand Up @@ -72,6 +77,8 @@ const CLASS_VR_ENABLED = 'vr-enabled';
this.renderer.on(EVENT_SCENE_LOADED, this.handleSceneLoaded);
this.renderer.on(EVENT_SHOW_VR_BUTTON, this.handleShowVrButton);
this.renderer.on(EVENT_ERROR, this.handleError);
this.renderer.on(EVENT_WEBGL_CONTEXT_RESTORED, this.handleContextRestored);
this.renderer.on(EVENT_WEBGL_CONTEXT_LOST, this.handleContextLost);
}

// For addition/removal of VR class when display stops presenting
Expand All @@ -96,6 +103,8 @@ const CLASS_VR_ENABLED = 'vr-enabled';
this.renderer.removeListener(EVENT_SCENE_LOADED, this.handleSceneLoaded);
this.renderer.removeListener(EVENT_SHOW_VR_BUTTON, this.handleShowVrButton);
this.renderer.removeListener(EVENT_ERROR, this.handleError);
this.renderer.removeListener(EVENT_WEBGL_CONTEXT_RESTORED, this.handleContextRestored);
this.renderer.removeListener(EVENT_WEBGL_CONTEXT_LOST, this.handleContextLost);
}

window.removeEventListener('vrdisplaypresentchange', this.onVrPresentChange);
Expand All @@ -122,15 +131,23 @@ const CLASS_VR_ENABLED = 'vr-enabled';
super.destroy();

this.detachEventHandlers();
this.destroySubModules();

this.destroyed = true;
}

/**
* Destroy any submodules required for previewing this document
*
* @return {void}
*/
destroySubModules() {
if (this.controls) {
this.controls.destroy();
}
if (this.renderer) {
this.renderer.destroy();
}

this.destroyed = true;
}

/**
Expand Down Expand Up @@ -191,6 +208,29 @@ const CLASS_VR_ENABLED = 'vr-enabled';
fullscreen.toggle(this.containerEl);
}

/**
* Handle the loss of the WebGL context by cleaning up the controls and renderer.
*
* @return {void}
*/
handleContextLost() {
this.contextNotification.show('WebGL Context Lost');
this.destroySubModules();
}

/**
* Handle the restoration of the WebGL context by reloading the preview.
*
* @return {void}
*/
handleContextRestored() {
this.detachEventHandlers();
this.contextNotification.show('WebGL Context Restored');
this.emit('progressstart');
showLoadingIndicator();
this.postLoad();
}

/**
* Handles toggle VR event
*
Expand Down
34 changes: 33 additions & 1 deletion src/lib/viewers/box3d/__tests__/Box3DRenderer-test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global Box3D */
/* eslint-disable no-unused-expressions */
import Box3DRenderer from '../Box3DRenderer';
import { EVENT_SHOW_VR_BUTTON } from '../box3DConstants';
import { EVENT_SHOW_VR_BUTTON, EVENT_WEBGL_CONTEXT_RESTORED } from '../box3DConstants';

const sandbox = sinon.sandbox.create();
const PREVIEW_CAMERA_CONTROLLER_ID = 'orbit_camera';
Expand Down Expand Up @@ -359,6 +359,28 @@ describe('lib/viewers/box3d/Box3DRenderer', () => {
expect(shouldBePromise).to.be.a('promise');
});

it('should bind to context loss and restore events', (done) => {
sandbox.stub(renderer, 'handleContextRestored');
const Box3DFake = {
Engine: function constructor() {
this.addEntities = sandbox.stub();
this.getAssetById = sandbox.stub().returns({
load: function load() {}
});
this.canvas = { addEventListener: () => {}};
sandbox.stub(this.canvas, 'addEventListener', () => {
renderer.handleContextRestored()
})
}
};
window.Box3D = Box3DFake;
renderer.createBox3d({}, {}).then(() => {
expect(renderer.box3d.canvas.addEventListener).to.be.called.twice;
expect(renderer.handleContextRestored).to.be.called;
done();
});
});

it('should not set reference if error occurs initializing engine', (done) => {
const Box3DFake = {
Engine: function constructor() {
Expand Down Expand Up @@ -409,6 +431,16 @@ describe('lib/viewers/box3d/Box3DRenderer', () => {
});
});

describe('handleContextRestored()', () => {
it('should fire event to be picked up by the viewer', () => {
const emitStub = sandbox.stub(renderer, 'emit', (eventName) => {
expect(eventName).to.equal(EVENT_WEBGL_CONTEXT_RESTORED);
});
renderer.handleContextRestored();
expect(emitStub).to.be.called;
});
});

describe('toggleVr()', () => {
it('should enable vr if it\'s currently disabled', () => {
let called = false;
Expand Down
53 changes: 52 additions & 1 deletion src/lib/viewers/box3d/__tests__/Box3DViewer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
EVENT_SCENE_LOADED,
EVENT_SHOW_VR_BUTTON,
EVENT_TOGGLE_FULLSCREEN,
EVENT_TOGGLE_VR
EVENT_TOGGLE_VR,
EVENT_WEBGL_CONTEXT_RESTORED
} from '../box3DConstants';

const sandbox = sinon.sandbox.create();
Expand Down Expand Up @@ -177,6 +178,15 @@ describe('lib/viewers/box3d/Box3DViewer', () => {

expect(onSpy.withArgs(EVENT_ERROR).called).to.be.true;
});

it('should invoke box3d.renderer.on() with EVENT_WEBGL_CONTEXT_RESTORED', () => {
const onSpy = sandbox.spy(box3d.renderer, 'on');
onSpy.withArgs(EVENT_WEBGL_CONTEXT_RESTORED);

box3d.attachEventHandlers();

expect(onSpy.withArgs(EVENT_WEBGL_CONTEXT_RESTORED).called).to.be.true;
});
});

it('should not attach handlers to renderer if renderer instance doesn\'t exist', () => {
Expand Down Expand Up @@ -258,6 +268,15 @@ describe('lib/viewers/box3d/Box3DViewer', () => {

expect(detachSpy.withArgs(EVENT_ERROR).called).to.be.true;
});

it('should invoke box3d.renderer.removeListener() with EVENT_WEBGL_CONTEXT_RESTORED', () => {
const detachSpy = sandbox.spy(box3d.renderer, 'removeListener');
detachSpy.withArgs(EVENT_WEBGL_CONTEXT_RESTORED);

box3d.detachEventHandlers();

expect(detachSpy.withArgs(EVENT_WEBGL_CONTEXT_RESTORED).called).to.be.true;
});
});

it('should not invoke renderer.removeListener() when renderer is undefined', () => {
Expand Down Expand Up @@ -513,4 +532,36 @@ describe('lib/viewers/box3d/Box3DViewer', () => {
expect(emitStub).to.be.called;
});
});

describe('handleContextLost()', () => {
it('should call destroySubModules', () => {
const destroySubModules = sandbox.stub(box3d, 'destroySubModules', () => {});
box3d.handleContextLost();
expect(destroySubModules).to.be.called;
});
});

describe('handleContextRestored()', () => {
it('should call emit() with params ["progressstart"]', () => {
const emitStub = sandbox.stub(box3d, 'emit', (eventName) => {
expect(eventName).to.equal('progressstart');
});

box3d.handleContextRestored();

expect(emitStub).to.be.called;
});

it('should call detachEventHandlers', () => {
const detachHandlers = sandbox.stub(box3d, 'detachEventHandlers', () => {});
box3d.handleContextRestored();
expect(detachHandlers).to.be.called;
});

it('should call postLoad', () => {
box3d.postLoad = sandbox.stub();
box3d.handleContextRestored();
expect(box3d.postLoad).to.be.called;
});
});
});
2 changes: 2 additions & 0 deletions src/lib/viewers/box3d/box3DConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const EVENT_SCENE_LOADED = 'sceneLoaded';
export const EVENT_SHOW_VR_BUTTON = 'showVrButton';
export const EVENT_TOGGLE_FULLSCREEN = 'toggleFullscreen';
export const EVENT_TRIGGER_RENDER = 'triggerRender';
export const EVENT_WEBGL_CONTEXT_RESTORED = 'webglContextRestored';
export const EVENT_WEBGL_CONTEXT_LOST = 'webglContextLost';

// CSS CLASSES
export const CSS_CLASS_BOX3D = 'bp-box3d';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ describe('lib/viewers/box3d/model3d/Model3DRenderer', () => {

describe('destroy()', () => {
it('should remove event listener from the engine instance canvas', () => {
sandbox.mock(renderer.box3d.canvas).expects('removeEventListener');
const removeListener = sandbox.stub(renderer.box3d.canvas, 'removeEventListener');
renderer.destroy();
expect(removeListener).to.be.called;
});

it('should do nothing if there is not box3d runtime instance', () => {
Expand Down
34 changes: 17 additions & 17 deletions src/third-party/model3d/0.125.0/box3d-runtime.min.js

Large diffs are not rendered by default.

0 comments on commit 0de993e

Please sign in to comment.