Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/core/CreateRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ var Features = require('../device/Features');
* build time and inlined into the bundle as compile-time constants. They are not available as runtime
* variables and determine which renderer classes are included in the build.
*
* If the renderer type is `Phaser.AUTO` and the WebGL context cannot be created, despite the
* feature detection passing, this will fall back to the Canvas Renderer. The original error is
* dispatched as an `error` event on `window`, so it remains visible to error monitoring.
*
* @function Phaser.Core.CreateRenderer
* @since 3.0.0
*
Expand All @@ -31,6 +35,9 @@ var CreateRenderer = function (game)
{
var config = game.config;

// Remember the requested type, as AUTO is resolved to a concrete renderer below
var isAutoRenderType = (config.renderType === CONST.AUTO);

if ((config.customEnvironment || config.canvas) && config.renderType === CONST.AUTO)
{
throw new Error('Must set explicit renderType in custom environment');
Expand Down Expand Up @@ -111,9 +118,27 @@ var CreateRenderer = function (game)
// Let the config pick the renderer type, as both are included
if (config.renderType === CONST.WEBGL)
{
game.renderer = new WebGLRenderer(game);
try
{
game.renderer = new WebGLRenderer(game);
}
catch (e)
{
if (!isAutoRenderType)
{
throw e;
}

e.message += '. Falling back to CanvasRenderer';
window.dispatchEvent(new ErrorEvent('error', {
error: e
}));

// Force the type to Canvas, since the WebGL context could not be created
config.renderType = CONST.CANVAS;
}
}
else
if (config.renderType === CONST.CANVAS)
{
game.renderer = new CanvasRenderer(game);
game.context = game.renderer.gameContext;
Expand Down
101 changes: 101 additions & 0 deletions tests/core/CreateRenderer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// jsdom has no WebGLRenderingContext, so Phaser's feature detection reports
// WebGL as unavailable and CreateRenderer never tries to build a WebGL
// renderer. Define it before loading Phaser so detection passes (the
// setup.js canvas mock returns an object for 'webgl' contexts), which lets
// these tests exercise the "detection passed but context creation failed" path.
if (typeof window.WebGLRenderingContext === 'undefined')
{
window.WebGLRenderingContext = function () {};
}

// CreateRenderer pulls in the WebGL renderer and its render nodes, which
// cannot be required from src directly (circular requires), so use the
// built bundle like the Phaser Game tests do.
require('../../dist/phaser.js');

var Phaser = global.Phaser;

var Config = Phaser.Core.Config;
var CreateRenderer = Phaser.Core.CreateRenderer;
var CanvasRenderer = Phaser.Renderer.Canvas.CanvasRenderer;
var EventEmitter = Phaser.Events.EventEmitter;

describe('CreateRenderer', function ()
{
var dispatchEvent;

// A WebGL context that reports itself as lost makes WebGLRenderer throw
// 'WebGL unsupported' during construction, without touching a real context.
var lostContext = {
isContextLost: function () { return true; }
};

function makeGame (configOverrides)
{
var config = new Config(Object.assign({
width: 64,
height: 48,
context: lostContext
}, configOverrides || {}));

return {
config: config,
scale: { baseSize: { width: 64, height: 48 } },
events: new EventEmitter(),
textures: new EventEmitter(),
canvas: null,
context: null,
renderer: null
};
}

beforeEach(function ()
{
// The fallback reports the WebGL error via window.dispatchEvent. Intercept it,
// otherwise the test runner treats the dispatched ErrorEvent as an unhandled error.
dispatchEvent = vi.spyOn(window, 'dispatchEvent').mockImplementation(function () { return true; });
});

afterEach(function ()
{
dispatchEvent.mockRestore();
});

describe('WebGL context failure', function ()
{
it('should fall back to CanvasRenderer when the type is AUTO', function ()
{
var game = makeGame({ type: Phaser.AUTO });

CreateRenderer(game);

expect(game.renderer).toBeInstanceOf(CanvasRenderer);
expect(game.config.renderType).toBe(Phaser.CANVAS);
expect(game.context).toBe(game.renderer.gameContext);
});

it('should dispatch the original error on window when falling back', function ()
{
var game = makeGame({ type: Phaser.AUTO });

CreateRenderer(game);

expect(dispatchEvent).toHaveBeenCalledTimes(1);

var event = dispatchEvent.mock.calls[0][0];

expect(event.type).toBe('error');
expect(event.error).toBeInstanceOf(Error);
expect(event.error.message).toBe('WebGL unsupported. Falling back to CanvasRenderer');
});

it('should still throw when the type is explicitly WEBGL', function ()
{
var game = makeGame({ type: Phaser.WEBGL });

expect(function () { CreateRenderer(game); }).toThrow('WebGL unsupported');
expect(game.renderer).toBeNull();
expect(dispatchEvent).not.toHaveBeenCalled();
});
});
});