mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 00:55:37 +00:00
1323 lines
41 KiB
JavaScript
1323 lines
41 KiB
JavaScript
/* 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++;
|
|
}
|
|
}
|
|
}
|