Pixi.js: Too many active WebGL contexts

Created on 11 Dec 2015  ·  29Comments  ·  Source: pixijs/pixi.js

I'm building an app integrated with React which loads several individual experiments, in pixi.js, three.js etc. Because I can't use an iframe to load the experiments, I need to clean after myself.

Every time there is a navigation to a new experiment, I'm checking if the a renderer exists, or not, and if it does I'm destroying it).

dummy code

if renderer
    renderer.destroy(true);
    renderer = null

renderer = new PIXI.autoDetectRenderer ......

It seems that I'm correctly cleaning my webgl renderer, however after 16 pages get loaded, I get the warning saying WARNING: Too many active WebGL contexts. Oldest context will be lost. This shouldnt be a problem on desktop as we only have a warning, but on the ipad it crashes the browser and a message appears: X A problem occurred with this web page, so it was reloaded..

I was looking at threejs and how they implement a solution for this, and they have the following code

Am I doing anything silly?

Most helpful comment

I think I found a good solution. This allows me to have map overlay React components that don't have to know about each other and allow's React to own the DOM.

It uses this technique to force the context loss:

gl.getExtension('WEBGL_lose_context').loseContext();

Here's a full example:

var document = require('global/document');
var PIXI = require('pixi.js');

// I want to keep this canvas / renderer around. For example, this might be a bottom layer PIXI / React map overlay.
var canvas1 = document.createElement('canvas');
document.body.appendChild(canvas1);
createRenderer(0xff0000, canvas1);

function createRenderer(color, canvas) {
  canvas = canvas || document.createElement('canvas');
  var renderer = new PIXI.WebGLRenderer(800, 600, {view: canvas});
  var stage = new PIXI.Container();
  var graphics = new PIXI.Graphics();
  graphics.beginFill(color, 0.5);
  graphics.drawCircle(0, 0, 200);
  graphics.endFill();
  stage.addChild(graphics);
  renderer.render(stage);
  return {renderer: renderer, stage: stage, graphics: graphics};
}

// Simulate frequent adding / removing of lots of PIXI / React map overlays on top.
for (var i = 0; i < 16; i++) {
  var canvas = document.createElement('canvas');
  document.body.appendChild(canvas);
  var scene = createRenderer(0x00ff00, canvas);
  // Uncomment to see that the original canvas isn't removed.
  /* scene.renderer.currentRenderTarget.gl
      .getExtension('WEBGL_lose_context').loseContext(); */
  scene.renderer.destroy();
  scene.stage.removeChild(scene.graphics);
  document.body.removeChild(canvas);
}

Maybe PIXI should add this to its own "destroy" method?

All 29 comments

@andrevenancio
I'm no expert in Pixi seeing as I've only been using it since monday or tuesday, however. If you are having multiple renderers and canvases due to many experiments(like this) in an idea of only one needs to be accessed at one time, I would have a global renderer or another, and keep the experiments in an array. Then when you need to display it, just pass the stage from each experiment into the renderer.

var renderer = PIXI.autoDetectRenderer({blah});
var experiments = [];
experiments.push(experiment1);
experiments.push(experiment2);
experiments.push(experiment3);

var currentExperiment = $('#experminetSelector').val();
renderer.render(experiments[currentExperiment]);

This way you're only making the renderer once and just passing it the stage from each experiment. Though this only works if you're trying to only render one page and have all the experiments on it rather than having a lot of different experiment pages.

DISCLAIMER: I'm new to pixi, and didn't try this out, your mileage may vary. Also it still sounds as if you found some kind of bug that maybe the devs would want to look into or give some guidance to.

Having a single renderer and multiple scenes should be fine. 1 renderer === 1 canvas element.

Hey guys, thanks for your feedback. @samuraiseoul What you're suggesting isnt silly at all, and that's how I would normally proceed to have several experiments in one framework... however, I will need to make another solution as the experiments can de done using pixi.js threejs or plain webgl. Also, on top of that, because of the type of the transitions already defined in the react structure, at a point you will see the old experiment, and the new experiment at the same time, rendering simultaneously, so the idea of one renderer won't work....

