blob: 00d50cf82c82a7c5a66a6ed0a0c71196d180eabe [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @param {InjectedScriptHostClass} InjectedScriptHost
* @param {Window} inspectedWindow
* @param {number} injectedScriptId
* @param {!InjectedScript} injectedScript
*/
(function (InjectedScriptHost, inspectedWindow, injectedScriptId, injectedScript) {
var TypeUtils = {
/**
* http://www.khronos.org/registry/typedarray/specs/latest/#7
* @const
* @type {!Array.<function(new:ArrayBufferView, ArrayBufferView)>}
*/
_typedArrayClasses: (function(typeNames) {
var result = [];
for (var i = 0, n = typeNames.length; i < n; ++i) {
if (inspectedWindow[typeNames[i]])
result.push(inspectedWindow[typeNames[i]]);
}
return result;
})(["Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array"]),
/**
* @const
* @type {!Array.<string>}
*/
_supportedPropertyPrefixes: ["webkit"],
/**
* @param {*} array
* @return {function(new:ArrayBufferView, ArrayBufferView)|null}
*/
typedArrayClass: function(array)
{
var classes = TypeUtils._typedArrayClasses;
for (var i = 0, n = classes.length; i < n; ++i) {
if (array instanceof classes[i])
return classes[i];
}
return null;
},
/**
* @param {*} obj
* @return {*}
*/
clone: function(obj)
{
if (!obj)
return obj;
var type = typeof obj;
if (type !== "object" && type !== "function")
return obj;
// Handle Array and ArrayBuffer instances.
if (typeof obj.slice === "function") {
console.assert(obj instanceof Array || obj instanceof ArrayBuffer);
return obj.slice(0);
}
var typedArrayClass = TypeUtils.typedArrayClass(obj);
if (typedArrayClass)
return new typedArrayClass(/** @type {ArrayBufferView} */ (obj));
if (obj instanceof HTMLImageElement) {
var img = /** @type {HTMLImageElement} */ (obj);
// Special case for Images with Blob URIs: cloneNode will fail if the Blob URI has already been revoked.
// FIXME: Maybe this is a bug in WebKit core?
if (/^blob:/.test(img.src))
return TypeUtils.cloneIntoCanvas(img);
return img.cloneNode(true);
}
if (obj instanceof HTMLCanvasElement)
return TypeUtils.cloneIntoCanvas(obj);
if (obj instanceof HTMLVideoElement)
return TypeUtils.cloneIntoCanvas(obj, obj.videoWidth, obj.videoHeight);
if (obj instanceof ImageData) {
var context = TypeUtils._dummyCanvas2dContext();
// FIXME: suppress type checks due to outdated builtin externs for createImageData.
var result = (/** @type {?} */ (context)).createImageData(obj);
for (var i = 0, n = obj.data.length; i < n; ++i)
result.data[i] = obj.data[i];
return result;
}
console.error("ASSERT_NOT_REACHED: failed to clone object: ", obj);
return obj;
},
/**
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} obj
* @param {number=} width
* @param {number=} height
* @return {HTMLCanvasElement}
*/
cloneIntoCanvas: function(obj, width, height)
{
var canvas = /** @type {HTMLCanvasElement} */ (inspectedWindow.document.createElement("canvas"));
canvas.width = width || +obj.width;
canvas.height = height || +obj.height;
var context = /** @type {CanvasRenderingContext2D} */ (Resource.wrappedObject(canvas.getContext("2d")));
context.drawImage(obj, 0, 0);
return canvas;
},
/**
* @param {Object=} obj
* @return {Object}
*/
cloneObject: function(obj)
{
if (!obj)
return null;
var result = {};
for (var key in obj)
result[key] = obj[key];
return result;
},
/**
* @param {!Array.<string>} names
* @return {!Object.<string, boolean>}
*/
createPrefixedPropertyNamesSet: function(names)
{
var result = Object.create(null);
for (var i = 0, name; name = names[i]; ++i) {
result[name] = true;
var suffix = name.substr(0, 1).toUpperCase() + name.substr(1);
for (var j = 0, prefix; prefix = TypeUtils._supportedPropertyPrefixes[j]; ++j)
result[prefix + suffix] = true;
}
return result;
},
/**
* @return {number}
*/
now: function()
{
try {
return inspectedWindow.performance.now();
} catch(e) {
try {
return Date.now();
} catch(ex) {
}
}
return 0;
},
/**
* @param {string} property
* @param {!Object} obj
* @return {boolean}
*/
isEnumPropertyName: function(property, obj)
{
return (/^[A-Z][A-Z0-9_]+$/.test(property) && typeof obj[property] === "number");
},
/**
* @return {CanvasRenderingContext2D}
*/
_dummyCanvas2dContext: function()
{
var context = TypeUtils._dummyCanvas2dContextInstance;
if (!context) {
var canvas = /** @type {HTMLCanvasElement} */ (inspectedWindow.document.createElement("canvas"));
context = /** @type {CanvasRenderingContext2D} */ (Resource.wrappedObject(canvas.getContext("2d")));
TypeUtils._dummyCanvas2dContextInstance = context;
}
return context;
}
}
/** @typedef {{name:string, valueIsEnum:(boolean|undefined), value:*, values:(!Array.<TypeUtils.InternalResourceStateDescriptor>|undefined), isArray:(boolean|undefined)}} */
TypeUtils.InternalResourceStateDescriptor;
/**
* @interface
*/
function StackTrace()
{
}
StackTrace.prototype = {
/**
* @param {number} index
* @return {{sourceURL: string, lineNumber: number, columnNumber: number}|undefined}
*/
callFrame: function(index)
{
}
}
/**
* @param {number=} stackTraceLimit
* @param {Function=} topMostFunctionToIgnore
* @return {StackTrace}
*/
StackTrace.create = function(stackTraceLimit, topMostFunctionToIgnore)
{
if (typeof Error.captureStackTrace === "function")
return new StackTraceV8(stackTraceLimit, topMostFunctionToIgnore || arguments.callee);
// FIXME: Support JSC, and maybe other browsers.
return null;
}
/**
* @constructor
* @implements {StackTrace}
* @param {number=} stackTraceLimit
* @param {Function=} topMostFunctionToIgnore
* @see http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
*/
function StackTraceV8(stackTraceLimit, topMostFunctionToIgnore)
{
StackTrace.call(this);
var oldPrepareStackTrace = Error.prepareStackTrace;
var oldStackTraceLimit = Error.stackTraceLimit;
if (typeof stackTraceLimit === "number")
Error.stackTraceLimit = stackTraceLimit;
/**
* @param {Object} error
* @param {Array.<CallSite>} structuredStackTrace
* @return {Array.<{sourceURL: string, lineNumber: number, columnNumber: number}>}
*/
Error.prepareStackTrace = function(error, structuredStackTrace)
{
return structuredStackTrace.map(function(callSite) {
return {
sourceURL: callSite.getFileName(),
lineNumber: callSite.getLineNumber(),
columnNumber: callSite.getColumnNumber()
};
});
}
var holder = /** @type {{stack: Array.<{sourceURL: string, lineNumber: number, columnNumber: number}>}} */ ({});
Error.captureStackTrace(holder, topMostFunctionToIgnore || arguments.callee);
this._stackTrace = holder.stack;
Error.stackTraceLimit = oldStackTraceLimit;
Error.prepareStackTrace = oldPrepareStackTrace;
}
StackTraceV8.prototype = {
/**
* @override
* @param {number} index
* @return {{sourceURL: string, lineNumber: number, columnNumber: number}|undefined}
*/
callFrame: function(index)
{
return this._stackTrace[index];
},
__proto__: StackTrace.prototype
}
/**
* @constructor
* @template T
*/
function Cache()
{
this.reset();
}
Cache.prototype = {
/**
* @return {number}
*/
size: function()
{
return this._size;
},
reset: function()
{
/** @type {!Object.<number, !T>} */
this._items = Object.create(null);
/** @type {number} */
this._size = 0;
},
/**
* @param {number} key
* @return {boolean}
*/
has: function(key)
{
return key in this._items;
},
/**
* @param {number} key
* @return {T|undefined}
*/
get: function(key)
{
return this._items[key];
},
/**
* @param {number} key
* @param {!T} item
*/
put: function(key, item)
{
if (!this.has(key))
++this._size;
this._items[key] = item;
}
}
/**
* @constructor
* @param {Resource|Object} thisObject
* @param {string} functionName
* @param {Array|Arguments} args
* @param {Resource|*=} result
* @param {StackTrace=} stackTrace
*/
function Call(thisObject, functionName, args, result, stackTrace)
{
this._thisObject = thisObject;
this._functionName = functionName;
this._args = Array.prototype.slice.call(args, 0);
this._result = result;
this._stackTrace = stackTrace || null;
if (!this._functionName)
console.assert(this._args.length === 2 && typeof this._args[0] === "string");
}
Call.prototype = {
/**
* @return {Resource}
*/
resource: function()
{
return Resource.forObject(this._thisObject);
},
/**
* @return {string}
*/
functionName: function()
{
return this._functionName;
},
/**
* @return {boolean}
*/
isPropertySetter: function()
{
return !this._functionName;
},
/**
* @return {!Array}
*/
args: function()
{
return this._args;
},
/**
* @return {*}
*/
result: function()
{
return this._result;
},
/**
* @return {StackTrace}
*/
stackTrace: function()
{
return this._stackTrace;
},
/**
* @param {StackTrace} stackTrace
*/
setStackTrace: function(stackTrace)
{
this._stackTrace = stackTrace;
},
/**
* @param {*} result
*/
setResult: function(result)
{
this._result = result;
},
/**
* @param {string} name
* @param {Object} attachment
*/
setAttachment: function(name, attachment)
{
if (attachment) {
/** @type {Object.<string, Object>} */
this._attachments = this._attachments || Object.create(null);
this._attachments[name] = attachment;
} else if (this._attachments)
delete this._attachments[name];
},
/**
* @param {string} name
* @return {Object}
*/
attachment: function(name)
{
return this._attachments && this._attachments[name];
},
freeze: function()
{
if (this._freezed)
return;
this._freezed = true;
for (var i = 0, n = this._args.length; i < n; ++i) {
// FIXME: freeze the Resources also!
if (!Resource.forObject(this._args[i]))
this._args[i] = TypeUtils.clone(this._args[i]);
}
},
/**
* @param {!Cache.<ReplayableResource>} cache
* @return {!ReplayableCall}
*/
toReplayable: function(cache)
{
this.freeze();
var thisObject = /** @type {ReplayableResource} */ (Resource.toReplayable(this._thisObject, cache));
var result = Resource.toReplayable(this._result, cache);
var args = this._args.map(function(obj) {
return Resource.toReplayable(obj, cache);
});
var attachments = TypeUtils.cloneObject(this._attachments);
return new ReplayableCall(thisObject, this._functionName, args, result, this._stackTrace, attachments);
},
/**
* @param {!ReplayableCall} replayableCall
* @param {!Cache.<Resource>} cache
* @return {!Call}
*/
replay: function(replayableCall, cache)
{
var replayableResult = replayableCall.result();
if (replayableResult instanceof ReplayableResource && !cache.has(replayableResult.id())) {
var resource = replayableResult.replay(cache);
console.assert(resource.calls().length > 0, "Expected create* call for the Resource");
return resource.calls()[0];
}
var replayObject = ReplayableResource.replay(replayableCall.replayableResource(), cache);
var replayArgs = replayableCall.args().map(function(obj) {
return ReplayableResource.replay(obj, cache);
});
var replayResult = undefined;
if (replayableCall.isPropertySetter())
replayObject[replayArgs[0]] = replayArgs[1];
else {
var replayFunction = replayObject[replayableCall.functionName()];
console.assert(typeof replayFunction === "function", "Expected a function to replay");
replayResult = replayFunction.apply(replayObject, replayArgs);
if (replayableResult instanceof ReplayableResource) {
var resource = replayableResult.replay(cache);
if (!resource.wrappedObject())
resource.setWrappedObject(replayResult);
}
}
this._thisObject = replayObject;
this._functionName = replayableCall.functionName();
this._args = replayArgs;
this._result = replayResult;
this._stackTrace = replayableCall.stackTrace();
this._freezed = true;
var attachments = replayableCall.attachments();
if (attachments)
this._attachments = TypeUtils.cloneObject(attachments);
var thisResource = Resource.forObject(replayObject);
if (thisResource)
thisResource.onCallReplayed(this);
return this;
}
}
/**
* @constructor
* @param {ReplayableResource} thisObject
* @param {string} functionName
* @param {Array.<ReplayableResource|*>} args
* @param {ReplayableResource|*} result
* @param {StackTrace} stackTrace
* @param {Object.<string, Object>} attachments
*/
function ReplayableCall(thisObject, functionName, args, result, stackTrace, attachments)
{
this._thisObject = thisObject;
this._functionName = functionName;
this._args = args;
this._result = result;
this._stackTrace = stackTrace;
if (attachments)
this._attachments = attachments;
}
ReplayableCall.prototype = {
/**
* @return {ReplayableResource}
*/
replayableResource: function()
{
return this._thisObject;
},
/**
* @return {string}
*/
functionName: function()
{
return this._functionName;
},
/**
* @return {boolean}
*/
isPropertySetter: function()
{
return !this._functionName;
},
/**
* @return {string}
*/
propertyName: function()
{
console.assert(this.isPropertySetter());
return /** @type {string} */ (this._args[0]);
},
/**
* @return {*}
*/
propertyValue: function()
{
console.assert(this.isPropertySetter());
return this._args[1];
},
/**
* @return {Array.<ReplayableResource|*>}
*/
args: function()
{
return this._args;
},
/**
* @return {ReplayableResource|*}
*/
result: function()
{
return this._result;
},
/**
* @return {StackTrace}
*/
stackTrace: function()
{
return this._stackTrace;
},
/**
* @return {Object.<string, Object>}
*/
attachments: function()
{
return this._attachments;
},
/**
* @param {string} name
* @return {Object}
*/
attachment: function(name)
{
return this._attachments && this._attachments[name];
},
/**
* @param {!Cache.<Resource>} cache
* @return {!Call}
*/
replay: function(cache)
{
var call = /** @type {!Call} */ (Object.create(Call.prototype));
return call.replay(this, cache);
}
}
/**
* @constructor
* @param {!Object} wrappedObject
* @param {string} name
*/
function Resource(wrappedObject, name)
{
/** @type {number} */
this._id = ++Resource._uniqueId;
/** @type {string} */
this._name = name || "Resource";
/** @type {number} */
this._kindId = Resource._uniqueKindIds[this._name] = (Resource._uniqueKindIds[this._name] || 0) + 1;
/** @type {ResourceTrackingManager} */
this._resourceManager = null;
/** @type {!Array.<Call>} */
this._calls = [];
/**
* This is to prevent GC from collecting associated resources.
* Otherwise, for example in WebGL, subsequent calls to gl.getParameter()
* may return a recently created instance that is no longer bound to a
* Resource object (thus, no history to replay it later).
*
* @type {!Object.<string, Resource>}
*/
this._boundResources = Object.create(null);
this.setWrappedObject(wrappedObject);
}
/**
* @type {number}
*/
Resource._uniqueId = 0;
/**
* @type {!Object.<string, number>}
*/
Resource._uniqueKindIds = {};
/**
* @param {*} obj
* @return {Resource}
*/
Resource.forObject = function(obj)
{
if (!obj)
return null;
if (obj instanceof Resource)
return obj;
if (typeof obj === "object")
return obj["__resourceObject"];
return null;
}
/**
* @param {Resource|*} obj
* @return {*}
*/
Resource.wrappedObject = function(obj)
{
var resource = Resource.forObject(obj);
return resource ? resource.wrappedObject() : obj;
}
/**
* @param {Resource|*} obj
* @param {!Cache.<ReplayableResource>} cache
* @return {ReplayableResource|*}
*/
Resource.toReplayable = function(obj, cache)
{
var resource = Resource.forObject(obj);
return resource ? resource.toReplayable(cache) : obj;
}
Resource.prototype = {
/**
* @return {number}
*/
id: function()
{
return this._id;
},
/**
* @return {string}
*/
name: function()
{
return this._name;
},
/**
* @return {string}
*/
description: function()
{
return this._name + "@" + this._kindId;
},
/**
* @return {Object}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @param {!Object} value
*/
setWrappedObject: function(value)
{
console.assert(value, "wrappedObject should not be NULL");
console.assert(!(value instanceof Resource), "Binding a Resource object to another Resource object?");
this._wrappedObject = value;
this._bindObjectToResource(value);
},
/**
* @return {Object}
*/
proxyObject: function()
{
if (!this._proxyObject)
this._proxyObject = this._wrapObject();
return this._proxyObject;
},
/**
* @return {ResourceTrackingManager}
*/
manager: function()
{
return this._resourceManager;
},
/**
* @param {ResourceTrackingManager} value
*/
setManager: function(value)
{
this._resourceManager = value;
},
/**
* @return {!Array.<!Call>}
*/
calls: function()
{
return this._calls;
},
/**
* @return {ContextResource}
*/
contextResource: function()
{
if (this instanceof ContextResource)
return /** @type {ContextResource} */ (this);
if (this._calculatingContextResource)
return null;
this._calculatingContextResource = true;
var result = null;
for (var i = 0, n = this._calls.length; i < n; ++i) {
result = this._calls[i].resource().contextResource();
if (result)
break;
}
delete this._calculatingContextResource;
console.assert(result, "Failed to find context resource for " + this._name + "@" + this._kindId);
return result;
},
/**
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var proxyObject = this.proxyObject();
if (!proxyObject)
return result;
var statePropertyNames = this._proxyStatePropertyNames || [];
for (var i = 0, n = statePropertyNames.length; i < n; ++i) {
var pname = statePropertyNames[i];
result.push({ name: pname, value: proxyObject[pname] });
}
result.push({ name: "context", value: this.contextResource() });
return result;
},
/**
* @return {string}
*/
toDataURL: function()
{
return "";
},
/**
* @param {!Cache.<ReplayableResource>} cache
* @return {!ReplayableResource}
*/
toReplayable: function(cache)
{
var result = cache.get(this._id);
if (result)
return result;
var data = {
id: this._id,
name: this._name,
kindId: this._kindId
};
result = new ReplayableResource(this, data);
cache.put(this._id, result); // Put into the cache early to avoid loops.
data.calls = this._calls.map(function(call) {
return call.toReplayable(cache);
});
this._populateReplayableData(data, cache);
var contextResource = this.contextResource();
if (contextResource !== this)
data.contextResource = Resource.toReplayable(contextResource, cache);
return result;
},
/**
* @param {!Object} data
* @param {!Cache.<ReplayableResource>} cache
*/
_populateReplayableData: function(data, cache)
{
// Do nothing. Should be overridden by subclasses.
},
/**
* @param {!Object} data
* @param {!Cache.<Resource>} cache
* @return {!Resource}
*/
replay: function(data, cache)
{
var resource = cache.get(data.id);
if (resource)
return resource;
this._id = data.id;
this._name = data.name;
this._kindId = data.kindId;
this._resourceManager = null;
this._calls = [];
this._boundResources = Object.create(null);
this._wrappedObject = null;
cache.put(data.id, this); // Put into the cache early to avoid loops.
this._doReplayCalls(data, cache);
console.assert(this._wrappedObject, "Resource should be reconstructed!");
return this;
},
/**
* @param {!Object} data
* @param {!Cache.<Resource>} cache
*/
_doReplayCalls: function(data, cache)
{
for (var i = 0, n = data.calls.length; i < n; ++i)
this._calls.push(data.calls[i].replay(cache));
},
/**
* @param {!Call} call
*/
pushCall: function(call)
{
call.freeze();
this._calls.push(call);
},
/**
* @param {!Call} call
*/
onCallReplayed: function(call)
{
// Ignore by default.
},
/**
* @param {!Object} object
*/
_bindObjectToResource: function(object)
{
Object.defineProperty(object, "__resourceObject", {
value: this,
writable: false,
enumerable: false,
configurable: true
});
},
/**
* @param {string} key
* @param {*} obj
*/
_registerBoundResource: function(key, obj)
{
var resource = Resource.forObject(obj);
if (resource)
this._boundResources[key] = resource;
else
delete this._boundResources[key];
},
/**
* @return {Object}
*/
_wrapObject: function()
{
var wrappedObject = this.wrappedObject();
if (!wrappedObject)
return null;
var proxy = Object.create(wrappedObject.__proto__); // In order to emulate "instanceof".
var customWrapFunctions = this._customWrapFunctions();
/** @type {Array.<string>} */
this._proxyStatePropertyNames = [];
/**
* @param {string} property
*/
function processProperty(property)
{
if (typeof wrappedObject[property] === "function") {
var customWrapFunction = customWrapFunctions[property];
if (customWrapFunction)
proxy[property] = this._wrapCustomFunction(this, wrappedObject, wrappedObject[property], property, customWrapFunction);
else
proxy[property] = this._wrapFunction(this, wrappedObject, wrappedObject[property], property);
} else if (TypeUtils.isEnumPropertyName(property, wrappedObject)) {
// Fast access to enums and constants.
proxy[property] = wrappedObject[property];
} else {
this._proxyStatePropertyNames.push(property);
Object.defineProperty(proxy, property, {
get: function()
{
var obj = wrappedObject[property];
var resource = Resource.forObject(obj);
return resource ? resource : obj;
},
set: this._wrapPropertySetter(this, wrappedObject, property),
enumerable: true
});
}
}
var isEmpty = true;
for (var property in wrappedObject) {
isEmpty = false;
processProperty.call(this, property);
}
if (isEmpty)
return wrappedObject; // Nothing to proxy.
this._bindObjectToResource(proxy);
return proxy;
},
/**
* @param {!Resource} resource
* @param {!Object} originalObject
* @param {!Function} originalFunction
* @param {string} functionName
* @param {!Function} customWrapFunction
* @return {!Function}
*/
_wrapCustomFunction: function(resource, originalObject, originalFunction, functionName, customWrapFunction)
{
return function()
{
var manager = resource.manager();
var isCapturing = manager && manager.capturing();
if (isCapturing)
manager.captureArguments(resource, arguments);
var wrapFunction = new Resource.WrapFunction(originalObject, originalFunction, functionName, arguments);
customWrapFunction.apply(wrapFunction, arguments);
if (isCapturing) {
var call = wrapFunction.call();
call.setStackTrace(StackTrace.create(1, arguments.callee));
manager.captureCall(call);
}
return wrapFunction.result();
};
},
/**
* @param {!Resource} resource
* @param {!Object} originalObject
* @param {!Function} originalFunction
* @param {string} functionName
* @return {!Function}
*/
_wrapFunction: function(resource, originalObject, originalFunction, functionName)
{
return function()
{
var manager = resource.manager();
if (!manager || !manager.capturing())
return originalFunction.apply(originalObject, arguments);
manager.captureArguments(resource, arguments);
var result = originalFunction.apply(originalObject, arguments);
var stackTrace = StackTrace.create(1, arguments.callee);
var call = new Call(resource, functionName, arguments, result, stackTrace);
manager.captureCall(call);
return result;
};
},
/**
* @param {!Resource} resource
* @param {!Object} originalObject
* @param {string} propertyName
* @return {function(*)}
*/
_wrapPropertySetter: function(resource, originalObject, propertyName)
{
return function(value)
{
resource._registerBoundResource(propertyName, value);
var manager = resource.manager();
if (!manager || !manager.capturing()) {
originalObject[propertyName] = Resource.wrappedObject(value);
return;
}
var args = [propertyName, value];
manager.captureArguments(resource, args);
originalObject[propertyName] = Resource.wrappedObject(value);
var stackTrace = StackTrace.create(1, arguments.callee);
var call = new Call(resource, "", args, undefined, stackTrace);
manager.captureCall(call);
};
},
/**
* @return {!Object.<string, Function>}
*/
_customWrapFunctions: function()
{
return Object.create(null); // May be overridden by subclasses.
}
}
/**
* @constructor
* @param {Object} originalObject
* @param {Function} originalFunction
* @param {string} functionName
* @param {Array|Arguments} args
*/
Resource.WrapFunction = function(originalObject, originalFunction, functionName, args)
{
this._originalObject = originalObject;
this._originalFunction = originalFunction;
this._functionName = functionName;
this._args = args;
this._resource = Resource.forObject(originalObject);
console.assert(this._resource, "Expected a wrapped call on a Resource object.");
}
Resource.WrapFunction.prototype = {
/**
* @return {*}
*/
result: function()
{
if (!this._executed) {
this._executed = true;
this._result = this._originalFunction.apply(this._originalObject, this._args);
}
return this._result;
},
/**
* @return {!Call}
*/
call: function()
{
if (!this._call)
this._call = new Call(this._resource, this._functionName, this._args, this.result());
return this._call;
},
/**
* @param {*} result
*/
overrideResult: function(result)
{
var call = this.call();
call.setResult(result);
this._result = result;
}
}
/**
* @param {function(new:Resource, !Object, string)} resourceConstructor
* @param {string} resourceName
* @return {function(this:Resource.WrapFunction)}
*/
Resource.WrapFunction.resourceFactoryMethod = function(resourceConstructor, resourceName)
{
/** @this Resource.WrapFunction */
return function()
{
var wrappedObject = /** @type {Object} */ (this.result());
if (!wrappedObject)
return;
var resource = new resourceConstructor(wrappedObject, resourceName);
var manager = this._resource.manager();
if (manager)
manager.registerResource(resource);
this.overrideResult(resource.proxyObject());
resource.pushCall(this.call());
}
}
/**
* @constructor
* @param {!Resource} originalResource
* @param {!Object} data
*/
function ReplayableResource(originalResource, data)
{
this._proto = originalResource.__proto__;
this._data = data;
}
ReplayableResource.prototype = {
/**
* @return {number}
*/
id: function()
{
return this._data.id;
},
/**
* @return {string}
*/
name: function()
{
return this._data.name;
},
/**
* @return {string}
*/
description: function()
{
return this._data.name + "@" + this._data.kindId;
},
/**
* @return {!ReplayableResource}
*/
contextResource: function()
{
return this._data.contextResource || this;
},
/**
* @param {!Cache.<Resource>} cache
* @return {!Resource}
*/
replay: function(cache)
{
var result = /** @type {!Resource} */ (Object.create(this._proto));
result = result.replay(this._data, cache)
console.assert(result.__proto__ === this._proto, "Wrong type of a replay result");
return result;
}
}
/**
* @param {ReplayableResource|*} obj
* @param {!Cache.<Resource>} cache
* @return {*}
*/
ReplayableResource.replay = function(obj, cache)
{
return (obj instanceof ReplayableResource) ? obj.replay(cache).wrappedObject() : obj;
}
/**
* @constructor
* @extends {Resource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function ContextResource(wrappedObject, name)
{
Resource.call(this, wrappedObject, name);
}
ContextResource.prototype = {
__proto__: Resource.prototype
}
/**
* @constructor
* @extends {Resource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function LogEverythingResource(wrappedObject, name)
{
Resource.call(this, wrappedObject, name);
}
LogEverythingResource.prototype = {
/**
* @override
* @return {!Object.<string, Function>}
*/
_customWrapFunctions: function()
{
var wrapFunctions = Object.create(null);
var wrappedObject = this.wrappedObject();
if (wrappedObject) {
for (var property in wrappedObject) {
/** @this Resource.WrapFunction */
wrapFunctions[property] = function()
{
this._resource.pushCall(this.call());
}
}
}
return wrapFunctions;
},
__proto__: Resource.prototype
}
////////////////////////////////////////////////////////////////////////////////
// WebGL
////////////////////////////////////////////////////////////////////////////////
/**
* @constructor
* @extends {Resource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLBoundResource(wrappedObject, name)
{
Resource.call(this, wrappedObject, name);
/** @type {!Object.<string, *>} */
this._state = {};
}
WebGLBoundResource.prototype = {
/**
* @override
* @param {!Object} data
* @param {!Cache.<ReplayableResource>} cache
*/
_populateReplayableData: function(data, cache)
{
var state = this._state;
data.state = {};
Object.keys(state).forEach(function(parameter) {
data.state[parameter] = Resource.toReplayable(state[parameter], cache);
});
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<Resource>} cache
*/
_doReplayCalls: function(data, cache)
{
var gl = this._replayContextResource(data, cache).wrappedObject();
/** @type {!Object.<string, Array.<string>>} */
var bindingsData = {
TEXTURE_2D: ["bindTexture", "TEXTURE_BINDING_2D"],
TEXTURE_CUBE_MAP: ["bindTexture", "TEXTURE_BINDING_CUBE_MAP"],
ARRAY_BUFFER: ["bindBuffer", "ARRAY_BUFFER_BINDING"],
ELEMENT_ARRAY_BUFFER: ["bindBuffer", "ELEMENT_ARRAY_BUFFER_BINDING"],
FRAMEBUFFER: ["bindFramebuffer", "FRAMEBUFFER_BINDING"],
RENDERBUFFER: ["bindRenderbuffer", "RENDERBUFFER_BINDING"]
};
var originalBindings = {};
Object.keys(bindingsData).forEach(function(bindingTarget) {
var bindingParameter = bindingsData[bindingTarget][1];
originalBindings[bindingTarget] = gl.getParameter(gl[bindingParameter]);
});
var state = {};
Object.keys(data.state).forEach(function(parameter) {
state[parameter] = ReplayableResource.replay(data.state[parameter], cache);
});
this._state = state;
Resource.prototype._doReplayCalls.call(this, data, cache);
Object.keys(bindingsData).forEach(function(bindingTarget) {
var bindMethodName = bindingsData[bindingTarget][0];
gl[bindMethodName].call(gl, gl[bindingTarget], originalBindings[bindingTarget]);
});
},
/**
* @param {!Object} data
* @param {!Cache.<Resource>} cache
* @return {WebGLRenderingContextResource}
*/
_replayContextResource: function(data, cache)
{
var calls = /** @type {!Array.<ReplayableCall>} */ (data.calls);
for (var i = 0, n = calls.length; i < n; ++i) {
var resource = ReplayableResource.replay(calls[i].replayableResource(), cache);
var contextResource = WebGLRenderingContextResource.forObject(resource);
if (contextResource)
return contextResource;
}
return null;
},
/**
* @param {number} target
* @param {string} bindMethodName
*/
pushBinding: function(target, bindMethodName)
{
if (this._state.bindTarget !== target) {
this._state.bindTarget = target;
this.pushCall(new Call(WebGLRenderingContextResource.forObject(this), bindMethodName, [target, this]));
}
},
__proto__: Resource.prototype
}
/**
* @constructor
* @extends {WebGLBoundResource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLTextureResource(wrappedObject, name)
{
WebGLBoundResource.call(this, wrappedObject, name);
}
WebGLTextureResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLTexture}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var glResource = WebGLRenderingContextResource.forObject(this);
var gl = glResource.wrappedObject();
var texture = this.wrappedObject();
if (!gl || !texture)
return result;
result.push({ name: "isTexture", value: gl.isTexture(texture) });
result.push({ name: "context", value: this.contextResource() });
var target = this._state.bindTarget;
if (typeof target !== "number")
return result;
var bindingParameter;
switch (target) {
case gl.TEXTURE_2D:
bindingParameter = gl.TEXTURE_BINDING_2D;
break;
case gl.TEXTURE_CUBE_MAP:
bindingParameter = gl.TEXTURE_BINDING_CUBE_MAP;
break;
default:
console.error("ASSERT_NOT_REACHED: unknown texture target " + target);
return result;
}
result.push({ name: "target", value: target, valueIsEnum: true });
var oldTexture = /** @type {WebGLTexture} */ (gl.getParameter(bindingParameter));
if (oldTexture !== texture)
gl.bindTexture(target, texture);
var textureParameters = [
"TEXTURE_MAG_FILTER",
"TEXTURE_MIN_FILTER",
"TEXTURE_WRAP_S",
"TEXTURE_WRAP_T",
"TEXTURE_MAX_ANISOTROPY_EXT" // EXT_texture_filter_anisotropic extension
];
glResource.queryStateValues(gl.getTexParameter, target, textureParameters, result);
if (oldTexture !== texture)
gl.bindTexture(target, oldTexture);
return result;
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<Resource>} cache
*/
_doReplayCalls: function(data, cache)
{
var gl = this._replayContextResource(data, cache).wrappedObject();
var state = {};
WebGLRenderingContextResource.PixelStoreParameters.forEach(function(parameter) {
state[parameter] = gl.getParameter(gl[parameter]);
});
WebGLBoundResource.prototype._doReplayCalls.call(this, data, cache);
WebGLRenderingContextResource.PixelStoreParameters.forEach(function(parameter) {
gl.pixelStorei(gl[parameter], state[parameter]);
});
},
/**
* @override
* @param {!Call} call
*/
pushCall: function(call)
{
var gl = WebGLRenderingContextResource.forObject(call.resource()).wrappedObject();
WebGLRenderingContextResource.PixelStoreParameters.forEach(function(parameter) {
var value = gl.getParameter(gl[parameter]);
if (this._state[parameter] !== value) {
this._state[parameter] = value;
var pixelStoreCall = new Call(gl, "pixelStorei", [gl[parameter], value]);
WebGLBoundResource.prototype.pushCall.call(this, pixelStoreCall);
}
}, this);
// FIXME: remove any older calls that no longer contribute to the resource state.
// FIXME: optimize memory usage: maybe it's more efficient to store one texImage2D call instead of many texSubImage2D.
WebGLBoundResource.prototype.pushCall.call(this, call);
},
/**
* Handles: texParameteri, texParameterf
* @param {!Call} call
*/
pushCall_texParameter: function(call)
{
var args = call.args();
var pname = args[1];
var param = args[2];
if (this._state[pname] !== param) {
this._state[pname] = param;
WebGLBoundResource.prototype.pushCall.call(this, call);
}
},
/**
* Handles: copyTexImage2D, copyTexSubImage2D
* copyTexImage2D and copyTexSubImage2D define a texture image with pixels from the current framebuffer.
* @param {!Call} call
*/
pushCall_copyTexImage2D: function(call)
{
var glResource = WebGLRenderingContextResource.forObject(call.resource());
var gl = glResource.wrappedObject();
var framebufferResource = /** @type {WebGLFramebufferResource} */ (glResource.currentBinding(gl.FRAMEBUFFER));
if (framebufferResource)
this.pushCall(new Call(glResource, "bindFramebuffer", [gl.FRAMEBUFFER, framebufferResource]));
else {
// FIXME: Implement this case.
console.error("ASSERT_NOT_REACHED: Could not properly process a gl." + call.functionName() + " call while the DRAWING BUFFER is bound.");
}
this.pushCall(call);
},
__proto__: WebGLBoundResource.prototype
}
/**
* @constructor
* @extends {Resource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLProgramResource(wrappedObject, name)
{
Resource.call(this, wrappedObject, name);
}
WebGLProgramResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLProgram}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
/**
* @param {!Object} obj
* @param {!Array.<TypeUtils.InternalResourceStateDescriptor>} output
*/
function convertToStateDescriptors(obj, output)
{
for (var pname in obj)
output.push({ name: pname, value: obj[pname], valueIsEnum: (pname === "type") });
}
var result = [];
var program = this.wrappedObject();
if (!program)
return result;
var glResource = WebGLRenderingContextResource.forObject(this);
var gl = glResource.wrappedObject();
var programParameters = ["DELETE_STATUS", "LINK_STATUS", "VALIDATE_STATUS"];
glResource.queryStateValues(gl.getProgramParameter, program, programParameters, result);
result.push({ name: "getProgramInfoLog", value: gl.getProgramInfoLog(program) });
result.push({ name: "isProgram", value: gl.isProgram(program) });
result.push({ name: "context", value: this.contextResource() });
// ATTACHED_SHADERS
var callFormatter = CallFormatter.forResource(this);
var shaders = gl.getAttachedShaders(program) || [];
var shaderDescriptors = [];
for (var i = 0, n = shaders.length; i < n; ++i) {
var shaderResource = Resource.forObject(shaders[i]);
var pname = callFormatter.enumNameForValue(shaderResource.type());
shaderDescriptors.push({ name: pname, value: shaderResource });
}
result.push({ name: "ATTACHED_SHADERS", values: shaderDescriptors, isArray: true });
// ACTIVE_UNIFORMS
var uniformDescriptors = [];
var uniforms = this._activeUniforms(true);
for (var i = 0, n = uniforms.length; i < n; ++i) {
var pname = "" + i;
var values = [];
convertToStateDescriptors(uniforms[i], values);
uniformDescriptors.push({ name: pname, values: values });
}
result.push({ name: "ACTIVE_UNIFORMS", values: uniformDescriptors, isArray: true });
// ACTIVE_ATTRIBUTES
var attributesCount = /** @type {number} */ (gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES));
var attributeDescriptors = [];
for (var i = 0; i < attributesCount; ++i) {
var activeInfo = gl.getActiveAttrib(program, i);
if (!activeInfo)
continue;
var pname = "" + i;
var values = [];
convertToStateDescriptors(activeInfo, values);
attributeDescriptors.push({ name: pname, values: values });
}
result.push({ name: "ACTIVE_ATTRIBUTES", values: attributeDescriptors, isArray: true });
return result;
},
/**
* @param {boolean=} includeAllInfo
* @return {!Array.<{name:string, type:number, value:*, size:(number|undefined)}>}
*/
_activeUniforms: function(includeAllInfo)
{
var uniforms = [];
var program = this.wrappedObject();
if (!program)
return uniforms;
var gl = WebGLRenderingContextResource.forObject(this).wrappedObject();
var uniformsCount = /** @type {number} */ (gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS));
for (var i = 0; i < uniformsCount; ++i) {
var activeInfo = gl.getActiveUniform(program, i);
if (!activeInfo)
continue;
var uniformLocation = gl.getUniformLocation(program, activeInfo.name);
if (!uniformLocation)
continue;
var value = gl.getUniform(program, uniformLocation);
var item = Object.create(null);
item.name = activeInfo.name;
item.type = activeInfo.type;
item.value = value;
if (includeAllInfo)
item.size = activeInfo.size;
uniforms.push(item);
}
return uniforms;
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<ReplayableResource>} cache
*/
_populateReplayableData: function(data, cache)
{
var glResource = WebGLRenderingContextResource.forObject(this);
var originalErrors = glResource.getAllErrors();
data.uniforms = this._activeUniforms();
glResource.restoreErrors(originalErrors);
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<Resource>} cache
*/
_doReplayCalls: function(data, cache)
{
Resource.prototype._doReplayCalls.call(this, data, cache);
var gl = WebGLRenderingContextResource.forObject(this).wrappedObject();
var program = this.wrappedObject();
var originalProgram = /** @type {WebGLProgram} */ (gl.getParameter(gl.CURRENT_PROGRAM));
var currentProgram = originalProgram;
data.uniforms.forEach(function(uniform) {
var uniformLocation = gl.getUniformLocation(program, uniform.name);
if (!uniformLocation)
return;
if (currentProgram !== program) {
currentProgram = program;
gl.useProgram(program);
}
var methodName = this._uniformMethodNameByType(gl, uniform.type);
if (methodName.indexOf("Matrix") === -1)
gl[methodName].call(gl, uniformLocation, uniform.value);
else
gl[methodName].call(gl, uniformLocation, false, uniform.value);
}.bind(this));
if (currentProgram !== originalProgram)
gl.useProgram(originalProgram);
},
/**
* @param {WebGLRenderingContext} gl
* @param {number} type
* @return {string}
*/
_uniformMethodNameByType: function(gl, type)
{
var uniformMethodNames = WebGLProgramResource._uniformMethodNames;
if (!uniformMethodNames) {
uniformMethodNames = {};
uniformMethodNames[gl.FLOAT] = "uniform1f";
uniformMethodNames[gl.FLOAT_VEC2] = "uniform2fv";
uniformMethodNames[gl.FLOAT_VEC3] = "uniform3fv";
uniformMethodNames[gl.FLOAT_VEC4] = "uniform4fv";
uniformMethodNames[gl.INT] = "uniform1i";
uniformMethodNames[gl.BOOL] = "uniform1i";
uniformMethodNames[gl.SAMPLER_2D] = "uniform1i";
uniformMethodNames[gl.SAMPLER_CUBE] = "uniform1i";
uniformMethodNames[gl.INT_VEC2] = "uniform2iv";
uniformMethodNames[gl.BOOL_VEC2] = "uniform2iv";
uniformMethodNames[gl.INT_VEC3] = "uniform3iv";
uniformMethodNames[gl.BOOL_VEC3] = "uniform3iv";
uniformMethodNames[gl.INT_VEC4] = "uniform4iv";
uniformMethodNames[gl.BOOL_VEC4] = "uniform4iv";
uniformMethodNames[gl.FLOAT_MAT2] = "uniformMatrix2fv";
uniformMethodNames[gl.FLOAT_MAT3] = "uniformMatrix3fv";
uniformMethodNames[gl.FLOAT_MAT4] = "uniformMatrix4fv";
WebGLProgramResource._uniformMethodNames = uniformMethodNames;
}
console.assert(uniformMethodNames[type], "Unknown uniform type " + type);
return uniformMethodNames[type];
},
/**
* @override
* @param {!Call} call
*/
pushCall: function(call)
{
// FIXME: remove any older calls that no longer contribute to the resource state.
// FIXME: handle multiple attachShader && detachShader.
Resource.prototype.pushCall.call(this, call);
},
__proto__: Resource.prototype
}
/**
* @constructor
* @extends {Resource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLShaderResource(wrappedObject, name)
{
Resource.call(this, wrappedObject, name);
}
WebGLShaderResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLShader}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @return {number}
*/
type: function()
{
var call = this._calls[0];
if (call && call.functionName() === "createShader")
return call.args()[0];
console.error("ASSERT_NOT_REACHED: Failed to restore shader type from the log.", call);
return 0;
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var shader = this.wrappedObject();
if (!shader)
return result;
var glResource = WebGLRenderingContextResource.forObject(this);
var gl = glResource.wrappedObject();
var shaderParameters = ["SHADER_TYPE", "DELETE_STATUS", "COMPILE_STATUS"];
glResource.queryStateValues(gl.getShaderParameter, shader, shaderParameters, result);
result.push({ name: "getShaderInfoLog", value: gl.getShaderInfoLog(shader) });
result.push({ name: "getShaderSource", value: gl.getShaderSource(shader) });
result.push({ name: "isShader", value: gl.isShader(shader) });
result.push({ name: "context", value: this.contextResource() });
// getShaderPrecisionFormat
var shaderType = this.type();
var precisionValues = [];
var precisionParameters = ["LOW_FLOAT", "MEDIUM_FLOAT", "HIGH_FLOAT", "LOW_INT", "MEDIUM_INT", "HIGH_INT"];
for (var i = 0, pname; pname = precisionParameters[i]; ++i)
precisionValues.push({ name: pname, value: gl.getShaderPrecisionFormat(shaderType, gl[pname]) });
result.push({ name: "getShaderPrecisionFormat", values: precisionValues });
return result;
},
/**
* @override
* @param {!Call} call
*/
pushCall: function(call)
{
// FIXME: remove any older calls that no longer contribute to the resource state.
// FIXME: handle multiple shaderSource calls.
Resource.prototype.pushCall.call(this, call);
},
__proto__: Resource.prototype
}
/**
* @constructor
* @extends {WebGLBoundResource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLBufferResource(wrappedObject, name)
{
WebGLBoundResource.call(this, wrappedObject, name);
}
WebGLBufferResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLBuffer}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var glResource = WebGLRenderingContextResource.forObject(this);
var gl = glResource.wrappedObject();
var buffer = this.wrappedObject();
if (!gl || !buffer)
return result;
result.push({ name: "isBuffer", value: gl.isBuffer(buffer) });
result.push({ name: "context", value: this.contextResource() });
var target = this._state.bindTarget;
if (typeof target !== "number")
return result;
var bindingParameter;
switch (target) {
case gl.ARRAY_BUFFER:
bindingParameter = gl.ARRAY_BUFFER_BINDING;
break;
case gl.ELEMENT_ARRAY_BUFFER:
bindingParameter = gl.ELEMENT_ARRAY_BUFFER_BINDING;
break;
default:
console.error("ASSERT_NOT_REACHED: unknown buffer target " + target);
return result;
}
result.push({ name: "target", value: target, valueIsEnum: true });
var oldBuffer = /** @type {WebGLBuffer} */ (gl.getParameter(bindingParameter));
if (oldBuffer !== buffer)
gl.bindBuffer(target, buffer);
var bufferParameters = ["BUFFER_SIZE", "BUFFER_USAGE"];
glResource.queryStateValues(gl.getBufferParameter, target, bufferParameters, result);
if (oldBuffer !== buffer)
gl.bindBuffer(target, oldBuffer);
return result;
},
/**
* @override
* @param {!Call} call
*/
pushCall: function(call)
{
// FIXME: remove any older calls that no longer contribute to the resource state.
// FIXME: Optimize memory for bufferSubData.
WebGLBoundResource.prototype.pushCall.call(this, call);
},
__proto__: WebGLBoundResource.prototype
}
/**
* @constructor
* @extends {WebGLBoundResource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLFramebufferResource(wrappedObject, name)
{
WebGLBoundResource.call(this, wrappedObject, name);
}
WebGLFramebufferResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLFramebuffer}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var framebuffer = this.wrappedObject();
if (!framebuffer)
return result;
var gl = WebGLRenderingContextResource.forObject(this).wrappedObject();
var oldFramebuffer = /** @type {WebGLFramebuffer} */ (gl.getParameter(gl.FRAMEBUFFER_BINDING));
if (oldFramebuffer !== framebuffer)
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
var attachmentParameters = ["COLOR_ATTACHMENT0", "DEPTH_ATTACHMENT", "STENCIL_ATTACHMENT"];
var framebufferParameters = ["FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE", "FRAMEBUFFER_ATTACHMENT_OBJECT_NAME", "FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL", "FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE"];
for (var i = 0, attachment; attachment = attachmentParameters[i]; ++i) {
var values = [];
for (var j = 0, pname; pname = framebufferParameters[j]; ++j) {
var value = gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, gl[attachment], gl[pname]);
value = Resource.forObject(value) || value;
values.push({ name: pname, value: value, valueIsEnum: WebGLRenderingContextResource.GetResultIsEnum[pname] });
}
result.push({ name: attachment, values: values });
}
result.push({ name: "isFramebuffer", value: gl.isFramebuffer(framebuffer) });
result.push({ name: "context", value: this.contextResource() });
if (oldFramebuffer !== framebuffer)
gl.bindFramebuffer(gl.FRAMEBUFFER, oldFramebuffer);
return result;
},
/**
* @override
* @param {!Call} call
*/
pushCall: function(call)
{
// FIXME: remove any older calls that no longer contribute to the resource state.
WebGLBoundResource.prototype.pushCall.call(this, call);
},
__proto__: WebGLBoundResource.prototype
}
/**
* @constructor
* @extends {WebGLBoundResource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLRenderbufferResource(wrappedObject, name)
{
WebGLBoundResource.call(this, wrappedObject, name);
}
WebGLRenderbufferResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLRenderbuffer}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var renderbuffer = this.wrappedObject();
if (!renderbuffer)
return result;
var glResource = WebGLRenderingContextResource.forObject(this);
var gl = glResource.wrappedObject();
var oldRenderbuffer = /** @type {WebGLRenderbuffer} */ (gl.getParameter(gl.RENDERBUFFER_BINDING));
if (oldRenderbuffer !== renderbuffer)
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
var renderbufferParameters = ["RENDERBUFFER_WIDTH", "RENDERBUFFER_HEIGHT", "RENDERBUFFER_INTERNAL_FORMAT", "RENDERBUFFER_RED_SIZE", "RENDERBUFFER_GREEN_SIZE", "RENDERBUFFER_BLUE_SIZE", "RENDERBUFFER_ALPHA_SIZE", "RENDERBUFFER_DEPTH_SIZE", "RENDERBUFFER_STENCIL_SIZE"];
glResource.queryStateValues(gl.getRenderbufferParameter, gl.RENDERBUFFER, renderbufferParameters, result);
result.push({ name: "isRenderbuffer", value: gl.isRenderbuffer(renderbuffer) });
result.push({ name: "context", value: this.contextResource() });
if (oldRenderbuffer !== renderbuffer)
gl.bindRenderbuffer(gl.RENDERBUFFER, oldRenderbuffer);
return result;
},
/**
* @override
* @param {!Call} call
*/
pushCall: function(call)
{
// FIXME: remove any older calls that no longer contribute to the resource state.
WebGLBoundResource.prototype.pushCall.call(this, call);
},
__proto__: WebGLBoundResource.prototype
}
/**
* @constructor
* @extends {Resource}
* @param {!Object} wrappedObject
* @param {string} name
*/
function WebGLUniformLocationResource(wrappedObject, name)
{
Resource.call(this, wrappedObject, name);
}
WebGLUniformLocationResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLUniformLocation}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @return {WebGLProgramResource}
*/
program: function()
{
var call = this._calls[0];
if (call && call.functionName() === "getUniformLocation")
return /** @type {WebGLProgramResource} */ (Resource.forObject(call.args()[0]));
console.error("ASSERT_NOT_REACHED: Failed to restore WebGLUniformLocation from the log.", call);
return null;
},
/**
* @return {string}
*/
name: function()
{
var call = this._calls[0];
if (call && call.functionName() === "getUniformLocation")
return call.args()[1];
console.error("ASSERT_NOT_REACHED: Failed to restore WebGLUniformLocation from the log.", call);
return "";
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var location = this.wrappedObject();
if (!location)
return result;
var programResource = this.program();
var program = programResource && programResource.wrappedObject();
if (!program)
return result;
var gl = WebGLRenderingContextResource.forObject(this).wrappedObject();
var uniformValue = gl.getUniform(program, location);
var name = this.name();
result.push({ name: "name", value: name });
result.push({ name: "program", value: programResource });
result.push({ name: "value", value: uniformValue });
result.push({ name: "context", value: this.contextResource() });
if (typeof this._type !== "number") {
var altName = name + "[0]";
var uniformsCount = /** @type {number} */ (gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS));
for (var i = 0; i < uniformsCount; ++i) {
var activeInfo = gl.getActiveUniform(program, i);
if (!activeInfo)
continue;
if (activeInfo.name === name || activeInfo.name === altName) {
this._type = activeInfo.type;
this._size = activeInfo.size;
if (activeInfo.name === name)
break;
}
}
}
if (typeof this._type === "number")
result.push({ name: "type", value: this._type, valueIsEnum: true });
if (typeof this._size === "number")
result.push({ name: "size", value: this._size });
return result;
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<ReplayableResource>} cache
*/
_populateReplayableData: function(data, cache)
{
data.type = this._type;
data.size = this._size;
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<Resource>} cache
*/
_doReplayCalls: function(data, cache)
{
this._type = data.type;
this._size = data.size;
Resource.prototype._doReplayCalls.call(this, data, cache);
},
__proto__: Resource.prototype
}
/**
* @constructor
* @extends {ContextResource}
* @param {!WebGLRenderingContext} glContext
*/
function WebGLRenderingContextResource(glContext)
{
ContextResource.call(this, glContext, "WebGLRenderingContext");
/** @type {Object.<number, boolean>} */
this._customErrors = null;
/** @type {!Object.<string, string>} */
this._extensions = {};
/** @type {!Object.<string, number>} */
this._extensionEnums = {};
}
/**
* @const
* @type {!Array.<string>}
*/
WebGLRenderingContextResource.GLCapabilities = [
"BLEND",
"CULL_FACE",
"DEPTH_TEST",
"DITHER",
"POLYGON_OFFSET_FILL",
"SAMPLE_ALPHA_TO_COVERAGE",
"SAMPLE_COVERAGE",
"SCISSOR_TEST",
"STENCIL_TEST"
];
/**
* @const
* @type {!Array.<string>}
*/
WebGLRenderingContextResource.PixelStoreParameters = [
"PACK_ALIGNMENT",
"UNPACK_ALIGNMENT",
"UNPACK_COLORSPACE_CONVERSION_WEBGL",
"UNPACK_FLIP_Y_WEBGL",
"UNPACK_PREMULTIPLY_ALPHA_WEBGL"
];
/**
* @const
* @type {!Array.<string>}
*/
WebGLRenderingContextResource.StateParameters = [
"ACTIVE_TEXTURE",
"ARRAY_BUFFER_BINDING",
"BLEND_COLOR",
"BLEND_DST_ALPHA",
"BLEND_DST_RGB",
"BLEND_EQUATION_ALPHA",
"BLEND_EQUATION_RGB",
"BLEND_SRC_ALPHA",
"BLEND_SRC_RGB",
"COLOR_CLEAR_VALUE",
"COLOR_WRITEMASK",
"CULL_FACE_MODE",
"CURRENT_PROGRAM",
"DEPTH_CLEAR_VALUE",
"DEPTH_FUNC",
"DEPTH_RANGE",
"DEPTH_WRITEMASK",
"ELEMENT_ARRAY_BUFFER_BINDING",
"FRAGMENT_SHADER_DERIVATIVE_HINT_OES", // OES_standard_derivatives extension
"FRAMEBUFFER_BINDING",
"FRONT_FACE",
"GENERATE_MIPMAP_HINT",
"LINE_WIDTH",
"PACK_ALIGNMENT",
"POLYGON_OFFSET_FACTOR",
"POLYGON_OFFSET_UNITS",
"RENDERBUFFER_BINDING",
"SAMPLE_COVERAGE_INVERT",
"SAMPLE_COVERAGE_VALUE",
"SCISSOR_BOX",
"STENCIL_BACK_FAIL",
"STENCIL_BACK_FUNC",
"STENCIL_BACK_PASS_DEPTH_FAIL",
"STENCIL_BACK_PASS_DEPTH_PASS",
"STENCIL_BACK_REF",
"STENCIL_BACK_VALUE_MASK",
"STENCIL_BACK_WRITEMASK",
"STENCIL_CLEAR_VALUE",
"STENCIL_FAIL",
"STENCIL_FUNC",
"STENCIL_PASS_DEPTH_FAIL",
"STENCIL_PASS_DEPTH_PASS",
"STENCIL_REF",
"STENCIL_VALUE_MASK",
"STENCIL_WRITEMASK",
"UNPACK_ALIGNMENT",
"UNPACK_COLORSPACE_CONVERSION_WEBGL",
"UNPACK_FLIP_Y_WEBGL",
"UNPACK_PREMULTIPLY_ALPHA_WEBGL",
"VERTEX_ARRAY_BINDING_OES", // OES_vertex_array_object extension
"VIEWPORT"
];
/**
* True for those enums that return also an enum via a getter API method (e.g. getParameter, getShaderParameter, etc.).
* @const
* @type {!Object.<string, boolean>}
*/
WebGLRenderingContextResource.GetResultIsEnum = TypeUtils.createPrefixedPropertyNamesSet([
// gl.getParameter()
"ACTIVE_TEXTURE",
"BLEND_DST_ALPHA",
"BLEND_DST_RGB",
"BLEND_EQUATION_ALPHA",
"BLEND_EQUATION_RGB",
"BLEND_SRC_ALPHA",
"BLEND_SRC_RGB",
"CULL_FACE_MODE",
"DEPTH_FUNC",
"FRONT_FACE",
"GENERATE_MIPMAP_HINT",
"FRAGMENT_SHADER_DERIVATIVE_HINT_OES",
"STENCIL_BACK_FAIL",
"STENCIL_BACK_FUNC",
"STENCIL_BACK_PASS_DEPTH_FAIL",
"STENCIL_BACK_PASS_DEPTH_PASS",
"STENCIL_FAIL",
"STENCIL_FUNC",
"STENCIL_PASS_DEPTH_FAIL",
"STENCIL_PASS_DEPTH_PASS",
"UNPACK_COLORSPACE_CONVERSION_WEBGL",
// gl.getBufferParameter()
"BUFFER_USAGE",
// gl.getFramebufferAttachmentParameter()
"FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE",
// gl.getRenderbufferParameter()
"RENDERBUFFER_INTERNAL_FORMAT",
// gl.getTexParameter()
"TEXTURE_MAG_FILTER",
"TEXTURE_MIN_FILTER",
"TEXTURE_WRAP_S",
"TEXTURE_WRAP_T",
// gl.getShaderParameter()
"SHADER_TYPE",
// gl.getVertexAttrib()
"VERTEX_ATTRIB_ARRAY_TYPE"
]);
/**
* @const
* @type {!Object.<string, boolean>}
*/
WebGLRenderingContextResource.DrawingMethods = TypeUtils.createPrefixedPropertyNamesSet([
"clear",
"drawArrays",
"drawElements"
]);
/**
* @param {*} obj
* @return {WebGLRenderingContextResource}
*/
WebGLRenderingContextResource.forObject = function(obj)
{
var resource = Resource.forObject(obj);
if (!resource)
return null;
resource = resource.contextResource();
return (resource instanceof WebGLRenderingContextResource) ? resource : null;
}
WebGLRenderingContextResource.prototype = {
/**
* @override (overrides @return type)
* @return {WebGLRenderingContext}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @override
* @return {string}
*/
toDataURL: function()
{
return this.wrappedObject().canvas.toDataURL();
},
/**
* @return {Array.<number>}
*/
getAllErrors: function()
{
var errors = [];
var gl = this.wrappedObject();
if (gl) {
while (true) {
var error = gl.getError();
if (error === gl.NO_ERROR)
break;
this.clearError(error);
errors.push(error);
}
}
if (this._customErrors) {
for (var key in this._customErrors) {
var error = Number(key);
errors.push(error);
}
delete this._customErrors;
}
return errors;
},
/**
* @param {Array.<number>} errors
*/
restoreErrors: function(errors)
{
var gl = this.wrappedObject();
if (gl) {
var wasError = false;
while (gl.getError() !== gl.NO_ERROR)
wasError = true;
console.assert(!wasError, "Error(s) while capturing current WebGL state.");
}
if (!errors.length)
delete this._customErrors;
else {
this._customErrors = {};
for (var i = 0, n = errors.length; i < n; ++i)
this._customErrors[errors[i]] = true;
}
},
/**
* @param {number} error
*/
clearError: function(error)
{
if (this._customErrors)
delete this._customErrors[error];
},
/**
* @return {number}
*/
nextError: function()
{
if (this._customErrors) {
for (var key in this._customErrors) {
var error = Number(key);
delete this._customErrors[error];
return error;
}
}
delete this._customErrors;
var gl = this.wrappedObject();
return gl ? gl.NO_ERROR : 0;
},
/**
* @param {string} name
* @param {Object} obj
*/
registerWebGLExtension: function(name, obj)
{
// FIXME: Wrap OES_vertex_array_object extension.
var lowerName = name.toLowerCase();
if (obj && !this._extensions[lowerName]) {
this._extensions[lowerName] = name;
for (var property in obj) {
if (TypeUtils.isEnumPropertyName(property, obj))
this._extensionEnums[property] = /** @type {number} */ (obj[property]);
}
}
},
/**
* @param {string} name
* @return {number|undefined}
*/
_enumValueForName: function(name)
{
if (typeof this._extensionEnums[name] === "number")
return this._extensionEnums[name];
var gl = this.wrappedObject();
return (typeof gl[name] === "number" ? gl[name] : undefined);
},
/**
* @param {function(this:WebGLRenderingContext, T, number):*} func
* @param {T} targetOrWebGLObject
* @param {!Array.<string>} pnames
* @param {!Array.<TypeUtils.InternalResourceStateDescriptor>} output
* @template T
*/
queryStateValues: function(func, targetOrWebGLObject, pnames, output)
{
var gl = this.wrappedObject();
for (var i = 0, pname; pname = pnames[i]; ++i) {
var enumValue = this._enumValueForName(pname);
if (typeof enumValue !== "number")
continue;
var value = func.call(gl, targetOrWebGLObject, enumValue);
value = Resource.forObject(value) || value;
output.push({ name: pname, value: value, valueIsEnum: WebGLRenderingContextResource.GetResultIsEnum[pname] });
}
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
/**
* @param {!Object} obj
* @param {!Array.<TypeUtils.InternalResourceStateDescriptor>} output
*/
function convertToStateDescriptors(obj, output)
{
for (var pname in obj)
output.push({ name: pname, value: obj[pname], valueIsEnum: WebGLRenderingContextResource.GetResultIsEnum[pname] });
}
var gl = this.wrappedObject();
var glState = this._internalCurrentState(null);
// VERTEX_ATTRIB_ARRAYS
var vertexAttribStates = [];
for (var i = 0, n = glState.VERTEX_ATTRIB_ARRAYS.length; i < n; ++i) {
var pname = "" + i;
var values = [];
convertToStateDescriptors(glState.VERTEX_ATTRIB_ARRAYS[i], values);
vertexAttribStates.push({ name: pname, values: values });
}
delete glState.VERTEX_ATTRIB_ARRAYS;
// TEXTURE_UNITS
var textureUnits = [];
for (var i = 0, n = glState.TEXTURE_UNITS.length; i < n; ++i) {
var pname = "TEXTURE" + i;
var values = [];
convertToStateDescriptors(glState.TEXTURE_UNITS[i], values);
textureUnits.push({ name: pname, values: values });
}
delete glState.TEXTURE_UNITS;
var result = [];
convertToStateDescriptors(glState, result);
result.push({ name: "VERTEX_ATTRIB_ARRAYS", values: vertexAttribStates, isArray: true });
result.push({ name: "TEXTURE_UNITS", values: textureUnits, isArray: true });
var textureBindingParameters = ["TEXTURE_BINDING_2D", "TEXTURE_BINDING_CUBE_MAP"];
for (var i = 0, pname; pname = textureBindingParameters[i]; ++i) {
var value = gl.getParameter(gl[pname]);
value = Resource.forObject(value) || value;
result.push({ name: pname, value: value });
}
// ENABLED_EXTENSIONS
var enabledExtensions = [];
for (var lowerName in this._extensions) {
var pname = this._extensions[lowerName];
var value = gl.getExtension(pname);
value = Resource.forObject(value) || value;
enabledExtensions.push({ name: pname, value: value });
}
result.push({ name: "ENABLED_EXTENSIONS", values: enabledExtensions, isArray: true });
return result;
},
/**
* @param {?Cache.<ReplayableResource>} cache
* @return {!Object.<string, *>}
*/
_internalCurrentState: function(cache)
{
/**
* @param {Resource|*} obj
* @return {Resource|ReplayableResource|*}
*/
function maybeToReplayable(obj)
{
return cache ? Resource.toReplayable(obj, cache) : (Resource.forObject(obj) || obj);
}
var gl = this.wrappedObject();
var originalErrors = this.getAllErrors();
// Take a full GL state snapshot.
var glState = Object.create(null);
WebGLRenderingContextResource.GLCapabilities.forEach(function(parameter) {
glState[parameter] = gl.isEnabled(gl[parameter]);
});
for (var i = 0, pname; pname = WebGLRenderingContextResource.StateParameters[i]; ++i) {
var enumValue = this._enumValueForName(pname);
if (typeof enumValue === "number")
glState[pname] = maybeToReplayable(gl.getParameter(enumValue));
}
// VERTEX_ATTRIB_ARRAYS
var maxVertexAttribs = /** @type {number} */ (gl.getParameter(gl.MAX_VERTEX_ATTRIBS));
var vertexAttribParameters = [
"VERTEX_ATTRIB_ARRAY_BUFFER_BINDING",
"VERTEX_ATTRIB_ARRAY_ENABLED",
"VERTEX_ATTRIB_ARRAY_SIZE",
"VERTEX_ATTRIB_ARRAY_STRIDE",
"VERTEX_ATTRIB_ARRAY_TYPE",
"VERTEX_ATTRIB_ARRAY_NORMALIZED",
"CURRENT_VERTEX_ATTRIB",
"VERTEX_ATTRIB_ARRAY_DIVISOR_ANGLE" // ANGLE_instanced_arrays extension
];
var vertexAttribStates = [];
for (var index = 0; index < maxVertexAttribs; ++index) {
var state = Object.create(null);
for (var i = 0, pname; pname = vertexAttribParameters[i]; ++i) {
var enumValue = this._enumValueForName(pname);
if (typeof enumValue === "number")
state[pname] = maybeToReplayable(gl.getVertexAttrib(index, enumValue));
}
state.VERTEX_ATTRIB_ARRAY_POINTER = gl.getVertexAttribOffset(index, gl.VERTEX_ATTRIB_ARRAY_POINTER);
vertexAttribStates.push(state);
}
glState.VERTEX_ATTRIB_ARRAYS = vertexAttribStates;
// TEXTURE_UNITS
var savedActiveTexture = /** @type {number} */ (gl.getParameter(gl.ACTIVE_TEXTURE));
var maxTextureImageUnits = /** @type {number} */ (gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS));
var textureUnits = [];
for (var i = 0; i < maxTextureImageUnits; ++i) {
gl.activeTexture(gl.TEXTURE0 + i);
var state = Object.create(null);
state.TEXTURE_2D = maybeToReplayable(gl.getParameter(gl.TEXTURE_BINDING_2D));
state.TEXTURE_CUBE_MAP = maybeToReplayable(gl.getParameter(gl.TEXTURE_BINDING_CUBE_MAP));
textureUnits.push(state);
}
glState.TEXTURE_UNITS = textureUnits;
gl.activeTexture(savedActiveTexture);
this.restoreErrors(originalErrors);
return glState;
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<ReplayableResource>} cache
*/
_populateReplayableData: function(data, cache)
{
var gl = this.wrappedObject();
data.originalCanvas = gl.canvas;
data.originalContextAttributes = gl.getContextAttributes();
data.extensions = TypeUtils.cloneObject(this._extensions);
data.extensionEnums = TypeUtils.cloneObject(this._extensionEnums);
data.glState = this._internalCurrentState(cache);
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<Resource>} cache
*/
_doReplayCalls: function(data, cache)
{
this._customErrors = null;
this._extensions = TypeUtils.cloneObject(data.extensions) || {};
this._extensionEnums = TypeUtils.cloneObject(data.extensionEnums) || {};
var canvas = data.originalCanvas.cloneNode(true);
var replayContext = null;
var contextIds = ["experimental-webgl", "webkit-3d", "3d"];
for (var i = 0, contextId; contextId = contextIds[i]; ++i) {
replayContext = canvas.getContext(contextId, data.originalContextAttributes);
if (replayContext)
break;
}
console.assert(replayContext, "Failed to create a WebGLRenderingContext for the replay.");
var gl = /** @type {!WebGLRenderingContext} */ (Resource.wrappedObject(replayContext));
this.setWrappedObject(gl);
// Enable corresponding WebGL extensions.
for (var name in this._extensions)
gl.getExtension(name);
var glState = data.glState;
gl.bindFramebuffer(gl.FRAMEBUFFER, /** @type {WebGLFramebuffer} */ (ReplayableResource.replay(glState.FRAMEBUFFER_BINDING, cache)));
gl.bindRenderbuffer(gl.RENDERBUFFER, /** @type {WebGLRenderbuffer} */ (ReplayableResource.replay(glState.RENDERBUFFER_BINDING, cache)));
// Enable or disable server-side GL capabilities.
WebGLRenderingContextResource.GLCapabilities.forEach(function(parameter) {
console.assert(parameter in glState);
if (glState[parameter])
gl.enable(gl[parameter]);
else
gl.disable(gl[parameter]);
});
gl.blendColor(glState.BLEND_COLOR[0], glState.BLEND_COLOR[1], glState.BLEND_COLOR[2], glState.BLEND_COLOR[3]);
gl.blendEquationSeparate(glState.BLEND_EQUATION_RGB, glState.BLEND_EQUATION_ALPHA);
gl.blendFuncSeparate(glState.BLEND_SRC_RGB, glState.BLEND_DST_RGB, glState.BLEND_SRC_ALPHA, glState.BLEND_DST_ALPHA);
gl.clearColor(glState.COLOR_CLEAR_VALUE[0], glState.COLOR_CLEAR_VALUE[1], glState.COLOR_CLEAR_VALUE[2], glState.COLOR_CLEAR_VALUE[3]);
gl.clearDepth(glState.DEPTH_CLEAR_VALUE);
gl.clearStencil(glState.STENCIL_CLEAR_VALUE);
gl.colorMask(glState.COLOR_WRITEMASK[0], glState.COLOR_WRITEMASK[1], glState.COLOR_WRITEMASK[2], glState.COLOR_WRITEMASK[3]);
gl.cullFace(glState.CULL_FACE_MODE);
gl.depthFunc(glState.DEPTH_FUNC);
gl.depthMask(glState.DEPTH_WRITEMASK);
gl.depthRange(glState.DEPTH_RANGE[0], glState.DEPTH_RANGE[1]);
gl.frontFace(glState.FRONT_FACE);
gl.hint(gl.GENERATE_MIPMAP_HINT, glState.GENERATE_MIPMAP_HINT);
gl.lineWidth(glState.LINE_WIDTH);
var enumValue = this._enumValueForName("FRAGMENT_SHADER_DERIVATIVE_HINT_OES");
if (typeof enumValue === "number")
gl.hint(enumValue, glState.FRAGMENT_SHADER_DERIVATIVE_HINT_OES);
WebGLRenderingContextResource.PixelStoreParameters.forEach(function(parameter) {
gl.pixelStorei(gl[parameter], glState[parameter]);
});
gl.polygonOffset(glState.POLYGON_OFFSET_FACTOR, glState.POLYGON_OFFSET_UNITS);
gl.sampleCoverage(glState.SAMPLE_COVERAGE_VALUE, glState.SAMPLE_COVERAGE_INVERT);
gl.stencilFuncSeparate(gl.FRONT, glState.STENCIL_FUNC, glState.STENCIL_REF, glState.STENCIL_VALUE_MASK);
gl.stencilFuncSeparate(gl.BACK, glState.STENCIL_BACK_FUNC, glState.STENCIL_BACK_REF, glState.STENCIL_BACK_VALUE_MASK);
gl.stencilOpSeparate(gl.FRONT, glState.STENCIL_FAIL, glState.STENCIL_PASS_DEPTH_FAIL, glState.STENCIL_PASS_DEPTH_PASS);
gl.stencilOpSeparate(gl.BACK, glState.STENCIL_BACK_FAIL, glState.STENCIL_BACK_PASS_DEPTH_FAIL, glState.STENCIL_BACK_PASS_DEPTH_PASS);
gl.stencilMaskSeparate(gl.FRONT, glState.STENCIL_WRITEMASK);
gl.stencilMaskSeparate(gl.BACK, glState.STENCIL_BACK_WRITEMASK);
gl.scissor(glState.SCISSOR_BOX[0], glState.SCISSOR_BOX[1], glState.SCISSOR_BOX[2], glState.SCISSOR_BOX[3]);
gl.viewport(glState.VIEWPORT[0], glState.VIEWPORT[1], glState.VIEWPORT[2], glState.VIEWPORT[3]);
gl.useProgram(/** @type {WebGLProgram} */ (ReplayableResource.replay(glState.CURRENT_PROGRAM, cache)));
// VERTEX_ATTRIB_ARRAYS
var maxVertexAttribs = /** @type {number} */ (gl.getParameter(gl.MAX_VERTEX_ATTRIBS));
for (var i = 0; i < maxVertexAttribs; ++i) {
var state = glState.VERTEX_ATTRIB_ARRAYS[i] || {};
if (state.VERTEX_ATTRIB_ARRAY_ENABLED)
gl.enableVertexAttribArray(i);
else
gl.disableVertexAttribArray(i);
if (state.CURRENT_VERTEX_ATTRIB)
gl.vertexAttrib4fv(i, state.CURRENT_VERTEX_ATTRIB);
var buffer = /** @type {WebGLBuffer} */ (ReplayableResource.replay(state.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING, cache));
if (buffer) {
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.vertexAttribPointer(i, state.VERTEX_ATTRIB_ARRAY_SIZE, state.VERTEX_ATTRIB_ARRAY_TYPE, state.VERTEX_ATTRIB_ARRAY_NORMALIZED, state.VERTEX_ATTRIB_ARRAY_STRIDE, state.VERTEX_ATTRIB_ARRAY_POINTER);
}
}
gl.bindBuffer(gl.ARRAY_BUFFER, /** @type {WebGLBuffer} */ (ReplayableResource.replay(glState.ARRAY_BUFFER_BINDING, cache)));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, /** @type {WebGLBuffer} */ (ReplayableResource.replay(glState.ELEMENT_ARRAY_BUFFER_BINDING, cache)));
// TEXTURE_UNITS
var maxTextureImageUnits = /** @type {number} */ (gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS));
for (var i = 0; i < maxTextureImageUnits; ++i) {
gl.activeTexture(gl.TEXTURE0 + i);
var state = glState.TEXTURE_UNITS[i] || {};
gl.bindTexture(gl.TEXTURE_2D, /** @type {WebGLTexture} */ (ReplayableResource.replay(state.TEXTURE_2D, cache)));
gl.bindTexture(gl.TEXTURE_CUBE_MAP, /** @type {WebGLTexture} */ (ReplayableResource.replay(state.TEXTURE_CUBE_MAP, cache)));
}
gl.activeTexture(glState.ACTIVE_TEXTURE);
ContextResource.prototype._doReplayCalls.call(this, data, cache);
},
/**
* @param {Object|number} target
* @return {Resource}
*/
currentBinding: function(target)
{
var resource = Resource.forObject(target);
if (resource)
return resource;
var gl = this.wrappedObject();
var bindingParameter;
var bindMethodName;
var bindMethodTarget = target;
switch (target) {
case gl.ARRAY_BUFFER:
bindingParameter = gl.ARRAY_BUFFER_BINDING;
bindMethodName = "bindBuffer";
break;
case gl.ELEMENT_ARRAY_BUFFER:
bindingParameter = gl.ELEMENT_ARRAY_BUFFER_BINDING;
bindMethodName = "bindBuffer";
break;
case gl.TEXTURE_2D:
bindingParameter = gl.TEXTURE_BINDING_2D;
bindMethodName = "bindTexture";
break;
case gl.TEXTURE_CUBE_MAP:
case gl.TEXTURE_CUBE_MAP_POSITIVE_X:
case gl.TEXTURE_CUBE_MAP_NEGATIVE_X:
case gl.TEXTURE_CUBE_MAP_POSITIVE_Y:
case gl.TEXTURE_CUBE_MAP_NEGATIVE_Y:
case gl.TEXTURE_CUBE_MAP_POSITIVE_Z:
case gl.TEXTURE_CUBE_MAP_NEGATIVE_Z:
bindingParameter = gl.TEXTURE_BINDING_CUBE_MAP;
bindMethodTarget = gl.TEXTURE_CUBE_MAP;
bindMethodName = "bindTexture";
break;
case gl.FRAMEBUFFER:
bindingParameter = gl.FRAMEBUFFER_BINDING;
bindMethodName = "bindFramebuffer";
break;
case gl.RENDERBUFFER:
bindingParameter = gl.RENDERBUFFER_BINDING;
bindMethodName = "bindRenderbuffer";
break;
default:
console.error("ASSERT_NOT_REACHED: unknown binding target " + target);
return null;
}
resource = Resource.forObject(gl.getParameter(bindingParameter));
if (resource)
resource.pushBinding(bindMethodTarget, bindMethodName);
return resource;
},
/**
* @override
* @param {!Call} call
*/
onCallReplayed: function(call)
{
var functionName = call.functionName();
var args = call.args();
switch (functionName) {
case "bindBuffer":
case "bindFramebuffer":
case "bindRenderbuffer":
case "bindTexture":
// Update BINDING state for Resources in the replay world.
var resource = Resource.forObject(args[1]);
if (resource)
resource.pushBinding(args[0], functionName);
break;
case "getExtension":
this.registerWebGLExtension(args[0], /** @type {Object} */ (call.result()));
break;
}
},
/**
* @override
* @return {!Object.<string, Function>}
*/
_customWrapFunctions: function()
{
var wrapFunctions = WebGLRenderingContextResource._wrapFunctions;
if (!wrapFunctions) {
wrapFunctions = Object.create(null);
wrapFunctions["createBuffer"] = Resource.WrapFunction.resourceFactoryMethod(WebGLBufferResource, "WebGLBuffer");
wrapFunctions["createShader"] = Resource.WrapFunction.resourceFactoryMethod(WebGLShaderResource, "WebGLShader");
wrapFunctions["createProgram"] = Resource.WrapFunction.resourceFactoryMethod(WebGLProgramResource, "WebGLProgram");
wrapFunctions["createTexture"] = Resource.WrapFunction.resourceFactoryMethod(WebGLTextureResource, "WebGLTexture");
wrapFunctions["createFramebuffer"] = Resource.WrapFunction.resourceFactoryMethod(WebGLFramebufferResource, "WebGLFramebuffer");
wrapFunctions["createRenderbuffer"] = Resource.WrapFunction.resourceFactoryMethod(WebGLRenderbufferResource, "WebGLRenderbuffer");
wrapFunctions["getUniformLocation"] = Resource.WrapFunction.resourceFactoryMethod(WebGLUniformLocationResource, "WebGLUniformLocation");
/**
* @param {string} methodName
* @param {function(this:Resource, !Call)=} pushCallFunc
*/
function stateModifyingWrapFunction(methodName, pushCallFunc)
{
if (pushCallFunc) {
/**
* @param {Object|number} target
* @this Resource.WrapFunction
*/
wrapFunctions[methodName] = function(target)
{
var resource = this._resource.currentBinding(target);
if (resource)
pushCallFunc.call(resource, this.call());
}
} else {
/**
* @param {Object|number} target
* @this Resource.WrapFunction
*/
wrapFunctions[methodName] = function(target)
{
var resource = this._resource.currentBinding(target);
if (resource)
resource.pushCall(this.call());
}
}
}
stateModifyingWrapFunction("bindAttribLocation");
stateModifyingWrapFunction("compileShader");
stateModifyingWrapFunction("detachShader");
stateModifyingWrapFunction("linkProgram");
stateModifyingWrapFunction("shaderSource");
stateModifyingWrapFunction("bufferData");
stateModifyingWrapFunction("bufferSubData");
stateModifyingWrapFunction("compressedTexImage2D");
stateModifyingWrapFunction("compressedTexSubImage2D");
stateModifyingWrapFunction("copyTexImage2D", WebGLTextureResource.prototype.pushCall_copyTexImage2D);
stateModifyingWrapFunction("copyTexSubImage2D", WebGLTextureResource.prototype.pushCall_copyTexImage2D);
stateModifyingWrapFunction("generateMipmap");
stateModifyingWrapFunction("texImage2D");
stateModifyingWrapFunction("texSubImage2D");
stateModifyingWrapFunction("texParameterf", WebGLTextureResource.prototype.pushCall_texParameter);
stateModifyingWrapFunction("texParameteri", WebGLTextureResource.prototype.pushCall_texParameter);
stateModifyingWrapFunction("renderbufferStorage");
/** @this Resource.WrapFunction */
wrapFunctions["getError"] = function()
{
var gl = /** @type {WebGLRenderingContext} */ (this._originalObject);
var error = this.result();
if (error !== gl.NO_ERROR)
this._resource.clearError(error);
else {
error = this._resource.nextError();
if (error !== gl.NO_ERROR)
this.overrideResult(error);
}
}
/**
* @param {string} name
* @this Resource.WrapFunction
*/
wrapFunctions["getExtension"] = function(name)
{
this._resource.registerWebGLExtension(name, this.result());
}
//
// Register bound WebGL resources.
//
/**
* @param {WebGLProgram} program
* @param {WebGLShader} shader
* @this Resource.WrapFunction
*/
wrapFunctions["attachShader"] = function(program, shader)
{
var resource = this._resource.currentBinding(program);
if (resource) {
resource.pushCall(this.call());
var shaderResource = /** @type {WebGLShaderResource} */ (Resource.forObject(shader));
if (shaderResource) {
var shaderType = shaderResource.type();
resource._registerBoundResource("__attachShader_" + shaderType, shaderResource);
}
}
}
/**
* @param {number} target
* @param {number} attachment
* @param {number} objectTarget
* @param {WebGLRenderbuffer|WebGLTexture} obj
* @this Resource.WrapFunction
*/
wrapFunctions["framebufferRenderbuffer"] = wrapFunctions["framebufferTexture2D"] = function(target, attachment, objectTarget, obj)
{
var resource = this._resource.currentBinding(target);
if (resource) {
resource.pushCall(this.call());
resource._registerBoundResource("__framebufferAttachmentObjectName", obj);
}
}
/**
* @param {number} target
* @param {Object} obj
* @this Resource.WrapFunction
*/
wrapFunctions["bindBuffer"] = wrapFunctions["bindFramebuffer"] = wrapFunctions["bindRenderbuffer"] = function(target, obj)
{
this._resource.currentBinding(target); // To call WebGLBoundResource.prototype.pushBinding().
this._resource._registerBoundResource("__bindBuffer_" + target, obj);
}
/**
* @param {number} target
* @param {WebGLTexture} obj
* @this Resource.WrapFunction
*/
wrapFunctions["bindTexture"] = function(target, obj)
{
this._resource.currentBinding(target); // To call WebGLBoundResource.prototype.pushBinding().
var gl = /** @type {WebGLRenderingContext} */ (this._originalObject);
var currentTextureBinding = /** @type {number} */ (gl.getParameter(gl.ACTIVE_TEXTURE));
this._resource._registerBoundResource("__bindTexture_" + target + "_" + currentTextureBinding, obj);
}
/**
* @param {WebGLProgram} program
* @this Resource.WrapFunction
*/
wrapFunctions["useProgram"] = function(program)
{
this._resource._registerBoundResource("__useProgram", program);
}
/**
* @param {number} index
* @this Resource.WrapFunction
*/
wrapFunctions["vertexAttribPointer"] = function(index)
{
var gl = /** @type {WebGLRenderingContext} */ (this._originalObject);
this._resource._registerBoundResource("__vertexAttribPointer_" + index, gl.getParameter(gl.ARRAY_BUFFER_BINDING));
}
WebGLRenderingContextResource._wrapFunctions = wrapFunctions;
}
return wrapFunctions;
},
__proto__: ContextResource.prototype
}
////////////////////////////////////////////////////////////////////////////////
// 2D Canvas
////////////////////////////////////////////////////////////////////////////////
/**
* @constructor
* @extends {ContextResource}
* @param {!CanvasRenderingContext2D} context
*/
function CanvasRenderingContext2DResource(context)
{
ContextResource.call(this, context, "CanvasRenderingContext2D");
}
/**
* @const
* @type {!Array.<string>}
*/
CanvasRenderingContext2DResource.AttributeProperties = [
"strokeStyle",
"fillStyle",
"globalAlpha",
"lineWidth",
"lineCap",
"lineJoin",
"miterLimit",
"shadowOffsetX",
"shadowOffsetY",
"shadowBlur",
"shadowColor",
"globalCompositeOperation",
"font",
"textAlign",
"textBaseline",
"lineDashOffset",
"imageSmoothingEnabled",
"webkitImageSmoothingEnabled",
"webkitLineDash",
"webkitLineDashOffset"
];
/**
* @const
* @type {!Array.<string>}
*/
CanvasRenderingContext2DResource.PathMethods = [
"beginPath",
"moveTo",
"closePath",
"lineTo",
"quadraticCurveTo",
"bezierCurveTo",
"arcTo",
"arc",
"rect"
];
/**
* @const
* @type {!Array.<string>}
*/
CanvasRenderingContext2DResource.TransformationMatrixMethods = [
"scale",
"rotate",
"translate",
"transform",
"setTransform"
];
/**
* @const
* @type {!Object.<string, boolean>}
*/
CanvasRenderingContext2DResource.DrawingMethods = TypeUtils.createPrefixedPropertyNamesSet([
"clearRect",
"drawImage",
"drawImageFromRect",
"drawCustomFocusRing",
"drawSystemFocusRing",
"fill",
"fillRect",
"fillText",
"putImageData",
"putImageDataHD",
"stroke",
"strokeRect",
"strokeText"
]);
CanvasRenderingContext2DResource.prototype = {
/**
* @override (overrides @return type)
* @return {CanvasRenderingContext2D}
*/
wrappedObject: function()
{
return this._wrappedObject;
},
/**
* @override
* @return {string}
*/
toDataURL: function()
{
return this.wrappedObject().canvas.toDataURL();
},
/**
* @override
* @return {!Array.<TypeUtils.InternalResourceStateDescriptor>}
*/
currentState: function()
{
var result = [];
var state = this._internalCurrentState(null);
for (var pname in state)
result.push({ name: pname, value: state[pname] });
result.push({ name: "context", value: this.contextResource() });
return result;
},
/**
* @param {?Cache.<ReplayableResource>} cache
* @return {!Object.<string, *>}
*/
_internalCurrentState: function(cache)
{
/**
* @param {Resource|*} obj
* @return {Resource|ReplayableResource|*}
*/
function maybeToReplayable(obj)
{
return cache ? Resource.toReplayable(obj, cache) : (Resource.forObject(obj) || obj);
}
var ctx = this.wrappedObject();
var state = Object.create(null);
CanvasRenderingContext2DResource.AttributeProperties.forEach(function(attribute) {
if (attribute in ctx)
state[attribute] = maybeToReplayable(ctx[attribute]);
});
if (ctx.getLineDash)
state.lineDash = ctx.getLineDash();
return state;
},
/**
* @param {Object.<string, *>} state
* @param {!Cache.<Resource>} cache
*/
_applyAttributesState: function(state, cache)
{
if (!state)
return;
var ctx = this.wrappedObject();
for (var attribute in state) {
if (attribute === "lineDash") {
if (ctx.setLineDash)
ctx.setLineDash(/** @type {Array.<number>} */ (state[attribute]));
} else
ctx[attribute] = ReplayableResource.replay(state[attribute], cache);
}
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<ReplayableResource>} cache
*/
_populateReplayableData: function(data, cache)
{
var ctx = this.wrappedObject();
// FIXME: Convert resources in the state (CanvasGradient, CanvasPattern) to Replayable.
data.currentAttributes = this._internalCurrentState(null);
data.originalCanvasCloned = TypeUtils.cloneIntoCanvas(ctx.canvas);
if (ctx.getContextAttributes)
data.originalContextAttributes = ctx.getContextAttributes();
},
/**
* @override
* @param {!Object} data
* @param {!Cache.<Resource>} cache
*/
_doReplayCalls: function(data, cache)
{
var canvas = TypeUtils.cloneIntoCanvas(data.originalCanvasCloned);
var ctx = /** @type {!CanvasRenderingContext2D} */ (Resource.wrappedObject(canvas.getContext("2d", data.originalContextAttributes)));
this.setWrappedObject(ctx);
for (var i = 0, n = data.calls.length; i < n; ++i) {
var replayableCall = /** @type {ReplayableCall} */ (data.calls[i]);
if (replayableCall.functionName() === "save")
this._applyAttributesState(replayableCall.attachment("canvas2dAttributesState"), cache);
this._calls.push(replayableCall.replay(cache));
}
this._applyAttributesState(data.currentAttributes, cache);
},
/**
* @param {!Call} call
*/
pushCall_setTransform: function(call)
{
var saveCallIndex = this._lastIndexOfMatchingSaveCall();
var index = this._lastIndexOfAnyCall(CanvasRenderingContext2DResource.PathMethods);
index = Math.max(index, saveCallIndex);
if (this._removeCallsFromLog(CanvasRenderingContext2DResource.TransformationMatrixMethods, index + 1))
this._removeAllObsoleteCallsFromLog();
this.pushCall(call);
},
/**
* @param {!Call} call
*/
pushCall_beginPath: function(call)
{
var index = this._lastIndexOfAnyCall(["clip"]);
if (this._removeCallsFromLog(CanvasRenderingContext2DResource.PathMethods, index + 1))
this._removeAllObsoleteCallsFromLog();
this.pushCall(call);
},
/**
* @param {!Call} call
*/
pushCall_save: function(call)
{
// FIXME: Convert resources in the state (CanvasGradient, CanvasPattern) to Replayable.
call.setAttachment("canvas2dAttributesState", this._internalCurrentState(null));
this.pushCall(call);
},
/**
* @param {!Call} call
*/
pushCall_restore: function(call)
{
var lastIndexOfSave = this._lastIndexOfMatchingSaveCall();
if (lastIndexOfSave === -1)
return;
this._calls[lastIndexOfSave].setAttachment("canvas2dAttributesState", null); // No longer needed, free memory.
var modified = false;
if (this._removeCallsFromLog(["clip"], lastIndexOfSave + 1))
modified = true;
var lastIndexOfAnyPathMethod = this._lastIndexOfAnyCall(CanvasRenderingContext2DResource.PathMethods);
var index = Math.max(lastIndexOfSave, lastIndexOfAnyPathMethod);
if (this._removeCallsFromLog(CanvasRenderingContext2DResource.TransformationMatrixMethods, index + 1))
modified = true;
if (modified)
this._removeAllObsoleteCallsFromLog();
var lastCall = this._calls[this._calls.length - 1];
if (lastCall && lastCall.functionName() === "save")
this._calls.pop();
else
this.pushCall(call);
},
/**
* @param {number=} fromIndex
* @return {number}
*/
_lastIndexOfMatchingSaveCall: function(fromIndex)
{
if (typeof fromIndex !== "number")
fromIndex = this._calls.length - 1;
else
fromIndex = Math.min(fromIndex, this._calls.length - 1);
var stackDepth = 1;
for (var i = fromIndex; i >= 0; --i) {
var functionName = this._calls[i].functionName();
if (functionName === "restore")
++stackDepth;
else if (functionName === "save") {
--stackDepth;
if (!stackDepth)
return i;
}
}
return -1;
},
/**
* @param {!Array.<string>} functionNames
* @param {number=} fromIndex
* @return {number}
*/
_lastIndexOfAnyCall: function(functionNames, fromIndex)
{
if (typeof fromIndex !== "number")
fromIndex = this._calls.length - 1;
else
fromIndex = Math.min(fromIndex, this._calls.length - 1);
for (var i = fromIndex; i >= 0; --i) {
if (functionNames.indexOf(this._calls[i].functionName()) !== -1)
return i;
}
return -1;
},
_removeAllObsoleteCallsFromLog: function()
{
// Remove all PATH methods between clip() and beginPath() calls.
var lastIndexOfBeginPath = this._lastIndexOfAnyCall(["beginPath"]);
while (lastIndexOfBeginPath !== -1) {
var index = this._lastIndexOfAnyCall(["clip"], lastIndexOfBeginPath - 1);
this._removeCallsFromLog(CanvasRenderingContext2DResource.PathMethods, index + 1, lastIndexOfBeginPath);
lastIndexOfBeginPath = this._lastIndexOfAnyCall(["beginPath"], index - 1);
}
// Remove all TRASFORMATION MATRIX methods before restore() or setTransform() but after any PATH or corresponding save() method.
var lastRestore = this._lastIndexOfAnyCall(["restore", "setTransform"]);
while (lastRestore !== -1) {
var saveCallIndex = this._lastIndexOfMatchingSaveCall(lastRestore - 1);
var index = this._lastIndexOfAnyCall(CanvasRenderingContext2DResource.PathMethods, lastRestore - 1);
index = Math.max(index, saveCallIndex);
this._removeCallsFromLog(CanvasRenderingContext2DResource.TransformationMatrixMethods, index + 1, lastRestore);
lastRestore = this._lastIndexOfAnyCall(["restore", "setTransform"], index - 1);
}
// Remove all save-restore consecutive pairs.
var restoreCalls = 0;
for (var i = this._calls.length - 1; i >= 0; --i) {
var functionName = this._calls[i].functionName();
if (functionName === "restore") {
++restoreCalls;
continue;
}
if (functionName === "save" && restoreCalls > 0) {
var saveCallIndex = i;
for (var j = i - 1; j >= 0 && i - j < restoreCalls; --j) {
if (this._calls[j].functionName() === "save")
saveCallIndex = j;
else
break;
}
this._calls.splice(saveCallIndex, (i - saveCallIndex + 1) * 2);
i = saveCallIndex;
}
restoreCalls = 0;
}
},
/**
* @param {!Array.<string>} functionNames
* @param {number} fromIndex
* @param {number=} toIndex
* @return {boolean}
*/
_removeCallsFromLog: function(functionNames, fromIndex, toIndex)
{
var oldLength = this._calls.length;
if (typeof toIndex !== "number")
toIndex = oldLength;
else
toIndex = Math.min(toIndex, oldLength);
var newIndex = Math.min(fromIndex, oldLength);
for (var i = newIndex; i < toIndex; ++i) {
var call = this._calls[i];
if (functionNames.indexOf(call.functionName()) === -1)
this._calls[newIndex++] = call;
}
if (newIndex >= toIndex)
return false;
this._calls.splice(newIndex, toIndex - newIndex);
return true;
},
/**
* @override
* @return {!Object.<string, Function>}
*/
_customWrapFunctions: function()
{
var wrapFunctions = CanvasRenderingContext2DResource._wrapFunctions;
if (!wrapFunctions) {
wrapFunctions = Object.create(null);
wrapFunctions["createLinearGradient"] = Resource.WrapFunction.resourceFactoryMethod(LogEverythingResource, "CanvasGradient");
wrapFunctions["createRadialGradient"] = Resource.WrapFunction.resourceFactoryMethod(LogEverythingResource, "CanvasGradient");
wrapFunctions["createPattern"] = Resource.WrapFunction.resourceFactoryMethod(LogEverythingResource, "CanvasPattern");
/**
* @param {string} methodName
* @param {function(this:Resource, !Call)=} func
*/
function stateModifyingWrapFunction(methodName, func)
{
if (func) {
/** @this Resource.WrapFunction */
wrapFunctions[methodName] = function()
{
func.call(this._resource, this.call());
}
} else {
/** @this Resource.WrapFunction */
wrapFunctions[methodName] = function()
{
this._resource.pushCall(this.call());
}
}
}
for (var i = 0, methodName; methodName = CanvasRenderingContext2DResource.TransformationMatrixMethods[i]; ++i)
stateModifyingWrapFunction(methodName, methodName === "setTransform" ? this.pushCall_setTransform : undefined);
for (var i = 0, methodName; methodName = CanvasRenderingContext2DResource.PathMethods[i]; ++i)
stateModifyingWrapFunction(methodName, methodName === "beginPath" ? this.pushCall_beginPath : undefined);
stateModifyingWrapFunction("save", this.pushCall_save);
stateModifyingWrapFunction("restore", this.pushCall_restore);
stateModifyingWrapFunction("clip");
CanvasRenderingContext2DResource._wrapFunctions = wrapFunctions;
}
return wrapFunctions;
},
__proto__: ContextResource.prototype
}
/**
* @constructor
* @param {!Object.<string, boolean>=} drawingMethodNames
*/
function CallFormatter(drawingMethodNames)
{
this._drawingMethodNames = drawingMethodNames || Object.create(null);
}
CallFormatter.prototype = {
/**
* @param {!ReplayableCall} replayableCall
* @param {string=} objectGroup
* @return {!Object}
*/
formatCall: function(replayableCall, objectGroup)
{
var result = {};
var functionName = replayableCall.functionName();
if (functionName) {
result.functionName = functionName;
result.arguments = [];
var args = replayableCall.args();
for (var i = 0, n = args.length; i < n; ++i)
result.arguments.push(this.formatValue(args[i], objectGroup));
if (replayableCall.result() !== undefined)
result.result = this.formatValue(replayableCall.result(), objectGroup);
if (this._drawingMethodNames[functionName])
result.isDrawingCall = true;
} else {
result.property = replayableCall.propertyName();
result.value = this.formatValue(replayableCall.propertyValue(), objectGroup);
}
return result;
},
/**
* @param {*} value
* @param {string=} objectGroup
* @return {!CanvasAgent.CallArgument}
*/
formatValue: function(value, objectGroup)
{
if (value instanceof Resource || value instanceof ReplayableResource) {
return {
description: value.description(),
resourceId: CallFormatter.makeStringResourceId(value.id())
};
}
var remoteObject = injectedScript.wrapObject(value, objectGroup || "", true, false);
var description = remoteObject.description || ("" + value);
var result = {
description: description,
type: /** @type {CanvasAgent.CallArgumentType} */ (remoteObject.type)
};
if (remoteObject.subtype)
result.subtype = /** @type {CanvasAgent.CallArgumentSubtype} */ (remoteObject.subtype);
if (remoteObject.objectId) {
if (objectGroup)
result.remoteObject = remoteObject;
else
injectedScript.releaseObject(remoteObject.objectId);
}
return result;
},
/**
* @param {string} name
* @return {?string}
*/
enumValueForName: function(name)
{
return null;
},
/**
* @param {number} value
* @param {Array.<string>=} options
* @return {?string}
*/
enumNameForValue: function(value, options)
{
return null;
},
/**
* @param {!Array.<TypeUtils.InternalResourceStateDescriptor>} descriptors
* @param {string=} objectGroup
* @return {!Array.<!CanvasAgent.ResourceStateDescriptor>}
*/
formatResourceStateDescriptors: function(descriptors, objectGroup)
{
var result = [];
for (var i = 0, n = descriptors.length; i < n; ++i) {
var d = descriptors[i];
var item;
if (d.values)
item = { name: d.name, values: this.formatResourceStateDescriptors(d.values, objectGroup) };
else {
item = { name: d.name, value: this.formatValue(d.value, objectGroup) };
if (d.valueIsEnum && typeof d.value === "number") {
var enumName = this.enumNameForValue(d.value);
if (enumName)
item.value.enumName = enumName;
}
}
var enumValue = this.enumValueForName(d.name);
if (enumValue)
item.enumValueForName = enumValue;
if (d.isArray)
item.isArray = true;
result.push(item);
}
return result;
}
}
/**
* @const
* @type {!Object.<string, !CallFormatter>}
*/
CallFormatter._formatters = {};
/**
* @param {string} resourceName
* @param {!CallFormatter} callFormatter
*/
CallFormatter.register = function(resourceName, callFormatter)
{
CallFormatter._formatters[resourceName] = callFormatter;
}
/**
* @param {!Resource|!ReplayableResource} resource
* @return {!CallFormatter}
*/
CallFormatter.forResource = function(resource)
{
var formatter = CallFormatter._formatters[resource.name()];
if (!formatter) {
var contextResource = resource.contextResource();
formatter = (contextResource && CallFormatter._formatters[contextResource.name()]) || new CallFormatter();
}
return formatter;
}
/**
* @param {number} resourceId
* @return {CanvasAgent.ResourceId}
*/
CallFormatter.makeStringResourceId = function(resourceId)
{
return "{\"injectedScriptId\":" + injectedScriptId + ",\"resourceId\":" + resourceId + "}";
}
/**
* @constructor
* @extends {CallFormatter}
* @param {!Object.<string, boolean>} drawingMethodNames
*/
function WebGLCallFormatter(drawingMethodNames)
{
CallFormatter.call(this, drawingMethodNames);
}
/**
* NOTE: The code below is generated from the IDL file by the script:
* /devtools/scripts/check_injected_webgl_calls_info.py
*
* @type {!Array.<{aname: string, enum: (!Array.<number>|undefined), bitfield: (!Array.<number>|undefined), returnType: string, hints: (!Array.<string>|undefined)}>}
*/
WebGLCallFormatter.EnumsInfo = [
{"aname": "activeTexture", "enum": [0]},
{"aname": "bindBuffer", "enum": [0]},
{"aname": "bindFramebuffer", "enum": [0]},
{"aname": "bindRenderbuffer", "enum": [0]},
{"aname": "bindTexture", "enum": [0]},
{"aname": "blendEquation", "enum": [0]},
{"aname": "blendEquationSeparate", "enum": [0, 1]},
{"aname": "blendFunc", "enum": [0, 1], "hints": ["ZERO", "ONE"]},
{"aname": "blendFuncSeparate", "enum": [0, 1, 2, 3], "hints": ["ZERO", "ONE"]},
{"aname": "bufferData", "enum": [0, 2]},
{"aname": "bufferSubData", "enum": [0]},
{"aname": "checkFramebufferStatus", "enum": [0], "returnType": "enum"},
{"aname": "clear", "bitfield": [0]},
{"aname": "compressedTexImage2D", "enum": [0, 2]},
{"aname": "compressedTexSubImage2D", "enum": [0, 6]},
{"aname": "copyTexImage2D", "enum": [0, 2]},
{"aname": "copyTexSubImage2D", "enum": [0]},
{"aname": "createShader", "enum": [0]},
{"aname": "cullFace", "enum": [0]},
{"aname": "depthFunc", "enum": [0]},
{"aname": "disable", "enum": [0]},
{"aname": "drawArrays", "enum": [0], "hints": ["POINTS", "LINES"]},
{"aname": "drawElements", "enum": [0, 2], "hints": ["POINTS", "LINES"]},
{"aname": "enable", "enum": [0]},
{"aname": "framebufferRenderbuffer", "enum": [0, 1, 2]},
{"aname": "framebufferTexture2D", "enum": [0, 1, 2]},
{"aname": "frontFace", "enum": [0]},
{"aname": "generateMipmap", "enum": [0]},
{"aname": "getBufferParameter", "enum": [0, 1]},
{"aname": "getError", "hints": ["NO_ERROR"], "returnType": "enum"},
{"aname": "getFramebufferAttachmentParameter", "enum": [0, 1, 2]},
{"aname": "getParameter", "enum": [0]},
{"aname": "getProgramParameter", "enum": [1]},
{"aname": "getRenderbufferParameter", "enum": [0, 1]},
{"aname": "getShaderParameter", "enum": [1]},
{"aname": "getShaderPrecisionFormat", "enum": [0, 1]},
{"aname": "getTexParameter", "enum": [0, 1], "returnType": "enum"},
{"aname": "getVertexAttrib", "enum": [1]},
{"aname": "getVertexAttribOffset", "enum": [1]},
{"aname": "hint", "enum": [0, 1]},
{"aname": "isEnabled", "enum": [0]},
{"aname": "pixelStorei", "enum": [0]},
{"aname": "readPixels", "enum": [4, 5]},
{"aname": "renderbufferStorage", "enum": [0, 1]},
{"aname": "stencilFunc", "enum": [0]},
{"aname": "stencilFuncSeparate", "enum": [0, 1]},
{"aname": "stencilMaskSeparate", "enum": [0]},
{"aname": "stencilOp", "enum": [0, 1, 2], "hints": ["ZERO", "ONE"]},
{"aname": "stencilOpSeparate", "enum": [0, 1, 2, 3], "hints": ["ZERO", "ONE"]},
{"aname": "texParameterf", "enum": [0, 1, 2]},
{"aname": "texParameteri", "enum": [0, 1, 2]},
{"aname": "texImage2D", "enum": [0, 2, 6, 7]},
{"aname": "texImage2D", "enum": [0, 2, 3, 4]},
{"aname": "texSubImage2D", "enum": [0, 6, 7]},
{"aname": "texSubImage2D", "enum": [0, 4, 5]},
{"aname": "vertexAttribPointer", "enum": [2]}
];
WebGLCallFormatter.prototype = {
/**
* @override
* @param {!ReplayableCall} replayableCall
* @param {string=} objectGroup
* @return {!Object}
*/
formatCall: function(replayableCall, objectGroup)
{
var result = CallFormatter.prototype.formatCall.call(this, replayableCall, objectGroup);
if (!result.functionName)
return result;
var enumsInfo = this._findEnumsInfo(replayableCall);
if (!enumsInfo)
return result;
var enumArgsIndexes = enumsInfo["enum"] || [];
for (var i = 0, n = enumArgsIndexes.length; i < n; ++i) {
var index = enumArgsIndexes[i];
var callArgument = result.arguments[index];
this._formatEnumValue(callArgument, enumsInfo["hints"]);
}
var bitfieldArgsIndexes = enumsInfo["bitfield"] || [];
for (var i = 0, n = bitfieldArgsIndexes.length; i < n; ++i) {
var index = bitfieldArgsIndexes[i];
var callArgument = result.arguments[index];
this._formatEnumBitmaskValue(callArgument, enumsInfo["hints"]);
}
if (enumsInfo.returnType === "enum")
this._formatEnumValue(result.result, enumsInfo["hints"]);
else if (enumsInfo.returnType === "bitfield")
this._formatEnumBitmaskValue(result.result, enumsInfo["hints"]);
return result;
},
/**
* @override
* @param {string} name
* @return {?string}
*/
enumValueForName: function(name)
{
this._initialize();
if (name in this._enumNameToValue)
return "" + this._enumNameToValue[name];
return null;
},
/**
* @override
* @param {number} value
* @param {Array.<string>=} options
* @return {?string}
*/
enumNameForValue: function(value, options)
{
this._initialize();
options = options || [];
for (var i = 0, n = options.length; i < n; ++i) {
if (this._enumNameToValue[options[i]] === value)
return options[i];
}
var names = this._enumValueToNames[value];
if (!names || names.length !== 1)
return null;
return names[0];
},
/**
* @param {!ReplayableCall} replayableCall
* @return {Object}
*/
_findEnumsInfo: function(replayableCall)
{
function findMaxArgumentIndex(enumsInfo)
{
var result = -1;
var enumArgsIndexes = enumsInfo["enum"] || [];
for (var i = 0, n = enumArgsIndexes.length; i < n; ++i)
result = Math.max(result, enumArgsIndexes[i]);
var bitfieldArgsIndexes = enumsInfo["bitfield"] || [];
for (var i = 0, n = bitfieldArgsIndexes.length; i < n; ++i)
result = Math.max(result, bitfieldArgsIndexes[i]);
return result;
}
var result = null;
for (var i = 0, enumsInfo; enumsInfo = WebGLCallFormatter.EnumsInfo[i]; ++i) {
if (enumsInfo["aname"] !== replayableCall.functionName())
continue;
var argsCount = replayableCall.args().length;
var maxArgumentIndex = findMaxArgumentIndex(enumsInfo);
if (maxArgumentIndex >= argsCount)
continue;
// To resolve ambiguity (see texImage2D, texSubImage2D) choose description with max argument indexes.
if (!result || findMaxArgumentIndex(result) < maxArgumentIndex)
result = enumsInfo;
}
return result;
},
/**
* @param {?CanvasAgent.CallArgument|undefined} callArgument
* @param {Array.<string>=} options
*/
_formatEnumValue: function(callArgument, options)
{
if (!callArgument || isNaN(callArgument.description))
return;
this._initialize();
var value = +callArgument.description;
var enumName = this.enumNameForValue(value, options);
if (enumName)
callArgument.enumName = enumName;
},
/**
* @param {?CanvasAgent.CallArgument|undefined} callArgument
* @param {Array.<string>=} options
*/
_formatEnumBitmaskValue: function(callArgument, options)
{
if (!callArgument || isNaN(callArgument.description))
return;
this._initialize();
var value = +callArgument.description;
options = options || [];
/** @type {!Array.<string>} */
var result = [];
for (var i = 0, n = options.length; i < n; ++i) {
var bitValue = this._enumNameToValue[options[i]] || 0;
if (value & bitValue) {
result.push(options[i]);
value &= ~bitValue;
}
}
while (value) {
var nextValue = value & (value - 1);
var bitValue = value ^ nextValue;
var names = this._enumValueToNames[bitValue];
if (!names || names.length !== 1) {
console.warn("Ambiguous WebGL enum names for value " + bitValue + ": " + names);
return;
}
result.push(names[0]);
value = nextValue;
}
result.sort();
callArgument.enumName = result.join(" | ");
},
_initialize: function()
{
if (this._enumNameToValue)
return;
/** @type {!Object.<string, number>} */
this._enumNameToValue = Object.create(null);
/** @type {!Object.<number, !Array.<string>>} */
this._enumValueToNames = Object.create(null);
/**
* @param {Object} obj
* @this WebGLCallFormatter
*/
function iterateWebGLEnums(obj)
{
if (!obj)
return;
for (var property in obj) {
if (TypeUtils.isEnumPropertyName(property, obj)) {
var value = /** @type {number} */ (obj[property]);
this._enumNameToValue[property] = value;
var names = this._enumValueToNames[value];
if (names) {
if (names.indexOf(property) === -1)
names.push(property);
} else
this._enumValueToNames[value] = [property];
}
}
}
/**
* @param {!Array.<string>} values
* @return {string}
*/
function commonSubstring(values)
{
var length = values.length;
for (var i = 0; i < length; ++i) {
for (var j = 0; j < length; ++j) {
if (values[j].indexOf(values[i]) === -1)
break;
}
if (j === length)
return values[i];
}
return "";
}
var gl = this._createUninstrumentedWebGLRenderingContext();
iterateWebGLEnums.call(this, gl);
var extensions = gl.getSupportedExtensions() || [];
for (var i = 0, n = extensions.length; i < n; ++i)
iterateWebGLEnums.call(this, gl.getExtension(extensions[i]));
// Sort to get rid of ambiguity.
for (var value in this._enumValueToNames) {
var names = this._enumValueToNames[value];
if (names.length > 1) {
// Choose one enum name if possible. For example:
// [BLEND_EQUATION, BLEND_EQUATION_RGB] => BLEND_EQUATION
// [COLOR_ATTACHMENT0, COLOR_ATTACHMENT0_WEBGL] => COLOR_ATTACHMENT0
var common = commonSubstring(names);
if (common)
this._enumValueToNames[value] = [common];
else
this._enumValueToNames[value] = names.sort();
}
}
},
/**
* @return {WebGLRenderingContext}
*/
_createUninstrumentedWebGLRenderingContext: function()
{
var canvas = /** @type {HTMLCanvasElement} */ (inspectedWindow.document.createElement("canvas"));
var contextIds = ["experimental-webgl", "webkit-3d", "3d"];
for (var i = 0, contextId; contextId = contextIds[i]; ++i) {
var context = canvas.getContext(contextId);
if (context)
return /** @type {WebGLRenderingContext} */ (Resource.wrappedObject(context));
}
return null;
},
__proto__: CallFormatter.prototype
}
CallFormatter.register("CanvasRenderingContext2D", new CallFormatter(CanvasRenderingContext2DResource.DrawingMethods));
CallFormatter.register("WebGLRenderingContext", new WebGLCallFormatter(WebGLRenderingContextResource.DrawingMethods));
/**
* @constructor
*/
function TraceLog()
{
/** @type {!Array.<ReplayableCall>} */
this._replayableCalls = [];
/** @type {!Cache.<ReplayableResource>} */
this._replayablesCache = new Cache();
/** @type {!Object.<number, boolean>} */
this._frameEndCallIndexes = {};
}
TraceLog.prototype = {
/**
* @return {number}
*/
size: function()
{
return this._replayableCalls.length;
},
/**
* @return {!Array.<ReplayableCall>}
*/
replayableCalls: function()
{
return this._replayableCalls;
},
/**
* @param {number} id
* @return {ReplayableResource|undefined}
*/
replayableResource: function(id)
{
return this._replayablesCache.get(id);
},
/**
* @param {!Resource} resource
*/
captureResource: function(resource)
{
resource.toReplayable(this._replayablesCache);
},
/**
* @param {!Call} call
*/
addCall: function(call)
{
this._replayableCalls.push(call.toReplayable(this._replayablesCache));
},
addFrameEndMark: function()
{
var index = this._replayableCalls.length - 1;
if (index >= 0)
this._frameEndCallIndexes[index] = true;
},
/**
* @param {number} index
* @return {boolean}
*/
isFrameEndCallAt: function(index)
{
return !!this._frameEndCallIndexes[index];
}
}
/**
* @constructor
* @param {!TraceLog} traceLog
*/
function TraceLogPlayer(traceLog)
{
/** @type {!TraceLog} */
this._traceLog = traceLog;
/** @type {number} */
this._nextReplayStep = 0;
/** @type {!Cache.<Resource>} */
this._replayWorldCache = new Cache();
}
TraceLogPlayer.prototype = {
/**
* @return {!TraceLog}
*/
traceLog: function()
{
return this._traceLog;
},
/**
* @param {number} id
* @return {Resource|undefined}
*/
replayWorldResource: function(id)
{
return this._replayWorldCache.get(id);
},
/**
* @return {number}
*/
nextReplayStep: function()
{
return this._nextReplayStep;
},
reset: function()
{
this._nextReplayStep = 0;
this._replayWorldCache.reset();
},
/**
* @return {Call}
*/
step: function()
{
return this.stepTo(this._nextReplayStep);
},
/**
* @param {number} stepNum
* @return {Call}
*/
stepTo: function(stepNum)
{
stepNum = Math.min(stepNum, this._traceLog.size() - 1);
console.assert(stepNum >= 0);
if (this._nextReplayStep > stepNum)
this.reset();
// FIXME: Replay all the cached resources first to warm-up.
var lastCall = null;
var replayableCalls = this._traceLog.replayableCalls();
while (this._nextReplayStep <= stepNum)
lastCall = replayableCalls[this._nextReplayStep++].replay(this._replayWorldCache);
return lastCall;
},
/**
* @return {Call}
*/
replay: function()
{
return this.stepTo(this._traceLog.size() - 1);
}
}
/**
* @constructor
*/
function ResourceTrackingManager()
{
this._capturing = false;
this._stopCapturingOnFrameEnd = false;
this._lastTraceLog = null;
}
ResourceTrackingManager.prototype = {
/**
* @return {boolean}
*/
capturing: function()
{
return this._capturing;
},
/**
* @return {TraceLog}
*/
lastTraceLog: function()
{
return this._lastTraceLog;
},
/**
* @param {!Resource} resource
*/
registerResource: function(resource)
{
resource.setManager(this);
},
startCapturing: function()
{
if (!this._capturing)
this._lastTraceLog = new TraceLog();
this._capturing = true;
this._stopCapturingOnFrameEnd = false;
},
/**
* @param {TraceLog=} traceLog
*/
stopCapturing: function(traceLog)
{
if (traceLog && this._lastTraceLog !== traceLog)
return;
this._capturing = false;
this._stopCapturingOnFrameEnd = false;
if (this._lastTraceLog)
this._lastTraceLog.addFrameEndMark();
},
/**
* @param {!TraceLog} traceLog
*/
dropTraceLog: function(traceLog)
{
this.stopCapturing(traceLog);
if (this._lastTraceLog === traceLog)
this._lastTraceLog = null;
},
captureFrame: function()
{
this._lastTraceLog = new TraceLog();
this._capturing = true;
this._stopCapturingOnFrameEnd = true;
},
/**
* @param {!Resource} resource
* @param {Array|Arguments} args
*/
captureArguments: function(resource, args)
{
if (!this._capturing)
return;
this._lastTraceLog.captureResource(resource);
for (var i = 0, n = args.length; i < n; ++i) {
var res = Resource.forObject(args[i]);
if (res)
this._lastTraceLog.captureResource(res);
}
},
/**
* @param {!Call} call
*/
captureCall: function(call)
{
if (!this._capturing)
return;
this._lastTraceLog.addCall(call);
},
markFrameEnd: function()
{
if (!this._lastTraceLog)
return;
this._lastTraceLog.addFrameEndMark();
if (this._stopCapturingOnFrameEnd && this._lastTraceLog.size())
this.stopCapturing(this._lastTraceLog);
}
}
/**
* @constructor
*/
var InjectedCanvasModule = function()
{
/** @type {!ResourceTrackingManager} */
this._manager = new ResourceTrackingManager();
/** @type {number} */
this._lastTraceLogId = 0;
/** @type {!Object.<string, TraceLog>} */
this._traceLogs = {};
/** @type {!Object.<string, TraceLogPlayer>} */
this._traceLogPlayers = {};
}
InjectedCanvasModule.prototype = {
/**
* @param {!WebGLRenderingContext} glContext
* @return {Object}
*/
wrapWebGLContext: function(glContext)
{
var resource = Resource.forObject(glContext) || new WebGLRenderingContextResource(glContext);
this._manager.registerResource(resource);
return resource.proxyObject();
},
/**
* @param {!CanvasRenderingContext2D} context
* @return {Object}
*/
wrapCanvas2DContext: function(context)
{
var resource = Resource.forObject(context) || new CanvasRenderingContext2DResource(context);
this._manager.registerResource(resource);
return resource.proxyObject();
},
/**
* @return {CanvasAgent.TraceLogId}
*/
captureFrame: function()
{
return this._callStartCapturingFunction(this._manager.captureFrame);
},
/**
* @return {CanvasAgent.TraceLogId}
*/
startCapturing: function()
{
return this._callStartCapturingFunction(this._manager.startCapturing);
},
markFrameEnd: function()
{
this._manager.markFrameEnd();
},
/**
* @param {function(this:ResourceTrackingManager)} func
* @return {CanvasAgent.TraceLogId}
*/
_callStartCapturingFunction: function(func)
{
var oldTraceLog = this._manager.lastTraceLog();
func.call(this._manager);
var traceLog = this._manager.lastTraceLog();
if (traceLog === oldTraceLog) {
for (var id in this._traceLogs) {
if (this._traceLogs[id] === traceLog)
return id;
}
}
var id = this._makeTraceLogId();
this._traceLogs[id] = traceLog;
return id;
},
/**
* @param {CanvasAgent.TraceLogId} id
*/
stopCapturing: function(id)
{
var traceLog = this._traceLogs[id];
if (traceLog)
this._manager.stopCapturing(traceLog);
},
/**
* @param {CanvasAgent.TraceLogId} id
*/
dropTraceLog: function(id)
{
var traceLog = this._traceLogs[id];
if (traceLog)
this._manager.dropTraceLog(traceLog);
delete this._traceLogs[id];
delete this._traceLogPlayers[id];
injectedScript.releaseObjectGroup(id);
},
/**
* @param {CanvasAgent.TraceLogId} id
* @param {number=} startOffset
* @param {number=} maxLength
* @return {!CanvasAgent.TraceLog|string}
*/
traceLog: function(id, startOffset, maxLength)
{
var traceLog = this._traceLogs[id];
if (!traceLog)
return "Error: Trace log with the given ID not found.";
// Ensure last call ends a frame.
traceLog.addFrameEndMark();
var replayableCalls = traceLog.replayableCalls();
if (typeof startOffset !== "number")
startOffset = 0;
if (typeof maxLength !== "number")
maxLength = replayableCalls.length;
var fromIndex = Math.max(0, startOffset);
var toIndex = Math.min(replayableCalls.length - 1, fromIndex + maxLength - 1);
var alive = this._manager.capturing() && this._manager.lastTraceLog() === traceLog;
var result = {
id: id,
/** @type {Array.<CanvasAgent.Call>} */
calls: [],
/** @type {Array.<CanvasAgent.CallArgument>} */
contexts: [],
alive: alive,
startOffset: fromIndex,
totalAvailableCalls: replayableCalls.length
};
/** @type {!Object.<string, boolean>} */
var contextIds = {};
for (var i = fromIndex; i <= toIndex; ++i) {
var call = replayableCalls[i];
var resource = call.replayableResource();
var contextResource = resource.contextResource();
var stackTrace = call.stackTrace();
var callFrame = stackTrace ? stackTrace.callFrame(0) || {} : {};
var item = CallFormatter.forResource(resource).formatCall(call);
item.contextId = CallFormatter.makeStringResourceId(contextResource.id());
item.sourceURL = callFrame.sourceURL;
item.lineNumber = callFrame.lineNumber;
item.columnNumber = callFrame.columnNumber;
item.isFrameEndCall = traceLog.isFrameEndCallAt(i);
result.calls.push(item);
if (!contextIds[item.contextId]) {
contextIds[item.contextId] = true;
result.contexts.push(CallFormatter.forResource(resource).formatValue(contextResource));
}
}
return result;
},
/**
* @param {CanvasAgent.TraceLogId} traceLogId
* @param {number} stepNo
* @return {{resourceState: !CanvasAgent.ResourceState, replayTime: number}|string}
*/
replayTraceLog: function(traceLogId, stepNo)
{
var traceLog = this._traceLogs[traceLogId];
if (!traceLog)
return "Error: Trace log with the given ID not found.";
this._traceLogPlayers[traceLogId] = this._traceLogPlayers[traceLogId] || new TraceLogPlayer(traceLog);
injectedScript.releaseObjectGroup(traceLogId);
var beforeTime = TypeUtils.now();
var lastCall = this._traceLogPlayers[traceLogId].stepTo(stepNo);
var replayTime = Math.max(0, TypeUtils.now() - beforeTime);
var resource = lastCall.resource();
var dataURL = resource.toDataURL();
if (!dataURL) {
resource = resource.contextResource();
dataURL = resource.toDataURL();
}
return {
resourceState: this._makeResourceState(resource.id(), traceLogId, resource, dataURL),
replayTime: replayTime
};
},
/**
* @param {CanvasAgent.TraceLogId} traceLogId
* @param {CanvasAgent.ResourceId} stringResourceId
* @return {!CanvasAgent.ResourceState|string}
*/
resourceState: function(traceLogId, stringResourceId)
{
var traceLog = this._traceLogs[traceLogId];
if (!traceLog)
return "Error: Trace log with the given ID not found.";
var parsedStringId1 = this._parseStringId(traceLogId);
var parsedStringId2 = this._parseStringId(stringResourceId);
if (parsedStringId1.injectedScriptId !== parsedStringId2.injectedScriptId)
return "Error: Both IDs must point to the same injected script.";
var resourceId = parsedStringId2.resourceId;
if (!resourceId)
return "Error: Wrong resource ID: " + stringResourceId;
var traceLogPlayer = this._traceLogPlayers[traceLogId];
var resource = traceLogPlayer && traceLogPlayer.replayWorldResource(resourceId);
return this._makeResourceState(resourceId, traceLogId, resource);
},
/**
* @param {CanvasAgent.TraceLogId} traceLogId
* @param {number} callIndex
* @param {number} argumentIndex
* @param {string} objectGroup
* @return {{result:(!RuntimeAgent.RemoteObject|undefined), resourceState:(!CanvasAgent.ResourceState|undefined)}|string}
*/
evaluateTraceLogCallArgument: function(traceLogId, callIndex, argumentIndex, objectGroup)
{
var traceLog = this._traceLogs[traceLogId];
if (!traceLog)
return "Error: Trace log with the given ID not found.";
var replayableCall = traceLog.replayableCalls()[callIndex];
if (!replayableCall)
return "Error: No call found at index " + callIndex;
var value;
if (replayableCall.isPropertySetter())
value = replayableCall.propertyValue();
else if (argumentIndex === -1)
value = replayableCall.result();
else {
var args = replayableCall.args();
if (argumentIndex < 0 || argumentIndex >= args.length)
return "Error: No argument found at index " + argumentIndex + " for call at index " + callIndex;
value = args[argumentIndex];
}
if (value instanceof ReplayableResource) {
var traceLogPlayer = this._traceLogPlayers[traceLogId];
var resource = traceLogPlayer && traceLogPlayer.replayWorldResource(value.id());
var resourceState = this._makeResourceState(value.id(), traceLogId, resource);
return { resourceState: resourceState };
}
var remoteObject = injectedScript.wrapObject(value, objectGroup, true, false);
return { result: remoteObject };
},
/**
* @return {CanvasAgent.TraceLogId}
*/
_makeTraceLogId: function()
{
return "{\"injectedScriptId\":" + injectedScriptId + ",\"traceLogId\":" + (++this._lastTraceLogId) + "}";
},
/**
* @param {number} resourceId
* @param {CanvasAgent.TraceLogId} traceLogId
* @param {Resource|undefined} resource
* @param {string=} overrideImageURL
* @return {!CanvasAgent.ResourceState}
*/
_makeResourceState: function(resourceId, traceLogId, resource, overrideImageURL)
{
var result = {
id: CallFormatter.makeStringResourceId(resourceId),
traceLogId: traceLogId
};
if (resource) {
result.imageURL = overrideImageURL || resource.toDataURL();
result.descriptors = CallFormatter.forResource(resource).formatResourceStateDescriptors(resource.currentState(), traceLogId);
}
return result;
},
/**
* @param {string} stringId
* @return {{injectedScriptId: number, traceLogId: ?number, resourceId: ?number}}
*/
_parseStringId: function(stringId)
{
return InjectedScriptHost.evaluate("(" + stringId + ")");
}
}
var injectedCanvasModule = new InjectedCanvasModule();
return injectedCanvasModule;
})