summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/webgl.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/webgl.js')
-rw-r--r--devtools/server/actors/webgl.js1322
1 files changed, 1322 insertions, 0 deletions
diff --git a/devtools/server/actors/webgl.js b/devtools/server/actors/webgl.js
new file mode 100644
index 000000000..137448647
--- /dev/null
+++ b/devtools/server/actors/webgl.js
@@ -0,0 +1,1322 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+const events = require("sdk/event/core");
+const promise = require("promise");
+const protocol = require("devtools/shared/protocol");
+const { ContentObserver } = require("devtools/shared/content-observer");
+const { on, once, off, emit } = events;
+const { method, Arg, Option, RetVal } = protocol;
+const {
+ shaderSpec,
+ programSpec,
+ webGLSpec,
+} = require("devtools/shared/specs/webgl");
+
+const WEBGL_CONTEXT_NAMES = ["webgl", "experimental-webgl", "moz-webgl"];
+
+// These traits are bit masks. Make sure they're powers of 2.
+const PROGRAM_DEFAULT_TRAITS = 0;
+const PROGRAM_BLACKBOX_TRAIT = 1;
+const PROGRAM_HIGHLIGHT_TRAIT = 2;
+
+/**
+ * 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.
+ */
+var ShaderActor = protocol.ActorClassWithSpec(shaderSpec, {
+ /**
+ * 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: function () {
+ return this.text;
+ },
+
+ /**
+ * Sets and compiles new source code for this shader.
+ */
+ compile: function (text) {
+ // Get the shader and corresponding program to change via the WebGL proxy.
+ 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.compileShader(program, shader, this.text = newText);
+
+ // If something went wrong, revert to the previous shader.
+ if (error.compile || error.link) {
+ proxy.compileShader(program, shader, this.text = oldText);
+ return error;
+ }
+ return undefined;
+ }
+});
+
+/**
+ * A WebGL program is composed (at the moment, analogue to OpenGL ES 2.0)
+ * of two shaders: a vertex shader and a fragment shader.
+ */
+var ProgramActor = protocol.ActorClassWithSpec(programSpec, {
+ /**
+ * 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() {
+ return this.linkedCache.ownerWindow;
+ },
+
+ get ownerContext() {
+ return this.linkedCache.ownerContext;
+ },
+
+ /**
+ * Gets the vertex shader linked to this program. This method guarantees
+ * a single actor instance per shader.
+ */
+ getVertexShader: function () {
+ return this._getShaderActor("vertex");
+ },
+
+ /**
+ * Gets the fragment shader linked to this program. This method guarantees
+ * a single actor instance per shader.
+ */
+ getFragmentShader: function () {
+ return this._getShaderActor("fragment");
+ },
+
+ /**
+ * Highlights any geometry rendered using this program.
+ */
+ highlight: function (tint) {
+ this.linkedProxy.highlightTint = tint;
+ this.linkedCache.setProgramTrait(this.program, PROGRAM_HIGHLIGHT_TRAIT);
+ },
+
+ /**
+ * Allows geometry to be rendered normally using this program.
+ */
+ unhighlight: function () {
+ this.linkedCache.unsetProgramTrait(this.program, PROGRAM_HIGHLIGHT_TRAIT);
+ },
+
+ /**
+ * Prevents any geometry from being rendered using this program.
+ */
+ blackbox: function () {
+ this.linkedCache.setProgramTrait(this.program, PROGRAM_BLACKBOX_TRAIT);
+ },
+
+ /**
+ * Allows geometry to be rendered using this program.
+ */
+ unblackbox: function () {
+ this.linkedCache.unsetProgramTrait(this.program, PROGRAM_BLACKBOX_TRAIT);
+ },
+
+ /**
+ * 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 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 WebGL Actor handles simple interaction with a WebGL context via a few
+ * high-level methods. After instantiating this actor, you'll need to set it
+ * up by calling setup().
+ */
+var WebGLActor = exports.WebGLActor = protocol.ActorClassWithSpec(webGLSpec, {
+ 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);
+ },
+ destroy: function (conn) {
+ protocol.Actor.prototype.destroy.call(this, conn);
+ this.finalize();
+ },
+
+ /**
+ * Starts waiting for the current tab actor's document global to be
+ * created, in order to instrument the Canvas context and become
+ * aware of everything the content does WebGL-wise.
+ *
+ * See ContentObserver and WebGLInstrumenter for more details.
+ */
+ setup: function ({ reload }) {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ this._programActorsCache = [];
+ this._webglObserver = new WebGLObserver();
+
+ on(this.tabActor, "window-ready", this._onGlobalCreated);
+ on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+ on(this._webglObserver, "program-linked", this._onProgramLinked);
+
+ if (reload) {
+ this.tabActor.window.location.reload();
+ }
+ },
+
+ /**
+ * Stops listening for document global changes and puts this actor
+ * to hibernation. This method is called automatically just before the
+ * actor is destroyed.
+ */
+ finalize: function () {
+ if (!this._initialized) {
+ return;
+ }
+ this._initialized = false;
+
+ off(this.tabActor, "window-ready", this._onGlobalCreated);
+ off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
+ off(this._webglObserver, "program-linked", this._onProgramLinked);
+
+ this._programActorsCache = null;
+ this._contentObserver = null;
+ this._webglObserver = null;
+ },
+
+ /**
+ * 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: function () {
+ let id = ContentObserver.GetInnerWindowID(this.tabActor.window);
+ return this._programActorsCache.filter(e => e.ownerWindow == id);
+ },
+
+ /**
+ * Waits for one frame via `requestAnimationFrame` on the tab actor's window.
+ * Used in tests.
+ */
+ waitForFrame: function () {
+ let deferred = promise.defer();
+ this.tabActor.window.requestAnimationFrame(deferred.resolve);
+ return deferred.promise;
+ },
+
+ /**
+ * Gets a pixel's RGBA value from a context specified by selector
+ * and the coordinates of the pixel in question.
+ * Currently only used in tests.
+ *
+ * @param string selector
+ * A string selector to select the canvas in question from the DOM.
+ * @param Object position
+ * An object with an `x` and `y` property indicating coordinates of the pixel being inspected.
+ * @return Object
+ * An object containing `r`, `g`, `b`, and `a` properties of the pixel.
+ */
+ getPixel: function ({ selector, position }) {
+ let { x, y } = position;
+ let canvas = this.tabActor.window.document.querySelector(selector);
+ let context = XPCNativeWrapper.unwrap(canvas.getContext("webgl"));
+ let { proxy } = this._webglObserver.for(context);
+ let height = canvas.height;
+
+ let buffer = new this.tabActor.window.Uint8Array(4);
+ buffer = XPCNativeWrapper.unwrap(buffer);
+
+ proxy.readPixels(x, height - y - 1, 1, 1, context.RGBA, context.UNSIGNED_BYTE, buffer);
+
+ return { r: buffer[0], g: buffer[1], b: buffer[2], a: buffer[3] };
+ },
+
+ /**
+ * Gets an array of all cached program actors belonging to all windows.
+ * This should only be used for tests.
+ */
+ _getAllPrograms: function () {
+ return this._programActorsCache;
+ },
+
+
+ /**
+ * Invoked whenever the current tab actor's document global is created.
+ */
+ _onGlobalCreated: function ({id, window, isTopLevel}) {
+ if (isTopLevel) {
+ WebGLInstrumenter.handle(window, this._webglObserver);
+ events.emit(this, "global-created", id);
+ }
+ },
+
+ /**
+ * Invoked whenever the current tab actor's inner window is destroyed.
+ */
+ _onGlobalDestroyed: function ({id, isTopLevel, isFrozen}) {
+ if (isTopLevel && !isFrozen) {
+ removeFromArray(this._programActorsCache, e => e.ownerWindow == id);
+ this._webglObserver.unregisterContextsForWindow(id);
+ events.emit(this, "global-destroyed", id);
+ }
+ },
+
+ /**
+ * Invoked whenever an observed WebGL context links a program.
+ */
+ _onProgramLinked: function (...args) {
+ let programActor = new ProgramActor(this.conn, args);
+ this._programActorsCache.push(programActor);
+ events.emit(this, "program-linked", programActor);
+ }
+});
+
+/**
+ * Instruments a HTMLCanvasElement with the appropriate inspection methods.
+ */
+var WebGLInstrumenter = {
+ /**
+ * Overrides the getContext method in the HTMLCanvasElement prototype.
+ *
+ * @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 = ContentObserver.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.
+ */
+ canvasPrototype.getContext = function (name, options) {
+ // Make sure a context was able to be created.
+ 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, 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 WebGL context instance.
+ * @param string funcName
+ * The function to override.
+ * @param array callbackName [optional]
+ * The two callback function names in the observer, corresponding to
+ * the "before" and "after" invocation times. If unspecified, they 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 -1 for "before" (default)
+ * 1 for "after" and 0 for "before and after".
+ */
+ _instrument: function (observer, context, funcName, callbackName = [], timing = -1) {
+ let { cache, proxy } = observer.for(context);
+ let originalFunc = context[funcName];
+ let beforeFuncName = callbackName[0] || funcName;
+ let afterFuncName = callbackName[1] || callbackName[0] || funcName;
+
+ context[funcName] = function (...glArgs) {
+ if (timing <= 0 && !observer.suppressHandlers) {
+ let glBreak = observer[beforeFuncName](glArgs, cache, proxy);
+ if (glBreak) return undefined;
+ }
+
+ // Invoking .apply on an unxrayed content function doesn't work, because
+ // the arguments array is inaccessible to it. Get Xrays back.
+ let glResult = Cu.waiveXrays(Cu.unwaiveXrays(originalFunc).apply(this, glArgs));
+
+ if (timing >= 0 && !observer.suppressHandlers) {
+ let glBreak = observer[afterFuncName](glArgs, glResult, cache, proxy);
+ if (glBreak) return undefined;
+ }
+
+ return glResult;
+ };
+ },
+
+ /**
+ * Override mappings for WebGL methods.
+ */
+ _methods: [{
+ timing: 1, // after
+ functions: [
+ "linkProgram", "getAttribLocation", "getUniformLocation"
+ ]
+ }, {
+ timing: -1, // before
+ callback: [
+ "toggleVertexAttribArray"
+ ],
+ functions: [
+ "enableVertexAttribArray", "disableVertexAttribArray"
+ ]
+ }, {
+ timing: -1, // before
+ callback: [
+ "attribute_"
+ ],
+ functions: [
+ "vertexAttrib1f", "vertexAttrib2f", "vertexAttrib3f", "vertexAttrib4f",
+ "vertexAttrib1fv", "vertexAttrib2fv", "vertexAttrib3fv", "vertexAttrib4fv",
+ "vertexAttribPointer"
+ ]
+ }, {
+ timing: -1, // before
+ callback: [
+ "uniform_"
+ ],
+ functions: [
+ "uniform1i", "uniform2i", "uniform3i", "uniform4i",
+ "uniform1f", "uniform2f", "uniform3f", "uniform4f",
+ "uniform1iv", "uniform2iv", "uniform3iv", "uniform4iv",
+ "uniform1fv", "uniform2fv", "uniform3fv", "uniform4fv",
+ "uniformMatrix2fv", "uniformMatrix3fv", "uniformMatrix4fv"
+ ]
+ }, {
+ timing: -1, // before
+ functions: [
+ "useProgram", "enable", "disable", "blendColor",
+ "blendEquation", "blendEquationSeparate",
+ "blendFunc", "blendFuncSeparate"
+ ]
+ }, {
+ timing: 0, // before and after
+ callback: [
+ "beforeDraw_", "afterDraw_"
+ ],
+ 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._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);
+ cache.refreshState(proxy);
+
+ 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 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 (glArgs, glResult, cache, proxy) {
+ let program = glArgs[0];
+ 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 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 (glArgs, glResult, cache) {
+ // Make sure the attribute's value is legal before caching.
+ if (glResult < 0) {
+ return;
+ }
+ let [program, name] = glArgs;
+ cache.addAttribute(program, name, glResult);
+ },
+
+ /**
+ * Called immediately *after* 'getUniformLocation' is requested in the context.
+ *
+ * @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 (glArgs, glResult, cache) {
+ // Make sure the uniform's value is legal before caching.
+ if (!glResult) {
+ return;
+ }
+ let [program, name] = glArgs;
+ cache.addUniform(program, name, glResult);
+ },
+
+ /**
+ * Called immediately *before* 'enableVertexAttribArray' or
+ * 'disableVertexAttribArray'is requested in the context.
+ *
+ * @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 (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 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 (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 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 (glArgs, cache) {
+ glArgs[0] = cache.getCurrentUniformLocation(glArgs[0]);
+ return !glArgs[0]; // Return true to break original function call.
+ },
+
+ /**
+ * Called immediately *before* 'useProgram' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ useProgram: function (glArgs, cache) {
+ // Manually keeping a cache and not using gl.getParameter(CURRENT_PROGRAM)
+ // because gl.get* functions are slow as potatoes.
+ cache.currentProgram = glArgs[0];
+ },
+
+ /**
+ * Called immediately *before* 'enable' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ enable: function (glArgs, cache) {
+ cache.currentState[glArgs[0]] = true;
+ },
+
+ /**
+ * Called immediately *before* 'disable' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ disable: function (glArgs, cache) {
+ cache.currentState[glArgs[0]] = false;
+ },
+
+ /**
+ * Called immediately *before* 'blendColor' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ blendColor: function (glArgs, cache) {
+ let blendColor = cache.currentState.blendColor;
+ blendColor[0] = glArgs[0];
+ blendColor[1] = glArgs[1];
+ blendColor[2] = glArgs[2];
+ blendColor[3] = glArgs[3];
+ },
+
+ /**
+ * Called immediately *before* 'blendEquation' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ blendEquation: function (glArgs, cache) {
+ let state = cache.currentState;
+ state.blendEquationRgb = state.blendEquationAlpha = glArgs[0];
+ },
+
+ /**
+ * Called immediately *before* 'blendEquationSeparate' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ blendEquationSeparate: function (glArgs, cache) {
+ let state = cache.currentState;
+ state.blendEquationRgb = glArgs[0];
+ state.blendEquationAlpha = glArgs[1];
+ },
+
+ /**
+ * Called immediately *before* 'blendFunc' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ blendFunc: function (glArgs, cache) {
+ let state = cache.currentState;
+ state.blendSrcRgb = state.blendSrcAlpha = glArgs[0];
+ state.blendDstRgb = state.blendDstAlpha = glArgs[1];
+ },
+
+ /**
+ * Called immediately *before* 'blendFuncSeparate' is requested in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @param WebGLCache cache
+ * The state storage for the WebGL context initiating this call.
+ */
+ blendFuncSeparate: function (glArgs, cache) {
+ let state = cache.currentState;
+ state.blendSrcRgb = glArgs[0];
+ state.blendDstRgb = glArgs[1];
+ state.blendSrcAlpha = glArgs[2];
+ state.blendDstAlpha = glArgs[3];
+ },
+
+ /**
+ * Called immediately *before* 'drawArrays' or 'drawElements' is requested
+ * in the context.
+ *
+ * @param array glArgs
+ * Overridable arguments with which the function is called.
+ * @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.
+ */
+ beforeDraw_: function (glArgs, cache, proxy) {
+ let traits = cache.currentProgramTraits;
+
+ // Handle program blackboxing.
+ if (traits & PROGRAM_BLACKBOX_TRAIT) {
+ return true; // Return true to break original function call.
+ }
+ // Handle program highlighting.
+ if (traits & PROGRAM_HIGHLIGHT_TRAIT) {
+ proxy.enableHighlighting();
+ }
+
+ return false;
+ },
+
+ /**
+ * Called immediately *after* 'drawArrays' or 'drawElements' is requested
+ * in the context.
+ *
+ * @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.
+ */
+ afterDraw_: function (glArgs, glResult, cache, proxy) {
+ let traits = cache.currentProgramTraits;
+
+ // Handle program highlighting.
+ if (traits & PROGRAM_HIGHLIGHT_TRAIT) {
+ proxy.disableHighlighting();
+ }
+ }
+};
+
+/**
+ * A mechanism for storing a single WebGL context's state, programs, shaders,
+ * attributes or uniforms.
+ *
+ * @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(id, context) {
+ this._id = id;
+ this._gl = context;
+ this._programs = new Map();
+ this.currentState = {};
+}
+
+WebGLCache.prototype = {
+ _id: 0,
+ _gl: null,
+ _programs: null,
+ _currentProgramInfo: null,
+ _currentAttributesMap: null,
+ _currentUniformsMap: null,
+
+ get ownerWindow() {
+ return this._id;
+ },
+
+ get ownerContext() {
+ return this._gl;
+ },
+
+ /**
+ * A collection of flags or properties representing the context's state.
+ * Implemented as an object hash and not a Map instance because keys are
+ * always either strings or numbers.
+ */
+ currentState: null,
+
+ /**
+ * Populates the current state with values retrieved from the context.
+ *
+ * @param WebGLProxy proxy
+ * The proxy methods for the WebGL context owning the state.
+ */
+ refreshState: function (proxy) {
+ let gl = this._gl;
+ let s = this.currentState;
+
+ // Populate only with the necessary parameters. Not all default WebGL
+ // state values are required.
+ s[gl.BLEND] = proxy.isEnabled("BLEND");
+ s.blendColor = proxy.getParameter("BLEND_COLOR");
+ s.blendEquationRgb = proxy.getParameter("BLEND_EQUATION_RGB");
+ s.blendEquationAlpha = proxy.getParameter("BLEND_EQUATION_ALPHA");
+ s.blendSrcRgb = proxy.getParameter("BLEND_SRC_RGB");
+ s.blendSrcAlpha = proxy.getParameter("BLEND_SRC_ALPHA");
+ s.blendDstRgb = proxy.getParameter("BLEND_DST_RGB");
+ s.blendDstAlpha = proxy.getParameter("BLEND_DST_ALPHA");
+ },
+
+ /**
+ * Adds a program to the cache.
+ *
+ * @param WebGLProgram program
+ * The shader for which the traits are to be cached.
+ * @param number traits
+ * A default properties mask set for the program.
+ */
+ addProgram: function (program, traits) {
+ this._programs.set(program, {
+ traits: traits,
+ attributes: [], // keys are GLints (numbers)
+ uniforms: new Map() // keys are WebGLUniformLocations (objects)
+ });
+ },
+
+ /**
+ * 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 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
+ */
+ 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.
+ * @param string name
+ * The attribute name.
+ * @param GLint value
+ * The attribute value.
+ */
+ addAttribute: function (program, name, value) {
+ this._programs.get(program).attributes[value] = {
+ name: name,
+ value: value
+ };
+ },
+
+ /**
+ * Adds a uniform to the cache.
+ *
+ * @param WebGLProgram program
+ * 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) {
+ this._programs.get(program).uniforms.set(new XPCNativeWrapper(value), {
+ name: name,
+ value: value
+ });
+ },
+
+ /**
+ * 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 WebGLProgram program
+ * The program for which the attributes need updating.
+ */
+ 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 WebGLProgram program
+ * The program for which the uniforms need updating.
+ */
+ 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 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 uniforms = this._currentUniformsMap;
+ let currentInfo = uniforms ? uniforms.get(initialValue) : null;
+ return currentInfo ? currentInfo.value : initialValue;
+ }
+};
+
+/**
+ * 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 watching function calls in the corresponding context.
+ */
+function WebGLProxy(id, context, cache, observer) {
+ this._id = id;
+ this._gl = context;
+ this._cache = cache;
+ this._observer = observer;
+
+ let exports = [
+ "isEnabled",
+ "getParameter",
+ "getAttachedShaders",
+ "getShaderSource",
+ "getShaderOfType",
+ "compileShader",
+ "enableHighlighting",
+ "disableHighlighting",
+ "readPixels"
+ ];
+ exports.forEach(e => this[e] = (...args) => this._call(e, args));
+}
+
+WebGLProxy.prototype = {
+ _id: 0,
+ _gl: null,
+ _cache: null,
+ _observer: null,
+
+ get ownerWindow() {
+ return this._id;
+ },
+ get ownerContext() {
+ return this._gl;
+ },
+
+ /**
+ * Test whether a WebGL capability is enabled.
+ *
+ * @param string name
+ * The WebGL capability name, for example "BLEND".
+ * @return boolean
+ * True if enabled, false otherwise.
+ */
+ _isEnabled: function (name) {
+ return this._gl.isEnabled(this._gl[name]);
+ },
+
+ /**
+ * Returns the value for the specified WebGL parameter name.
+ *
+ * @param string name
+ * The WebGL parameter name, for example "BLEND_COLOR".
+ * @return any
+ * The corresponding parameter's value.
+ */
+ _getParameter: function (name) {
+ return this._gl.getParameter(this._gl[name]);
+ },
+
+ /**
+ * Returns the renderbuffer property value for the specified WebGL parameter.
+ * If no renderbuffer binding is available, null is returned.
+ *
+ * @param string name
+ * The WebGL parameter name, for example "BLEND_COLOR".
+ * @return any
+ * The corresponding parameter's value.
+ */
+ _getRenderbufferParameter: function (name) {
+ if (!this._getParameter("RENDERBUFFER_BINDING")) {
+ return null;
+ }
+ let gl = this._gl;
+ return gl.getRenderbufferParameter(gl.RENDERBUFFER, gl[name]);
+ },
+
+ /**
+ * Returns the framebuffer property value for the specified WebGL parameter.
+ * If no framebuffer binding is available, null is returned.
+ *
+ * @param string type
+ * The framebuffer object attachment point, for example "COLOR_ATTACHMENT0".
+ * @param string name
+ * The WebGL parameter name, for example "FRAMEBUFFER_ATTACHMENT_OBJECT_NAME".
+ * If unspecified, defaults to "FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE".
+ * @return any
+ * The corresponding parameter's value.
+ */
+ _getFramebufferAttachmentParameter: function (type, name = "FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE") {
+ if (!this._getParameter("FRAMEBUFFER_BINDING")) {
+ return null;
+ }
+ let gl = this._gl;
+ return gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl[type], gl[name]);
+ },
+
+ /**
+ * 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 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 object
+ * An object containing the compilation and linking status.
+ */
+ _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.updateAttributesForProgram(program);
+ this._cache.updateUniformsForProgram(program);
+
+ return error;
+ },
+
+ /**
+ * Enables color blending based on the geometry highlight tint.
+ */
+ _enableHighlighting: function () {
+ let gl = this._gl;
+
+ // Avoid changing the blending params when "rendering to texture".
+
+ // Check drawing to a custom framebuffer bound to the default renderbuffer.
+ let hasFramebuffer = this._getParameter("FRAMEBUFFER_BINDING");
+ let hasRenderbuffer = this._getParameter("RENDERBUFFER_BINDING");
+ if (hasFramebuffer && !hasRenderbuffer) {
+ return;
+ }
+
+ // Check drawing to a depth or stencil component of the framebuffer.
+ let writesDepth = this._getFramebufferAttachmentParameter("DEPTH_ATTACHMENT");
+ let writesStencil = this._getFramebufferAttachmentParameter("STENCIL_ATTACHMENT");
+ if (writesDepth || writesStencil) {
+ return;
+ }
+
+ // Non-premultiplied alpha blending based on a predefined constant color.
+ // Simply using gl.colorMask won't work, because we want non-tinted colors
+ // to be drawn as black, not ignored.
+ gl.enable(gl.BLEND);
+ gl.blendColor.apply(gl, this.highlightTint);
+ gl.blendEquation(gl.FUNC_ADD);
+ gl.blendFunc(gl.CONSTANT_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.CONSTANT_COLOR, gl.ZERO);
+ this.wasHighlighting = true;
+ },
+
+ /**
+ * Disables color blending based on the geometry highlight tint, by
+ * reverting the corresponding params back to their original values.
+ */
+ _disableHighlighting: function () {
+ let gl = this._gl;
+ let s = this._cache.currentState;
+
+ gl[s[gl.BLEND] ? "enable" : "disable"](gl.BLEND);
+ gl.blendColor.apply(gl, s.blendColor);
+ gl.blendEquationSeparate(s.blendEquationRgb, s.blendEquationAlpha);
+ gl.blendFuncSeparate(s.blendSrcRgb, s.blendDstRgb, s.blendSrcAlpha, s.blendDstAlpha);
+ },
+
+ /**
+ * Returns the pixel values at the position specified on the canvas.
+ */
+ _readPixels: function (x, y, w, h, format, type, buffer) {
+ this._gl.readPixels(x, y, w, h, format, type, buffer);
+ },
+
+ /**
+ * The color tint used for highlighting geometry.
+ * @see _enableHighlighting and _disableHighlighting.
+ */
+ highlightTint: [0, 0, 0, 0],
+
+ /**
+ * 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, args) {
+ let prevState = this._observer.suppressHandlers;
+
+ this._observer.suppressHandlers = true;
+ let result = this["_" + funcName].apply(this, args);
+ this._observer.suppressHandlers = prevState;
+
+ return result;
+ }
+};
+
+// Utility functions.
+
+function removeFromMap(map, predicate) {
+ for (let [key, value] of map) {
+ if (predicate(value)) {
+ map.delete(key);
+ }
+ }
+}
+
+function removeFromArray(array, predicate) {
+ for (let i = 0; i < array.length;) {
+ if (predicate(array[i])) {
+ array.splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+}