diff options
Diffstat (limited to 'devtools/server/actors/webgl.js')
-rw-r--r-- | devtools/server/actors/webgl.js | 1322 |
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++; + } + } +} |