Plus.... this is a work around for the problem, not a solution itself... If the .destroy() method actually doesn't destroy the reference to the webgl context... what's the point? :(

Yo buddy! Totally agree that the destroy function is not exactly doing what its supposed to if the contexts are not being destroyed :)

I wander, are you using autodetectRenderer ? It could be that the test context we create to detect if the browser is webGL capable is not being destroyed.

Some extra info: You can't destroy a webgl context, they are garbage collected (unless something has changed?)

I think we remove the references we hold, and optionally remove the canvas element (which is required for context cleanup) but then it is up to the browser to do the clean. It is possible that contexts are leaking, or that they are being created faster than the browser wants. Honestly I'm not sure.

Do dev tools indicate leaks around contexts like this? Maybe that can help? Sounds like something is keeping the context around.. Maybe a js ref to the context or the a js ref to the underlying canvas?

This is why I still love iframes no matter what.

@GoodBoyDigital I'm using the autodetectRenderer like this

@renderer = new PIXI.autoDetectRenderer canvas.width, canvas.height, { view: canvas, antialias: false, backgroundColor: 0xffffff }

I'm parking this for now, will let you guys know what the solution is when I found one.. I was just thinking that forcing that context lost, would maybe force the garbage collection to clean it.. but not sure of it either :)

I'm having the same issue. What I am building requires that Pixi be destroyed and rebuilt quite often and I'm concerned about the memory implications. Each WebGL instance is quite small at the moment, but it will get much much larger as we continue to build this out.

Any progress?

To be clear, I'm using Phaser, but it seems to be calling Pixi's renderer.destroy method.

no luck yet. If you can either use one global renderer and you just render different scenes into it, or, just load content through an iframe and that disposes properly of whatever is hanging.

Hey peeps, yes this is a bit of a tricky one really. As far as I can see we do everything we can to destroy a context. So all textures / programs arraybuffers ect are destroyed. What we could do is maybe pool the contexts once they are destroyed so that we reuse them rather than create a new one each time?

hey @GoodBoyDigital in my case i had an animation in/out so i would always needed 2 contexts... that's the problem with integrating with React and not making it full pixi... anyway, not sure this is a bug you can fix per say.. I think its more like the browser manages the contexts... don't even know if this hack from three.js works... maybe give it a go on ur V4 and try to open 16+ tabs with pixi https://github.com/mrdoob/three.js/blob/master/src/renderers/WebGLRenderer.js#L289

Otherwise no worries mate, I already changed the architecture of my app... iframe FTW :)

Good stuff man! I will definitely look into adding the three.js trick too.

I would love to use one global rendered. In fact, that's my ultimate goal. However, I'd need a way to stream code into the existing context.

In other words, the user is writing the code. It's not pre-written.

I think I found a good solution. This allows me to have map overlay React components that don't have to know about each other and allow's React to own the DOM.

It uses this technique to force the context loss:

gl.getExtension('WEBGL_lose_context').loseContext();

Here's a full example:

var document = require('global/document');
var PIXI = require('pixi.js');

// I want to keep this canvas / renderer around. For example, this might be a bottom layer PIXI / React map overlay.
var canvas1 = document.createElement('canvas');
document.body.appendChild(canvas1);
createRenderer(0xff0000, canvas1);

function createRenderer(color, canvas) {
  canvas = canvas || document.createElement('canvas');
  var renderer = new PIXI.WebGLRenderer(800, 600, {view: canvas});
  var stage = new PIXI.Container();
  var graphics = new PIXI.Graphics();
  graphics.beginFill(color, 0.5);
  graphics.drawCircle(0, 0, 200);
  graphics.endFill();
  stage.addChild(graphics);
  renderer.render(stage);
  return {renderer: renderer, stage: stage, graphics: graphics};
}

// Simulate frequent adding / removing of lots of PIXI / React map overlays on top.
for (var i = 0; i < 16; i++) {
  var canvas = document.createElement('canvas');
  document.body.appendChild(canvas);
  var scene = createRenderer(0x00ff00, canvas);
  // Uncomment to see that the original canvas isn't removed.
  /* scene.renderer.currentRenderTarget.gl
      .getExtension('WEBGL_lose_context').loseContext(); */
  scene.renderer.destroy();
  scene.stage.removeChild(scene.graphics);
  document.body.removeChild(canvas);
}

