Bug 937627 - The webgl constructs cache should be per context, not per document global. r=rcampbell, a=bajaj
authorVictor Porof <vporof@mozilla.com>
Mon, 18 Nov 2013 10:26:00 +0200
changeset 167577 a231b9eade564f5c4524db8c2408a6fb106ff2aa
parent 167576 02294155afa93afd999f259abfd72c700cb0fe7b
child 167578 0902615834417d4619de07454b8e003b01a845a6
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell, bajaj
bugs937627
milestone27.0a2
Bug 937627 - The webgl constructs cache should be per context, not per document global. r=rcampbell, a=bajaj
browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
toolkit/devtools/server/actors/webgl.js
--- a/browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
+++ b/browser/devtools/shadereditor/test/browser_webgl-actor-test-16.js
@@ -5,22 +5,40 @@
  * Tests if program actors are invalidated from the cache when a window is
  * removed from the bfcache.
  */
 
 function ifWebGLSupported() {
   let [target, debuggee, front] = yield initBackend(SIMPLE_CANVAS_URL);
   front.setup({ reload: false });
 
+  // 0. Perform the initial reload.
+
   reload(target);
   let firstProgram = yield once(front, "program-linked");
+  let programs = yield front.getPrograms();
+  is(programs.length, 1,
+    "The first program should be returned by a call to getPrograms().");
+  is(programs[0], firstProgram,
+    "The first programs was correctly retrieved from the cache.");
+
+  // 1. Perform a simple navigation.
 
   navigate(target, MULTIPLE_CONTEXTS_URL);
   let secondProgram = yield once(front, "program-linked");
   let thirdProgram = yield once(front, "program-linked");
+  let programs = yield front.getPrograms();
+  is(programs.length, 2,
+    "The second and third programs should be returned by a call to getPrograms().");
+  is(programs[0], secondProgram,
+    "The second programs was correctly retrieved from the cache.");
+  is(programs[1], thirdProgram,
+    "The third programs was correctly retrieved from the cache.");
+
+  // 2. Perform a bfcache navigation.
 
   yield navigateInHistory(target, "back");
   let globalDestroyed = observe("inner-window-destroyed");
   let globalCreated = observe("content-document-global-created");
   reload(target);
 
   yield globalDestroyed;
   let programs = yield front.getPrograms();
@@ -30,16 +48,18 @@ function ifWebGLSupported() {
   yield globalCreated;
   let programs = yield front.getPrograms();
   is(programs.length, 1,
     "There should be 1 cached program actor now.");
 
   yield checkHighlightingInTheFirstPage(programs[0]);
   ok(true, "The cached programs behave correctly after navigating back and reloading.");
 
+  // 3. Perform a bfcache navigation and a page reload.
+
   yield navigateInHistory(target, "forward");
   let globalDestroyed = observe("inner-window-destroyed");
   let globalCreated = observe("content-document-global-created");
   reload(target);
 
   yield globalDestroyed;
   let programs = yield front.getPrograms();
   is(programs.length, 0,
--- a/toolkit/devtools/server/actors/webgl.js
+++ b/toolkit/devtools/server/actors/webgl.js
@@ -9,68 +9,90 @@ Cu.import("resource://gre/modules/Servic
 
 const events = require("sdk/event/core");
 const protocol = require("devtools/server/protocol");
 
 const { on, once, off, emit } = events;
 const { method, Arg, Option, RetVal } = protocol;
 
 const WEBGL_CONTEXT_NAMES = ["webgl", "experimental-webgl", "moz-webgl"];
+
 const HIGHLIGHT_FRAG_SHADER = [
   "precision lowp float;",
   "void main() {",
     "gl_FragColor.rgba = vec4(%color);",
   "}"
 ].join("\n");
 
+// These traits are bit masks. Make sure they're powers of 2.
+const PROGRAM_DEFAULT_TRAITS = 0;
+const PROGRAM_BLACKBOX_TRAIT = 1;
+
 exports.register = function(handle) {
   handle.addTabActor(WebGLActor, "webglActor");
 }
 
 exports.unregister = function(handle) {
   handle.removeTabActor(WebGLActor);
 }
 
 /**
  * A WebGL Shader contributing to building a WebGL Program.
  * You can either retrieve, or compile the source of a shader, which will
  * automatically inflict the necessary changes to the WebGL state.
  */
 let ShaderActor = protocol.ActorClass({
   typeName: "gl-shader",
-  initialize: function(conn, id) {
+
+  /**
+   * Create the shader actor.
+   *
+   * @param DebuggerServerConnection conn
+   *        The server connection.
+   * @param WebGLProgram program
+   *        The WebGL program being linked.
+   * @param WebGLShader shader
+   *        The cooresponding vertex or fragment shader.
+   * @param WebGLProxy proxy
+   *        The proxy methods for the WebGL context owning this shader.
+   */
+  initialize: function(conn, program, shader, proxy) {
     protocol.Actor.prototype.initialize.call(this, conn);
+    this.program = program;
+    this.shader = shader;
+    this.text = proxy.getShaderSource(shader);
+    this.linkedProxy = proxy;
   },
 
   /**
    * Gets the source code for this shader.
    */
   getText: method(function() {
     return this.text;
   }, {
     response: { text: RetVal("string") }
   }),
 
   /**
    * Sets and compiles new source code for this shader.
    */
   compile: method(function(text) {
     // Get the shader and corresponding program to change via the WebGL proxy.
-    let { context, shader, program, observer: { proxy } } = this;
+    let { linkedProxy: proxy, shader, program } = this;
 
     // Get the new shader source to inject.
     let oldText = this.text;
     let newText = text;
 
     // Overwrite the shader's source.
-    let error = proxy.call("compileShader", context, program, shader, this.text = newText);
+    let error = proxy.compileShader(program, shader, this.text = newText);
 
     // If something went wrong, revert to the previous shader.
     if (error.compile || error.link) {
-      proxy.call("compileShader", context, program, shader, this.text = oldText);
+      proxy.compileShader(program, shader, this.text = oldText);
       return error;
     }
     return undefined;
   }, {
     request: { text: Arg(0, "string") },
     response: { error: RetVal("nullable:json") }
   })
 });
@@ -85,21 +107,43 @@ let ShaderFront = protocol.FrontClass(Sh
 });
 
 /**
  * A WebGL program is composed (at the moment, analogue to OpenGL ES 2.0)
  * of two shaders: a vertex shader and a fragment shader.
  */
 let ProgramActor = protocol.ActorClass({
   typeName: "gl-program",
-  initialize: function(conn, id) {
+
+  /**
+   * Create the program actor.
+   *
+   * @param DebuggerServerConnection conn
+   *        The server connection.
+   * @param WebGLProgram program
+   *        The WebGL program being linked.
+   * @param WebGLShader[] shaders
+   *        The WebGL program's cooresponding vertex and fragment shaders.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context owning this program.
+   * @param WebGLProxy proxy
+   *        The proxy methods for the WebGL context owning this program.
+   */
+  initialize: function(conn, [program, shaders, cache, proxy]) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this._shaderActorsCache = { vertex: null, fragment: null };
+    this.program = program;
+    this.shaders = shaders;
+    this.linkedCache = cache;
+    this.linkedProxy = proxy;
   },
 
+  get ownerWindow() this.linkedCache.ownerWindow,
+  get ownerContext() this.linkedCache.ownerContext,
+
   /**
    * Gets the vertex shader linked to this program. This method guarantees
    * a single actor instance per shader.
    */
   getVertexShader: method(function() {
     return this._getShaderActor("vertex");
   }, {
     response: { shader: RetVal("gl-shader") }
@@ -139,50 +183,45 @@ let ProgramActor = protocol.ActorClass({
   }, {
     oneway: true
   }),
 
   /**
    * Prevents any geometry from being rendered using this program.
    */
   blackbox: method(function() {
-    this.observer.cache.blackboxedPrograms.add(this.program);
+    this.linkedCache.setProgramTrait(this.program, PROGRAM_BLACKBOX_TRAIT);
   }, {
     oneway: true
   }),
 
   /**
    * Allows geometry to be rendered using this program.
    */
   unblackbox: method(function() {
-    this.observer.cache.blackboxedPrograms.delete(this.program);
+    this.linkedCache.unsetProgramTrait(this.program, PROGRAM_BLACKBOX_TRAIT);
   }, {
     oneway: true
   }),
 
   /**
    * Returns a cached ShaderActor instance based on the required shader type.
    *
    * @param string type
    *        Either "vertex" or "fragment".
    * @return ShaderActor
    *         The respective shader actor instance.
    */
   _getShaderActor: function(type) {
     if (this._shaderActorsCache[type]) {
       return this._shaderActorsCache[type];
     }
-
-    let shaderActor = new ShaderActor(this.conn);
-    shaderActor.context = this.context;
-    shaderActor.observer = this.observer;
-    shaderActor.program = this.program;
-    shaderActor.shader = this.shadersData[type].ref;
-    shaderActor.text = this.shadersData[type].text;
-
+    let proxy = this.linkedProxy;
+    let shader = proxy.getShaderOfType(this.shaders, type);
+    let shaderActor = new ShaderActor(this.conn, this.program, shader, proxy);
     return this._shaderActorsCache[type] = shaderActor;
   }
 });
 
 /**
  * The corresponding Front object for the ProgramActor.
  */
 let ProgramFront = protocol.FrontClass(ProgramActor, {
@@ -199,17 +238,16 @@ let ProgramFront = protocol.FrontClass(P
 let WebGLActor = exports.WebGLActor = protocol.ActorClass({
   typeName: "webgl",
   initialize: function(conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
     this._onGlobalCreated = this._onGlobalCreated.bind(this);
     this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
     this._onProgramLinked = this._onProgramLinked.bind(this);
-    this._programActorsCache = [];
   },
   destroy: function(conn) {
     protocol.Actor.prototype.destroy.call(this, conn);
     this.finalize();
   },
 
   /**
    * Starts waiting for the current tab actor's document global to be
@@ -218,18 +256,21 @@ let WebGLActor = exports.WebGLActor = pr
    *
    * See ContentObserver and WebGLInstrumenter for more details.
    */
   setup: method(function({ reload }) {
     if (this._initialized) {
       return;
     }
     this._initialized = true;
+
+    this._programActorsCache = [];
     this._contentObserver = new ContentObserver(this.tabActor);
     this._webglObserver = new WebGLObserver();
+
     on(this._contentObserver, "global-created", this._onGlobalCreated);
     on(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
     on(this._webglObserver, "program-linked", this._onProgramLinked);
 
     if (reload) {
       this.tabActor.window.location.reload();
     }
   }, {
@@ -242,31 +283,36 @@ let WebGLActor = exports.WebGLActor = pr
    * to hibernation. This method is called automatically just before the
    * actor is destroyed.
    */
   finalize: method(function() {
     if (!this._initialized) {
       return;
     }
     this._initialized = false;
+
     this._contentObserver.stopListening();
     off(this._contentObserver, "global-created", this._onGlobalCreated);
     off(this._contentObserver, "global-destroyed", this._onGlobalDestroyed);
     off(this._webglObserver, "program-linked", this._onProgramLinked);
+
+    this._programActorsCache = null;
+    this._contentObserver = null;
+    this._webglObserver = null;
   }, {
    oneway: true
   }),
 
   /**
    * Gets an array of cached program actors for the current tab actor's window.
    * This is useful for dealing with bfcache, when no new programs are linked.
    */
   getPrograms: method(function() {
     let id = getInnerWindowID(this.tabActor.window);
-    return this._programActorsCache.filter(e => e.owner == id).map(e => e.actor);
+    return this._programActorsCache.filter(e => e.ownerWindow == id);
   }, {
     response: { programs: RetVal("array:gl-program") }
   }),
 
   /**
    * Events emitted by this actor. The "program-linked" event is fired
    * every time a WebGL program was linked with its respective two shaders.
    */
@@ -283,52 +329,26 @@ let WebGLActor = exports.WebGLActor = pr
   _onGlobalCreated: function(window) {
     WebGLInstrumenter.handle(window, this._webglObserver);
   },
 
   /**
    * Invoked whenever the current tab actor's inner window is destroyed.
    */
   _onGlobalDestroyed: function(id) {
-    this._programActorsCache =
-      this._programActorsCache.filter(e => e.owner != id);
+    removeFromArray(this._programActorsCache, e => e.ownerWindow == id);
+    this._webglObserver.unregisterContextsForWindow(id);
   },
 
   /**
-   * Invoked whenever the current WebGL context links a program.
+   * Invoked whenever an observed WebGL context links a program.
    */
-  _onProgramLinked: function(gl, program, shaders) {
-    let observer = this._webglObserver;
-    let shadersData = { vertex: null, fragment: null };
-
-    for (let shader of shaders) {
-      let text = observer.cache.call("getShaderInfo", shader);
-      let data = { ref: shader, text: text };
-
-      // Make sure the shader data object always contains the vertex shader
-      // first, and the fragment shader second. There are no guarantees that
-      // the compilation order of shaders in the debuggee is always the same.
-      if (gl.getShaderParameter(shader, gl.SHADER_TYPE) == gl.VERTEX_SHADER) {
-        shadersData.vertex = data;
-      } else {
-        shadersData.fragment = data;
-      }
-    }
-
-    let programActor = new ProgramActor(this.conn);
-    programActor.context = gl;
-    programActor.observer = observer;
-    programActor.program = program;
-    programActor.shadersData = shadersData;
-
-    this._programActorsCache.push({
-      owner: getInnerWindowID(this.tabActor.window),
-      actor: programActor
-    });
-
+  _onProgramLinked: function(...args) {
+    let programActor = new ProgramActor(this.conn, args);
+    this._programActorsCache.push(programActor);
     events.emit(this, "program-linked", programActor);
   }
 });
 
 /**
  * The corresponding Front object for the WebGLActor.
  */
 let WebGLFront = exports.WebGLFront = protocol.FrontClass(WebGLActor, {
@@ -401,16 +421,17 @@ let WebGLInstrumenter = {
    * @param nsIDOMWindow window
    *        The window to perform the instrumentation in.
    * @param WebGLObserver observer
    *        The observer watching function calls in the context.
    */
   handle: function(window, observer) {
     let self = this;
 
+    let id = getInnerWindowID(window);
     let canvasElem = XPCNativeWrapper.unwrap(window.HTMLCanvasElement);
     let canvasPrototype = canvasElem.prototype;
     let originalGetContext = canvasPrototype.getContext;
 
     /**
      * Returns a drawing context on the canvas, or null if the context ID is
      * not supported. This override creates an observer for the targeted context
      * type and instruments specific functions in the targeted context instance.
@@ -420,553 +441,656 @@ let WebGLInstrumenter = {
       let context = originalGetContext.call(this, name, options);
       if (!context) {
         return context;
       }
       // Make sure a WebGL (not a 2D) context will be instrumented.
       if (WEBGL_CONTEXT_NAMES.indexOf(name) == -1) {
         return context;
       }
+      // Repeated calls to 'getContext' return the same instance, no need to
+      // instrument everything again.
+      if (observer.for(context)) {
+        return context;
+      }
+
+      // Create a separate state storage for this context.
+      observer.registerContextForWindow(id, context);
 
       // Link our observer to the new WebGL context methods.
       for (let { timing, callback, functions } of self._methods) {
         for (let func of functions) {
-          self._instrument(observer, context, func, timing, callback);
+          self._instrument(observer, context, func, callback, timing);
         }
       }
 
       // Return the decorated context back to the content consumer, which
       // will continue using it normally.
       return context;
     };
   },
 
   /**
    * Overrides a specific method in a HTMLCanvasElement context.
    *
    * @param WebGLObserver observer
    *        The observer watching function calls in the context.
    * @param WebGLRenderingContext context
-   *        The targeted context instance.
+   *        The targeted WebGL context instance.
    * @param string funcName
    *        The function to override.
-   * @param string timing [optional]
-   *        When to issue the callback in relation to the actual context
-   *        function call. Availalble values are "before" and "after" (default).
    * @param string callbackName [optional]
    *        A custom callback function name in the observer. If unspecified,
    *        it will default to the name of the function to override.
+   * @param number timing [optional]
+   *        When to issue the callback in relation to the actual context
+   *        function call. Availalble values are 0 for "before" (default)
+   *        and 1 for "after".
    */
-  _instrument: function(observer, context, funcName, timing, callbackName) {
+  _instrument: function(observer, context, funcName, callbackName, timing = 0) {
+    let { cache, proxy } = observer.for(context);
     let originalFunc = context[funcName];
+    let proxyFuncName = callbackName || funcName;
 
-    context[funcName] = function() {
-      let glArgs = Array.slice(arguments);
-      let glResult, glBreak;
-
-      if (timing == "before" && !observer.suppressHandlers) {
-        glBreak = observer.call(callbackName || funcName, context, glArgs);
+    context[funcName] = function(...glArgs) {
+      if (timing == 0 && !observer.suppressHandlers) {
+        let glBreak = observer[proxyFuncName](glArgs, cache, proxy);
         if (glBreak) return undefined;
       }
 
-      glResult = originalFunc.apply(this, glArgs);
+      let glResult = originalFunc.apply(this, glArgs);
 
-      if (timing == "after" && !observer.suppressHandlers) {
-        glBreak = observer.call(callbackName || funcName, context, glArgs, glResult);
+      if (timing == 1 && !observer.suppressHandlers) {
+        let glBreak = observer[proxyFuncName](glArgs, glResult, cache, proxy);
         if (glBreak) return undefined;
       }
 
       return glResult;
     };
   },
 
   /**
    * Override mappings for WebGL methods.
    */
   _methods: [{
-    timing: "after",
+    timing: 1, // after
     functions: [
       "linkProgram", "getAttribLocation", "getUniformLocation"
     ]
   }, {
-    timing: "before",
     callback: "toggleVertexAttribArray",
     functions: [
       "enableVertexAttribArray", "disableVertexAttribArray"
     ]
   }, {
-    timing: "before",
     callback: "attribute_",
     functions: [
       "vertexAttrib1f", "vertexAttrib2f", "vertexAttrib3f", "vertexAttrib4f",
       "vertexAttrib1fv", "vertexAttrib2fv", "vertexAttrib3fv", "vertexAttrib4fv",
       "vertexAttribPointer"
     ]
   }, {
-    timing: "before",
     callback: "uniform_",
     functions: [
       "uniform1i", "uniform2i", "uniform3i", "uniform4i",
       "uniform1f", "uniform2f", "uniform3f", "uniform4f",
       "uniform1iv", "uniform2iv", "uniform3iv", "uniform4iv",
       "uniform1fv", "uniform2fv", "uniform3fv", "uniform4fv",
       "uniformMatrix2fv", "uniformMatrix3fv", "uniformMatrix4fv"
     ]
   }, {
-    timing: "after",
+    timing: 1, // after
     functions: ["useProgram"]
   }, {
-    timing: "before",
     callback: "draw_",
     functions: [
       "drawArrays", "drawElements"
     ]
   }]
   // TODO: It'd be a good idea to handle other functions as well:
   //   - getActiveUniform
   //   - getUniform
   //   - getActiveAttrib
   //   - getVertexAttrib
 };
 
 /**
  * An observer that captures a WebGL context's method calls.
  */
 function WebGLObserver() {
-  this.cache = new WebGLCache(this);
-  this.proxy = new WebGLProxy(this);
+  this._contexts = new Map();
 }
 
 WebGLObserver.prototype = {
+  _contexts: null,
+
+  /**
+   * Creates a WebGLCache and a WebGLProxy for the specified window and context.
+   *
+   * @param number id
+   *        The id of the window containing the WebGL context.
+   * @param WebGLRenderingContext context
+   *        The WebGL context used in the cache and proxy instances.
+   */
+  registerContextForWindow: function(id, context) {
+    let cache = new WebGLCache(id, context);
+    let proxy = new WebGLProxy(id, context, cache, this);
+
+    this._contexts.set(context, {
+      ownerWindow: id,
+      cache: cache,
+      proxy: proxy
+    });
+  },
+
+  /**
+   * Removes all WebGLCache and WebGLProxy instances for a particular window.
+   *
+   * @param number id
+   *        The id of the window containing the WebGL context.
+   */
+  unregisterContextsForWindow: function(id) {
+    removeFromMap(this._contexts, e => e.ownerWindow == id);
+  },
+
+  /**
+   * Gets the WebGLCache and WebGLProxy instances for a particular context.
+   *
+   * @param WebGLRenderingContext context
+   *        The WebGL context used in the cache and proxy instances.
+   * @return object
+   *         An object containing the corresponding { cache, proxy } instances.
+   */
+  for: function(context) {
+    return this._contexts.get(context);
+  },
+
   /**
    * Set this flag to true to stop observing any context function calls.
    */
   suppressHandlers: false,
 
   /**
    * Called immediately *after* 'linkProgram' is requested in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
    * @param void glResult
    *        The returned value of the original function call.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
+   * @param WebGLProxy proxy
+   *        The proxy methods for the WebGL context initiating this call.
    */
-  linkProgram: function(gl, glArgs, glResult) {
+  linkProgram: function(glArgs, glResult, cache, proxy) {
     let program = glArgs[0];
-    let shaders = gl.getAttachedShaders(program);
-
-    for (let shader of shaders) {
-      let source = gl.getShaderSource(shader);
-      this.cache.call("addShaderInfo", shader, source);
-    }
-
-    emit(this, "program-linked", gl, program, shaders);
+    let shaders = proxy.getAttachedShaders(program);
+    cache.addProgram(program, PROGRAM_DEFAULT_TRAITS);
+    emit(this, "program-linked", program, shaders, cache, proxy);
   },
 
   /**
    * Called immediately *after* 'getAttribLocation' is requested in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
    * @param GLint glResult
    *        The returned value of the original function call.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
    */
-  getAttribLocation: function(gl, glArgs, glResult) {
+  getAttribLocation: function(glArgs, glResult, cache) {
+    // Make sure the attribute's value is legal before caching.
+    if (glResult < 0) {
+      return;
+    }
     let [program, name] = glArgs;
-    this.cache.call("addAttribute", program, name, glResult);
+    cache.addAttribute(program, name, glResult);
   },
 
   /**
    * Called immediately *after* 'getUniformLocation' is requested in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
    * @param WebGLUniformLocation glResult
    *        The returned value of the original function call.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
    */
-  getUniformLocation: function(gl, glArgs, glResult) {
+  getUniformLocation: function(glArgs, glResult, cache) {
+    // Make sure the uniform's value is legal before caching.
+    if (!glResult) {
+      return;
+    }
     let [program, name] = glArgs;
-    this.cache.call("addUniform", program, name, glResult);
+    cache.addUniform(program, name, glResult);
   },
 
   /**
    * Called immediately *before* 'enableVertexAttribArray' or
    * 'disableVertexAttribArray'is requested in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
    */
-  toggleVertexAttribArray: function(gl, glArgs) {
-    glArgs[0] = this.cache.call("getCurrentAttributeLocation", glArgs[0]);
+  toggleVertexAttribArray: function(glArgs, cache) {
+    glArgs[0] = cache.getCurrentAttributeLocation(glArgs[0]);
     return glArgs[0] < 0; // Return true to break original function call.
   },
 
   /**
    * Called immediately *before* 'attribute_' is requested in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
    */
-  attribute_: function(gl, glArgs) {
-    glArgs[0] = this.cache.call("getCurrentAttributeLocation", glArgs[0]);
+  attribute_: function(glArgs, cache) {
+    glArgs[0] = cache.getCurrentAttributeLocation(glArgs[0]);
     return glArgs[0] < 0; // Return true to break original function call.
   },
 
   /**
    * Called immediately *before* 'uniform_' is requested in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
    */
-  uniform_: function(gl, glArgs) {
-    glArgs[0] = this.cache.call("getCurrentUniformLocation", glArgs[0]);
+  uniform_: function(glArgs, cache) {
+    glArgs[0] = cache.getCurrentUniformLocation(glArgs[0]);
     return !glArgs[0]; // Return true to break original function call.
   },
 
   /**
    * Called immediately *after* 'useProgram' is requested in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
    * @param void glResult
    *        The returned value of the original function call.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
    */
-  useProgram: function(gl, glArgs, glResult) {
+  useProgram: function(glArgs, glResult, cache) {
     // Manually keeping a cache and not using gl.getParameter(CURRENT_PROGRAM)
     // because gl.get* functions are slow as potatoes.
-    this.cache.currentProgram = glArgs[0];
+    cache.currentProgram = glArgs[0];
   },
 
   /**
    * Called immediately *before* 'drawArrays' or 'drawElements' is requested
    * in the context.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context initiating this call.
    * @param array glArgs
    *        Overridable arguments with which the function is called.
+   * @param WebGLCache cache
+   *        The state storage for the WebGL context initiating this call.
    */
-  draw_: function(gl, glArgs) {
+  draw_: function(glArgs, cache) {
     // Return true to break original function call.
-    return this.cache.blackboxedPrograms.has(this.cache.currentProgram);
-  },
-
-  /**
-   * Executes a function in this object.
-   * This method makes sure that any handlers in the context observer are
-   * suppressed, hence stopping observing any context function calls.
-   *
-   * @param string funcName
-   *        The function to call.
-   */
-  call: function(funcName, ...args) {
-    let prevState = this.suppressHandlers;
-
-    this.suppressHandlers = true;
-    let result = this[funcName].apply(this, args);
-    this.suppressHandlers = prevState;
-
-    return result;
+    return cache.currentProgramTraits & PROGRAM_BLACKBOX_TRAIT;
   }
 };
 
 /**
- * A cache storing WebGL state, like shaders, attributes or uniforms.
+ * A mechanism for storing a single WebGL context's state, programs, shaders,
+ * attributes or uniforms.
  *
- * @param WebGLObserver observer
- *        The observer for the target context.
+ * @param number id
+ *        The id of the window containing the WebGL context.
+ * @param WebGLRenderingContext context
+ *        The WebGL context for which the state is stored.
  */
-function WebGLCache(observer) {
-  this._observer = observer;
-
-  this.currentProgram = null;
-  this.blackboxedPrograms = new Set();
-
-  this._shaders = new Map();
-  this._attributes = [];
-  this._uniforms = [];
-  this._attributesBridge = new Map();
-  this._uniformsBridge = new Map();
+function WebGLCache(id, context) {
+  this._id = id;
+  this._gl = context;
+  this._programs = new Map();
 }
 
 WebGLCache.prototype = {
-  /**
-   * The current program in the observed WebGL context.
-   */
-  currentProgram: null,
+  _id: 0,
+  _gl: null,
+  _programs: null,
+  _currentProgramInfo: null,
+  _currentAttributesMap: null,
+  _currentUniformsMap: null,
 
-  /**
-   * A set of blackboxed programs in the observed WebGL context.
-   */
-  blackboxedPrograms: null,
+  get ownerWindow() this._id,
+  get ownerContext() this._gl,
 
   /**
-   * Adds shader information to the cache.
+   * Adds a program to the cache.
    *
-   * @param WebGLShader shader
-   *        The shader for which the source is to be cached. If the shader
-   *        was already cached, nothing happens.
-   * @param string text
-   *        The current shader text.
+   * @param WebGLProgram program
+   *        The shader for which the traits are to be cached.
+   * @param number traits
+   *        A default properties mask set for the program.
    */
-  _addShaderInfo: function(shader, text) {
-    if (!this._shaders.has(shader)) {
-      this._shaders.set(shader, text);
-    }
+  addProgram: function(program, traits) {
+    this._programs.set(program, {
+      traits: traits,
+      attributes: [], // keys are GLints (numbers)
+      uniforms: new Map() // keys are WebGLUniformLocations (objects)
+    });
   },
 
   /**
-   * Gets shader information from the cache.
+   * Adds a specific trait to a program. The effect of such properties is
+   * determined by the consumer of this cache.
+   *
+   * @param WebGLProgram program
+   *        The program to add the trait to.
+   * @param number trait
+   *        The property added to the program.
+   */
+  setProgramTrait: function(program, trait) {
+    this._programs.get(program).traits |= trait;
+  },
+
+  /**
+   * Removes a specific trait from a program.
    *
-   * @param WebGLShader shader
-   *        The shader for which the source was cached.
-   * @return object | null
-   *         The original shader source, or null if there's a cache miss.
+   * @param WebGLProgram program
+   *        The program to remove the trait from.
+   * @param number trait
+   *        The property removed from the program.
+   */
+  unsetProgramTrait: function(program, trait) {
+    this._programs.get(program).traits &= ~trait;
+  },
+
+  /**
+   * Sets the currently used program in the context.
+   * @param WebGLProgram program
    */
-  _getShaderInfo: function(shader) {
-    return this._shaders.get(shader);
+  set currentProgram(program) {
+    let programInfo = this._programs.get(program);
+    if (programInfo == null) {
+      return;
+    }
+    this._currentProgramInfo = programInfo;
+    this._currentAttributesMap = programInfo.attributes;
+    this._currentUniformsMap = programInfo.uniforms;
+  },
+
+  /**
+   * Gets the traits for the currently used program.
+   * @return number
+   */
+  get currentProgramTraits() {
+    return this._currentProgramInfo.traits;
   },
 
   /**
    * Adds an attribute to the cache.
    *
    * @param WebGLProgram program
-   *        The program for which the attribute is bound. If the attribute
-   *        was already cached, nothing happens.
+   *        The program for which the attribute is bound.
    * @param string name
    *        The attribute name.
    * @param GLint value
    *        The attribute value.
    */
-  _addAttribute: function(program, name, value) {
-    let isCached = this._attributes.some(e => e.program == program && e.name == name);
-    if (isCached || value < 0) {
-      return;
-    }
-    let attributeInfo = {
-      program: program,
+  addAttribute: function(program, name, value) {
+    this._programs.get(program).attributes[value] = {
       name: name,
       value: value
     };
-    this._attributes.push(attributeInfo);
-    this._attributesBridge.set(value, attributeInfo);
   },
 
   /**
    * Adds a uniform to the cache.
    *
    * @param WebGLProgram program
-   *        The program for which the uniform is bound. If the uniform
-   *        was already cached, nothing happens.
+   *        The program for which the uniform is bound.
    * @param string name
    *        The uniform name.
    * @param WebGLUniformLocation value
    *        The uniform value.
    */
-  _addUniform: function(program, name, value) {
-    let isCached = this._uniforms.some(e => e.program == program && e.name == name);
-    if (isCached || !value) {
-      return;
-    }
-    let uniformInfo = {
-      program: program,
+  addUniform: function(program, name, value) {
+    this._programs.get(program).uniforms.set(new XPCNativeWrapper(value), {
       name: name,
       value: value
-    };
-    this._uniforms.push(uniformInfo);
-    this._uniformsBridge.set(new XPCNativeWrapper(value), uniformInfo);
-  },
-
-  /**
-   * Gets all the cached attributes for a specific program.
-   *
-   * @param WebGLProgram program
-   *        The program for which the attributes are bound.
-   * @return array
-   *         A list containing information about all the attributes.
-   */
-  _getAttributesForProgram: function(program) {
-    return this._attributes.filter(e => e.program == program);
-  },
-
-  /**
-   * Gets all the cached uniforms for a specific program.
-   *
-   * @param WebGLProgram program
-   *        The program for which the uniforms are bound.
-   * @return array
-   *         A list containing information about all the uniforms.
-   */
-  _getUniformsForProgram: function(program) {
-    return this._uniforms.filter(e => e.program == program);
+    });
   },
 
   /**
    * Updates the attribute locations for a specific program.
    * This is necessary, for example, when the shader is relinked and all the
    * attribute locations become obsolete.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context owning the program.
    * @param WebGLProgram program
    *        The program for which the attributes need updating.
    */
-  _updateAttributesForProgram: function(gl, program) {
-    let dirty = this._attributes.filter(e => e.program == program);
-    dirty.forEach(e => e.value = gl.getAttribLocation(program, e.name));
+  updateAttributesForProgram: function(program) {
+    let attributes = this._programs.get(program).attributes;
+    for (let attribute of attributes) {
+      attribute.value = this._gl.getAttribLocation(program, attribute.name);
+    }
   },
 
   /**
    * Updates the uniform locations for a specific program.
    * This is necessary, for example, when the shader is relinked and all the
    * uniform locations become obsolete.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context owning the program.
    * @param WebGLProgram program
    *        The program for which the uniforms need updating.
    */
-  _updateUniformsForProgram: function(gl, program) {
-    let dirty = this._uniforms.filter(e => e.program == program);
-    dirty.forEach(e => e.value = gl.getUniformLocation(program, e.name));
+  updateUniformsForProgram: function(program) {
+    let uniforms = this._programs.get(program).uniforms;
+    for (let [, uniform] of uniforms) {
+      uniform.value = this._gl.getUniformLocation(program, uniform.name);
+    }
   },
 
   /**
    * Gets the actual attribute location in a specific program.
    * When relinked, all the attribute locations become obsolete and are updated
    * in the cache. This method returns the (current) real attribute location.
    *
    * @param GLint initialValue
    *        The initial attribute value.
    * @return GLint
    *         The current attribute value, or the initial value if it's already
    *         up to date with its corresponding program.
    */
-  _getCurrentAttributeLocation: function(initialValue) {
-    let currentInfo = this._attributesBridge.get(initialValue);
+  getCurrentAttributeLocation: function(initialValue) {
+    let attributes = this._currentAttributesMap;
+    let currentInfo = attributes ? attributes[initialValue] : null;
     return currentInfo ? currentInfo.value : initialValue;
   },
 
   /**
    * Gets the actual uniform location in a specific program.
    * When relinked, all the uniform locations become obsolete and are updated
    * in the cache. This method returns the (current) real uniform location.
    *
    * @param WebGLUniformLocation initialValue
    *        The initial uniform value.
    * @return WebGLUniformLocation
    *         The current uniform value, or the initial value if it's already
    *         up to date with its corresponding program.
    */
-  _getCurrentUniformLocation: function(initialValue) {
-    let currentInfo = this._uniformsBridge.get(initialValue);
+  getCurrentUniformLocation: function(initialValue) {
+    let uniforms = this._currentUniformsMap;
+    let currentInfo = uniforms ? uniforms.get(initialValue) : null;
     return currentInfo ? currentInfo.value : initialValue;
-  },
-
-  /**
-   * Executes a function in this object.
-   * This method makes sure that any handlers in the context observer are
-   * suppressed, hence stopping observing any context function calls.
-   *
-   * @param string funcName
-   *        The function to call.
-   * @return any
-   *         The called function result.
-   */
-  call: function(funcName, ...aArgs) {
-    let prevState = this._observer.suppressHandlers;
-
-    this._observer.suppressHandlers = true;
-    let result = this["_" + funcName].apply(this, aArgs);
-    this._observer.suppressHandlers = prevState;
-
-    return result;
   }
 };
 
 /**
- * A mechanism for injecting or qureying state into/from a WebGL context.
+ * A mechanism for injecting or qureying state into/from a single WebGL context.
+ *
+ * Any interaction with a WebGL context should go through this proxy.
+ * Otherwise, the corresponding observer would register the calls as coming
+ * from content, which is usually not desirable. Infinite call stacks are bad.
  *
+ * @param number id
+ *        The id of the window containing the WebGL context.
+ * @param WebGLRenderingContext context
+ *        The WebGL context used for the proxy methods.
+ * @param WebGLCache cache
+ *        The state storage for the corresponding context.
  * @param WebGLObserver observer
- *        The observer for the target context.
+ *        The observer watching function calls in the corresponding context.
  */
-function WebGLProxy(observer) {
+function WebGLProxy(id, context, cache, observer) {
+  this._id = id;
+  this._gl = context;
+  this._cache = cache;
   this._observer = observer;
+
+  let exports = [
+    "getAttachedShaders",
+    "getShaderSource",
+    "getShaderOfType",
+    "compileShader"
+  ];
+  exports.forEach(e => this[e] = (...args) => this._call(e, args));
 }
 
 WebGLProxy.prototype = {
-  get cache() this._observer.cache,
+  _id: 0,
+  _gl: null,
+  _cache: null,
+  _observer: null,
+
+  get ownerWindow() this._id,
+  get ownerContext() this._gl,
+
+  /**
+   * Returns the shader objects attached to a program object.
+   *
+   * @param WebGLProgram program
+   *        The program for which to retrieve the attached shaders.
+   * @return array
+   *         The attached vertex and fragment shaders.
+   */
+  _getAttachedShaders: function(program) {
+    return this._gl.getAttachedShaders(program);
+  },
+
+  /**
+   * Returns the source code string from a shader object.
+   *
+   * @param WebGLShader shader
+   *        The shader for which to retrieve the source code.
+   * @return string
+   *         The shader's source code.
+   */
+  _getShaderSource: function(shader) {
+    return this._gl.getShaderSource(shader);
+  },
+
+  /**
+   * Finds a shader of the specified type in a list.
+   *
+   * @param WebGLShader[] shaders
+   *        The shaders for which to check the type.
+   * @param string type
+   *        Either "vertex" or "fragment".
+   * @return WebGLShader | null
+   *         The shader of the specified type, or null if nothing is found.
+   */
+  _getShaderOfType: function(shaders, type) {
+    let gl = this._gl;
+    let shaderTypeEnum = {
+      vertex: gl.VERTEX_SHADER,
+      fragment: gl.FRAGMENT_SHADER
+    }[type];
+
+    for (let shader of shaders) {
+      if (gl.getShaderParameter(shader, gl.SHADER_TYPE) == shaderTypeEnum) {
+        return shader;
+      }
+    }
+    return null;
+  },
 
   /**
    * Changes a shader's source code and relinks the respective program.
    *
-   * @param WebGLRenderingContext gl
-   *        The WebGL context owning the program.
    * @param WebGLProgram program
    *        The program who's linked shader is to be modified.
    * @param WebGLShader shader
    *        The shader to be modified.
    * @param string text
    *        The new shader source code.
-   * @return string
-   *         The shader's compilation and linking status.
+   * @return object
+   *         An object containing the compilation and linking status.
    */
-  _compileShader: function(gl, program, shader, text) {
+  _compileShader: function(program, shader, text) {
+    let gl = this._gl;
     gl.shaderSource(shader, text);
     gl.compileShader(shader);
     gl.linkProgram(program);
 
     let error = { compile: "", link: "" };
 
     if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
       error.compile = gl.getShaderInfoLog(shader);
     }
     if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
       error.link = gl.getShaderInfoLog(shader);
     }
 
-    this.cache.call("updateAttributesForProgram", gl, program);
-    this.cache.call("updateUniformsForProgram", gl, program);
+    this._cache.updateAttributesForProgram(program);
+    this._cache.updateUniformsForProgram(program);
 
     return error;
   },
 
   /**
    * Executes a function in this object.
+   *
    * This method makes sure that any handlers in the context observer are
    * suppressed, hence stopping observing any context function calls.
    *
    * @param string funcName
    *        The function to call.
+   * @param array args
+   *        An array of arguments.
    * @return any
    *         The called function result.
    */
-  call: function(funcName, ...aArgs) {
+  _call: function(funcName, args) {
     let prevState = this._observer.suppressHandlers;
 
     this._observer.suppressHandlers = true;
-    let result = this["_" + funcName].apply(this, aArgs);
+    let result = this["_" + funcName].apply(this, args);
     this._observer.suppressHandlers = prevState;
 
     return result;
   }
 };
 
+// Utility functions.
+
 function getInnerWindowID(window) {
   return window
     .QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindowUtils)
     .currentInnerWindowID;
 }
+
+function removeFromMap(map, predicate) {
+  for (let [key, value] of map) {
+    if (predicate(value)) {
+      map.delete(key);
+    }
+  }
+};
+
+function removeFromArray(array, predicate) {
+  for (let value of array) {
+    if (predicate(value)) {
+      array.splice(array.indexOf(value), 1);
+    }
+  }
+}