Maybe PIXI should add this to its own "destroy" method?

That makes sense if we own the context, which I think right now we assume we do (v3).

In the function isWebGLSupported() (source/core/utils/index.js) replacing
return !!(gl && gl.getContextAttributes().stencil);

with
var success = !!(gl && gl.getContextAttributes().stencil); gl.getExtension('WEBGL_lose_context').loseContext(); gl = undefined; return success;

fixed the problem for me. It was most prominent in Google Chrome - Firefox worked either way. As I'm a bit of a WebGL noob I'd be really grateful for comment on this approach.

Good call losing the context forcibly when checking, that is a great idea since we know (100%) that we own that context.

Did this get merged ? We are having the same problem.

ping @englercj i think this can be closed now

gl.getExtension('WEBGL_lose_context').loseContext() may suppress the warning but the spec says it merely simulates losing the context, until WEBGL_lose_context.restoreContext() is called - which can restore the context only if it wasn't truly lost in the first place.

This might be useful: lose_context() is only a simulation (according to the spec) but in this thread (Public WebGL: WEBGL_lose_context we learn (from mozilla developer Jeff Gilbert) that in Firefox loseContext() is a byword for "release this context and its
resources"
.

The problem is that other browsers handle this differently. There are comments from a Google developer (Ken Russell) recommending : Please use the explicit delete* APIs on the WebGLRenderingContext to release GPU resources that your application is no longer using.

I have to admit I haven't experimented with any of this but it looks like any solution is going to be browser specific.

Interesting FF implementation note. Indeed, Ken's advice about deleting individual resources looks like the standard way to go, FF is but one among the many popular browsers and even FF behavior might change without notice. I also haven't tested per browser because of not wanting to rely on implementation detail and fakeable user agent string or browser sniffing and additional code paths when there's an alternative.

I was doing some testing for my app today on iOS and ran into this error. Is there a known workaround? My pixi view is a subcomponent of a screen on my app, so it gets destroyed when the user navigates away from that page. I was thinking, would it work to somehow detach/reattach the same renderer to the view's canvas to work around the issue?

The workaround is to not create multiple contexts. Just reuse ones you have. If you navigate pages, then it shouldn't matter what was created on the previous page.

Hmm, so how would I go about reusing a context? I'm using angularJS, so I'm working within a templating framework. Like, could I detach the canvas element from the DOM and store it, then when the user comes back to the page, reattach it to its place in the page? It'd be great if there was a way within PIXI to say "use this existing renderer + context and attach them to this new canvas", but I don't understand how it happens under the hood.

The reason I care is I'm using one big pixi view as the background of my app, and I have a smaller one that I use on my settings page. If the user goes to the settings page multiple times, it kills the main background one because it was created first.

Seems like this would be an issue with using PIXI within any single page app.

Saving the instance of your PIXI.Application/PIXI.WebGLRenderer/PIXI.CanvasRenderer will also save the instance of your canvas element. For instance, in PIXI.Application, you can get the view property which is the HTMLCanvasElement. You can insert this canvas element and remove it from your AngularJS/React app. In PIXI you can tell the Renderer which canvas element to use by passing it as an option, however, you shouldn't use this. Instead, allow PIXI to create it's own canvas element internally and just appendChild to some DOM container.

<div id="pixi-container"></div>
// maintain the reference to this Application even as your SPA view gets destroyed
const app = new PIXI.Application();

// Insert in the DOM or whatever the equivalent is in Angular
document.querySelector("#pixi-container").appendChild(app.view);

Ok, thanks, I'll give that a try. Doesn't look bad at all actually.

Alright, detaching and reappending the view instead of destroying it solved my issue, thanks for the help guys.

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

softshape picture softshape  ·  3Comments

distinctdan picture distinctdan  ·  3Comments

samueller picture samueller  ·  3Comments

neciszhang picture neciszhang  ·  3Comments

lucap86 picture lucap86  ·  3Comments